/* ═══ §7 · SHARED · MODALS ════════════════════════════════════════════════════════════
   Three chassis, and every modal in the app is one of them:
     .modal            a bottom sheet with a grabber   — ticker · scanner · replay
     .composite-modal  a centred card                  — statik · why · session plan · brain · regime
     .council-modal    the round table                 — the Fusion
   §7.1 is the sheet chrome; §7.2–§7.9 are the individual modals in that order. */

/* ── §7.1 the bottom-sheet chassis ── */
.modal {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 10001;
  gap: var(--sp-md);
  /* side padding only — the content owns its own top/bottom so the sheet can sit flush */
  padding-left: var(--sp-md);
  padding-right: var(--sp-md);
  padding-top: calc(env(safe-area-inset-top, 0px) + 12px);
  padding-bottom: 0;
}
/* ★ TICKER OVER REPLAY (operator 2026-08-20): ticker · scanner · replay all inherit the same
   base z-index above, so two of them open at once stack by DOM ORDER, not open order — and
   #replay-modal sits AFTER #ticker-modal in system.html, so replay always won the tie. Scanner
   never exposed this because showTickerModalFromScanner closes the scanner modal before opening
   the ticker (see modal-scanner.js) — only one .modal is ever visible at a time on that path.
   Replay's ticker row (modal-replay.js _paintReplayFrame → openTickerModalByTicker) does NOT
   close replay first, by design — the operator wants the ticker to open OVER an untouched
   replay session so closing the ticker lands back exactly where playback was, with no reload.
   Dropping replay one step below the shared base (rather than raising ticker) is the minimal
   fix: it changes nothing about ticker-modal's own stacking against anything else (statik-sheet,
   chart-fullscreen), only its order relative to replay specifically. */
#replay-modal { z-index: 10000; }
/* ★★★ AND HERE IS THE MECHANISM THE COMMENT ABOVE ONLY HALF-NAMED — MEASURED 2026-08-22, after a
   z-index written in this file did nothing at all. The note above says these sheets "stack by DOM
   ORDER, not open order"; that is true, and the REASON is that `#app` computes
   `position: relative; z-index: 1`. #app is a STACKING CONTEXT, so ticker · scanner · replay are
   sealed at z-index 1 against the document: their z-indices only ever compare with each OTHER.
   A sheet inside #app can never out-paint one of the seven host-pattern modals (analytics · why ·
   brain · regime · engram · session-plan · council), whose hosts are appended to <body> — no
   matter what number it carries. `#grades-modal { z-index: 10002 }` was written here first, and
   the sheet opened INVISIBLY under the Analytics sheet while getComputedStyle reported exactly
   10002. The number was honoured; it was honoured inside the wrong context.
   ⚠ SO WHERE A SHEET'S HOST IS ATTACHED IS THE REAL CONTROL, and it is chosen per sheet in JS
   (`_overlayHost(id, parent)`), not here. The two rules below only order sheets WITHIN their own
   context, which is all a z-index can ever do:

   ★ THE SIGNALS SHEET takes replay's rule for replay's reason — its host is attached inside #app,
   so this drop below the shared base lets `#ticker-modal` (10001) open OVER it. A signal row does
   NOT close this sheet first, so closing the ticker lands back on the same day of the same list. */
#signals-modal { z-index: 10000; }
/* ★ THE NEWS SHEET takes the same rule for the same reason — its host is attached inside #app and
   every one of its rows opens a ticker, which must paint over it. */
#news-modal { z-index: 10000; }
/* ★ THE GRADES SHEET is attached to <body>, beside the Analytics host it opens from, which is the
   only place its z-index can beat that host's 10001 at all. */
#grades-modal { z-index: 10002; }
/* ★★ THE TICKER SHEET IS THE DETAIL EVERY OTHER SURFACE LINKS INTO — a race row, a scan row, a
   replayed minute, a signal, a grade — so it sits above all of them. modal-ticker.js MOVES this
   node to <body> for exactly that reason: inside #app the number below is unreachable by anything
   at body level, and the sheet opened behind the Analytics and Grades sheets (operator 2026-08-22:
   "it opens behind the current two modals so i have to close out of both to see the ticker").
   ⚠ THE ORDER BELOW IS THE WHOLE LADDER — read it as one thing, not six separate rules:
        10000  replay · signals    (inside #app — a ticker may open over them)
        10001  scanner (in #app) · the six overlay-host modals (at <body>)
        10002  grades             (at <body>, above the analytics sheet it opens from)
        10003  ticker             (at <body>, above every surface that links INTO it)
        10004  why                (at <body>, above ticker — the one link going the OTHER way)
   A sheet's ADDRESS decides which of these numbers it can even be compared against; see
   `_overlayHost`'s own header. Adding a sheet means choosing both, deliberately. */
#ticker-modal { z-index: 10003; }
/* ★★ WHY IS THE ONE SHEET THAT OPENS *OUT* OF THE TICKER SHEET, and that edge is new (operator
   2026-08-25: "when i press the grade in chart modal, the grade modal opens behind the chart
   modal when it should be above"). This rule's predecessor said ticker was "the top of the ladder,
   ALWAYS — there is no arrangement in which something should cover it." That was true while every
   arrow pointed INTO the ticker sheet; the Statik-says grade strip added the first arrow pointing
   out of it, so the claim expired the moment the strip shipped.
   ⚠ IT NEEDED AN ID TO BE ADDRESSABLE AT ALL. modal-why.js built a bare `<div class="modal">`, so
   it took §7.1's shared 10001 and the ticker sheet's 10003 painted straight over it — the number
   was honoured, there was simply no selector that could raise it. The id is added in that card in
   the same edit; a rule here matching nothing would have looked like a fix and changed nothing. */
#why-modal { z-index: 10004; }

.modal-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.modal-content {
  position: relative;
  width: 100%;
  max-width: 400px;
  border-radius: 20px 20px 0 0;
  display: flex;
  flex-direction: column;
  /* flush to the viewport bottom (the sheet covers the nav while open); max-height keeps the
     TOP edge where it was before the sheet was allowed to extend down. */
  max-height: calc(100vh - env(safe-area-inset-top, 0px) - 20px);
  /* ★ THE BLACK-GAP BUG (operator 2026-08-20: "not a single modal scrolls off page. they all have
     black gaps... doesn't fit the came from bottom theme"). Same defect shell.css already names for
     #app/.view-container: a bare `100vh` is computed against the LARGEST possible iOS Safari viewport
     (toolbar hidden), not what is actually visible at the scroll state the sheet opens at — so the
     sheet's own max-height (and therefore its bottom edge) sits below the true visible bottom,
     leaving dead space under it instead of a flush fill. `100dvh` tracks the ACTUAL visible height;
     `modal-chart-scroll.is-fullscreen` below already proves it live in this same file. The `100vh`
     line above stays as the fallback for engines that do not understand `dvh` — this line only
     overrides it where dvh is understood. */
  max-height: calc(100dvh - env(safe-area-inset-top, 0px) - 20px);
  margin-bottom: 0;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  overflow-y: auto;
  animation: slideUp 0.32s cubic-bezier(0.32, 0.72, 0, 1);
  will-change: transform;
}
/* ── EVERY SHEET RISES FROM THE BOTTOM EDGE (operator 2026-08-12: "every modal needs to come from
      the bottom of the page (rewind/chart/chat/scanner/contact)") ──
   It was `translateY(100px)` — a 100px HOP that starts with most of the sheet already painted, which
   is not the same gesture as a sheet coming up off the edge. `100%` is the card's OWN height, so it
   starts entirely below the viewport bottom whatever the content's length; a pixel constant cannot
   do that because the sheets are three different heights.

   ★★ AND THE EASING HAD TO CHANGE WITH IT, WHICH IS NOT A STYLE PREFERENCE — IT IS THE GEOMETRY.
   `cubic-bezier(0.34, 1.56, 0.64, 1)` is a SPRING: its second control point is 1.56, so it travels
   PAST its destination and settles back. Over a 100px hop the overshoot lands harmlessly inside the
   card. Over a full-height rise the destination is `translateY(0)` — flush to the viewport bottom —
   so overshooting means NEGATIVE y: the sheet lifts OFF the bottom edge and the dimmed page shows
   through the gap underneath it, every single time it opens. The one curve that was right for a hop
   is wrong for a rise. `cubic-bezier(0.32, 0.72, 0, 1)` is monotone — it decelerates into the edge
   and stops there.
   ⚠ THE NAME IS KEPT DELIBERATELY. Renaming to something like `sheetRise` would orphan any other
   card that animates with `slideUp` without my having read it; keeping the name means every existing
   user inherits the bottom rise, which is exactly what was asked for. */
@keyframes slideUp {
  from { transform: translateY(100%); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}
.modal-grabber {
  width: 52px;
  height: 5px;
  background: rgba(255, 255, 255, 0.42);
  border-radius: 3px;
  margin: 10px auto 6px;
  cursor: grab;
  flex-shrink: 0;
  transition: background 0.15s ease, transform 0.15s ease;
}
/* ★★ THE HOVER LAW, AND IT GOVERNS EVERY `:hover` IN THIS FILE — NOT JUST THIS ONE.
   iOS Safari LATCHES `:hover` on the last-tapped element and never releases it: there is no
   pointer to leave, so the state persists until some other element is tapped. A hover rule that
   paints a colour therefore reads as a control that stayed switched on.

   ⚠ THIS GUARD WAS WRITTEN FOR THE GRABBER AND APPLIED TO NOTHING ELSE, and that is the whole of
   the operator's 2026-08-11 report: "unless pressed off the tool/fullscreen/fileupload all stay
   gold… after trigger they need to go back to the original grey colour". Every control on the chat
   sheet carried a bare `:hover { color: var(--accent-gold) }`, so one tap gilded it permanently.
   Every hover in this file is now inside `@media (hover: hover)`.

   ★ AND A STATE CLASS MUST NEVER RIDE INSIDE THE GUARD. Where a rule read
   `.x:hover, .x.is-open { … }` the two halves are SPLIT: `.is-open` / `.is-on` / `.active` are
   FACTS the organism sets (this pop is open, the web read is armed) and must paint on every
   device. Folding them into a hover-capable media query would have traded a stuck highlight for a
   state nobody can see — the repaired-checker trap, in CSS. */
@media (hover: hover) {
  .modal-grabber:hover { background: rgba(255, 255, 255, 0.65); transform: scaleX(1.1); }
}
.modal-grabber:active { cursor: grabbing; background: rgba(255, 255, 255, 0.75); }

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  padding: var(--sp-lg);
  border-bottom: 1px solid var(--glass-border);
}
.modal-title {
  font-family: var(--font-mono);
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--accent-gold);
}
.modal-subtitle {
  font-size: 0.9rem;
  color: var(--text-secondary);
  margin-top: 4px;
}
.modal-subtitle.positive { color: var(--accent-green); }
.modal-subtitle.negative { color: var(--accent-red); }

