/* ============================================================
   deslop.media · AI Futures — PANEL ⇄ MODEL TWINS SHELL (staged).
   The ONE persistent shell that owns the [Panel | Model] toggle and
   hosts both engines in sibling panes. NEW FILE — edits no engine.

   Layering contract (the single-sticky rule):
     · The shared .aff-mast brand row scrolls in normal flow (as on
       both engine pages today).
     · .twin-bar is the ONE viewport-sticky chrome — top:0, ~48px tall,
       z-index 70 (above Panel's .px-top z62 and Model's .fi-sticky z60).
     · Each engine's OWN sticky/fixed chrome is neutralized by SCOPED
       shell rules below — never by editing pyramid.css / forecast-page.css
       / speedo-morph.css / forecast-instrument.css.

   Both engines own disjoint class prefixes (.iv2/.mx/.px vs .fi/.fp) and
   disjoint globals (IV2/V3/SPINE vs FI/FIC/FORECAST), so they coexist on
   one document; the shell only resolves the viewport-sticky collisions.
   ============================================================ */

/* The shell body uses the franchise dark-native ground so the seam between
   the two engines (which each set their own body bg) is invisible. */
.twin-body {
  margin: 0;
  background: var(--aff-bg, #101820);
  color: var(--aff-text, #E8E6E1);
  font-family: var(--aff-font-sans, 'Inter', -apple-system, sans-serif);
  -webkit-font-smoothing: antialiased;
}

/* P4 (F5) — the --twin-bar-h LATTICE. ONE var (on :root) drives the bar height,
   the #fi-controls sticky top, and (via JS) the pin-threshold, so the 48px panel
   bar and the 88px grown MODEL summary never desync from the dial-row pin. Default
   48px; twins-shell.js raises it to 88px on :root when the model summary is live
   (view=model + gauge scrolled off) and drops it back for panel/idle. It lives on
   :root (NOT .twin-bar) so #fi-controls — not a bar descendant — can read it too. */
:root { --twin-bar-h: 48px; }

/* ── the sticky shell bar — the SINGLE outer sticky ──────────────── */
.twin-bar {
  position: sticky;
  top: 0;
  z-index: 70;                       /* above .px-top (z62) and .fi-sticky (z60) */
  height: var(--twin-bar-h, 48px);
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 0 20px;
  background: rgba(16, 24, 32, 0.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--aff-border, #2a3240);
  transition: height .18s var(--aff-ease, ease);
}
@media (prefers-reduced-motion: reduce) { .twin-bar { transition: none; } }

/* The segmented [Panel | Model] control — the persistent twin affordance. */
.twin-seg {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  background: var(--aff-surface-2, #151c27);
  border: 1px solid var(--aff-border, #2a3240);
  border-radius: 8px;
  padding: 2px;
}
.twin-seg__btn {
  appearance: none;
  border: 0;
  background: transparent;
  font-family: var(--aff-font-mono, 'JetBrains Mono', monospace);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--aff-text-2, #8A8F98);
  text-decoration: none;
  padding: 5px 18px;
  border-radius: 6px;
  cursor: pointer;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  transition: color .18s var(--aff-ease, ease),
              background .18s var(--aff-ease, ease);
}
/* TOUCH TARGET (item 8): the PANEL|MODEL toggle is the one persistent control,
   yet its visible pill is ~64×21px — below the 44px touch minimum. Extend the
   HIT AREA to ≥44px tall with an overlaid pseudo-element (it captures the tap
   without enlarging the visible pill), and set position:relative so it anchors
   the overlay. The pill's painted size is unchanged. */
.twin-seg__btn { position: relative; }
.twin-seg__btn::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: 44px;
  min-height: 44px;
}
.twin-seg__btn:hover { color: var(--aff-text, #E8E6E1); }
.twin-seg__btn[aria-pressed="true"] {
  color: var(--aff-volt, #CEFF00);
  background: var(--aff-surface, #1a2230);
}
.twin-seg__btn:focus-visible {
  outline: 2px solid var(--aff-volt, #CEFF00);
  outline-offset: 2px;
}
/* A thin loading hint while the inactive engine is being injected. */
.twin-seg__btn.is-loading::after {
  content: "";
  width: 9px; height: 9px;
  margin-left: 8px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  display: inline-block;
  animation: twin-spin .7s linear infinite;
}
@keyframes twin-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .twin-seg__btn.is-loading::after { animation: none; opacity: .6; }
}

/* ── ITEM 1 — the reparented selector's slot (adjacent to the instrument) ──
   The shell moves the single .twin-seg node into the visible pane's slot. The
   slot is a centred row so the segmented control sits above the dial in both
   panes at the SAME offset (Fix 6 then keeps it visually stationary across a
   swap). When empty it collapses (no reserved gap). */
.twin-seg-slot {
  display: flex;
  justify-content: center;
  margin: 0 0 14px;
}
.twin-seg-slot:empty { display: none; }
/* The seg picks up a hairline frame from its own .twin-seg styles; near the
   instrument it wants a touch more presence than in the top bar. */
.twin-seg-slot .twin-seg { box-shadow: 0 1px 0 rgba(0,0,0,0.25); }

/* Quiet Panel/Model wayfinding links in the masthead (parity with the hub).
   They inherit .aff-mast__link; the shell marks the active one. */
.twin-mast__view[aria-current="page"] { color: var(--aff-volt, #CEFF00); }

/* ── ITEM 2 — the razor-thin read-out bar (the .twin-bar's live payload) ──
   The .twin-bar stays the ONE 48px viewport sticky (position:sticky, top:0,
   z-index 70 — DO NOT override its position here; a `position:relative` would
   silently break the single-sticky contract and unpin the bar). When the
   selector is reparented out, the bar carries the read-out: hidden until the
   instrument leaves the viewport, then it fades in. Volt is on the arm glyph
   ONLY; the lean phrase + meta render in text ink (no name in Volt → no re-crown). */
.twin-readout {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  opacity: 0;
  transform: translateY(-2px);
  transition: opacity .22s var(--aff-ease, ease), transform .22s var(--aff-ease, ease);
  pointer-events: none;
  max-width: min(92vw, 720px);
  overflow: hidden;
  white-space: nowrap;
}
.twin-readout[hidden] { display: none; }
.twin-readout.is-on { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .twin-readout { transition: none; transform: none; }
}
.twin-ro__glyph {
  width: 40px; height: 26px; flex: 0 0 auto;
  display: block;
}
/* R17h — the MODEL variant of the collapsed 48px readout carries a micro
   FLATTENED-arc glyph (56×20), the small echo of the frozen bar's flat gauge. */
.twin-ro__glyph--flat { width: 56px; height: 20px; }
.twin-ro__glyph .twin-ro-flatband {
  fill: var(--aff-surface-2, #151c27);
  stroke: var(--aff-border, #2a3240);
  stroke-width: 0.6;
}
.twin-ro__lean {
  font-family: var(--aff-font-mono, 'JetBrains Mono', monospace);
  font-size: 12px; letter-spacing: 0.04em;
  color: var(--aff-text, #E8E6E1);       /* TEXT ink — never Volt */
  text-transform: none;
  white-space: nowrap;
}
.twin-ro__sep { color: var(--aff-text-2, #8A8F98); font-size: 12px; }
.twin-ro__meta {
  font-family: var(--aff-font-mono, 'JetBrains Mono', monospace);
  font-size: 11px; letter-spacing: 0.03em;
  color: var(--aff-text-2, #8A8F98);      /* TEXT ink — never Volt */
  white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* PANEL horizon ("next 2–3 years") — same treatment as the meta; carried in its
   own span (+ .twin-ro__sep--h) so ≤640px can shed only the horizon. */
.twin-ro__horizon {
  font-family: var(--aff-font-mono, 'JetBrains Mono', monospace);
  font-size: 11px; letter-spacing: 0.03em;
  color: var(--aff-text-2, #8A8F98);      /* TEXT ink — never Volt */
  white-space: nowrap;
}
/* Respect the hidden attribute (JS toggles it when there is no horizon / meta). */
.twin-ro__horizon[hidden], .twin-ro__sep--h[hidden] { display: none; }
/* The arm glyph is the ONLY Volt in the read-out; the model variant is dimmed. */
.twin-ro__glyph .twin-ro-arm { stroke: var(--aff-volt, #CEFF00); }
.twin-ro__glyph .twin-ro-arm--model { stroke: rgba(206,255,0,0.55); }
.twin-ro__glyph .twin-ro-scale { stroke: var(--aff-border, #2a3240); }
.twin-ro__glyph .twin-ro-pivot { fill: var(--aff-volt, #CEFF00); }
.twin-ro__glyph .twin-ro-pivot--model { fill: none; stroke: rgba(206,255,0,0.55); }

/* ══════════════════════════════════════════════════════════════════════════
   P4 / R17h — THE FROZEN LIVE SUMMARY BAR (model view): "one leader, one flat
   gauge". Lives INSIDE the one .twin-bar (still the single sticky). It grows
   the bar to --twin-bar-h (88px) and carries, updated LIVE on every dial/year
   turn (a dumb twins:readout listener):
     · the FLATTENED gauge — a wide shallow ~84° arc band (156×48 desktop,
       120×38 @≤640): 9 cold→hot band-tinted wedges (S7 → a held-empty tick)
       + a radial dashed dimmed volt needle tick at needleF. The tinted wedges
       absorb the old standalone cascade strip (one color figure, not two).
     · the MODAL-LEADER text — eyebrow ("contested · heaviest") + the heaviest
       scenario's FULL name and share, no truncation, name in text ink.
   No-crown law: never "leads/wins"; Volt lives ONLY on the dashed needle tick;
   every name/%/label renders in text ink. R17e-b also arms this bar AT LOAD on
   mobile Model when the gauge sits below the fold (the frame-one mini-hero).
   ══════════════════════════════════════════════════════════════════════════ */
.twin-summary {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  max-width: min(96vw, 760px);
  opacity: 0;
  transform: translateY(-2px);
  transition: opacity .22s var(--aff-ease, ease), transform .22s var(--aff-ease, ease);
  pointer-events: none;
  min-width: 0;                         /* let the text column shrink + ellipsize */
}
.twin-summary[hidden] { display: none; }
.twin-summary.is-on { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .twin-summary { transition: none; transform: none; }
}
/* R17h — the FLATTENED gauge (a wide shallow arc band; ~2.4x the old glyph). */
.twin-su__gauge {
  width: 156px; height: 48px; flex: 0 0 auto; display: block;
}
.twin-su-scale { fill: none; stroke: var(--aff-border, #2a3240); stroke-width: 1; opacity: .5; }
.twin-su-wedge { stroke: #101820; stroke-width: 0.4; }
.twin-su-tick  { stroke: var(--aff-text-2, #8A8F98); stroke-width: 1; opacity: .55; }
/* the needle tick — the ONLY Volt, in the projected (dimmed, dashed) register. */
.twin-su-arm   { stroke: rgba(206,255,0,0.7); stroke-width: 1.8; stroke-linecap: round; stroke-dasharray: 3 2; }
/* the text column (eyebrow + modal leader), stacked vertically. */
.twin-su__col { display: flex; flex-direction: column; gap: 5px; min-width: 0; flex: 1 1 auto; }
/* R17h — the modal-leader text: two stacked lines, no truncation. */
.twin-su__heavy {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  min-width: 0;
  font-family: var(--aff-font-mono, 'JetBrains Mono', monospace);
  white-space: nowrap;
}
.twin-su__label {
  font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--aff-text-2, #8A8F98); flex: 0 0 auto;
}
.twin-su__leader {
  display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap;
}
.twin-su__nm {
  font-family: var(--aff-font-sans, 'Inter', -apple-system, sans-serif);
  font-size: 15px; font-weight: 600; letter-spacing: 0;
  color: var(--aff-text, #E8E6E1);     /* TEXT ink — never Volt (no re-crown) */
  white-space: nowrap;                 /* FULL name, never ellipsized (R17h) */
}
.twin-su__pc {
  font-size: 12px; color: var(--aff-text-2, #8A8F98); flex: 0 0 auto;
}
.twin-su__year {
  font-family: var(--aff-font-mono, 'JetBrains Mono', monospace);
  font-size: 11px; letter-spacing: 0.03em; color: var(--aff-text-2, #8A8F98);
  flex: 0 0 auto; align-self: center;
}
/* R17h — the ↑ cue is DROPPED (when the bar arms at load it sits ABOVE the
   gauge, so the arrow pointed the wrong way); the cursor + role/aria carry
   the tap affordance. Kept in the DOM (no markup edit), hidden here. */
.twin-su__up { display: none; }
/* the whole tall Model bar is the tap-target (twins-shell.js) — cue the cursor. */
.twin-bar--tall { cursor: pointer; }
.twin-bar--tall .twin-seg { cursor: auto; }   /* the selector keeps its own cursor */
/* R17h — the standalone cascade strip is RETIRED (the flattened gauge's tinted
   wedges carry the cold→hot color read). Node kept in the DOM, hidden. */
.twin-su__cascade { display: none !important; }
.twin-su__seg { height: 100%; display: block; }

/* @375 — the flattened gauge narrows; the leader keeps its FULL name (verified
   fit: "Political Override 24%" ≈180px in the ~221px text budget @375). The
   name floor is 14px — it never drops below (here it stays 15px). */
@media (max-width: 640px) {
  .twin-summary { gap: 8px; max-width: 96vw; }
  .twin-su__gauge { width: 120px; height: 38px; }
  .twin-su__year { display: none; }            /* year lives on the page dials @375 */
}
/* the narrowest phones (≤400px) — tighten the gaps; the full name still fits. */
@media (max-width: 400px) {
  .twin-summary { gap: 6px; }
}

@media (max-width: 640px) {
  .twin-seg-slot { margin-bottom: 10px; }
  /* Razor-thin on mobile, but KEEP the coverage: show "leans cool · NN% placed"
     (the lean word, its separator, and the placed% meta). Drop ONLY the trailing
     horizon ("next 2–3 years") and its OWN leading separator — the horizon rides
     .twin-ro__horizon + .twin-ro__sep--h, so hiding just that pair leaves no
     dangling middot. (An earlier fix hid the whole .twin-ro__meta/.twin-ro__sep
     and lost the % entirely.) The placed% span keeps its ellipsis for safety, so
     even the narrowest phone clips gracefully rather than overflowing. */
  .twin-ro__horizon, .twin-ro__sep--h { display: none !important; }
  .twin-readout { gap: 7px; }
}

/* ── the two sibling panes — exactly one visible ─────────────────── */
.twin-pane[hidden] { display: none !important; }
/* When a pane is active it is plain flow; the engine inside paints normally. */
.twin-pane { display: block; }

/* ════════════════════ ENGINE STICKY NEUTRALIZATION ════════════════════
   Scoped, shell-owned overrides so .twin-bar is the only viewport sticky.
   These DO NOT touch any engine file — they live here and are scoped to the
   pane id so they can never bleed onto the canonical standalone pages. */

/* PANEL: its own .px-top masthead bar.
   Item 6 (twins masthead parity): the PANEL pane carried a ".px-top" sub-strip
   ("AI SPEEDOMETER · ISSUE 01") that the MODEL pane has no equivalent of — a
   cousins-tell (one twin wears a masthead the other doesn't). The MODEL hero has
   no .px-top host, so rendering the same strip on both would mean grafting new
   chrome into the cloned forecast markup; instead we DROP the panel strip and let
   the unified .lede__kicker eyebrow carry the piece identity on BOTH panes (the
   panel hero already leads with "The AI Speedometer"; item 7 gives the model hero
   the same .lede__kicker treatment). The shared franchise .aff-mast still carries
   the brand wordmark once, above both. Lowest-risk + most cohesive: no engine
   markup added, one redundant strip removed, both eyebrows now siblings.
   Hidden (not removed) so the matrix-header sticky math that offsets against the
   shell bar is untouched. Scoped to #twin-panel — the standalone page is intact. */
#twin-panel .px-top { display: none; }

/* MODEL: the hero pin and the compact fixed readout must not become a second
   viewport sticky. The default Model hero is the STATIC compact hero
   (fp-anim-paused), so .fp-hero__pin is already collapsed there; we belt-and-
   suspenders it static while scoped to the pane. */
#twin-model .fp-hero__pin { position: static; top: auto; min-height: auto; }

/* MODEL's .fi-sticky compact readout is created by forecast-instrument.js and
   appended to <body> as position:fixed (it escapes #twin-model). The shell bar
   is the persistent chrome, so the engine's redundant fixed readout is
   suppressed globally — it is a duplicate of the role the shell bar plays, and
   leaving it would double the top chrome whenever Model is scrolled. This is a
   shell decision (hide a redundant fixed overlay), not an engine edit. */
#fi-sticky { display: none !important; }

/* ══════════════ PLACEMENT-MATRIX STICKY-HEADER RESTORE (shell-scoped) ══════════════
   THE BUG (desktop): the placement matrix's question-header row — the angled
   question labels (.mx-hd), the "figure ⇣ question" corner (.mx-corner), and the
   left figure-name column (.mx-nm) — was authored in decomp-v3.css to pin against
   the standalone Panel page's own sticky masthead (.px-top, ~48px). On DESKTOP
   decomp-v3.css makes .matrix an INTERNAL scroll container (max-height:78vh;
   overflow:auto) and pins those headers at top:0 against the MATRIX box. In this
   shell .px-top is forced static and the SHELL bar (.twin-bar — sticky, top:0,
   48px, z-index 70) is the one viewport chrome. The matrix box itself is a normal
   block in page flow, so as the reader scrolls the page the whole matrix box —
   and its top:0-pinned headers with it — slides UP behind the 48px .twin-bar and
   the angled question labels get clipped (the operator's report).

   THE FIX (desktop only; the mobile branch in decomp-v3.css already pins these at
   top:48px against the viewport and is UNTOUCHED): inside the shell, de-clip the
   matrix on desktop so .matrix is no longer the sticky containing block, exactly
   as decomp-v3.css already does for mobile. Then the question-header row resolves
   its sticky against the VIEWPORT and pins just below the 48px shell bar — the
   labels AND the figure-name column stay readable while the figure rows scroll
   beneath. z-index sits BELOW the shell bar (70) and ABOVE the figure cells; an
   opaque franchise-dark backdrop (--aff-bg #101820) hides rows scrolling under it.
   Scoped to #twin-panel + min-width:641px → it edits no engine file and cannot
   reach the canonical standalone Panel page. */
@media (min-width: 641px) {
  /* .matrix must NOT be a scroll container in the shell, or the headers anchor to
     it (the matrix box) and ride under the shell bar with the page. Releasing the
     internal scroll lets the headers anchor to the page/viewport instead. The wide
     (min 660px) grid overflows into the page's normal horizontal scroll, which is
     unchanged. */
  #twin-panel .matrix { max-height: none; overflow: visible; overscroll-behavior: auto; -webkit-overflow-scrolling: auto; }
  #twin-panel .matrix-panel.open { overflow: visible; }
  /* page-pinned headers: sticky against the VIEWPORT, just below the 48px shell
     bar. z 60 < the bar's 70 and > the figure cells; opaque dark backdrop so the
     scrolling rows are hidden cleanly behind the header. The §-band (row 1) pins
     at 48px; the question-tick row (row 2) stacks directly beneath it at 68px. */
  #twin-panel .mx-corner,
  #twin-panel .mx-band,
  #twin-panel .mx-hd {
    position: -webkit-sticky; position: sticky;
    z-index: 60;
    background: var(--aff-bg, #101820);
  }
  #twin-panel .mx-corner,
  #twin-panel .mx-band { top: 48px; }
  #twin-panel .mx-hd { top: 68px; }
  /* the band sits one layer above the ticks; the corner above the band (it alone
     also pins LEFT and spans both header rows). */
  #twin-panel .mx-band { z-index: 61; }
  #twin-panel .mx-corner { z-index: 62; }
  /* the left figure-name column keeps its sticky-LEFT pin so names stay readable
     during horizontal pan; it stays BELOW the top header row. Opaque dark backdrop
     so cells don't show through, matching the new header surface. */
  #twin-panel .mx-nm { background: var(--aff-bg, #101820); }
}

/* ════════════════ TIGHTER TOP (shell-scoped — edits no engine file) ════════════════
   THE ASK (operator): when the viewport is centred on the instrument, BOTH the
   "Views" and "READ" selector bars should be visible. The hero (kicker + "Where
   is AI going?" + the two-line standfirst) plus the 22px gap before the
   instrument pushed the control bars below a centred viewport. Compress the
   hero's top/bottom rhythm and the gap before the instrument — clean, nothing
   cropped. Scoped to #twin-panel so the canonical standalone Panel page is
   untouched. decomp-v3.css baseline: .lede padding 40px…4px, h1 margin 18/16,
   .lede__thesis margin 6/16, .instrument margin-top 22px. */
#twin-panel .lede { padding-top: 20px; padding-bottom: 2px; }
#twin-panel .lede h1 { margin: 10px 0 10px; }
#twin-panel .lede p.lede__thesis { margin: 4px 0 9px; }
#twin-panel .lede p.lede__sub { margin-bottom: 2px; }
#twin-panel .instrument { margin-top: 12px; }
@media (max-width: 640px) {
  #twin-panel .lede { padding-top: 16px; }
  #twin-panel .instrument { margin-top: 10px; }
}

/* ════════════════ READ ROW REDESIGN (shell-scoped — edits no engine file) ════════════════
   THE ASK (operator, exact spec): the "READ" source row reads
   EXPERTS | MODEL | YOU | READERS. Rename EXPERTS → "PANEL"; GROUP
   [PANEL · YOU · READERS] (single-weight dividers — same "measured"
   methodology); set MODEL APART at the END after a TRIPLE-weight divider; and
   HIGHLIGHT the selected option. The row label "READ" stays. The data-src
   STRINGS are unchanged (substrate routing keys on them) — this is markup ORDER
   + label + dividers + selected-highlight only. The button ORDER is set in
   twins-preview.html (PANEL · YOU · READERS · MODEL); the dividers + highlight
   are styled here. .segseg--read / .segbtn are authored in decomp-v3.css; these
   #twin-panel-scoped rules layer on top without touching that file.

   Layout: a flex row with thin single-weight rules between the measured trio
   (PANEL · YOU · READERS). ITEM 1: the reserved MODEL radio was removed (the
   reparented PANEL|MODEL segmented control is the one Model affordance), so the
   old "TRIPLE-weight rule before the last child" is GONE — otherwise Readers,
   now the last child, would wrongly inherit MODEL's set-apart Volt rule. */
#twin-panel .segseg--read { position: relative; }
/* single-weight dividers between the measured options (PANEL · YOU · READERS). */
#twin-panel .segseg--read > .segbtn + .segbtn {
  border-left: 1px solid var(--color-border, #2a3240);
}
/* HIGHLIGHT the selected option — the substrate sets data-on="1" / aria-checked
   on the active button. Lift it with the volt accent so the current read is
   unmistakable (over and above the engine's default active treatment). */
#twin-panel .segseg--read > .segbtn[aria-checked="true"],
#twin-panel .segseg--read > .segbtn[data-on="1"] {
  background: rgba(206, 255, 0, 0.10);
  box-shadow: inset 0 -2px 0 0 var(--color-volt, #CEFF00);
}
#twin-panel .segseg--read > .segbtn[aria-checked="true"] .segbtn__nm,
#twin-panel .segseg--read > .segbtn[data-on="1"] .segbtn__nm {
  color: var(--color-volt, #CEFF00);
}

/* ════════════════ ONE-EXPERT FOLD (item 4, shell-scoped — edits no engine file) ════════════════
   After the cohort-dedup the #srcsegExperts radiogroup holds a SINGLE option
   ("One expert"). At ≤640px the READ lane (#srcseg: Panel·You·Readers·Forecast)
   already fills the row, so the lone "One expert" button wrapped to its own row
   as an orphaned singleton. Fold it into the READ control cluster: make it a
   full-width trailing member tied to the lane by a thin top divider and bound
   to the lane's left edge, so it reads as a continuation of the same control,
   not a stray button on an empty row. Desktop is unchanged (it sits inline
   after the READ lane with room to spare). Scoped to #twin-panel + ≤640px so
   the canonical standalone Panel page is untouched. */
@media (max-width: 640px) {
  #twin-panel .ctl > #srcsegExperts {
    flex: 1 0 100%;
    margin-top: 6px;
    padding-top: 7px;
    border-top: 1px solid var(--color-border, #2a3240);
  }
  /* the lone button aligns flush-left under the READ lane and reads as part of
     the cluster (no orphan-centred float). */
  #twin-panel #srcsegExperts > button[data-emode="one"] {
    margin-left: 0;
  }
}

/* ════════════════ COHORT DE-DUP (shell-scoped — edits no engine file) ════════════════
   THE ASK (operator, option a): two cohort pickers existed — the panel's
   #srcsegExperts (All | Operators | External | One expert) and the instrument's
   iv2-cohort (All figures | External only). Collapse to ONE: the instrument's
   "All figures / External only" becomes the single cohort control (it keeps the
   "includes lab insiders, who lean warm" caveat). Remove the panel's redundant
   ALL + OPERATORS + EXTERNAL buttons; KEEP "One expert" (a separate single-figure
   drill). The substrate re-inserts op/ext via ensureCohortBtns(), so a static
   display:none alone is fragile — hidden here AND defensively removed by a small
   shell init in twins-preview.html. This CSS is the first line: hide the three
   redundant cohort buttons, keep "One expert". */
#twin-panel #srcsegExperts > button[data-emode="all"],
#twin-panel #srcsegExperts > button[data-emode="op"],
#twin-panel #srcsegExperts > button[data-emode="ext"] {
  display: none !important;
}

/* ════════════════ RESERVED CONTROLS (items 10 + 11, shell-scoped) ════════════════
   A control that changes nothing the reader sees must NOT read as live. These
   rules give a uniform "reserved / soon" treatment to:
     · the hero Views "Cascade" button (item 10 — renderer retired with the radial
       hero; reserved rather than a silent no-op),
     · the READ sources that are not-ready (item 11 — "You" + "Readers" do not
       drive the desktop hero gauge yet; reserved rather than active-but-dead).
   The treatment: dimmed, not-allowed cursor, a small "soon" tag, and (because
   disabled buttons don't always defeat a click handler bound by the engine) the
   pointer events are killed so a reserved control can never fire a no-op. Scoped
   to #twin-panel so the canonical standalone Panel page is untouched. */
#twin-panel .segbtn.is-reserved,
#twin-panel .segseg--read > .segbtn.is-reserved {
  opacity: 0.42;
  cursor: not-allowed;
  pointer-events: none;        /* never fires the engine's click no-op */
}
/* the small "soon" affordance tag — a quiet uppercase micro-label. */
#twin-panel .segbtn__soon {
  display: inline-block;
  margin-left: 6px;
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-secondary, #8aa0b0);
  border: 1px solid currentColor;
  border-radius: 3px;
  padding: 0 3px;
  vertical-align: middle;
  line-height: 1.4;
}
/* a reserved READ source must NOT light up as the selected read even if the
   engine flips data-on / aria-checked on it — the active-highlight is suppressed
   so it never reads as "selected but changing nothing". */
#twin-panel .segseg--read > .segbtn.is-reserved[aria-checked="true"],
#twin-panel .segseg--read > .segbtn.is-reserved[data-on="1"] {
  background: transparent;
  box-shadow: none;
}
#twin-panel .segseg--read > .segbtn.is-reserved[aria-checked="true"] .segbtn__nm,
#twin-panel .segseg--read > .segbtn.is-reserved[data-on="1"] .segbtn__nm {
  color: inherit;
}

/* ── FIRST-VIEW DECLUTTER (codex review: controls-before-instrument) ──────
   On the opening desktop view the reader met a Views row with a disabled
   Cascade, a Read row with two disabled future sources (Readers + an in-gauge
   Model that duplicates the top Panel|Model toggle), Replay and One-expert —
   with the donut itself below the fold. The reserved placeholders are kept in
   the DOM (routing + a later flip-on depends on them) but HIDDEN at desktop so
   the opening viewport shows only LIVE controls (Panel/You, All/Operators/
   External/One, Replay) and the donut rises into view. Scoped to #twin-panel;
   the reserved buttons stay visible below 721px where the stacked layout has
   room. The whole Views row collapses because its only non-reserved child is
   the current-view "Speedometer" label (Cascade is the only alternative and it
   is reserved), so with Cascade hidden the row carries no live choice. */
@media (min-width: 721px) {
  #twin-panel .segbtn.is-reserved { display: none; }
  #twin-panel .viewsw { display: none; }          /* Views row: no live alternative left */
  #twin-panel .viewsw__label { display: none; }
}

/* ── no-JS / fallback affordance ─────────────────────────────────── */
.twin-nojs {
  display: none;
  text-align: center;
  font-family: var(--aff-font-sans, 'Inter', sans-serif);
  font-size: 14px;
  color: var(--aff-text-2, #8A8F98);
  padding: 40px 20px;
}
.twin-nojs a { color: var(--aff-volt, #CEFF00); }
/* With JS off, the controller never runs; reveal the plain links + the panes
   render their inert markup. The <noscript> block flips this on. */

/* ── View Transitions: a calm cross-fade for the pane swap ───────── */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(twin-pane),
  ::view-transition-new(twin-pane) {
    animation-duration: .22s;
    animation-timing-function: var(--aff-ease, cubic-bezier(0.22,0.61,0.36,1));
  }
}

/* ── mobile @≤640px — keep the shell bar slim ────────────────────── */
@media (max-width: 640px) {
  /* Height still tracks --twin-bar-h (48px razor-thin, 88px when the model summary
     is live) so the frozen bar can grow on a phone too; only the padding tightens. */
  .twin-bar { padding: 0 12px; gap: 8px; }
  .twin-seg__btn { padding: 5px 14px; font-size: 11px; }
}

/* ════════════════════ PER-PANE TOKEN SCOPING ════════════════════
   COLLISION FIX (CSS, shell-owned — edits no engine file):
   Both engine stylesheets declare the SAME design-token NAMES on the
   global :root (deslop-tokens.css for Panel, forecast-page.css for Model),
   with DIFFERENT responsive type-scale VALUES (e.g. --fs-display 3.5rem on
   Panel vs 2.25rem→3.5rem on Model; --fs-h1/h2/h3, --fs-micro, --fs-tiny,
   --fs-body-sm, --fs-meta diverge). Because both target :root, the
   LAST-LOADED file would win for the WHOLE document and render one engine
   with the other's type scale.

   Fix: re-declare each engine's own type scale SCOPED to its pane. Custom
   properties inherit, so a value set on #twin-panel / #twin-model overrides
   the global :root value for that subtree only — each engine reads its own
   scale regardless of stylesheet load order. Colour/space/font tokens are
   byte-identical across the two files, so only the divergent type scale is
   pinned here (kept minimal + auditable). The desktop bumps mirror each
   engine's own media query so the responsive behaviour is preserved. */

/* PANEL — deslop-tokens.css :root values (constant across widths). */
#twin-panel {
  --fs-display: 3.5rem;
  --fs-h1: 2.5rem;
  --fs-h2: 1.75rem;
  --fs-h3: 1.25rem;
  --fs-h4: 1.0625rem;
  --fs-body: 1rem;
  --fs-body-sm: 0.875rem;
  --fs-meta: 0.8125rem;
  --fs-mono-sm: 0.75rem;
  --fs-micro: 0.6875rem;
  --fs-tiny: 0.625rem;
}

/* MODEL — forecast-page.css :root values (mobile-first). */
#twin-model {
  --fs-display: 2.25rem;
  --fs-h1: 1.875rem;
  --fs-h2: 1.5rem;
  --fs-h3: 1.1875rem;
  --fs-body: 1rem;
  --fs-body-sm: 0.9375rem;
  --fs-meta: 0.8125rem;
  --fs-micro: 0.75rem;
  --fs-tiny: 0.6875rem;
}
/* MODEL desktop bump — mirrors forecast-page.css @media (min-width:720px). */
@media (min-width: 720px) {
  #twin-model {
    --fs-display: 3.5rem;
    --fs-h1: 2.5rem;
    --fs-h2: 1.75rem;
    --fs-h3: 1.25rem;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   Citation-card grid: uniform heights (shell-scoped — never touches the live
   citefeed.css that index.html also loads). Quote length varies, so the cards
   used to settle to their natural height (citefeed.css sets the grid to
   align-items:start), leaving a ragged row. Here each card stretches to the
   tallest in its grid row and the footer (source · date · "why this read")
   is pinned to the card bottom so a short card no longer floats its footer
   mid-card. Quotes are never truncated; the slack becomes quiet surface
   between the quote and the footer.
   Scoped to .twin-body so only the twins shell is affected.
   ───────────────────────────────────────────────────────────────────────── */
.twin-body .citefeed .cards { align-items: stretch; }
.twin-body .citefeed .card { height: 100%; }
.twin-body .citefeed .card .crbar { margin-top: auto; }

/* ─────────────────────────────────────────────────────────────────────────
   Kicker / eyebrow type-system parity across the twins (item 6).
   The PANEL's section labels (.iv2-* mono kickers) render at 10px / 600 /
   0.14em tracking / grey #5a6070; the MODEL's mono labels (.label-mono,
   .fi-section__label, .fi-block__kicker) render at 12px / 500 / ~0.11em /
   grey #8A8F98 — so the two halves read as two different type systems.
   We adopt the PANEL spec as canonical and normalize the MODEL's
   SECTION-LABEL mono furniture to match, SCOPED to #twin-model so the live
   forecast-instrument.css / forecast-page.css are never edited. Data-viz
   tick/axis/needle labels are NOT these classes and are intentionally left
   untouched (this only governs section eyebrows, not the instruments).
   ───────────────────────────────────────────────────────────────────────── */
#twin-model .label-mono,
#twin-model .fi-section__label,
#twin-model .fi-block__kicker {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  color: #5a6070;
}

/* R11 — the Model's section kickers are volt, matching the Panel's .hd__ix.
   Out-specifies the #twin-model .label-mono gray harmonizer above
   (1-2-0 > 1-1-0) so the wayfinding eyebrows (Backing data / 01 · Methodology
   / 02 · Evidence / 03 · FAQ / 04 · Machine channel) render volt in the Model
   pane too. Volt stays kickers-only: .fi-section__label / .fi-block__kicker
   stay gray (and are mostly cut by R10). */
#twin-model .fp-section__kicker.label-mono {
  font-size: 11px;   /* R11 — re-assert 11px over the :686 harmonizer's 10px
                        (which this compound did not reset), matching the
                        Panel's .hd__ix 11px kicker in the twins pane too. */
  color: var(--color-volt, #CEFF00);
  font-weight: 600;
  letter-spacing: 0.16em;
}

/* R16 — the standalone's source toggle (#fi-source) is removed entirely (the
   Voices view is gone), so the old "#twin-model .fi-source { display:none }"
   suppression is no longer needed and has been dropped. */

/* R12 — no floating chrome over the Model view; the frozen bar is the way back.
   The wayfinding host (.wayfind — the ↑TOP + back-to-grid, Panel-shared) is
   broken in the Model view (its topTarget resolves Panel elements inside the
   hidden pane) AND floats at z-70 over the cascade / gauge-wedge drawer. Hide
   the whole host in the Model view; back-to-grid is only ever armed by Panel
   matrix clicks, so nothing is lost. The Panel view keeps ↑TOP working. */
body[data-twin-view="model"] .wayfind { display: none; }

/* ─────────────────────────────────────────────────────────────────────────
   MODEL HERO EYEBROW PARITY (item 7, shell-scoped — edits no engine file).
   The model hero ships the forecast engine's native .fp-hero__stamp
   ("deslop.media · The AI Speedometer Model · as of Jun 2026", 10px mono, no
   volt-dash). twins-shell.js suppresses that stamp (hidden) and injects a
   .lede__kicker eyebrow into #twin-model's .fp-hero__pin so the two twins lead
   with the SAME eyebrow shape — the volt-dash + "The AI Speedometer" treatment
   the panel hero already uses (.lede__kicker is authored in speedo-morph.css,
   which the twins page loads; it renders with the volt-dash ::before there).
   These rules pin the panel's kicker spec onto the model pane explicitly so it
   reads identically regardless of the model pane's own type-token scope, and
   style the folded-in "Model · <date>" tail as a quiet sibling of the wordmark.
   The native .fp-hero__stamp stays in the DOM (hidden) — no engine edit. */
#twin-model .fp-hero__pin .lede__kicker {
  display: inline-flex;        /* matches the panel kicker's inline-flex exactly */
  align-items: center;
  gap: 10px;
  /* NATURAL-BOUNDARY WRAP (375px): as a nowrap inline-flex row the bare label
     text node line-broke INTERNALLY mid-label ("THE AI SPEEDOMETER ·" /
     "MODEL"). Let the ROW wrap instead, and keep each unit atomic (the nowrap
     spans below) so the only break point is the label/date boundary. */
  flex-wrap: wrap;
  row-gap: 3px;
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-text, #E8E6E1);
  margin: 0 0 13px;
}
#twin-model .fp-hero__pin .lede__kicker::before {
  content: "";
  width: 24px;
  height: 2px;
  background: var(--color-volt, #CEFF00);
  flex: 0 0 auto;
  border-radius: 1px;
}
#twin-model .fp-hero__pin .lede__kicker-label { white-space: nowrap; }
#twin-model .fp-hero__pin .lede__kicker-date {
  font-weight: 500;
  letter-spacing: 0.12em;
  color: var(--aff-text-2, #8A8F98);
  white-space: nowrap;
}
@media (max-width: 640px) {
  /* gap is a shorthand (row+column): restate the tight row-gap after it so a
     wrapped second line hugs the first instead of opening a 9px band. */
  #twin-model .fp-hero__pin .lede__kicker { font-size: 12px; letter-spacing: 0.15em; gap: 9px; row-gap: 3px; margin-bottom: 11px; }
  #twin-model .fp-hero__pin .lede__kicker::before { width: 20px; }
}
/* belt-and-suspenders: the engine's native stamp is hidden by the [hidden]
   attribute twins-shell.js sets; also hide it via CSS so a re-render that drops
   the attribute can't flash the old 10px eyebrow back. */
#twin-model .fp-hero__stamp { display: none !important; }

/* ─────────────────────────────────────────────────────────────────────────
   Desktop: collapse the big vertical gap between the Views/READ control bar
   and the instrument card so the gauge sits above the fold (item 7). The
   .iv2-stage ships 40px desktop top-padding; with the duplicate instrument
   eyebrow now dropped (one "THE AI SPEEDOMETER", in the page hero kicker),
   that headroom is dead space. Tighten it desktop-only, scoped to .twin-body
   so the standalone panel page is unaffected by this shell rule. Mobile keeps
   its own .iv2-stage padding (instrument-v2.css @max-width:720 → 20px).
   ───────────────────────────────────────────────────────────────────────── */
@media (min-width: 720px) {
  .twin-body #twin-panel .iv2-stage { padding-top: 10px; }
}

/* Persistent consumer CTA in the shared masthead — push it to the right edge
   so it rides both views without crowding the brand lockup. Inherits the
   .px-top__connect pill style (pyramid.css). On narrow widths the masthead
   flex-wraps it to a second row (its own line), which keeps it tappable. */
.aff-mast .twin-mast__connect { margin-left: auto; }
@media (max-width: 560px) {
  .aff-mast .twin-mast__connect { margin-left: 0; }
}

/* ─────────────────────────────────────────────────────────────────────────
   MODEL-INK LAW on the forecast gauge needle (R7 B, shell-scoped — edits no
   engine file). The forecast engine (forecast-needle.js) paints its gauge
   needle wedge-cap and the "leaning" readout value in SOLID VOLT — the ink the
   panel reserves for a MEASURED read. But inside #twin-model the gauge is the
   Model's read, and the Model read is a PROJECTION, not a measurement, so it
   must obey the same MODEL-INK LAW the panel's own projected needle already
   follows (speedo-morph.css:102-120 — .rdl-model: desaturated steel ghost,
   open blade, dotted spine, steel #8a98ad, never Volt). The engine paints the
   wedge-cap via an injected <style> RULE (forecast-needle.js:440), so an
   ID+class selector here outspecifies it without touching forecast-*. The
   "leaning" value (.fi-needle-window-v) is colored by an INLINE style the
   engine sets when the lead scenario runs warm (forecast-needle.js:322/330),
   so it needs !important to lose the Volt. Both reachable shell-side because
   #twin-model is cloned into THIS document (no iframe). Steel #8a98ad keeps AA
   on the #101820 gauge field.
   ───────────────────────────────────────────────────────────────────────── */
#twin-model .fi-needle-wedge-cap {
  stroke: #8a98ad;
  stroke-dasharray: 2 4;          /* open, dotted — reads projected, not solid */
  opacity: 0.9;
}
#twin-model .fi-needle-wg.is-lead .fi-needle-wedge-cap {
  stroke: #8a98ad;                /* the leader gets no Volt promotion either */
}
/* the engine's needle blade/spine/pip in the same Model pane → steel ghost too,
   so the whole needle reads as one projected instrument, not a Volt blade with
   a steel cap. */
#twin-model .fi-needle-blade { fill: #8a98ad; opacity: 0.30; }
#twin-model .fi-needle-spine { stroke: #8a98ad; }
#twin-model .fi-needle-tail  { fill: rgba(138,152,173,0.6); }
#twin-model .fi-needle-pip   { stroke: #8a98ad; }
/* the "leaning" readout value — inline-styled Volt when warm; !important demotes
   it to the steel ghost register so the projected read never shows measured ink. */
#twin-model .fi-needle-window-v { color: #aab6c8 !important; }

/* ─────────────────────────────────────────────────────────────────────────
   MODEL MACHINE-CHANNEL MCP ON-RAMP (R7 C, shell-scoped — edits no engine
   file). The static .fp-mc-onramp block in twins-preview.html's §04 sits
   OUTSIDE #fi-machine (which forecast-machine.js owns + repaints); this styles
   it to read as a quiet sibling of the engine's own .fp-mc-lede / .fp-mc-stance
   furniture, reusing the forecast engine's design tokens so it matches the
   machine-channel look. Volt-free border-left mirrors the panel on-ramp's
   restraint (the stance block keeps the one Volt rule). */
#twin-model .fp-mc-onramp {
  max-width: var(--read-max, 68ch);
  margin-top: var(--space-lg, 28px);
  padding-top: var(--space-md, 18px);
  border-top: 1px solid var(--color-border, rgba(176,188,206,0.18));
}
#twin-model .fp-mc-onramp__q {
  margin: 0 0 var(--space-xs, 8px);
  color: var(--color-text-secondary, #b6c0cf);
  letter-spacing: 0.12em;
}
/* R17b — 16px reading floor: the on-ramp is reading prose. */
#twin-model .fp-mc-onramp__lede {
  max-width: var(--read-max, 68ch);
  margin: 0 0 var(--space-md, 18px);
  font-size: var(--fs-body, 16px);
  color: var(--color-text-secondary, #b6c0cf);
}
#twin-model .fp-mc-onramp__list {
  margin: 0;
  padding-left: 1.2em;
  font-size: var(--fs-body, 16px);
  color: var(--color-text-secondary, #b6c0cf);
}
#twin-model .fp-mc-onramp__list li { margin-bottom: var(--space-sm, 12px); }
#twin-model .fp-mc-onramp__list li:last-child { margin-bottom: 0; }
#twin-model .fp-mc-onramp code {
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 0.92em;
  color: var(--color-steel, #8a98ad);
}
#twin-model .fp-mc-onramp a code { color: inherit; }

/* ─────────────────────────────────────────────────────────────────────────
   HARMONIZE: the Model gauge becomes the SAME 270° donut as the Panel
   (shell-scoped — edits no engine file).

   THE ASK (operator): harmonize the instrument across Panel/Model. The Model
   shipped the forecast engine's native half-gauge (forecast-needle.js appends
   a .fi-needle-wrap radial wedge gauge into #fi-needle). The Panel draws a
   270° open-bottom donut (instrument-v2.js / window.IV2). Two different gauge
   SHAPES read as two figures, not twins.

   THE FIX: a shell-owned adapter (twins-model-donut.js) redraws the Model
   gauge as the SAME donut shape — reusing window.IV2's geometry library
   (ring/pt/CFG_DESKTOP) but FED BY window.FI.get().scenarios, in projected-
   steel ink (MODEL-INK LAW) with a steel needle, repainting live on every
   dial turn (FI.subscribe). It mounts a .tmd-host into #fi-needle, BELOW the
   engine's native "Pace / Where the engine is leaning" caption nodes (those
   stay). Here we HIDE the engine's native gauge wrap (.fi-needle-wrap) so only
   the harmonized donut shows in the slot. Scoped to #twin-model so the
   canonical standalone forecast page (/aifutures/ai-forecast/) is untouched.
   ───────────────────────────────────────────────────────────────────────── */
#twin-model #fi-needle .fi-needle-wrap { display: none !important; }

/* ── P3b — THE CONVERGED MODEL GAUGE HOST ─────────────────────────────────
   twins-model-gauge.js mounts the SAME Panel instrument (IV2.mountGauge,
   chrome:'model') into a .tmg-host in #fi-needle, so the model pane now renders
   the full .iv2-* instrument (mass-width wedges + score-range arm + reading
   panel), sourced from the model's live distribution. instrument-v2.css already
   styles every .iv2-* class on this page; twins-shell.css only needs to (a) let
   the host fill the slot and (b) re-skin the arm into the PROJECTED register. */
#twin-model .tmg-host {
  display: block;
  width: 100%;
  margin: 10px auto 0;
}
/* The instrument's .iv2-row is a nowrap flex (gauge column + reading panel) sized
   for the Panel's wide .iv2-stage. The Model slot (#fi-needle) is narrower, so the
   reading panel would be crushed to width 0. STACK the two columns in the model
   pane: wrap the row, let the gauge column be full-width (centred, capped to its
   aspect) and the reading panel take the full width below it. Layout-only; no ink
   change. Mirrors how the Panel stacks at its own narrow breakpoint. */
#twin-model .tmg-host .iv2-row {
  flex-wrap: wrap;
  gap: 8px;
}
#twin-model .tmg-host .iv2-gaugecol {
  flex: 1 1 100%;
  max-width: 460px;
  margin: 0 auto;
}
#twin-model .tmg-host .iv2-panel {
  flex: 1 1 100%;
  min-width: 0;
}
/* Legacy donut host (kept harmless in case a stale adapter ever mounts). */
#twin-model .tmd-host {
  display: block;
  width: 100%;
  max-width: 420px;
  margin: 10px auto 0;
}
#twin-model .tmd-svg { display: block; width: 100%; height: auto; }
#twin-model .tmd-needle line { opacity: 1; }

/* ── THE CANONICAL ARM on the Model pane — PROJECTED register ──────────────
   The converged gauge draws the SAME window.IV2.buildArm geometry the Panel
   uses, so the arm carries the Panel's .iv2-arm-* classes. instrument-v2.css
   paints those FULL Volt (a MEASURED read). twins-shell.css loads AFTER it, and
   #twin-model .tmg-host .iv2-arm-* out-specifies .iv2-arm-*, so these rules
   re-skin the SAME blade into the Model's PROJECTED variant WITHOUT any engine
   edit: dimmed Volt (--arm-volt-projected), a DASHED spine, and an OPEN (hollow)
   pivot ring. This keeps the arm the projected-not-measured register and never
   blurs with the Panel (solid) or the reader (hollow-outline blade + chip) arms.
   NO glow/shadow. The .tmd-needle selectors are kept for the legacy donut. */
#twin-model .tmg-host .iv2-arm-blade,
#twin-model .tmd-needle .iv2-arm-blade {
  fill: var(--arm-volt-projected, rgba(206, 255, 0, 0.45));
  stroke: #101820; stroke-width: 0.6;
}
#twin-model .tmg-host .iv2-arm-tail,
#twin-model .tmd-needle .iv2-arm-tail {
  fill: var(--arm-volt-projected, rgba(206, 255, 0, 0.45)); opacity: 0.32;
}
#twin-model .tmg-host .iv2-arm-tip,
#twin-model .tmd-needle .iv2-arm-tip {
  fill: var(--arm-volt-projected, rgba(206, 255, 0, 0.45)); stroke: #101820; stroke-width: 0.5;
}
#twin-model .tmg-host .iv2-arm-spine,
#twin-model .tmd-needle .iv2-arm-spine {
  stroke: var(--arm-volt-projected, rgba(206, 255, 0, 0.45));
  stroke-dasharray: 5 4;            /* dashed = projected */
  opacity: 0.9; stroke-linecap: round;
}
#twin-model .tmg-host .iv2-arm-pivot,
#twin-model .tmd-needle .iv2-arm-pivot {
  fill: none;                        /* OPEN (hollow) pivot ring = projected */
  stroke: var(--arm-volt-projected, rgba(206, 255, 0, 0.45));
}
/* reduced-motion: the gauge never animates a repaint; this belt-and-suspenders
   kills any inherited transition on the instrument SVG. */
@media (prefers-reduced-motion: reduce) {
  #twin-model .tmg-host .iv2-svg, #twin-model .tmg-host .iv2-svg * { transition: none !important; }
  #twin-model .tmd-svg, #twin-model .tmd-svg * { transition: none !important; }
}
@media (max-width: 640px) {
  #twin-model .tmd-host { max-width: 340px; margin-top: 8px; }
}

/* ─────────────────────────────────────────────────────────────────────────
   PART B — FROZEN / STICKY DIAL ROW (shell-scoped — edits no engine file).

   THE ASK (operator): restore the sticky/frozen instrument row — pin the four
   dials (+ the horizon scrubber) to the top of the viewport so that as the
   reader scrolls the Model content (the distribution list below), the dials
   stay PINNED and twisting them updates the harmonized donut + the
   distribution LIVE while in view.

   HISTORY: the panel's hero once carried this idea as
   speedo-morph.css:159-163 — `body.is-armed .mh-pin{position:sticky;
   top:var(--mh-mast,49px)}` — a scroll-pinned instrumentation row that kept
   "the selector bar + gauge in the same viewport" while the content built
   beneath. We reapply that pattern to the MODEL's #fi-controls.

   THE FIX: make #fi-controls a position:sticky row pinned just BELOW the one
   viewport chrome (the .twin-bar shell toggle — sticky, top:0, 48px, z-index
   70). So #fi-controls pins at top:48px, z-index 60 (below the bar, above the
   content), with an opaque franchise-dark backdrop so the distribution rows
   scroll cleanly behind it. It must NOT fight the shell bar's own sticky — it
   stacks below it. Scoped to #twin-model so the standalone forecast page (no
   .twin-bar) is untouched.

   GOTCHAS WATCHED (recon flags):
     · An ancestor with overflow:hidden/auto/scroll defeats position:sticky.
       #fi-controls' ancestors in the cloned model markup are .fi-instrument
       → .fp-page → #twin-model → body, none of which set overflow here, so
       the sticky resolves against the viewport.
     · Mobile: the four full-width dials + scrubber are tall; a pinned block
       that eats the whole viewport is hostile on a phone. At ≤640px we RELEASE
       the sticky (position:static) so the dials scroll normally — the donut
       still repaints live on every turn, just not pinned. Desktop keeps the
       pin where there's vertical room.
   ───────────────────────────────────────────────────────────────────────── */
@media (min-width: 721px) {
  /* STICKY BLOCKER FIX: the cloned model root <main class="fp-page" id="fp-top">
     carries `overflow-x: hidden` (forecast-page.css:130). Per CSS spec, when one
     overflow axis is non-visible the OTHER computes to `auto` — so overflow-y
     resolves to `auto`, which makes .fp-page a SCROLL CONTAINER and defeats any
     descendant position:sticky resolving against the viewport (the recon's
     overflow gotcha). Swap to `overflow-x: clip`: it still clips the horizontal
     bleed forecast-page.css wanted, but `clip` does NOT force overflow-y to
     `auto` and does NOT establish a scroll container, so the dial row's sticky
     pins against the viewport again. Scoped to #twin-model — the standalone
     forecast page keeps its own overflow-x:hidden untouched. */
  #twin-model .fp-page { overflow-x: clip; }
  #twin-model #fi-controls {
    position: -webkit-sticky;
    position: sticky;
    /* P4 (F5) — pin just below the shell bar's LIVE height. Reading --twin-bar-h
       (not a hard 48px) means when the frozen model summary grows the bar to 88px
       the dial row follows it instead of hiding partially behind the taller bar. */
    top: var(--twin-bar-h, 48px);
    z-index: 60;                     /* below the bar (70), above the content */
    background: var(--aff-bg, #101820);
    /* a hairline rule + a touch of breathing room so the pinned row reads as a
       distinct frozen band, not floating text over the scrolling content. */
    border-bottom: 1px solid var(--aff-border, #2a3240);
    padding-bottom: 10px;
    /* round the bottom corners subtly so it reads as a console band. */
    box-shadow: 0 6px 14px -8px rgba(0, 0, 0, 0.5);
    margin-left: calc(-1 * var(--space-md, 18px));
    margin-right: calc(-1 * var(--space-md, 18px));
    padding-left: var(--space-md, 18px);
    padding-right: var(--space-md, 18px);
  }
}
/* mobile (≤640px) + small tablets (≤720px): release the pin — a tall pinned
   dial stack is hostile on a phone. The donut still repaints live on every
   turn; the dials simply scroll in normal flow. */
@media (max-width: 720px) {
  #twin-model #fi-controls { position: static; top: auto; }
}

/* ── DIET v2 (R17f) — ONE scenario list on the twins Model view ──────────────
   The engine's reading panel (.iv2-panel: THE READING eyebrow + paneltitle +
   blurb + band list + DETAIL inspect) restates masses the gauge wedges shape,
   band totals the gauge band overlay already prints, and the no-crown/held-
   empty caveats the disclosure + arm caption own. The glossed #fi-dist list
   below is the ONE list (it alone carries the glosses + the 10–90 fans). The
   engine's bytes are FROZEN (Panel parity), so the merge is this shell-scoped
   hide — zero engine edits, Panel untouched (#twin-model scope only).
   Also hidden (disclosure dedup):
   · .iv2-coverage — the MODEL SCOPE box, a near-verbatim repeat of the arm
     caption 60px above it; the arm caption keeps the instrument's single
     honesty line ("…not a calibrated forecast; no scenario is crowned").
   · .iv2-center-tiny — the center's "contested · no scenario leads outright"
     line (R17c): the wedges show it; the center keeps the STRUCTURAL MODEL
     kicker + year. */
#twin-model .tmg-host .iv2-panel { display: none !important; }
#twin-model .tmg-host .iv2-coverage { display: none !important; }
#twin-model .tmg-host .iv2-center-tiny { display: none !important; }
/* the orphaned gauge column centers in the row the panel used to share */
#twin-model .tmg-host .iv2-row { justify-content: center; }
/* R17b — 16px reading floor: with .iv2-coverage and .iv2-panel hidden, the
   arm caption is the instrument's ONE honesty line on the Model view; it is
   two reading sentences, not a micro-label. Model pane only (Panel untouched). */
#twin-model .tmg-host .iv2-armcap {
  font-size: 16px;
  line-height: 1.45;
}

/* ════════════════ R17e (c) — MOBILE MODEL CHROME DIET (≤640px) ════════════════
   Frame one on a phone was ~590px of masthead + text-nav + two CTA pills +
   kicker + headline before the gauge. Cut the chrome so the FULL gauge crests
   the fold (target: gauge top ≤ ~y320 @375×812). All shell-scoped: the Panel
   view and the canonical standalone pages are untouched; the hidden nav stays
   in the DOM (no-JS never sets data-twin-view, so nothing is lost without the
   shell). Paired with R17e (b): the frozen summary bar arms AT LOAD while the
   gauge is still below the fold (twins-shell.js), so frame one reads compact
   live read up top + the gauge cresting beneath. */
@media (max-width: 640px) {
  /* the masthead text-nav row duplicates the PANEL|MODEL toggle just below;
     the two CTA pills leave the reader's frame one (Apply is a publisher CTA —
     the end-of-read band keeps that door; Follow returns on the Panel view
     and at desktop widths). */
  body[data-twin-view="model"] .aff-mast__nav { display: none; }
  body[data-twin-view="model"] .aff-mast__cta { display: none; }
  body[data-twin-view="model"] .aff-mast .twin-mast__connect { display: none; }
  /* ONE-LINE kicker: the label + the as-of date share a single line (the date
     is this surface's one as-of; the wrapped second row is gone). */
  #twin-model .fp-hero__pin .lede__kicker {
    flex-wrap: nowrap;
    font-size: 11px;
    letter-spacing: 0.12em;
    gap: 8px;
    row-gap: 0;
    margin-bottom: 8px;
  }
  #twin-model .fp-hero__pin .lede__kicker::before { width: 16px; }
  #twin-model .fp-hero__pin .lede__kicker-date { letter-spacing: 0.1em; }
  /* tighten the hero→instrument rhythm (margins, not fonts — cut space, not
     legibility; the 16px reading floor is untouched). */
  #twin-model .fp-hero__pin { padding-block: 10px 2px; }
  #twin-model .twin-seg-slot { margin-bottom: 6px; }
  #twin-model #fi-headline { margin-top: 8px; padding-top: 0; padding-bottom: 2px; }
  #twin-model .fi-hl-read { margin-bottom: 4px; }
  #twin-model .fi-instrument-top #fi-needle { margin-top: 8px; padding-top: 0; }
  #twin-model .tmg-host { margin-top: 4px; }
  /* crop the DEAD SPACE below the gauge: the 270° donut's visual mass ends
     above the 524×400 box bottom (open-bottom notch + dead corners); pull the
     stack below up over the empty band. Scoped to the Model pane's mounted
     host — the Panel instrument never sees this. */
  #twin-model .tmg-host .iv2-gaugewrap { margin-bottom: -34px; }
}
