/* ─────────────────────────────────────────────────────────────────────
   threat-map.css — shared interactive threat-map engine styles.
   Extracted from detail-view (the canonical map). Loaded AFTER style.css
   so it can use the design tokens (--mint, --border, --page-title, …).
   ───────────────────────────────────────────────────────────────────── */

    /* ── Apollo extended mint palette ───────────────── */
    :root {
      --mint-deep:   #0FA995;
      --mint-mid:    #8FE6DC;
      --mint-soft:   #C5EFE9;
      --mint-tint:   #D7F3EF;
      --mint-wash:   #ECF9F7;
      --mint-bg:     #F5FBFA;
      --mint-text:   var(--teal-deep);   /* shared deep teal (components.css) */
    }

    /* ── Page-level: allow vertical scroll ─────────── */
    body { overflow-x: hidden; overflow-y: auto; }

    /* ── Map pane (top, fixed height) ──────────────── */
    .tm-pane-map {
      position: relative;
      margin-top: 64px;
      height: 55vh;
      min-height: 340px;
      max-height: 680px;
      width: 100%;
      background: var(--surface-bg);
      overflow: hidden;
      transition: height 0.4s var(--ease-out);
      border-bottom: 1px solid var(--border);
    }
    .tm-pane-map.is-node-expanded {
      height: 62vh;
      max-height: 740px;
    }
    .tm-pane-map.is-fullscreen {
      position: fixed;
      inset: 0;
      z-index: 200;
      height: 100vh !important;
      height: 100dvh !important;   /* dvh tracks the mobile URL bar so fullscreen isn't clipped */
      max-height: none !important;
      margin-top: 0;
    }
    .tm-pane-map.is-fullscreen::after { display: none; }

    /* ── Header bar for map pane (matches header-row-2) */
    .tm-pane-head {
      position: absolute;
      top: 0; left: 0; right: 0;
      z-index: 8;
      background: rgba(255,255,255,0.97);
      border: none;
      border-bottom: 1px solid var(--border);
      border-radius: 0;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      padding: 0 20px;
      height: 40px;
    }
    .tm-pane-eyebrow {
      font-size: 11px;
      font-weight: 500;
      letter-spacing: 0.18em;
      text-transform: uppercase;
      color: var(--ink-muted);
      white-space: nowrap;
      /* Centered in the head — robust regardless of the (empty) actions wrapper. */
      position: absolute;
      left: 50%;
      transform: translateX(-50%);
    }
    .tm-pane-eyebrow .accent { color: var(--mint); margin-right: 6px; }
    .tm-pane-actions { display: flex; align-items: center; gap: 0; }
    /* v2 removed the pane-head back button, leaving .tm-pane-actions empty;
       collapse it so space-between doesn't push the "Threat Map" eyebrow
       right — keeps it flush-left as in v1. */
    .tm-pane-actions:empty { display: none; }
    .tm-icon-btn {
      display: inline-flex; align-items: center; justify-content: center; gap: 5px;
      height: auto; padding: 0 10px;
      border: none; border-radius: 0;
      background: transparent; color: var(--ink-3);
      font-family: inherit; font-size: 12.5px; font-weight: 500;
      cursor: pointer;
      transition: color 0.15s;
      white-space: nowrap;
    }
    .tm-icon-btn:hover { color: var(--text); background: transparent; border-color: transparent; }
    .tm-icon-btn svg { width: 12px; height: 12px; flex-shrink: 0; opacity: 0.6; }

    /* ── Map stage (fills map pane) ─────────────────── */
    .tm-map-stage {
      position: absolute; inset: 0;
      overflow: hidden;
      background: var(--surface-page);
    }
    /* dot grid — matches the shared page dots (app.css) */
    .tm-map-stage {
      background-image: url("figures/dot-grid.svg");
      background-repeat: repeat;
      background-size: 16px 16px;
      background-position: 0 -3px;   /* nudge up so the first dot row isn't flush against the pane's top edge */
    }

    /* SVG map */
    #threat-map {
      position: absolute; inset: 0;
      width: 100%; height: 100%;
      display: block;
      cursor: grab;
      user-select: none;
      /* Hand ALL touch gestures to the pan-zoom controller (single-finger pan,
         two-finger pinch). Without this the browser claims them for native
         scroll/zoom and the custom gestures never fire on mobile. */
      touch-action: none;
      z-index: 1;
    }
    #threat-map:active { cursor: grabbing; }
    /* Inert cyan state/outcome boxes — non-interactive labels. pointer-events:none
       lets a press fall through to the map for panning. */
    .tm-inert { pointer-events: none; }

    /* Map reveal: the whole map is hidden until the first fit lands (after the
       PP web font loads, so the fit uses real text-box bounds, not fallback
       metrics). This guarantees the user never sees an un-fit / wrong-zoom
       frame — only the empty dotted bg, then the correctly-fitted map fading in
       once. Deterministic default view, no load flash. */
    #viewport { transition: opacity 0.2s var(--ease-out); }
    .tm-map-stage.tm-fit-pending #viewport { opacity: 0; transition: none; }

    /* Monitoring section is hidden until the user clicks "see how monitoring might be undermined" */
    .tm-map-stage:not(.is-monitoring-revealed) .tm-monitoring { display: none; }

    /* First-reveal "wow": the monitoring-subversion tree stages in as the view flies
       there — nodes rise + un-blur in a cascade (--mi index set in JS, left→right),
       edges fade in just behind their from-node. Plays once (class removed after).
       Reduced-motion: the global reset zeroes the duration, so it simply appears. */
    @keyframes tm-monitoring-in {
      from { opacity: 0; transform: translateY(14px); filter: blur(3px); }
      to   { opacity: 1; transform: translateY(0);    filter: blur(0); }
    }
    @keyframes tm-monitoring-edge-in { from { opacity: 0; } to { opacity: 1; } }
    .tm-map-stage.tm-monitoring-revealing .tm-monitoring.map-node {
      animation: tm-monitoring-in 0.52s var(--ease-out) backwards;
      animation-delay: calc(var(--mi, 0) * 65ms);
    }
    .tm-map-stage.tm-monitoring-revealing path.tm-monitoring {
      animation: tm-monitoring-edge-in 0.4s var(--ease-out) backwards;
      animation-delay: calc(var(--mi, 0) * 65ms + 160ms);
    }

    /* Resting card elevation = --card-shadow, translated to drop-shadows (SVG rects can't
       take box-shadow) so detail cards carry the SAME soft shadow as VS/Standard. All three
       states (resting/hover/active) use a matching 2× drop-shadow structure so `filter`
       interpolates smoothly instead of popping. stroke transitions are quick (0.1s) so the
       active 2px dark border appears near-instantly, not as a staggered growth. */
    /* Transparent hover hit-buffer. pointer-events:all makes the (invisible) stroke
       perimeter hit-testable, so the node's hit area extends ~5px on screen all round
       — counter-scaled by --zoom-scale so it's a constant width at any zoom, matching
       the lift. This keeps the cursor "inside" through the ~3px hover lift so the card
       can't flicker (lift off cursor → unhover → drop → re-hover) when hovered on its
       edge. fill:none so it never paints; it's purely a pointer target. */
    .map-node .tm-hit {
      fill: none; stroke: transparent;
      stroke-width: calc(10px / var(--zoom-scale, 1));
      pointer-events: all;
      filter: none; transition: none;   /* out-set the .map-node rect shadow/transition: it never paints */
    }
    .map-node rect {
      stroke-width: 0.5px;   /* match the Visual Summary card border (0.5px), not the old 1.5px */
      filter: drop-shadow(0 1px 1.5px rgba(16,24,40,0.10)) drop-shadow(0 1px 1px rgba(16,24,40,0.06));
      transition: filter 0.16s var(--ease-out), stroke-width 0.1s var(--ease-out), stroke 0.1s var(--ease-out);
    }
    /* Coloured cards take the arrow-weight border (1.5px = --diag-node-border), matching
       the edges + the Standard view's coloured cards. Class lives on the visible rect (not
       the .tm-hit buffer) so specificity stays (0,2,1) — the .node-selected / :focus-visible
       active strokes (defined later / !important) still win when a card is selected. */
    .map-node rect.tm-card-colored { stroke-width: var(--diag-node-border); }
    /* Lift is counter-scaled by the live pan-zoom scale (--zoom-scale, set in
       applyTransform) so an SVG node rises a constant ~3px ON SCREEN at any zoom —
       a plain translateY would scale with the map (invisible when zoomed out, huge
       when zoomed in). */
    /* .map-node.detail-node (every node carries both) so this wins over the bare
       .detail-node { transition: opacity } — that shorthand would otherwise wipe the
       transform transition and the lift would snap. Both transform (lift) and opacity
       (the selection dim/ripple) need to animate, so enumerate both here. */
    .map-node.detail-node { transition: transform 0.2s var(--ease-out), opacity 0.34s var(--ease-out); }
    /* Section group rect — on hover/highlight the panel BRIGHTENS (a more-opaque white
       fill, not a darken) and its border firms (darker colour + 2px) but STAYS DASHED.
       Driven by :hover (real pointers only) and a JS .group-hovered class (added in the
       interaction phase to stop child-card flicker).
       Selector is .tm-group-rect.detail-node (every group rect carries both classes) so
       this transition wins over .detail-node's `transition: opacity` — otherwise the
       shorthand there wipes the fill/stroke transitions and the brighten snaps. opacity
       is folded back in so the map's reveal fade still works. */
    .tm-group-rect.detail-node {
      transition: fill 0.34s var(--ease-out), stroke 0.34s var(--ease-out),
                  stroke-width 0.34s var(--ease-out), opacity 0.35s var(--ease-out);
    }
    .tm-group-rect.group-hovered {
      fill: rgba(255,255,255,0.92); stroke: #A5A5A5; stroke-width: 2px;   /* dashes kept */
    }
    /* Selected section box — firmer dark dashed border (clicking a section reads as
       "active"). Defined after .group-hovered so it wins when both apply. */
    .tm-group-rect.node-selected {
      stroke: var(--diag-selection); stroke-width: 2px;   /* dashes kept */
    }
    /* Arrowhead markers sourced from the shared diagram tokens. */
    #arr polygon     { fill: var(--diag-edge-arrow); }
    #arr-red polygon { fill: var(--diag-edge-required-arrow); }
    @media (hover: hover) and (pointer: fine) {
      /* Premium card hover for the SVG map: a drop-shadow reads as "lift" without an
         actual translate — translateY on a <g> scales with the pan-zoom parent (tiny
         at fit, huge zoomed in) and would slip the node out from under the cursor. The
         shadow elevates only the hovered node, so it's flicker-free and cheap. A whisper
         of brightness keeps the existing contrast against the brightening container. */
      .map-node:not(.tm-inert):hover { transform: translateY(calc(-3px / var(--zoom-scale, 1))); }
      /* Lift shadow — same 2× drop-shadow structure as the resting state so it morphs smoothly. */
      /* box-shadow blur maps to a Gaussian stdDev of blur/2; filter:drop-shadow maps to the
         full blur, so drop-shadow reads ~2x softer at the same value. To match the standard
         view box-shadow(...16px), drop-shadow blur ~= 10.5px (9.5 to match + a small bump for
         the detail boxes larger native size, 300 vs 260px). */
      .map-node:not(.tm-inert):hover rect { filter: drop-shadow(0 6px 10.5px rgba(16,24,40,0.10)); }
      /* container hover is driven by JS .group-hovered (bounds-based) so it
         stays steady while the cursor crosses child cards — see #11. */
    }
    .detail-node { transition: opacity 0.35s var(--ease-out); }
    .detail-node.hidden { opacity: 0; pointer-events: none; }
    /* The detail map appears instantly (no per-node fade-in from empty); the
       whole map is revealed once via #viewport opacity after the first fit. */
    .tm-no-reveal-anim .detail-node { transition: none; }

    /* ── Breadcrumb (hidden) ───────────────────────── */
    .tm-breadcrumb { display: none; }

    /* ── Legend ────────────────────────────────────── */
    /* The .map-legend / .legend__* component now lives in app.css (loaded by BOTH
       the detail and standard diagram routes) so the two pages share one legend
       component. See "Map legend (shared)" there. */

    /* ── Zoom controls (editorial pill, bottom-left) ── */
    /* Zoom pill — position only; shared look lives in app.css (.app-zoom,
       .map-controls, #mtm-zoom-pill). Figma 89:3083 (grey, bottom-right, 36px). */
    .map-controls { position: absolute; bottom: 24px; right: 24px; }

    .tm-context-link {
      display: none;
    }
    .tm-pane-map.is-fullscreen .tm-context-link {
      display: inline-flex; align-items: center; gap: 4px;
      position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
      z-index: 8; font-size: 12.5px; font-weight: 500; color: var(--ink-inverse);
      white-space: nowrap; text-decoration: none; letter-spacing: normal;
      background: var(--ink); border: 1px solid var(--ink); border-radius: 999px;
      padding: 8px 16px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.18);
      transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s, transform 0.15s;
    }
    /* Floating "Back" pill shown after fly-to-monitoring. The look/hover/press come
       from the shared .app-pill.app-pill--ghost (the Previous control) — markup now
       carries those classes. Only the centred placement lives here; the :active
       scale re-applies translateX so the shared press scale stays centred, and
       [hidden] is restated because .app-pill's inline-flex would beat the attribute. */
    .tm-back-btn {
      position: absolute; top: 56px; left: 50%; transform: translateX(-50%); z-index: 9;
    }
    .tm-back-btn[hidden] { display: none; }
    .tm-back-btn:active { transform: translateX(-50%) scale(0.97); }

    .tm-pane-map.is-fullscreen .tm-context-link:hover {
      background: var(--ink);
      border-color: var(--ink);
      color: var(--ink-inverse);
      box-shadow: 0 4px 14px rgba(0,0,0,0.28);
      transform: translateX(-50%) translateY(-1px);
    }

    /* ── Search bar (explore mode) ───────────────────── */
    /* Top-RIGHT, in the band between the headline box (420px, left) and the TOC /
       search-results drawer (--toc-w, right): right-aligned and shrinking as the
       viewport narrows, so it neither overlays the title nor hides behind the
       drawer. Hidden below 1024px (desktop-only affordance). */
    /* Toolbar holds the machine-readable download button + the search bar as one
       right-anchored row in the band between the headline box (420px, left) and the
       TOC / results drawer (--toc-w, right). align-items:stretch makes the button the
       SAME height as the search box. */
    .tm-toolbar {
      position: absolute; top: 24px;               /* 24px below the header (stage starts at the header bottom) — matches the Contents toggle */
      left: 448px;                                 /* clear the 420px headline box + gap */
      right: calc(var(--toc-w, 300px) + 28px);     /* clear the TOC / results drawer + gap */
      display: flex; align-items: stretch; justify-content: flex-end; gap: 10px;
      z-index: 5;
    }
    /* Machine-readable download — white ghost pill matching .app-pill--ghost / the
       search field (white bg, thin neutral border, pill radius, same shadow). */
    .tm-mr-btn {
      display: inline-flex; align-items: center; gap: 7px; flex: none;
      padding: 8px 14px;
      border: 0.5px solid rgba(170,170,170,0.3); border-radius: var(--r-pill);
      background: var(--surface-white); color: var(--ink);
      font-family: "PP",-apple-system,sans-serif; font-size: 12.5px; font-weight: 500;
      letter-spacing: 0.02em; line-height: 1; text-decoration: none; white-space: nowrap;
      cursor: pointer; box-shadow: var(--shadow-pill);
      transition: border-color 0.16s var(--ease-out), box-shadow 0.16s var(--ease-out), background 0.16s var(--ease-out);
    }
    .tm-mr-btn:hover { border-color: rgba(120,120,120,0.55); background: var(--surface-bg); }
    .tm-mr-btn:active { transform: scale(0.97); }
    .tm-mr-btn svg { width: 13px; height: 13px; display: block; }
    /* Search is now a flex child of .tm-toolbar (relative anchors the clear-× inside).
       Compact at rest (256px); expands to 320px on focus, growing leftward. */
    .tm-search {
      position: relative;
      flex: 0 1 256px; max-width: 256px;
      transition: max-width 0.22s var(--ease-out), flex-basis 0.22s var(--ease-out);
    }
    .tm-search:focus-within { max-width: 320px; flex-basis: 320px; }
    @media (max-width: 1024px) { .tm-toolbar { display: none; } }
    /* top:24px is consistent across every breakpoint (the base rule already sets it),
       so the search keeps a 24px gap to the header and stays top-aligned with the
       Contents toggle (which also sits 24px below the header) at 1024–1080. */
    /* Search input echoes the ghost pill (the Previous button): white, thin
       neutral border, pill radius, same shadow; neutral (not mint) focus. */
    .tm-search-input {
      width: 100%; padding: 8px 38px 8px 16px;   /* right room for the × button */
      border: 0.5px solid rgba(170,170,170,0.3); border-radius: var(--r-pill);
      /* Explicit PP — form controls don't reliably inherit font-family, so the
         search field matches the pills/back-button instead of the UA default. */
      font-size: 12.5px; font-weight: 500; letter-spacing: 0.02em;
      font-family: "PP",-apple-system,sans-serif; color: var(--ink);
      background: var(--surface-white);
      outline: none; box-sizing: border-box;
      transition: border-color 0.16s var(--ease-out), box-shadow 0.16s var(--ease-out);
      box-shadow: var(--shadow-pill);
    }
    .tm-search-input:hover { border-color: rgba(120,120,120,0.55); }
    /* Active state = ONE border: the input's own border turns dark. The shared
       a11y outline ring is suppressed here because, offset outside this pill, it
       read as a second (double) border. The dark border is the focus indicator. */
    .tm-search-input:focus { border-color: var(--ink); }
    /* Keyboard focus: keep the single dark border (no offset outline → no double
       border), but add a soft shadow ring OUTSIDE the pill so keyboard users get a
       clear focus affordance the mouse-focus dark border alone doesn't provide. */
    .tm-search-input:focus-visible { outline: none; box-shadow: var(--shadow-pill), 0 0 0 3px rgba(26,26,26,0.14); }
    .tm-search-input::placeholder { color: #9a9a9a; font-weight: 500; }  /* match the pill labels (500) so the empty-state label reads like the other UI text */
    /* Clear (×) button — only shown when the field has text (toggled in JS). */
    .tm-search-clear {
      position: absolute; top: 50%; right: 8px; transform: translateY(-50%);
      width: 22px; height: 22px; padding: 0; border: none; border-radius: 999px;
      display: flex; align-items: center; justify-content: center;
      background: transparent; color: var(--ink-muted); cursor: pointer;
      transition: background 0.13s var(--ease-out), color 0.13s var(--ease-out),
                  transform 0.12s var(--ease-out);
    }
    .tm-search-clear:hover { background: rgba(0,0,0,0.06); color: var(--ink-2); }
    /* press feedback — keep the translateY(-50%) centring while compressing */
    .tm-search-clear:active { transform: translateY(-50%) scale(0.9); }
    .tm-search-clear[hidden] { display: none; }
    /* Reveal: when JS un-hides the × (field gained text) it scales + fades in
       rather than popping. Plays each time display flips from none. */
    @keyframes tm-clear-in {
      from { opacity: 0; transform: translateY(-50%) scale(0.8); }
      to   { opacity: 1; transform: translateY(-50%) scale(1); }
    }
    .tm-search-clear:not([hidden]) { animation: tm-clear-in 0.15s var(--ease-out); }
    .tm-search-clear svg { width: 11px; height: 11px; display: block; }

    /* ── Search results sidebar ───────────────────────── */
    .tm-search-panel {
      position: absolute; top: 0; right: 0; bottom: 0; width: 300px;  /* match .tm-mit-panel */
      overflow-y: auto; padding: 24px 20px; box-sizing: border-box;
    }
    /* Same white-card look as the map nodes (Figma 72:4415 / image ref): subtle
       border, soft --card-shadow, 8px radius, and a 3px hover lift — so every white
       card on the page reads as one component. */
    .tm-search-result {
      padding: 10px 12px; font-size: 11.5px; font-weight: 400; cursor: pointer;
      /* exact map-node white-card tokens so it themes identically (light + dark) */
      border: 1px solid var(--diag-action-border); border-radius: 8px;
      background: var(--diag-action-fill); box-shadow: var(--card-shadow);
      color: var(--text); line-height: 1.4;
      transition: transform 0.16s var(--ease-out), box-shadow 0.16s var(--ease-out), border-color 0.16s var(--ease-out);
      margin-bottom: 8px; display: block;
    }
    .tm-search-result:hover { transform: translateY(var(--card-lift)); box-shadow: var(--card-shadow-hover); border-color: #c2c2c2; }
    /* No-match empty state — a quiet, oriented dead-end instead of the panel vanishing. */
    .tm-search-empty {
      padding: 14px 12px; font-size: 12px; font-weight: 400; line-height: 1.5;
      color: var(--ink-faint); text-align: center;
      animation: drawer-content-in 0.18s var(--ease-out);
    }
    .tm-search-badge { /* type from shared .eyebrow--sm */
      display: inline-block;
      padding: 1px 5px; border-radius: 3px; margin-right: 5px;
      vertical-align: middle; flex-shrink: 0;
    }
    .tm-search-badge--action     { background: var(--diag-action-fill); color: var(--diag-action-text); border: 1px solid var(--diag-action-border); }
    .tm-search-badge--state      { background: var(--diag-state-fill);  color: var(--diag-state-text); }
    .tm-search-badge--outcome    { background: var(--diag-key-fill);    color: var(--diag-key-text); }
    .tm-search-badge--mechanism  { background: var(--diag-blue-fill);   color: var(--diag-blue-text); } /* fallback */
    .tm-search-badge--mitigation { background: #D7F3EF; color: var(--mint-text); }

    /* ── Mitigation sidebar ─────────────────────────── */
    .tm-mit-panel {
      position: absolute; top: 0; right: 0; bottom: 0; width: 300px;
      overflow-y: auto; padding: 24px 20px; box-sizing: border-box;
    }
    /* Shared circular close button sits top-right of each drawer (look lives in
       components.css .app-drawer-close). */
    .tm-mit-panel > .app-drawer-close,
    .tm-search-panel > .app-drawer-close {
      position: absolute; top: 16px; right: 16px; z-index: 1;
    }
    .tm-mit-section { margin-bottom: 20px; }
    .tm-mit-section:last-child { margin-bottom: 0; }
    .tm-mit-section-label {
      font-size: 11px; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase;
      color: var(--ink-muted); margin-bottom: 6px; padding-bottom: 5px; border-bottom: 1px solid var(--border);
    }

    /* ── Mitigation bubble ──────────────────────────── */
    .tm-mit-bubble {
      position: absolute; display: none; width: 280px;
      background: var(--surface-white); border: 1px solid var(--border);
      border-radius: 10px;
      box-shadow: 0 8px 32px rgba(0,0,0,0.13), 0 2px 8px rgba(0,0,0,0.07);
      z-index: 20; padding: 16px 18px 18px; box-sizing: border-box;
      pointer-events: auto;
    }
    .tm-mit-bubble::before {
      content: ''; position: absolute; top: -7px; left: 50%;
      transform: translateX(-50%); width: 13px; height: 7px;
      background: var(--surface-white); clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
      border-left: 1px solid var(--border); border-right: 1px solid var(--border);
    }
    .tm-mit-eyebrow { color: var(--mint-deep); margin-bottom: 8px; } /* type from shared .eyebrow */
    .tm-mit-title   { font-size: 13.5px; font-weight: 500; line-height: 1.4; margin-bottom: 14px; padding-right: 20px; color: var(--ink); letter-spacing: 0.01em; }
    .tm-mit-close   { position: absolute; top: 10px; right: 10px; background: none; border: none; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--ink-faint); line-height: 1; transition: color 0.12s; padding: 2px 4px; }
    .tm-mit-close:hover { color: var(--ink); }
    .tm-mit-list    { list-style: none; padding: 0; margin: 0; }
    /* Rows cascade in (texts-reveal rhythm) each time the drawer renders — on open and
       on node→node swap. The mint → arrows arriving in sequence is the sweet detail. */
    .tm-mit-list li { display: flex; gap: 8px; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 500; line-height: 1.5; letter-spacing: 0.01em; color: var(--ink-body);
                      animation: drawer-content-in var(--dur-reveal) var(--ease-out) backwards;
                      animation-delay: calc(var(--stagger) * var(--i, 0)); }
    .tm-mit-list li:nth-child(1){--i:1} .tm-mit-list li:nth-child(2){--i:2}
    .tm-mit-list li:nth-child(3){--i:3} .tm-mit-list li:nth-child(4){--i:4}
    .tm-mit-list li:nth-child(5){--i:5} .tm-mit-list li:nth-child(n+6){--i:6}
    .tm-mit-list li:last-child { border-bottom: none; }
    .tm-mit-list li::before { content: '→'; color: var(--mint-deep); flex-shrink: 0; font-weight: 500; }
    .tm-mit-monitor-inner { display: flex; flex-direction: column; gap: 3px; }
    .tm-mit-monitor-link {
      font-size: 12.5px; font-weight: 500; color: var(--mint-deep);
      text-decoration: none; align-self: flex-start;
      transition: color 0.15s;
    }
    .tm-mit-monitor-link:hover { color: var(--teal-deep); text-decoration: underline; }
    #tm-mit-panel-content a { color: var(--mint-deep); text-decoration: underline; text-underline-offset: 2px; }
    #tm-mit-panel-content a:hover { color: var(--teal-deep); }
    #tm-mit-panel-content sup.tm-cite-sup,
    .tm-text-body sup.tm-cite-sup {
      font-size: 12px; font-weight: 500; line-height: 0; white-space: nowrap;
      margin-left: 1px;
    }
    #tm-mit-panel-content sup.tm-cite-sup a,
    .tm-text-body sup.tm-cite-sup a {
      color: var(--mint-deep);
      text-decoration: none;
      padding: 0 1px;
      font-weight: 500;
    }
    #tm-mit-panel-content sup.tm-cite-sup a:hover,
    .tm-text-body sup.tm-cite-sup a:hover { text-decoration: underline; }
    /* Body-article footnote markers use a deeper teal than the mint used
       in the mitigation panel — keeps in-prose citations from glowing. */
    .tm-text-body sup.tm-cite-sup a { color: var(--mint-text); }
    #tm-mit-panel-content .tm-cite-list,
    .tm-text-body .tm-cite-list {
      margin-top: 22px;
      padding-top: 12px;
      border-top: 1px solid var(--border, #E6E6E6);
    }
    #tm-mit-panel-content .tm-cite-list-head,
    .tm-text-body .tm-cite-list-head {
      font-size: 9.5px; font-weight: 600; letter-spacing: 0.2em;
      text-transform: uppercase; color: var(--ink-3); margin-bottom: 8px;
    }
    #tm-mit-panel-content .tm-cite-list ol,
    .tm-text-body .tm-cite-list ol {
      padding-left: 22px; margin: 0;
      font-size: 11.5px; font-weight: 400; line-height: 1.55; color: var(--ink-2);
    }
    #tm-mit-panel-content .tm-cite-list ol li { padding-left: 4px; margin-bottom: 4px; }
    #tm-mit-panel-content .tm-cite-list ol li a { color: var(--mint-text); }
    .tm-text-body .tm-cite-list ol { font-weight: 500; }
    .tm-text-body .tm-cite-list ol li { padding-left: 4px; margin-bottom: 10px; line-height: 1.6; }
    .tm-text-body .tm-cite-list ol li a { color: var(--mint-text); word-break: break-all; }
    .tm-mit-section--clickable {
      cursor: pointer; border-radius: 8px;
      padding: 10px 12px; margin: 0 -12px;
      transition: background 0.13s;
    }
    .tm-mit-section--clickable:hover { background: #ECF9F7; }
    .tm-mit-section--clickable:hover .tm-mit-section-label { color: var(--mint-text); }
    .tm-mit-section--clickable:hover .tm-mit-list li::before { color: var(--mint-text); }

    /* ── Path highlighting ──────────────────────────── */
    .map-node.node-dimmed { opacity: 0.13; pointer-events: none; }
    /* Non-related SECTION containers + labels fade with their dimmed nodes — group
       rects carry data-group-id (not data-node-id) so the per-node dim loops skip
       them; syncGroupDim() toggles this class instead. */
    .detail-node.group-dimmed { opacity: 0.13; }
    /* Active state: the selected card gets a crisp 2px dark border (was a 4px
       same-colour stroke that read as just a thicker outline) plus a soft elevation,
       matching the shared card system — an active card that opened its drawer reads
       as raised + ringed. */
    /* Active stays lifted (a pinned hover) — same counter-scaled rise as :hover. */
    .map-node.node-selected { transform: translateY(calc(-3px / var(--zoom-scale, 1))); }
    .map-node.node-selected rect {
      stroke: var(--diag-selection) !important; stroke-width: 2px !important;
      filter: drop-shadow(0 6px 10.5px rgba(16,24,40,0.10));   /* matched to the standard-view box-shadow (see hover note above) */
    }
    /* Keyboard focus on an interactive (mitigation-bearing) node: ring the rect,
       not the <g> — outline on an SVG group is unreliable across browsers. */
    .map-node:focus { outline: none; }
    .map-node:focus-visible { outline: none; }
    .map-node:focus-visible rect { stroke: var(--diag-selection) !important; stroke-width: 2px !important; }
    [data-edge-from].edge-dimmed { opacity: 0.06 !important; }
    /* On-path edges keep their OWN authored colour (grey #AAAAAA / red #cc3333) and
       their matching arrowhead — highlighting only un-dims them. (Previously forced
       green, which mismatched the grey arrowhead and recoloured grey-card edges.) */
    [data-edge-from].edge-on-path { opacity: 1 !important; }
    /* Flowing dashes along the SELECTED subgraph's required (red, AND-gate) edges only —
       marching ants signal the critical "both-required" chain. Scoped to on-path
       required edges (a handful), never all ~50. Reduced-motion: the global reset
       stops the loop (leaving a static dash, which still reads as "the critical link"). */
    @keyframes tm-edge-flow { to { stroke-dashoffset: -24; } }
    [data-edge-required].edge-on-path {
      stroke-dasharray: 7 5;
      animation: tm-edge-flow 0.9s linear infinite;
    }

    /* Pulse a node when it's jumped to via the search results — a soft white
       halo that fades. Subtle enough not to dominate, visible against the
       slightly-grey map background. */
    @keyframes tm-search-pulse {
      0%, 100% { filter: drop-shadow(0 0 0 rgba(255,255,255,0)); }
      50%      { filter: drop-shadow(0 0 12px rgba(255,255,255,0.95)); }
    }
    .map-node.tm-search-pulse { animation: tm-search-pulse 1.3s ease-in-out 2; }

    /* ── Text pane (below map) ──────────────────────── */
    .tm-pane-text {
      max-width: 720px;
      margin: 0 auto;
      padding: 40px 32px 32px;
    }
    .tm-text-header {
      margin-bottom: 28px;
      padding-bottom: 20px;
      border-bottom: 1px solid var(--border);
    }
    .tm-text-header .meta {
      font-size: 11px; font-weight: 500; letter-spacing: 0.18em;
      text-transform: uppercase; color: var(--purple, #8B5CF6); margin-bottom: 12px;
    }
    .tm-text-header h1 {
      font-size: var(--page-title); font-weight: 100; line-height: 1.1; letter-spacing: -0.01em;
      margin-bottom: 0;
    }
    .tm-text-body p {
      font-size: 17px; font-weight: 400; line-height: 1.7; color: var(--ink-body); margin-bottom: 1.1rem;
      letter-spacing: normal; /* 0.2% of font size — CSS letter-spacing has no % unit */
    }
    .tm-text-body p:last-child { margin-bottom: 0; }
    .tm-text-body cite { font-style: normal; color: var(--muted); font-size: 13px; font-weight: 400; }

    /* ── Mobile fallback (basic) — detail map controls ──────────────
       Stop the bottom controls overlapping at phone widths: prev/next stays the
       bottom-most row (app.css), legend + zoom stack in a row above it, and the
       search (a desktop power feature, and a head-overlap source) is hidden. The
       map stays pannable + pinch-zoomable. */
    @media (max-width: 600px) {
      .tm-toolbar { display: none; }
      .map-controls { bottom: 58px; right: 12px; }
      /* .map-legend mobile position is in app.css (shared with the standard view). */
    }