/* ⚠ `.modal-close` IS DELETED, NOT COMMENTED (R3b). The replay sheet was its LAST site (operator
   2026-08-12: "the rewind modal needs its X removed too"); ticker and scanner never had one. Proven
   dead two independent ways rather than by reading seven more JS cards:
     · system.html read WHOLE, line 1 -> 1,061: the class occurred EXACTLY ONCE, and that ✕ is gone
     · the LIVE DOM at 390px after opening all three sheets + statik · council · brain · session-plan
       · regime · engram · why  ->  `.modal-close` 0, while `.composite-modal-close` 4,
       `.council-close` 1 and `.modal-grabber` 4 in the same query. The census DISCRIMINATES, so the
       zero is a fact about this class and not about the probe.
   The centred cards keep `.composite-modal-close` and the round table keeps `.council-close`; those
   are different rules and are untouched. */

.modal-body {
  padding: var(--sp-lg);
  padding-bottom: calc(var(--sp-lg) + 24px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
  flex: 1;
  overflow-y: auto;
  /* ★ THE SCROLLBAR'S ROOM IS RESERVED WHETHER OR NOT IT IS THERE. Without this the gutter appears
     only once the content overflows, so a child that measured itself against the full width — the
     `.discover-stats` grid in the Brain modal — ends up with its last column underneath the bar.
     READ off the 390px shot. A stable gutter makes the content width one number instead of two. */
  scrollbar-gutter: stable;
  /* a flex column's items default to `stretch`; this stops one wide child from widening the box */
  min-width: 0;
}
/* and every direct child is free to SHRINK rather than force the body wider — the other half of the
   same rule, because a flex item's default `min-width: auto` is its own content's width. */
.modal-body > * { min-width: 0; }
.modal-section {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}
.modal-section-title {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}
.modal-stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-md);
}
.modal-stat {
  padding: var(--sp-md);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  border-radius: 12px;
  text-align: center;
}
.modal-stat-label {
  font-size: 0.65rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 4px;
}
.modal-stat-value {
  font-family: var(--font-mono);
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--accent-gold);
}
.modal-footer {
  padding: var(--sp-lg);
  border-top: 1px solid var(--glass-border);
  display: flex;
  gap: var(--sp-sm);
}
.modal-btn {
  flex: 1;
  padding: 10px;
  font-family: var(--font-display);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--accent-gold);
  background: rgba(212, 175, 55, 0.08);
  border: 1px solid rgba(212, 175, 55, 0.2);
  border-radius: 10px;
  cursor: pointer;
  transition: all 0.2s;
}
.modal-btn:active { transform: scale(0.97); }

/* ── §7.2 the TICKER modal — the score breakdown block it owns ── */
.score-breakdown {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  padding: var(--sp-md);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  border-radius: 12px;
}
.score-breakdown-formula {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--accent-gold);
  letter-spacing: 0.02em;
}
.score-breakdown-note {
  font-size: 0.7rem;
  color: var(--text-dim);
  line-height: 1.4;
}
.score-breakdown-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-xs);
  font-family: var(--font-mono);
  font-size: 0.75rem;
}
.score-breakdown-grid .kv {
  display: flex;
  justify-content: space-between;
  padding: 2px 6px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.04);
}
.score-breakdown-grid .kv .k { color: var(--text-dim); }
.score-breakdown-grid .kv .v { color: var(--text-primary); }
.score-breakdown-grid .kv .v.pending { color: var(--text-dim); font-style: italic; }

/* ── §7.3 the CHART, inside the ticker modal, and its FULLSCREEN ──────────────────────
   The chart is not a modal of its own: it is a section of the ticker sheet, and pressing it
   takes the whole viewport the same way the theater does. */

/* the timeframe toolbar — one scrolling row, never two wrapped ones on a phone */
.modal-chart-toolbar {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  padding-bottom: 2px;
}
.modal-chart-toolbar::-webkit-scrollbar { height: 4px; }
.modal-chart-toolbar::-webkit-scrollbar-thumb {
  background: rgba(192, 192, 192, 0.25);
  border-radius: 2px;
}
.modal-chart-toolbar .tf-btn { flex: 0 0 auto; }

.tf-btn {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--glass-border);
  color: var(--text-dim);
  font: 600 0.7rem/1 var(--font-mono);
  padding: 6px 10px;
  border-radius: 10px;
  cursor: pointer;
  transition: all 0.15s ease;
}
@media (hover: hover) {
  .tf-btn:hover { color: var(--text-primary); background: rgba(212, 175, 55, 0.12); }
}
.tf-btn.active {
  color: var(--accent-gold);
  background: rgba(212, 175, 55, 0.18);
  border-color: rgba(212, 175, 55, 0.55);
  box-shadow: 0 0 14px rgba(212, 175, 55, 0.18) inset;
}
.tf-rth {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: auto;
  color: var(--text-dim);
  font: 600 0.65rem/1 var(--font-mono);
  cursor: pointer;
  user-select: none;
}
.tf-rth input { accent-color: var(--accent-gold); }

/* the crosshair OHLCV readout — absolutely placed inside the chart wrapper, pointer-events off
   so the SVG underneath still receives movement */
.chart-crosshair-wrap { touch-action: none; }
.chart-crosshair-legend {
  position: absolute;
  pointer-events: none;
  min-width: 118px;
  padding: 6px 8px;
  background: var(--glass-bg-heavy);
  border: 1px solid rgba(212, 175, 55, 0.35);
  border-radius: 6px;
  color: var(--ink-panel);
  font: 500 0.68rem/1.25 var(--font-mono);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
  z-index: 4;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.chart-crosshair-legend.up   { border-color: rgba(46, 204, 113, 0.55); }
.chart-crosshair-legend.down { border-color: rgba(232, 93, 42, 0.55); }
.chart-crosshair-legend .xh-time {
  font-size: 0.62rem;
  color: var(--text-dim);
  margin-bottom: 3px;
  letter-spacing: 0.04em;
}
.chart-crosshair-legend .xh-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 8px;
  row-gap: 1px;
}
.chart-crosshair-legend .xh-k { color: var(--text-dim); font-weight: 700; }
.chart-crosshair-legend .xh-v { text-align: right; font-variant-numeric: tabular-nums; }

/* the session legend under the chart (pre / RTH / after-hours / overnight) — operator 2026-08-12:
   "properly add color divides for pre-intra-after-overnight". The swatch shape was already here,
   unwired to any colour or any JS; these four are the same phases app/system/js/chart.js now
   paints as background bands behind the candles, and the same hex values (read off these tokens
   into chart.js's own colour map, since an SVG fill cannot read a CSS custom property set on a
   DIFFERENT element's font shorthand the way `currentColor` can) — legend and chart cannot show
   two different colours for the same session because there is only one set of values, used twice. */
#modal-chart-legend .sess-swatch.overnight { background: var(--accent-violet); }
#modal-chart-legend .sess-swatch.pre       { background: var(--accent-cyan); }
#modal-chart-legend .sess-swatch.regular   { background: var(--text-dim); }
#modal-chart-legend .sess-swatch.post      { background: var(--accent-orange); }
#modal-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 5px;
  padding: 0 2px;
  font-size: 0.58rem;
  color: var(--text-dim);
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
}
#modal-chart-legend .sess-swatch {
  display: inline-block;
  width: 14px;
  height: 10px;
  border-radius: 2px;
  margin-right: 4px;
  vertical-align: middle;
  opacity: 0.7;
}
#modal-chart-legend > span {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  letter-spacing: 0.02em;
}


/* ★★ THE SCROLL BOX IS THE CHART'S SIZE, AND IT IS DECLARED HERE — that is what makes the
   fullscreen chart possible at all. `chart.js` MEASURES this box and draws into the measurement,
   so the strip's height is a CSS fact and the fullscreen rule below is the only thing that
   changes it. 216px is the BORDER box (`box-sizing: border-box`, reset.css §1): 214px of drawing
   between two 1px borders — the exact height the renderer used to bake into the SVG as a constant.
   ⚠ THE BORDER, THE RADIUS AND THE SURFACE LIVE HERE, NOT ON THE ELEMENT. They were inline styles
   written by the renderer, which is why the fullscreen rule below needed `!important` to repaint
   the box. An inline style loses nothing by moving here and the override disappears with it. */
#modal-chart-scroll {
  position: relative;
  height: 216px;
  overflow-x: auto;
  /* the drawing is exactly as tall as this box, so there is never a vertical axis to scroll — and
     a classic 4px horizontal scrollbar must not be allowed to invent one. */
  overflow-y: hidden;
  border: 1px solid var(--glass-border);
  border-radius: 10px;
  background: var(--bg-void);
  scrollbar-width: thin;
  /* iOS Safari needs `pan-x` to allow horizontal panning inside a fixed-height overflow box
     nested in a sheet; without it the parent's vertical pan handler captures the touch — TRUE
     for a box the browser is allowed to pan natively. It stops being true the moment a gesture
     needs to become something else mid-touch (a hold that turns into "drag the crosshair, don't
     scroll the chart"), because the Pointer Events spec fixes the touch-action decision at
     `pointerdown` and ignores any change made afterward (verified against the spec and Chromium's
     own bug tracker, 2026-08-14 — dynamically toggling this value once a hold fires does nothing;
     the browser already decided). `none` hands the WHOLE gesture to chart.js's own
     `_chartWireCursor`, which now implements panning, momentum-free drag, and the hold itself by
     hand — the only way to get "quick drag pans, held-still-then-drag inspects" without the
     browser's own gesture recognizer occasionally winning the race and firing `pointercancel`
     out from under it. */
  touch-action: none;
  overscroll-behavior-x: contain;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
#modal-chart-scroll::-webkit-scrollbar { height: 4px; }
#modal-chart-scroll::-webkit-scrollbar-thumb {
  background: rgba(192, 192, 192, 0.25);
  border-radius: 2px;
}

/* ★★ THE FRAME AND ITS TOGGLE — and their ABSENCE from this file was the entire defect (operator
   2026-08-11: "fullscreen charts are still not available in any chart modal in the ui").
   `chart.js` has emitted `.modal-chart-frame` and `.modal-chart-full` all along, with the handler,
   the icon, the Escape release and state that survives a timeframe re-render. modals.css styled
   NEITHER class — 0 lines each. An unstyled <button> holding an <svg> that carries only a `viewBox`
   has no intrinsic size, so the control drew as a ~2px sliver in flow BELOW the chart: in the DOM,
   unreachable by a finger. Nothing was lost; it was never dressed.
   44x44 is the finger floor #411 settled for the theater's twin. */
.modal-chart-frame { position: relative; }
/* ★ CLEAN CORNER BY DEFAULT, DODGES THE PRICE AXIS ONLY WHEN SCROLLED TO WHERE IT'S ACTUALLY
   UNDER IT (operator 2026-08-14). `right: 6px` is the resting position; chart.js's
   `_chartUpdateFullBtnPos` adds `.avoid-price` — sheet mode only — the moment the current scroll
   position would put the price axis label underneath it, and removes it the moment that's no
   longer true. The background/border/blur it used to carry are gone too (operator 2026-08-14:
   "remove the background... it stops covering price") — just the icon, with a drop-shadow
   standing in for the backing plate's legibility over whatever chart content sits behind it now
   instead of a fixed dark card. */
.modal-chart-full {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 10px;
  color: var(--text-dim);
  cursor: pointer;
  z-index: 3;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.85));
  transition: right 0.15s ease;
}
.modal-chart-full.avoid-price { right: 54px; }
.modal-chart-full svg { width: 20px; height: 20px; }
@media (hover: hover) {
  .modal-chart-full:hover { color: var(--accent-gold); border-color: rgba(212, 175, 55, 0.5); }
}
/* ⚠ AND IN FULLSCREEN THE BUTTON MUST LEAVE THE FRAME BEHIND, WHICH IS LOAD-BEARING ON A PHONE.
   `body.chart-fullscreen .modal-content` is `visibility: hidden` and only the scroll box is
   re-shown — the frame is inside that sheet, so the toggle would vanish with it. Escape is the
   only other exit and a phone has no Escape key, so entering fullscreen would strand the operator
   in a chart with no way out. Fixed to the viewport, above the scroll box's 12000. */
body.chart-fullscreen .modal-chart-full {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  right: 12px;
  z-index: 12001;
  visibility: visible;
  /* the sheet's button just gave up its background/border/blur to stop covering the price axis
     (a sheet-only overlap — fullscreen's own gutter is 68px, well clear of the button here); this
     restores fullscreen's original solid look explicitly rather than silently inheriting the
     change, since only the small chart's button was asked to lose its backing. */
  background: rgba(10, 10, 14, 0.72);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  filter: none;
}

/* ── THE FULLSCREEN TIMEFRAME DROPDOWN (operator 2026-08-13: "add the time frames into the
   fullscreen chart as a drop down from top left corner to be able to adjust"). A native <select>,
   not a custom popover — it opens the device's own picker sheet, which is exactly what "dropdown"
   means on a phone, and needs none of a custom menu's click-outside/keyboard-nav/z-index-stacking
   code to get that behaviour. Hidden here by default: the windowed chart already has the toolbar
   row for this, and drawing the same control twice on a phone screen that has it once already
   would be clutter, not a feature. */
.modal-chart-tf-dd { display: none; }
/* THE SAME ESCAPE THE EXIT BUTTON ABOVE ALREADY MAKES, MIRRORED TO THE OTHER CORNER. `body.
   chart-fullscreen .modal-content` is `visibility: hidden` — normal chart-frame children go dark
   with it — so this needs the identical `position: fixed` + explicit `visibility: visible` escape,
   at `left` where the exit control sits at `right`, and the same 12001 so it stacks above the
   fullscreen scroll box's own 12000 rather than being drawn under the chart. */
body.chart-fullscreen .modal-chart-tf-dd {
  display: block;
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  left: 12px;
  z-index: 12001;
  visibility: visible;
  min-height: 44px;               /* the same finger floor .modal-chart-full's own comment cites */
  padding: 0 30px 0 12px;
  background: rgba(10, 10, 14, 0.72);
  border: 1px solid var(--glass-border);
  border-radius: 10px;
  color: var(--text-primary);
  font: 600 0.78rem/1 var(--font-mono);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  /* a plain <select> carries no chevron on iOS Safari without one drawn — this is that chevron,
     an inline SVG data-URI rather than a font icon so no extra request or icon-font dependency
     rides along with a single dropdown. */
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23c0c0c0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 16px;
}
@media (hover: hover) {
  body.chart-fullscreen .modal-chart-tf-dd:hover { border-color: rgba(212, 175, 55, 0.5); }
}

/* FULLSCREEN — the same two paths the theater uses, so both surfaces behave identically:
   `.is-fullscreen` on the scroll box, and `body.chart-fullscreen` to take the app chrome out
   of the shot. Two root causes had to be released for it to work at all:
     (1) `.modal-content` carries `will-change: transform`, which per spec makes IT the
         containing block for any position:fixed descendant — so `inset: 0` anchored to the
         400px sheet, not the viewport;
     (2) `glass-heavy`'s backdrop-filter traps fixed descendants the same way.
   Both are released below, along with filter/perspective/contain for safety. */
#modal-chart-scroll.is-fullscreen {
  position: fixed;
  inset: 0;
  height: 100dvh !important;
  width: 100vw !important;
  z-index: 12000;
  /* the strip's rounded corners belong to a card inside a sheet, not to the whole frame */
  border-radius: 0;
  padding: env(safe-area-inset-top, 0px) 0 env(safe-area-inset-bottom, 0px) 0;
}
body.chart-fullscreen .modal-overlay { display: none; }
body.chart-fullscreen .modal-content {
  transform: none !important;
  will-change: auto;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  filter: none !important;
  perspective: none !important;
  contain: none !important;
  overflow: visible;
  /* hide the sheet's in-flow children so they do not paint over the fullscreen chart; the rule
     below re-shows only the scroll box. */
  visibility: hidden;
}
body.chart-fullscreen #modal-chart-scroll.is-fullscreen { visibility: visible; }
/* the app chrome steps out of the shot, same as the theater */
body.chart-fullscreen .status-bar,
body.chart-fullscreen .holo-nav { display: none; }

/* ── §7.4 the SCANNER modal ── */
.scanner-modal-top20 {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.scanner-modal-row {
  display: grid;
  grid-template-columns: 34px 1fr auto auto;
  gap: 8px;
  align-items: center;
  padding: 8px 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.1s ease;
}
.scanner-modal-row:active { background: rgba(212, 175, 55, 0.12); }
.scanner-modal-rank {
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 0.68rem;
}
.scanner-modal-tick {
  font-weight: 600;
  letter-spacing: 0.03em;
  font-size: 0.86rem;
}
.scanner-modal-price {
  font-family: var(--font-mono);
  font-size: 0.76rem;
  color: var(--text-dim);
}
.scanner-modal-pct {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 600;
  min-width: 56px;
  text-align: right;
}

/* ── §7.5 the REPLAY modal — the board re-read from the engram, seeked and fast-forwarded.
   Window pills across the top, the session date under them, transport at the foot. ── */
.replay-modal-controls {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-bottom: 4px;
}
.replay-window-pills {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
}
/* prior-day row is slightly dimmer at rest to signal it is a different trading day */
.replay-window-pills-prior { opacity: 0.82; }
.replay-window-pills-prior .replay-pill.active,
.replay-window-pills-prior .replay-pill.has-frames { opacity: 1; }
.replay-pill {
  padding: 7px 6px;
  border: 1px solid rgba(212, 175, 55, 0.28);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-secondary);
  border-radius: 8px;
  font-family: 'Orbitron', monospace;
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
@media (hover: hover) {
  .replay-pill:hover { border-color: rgba(212, 175, 55, 0.55); color: var(--text-primary); }
}
.replay-pill.active {
  background: rgba(212, 175, 55, 0.22);
  border-color: rgba(212, 175, 55, 0.7);
  color: #fff;
}
/* ★ EVERY PILL SAYS WHAT IT HOLDS (operator 2026-08-11: "rewind is not working like it was
   yesterday"). It was working; it opened on a window with 0 frames while 113 sat one pill away, and
   nothing on screen could tell him that. The count is the whole repair: an empty window now LOOKS
   empty before it is tapped, and a full one is obvious.
   Three visual states, and they are three different FACTS, never two:
     has-frames   a measured count > 0    gold
     no-frames    a measured ZERO         dimmed — the window really is empty
     .unread `?`  the read FAILED         red — the instrument could not say, which is not a zero */
.replay-pill { line-height: 1.25; }
.replay-pill-n {
  display: block;
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--text-dim);
}
.replay-pill.has-frames .replay-pill-n { color: var(--accent-gold); }
.replay-pill.no-frames { opacity: 0.55; }
.replay-pill-n.unread { color: var(--accent-red); }
/* ── THE DAY PICKER, SHARED BY EVERY SHEET THAT HAS ONE ────────────────────────────────
   Renamed from `.replay-date-*` when the Signals and Grades sheets earned the same control
   (operator 2026-08-22: "a date drop down like the Race Replay calendar drop down"). It is a
   RENAME, not an alias: keeping `replay-*` and adding `sheet-*` beside it would be two names for
   one rule, and the second one is always the one that drifts. All three sheets' markup moved in
   the same edit. `<input type="date">` is what opens the phone's own calendar — that native
   picker IS the dropdown in the operator's screenshot, so there is nothing to build. */
.sheet-date-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.sheet-date-label {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.sheet-date-input {
  flex: 1;
  background: rgba(10, 10, 15, 0.8);
  border: 1px solid rgba(212, 175, 55, 0.3);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  padding: 6px 10px;
  border-radius: 8px;
  /* 44px in flow — the same tap floor tap-targets.css holds every promised control to. A native
     date input reports whatever height its own shadow tree wants, which at 0.82rem is ~30px. */
  min-height: 44px;
}

/* ── THE DAY FIELD'S RANGE LABEL (2026-08-23) ─────────────────────────────────────────────────
   A sheet showing a MERGED window ("last 3 days") has no single date to put in its picker, so the
   field rendered blank — which reads as "no filter" while three specific days are being merged
   (operator: "it should say the date-date for 3 day not blank in date bar").

   ⚠ IT HAS TO BE AN OVERLAY, NOT A VALUE. `<input type="date">` accepts a single yyyy-mm-dd and
   nothing else; a range string assigned to `.value` is rejected and the field stays empty, so the
   fix would look like no fix at all. This lays the label OVER the field and hands every tap
   through to the native picker underneath (`pointer-events: none`), so the control loses nothing.

   `:empty` collapses it the moment a real day is picked, the same way `.sheet-note:empty` already
   hides itself rather than reserving space for text that is not there. Additive: a `.sheet-date-input`
   that is still a direct child of `.sheet-date-row` (Signals, Grades, Replay) keeps its own `flex: 1`
   and is untouched by any of this. */
.sheet-date-field {
  position: relative;
  flex: 1;
  display: flex;
}
.sheet-date-field .sheet-date-input { width: 100%; }
.sheet-date-range {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  /* the input's own padding, so the label sits exactly where a typed value would */
  padding: 6px 10px;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  color: var(--text-primary);
  pointer-events: none;
}
.sheet-date-range:empty { display: none; }

/* ⚠ AND THE INPUT'S OWN TEXT HAS TO GET OUT OF THE WAY, WHICH THE FIRST VERSION DID NOT DO.
   An EMPTY `<input type="date">` is blank on iOS but renders a live `mm/dd/yyyy` placeholder in
   desktop Chrome — so the overlay landed on top of it and the field read
   "Wed,dd/yyyy19 – Fri, Aug 21", two strings interleaved. Every computed assertion passed while
   that was on screen (the label WAS present, positioned over the input, with pointer-events:none);
   only reading the PNG showed it. Hence this rule, and hence the probe now asserts the colour too.
   `color: transparent` covers browsers without the vendor pseudo-element; the ::-webkit- rules
   hide the placeholder proper while keeping the calendar glyph that opens the picker. */
.sheet-date-field.has-range .sheet-date-input { color: transparent; }
.sheet-date-field.has-range .sheet-date-input::-webkit-datetime-edit { opacity: 0; }
.sheet-date-field.has-range .sheet-date-input::-webkit-calendar-picker-indicator { opacity: 1; }

/* THE TRANSPORT — play/pause · the seek scrubber · the clock. It is `hidden` until frames
   load, so an empty window shows controls for nothing. */
.replay-transport {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 2px 4px;
}
.replay-transport[hidden] { display: none; }
.replay-play {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(212, 175, 55, 0.4);
  background: rgba(212, 175, 55, 0.12);
  color: var(--accent-gold);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  transition: background 0.15s ease;
}
@media (hover: hover) { .replay-play:hover { background: rgba(212, 175, 55, 0.22); } }
.replay-seek {
  flex: 1 1 auto;
  min-width: 0;
  accent-color: var(--accent-gold);
  height: 44px;
}
.replay-clock {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  color: var(--text-dim);
  text-align: right;
  min-width: 0;
  overflow-wrap: anywhere;
}
/* the fast-forward multiplier — a packed chip row, so 44px is taken in flow */
.replay-speeds {
  display: flex;
  gap: 6px;
  padding: 2px 2px 6px;
}
.replay-speed {
  flex: 1 1 0;
  min-height: 44px;
  border-radius: 8px;
  border: 1px solid rgba(212, 175, 55, 0.28);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.replay-speed.active {
  background: rgba(212, 175, 55, 0.22);
  border-color: rgba(212, 175, 55, 0.7);
  color: #fff;
}

.replay-modal-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 55vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* ONE REPLAYED MINUTE'S ROW — rank · symbol · price · move. The board repaints this list once
   per frame, so the row must not reflow when a value changes width: every numeric cell is
   tabular and right-aligned at a fixed minimum. */
.replay-row {
  display: grid;
  grid-template-columns: 28px 1fr auto auto;
  gap: 8px;
  align-items: center;
  padding: 8px 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.1s ease;
}
.replay-row:active { background: rgba(212, 175, 55, 0.14); }
.replay-row .rnk {
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 0.68rem;
}
.replay-row .sym {
  font-weight: 700;
  letter-spacing: 0.03em;
  font-size: 0.86rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.replay-row .px {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-dim);
  min-width: 58px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.replay-row .pct {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 700;
  min-width: 62px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.replay-row .pct.pos { color: var(--outcome-win); }
.replay-row .pct.neg { color: var(--outcome-fade); }

/* ── §7.5b the DAY SHEETS — Signals and Grades. Two sheets, one shape: a day picker, a measured
   note, and the day's own rows. Neither owns a row renderer (`_tplSignalRow` /
   `_tplJournalEntries` draw them, the same functions the Dashboard and the Analytics sheet use),
   so what is defined here is only the frame those rows sit in. ── */
.sheet-list {
  max-height: 62vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* the measured line under the picker — how much is held, what could not be placed on a day, and
   any bound the read stopped at. Same ink as `.replay-clock`, because it is the same kind of
   statement: the instrument describing its own coverage. */
.sheet-note {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  color: var(--text-dim);
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.sheet-note:empty { display: none; }

/* the publisher's own page, reachable from a news row WITHOUT the row losing its tap. A full-width
   <a> across the headline would swallow every press meant for the ticker underneath it, so the
   link is this one glyph in the meta cell and it stops the event itself. */
.news-out {
  margin-left: 5px;
  color: var(--accent-gold);
  text-decoration: none;
  font-weight: 700;
  /* the 44px floor, grown around the glyph rather than around the row */
  position: relative;
  display: inline-block;
}
.news-out::after { content: ''; position: absolute; inset: -14px -10px; }
.news-out:active { color: #fff; }

/* the day divider both signal surfaces draw (`_tplDayHeader`) — lifted out of an inline style in
   home.js when the Signals sheet became the second surface drawing it. */
.sheet-day-head {
  padding: 7px 2px 3px;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  color: var(--text-dim);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

/* ★ WHAT REPLACED THE `load older signals` BUTTON. The whole Dashboard card is the door now, so a
   button inside it would be a second way to do one thing — and the one that LOOKS like a button
   would swallow the taps meant for the card. This is a SENTENCE, deliberately not a control: it
   states the count the card is not showing and where the rest is. */
.sheet-more-hint {
  padding: 8px 2px 2px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  color: var(--text-dim);
  letter-spacing: 0.02em;
}

/* the Journal section of the Analytics sheet, made a door to the Grades sheet. The cue is what
   tells a reader the block is pressable at all — a heading that opens something must look
   different from one that does not. */
.journal-door { cursor: pointer; border-radius: 10px; transition: background 0.12s ease; }
.journal-door:active { background: rgba(212, 175, 55, 0.07); }
/* ★ THE TICKER CELL IS A SECOND, SMALLER DOOR INSIDE THE FIRST, so it has to LOOK like one —
   otherwise the only discoverable gesture is the one that opens the wrong thing (operator
   2026-08-22: "the ticker word itself should open that tickers modal"). Gold + underline is the
   link language this board already uses for an outbound news headline; nothing new is introduced.
   The 44px floor is met by `padding` on the cell rather than a min-height, because a table row
   cannot grow one cell without growing all of them. */
.jrn-ticker {
  color: var(--accent-gold) !important;
  text-decoration: underline;
  text-decoration-color: rgba(212, 175, 55, 0.4);
  text-underline-offset: 3px;
  cursor: pointer;
  padding-top: 10px !important;
  padding-bottom: 10px !important;
}
.jrn-ticker:active { color: #fff !important; text-decoration-color: #fff; }
.telem-section-label.journal-door { display: flex; align-items: baseline; gap: 8px; }
.journal-door-cue {
  margin-left: auto;
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--accent-gold);
  text-transform: none;
}

/* ── §7.6 the CORPUS panel — the RAG surface inside the ticker/why modal. This is where the
   brain's learned corpus is read back: a passage, where it came from, and how strongly it
   matched. ── */
.corpus-subtitle {
  font-size: 0.72rem;
  color: var(--text-dim);
  line-height: 1.4;
  margin-bottom: 10px;
}
.corpus-tag {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  background: rgba(212, 175, 55, 0.08);
  border: 1px solid var(--glass-border);
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--accent-gold);
  letter-spacing: 0.04em;
}
.corpus-input-row {
  display: flex;
  gap: 6px;
  margin-bottom: 10px;
}
.corpus-input {
  flex: 1;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-primary);
  font-size: 0.82rem;
}
.corpus-input::placeholder { color: var(--text-dim); }
.corpus-results {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.corpus-card {
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  border-radius: 10px;
}
.corpus-card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 3px;
}
.corpus-card-source {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--accent-gold);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}
@media (hover: hover) { .corpus-card-source:hover { text-decoration: underline; } }
.corpus-card-score {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--outcome-win);
  font-weight: 700;
}
.corpus-card-meta {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--text-dim);
  margin-bottom: 6px;
}
.corpus-card-text {
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-primary);
  max-height: 190px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  padding-right: 4px;
}
.corpus-empty {
  font-size: 0.75rem;
  color: var(--text-dim);
  font-style: italic;
  padding: 8px 0;
  text-align: center;
}
.corpus-empty code {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  padding: 2px 5px;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 4px;
  font-style: normal;
}

/* ── §7.7 the COMPOSITE chassis — a centred card. Statik chat, Why, Session Plan, The Brain
   and Market Regime all use it. ── */
.composite-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  backdrop-filter: blur(4px);
}
.composite-modal {
  background: rgba(20, 20, 30, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 14px;
  padding: 18px 16px;
  max-width: 480px;
  width: 100%;
  max-height: 85vh;
  overflow-y: auto;
  font-family: var(--font-display);
  color: var(--ink-panel);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
}
.composite-modal-hdr {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  padding-bottom: 8px;
}
.composite-modal-title {
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: 0.04em;
}
.composite-modal-close {
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 1.4rem;
  cursor: pointer;
  line-height: 1;
  padding: 0 4px;
}
@media (hover: hover) { .composite-modal-close:hover { color: var(--ink-panel); } }

/* the factor rows — used by the P.R.E.P breakdown in Why AND by the regime mix bars */
.composite-factor-row {
  display: grid;
  grid-template-columns: 130px 1fr 50px;
  gap: 8px;
  align-items: center;
  margin-bottom: 6px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
}
.composite-factor-name {
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.64rem;
}
.composite-factor-bar {
  height: 8px;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 4px;
  overflow: hidden;
  position: relative;
}
.composite-factor-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--outcome-win), var(--accent-gold));
  border-radius: 4px;
  transition: width 0.4s ease;
}
.composite-factor-fill.is-neutral { background: rgba(180, 180, 180, 0.5); }
.composite-factor-fill.is-drag { background: linear-gradient(90deg, #ff8aa0, #ff4060); }
.composite-factor-val {
  text-align: right;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink-panel);
}

/* the Session Plan modal's markdown body */
.session-plan-body {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-primary);
  line-height: 1.55;
  padding: 4px 4px 16px;
}
.session-plan-body h1,
.session-plan-body h2,
.session-plan-body h3 {
  color: var(--accent-gold);
  font-weight: 700;
  letter-spacing: 0.04em;
  margin: 18px 0 8px;
  font-size: 13px;
  text-transform: uppercase;
  border-bottom: 1px solid rgba(230, 57, 70, 0.25);
  padding-bottom: 4px;
}
.session-plan-body h1 { font-size: 14px; }
.session-plan-body p { margin: 6px 0; color: var(--text-secondary); }
.session-plan-body strong, .session-plan-body b { color: var(--accent-gold); font-weight: 600; }
.session-plan-body em, .session-plan-body i { color: var(--text-primary); font-style: normal; }
.session-plan-body ul, .session-plan-body ol { margin: 6px 0 10px 18px; padding: 0; }
.session-plan-body li { margin: 3px 0; color: var(--text-primary); }
.session-plan-body table {
  width: 100%;
  border-collapse: collapse;
  margin: 8px 0;
  font-size: 11px;
}
.session-plan-body th {
  text-align: left;
  color: var(--accent-gold);
  border-bottom: 1px solid rgba(230, 57, 70, 0.25);
  padding: 4px 6px;
  font-weight: 700;
}
.session-plan-body td {
  padding: 3px 6px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  color: var(--text-secondary);
}
.session-plan-body code, .session-plan-body pre {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 11px;
  color: var(--accent-gold);
}
.session-plan-body pre {
  padding: 8px 10px;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-x: auto;
}
.session-plan-body hr {
  border: none;
  border-top: 1px solid rgba(230, 57, 70, 0.18);
  margin: 14px 0;
}
.session-plan-body img, .session-plan-body svg { max-width: 24px; vertical-align: middle; }

/* ── §7.8 STATIK CHAT — THE BOTTOM SHEET (operator 2026-08-11) ─────────────────────────────
   It is a `.modal` now, exactly like ticker · scanner · replay: it RISES FROM THE BOTTOM EDGE and
   its top is wherever its content ends. The old arrangement was a `.composite-modal` (a CENTRED
   card) forced to `align-items: stretch; height: 100%` — which is precisely why "it goes too high":
   a stretched centred card's top edge is the backdrop's padding, i.e. the top of the screen,
   whatever the conversation's length. §7.1 already owns the slide-up, the radius, the grabber and
   the safe-area padding, so this section declares only what is the CHAT's own.
   ⚠ The old `.composite-modal.statik-chat` and `#statik-chat-backdrop` rules are DELETED, not
   overridden — a replacement deletes what it supersedes (R3b). */
/* ★ NOTHING HERE POSITIONS THE SHEET — SFTi.Trade_Grade's arrangement, ported (operator
   2026-08-11: "use Trade_Grade manipulation if needed. theirs works").
   The card's TOP is the reserve below, a CSS constant that JS never writes, so the header stays
   visible by construction. `_statikChatViewportSync` writes ONE property — the card's `height` —
   as `(visualViewport.offsetTop + visualViewport.height) − card.top`, anchored on the card's own
   measured top. Both terms are LAYOUT-viewport coordinates; that is the point of the `offsetTop`,
   and mixing in `window.innerHeight` is what left the bar floating above the keyboard.
   ⚠ THE THREE ARRANGEMENTS THAT FAILED ON HIS PHONE ALL WROTE A POSITION:
     `top: 0` + `bottom: keyboard`   → ~204px too HIGH once iOS panned the page on a re-open
     a `getBoundingClientRect` residual → ~343px too LOW (that rect does not report a fixed
                                          element's painted position on iOS as it does in Chromium)
     `top = visualViewport.offsetTop`   → still a position, still wrong
   A position has to be CORRECT. A height only has to REACH. That is the whole difference, and it
   is why this one cannot drift. */
.modal.statik-sheet {
  z-index: 10002;                              /* above the sheets, below the chart fullscreen */
  --statik-kb: 0px;
  align-items: flex-start;                     /* the card grows DOWN from the reserve — it can
                                                  never overflow upward the way flex-end allows */
  padding-top: 12vh;                           /* THE RESERVE. The card's top edge, and the one
                                                  number that makes the header readable. */
}
/* FULLSCREEN reserves the NOTCH AND NOTHING ELSE — the header goes under, which is what "truly full
   screen" means. The sheet already sits above the nav (10002 vs 9999), so the whole frame is its. */
.modal.statik-sheet.is-full { padding-top: env(safe-area-inset-top, 0px); }
/* ★ AND THE TOOL SHEET RELEASES THE SAME RESERVE, WHICH IS WHY IT NOW FITS IN BOTH (operator
   2026-09-10: "in non-full screen its cutoff, in fullscreen its not… fix its spacing to fit in both
   in one line as it is").
   MEASURED cause, not a guess: the tools pop is one column of 4 headers + 10 rows ~= 580px and it
   grows UPWARD from the composer with `max-height: none` — deliberately, because the requirement is
   "not scrollable, users see all the tools at once". `.modal-content.statik-chat` carries
   `overflow: hidden` so the LOG scrolls rather than the card, and that clips any child taller than
   the card. In FULLSCREEN the card is the whole frame and 580px fits; in the sheet the rule above
   reserves `12vh` — about 101px on an 844px screen — the card is exactly that much shorter, and the
   top of the list (its first group header) is what gets cut.
   ⚠ THE FIX IS THE RESERVE, NOT THE POP. Shrinking the rows would trade a tap target for a layout
   (40px is already at the 44px guideline), and re-anchoring the pop to the viewport would give this
   one element a second positioning model that the keyboard sync would then have to agree with.
   Releasing the reserve for as long as the tools are open makes the card the same height in both
   modes, so there is ONE geometry to be right rather than two that must match. */
.modal.statik-sheet.tools-open { padding-top: env(safe-area-inset-top, 0px); }
.modal.statik-sheet.is-full .modal-content.statik-chat { border-radius: 0; border-top: 0; }
/* the root is content-height now, so its dim/tap-to-close layer takes the screen itself */
.modal.statik-sheet .modal-overlay { position: fixed; }

.modal-content.statik-chat {
  max-width: 620px;
  /* HEIGHT IS JS'S, and only height. `max-height` from §7.1 is released because a second opinion
     about this box's size is exactly what the keyboard has to be free to change. */
  max-height: none;
  /* THE COMPOSER SITS ON THE KEYBOARD. The home indicator is the only thing that ever needs
     clearing down here, and the keyboard covers it — so with the keyboard up this is ZERO. */
  padding: 0 14px max(0px, calc(env(safe-area-inset-bottom, 0px) - var(--statik-kb)));
  overflow: hidden;                            /* the LOG scrolls, not the card */
  /* ── IT RISES FROM THE BOTTOM LIKE THE OTHER FOUR (operator 2026-08-12) — AND THE #218 CONSTRAINT
        THAT BANNED A TRANSFORM HERE IS RESPECTED, NOT OVERRULED. ──
     The ban was on a transform AT REST: #218 measured that a compositor-promoted element tracks the
     LAYOUT viewport, so a resting transform drifts away from the keyboard. `slideUp` carries NO
     `animation-fill-mode`, so it stops existing the instant it ends — the card's resting transform
     is `none`, byte for byte what this rule declared before. `will-change: auto` stays, so nothing
     is promoted outside the 320ms window either.
     ⚠ AND THE ONE THING THAT WINDOW TOUCHES IS NAMED HONESTLY: `_statikChatViewportSync` writes the
     card's HEIGHT off its own measured top, so a sync landing mid-rise would compute against a top
     that is still moving and correct itself on the next one. The keyboard cannot be raised in a
     headless session, so THAT interaction is NOT claimed verified here — only that no transform
     survives the animation. */
  animation: slideUp 0.32s cubic-bezier(0.32, 0.72, 0, 1);
  will-change: auto;
  background: linear-gradient(180deg, rgba(22, 22, 32, 0.98), rgba(10, 10, 15, 0.98));
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-bottom: 0;
}

/* THE BAR — history · the brain · new chat. `minmax(0, 1fr)` on the side tracks is what keeps the
   chip on the true centre: a plain `1fr` is `minmax(auto, 1fr)`, and that auto minimum lets a wider
   side shove the mark off centre (the same law shell.css states for the status bar). */
.statik-bar {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  align-items: center;
  gap: 8px;
  padding: 2px 0 4px;
}
.statik-bar-btn {
  width: 44px; height: 44px;                   /* the finger floor, in flow */
  display: flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--glass-border);
  border-radius: 50%;
  color: var(--text-dim);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.statik-bar-btn svg { width: 17px; height: 17px; }
.statik-bar-btn.is-open { color: var(--accent-gold); border-color: rgba(212, 175, 55, 0.5); }
@media (hover: hover) {
  .statik-bar-btn:hover { color: var(--accent-gold); border-color: rgba(212, 175, 55, 0.5); }
}

.statik-model-wrap { position: relative; display: flex; justify-content: center; min-width: 0; }
.statik-model {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 34px;
  max-width: 100%;
  padding: 6px 12px;
  background: rgba(10, 10, 15, 0.9);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.statik-model.is-open { border-color: rgba(212, 175, 55, 0.55); }
@media (hover: hover) { .statik-model:hover { border-color: rgba(212, 175, 55, 0.55); } }
.statik-model-name b { color: var(--accent-gold); font-weight: 700; }
/* the state lamp — GOLD and breathing while the organism is answering, quiet at rest */
.statik-model-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent-gold);
  box-shadow: 0 0 6px rgba(212, 175, 55, 0.7);
  flex-shrink: 0;
  animation: statik-breathe 2.6s ease-in-out infinite;
}
@keyframes statik-breathe { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } }

/* the fullscreen toggle — centred UNDER the bar, the way P.R.E.P places it, at 44px */
.statik-full-btn {
  align-self: center;
  width: 44px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; padding: 0;
  color: var(--text-dim);
  cursor: pointer;
  flex: 0 0 auto;
  -webkit-tap-highlight-color: transparent;
}
.statik-full-btn svg { width: 13px; height: 13px; }
@media (hover: hover) { .statik-full-btn:hover { color: var(--accent-gold); } }

/* ── THE POPS — tools · modes+lobes · recent. One look, three contents. ── */
.statik-pop {
  position: absolute;
  z-index: 40;
  min-width: 208px;
  max-width: min(320px, calc(100vw - 40px));
  max-height: 46vh;
  overflow-y: auto;
  padding: 6px;
  background: linear-gradient(135deg, rgba(30, 30, 40, 0.99), rgba(16, 16, 22, 0.99));
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 14px;
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.62);
  animation: statik-pop-in 0.16s ease-out;
}
.statik-pop[hidden] { display: none; }
@keyframes statik-pop-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.statik-history-pop { top: 52px; left: 0; }
/* ★ CENTRED WITHOUT A TRANSFORM, AND THAT IS THE WHOLE FIX (operator 2026-08-11: "when pressing
   statik.ai to adjust thought level, it phases from the right to center when it should phase center
   to center"). It was `left: 50%; transform: translateX(-50%)`, and `statik-pop-in` ANIMATES
   `transform` — so for the animation's 160ms the browser used the keyframe's value and the rule's
   centring did not exist. The pop entered at `left: 50%` with no correction (its LEFT edge on the
   centre line, i.e. a half-width to the right) and snapped into place when the animation released
   the property. The settled position was always correct, which is exactly why it read as a phase
   FROM the right rather than as a mis-placed pop.
   Auto margins in a left:0/right:0 box centre the same shrink-to-fit width using NO transform, so
   the shared keyframes are free to own that property. */
.statik-model-pop {
  top: 52px;
  left: 0;
  right: 0;
  margin-inline: auto;
  width: max-content;
}

/* THE TOOL SHEET TAKES THE WHOLE WIDTH AND NEVER SCROLLS (operator 2026-08-11: "have the tools span
   the full page if needed, not scrollable please, so users see all the tools at once").
   ONE COLUMN (operator 2026-08-11: "our tools need to be one column not two... but still fit in
   mobile with no scroll").
   ★ IT FITS, AND THE MEASUREMENT IS WHY IT CAN. Read off his 390px shot: the sheet's band above the
   composer is ~657px and the two-column pop used only 446 of it, leaving ~211px empty. One column is
   4 headers + 10 rows ≈ 580px — inside 657 with room over. What RELEASES that band is dropping the
   keyboard when the sheet opens (`toggleStatikTools`); with the caret held there are only ~200px and
   no single column of ten can fit. The two are one edit, not two.
   ⚠ The shared `max-height`/`overflow-y` are RELEASED rather than raised — a cap that is merely
   generous still scrolls on the day the roster grows, and "no scroll" is the requirement. */
.statik-tools-pop {
  bottom: calc(100% + 8px);
  left: 0;
  right: 0;
  max-width: none;
  max-height: none;
  overflow: visible;
}

.statik-pop-group {
  padding: 8px 10px 4px;
  font-family: var(--font-mono);
  font-size: 0.55rem;
  letter-spacing: 0.12em;
  color: var(--text-dim);
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  margin-bottom: 3px;
}
.statik-pop-group:not(:first-child) { margin-top: 8px; }
.statik-pop-item {
  display: flex; align-items: center; gap: 9px;
  width: 100%;
  min-height: 40px;
  padding: 8px 10px;
  background: none; border: 0; border-radius: 9px;
  color: var(--text-primary);
  font-family: var(--font-display);
  font-size: 0.76rem;
  text-align: left;
  cursor: pointer;
  transition: background 0.14s ease;
}
@media (hover: hover) { .statik-pop-item:hover { background: rgba(212, 175, 55, 0.13); } }
.statik-pop-item.is-on { background: rgba(212, 175, 55, 0.18); color: #fff; }
.statik-pop-item i {
  margin-left: auto;
  font-style: normal;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  color: var(--text-dim);
}
/* OUR OWN GLYPHS, stroke-only on currentColor — so a row's icon takes the gold with its label */
.statik-pop-icon {
  width: 18px; height: 18px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.statik-pop-icon svg { width: 16px; height: 16px; display: block; }
.statik-pop-empty {
  padding: 10px;
  font-size: 0.64rem;
  line-height: 1.5;
  color: var(--text-dim);
}
/* a lobe that LIT UP on the last answer */
.statik-lobe {
  display: flex; align-items: baseline; gap: 8px;
  padding: 6px 10px;
  font-family: var(--font-mono);
  font-size: 0.63rem;
}
.statik-lobe b { color: var(--accent-gold); font-weight: 700; }
.statik-lobe i { margin-left: auto; font-style: normal; color: var(--text-dim); font-size: 0.58rem; }
.statik-lobe-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent-gold);
  box-shadow: 0 0 5px rgba(212, 175, 55, 0.8);
  flex-shrink: 0;
}
.statik-hist-row { position: relative; padding-right: 34px; }
.statik-hist-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.statik-hist-when { font-family: var(--font-mono); font-size: 0.53rem; color: var(--text-dim); }
.statik-hist-del {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 26px; height: 26px;
  background: none; border: 0; border-radius: 50%;
  color: var(--text-dim); font-size: 1rem; line-height: 1; cursor: pointer;
}
@media (hover: hover) {
  .statik-hist-del:hover { background: rgba(255, 90, 122, 0.16); color: var(--outcome-fade); }
}

.statik-chat-log {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 4px 2px 8px;
  scroll-behavior: smooth;
}
.statik-chat-log::-webkit-scrollbar { width: 6px; }
.statik-chat-log::-webkit-scrollbar-thumb {
  background: rgba(192, 192, 192, 0.28);
  border-radius: 3px;
}

/* ONE TURN — avatar beside a bubble. The operator's turn mirrors to the right; the organism's
   and an error stay left, so the column reads as a conversation at a glance. */
.statik-chat-msg {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  max-width: 100%;
}
.statik-chat-msg.op { flex-direction: row-reverse; }
.statik-chat-avatar {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  line-height: 1;
  background: rgba(10, 10, 15, 0.9);
  border: 1px solid rgba(212, 175, 55, 0.25);
  color: var(--accent-gold);
}
.statik-chat-avatar svg { width: 15px; height: 15px; display: block; }
/* ── THE ORGANISM'S OWN BRAIN — the same brain-boot.svg the header button carries ──────────────
   It is an <img> rather than an inline stroke path because that file paints its own gradient; an
   `_ico()` glyph inherits `currentColor` and could only ever be a flat outline of it. Slightly
   inset so the circle's gold hairline stays a frame around the mark rather than a crop of it. */
.statik-chat-avatar.is-brain { overflow: hidden; }
.statik-chat-avatar.is-brain img { width: 20px; height: 19px; display: block; }
/* ── THE OPERATOR'S PICTURE — it FILLS the circle, it does not sit inside it ───────────────────
   `object-fit: cover` on a square box is what makes any aspect ratio land as a round face without
   distorting it; the overflow clip is what turns the border-radius into an actual crop. Without
   both, a 4:3 photo renders letterboxed inside a circle, which reads as a broken upload. */
.statik-chat-avatar.has-pfp { overflow: hidden; padding: 0; }
.statik-chat-avatar.has-pfp img {
  width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 50%;
}
.statik-chat-msg.op .statik-chat-avatar {
  border-color: rgba(58, 255, 138, 0.4);
  color: var(--outcome-win);
  font-size: 0.66rem;
  font-weight: 700;
  font-family: var(--font-mono);
  text-transform: uppercase;
}
.statik-chat-msg.err .statik-chat-avatar {
  border-color: rgba(255, 90, 122, 0.45);
  color: var(--outcome-fade);
}
/* THE BUBBLE MUST BE ABLE TO SHRINK. Without `min-width: 0` a long unbroken token forces the
   flex item wider than the card and the operator's own message renders one letter per line. */
.statik-chat-bubble {
  min-width: 0;
  max-width: 100%;
  padding: 9px 12px;
  border-radius: 12px;
  font-size: 0.78rem;
  line-height: 1.55;
  overflow-wrap: anywhere;
  word-break: break-word;
  background: rgba(212, 175, 55, 0.06);
  border: 1px solid rgba(212, 175, 55, 0.18);
  color: var(--text-primary);
}
.statik-chat-msg.op .statik-chat-bubble {
  background: rgba(58, 255, 138, 0.09);
  border-color: rgba(58, 255, 138, 0.25);
  color: #d3f5e0;
  white-space: pre-wrap;
}
.statik-chat-msg.err .statik-chat-bubble {
  background: rgba(255, 90, 122, 0.08);
  border-color: rgba(255, 90, 122, 0.3);
  color: #ffb3c0;
  white-space: pre-wrap;
}
.statik-chat-bubble p { margin: 0 0 6px; }
.statik-chat-bubble p:last-child { margin-bottom: 0; }
.statik-chat-bubble ul, .statik-chat-bubble ol { margin: 4px 0 6px 18px; padding: 0; }
.statik-chat-bubble li { margin: 2px 0; }
.statik-chat-bubble code {
  font-family: var(--font-mono);
  font-size: 0.92em;
  padding: 1px 4px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.06);
  color: var(--accent-gold);
}
.statik-chat-bubble strong { color: var(--accent-gold); font-weight: 700; }

/* a fenced code block in a reply — header row with the language and a copy button */
.statik-code-block {
  margin: 6px 0;
  border: 1px solid rgba(212, 175, 55, 0.18);
  border-radius: 8px;
  overflow: hidden;
  background: rgba(8, 8, 12, 0.85);
}
.statik-code-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 8px;
  background: rgba(255, 255, 255, 0.05);
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.statik-code-copy {
  background: transparent;
  border: 1px solid rgba(212, 175, 55, 0.3);
  color: var(--accent-gold);
  border-radius: 5px;
  padding: 1px 8px;
  font-size: 0.58rem;
  font-family: inherit;
  cursor: pointer;
}
@media (hover: hover) { .statik-code-copy:hover { background: rgba(212, 175, 55, 0.12); } }
.statik-code-block pre {
  margin: 0;
  padding: 8px 10px;
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  line-height: 1.5;
  color: var(--text-primary);
}

.statik-chat-empty {
  margin: auto;
  padding: 24px 12px;
  text-align: center;
}
.statik-chat-empty-icon { opacity: 0.45; margin-bottom: 8px; color: var(--text-secondary); }
.statik-chat-empty-icon svg { width: 34px; height: 34px; display: block; margin: 0 auto; }
.statik-chat-empty-text {
  font-size: 0.9rem;
  color: var(--text-secondary);
  margin-bottom: 4px;
}
.statik-chat-empty-sub {
  font-size: 0.68rem;
  color: var(--text-dim);
  line-height: 1.5;
  max-width: 34ch;
  margin: 0 auto;
}

/* the in-flight strip between the log and the composer.
   ⚠ CYAN SURVIVES HERE ON PURPOSE. Cyan is the deck's STATUS colour (tokens.css declares it and
   shop wears it on its status strip); this strip and its spinner report a live STATE — a request
   is in flight — rather than inking a surface. Everything that was cyan merely because it was a
   title, a value or a border is gold/red/silver above. */
.statik-chat-status {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 2px;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--accent-cyan);
  flex: 0 0 auto;
}
.statik-chat-status[hidden] { display: none; }
.statik-chat-spinner {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid rgba(0, 229, 255, 0.25);
  border-top-color: var(--accent-cyan);
  animation: statikSpin 0.7s linear infinite;
}
@keyframes statikSpin { to { transform: rotate(360deg); } }

/* ── THE COMPOSER — ONE BORDERED BOX, AND IT IS A COLUMN ───────────────────────────────────
   The old composer was a ROW: a textarea beside a 44px send button, with no box around them. A row
   has nowhere to put the scrap strip, the attachment thumbnails or a control rail — which is the
   whole of the operator's "the chat bar itself is wrong" (2026-08-11).
   The column stacks, top to bottom: SCRAPS · THUMBS · TEXTAREA · CONTROLS. The border and the
   focus glow belong to the BOX, so the whole composer lights as one object when the caret enters
   it — the way the surface it was modelled on behaves.
   ⚠ `.statik-chat-inputbar` / `.statik-chat-wrap` / `.statik-chat-send` are DELETED (R3b). */
.statik-composer {
  position: relative;                          /* the tools pop anchors here */
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 10px;
  background: rgba(16, 16, 24, 0.85);
  border: 2px solid rgba(255, 255, 255, 0.08);
  border-radius: 14px;
  transition: border-color 0.22s ease, box-shadow 0.22s ease, background 0.22s ease;
}
.statik-composer:focus-within {
  border-color: rgba(212, 175, 55, 0.6);
  background: rgba(10, 10, 15, 0.95);
  box-shadow: 0 0 0 4px rgba(212, 175, 55, 0.12);
}

.statik-chat-textarea {
  width: 100%;
  min-height: 22px;
  max-height: 104px;
  resize: none;
  background: transparent;                     /* the BOX carries the surface now, not the field */
  border: 0;
  color: var(--text-primary);
  font-family: var(--font-mono);
  /* 16px is not a style choice: iOS zooms the page on focus for anything smaller, and that zoom is
     what moves a composer out from under the thumb. */
  font-size: 16px;
  line-height: 1.45;
  padding: 0;
  overflow-y: auto;
}
.statik-chat-textarea:focus { outline: none; }
.statik-chat-textarea::placeholder { color: var(--text-dim); }

/* the control rail — ⋮ tools · 📎 attach · «gap» · 🌐 web · ➤ send */
.statik-controls { display: flex; align-items: center; gap: 4px; width: 100%; }
.statik-ctl-gap { flex: 1 1 auto; }
.statik-ctl {
  width: 44px; height: 44px;                   /* every control clears the finger floor in flow */
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; border-radius: 10px;
  color: var(--text-dim);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.15s ease, background 0.15s ease;
}
.statik-ctl svg { width: 19px; height: 19px; }
.statik-ctl:focus { outline: none; }

/* ★★ ONE LIT LOOK, THREE MECHANISMS (operator 2026-08-11: "fix it so they light up on trigger and
   unlight up after"). Every control on this rail RESTS DIM and LIGHTS RED while it is doing
   something. What differs between them is only WHAT HOLDS THE LIGHT ON — and each holder is a fact
   the code already knows, never a touch:
     ⋮   `.is-open`   held while the tool sheet is open   — `_statikOpenPop` / `_statikClosePops`
     📎  `.is-armed`  held while the file picker is open  — `_statikArmAttach`
     🌐  `.is-on`     LATCHED: the web read stays armed until it is pressed off — `toggleStatikWeb`
   Declaring the look ONCE across the three is what stops them drifting into three near-identical
   reds. `--accent-red` is the deck's own red (shop's #e63946), the same ink the send arrow wears
   below — so the whole rail speaks one colour instead of gold-here-cyan-there.

   ⚠ THE GLYPH LIGHTS, NOT A BOX (operator 2026-08-11: "only the icon should light up, they all now
   have red boxes"). The lit state is INK AND GLOW on the mark itself — no background fill. A tinted
   pill behind a 19px icon reads as a pressed BUTTON, which is a different claim: it says "this
   control is selected" where the design says "this faculty is doing something". The 44x44 hit box
   stays exactly as it was; only the paint changed. */
.statik-ctl.is-open,
.statik-ctl.is-armed,
.statik-web.is-on { color: var(--accent-red); }
.statik-ctl.is-open svg,
.statik-ctl.is-armed svg,
.statik-web.is-on svg { filter: drop-shadow(0 0 6px rgba(230, 57, 70, 0.55)); }

/* ★ SEND IS THE ONE THAT RESTS LIT (operator: "send stays light up just gets brighter once pressed
   to send and dims back down after sent"). It carries the red at rest and `--accent-red-bright`
   while the turn is in flight. Both are DECK TOKENS, which is why this is a pair of names rather
   than a hand-mixed lighter red that would then be the only one of its kind in the app.
   ⚠ AND THE DISABLED DIM MUST NOT FIGHT THE BRIGHT. `sendStatikChat` disables the button for
   exactly the window it is `is-live`, so an unscoped `opacity: .4` would dim the one moment he
   asked to be brightest — the two rules would be arguing about the same instant. */
.statik-send { color: var(--accent-red); }
.statik-send.is-live {
  color: var(--accent-red-bright);
  filter: drop-shadow(0 0 8px rgba(255, 107, 120, 0.6));
}
.statik-send:disabled:not(.is-live) { opacity: 0.4; cursor: not-allowed; }
@media (hover: hover) {
  .statik-ctl:hover { color: var(--accent-red); }
  .statik-send:hover:not(:disabled) { color: var(--accent-red-bright); }
}
.statik-file-input { display: none; }

/* ── THE SCRAP STRIP — what the brain is LOOKING AT, above the chatbar ─────────────────────
   ★ IT SHOWS ONLY WHAT THE ORGANISM RETURNED. `_statikPaintScraps` renders the reply's own
   `scraps`; a turn that fetched nothing leaves the strip HIDDEN rather than drawing an empty
   frame. "What I asked it to read" is not "what it read" (R3a). */
.statik-scraps {
  display: flex; gap: 6px;
  overflow-x: auto;
  padding-bottom: 2px;
  scrollbar-width: none;
  order: -2;
}
.statik-scraps[hidden] { display: none; }
.statik-scraps::-webkit-scrollbar { display: none; }
.statik-scrap {
  flex: 0 0 auto;
  max-width: 190px;
  padding: 5px 9px;
  background: rgba(0, 229, 255, 0.07);
  border: 1px solid rgba(0, 229, 255, 0.22);
  border-radius: 9px;
  text-decoration: none;
  display: flex; flex-direction: column; gap: 1px;
  animation: statik-pop-in 0.2s ease-out;
}
.statik-scrap-host {
  font-family: var(--font-mono);
  font-size: 0.52rem;
  letter-spacing: 0.06em;
  color: var(--accent-cyan);
  text-transform: uppercase;
}
.statik-scrap-title {
  font-size: 0.62rem;
  color: var(--text-secondary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── ATTACHMENT THUMBNAILS — inside the box, above the field ── */
.statik-thumbs { display: flex; gap: 8px; flex-wrap: wrap; order: -1; }
.statik-thumbs[hidden] { display: none; }
.statik-thumb {
  position: relative;
  width: 52px; height: 52px;
  border-radius: 9px;
  overflow: visible;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--glass-border);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.statik-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 8px; }
.statik-thumb-ext {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  font-weight: 700;
  color: var(--text-dim);
  letter-spacing: 0.04em;
}
.statik-thumb-err {
  position: absolute; inset: auto 0 0 0;
  font-size: 0.46rem;
  text-align: center;
  color: var(--outcome-fade);
  background: rgba(0, 0, 0, 0.72);
  border-radius: 0 0 8px 8px;
}
.statik-thumb-x {
  position: absolute; top: -7px; right: -7px;
  width: 20px; height: 20px;
  border-radius: 50%;
  background: rgba(230, 57, 70, 0.95);
  border: 2px solid rgba(10, 10, 15, 0.9);
  color: #fff;
  font-size: 0.68rem; line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}

/* ── SOURCES under a reply that named them ── */
.statik-cites { margin-top: 9px; padding-top: 8px; border-top: 1px solid rgba(255, 255, 255, 0.08); }
.statik-cites-label {
  font-family: var(--font-mono);
  font-size: 0.5rem;
  letter-spacing: 0.12em;
  color: var(--text-dim);
  margin-bottom: 5px;
}
.statik-cite {
  display: inline-block;
  margin: 0 4px 4px 0;
  padding: 2px 7px;
  background: rgba(0, 229, 255, 0.1);
  border: 1px solid rgba(0, 229, 255, 0.26);
  border-radius: 6px;
  font-size: 0.58rem;
  color: var(--accent-cyan);
  text-decoration: none;
}
@media (hover: hover) { .statik-cite:hover { background: rgba(0, 229, 255, 0.2); } }
/* the basis sits ABOVE the composer now, so its breathing room goes BELOW it */
.statik-chat-foot {
  flex: 0 0 auto;
  padding-bottom: 6px;
  font-size: 0.55rem;
  line-height: 1.5;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

/* ── §7.9 the COUNCIL modal — the Fusion's round table ── */
.council-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
  backdrop-filter: blur(6px);
}
.council-modal {
  position: relative;
  /* THE BOX THE ROUND TABLE IS MEASURED AGAINST — see the `@container council` block below for the
     measurement that made this necessary. `inline-size` contains the WIDTH axis only, so the
     modal's own height still grows with its content and `max-height`/`overflow-y` below are
     untouched. */
  container-type: inline-size;
  container-name: council;
  background: linear-gradient(135deg, rgba(22, 22, 32, 0.96), rgba(10, 10, 15, 0.96));
  border: 1px solid rgba(230, 57, 70, 0.18);
  border-radius: 18px;
  padding: 18px 16px 14px;
  width: 100%;
  max-width: 720px;
  max-height: 92vh;
  overflow-y: auto;
  font-family: var(--font-display);
  color: var(--ink-panel);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.7),
              inset 0 0 80px rgba(212, 175, 55, 0.04);
}
.council-hdr {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  padding-bottom: 10px;
  margin-bottom: 14px;
}
.council-title { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.council-decision {
  display: inline-block;
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  padding: 3px 8px;
  border-radius: 6px;
  margin-bottom: 4px;
  width: fit-content;
}
.council-decision.is-emit {
  background: rgba(58, 255, 138, 0.16);
  color: var(--outcome-win);
  border: 1px solid rgba(58, 255, 138, 0.5);
}
.council-decision.is-block {
  background: rgba(255, 90, 122, 0.16);
  color: #ff8aa0;
  border: 1px solid rgba(255, 90, 122, 0.5);
}
/* ⚠ CYAN SURVIVES ON THIS ONE BADGE. The decision trio IS a status readout — emit is green, block
   is red, and STANDING is the deck's status colour. It reports a state; it does not ink a surface. */
.council-decision.is-standing {
  background: rgba(108, 248, 255, 0.16);
  color: rgba(108, 248, 255, 0.95);
  border: 1px solid rgba(108, 248, 255, 0.5);
}
.council-ticker {
  font-size: 1.4rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--ink-panel);
}
.council-sub {
  font-size: 0.66rem;
  font-family: var(--font-mono);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.council-close {
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
}
@media (hover: hover) { .council-close:hover { color: var(--ink-panel); } }
.council-empty {
  padding: 40px 12px;
  text-align: center;
  color: var(--text-dim);
  font-size: 0.85rem;
}

.council-table {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  max-width: 580px;
  margin: 4px auto 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
              rgba(230, 57, 70, 0.06) 0%,
              rgba(212, 175, 55, 0.04) 35%,
              rgba(10, 10, 15, 0.0) 70%);
  box-shadow: inset 0 0 60px rgba(212, 175, 55, 0.05);
}
.council-table-center {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.95rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  color: var(--accent-gold);
  text-shadow: 0 0 12px rgba(212, 175, 55, 0.4);
  line-height: 1.1;
}
.council-table-tag {
  display: block;
  margin-top: 4px;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--text-dim);
}
/* each seat is placed around the rim; --i is the agent index, --N the total */
.council-seat {
  position: absolute;
  --angle: calc((var(--i) / var(--N)) * 360deg - 90deg);
  --radius: 38%;
  left: calc(50% + cos(var(--angle)) * var(--radius));
  top:  calc(50% + sin(var(--angle)) * var(--radius));
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 138px;
  pointer-events: auto;
}
.council-avatar {
  font-size: 1.4rem;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 10, 15, 0.85);
  border: 1px solid rgba(212, 175, 55, 0.25);
  border-radius: 50%;
  margin-bottom: 4px;
  box-shadow: 0 0 8px rgba(212, 175, 55, 0.18);
}
.council-bubble {
  background: rgba(22, 22, 32, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 8px;
  padding: 5px 8px;
  font-size: 0.62rem;
  line-height: 1.3;
  text-align: left;
  max-width: 100%;
  /* long knight lines are CAPPED per seat so the round table's seats never grow tall enough to
     bury each other */
  max-height: 128px;
  overflow-y: auto;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
  animation: council-bubble-in 0.4s ease;
}
@keyframes council-bubble-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.council-agent {
  font-weight: 700;
  font-size: 0.58rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent-gold);
  margin-bottom: 2px;
}
.council-line { color: var(--ink-panel); font-family: var(--font-mono); }

.council-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.council-chip {
  padding: 4px 8px;
  font-size: 0.62rem;
  font-family: var(--font-mono);
  font-weight: 700;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 8px;
  color: var(--text-dim);
  cursor: pointer;
  white-space: nowrap;
}
@media (hover: hover) {
  .council-chip:hover { background: rgba(212, 175, 55, 0.08); color: var(--ink-panel); }
}
.council-chip.active {
  background: rgba(58, 255, 138, 0.14);
  color: var(--outcome-win);
  border-color: rgba(58, 255, 138, 0.5);
}

/* ★★ THE ROUND TABLE IS ABANDONED WHEN THE BOX CANNOT HOLD IT — AND THE QUESTION IS ABOUT THE BOX,
   NOT THE SCREEN. The seats are absolute at `--radius: 38%` and 138px wide, so the rim has to carry
   `N × 138px`; a 400px table offers `2π × 152 = 955px` and thirteen seats want 1,794. They overlap
   into an unreadable pile — which is exactly what this block already existed to prevent.
   ⚠ IT WAS KEYED ON THE VIEWPORT AND THE MODAL DOES NOT TRACK THE VIEWPORT. MEASURED: on a 1440px
   window the sheet is ~400px, `max-width: 640px` answered "not a phone", the round table was kept
   in a box less than half the width it needs, and all thirteen seats piled into the centre. The
   `.discover-stats` grid had the identical defect in the same shot — a viewport query sizing
   something inside a fixed-width container is asking about the wrong box, and it is worth naming as
   a class because both surfaces looked correct on the page and wrong in a modal.
   ★ A CONTAINER QUERY ASKS THE RIGHT ONE, and it covers BOTH cases with one rule rather than two
   that have to agree: a 390px phone makes the modal ~370px and this fires, and a 400px sheet on a
   desktop makes it ~400px and this fires. The `@media` version is DELETED rather than kept beside
   it (R3) — two rules deciding one layout is how the modal case came to be wrong.
   ★★ AND THE THRESHOLD IS THE GEOMETRY, NOT A PHONE WIDTH. The rim has to carry `N × 138px` and the
   rim is `2π × 0.38 × W`, so the table needs `W >= N × 138 / 2.388 = N × 57.8`. At the live roster
   of THIRTEEN seats that is **751px** — and `.council-modal` is capped at `max-width: 720px`. The
   round table cannot hold this roster at any width it is allowed to be, which is why 640px never
   fired and why raising it to 720 would not have helped either: the branch is unreachable at this
   seat count, and the pile in the shot is what "unreachable" looks like when nothing says so.
   760px is 720 (the cap) plus room, so the list is the layout for every size this modal takes. The
   round table returns by itself the day the roster is small enough for the rim to hold it —
   `N × 57.8 <= W` — and until then this is not a fallback, it is THE layout.
   `!important` still overrides the inline absolute positioning the JS sets per seat. */
@container council (max-width: 760px) {
  .council-modal { padding: 12px 10px 12px; }
  .council-table {
    position: relative;
    aspect-ratio: auto;
    max-width: 100%;
    border-radius: 0;
    background: none;
    box-shadow: none;
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 6px 0 0;
  }
  .council-table-center {
    position: static;
    transform: none;
    margin: 0 0 4px;
    font-size: 0.85rem;
    order: -1;
  }
  .council-seat,
  .council-seat.statik-head {
    position: static !important;
    transform: none !important;
    left: auto !important;
    top: auto !important;
    width: 100% !important;
    max-width: 100% !important;
  }
  .council-seat.statik-head { flex-direction: column; }
  .council-seat:not(.statik-head) {
    flex-direction: row;
    align-items: flex-start;
    gap: 8px;
    padding: 6px 8px;
    background: rgba(22, 22, 32, 0.55);
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: 10px;
  }
  .council-seat:not(.statik-head) .council-avatar { flex: 0 0 auto; margin-bottom: 0; }
  .council-bubble {
    max-width: none !important;
    flex: 1 1 auto;
    font-size: 0.72rem;
    background: transparent;
    border: none;
    box-shadow: none;
    padding: 0;
    max-height: none;
    overflow-y: visible;
  }
  .council-agent { font-size: 0.66rem; margin-bottom: 3px; }
  .council-line { font-size: 0.72rem; }
}

