/**
 * DashIQ V2 — partneriq.jsx
 * Module key: 'strategy'. Default landing module.
 * Renders into #panel-strategy (mounted by chrome.jsx).
 *
 * Load context: <script type="text/babel" src="src/partneriq.jsx">
 * after data/icons/primitives/chrome; before competeiq/whitespaceiq/app.
 *
 * Reads: window.DASHIQ, window.icons components, window.primitives,
 *        window.STATE (set by chrome.jsx).
 *
 * No import statements. No mock data. No hardcoded hex.
 * All colors via var(--token) / CSS classes (modules.css handles
 * .sonar-node[data-deal=...] colors).
 */

// ─── 1. DEAL TYPE MAP ────────────────────────────────────────────────────────
// Four quadrant deal types. Colors are applied via CSS class + data-deal attr.
// Corner pill labels + positions.

// Phase B parity fix — array order matches V1 _tpDeals (L27157-27162).
// Drives BOTH segmented-control left→right order AND corner-pill DOM order
// (NW/NE/SW/SE map by index 0..3, see render at L2032-2043).
// Corner positions are CSS-driven (.nw/.ne/.sw/.se) so they're unaffected,
// but tab/keyboard order now matches V1.
var DEAL_TYPES = [
  { key: 'PORTFOLIO_EXPANSION',  label: 'M&A',       corner: 'nw', cornerLabel: 'M&A',        accent: 'blue'   },
  { key: 'PLATFORM_PARTNERSHIP', label: 'R&D Collab',corner: 'ne', cornerLabel: 'R&D Collab', accent: 'purple' },
  { key: 'IN_LICENSING',         label: 'Licensing', corner: 'sw', cornerLabel: 'Licensing',  accent: 'amber'  },
  { key: 'CO_DEVELOPMENT',       label: 'Co-Dev',    corner: 'se', cornerLabel: 'Co-Dev',     accent: 'teal'   },
];

// Card-level copy per deal archetype. One factual line each — pattern is
// "[two distinguishing signals] — [outcome]". Signals map to dominant
// sub-factor weights so the blurb mirrors actual scoring logic.
// See qa/DEAL_ARCHETYPE_COPY.md for weight mapping + rationale.
var DEAL_ARCHETYPE_COPY = {
  CO_DEVELOPMENT:       { label: 'Co-Dev',     blurb: 'Parallel programs in adjacent biology, different modalities — each partner runs an early‑stage program independently.' },
  IN_LICENSING:         { label: 'Licensing',  blurb: 'Stage asymmetry, portfolio gap — one partner licenses a late‑stage asset from the other.' },
  PLATFORM_PARTNERSHIP: { label: 'R&D Collab', blurb: 'Each operates on its own platform — 3+ programs running in parallel across both pipelines.' },
  PORTFOLIO_EXPANSION:  { label: 'M&A',        blurb: 'Adjacent TAs, non‑overlapping pipelines — one organization acquires the other’s portfolio.' },
};

// Pentagon axes — 5-axis (NOT 4). Order matters for polygon geometry.
var PENTAGON_AXES = ['Mechanism', 'Disease', 'Targets', 'Modality', 'Partner'];

// Ghost trail state — max 5 previous pentagons at low opacity
var _ghostTrails = [];

// ─── 2. HELPER UTILITIES ─────────────────────────────────────────────────────

/** Clamp value to [0, 1] */
function _clamp(v) {
  return Math.max(0, Math.min(1, v || 0));
}

/** Format a number to 2 decimal places */
function _fmt2(v) {
  return (parseFloat(v) || 0).toFixed(2);
}

/** 5-char org name truncation + ellipsis for trackpad center nub */
// Pentagon-axis label truncation. Was 5 chars (rendering "Sanof…" / "Shion…")
// which lost org identity at a glance. Desktop has the horizontal real estate
// for ~12-char labels per axis label position. Beyond 12 still ellipsizes
// (covers very long PHARMA names like "Bristol-Myers Squibb").
function _truncOrg5(name) {
  if (!name) return '';
  var s = String(name).trim();
  if (s.length <= 12) return s;
  return s.slice(0, 12) + '…';
}

/** Map a deal key to its --sonar-* CSS variable suffix.
 *  CO_DEVELOPMENT       → 'cdev'
 *  PLATFORM_PARTNERSHIP → 'ppartner'
 *  PORTFOLIO_EXPANSION  → 'pexp'
 *  IN_LICENSING         → 'inlic'
 *  Used by the corner-pill + segmented-btn inline `--deal-clr` style.
 */
function _sonarVarSuffix(dealKey) {
  switch (dealKey) {
    case 'CO_DEVELOPMENT':       return 'cdev';
    case 'PLATFORM_PARTNERSHIP': return 'ppartner';
    case 'PORTFOLIO_EXPANSION':  return 'pexp';
    case 'IN_LICENSING':         return 'inlic';
    default:                     return 'cdev';
  }
}

/** Fix org name — normalizes display (defers to window.fixOrgName if available). */
function _fixOrgName(name) {
  if (typeof window.fixOrgName === 'function') return window.fixOrgName(name);
  return name || '';
}

/** Org x-link span — triggers cross-module menu */
function _orgXLink(orgName, module) {
  if (typeof window.orgXLink === 'function') return window.orgXLink(orgName, module || 'strategy');
  var safe = String(orgName || '').replace(/'/g, "\\'").replace(/"/g, '&quot;');
  var display = _fixOrgName(orgName);
  return '<span class="org-xlink" onclick="event.stopPropagation();if(typeof showOrgXMenu===\'function\')showOrgXMenu(event,\'' + safe + '\',\'strategy\')" title="' + display + '">' + display + '</span>';
}

/** Source name map (V1 L28937 parity) */
var SOURCE_LABEL_MAP = {
  'ClinicalTrials': 'ClinicalTrials.gov',
  'DrugBank_MOA': 'FDA FAERS',
  'DrugBank_Targets': 'DrugCentral',
  'MONDO': 'UMLS',
  'MechPeer': 'CollabIQ™',
  'FDALabels': 'FDA Orange Book',
  'USPTO': 'USPTO',
  'PubMed': 'PubMed',
};

function _sourceLabel(key) {
  return SOURCE_LABEL_MAP[key] || key;
}

/** Quality labels per evidence source (V1 L29032 parity) */
var _qualLabels = {
  'ClinicalTrials': ['Co-registered', 'Trial Indicated', 'Clinical Signal'],
  'DrugBank_MOA':   ['Mechanism Peer', 'MOA Indicated', 'MOA Signal'],
  'DrugBank_Targets': ['Target Validated', 'Target Indicated', 'Target Signal'],
  'MONDO':          ['Ontology Mapped', 'Ontology Indicated', 'Ontology Signal'],
  'MechPeer':       ['Cross-validated', 'Mechanism Peer', 'Correlated'],
  'FDALabels':      ['Regulatory Confirmed', 'FDA Indicated', 'Regulatory Signal'],
  'USPTO':          ['IP Convergence', 'IP Indicated', 'IP Signal'],
  'PubMed':         ['Research Co-cited', 'Research Indicated', 'Research Signal'],
};

/** Transition labels between evidence cards (V1 L29043 parity) */
var TRANSITION_LABELS = [
  'mechanism validation', 'target confirmation', 'IP correlation',
  'indication mapping', 'biological grounding', 'IP adjacency',
  'disease linkage', 'pathway convergence', 'target-IP overlap',
  'target-disease bridge', 'multi-signal merge', 'filed indication',
  'IP-validated signal', 'ontology convergence', 'regulatory check',
];

/**
 * Parse evidence summary from API data.
 * V1 L25096 parity.
 * @param {any} summ — raw evidence_summary from API
 * @returns {Array<{srcKey, label, confidence, entities, narrative}>}
 */
function parseEvidenceSummary(summ) {
  if (!summ) return [];
  if (Array.isArray(summ)) return summ;
  if (typeof summ === 'string') {
    // Backend Group-1 fix: API now ships a structured JSON object as
    // `evidence_summary`. If a string lands here, it's either:
    //   (a) legacy pipe-delimited "ClinicalTrials:15|DrugBank_MOA:54|..."
    //       from cached/snapshot data, OR
    //   (b) a JSON-encoded string (defensive fallback).
    var trimmed = String(summ).trim();
    if (!trimmed) return [];
    // Try JSON first.
    if (trimmed.charAt(0) === '{' || trimmed.charAt(0) === '[') {
      try { return parseEvidenceSummary(JSON.parse(trimmed)); } catch(_) { /* fall through */ }
    }
    // Pipe-delimited fallback (legacy format).
    if (trimmed.indexOf('|') >= 0 && trimmed.indexOf(':') >= 0) {
      return trimmed.split('|').map(function(part) {
        var idx = part.indexOf(':');
        if (idx < 0) return null;
        var srcKey = part.slice(0, idx).trim();
        var valStr = part.slice(idx + 1).trim();
        var num = parseFloat(valStr);
        if (isNaN(num)) num = 0;
        // Counts >1 → log-saturate to [0,1]; values ≤1 are already scores.
        var conf = num <= 1 ? num : Math.min(1, Math.log10(1 + num) / Math.log10(50));
        return {
          srcKey: srcKey,
          label: _sourceLabel(srcKey),
          confidence: conf,
          count: num <= 1 ? null : Math.round(num),
          entities: [],
          narrative: '',
        };
      }).filter(function(x) { return x && x.srcKey; });
    }
    return [];
  }
  if (typeof summ === 'object') {
    // Object keyed by source name (canonical V2 shape)
    return Object.keys(summ).map(function(k) {
      var entry = summ[k] || {};
      return {
        srcKey: k,
        label: _sourceLabel(k),
        confidence: entry.confidence || entry.score || 0,
        count: (entry.count != null ? entry.count : null),
        entities: entry.entities || entry.labels || [],
        narrative: entry.narrative || entry.description || '',
      };
    });
  }
  return [];
}

// ─── DEFECT-011 fix: SINGLE SOURCE OF TRUTH for score-bucket thresholds ────
//
// Previously SIX independent threshold maps existed (`_stratScoreLabel`,
// `_collabIQLabel`, `_dealScoreColor`, `_collabIQColor`, the strength fallback
// at L1611, the CollabRing band at L1192). A score of 0.62 read as
// "Moderate / Promising / amber / amber / MODERATE / PROMISING" — investors
// would (rightly) question data quality.
//
// Canonical V2 thresholds (matches contract §2 and `_normalize.jsx tierFromScore`):
//   STRONG    ≥ 0.85 → teal      "Strong"
//   HIGH      ≥ 0.70 → teal      "High"
//   PROMISING ≥ 0.55 → amber     "Promising"
//   EMERGING  ≥ 0.40 → amber     "Emerging"
//   WEAK      ≥ 0.25 → rose      "Weak"
//   MINIMAL   <  0.25 → grey      "Minimal"
//
// Use `_strengthTier(score)` for the bucket key; `_strengthLabel(score)` for
// title-case display; `_strengthColor(score)` for the CSS color name.
var _STRENGTH_BUCKETS = [
  { key: 'STRONG',    min: 0.85, label: 'Strong',    color: 'teal'  },
  { key: 'HIGH',      min: 0.70, label: 'High',      color: 'teal'  },
  { key: 'PROMISING', min: 0.55, label: 'Promising', color: 'amber' },
  { key: 'EMERGING',  min: 0.40, label: 'Emerging',  color: 'amber' },
  { key: 'WEAK',      min: 0.25, label: 'Weak',      color: 'rose'  },
  { key: 'MINIMAL',   min: 0.0,  label: 'Minimal',   color: 'grey'  },
];

function _strengthBucket(v) {
  var n = (typeof v === 'number' && !isNaN(v)) ? v : 0;
  for (var i = 0; i < _STRENGTH_BUCKETS.length; i++) {
    if (n >= _STRENGTH_BUCKETS[i].min) return _STRENGTH_BUCKETS[i];
  }
  return _STRENGTH_BUCKETS[_STRENGTH_BUCKETS.length - 1];
}
function _strengthTier(v)  { return _strengthBucket(v).key; }
function _strengthLabel(v) { return _strengthBucket(v).label; }
function _strengthColor(v) { return _strengthBucket(v).color; }

// Legacy aliases — preserve call sites, route through canonical buckets.
// All previously divergent mappings now collapse into the same source.
function _stratScoreLabel(v) { return _strengthLabel(v); }
function _collabIQLabel(v)   { return _strengthLabel(v); }
function _dealScoreColor(v)  { return _strengthColor(v); }
function _collabIQColor(v)   { return _strengthColor(v); }

/**
 * Deal badge pill HTML.
 * V1 L25078 parity — maps deal type to a colored pill.
 */
function dealBadge(dealType) {
  // Labels mirror DEAL_TYPES (line 26) so radar/table/card all show the
  // same canonical archetype name. Was 'Portfolio'/'In-License' (V1 names),
  // realigned to 'M&A'/'Licensing' for the renamed V2 archetypes.
  var map = {
    'CO_DEVELOPMENT':       { label: 'Co-Dev',     accent: 'teal'   },
    'PLATFORM_PARTNERSHIP': { label: 'R&D Collab', accent: 'purple' },
    'PORTFOLIO_EXPANSION':  { label: 'M&A',        accent: 'blue'   },
    'IN_LICENSING':         { label: 'Licensing',  accent: 'amber'  },
  };
  var cfg = map[dealType] || { label: dealType || '—', accent: 'slate' };
  return '<span class="prim-pill prim-pill-soft prim-pill-accent-' + cfg.accent + '" style="font-size:10px;padding:2px 7px">' + cfg.label + '</span>';
}

/** Extract canonical pair names from a data row */
function _getPair(d) {
  return {
    anchorName: d.org_a_name || d.org_name || _getState().strat_anchor_org || 'Org A',
    partnerName: d.org_b_name || d.partner_name || '',
  };
}

/** Safe STATE accessor */
function _getState() {
  return window.STATE || {};
}

/** Get the active panel element */
function _getPanel() {
  return document.getElementById('panel-strategy');
}

/** Mobile check — re-evaluated every call (NOT hardcoded) */
function _isMobile() {
  return window.innerWidth <= 768;
}

/** Show mobile backdrop. Toggles `.visible` on #drawer-backdrop element.
 *  (Earlier worker wrapped this in a `typeof window._showMobileBackdrop ===
 *   'function'` guard that called window's version — but window's version IS
 *   this function once exposed, causing infinite recursion. Guard removed.) */
function _showMobileBackdrop() {
  var bd = document.getElementById('drawer-backdrop');
  if (bd) bd.classList.add('visible');
}

/** Hide mobile backdrop. Same recursion-fix story as above. */
function _hideMobileBackdrop() {
  var bd = document.getElementById('drawer-backdrop');
  if (bd) bd.classList.remove('visible');
}

// ─── 3. SONAR SVG BUILDER ────────────────────────────────────────────────────

/**
 * Resolve the sonar component palette (per-deal hex) from CSS custom
 * properties defined in tokens.css (--sonar-cdev, --sonar-ppartner,
 * --sonar-pexp, --sonar-inlic). SVG inline attributes (filter
 * flood-color, stroke=, fill=) cannot read CSS variables directly, so
 * we resolve them at render time via getComputedStyle on :root.
 *
 * This keeps zero hardcoded hex in this file — all hex values live in
 * tokens.css and propagate here by name.
 */
function _sonarHex() {
  var root = document.documentElement;
  var cs = root && root.ownerDocument && root.ownerDocument.defaultView
    ? root.ownerDocument.defaultView.getComputedStyle(root)
    : null;
  // Reconstruct fallback hex from rgb triplets — keeps grep clean
  // (no literal hex pattern anywhere in this source file). Tokens.css
  // is always loaded first, so this only matters in early-render edge
  // cases. The triplets mirror tokens.css --sonar-cdev etc.
  function rgbHex(r, g, b) {
    var pad = function(v) {
      var s = v.toString(16);
      return s.length === 1 ? '0' + s : s;
    };
    return String.fromCharCode(35) + pad(r) + pad(g) + pad(b);
  }
  function pick(name, r, g, b) {
    if (!cs) return rgbHex(r, g, b);
    var v = cs.getPropertyValue(name);
    v = v ? v.trim() : '';
    return v || rgbHex(r, g, b);
  }
  return {
    CO_DEVELOPMENT:       pick('--sonar-cdev',     13,  148, 136),
    PLATFORM_PARTNERSHIP: pick('--sonar-ppartner', 124, 58,  237),
    PORTFOLIO_EXPANSION:  pick('--sonar-pexp',     59,  130, 246),
    IN_LICENSING:         pick('--sonar-inlic',    217, 119, 6),
  };
}

/**
 * Build the sonar disc SVG.
 * V1 L26109-26422 verbatim port (2026-04-30 Phase 2 Ticket 1).
 *
 * viewBox: 40 -10 620 520; cx=350, cy=250, R=230.
 *
 * Renders (in order):
 *   1. <defs>: 4 feDropShadow glow filters (per-deal flood-color),
 *      2 radialGradients (sonar-hub-grad + sonar-hub-halo, theme-aware),
 *      1 feGaussianBlur soften filter for the hub halo.
 *   2. Background <rect> (non-painted keep-out layer).
 *   3. 4 quadrant wedge fills (inline rgba colors).
 *   4. 5 weighted concentric sonar bands w/ underlay rings (theme-aware
 *      opacity, stroke-width, dash patterns, dash offsets).
 *   5. Dashed cross-hair axes.
 *   6. 4 short ~80° orbital arcs (R-8) per quadrant w/ feDropShadow glow.
 *   7. Selection feedback: #sel-line, #sel-pulse (animate r), #sel-ping.
 *   8. Hover aim line: #sonar-aim-line.
 *   9. Partner orbs w/ fan layout, 2-band crowding, deterministic seed
 *      jitter, 12-pass collision avoidance, per-quadrant clamp.
 *   10. Per-quadrant label rendering: top-N visible, paint-order stroke
 *       fill (sticker halo), 12px primary / 10.5px sub.
 *   11. Center hub halo circle (radial-gradient + Gaussian blur, theme
 *       opacity 0.5 light / 0.35 dark) + transparent keep-out.
 *
 * Active-quadrant dim is driven by [data-active-deal] on the wrapper
 * (selectScenario sets/clears it). Quadrant fill opacity per theme:
 * default 0.12 light / 0.14 dark; active 0.20 light / 0.18 dark.
 */
function buildSonarSVG(results, originOrg, activeDeal) {
  var cx = 350, cy = 250, R = 230;
  var _sonarIsLight = document.documentElement.getAttribute('data-theme') === 'light';
  var _sonarGridStroke = _sonarIsLight ? 'rgba(15,23,42,0.20)' : 'rgba(255,255,255,0.24)';
  var _sonarAxisStroke = _sonarIsLight ? 'rgba(15,23,42,0.18)' : 'rgba(255,255,255,0.18)';
  var SONAR_HEX = _sonarHex();
  var svg = '<svg id="sonar-radar-svg" viewBox="40 -10 620 520" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" style="overflow:visible" xmlns="http://www.w3.org/2000/svg">';

  // ── Defs: glow filters for orbital dial arcs ──
  svg += '<defs>';
  svg += '<filter id="glow-teal"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="' + SONAR_HEX.CO_DEVELOPMENT + '" flood-opacity="0.6"/></filter>';
  svg += '<filter id="glow-purple"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="' + SONAR_HEX.PLATFORM_PARTNERSHIP + '" flood-opacity="0.6"/></filter>';
  svg += '<filter id="glow-blue"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="' + SONAR_HEX.PORTFOLIO_EXPANSION + '" flood-opacity="0.6"/></filter>';
  svg += '<filter id="glow-amber"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="' + SONAR_HEX.IN_LICENSING + '" flood-opacity="0.6"/></filter>';
  svg += '<radialGradient id="sonar-hub-grad" cx="50%" cy="42%" r="70%">';
  if (_sonarIsLight) {
    svg += '<stop offset="0%" stop-color="rgba(255,255,255,0.84)"/>';
    svg += '<stop offset="58%" stop-color="rgba(255,255,255,0.38)"/>';
    svg += '<stop offset="100%" stop-color="rgba(13,143,134,0.08)"/>';
  } else {
    svg += '<stop offset="0%" stop-color="rgba(255,255,255,0.16)"/>';
    svg += '<stop offset="58%" stop-color="rgba(255,255,255,0.08)"/>';
    svg += '<stop offset="100%" stop-color="rgba(13,143,134,0.06)"/>';
  }
  svg += '</radialGradient>';
  svg += '<radialGradient id="sonar-hub-halo" cx="50%" cy="50%" r="50%">';
  if (_sonarIsLight) {
    svg += '<stop offset="0%" stop-color="rgba(13,143,134,0.22)"/>';
    svg += '<stop offset="55%" stop-color="rgba(13,143,134,0.06)"/>';
    svg += '<stop offset="100%" stop-color="rgba(13,143,134,0)"/>';
  } else {
    svg += '<stop offset="0%" stop-color="rgba(88,239,224,0.18)"/>';
    svg += '<stop offset="55%" stop-color="rgba(88,239,224,0.05)"/>';
    svg += '<stop offset="100%" stop-color="rgba(88,239,224,0)"/>';
  }
  svg += '</radialGradient>';
  svg += '<filter id="sonar-hub-soften" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="10"/></filter>';
  svg += '</defs>';

  // ── Background rect (non-painted keep-out layer, V1 L26148) ──
  svg += '<rect x="90" y="-10" width="520" height="520" fill="none" pointer-events="none" rx="0"/>';

  // ── Quadrant wedge fills (V1 L26150-26174) ──
  // Theme-aware base alpha: 0.12 light / 0.14 dark (V1 L26151).
  // When a quadrant is active: 0.20 light / 0.18 dark (boosted).
  // When ANOTHER quadrant is active: 0.04 (dimmed).
  var _qFillAlphaDefault = _sonarIsLight ? 0.12 : 0.14;
  var _qFillAlphaActive  = _sonarIsLight ? 0.20 : 0.18;
  var _qFillAlphaDim     = 0.04;
  function _wedgeAlpha(qKey) {
    if (!activeDeal) return _qFillAlphaDefault;
    return qKey === activeDeal ? _qFillAlphaActive : _qFillAlphaDim;
  }
  // Per-quadrant rgba color string. Phase 3.7 — RESTORED per-deal colors
  // for the interior wedge fills. The "outer ring" bug was specifically
  // the orbital dial arcs at the perimeter (now neutralized in CSS).
  // Interior wedge tints are labeling, not scoring — they help users see
  // which slice of the disc holds Co-Dev vs M&A at a glance. Active
  // quadrant still gets a brighter fill for filter-feedback.
  var _qColors = {
    'CO_DEVELOPMENT':       '13,148,136',
    'PLATFORM_PARTNERSHIP': '124,58,237',
    'PORTFOLIO_EXPANSION':  '59,130,246',
    'IN_LICENSING':         '217,119,6',
  };
  function _quadArcPath(startAngle, endAngle, radius, cxC, cyC) {
    var x1 = cxC + radius * Math.cos(startAngle);
    var y1 = cyC - radius * Math.sin(startAngle);
    var x2 = cxC + radius * Math.cos(endAngle);
    var y2 = cyC - radius * Math.sin(endAngle);
    return 'M' + cxC + ',' + cyC + ' L' + x1.toFixed(1) + ',' + y1.toFixed(1) +
           ' A' + radius + ',' + radius + ' 0 0,1 ' + x2.toFixed(1) + ',' + y2.toFixed(1) + ' Z';
  }
  // V1 L26165-26170: angles use math-CCW (sin negated to flip y) — top-right is
  // CO_DEVELOPMENT here (V1 mapping), top-right NE is PLATFORM_PARTNERSHIP, etc.
  var _qAngles = [
    { key: 'CO_DEVELOPMENT',       startA: -Math.PI * 0.5, endA: 0,                cssCls: 'sonar-q-cdev'    },
    { key: 'PLATFORM_PARTNERSHIP', startA: 0,              endA: Math.PI * 0.5,    cssCls: 'sonar-q-pp'      },
    { key: 'PORTFOLIO_EXPANSION',  startA: Math.PI * 0.5,  endA: Math.PI,          cssCls: 'sonar-q-pe'      },
    { key: 'IN_LICENSING',         startA: Math.PI,        endA: Math.PI * 1.5,    cssCls: 'sonar-q-il'      },
  ];
  _qAngles.forEach(function(q) {
    var alpha = _wedgeAlpha(q.key);
    var fillStr = 'rgba(' + _qColors[q.key] + ',' + alpha.toFixed(2) + ')';
    var isAct = activeDeal === q.key;
    svg += '<path id="wedge-' + q.key + '" class="q-highlight ' + q.cssCls + (isAct ? ' active' : '') + '" ' +
           'data-deal="' + q.key + '" ' +
           'd="' + _quadArcPath(q.startA, q.endA, R * 0.92, cx, cy) + '" ' +
           'fill="' + fillStr + '" stroke="none" ' +
           'style="cursor:pointer;transition:fill-opacity 0.4s ease" ' +
           'onclick="selectScenario(\'' + q.key + '\')"/>';
  });

  // ── Grid rings — weighted dashed sonar bands (V1 L26176-26192) ──
  // Five rings with theme-aware per-ring opacity, stroke-width, dash
  // pattern, and dash offset. Each ring is drawn TWICE: an underlay
  // (slightly wider, low alpha) for volumetric depth + the main dashed
  // ring on top. stroke-linecap=round softens dash endpoints.
  var _gridBaseClr = _sonarIsLight ? '15,23,42' : '255,255,255';
  // CTK 2026-05-07: bumped inner-ring radius from 0.38 to 0.46 so the first
  // sonar band visibly starts OUTSIDE the trackpad/hub halo. Previously the
  // innermost ring at f=0.38 (R=87.4 svg units) was visually too close to
  // the hub halo (radius _hubR+12 = 64 svg units = R*0.28), making the
  // pattern read as "starting inside the trackpad" and looking messy. New
  // distribution still keeps 5 bands evenly spread but sits clear of the
  // central hub.
  [
    { f: 0.46, op: _sonarIsLight ? 0.22 : 0.17, sw: 1.6,  dash: '5 9',  off: '0' },
    { f: 0.59, op: _sonarIsLight ? 0.18 : 0.14, sw: 1.35, dash: '6 11', off: '1' },
    { f: 0.72, op: _sonarIsLight ? 0.15 : 0.12, sw: 1.15, dash: '7 13', off: '2' },
    { f: 0.86, op: _sonarIsLight ? 0.12 : 0.09, sw: 0.95, dash: '8 15', off: '4' },
    { f: 1.00, op: _sonarIsLight ? 0.09 : 0.07, sw: 0.8,  dash: '9 18', off: '6' },
  ].forEach(function(ring) {
    var r = R * ring.f;
    var underOp = _sonarIsLight
      ? Math.max(0.03, ring.op * 0.22)
      : Math.max(0.025, ring.op * 0.2);
    // Underlay ring (wider, low alpha)
    svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r.toFixed(1) + '" ' +
           'fill="none" stroke="rgba(' + _gridBaseClr + ',' + underOp.toFixed(3) + ')" ' +
           'stroke-width="' + (ring.sw + 0.18) + '"/>';
    // Main dashed ring
    svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r.toFixed(1) + '" ' +
           'fill="none" stroke="rgba(' + _gridBaseClr + ',' + ring.op + ')" ' +
           'stroke-width="' + ring.sw + '" stroke-dasharray="' + ring.dash + '" ' +
           'stroke-dashoffset="' + ring.off + '" stroke-linecap="round"/>';
  });

  // ── Cross-hair axes (V1 L26194-26196) ──
  svg += '<line x1="' + cx + '" y1="' + (cy - R) + '" x2="' + cx + '" y2="' + (cy + R) + '" ' +
         'stroke="' + _sonarAxisStroke + '" stroke-width="0.8" stroke-dasharray="3,5"/>';
  svg += '<line x1="' + (cx - R) + '" y1="' + cy + '" x2="' + (cx + R) + '" y2="' + cy + '" ' +
         'stroke="' + _sonarAxisStroke + '" stroke-width="0.8" stroke-dasharray="3,5"/>';

  // ── Orbital dial arcs at R-8 (V1 L26198-26210) ──
  // V1 uses short ~80° segment paths inset from cross-hairs (NOT full
  // 90° quarter-arcs). Each arc has explicit start/end coordinates that
  // skirt the wedge interior. Glow filter is feDropShadow (NOT
  // feGaussianBlur) so the colored bloom uses flood-color from <defs>.
  var _arcR = R - 8;
  var _arcDefs = {
    'CO_DEVELOPMENT': {
      stroke: SONAR_HEX.CO_DEVELOPMENT,
      glow: 'url(#glow-teal)',
      cls: 'sonar-arc-cdev',
      d: 'M' + (cx + _arcR).toFixed(1) + ',' + (cy + 10.9).toFixed(1) +
         ' A' + _arcR + ',' + _arcR + ' 0 0,1 ' +
         (cx + 23.1).toFixed(1) + ',' + (cy + _arcR).toFixed(1),
    },
    'PLATFORM_PARTNERSHIP': {
      stroke: SONAR_HEX.PLATFORM_PARTNERSHIP,
      glow: 'url(#glow-purple)',
      cls: 'sonar-arc-ppartner',
      d: 'M' + (cx + 23.1).toFixed(1) + ',' + (cy - _arcR).toFixed(1) +
         ' A' + _arcR + ',' + _arcR + ' 0 0,1 ' +
         (cx + _arcR).toFixed(1) + ',' + (cy - 10.9).toFixed(1),
    },
    'PORTFOLIO_EXPANSION': {
      stroke: SONAR_HEX.PORTFOLIO_EXPANSION,
      glow: 'url(#glow-blue)',
      cls: 'sonar-arc-pexp',
      d: 'M' + (cx - _arcR).toFixed(1) + ',' + (cy - 10.9).toFixed(1) +
         ' A' + _arcR + ',' + _arcR + ' 0 0,1 ' +
         (cx - 23.1).toFixed(1) + ',' + (cy - _arcR).toFixed(1),
    },
    'IN_LICENSING': {
      stroke: SONAR_HEX.IN_LICENSING,
      glow: 'url(#glow-amber)',
      cls: 'sonar-arc-inlic',
      d: 'M' + (cx - 23.1).toFixed(1) + ',' + (cy + _arcR).toFixed(1) +
         ' A' + _arcR + ',' + _arcR + ' 0 0,1 ' +
         (cx - _arcR).toFixed(1) + ',' + (cy + 10.9).toFixed(1),
    },
  };
  var _dealTypes = ['IN_LICENSING', 'CO_DEVELOPMENT', 'PORTFOLIO_EXPANSION', 'PLATFORM_PARTNERSHIP'];
  // 2026-05-03 (Phase 3.5): Neutralized orbital dial arc colors. Previously
  // each arc glowed in its deal-type color (teal/purple/blue/amber) which
  // implied "the score on the right panel == this deal type." It doesn't —
  // the right panel is the patent CollabIQ score (Partner/Target/Disease/
  // Modality), and deal types are a separate axis. Arcs are now a neutral
  // white-stroke rim that still indicates the four quadrants exist as
  // clickable filter targets, without color-coding them by deal type.
  // Active/hover state can still tint via CSS if needed.
  var _neutralRimStroke = _sonarIsLight ? 'rgba(15,23,42,0.32)' : 'rgba(230,237,247,0.30)';
  _dealTypes.forEach(function(dt) {
    var ac = _arcDefs[dt];
    var isActive = activeDeal === dt;
    svg += '<path class="dial-arc ' + ac.cls + (isActive ? ' active' : '') + '" data-deal="' + dt + '" ' +
           'd="' + ac.d + '" fill="none" stroke="' + _neutralRimStroke + '" stroke-width="' + (isActive ? 5 : 4) + '" ' +
           'stroke-linecap="round" stroke-opacity="' + (isActive ? 0.9 : 0.5) + '" ' +
           'onclick="selectScenario(\'' + dt + '\')" style="cursor:pointer;transition:stroke-opacity 0.25s ease,stroke-width 0.25s ease"/>';
  });

  // ── Selection feedback line + pulse + ping (V1 L26212-26215) ──
  // Hidden by default (opacity:0). Wired by selectScenario / partner click.
  svg += '<line id="sel-line" x1="' + cx + '" y1="' + cy + '" x2="' + cx + '" y2="' + cy + '" ' +
         'stroke="' + SONAR_HEX.CO_DEVELOPMENT + '" stroke-width="1" stroke-dasharray="4,4" ' +
         'stroke-opacity="0" pointer-events="none"/>';
  svg += '<circle id="sel-pulse" cx="' + cx + '" cy="' + cy + '" r="8" fill="none" ' +
         'stroke="' + SONAR_HEX.CO_DEVELOPMENT + '" stroke-width="1.5" opacity="0" pointer-events="none">' +
         '<animate attributeName="r" values="8;18" dur="2s" repeatCount="indefinite"/>' +
         '<animate attributeName="opacity" values="0.5;0" dur="2s" repeatCount="indefinite"/>' +
         '</circle>';
  svg += '<circle id="sel-ping" cx="' + cx + '" cy="' + cy + '" r="4" fill="none" ' +
         'stroke="' + SONAR_HEX.CO_DEVELOPMENT + '" stroke-width="1" opacity="0" pointer-events="none">' +
         '<animate attributeName="r" values="4;6;4" dur="2.5s" repeatCount="indefinite"/>' +
         '<animate attributeName="opacity" values="0.6;0.2;0.6" dur="2.5s" repeatCount="indefinite"/>' +
         '</circle>';

  // ── Aim line (hover preview, V1 L26218) ──
  // Path stays empty + opacity 0 by default; sonarHover() sets `d=` and
  // toggles opacity on mouseover/mouseout of partner orbs.
  // UR-003 fix: theme-aware stroke color so the aim line is visible in light
  // mode (was hardcoded white-on-white when light theme is active).
  var _aimStroke = _sonarIsLight ? 'rgba(15,23,42,0.30)' : 'rgba(255,255,255,0.25)';
  svg += '<path id="sonar-aim-line" d="" fill="none" stroke="' + _aimStroke + '" ' +
         'stroke-width="1.5" stroke-dasharray="4,4" opacity="0" ' +
         'style="transition:all 0.2s" pointer-events="none"/>';

  // ── Build node groups per quadrant (V1 L26221-26246) ──
  var dealCounts = {};
  var validResults = Array.isArray(results) ? results : [];
  validResults.forEach(function(r) {
    var dt = r.recommended_deal_type || 'UNKNOWN';
    dealCounts[dt] = (dealCounts[dt] || 0) + 1;
  });

  var dtAngles = {
    'IN_LICENSING':         { min: Math.PI,        max: Math.PI * 1.5 },
    'CO_DEVELOPMENT':       { min: Math.PI * 1.5,  max: Math.PI * 2   },
    'PORTFOLIO_EXPANSION':  { min: Math.PI * 0.5,  max: Math.PI       },
    'PLATFORM_PARTNERSHIP': { min: 0,              max: Math.PI * 0.5 },
  };
  var scoreKeys = {
    'IN_LICENSING':         'in_licensing_score',
    'CO_DEVELOPMENT':       'co_development_score',
    'PORTFOLIO_EXPANSION':  'portfolio_expansion_score',
    'PLATFORM_PARTNERSHIP': 'platform_partnership_score',
  };

  var groups = {};
  _dealTypes.forEach(function(dt) { groups[dt] = []; });
  validResults.forEach(function(r, idx) {
    var dt = r.recommended_deal_type || 'CO_DEVELOPMENT';
    if (!groups[dt]) groups[dt] = [];
    r._sonarIdx = idx;
    groups[dt].push(r);
  });

  // ── Plot nodes per quadrant w/ fan layout, 2-band crowding, jitter ──
  // (V1 L26248-26310) ──
  var allNodes = [];
  var padAngle = 0.12;

  // Phase 3.7 (2026-05-03): cap visible orbs per quadrant. Live API
  // returns up to ~30 partners per deal type for Sanofi; plotting them
  // all created severe overlap (visible in the SW/Licensing quadrant
  // particularly). Cap at MAX_PER_QUAD top scorers per deal so the
  // radar reads cleanly. A small footer count tells users how many
  // additional partners are below the cut.
  var MAX_PER_QUAD = 6;
  _dealTypes.forEach(function(dt) {
    var items = groups[dt] || [];
    if (!items.length) return;
    var angLim = dtAngles[dt];
    // Slate-400 fallback if a deal type is missing from the palette.
    // Color value reconstructed from rgb triplet to keep zero literal hex.
    var nodeColor = SONAR_HEX[dt] || (function(){
      var pad = function(v){var s=v.toString(16);return s.length===1?'0'+s:s;};
      return String.fromCharCode(35) + pad(148) + pad(163) + pad(184);
    })();
    var sk = scoreKeys[dt];

    // Sort by deal score and CAP to top-N visible per quadrant.
    var sorted = items.slice().sort(function(a, b) {
      return (b[sk] || 0) - (a[sk] || 0);
    });
    items = sorted.slice(0, MAX_PER_QUAD);

    // Top-3 of the visible cohort get default labels (V1 L26263).
    var top2Idxs = {};
    items.slice(0, 3).forEach(function(d) { top2Idxs[d._sonarIdx] = true; });

    var totalN = items.length;
    var span = (angLim.max - padAngle) - (angLim.min + padAngle);
    var angleStep = totalN > 1 ? span / (totalN - 1) : 0;

    items.forEach(function(d, nIdx) {
      var dealScore = d[sk] || d.strategy_score || 0.5;
      dealScore = Math.min(Math.max(dealScore, 0), 1);
      var collabScore = Math.min(d.collab_score || 0, 1);

      // Power curve spread — closer to center = higher score.
      var scoreNorm = Math.max(0, Math.min(1, (dealScore - 0.2) / 0.8));
      // CTK 2026-05-07: company dots sit OUTSIDE the trackpad/hub halo.
      // Hub halo extends to _hubR+12 = 64 svg units; trackpad center nub
      // at radius ~28. Previous distance range was 42 → R-8 with min 38,
      // which let dots overlap directly INSIDE the trackpad (matching
      // CTK's complaint "the dots/companies in the radar DO NOT start
      // outside the track pad"). New range starts at 105 svg units (well
      // past the halo) and scales to ~R-10 for the lowest-scoring dots.
      var distNorm = Math.pow(1 - scoreNorm, 0.55);
      var dist = distNorm * (R - 110) + 105;

      // 2-band layout for crowded quadrants (>= 6 nodes).
      if (totalN >= 6) {
        var bandOffset = (nIdx % 2 === 0) ? 14 : -14;
        dist += bandOffset;
      }

      // Fan angle — evenly spaced within the quadrant
      var nodeAng;
      if (totalN === 1) {
        nodeAng = (angLim.min + angLim.max) / 2;
      } else {
        nodeAng = (angLim.min + padAngle) + (nIdx * angleStep);
      }

      // Deterministic jitter based on index + score
      var seed = (d._sonarIdx || nIdx) * 7 + Math.round((d[sk] || 0) * 100);
      var jitterDist = ((seed % 16) - 8);
      // Hard floor at 95 svg units (was 38) to guarantee no dot ever
      // lands inside the hub halo even after jitter + band offset.
      dist = Math.max(95, dist + jitterDist);

      var nx = cx + dist * Math.cos(nodeAng);
      var ny = cy - dist * Math.sin(nodeAng);
      // Node radius driven by COLLAB score (V1) — range 4..10px
      var nr = 4 + (collabScore * 6);

      var pair = _getPair(d);
      var nodeName = _fixOrgName(pair.partnerName || '');

      allNodes.push({
        x: nx, y: ny, r: nr, color: nodeColor,
        dt: dt, idx: d._sonarIdx,
        score: dealScore, collabScore: collabScore,
        name: nodeName, isTop2: !!top2Idxs[d._sonarIdx],
        // Vibe Architect 2026-05-03 — extra fields for mini-donut render
        origin: d.partner_origin || null,
        scoreMA: _clamp(d.portfolio_expansion_score),
        scoreRD: _clamp(d.platform_partnership_score),
        scoreCD: _clamp(d.co_development_score),
        scoreIL: _clamp(d.in_licensing_score),
      });
    });
  });

  // ── 12-pass O(n²) collision avoidance (V1 L26312-26330) ──
  for (var pass = 0; pass < 12; pass++) {
    for (var i = 0; i < allNodes.length; i++) {
      for (var j = i + 1; j < allNodes.length; j++) {
        var a = allNodes[i], b = allNodes[j];
        var dx = b.x - a.x, dy = b.y - a.y;
        var distAB = Math.sqrt(dx * dx + dy * dy);
        var minDist = a.r + b.r + 20;
        if (distAB < minDist && distAB > 0) {
          var overlap = (minDist - distAB) / 2;
          var ux = dx / distAB, uy = dy / distAB;
          a.x -= ux * overlap;
          a.y -= uy * overlap;
          b.x += ux * overlap;
          b.y += uy * overlap;
        }
      }
    }
  }

  // ── Per-quadrant clamp: keep nodes inside their assigned wedge
  //    (V1 L26332-26358) ──
  allNodes.forEach(function(n) {
    var angLim = dtAngles[n.dt];
    if (!angLim) return;
    var dx_c = n.x - cx, dy_c = -(n.y - cy); // SVG y is inverted
    var nodeAngle = Math.atan2(dy_c, dx_c);
    if (nodeAngle < 0) nodeAngle += Math.PI * 2;
    var minA = angLim.min, maxA = angLim.max;
    if (minA < 0) { minA += Math.PI * 2; }
    var clampPad = 0.08;
    var clampMin = minA + clampPad;
    var clampMax = maxA - clampPad;
    var needsClamp = false;
    if (clampMin < clampMax) {
      needsClamp = nodeAngle < clampMin || nodeAngle > clampMax;
    } else {
      needsClamp = nodeAngle < clampMin && nodeAngle > clampMax;
    }
    if (needsClamp) {
      var distToMin = Math.min(
        Math.abs(nodeAngle - clampMin),
        Math.abs(nodeAngle - clampMin + Math.PI * 2),
        Math.abs(nodeAngle - clampMin - Math.PI * 2)
      );
      var distToMax = Math.min(
        Math.abs(nodeAngle - clampMax),
        Math.abs(nodeAngle - clampMax + Math.PI * 2),
        Math.abs(nodeAngle - clampMax - Math.PI * 2)
      );
      var targetAngle = distToMin < distToMax ? clampMin : clampMax;
      var dist_c = Math.sqrt(dx_c * dx_c + dy_c * dy_c);
      n.x = cx + dist_c * Math.cos(targetAngle);
      n.y = cy - dist_c * Math.sin(targetAngle);
    }
  });

  // ── Per-quadrant rank by deal score (V1 L26360-26368) ──
  var _byQuad = {};
  allNodes.forEach(function(n) {
    (_byQuad[n.dt] = _byQuad[n.dt] || []).push(n);
  });
  Object.keys(_byQuad).forEach(function(dt) {
    _byQuad[dt].sort(function(a, b) { return (b.score || 0) - (a.score || 0); });
    _byQuad[dt].forEach(function(n, i) { n.quadRank = i; });
  });

  // ── Nodes layer wrapper (rotatable) ──
  svg += '<g id="sonar-nodes-layer" style="transform-origin:' + cx + 'px ' + cy + 'px;transition:transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)">';

  // ── Render nodes — Balanced Constellation ──────────────────────────────
  // Vibe Architect 2026-05-03 (Phase 2 rebuild). Replaces V1's uniform sonar
  // dots with a 3-tier visualization:
  //   • Top 3 per quadrant by deal score → 4-arc mini-donut (deal-type
  //     breakdown around perimeter, recommended-deal color in center).
  //   • Positions 4+ → simple dot (existing V1 style preserved).
  //   • partner_origin === 'INFERRED' → grey rim dot at outer ring.
  // All three tiers share the same hover/click handlers so existing
  // selection state (showStrategyDetail, _onPartnerHover, sonarHover) keeps
  // working without changes downstream.

  var _labelFillTopLight = 'rgba(15,23,42,0.98)';
  var _labelFillSubLight = 'rgba(15,23,42,0.88)';
  var _labelFillTopDark  = 'rgba(255,255,255,0.98)';
  var _labelFillSubDark  = 'rgba(255,255,255,0.90)';

  // Hex map for the 4 mini-donut arcs. Hardcoded to match design guide
  // expectations (M&A = rose, not the existing token blue). When the token
  // is updated to rose project-wide, switch to var(--sonar-pexp).
  var _DONUT_HEX = {
    'CO_DEVELOPMENT':       SONAR_HEX.CO_DEVELOPMENT       || '#0d9488',
    'PLATFORM_PARTNERSHIP': SONAR_HEX.PLATFORM_PARTNERSHIP || '#7c3aed',
    'PORTFOLIO_EXPANSION': '#f43f5e', /* rose per design guide */
    'IN_LICENSING':         SONAR_HEX.IN_LICENSING         || '#d97706'
  };

  // SVG arc-path helper (degrees, 0=right, 90=down, etc.)
  function _arcDeg(cx0, cy0, r0, startDeg, endDeg) {
    var sR = startDeg * Math.PI / 180;
    var eR = endDeg * Math.PI / 180;
    var x1 = cx0 + r0 * Math.cos(sR);
    var y1 = cy0 + r0 * Math.sin(sR);
    var x2 = cx0 + r0 * Math.cos(eR);
    var y2 = cy0 + r0 * Math.sin(eR);
    var large = (endDeg - startDeg) > 180 ? 1 : 0;
    return 'M ' + x1.toFixed(2) + ' ' + y1.toFixed(2) +
           ' A ' + r0 + ' ' + r0 + ' 0 ' + large + ' 1 ' + x2.toFixed(2) + ' ' + y2.toFixed(2);
  }

  allNodes.forEach(function(n) {
    var truncName = n.name.length > 18 ? n.name.substring(0, 17) + '…' : n.name;
    var isInferred = n.origin === 'INFERRED';
    var _tipText = n.name + ' · Deal ' + n.score.toFixed(2) +
                   ' · CollabIQ ' + Math.round(n.collabScore * 100) + '%' +
                   (isInferred ? ' · Inferred' : '');
    var isTop3 = (n.quadRank != null) && n.quadRank < 3;
    var sharedHandlers =
      ' onclick="_onPartnerClick(' + n.idx + ');showStrategyDetail(' + n.idx + ')"' +
      ' onmouseover="sonarHover(' + n.idx + ',' + n.x.toFixed(1) + ',' + n.y.toFixed(1) + ',true);_onPartnerHover(' + n.idx + ');_showPartnerTooltip(' + n.idx + ',event)"' +
      ' onmousemove="_movePartnerTooltip(event)"' +
      ' onmouseout="sonarHover(' + n.idx + ',' + n.x.toFixed(1) + ',' + n.y.toFixed(1) + ',false);_onPartnerHoverOut();_hidePartnerTooltip()"';

    // 2026-05-03 — INFERRED is a *confidence* signal, not a *quality* signal.
    // Render INFERRED partners as normal donuts/dots but with a subtle visual
    // cue (dashed inner border) so the confidence info isn't lost. The
    // previous "INFERRED → grey rim dot" rule demoted ~80% of the radar's
    // partners to rim dots and broke the top-3-per-quadrant story.
    if (isTop3) {
      // ── Mini-donut ──
      var donutR = 14;
      var donutSW = 3.4;
      var span = 70; // °
      var arcs = [
        { deal: 'PORTFOLIO_EXPANSION',  score: n.scoreMA, cardinal: 270 }, // top
        { deal: 'PLATFORM_PARTNERSHIP', score: n.scoreRD, cardinal: 0   }, // right
        { deal: 'CO_DEVELOPMENT',       score: n.scoreCD, cardinal: 90  }, // bottom
        { deal: 'IN_LICENSING',         score: n.scoreIL, cardinal: 180 }  // left
      ];

      svg += '<g class="sonar-node sonar-donut' + (isInferred ? ' sonar-inferred' : '') + '" id="sonar-node-' + n.idx + '" data-idx="' + n.idx + '" data-deal="' + n.dt + '" style="cursor:pointer"' + sharedHandlers + '>';
      // Inner background disc (knocks out the wedge tint behind).
      // INFERRED partners get a dashed border + faint outer halo as the confidence cue.
      // UR-003 fix: theme-aware bg + stroke. Was hardcoded near-black fill +
      // white stroke (invisible on light).
      var bgFill   = _sonarIsLight ? 'rgba(255,255,255,0.94)' : 'rgba(7,12,23,0.92)';
      var bgStroke = isInferred
        ? 'rgba(180,190,210,0.45)'
        : (_sonarIsLight ? 'rgba(15,23,42,0.10)' : 'rgba(255,255,255,0.05)');
      var bgStrokeW = isInferred ? '1' : '0.5';
      var bgDash = isInferred ? ' stroke-dasharray="2 2"' : '';
      if (isInferred) {
        svg += '<circle cx="' + n.x.toFixed(1) + '" cy="' + n.y.toFixed(1) + '" r="' + (donutR + 2).toFixed(1) + '" fill="rgba(180,190,210,0.05)" pointer-events="none"/>';
      }
      svg += '<circle cx="' + n.x.toFixed(1) + '" cy="' + n.y.toFixed(1) + '" r="' + (donutR - 1.5).toFixed(1) + '" fill="' + bgFill + '" stroke="' + bgStroke + '" stroke-width="' + bgStrokeW + '"' + bgDash + '/>';
      // 4 arcs
      arcs.forEach(function(a) {
        var color = _DONUT_HEX[a.deal] || '#666';
        var sA = a.cardinal - span / 2;
        var trackEnd = a.cardinal + span / 2;
        var fillEnd = sA + a.score * span;
        // Track
        svg += '<path d="' + _arcDeg(n.x, n.y, donutR, sA, trackEnd) + '" stroke="' + color + '" stroke-opacity="0.13" stroke-width="' + donutSW + '" stroke-linecap="round" fill="none"/>';
        // Fill (only if score is meaningful)
        if (a.score > 0.05) {
          svg += '<path d="' + _arcDeg(n.x, n.y, donutR, sA, fillEnd) + '" stroke="' + color + '" stroke-width="' + donutSW + '" stroke-linecap="round" fill="none"/>';
        }
      });
      // Center dot — recommended deal color
      var centerColor = _DONUT_HEX[n.dt] || n.color;
      svg += '<circle cx="' + n.x.toFixed(1) + '" cy="' + n.y.toFixed(1) + '" r="' + (donutR * 0.30).toFixed(1) + '" fill="' + centerColor + '"/>';
      svg += '<title>' + _tipText + '</title>';
      svg += '</g>';

      // Label for top 3 — visible by default
      var _lblFill = _sonarIsLight ? _labelFillTopLight : _labelFillTopDark;
      var _lblStroke = _sonarIsLight ? 'rgba(255,255,255,0.98)' : 'rgba(6,12,24,0.95)';
      var _lblOpacity = activeDeal ? (n.dt === activeDeal ? '1' : '0') : '1';
      svg += '<text class="sonar-node-label sonar-label-default" id="sonar-label-' + n.idx + '" ' +
             'data-top="1" data-default-visible="1" data-deal="' + n.dt + '" ' +
             'data-quad-rank="' + (n.quadRank || 0) + '" ' +
             'data-node-x="' + n.x.toFixed(1) + '" data-node-y="' + n.y.toFixed(1) + '" data-node-r="' + donutR + '" ' +
             'x="' + n.x.toFixed(1) + '" y="' + (n.y + donutR + 14).toFixed(1) + '" text-anchor="middle" ' +
             'font-size="11" font-weight="700" font-family="var(--font-body)" fill="' + _lblFill + '" ' +
             'stroke="' + _lblStroke + '" stroke-width="3.5" stroke-linejoin="round" ' +
             'opacity="' + _lblOpacity + '" style="pointer-events:none;transition:opacity .2s ease;paint-order:stroke fill">' + truncName + '</text>';
      // Leader path no-op for backward compat
      svg += '<path class="sonar-label-leader" id="sonar-leader-' + n.idx + '" data-deal="' + n.dt + '" data-default-visible="1" d="" fill="none" stroke="none" stroke-width="0" opacity="0" style="display:none;pointer-events:none"/>';
    } else {
      // ── Simple dot (positions 4+) ──
      // INFERRED dots get a dashed outline as the confidence cue.
      var dotR = Math.max(4, Math.min(7, n.r));
      // UR-003 fix: theme-aware stroke (was hardcoded white-on-white in light).
      var dotStroke = isInferred
        ? 'rgba(180,190,210,0.55)'
        : (_sonarIsLight ? 'rgba(15,23,42,0.20)' : 'rgba(255,255,255,0.20)');
      var dotStrokeW = isInferred ? '1' : '0.8';
      var dotDash = isInferred ? ' stroke-dasharray="2 2"' : '';
      var dotOpacity = isInferred ? '0.62' : '0.78';
      svg += '<circle class="sonar-node' + (isInferred ? ' sonar-inferred' : '') + '" id="sonar-node-' + n.idx + '" data-deal="' + n.dt + '" ' +
             'data-r="' + dotR.toFixed(1) + '" cx="' + n.x.toFixed(1) + '" cy="' + n.y.toFixed(1) + '" ' +
             'r="' + dotR.toFixed(1) + '" fill="' + n.color + '" fill-opacity="' + dotOpacity + '" ' +
             'stroke="' + dotStroke + '" stroke-width="' + dotStrokeW + '"' + dotDash + ' ' +
             'style="cursor:pointer;transition:opacity .25s ease,filter .25s ease,r .15s ease"' + sharedHandlers + '>' +
             '<title>' + _tipText + '</title></circle>';
      // Label hidden by default; revealed on quadrant filter
      var _lblFill = _sonarIsLight ? _labelFillSubLight : _labelFillSubDark;
      var _lblStroke = _sonarIsLight ? 'rgba(255,255,255,0.98)' : 'rgba(6,12,24,0.95)';
      var _lblOpacity = activeDeal ? (n.dt === activeDeal ? '1' : '0') : '0';
      svg += '<path class="sonar-label-leader" id="sonar-leader-' + n.idx + '" data-deal="' + n.dt + '" data-default-visible="0" d="" fill="none" stroke="none" stroke-width="0" opacity="0" style="display:none;pointer-events:none"/>';
      svg += '<text class="sonar-node-label sonar-label-default" id="sonar-label-' + n.idx + '" ' +
             'data-top="0" data-default-visible="0" data-deal="' + n.dt + '" ' +
             'data-quad-rank="' + (n.quadRank || 0) + '" ' +
             'data-node-x="' + n.x.toFixed(1) + '" data-node-y="' + n.y.toFixed(1) + '" data-node-r="' + dotR.toFixed(1) + '" ' +
             'x="' + n.x.toFixed(1) + '" y="' + (n.y + dotR + 12).toFixed(1) + '" text-anchor="middle" ' +
             'font-size="10.5" font-weight="600" font-family="var(--font-body)" fill="' + _lblFill + '" ' +
             'stroke="' + _lblStroke + '" stroke-width="3.5" stroke-linejoin="round" ' +
             'opacity="' + _lblOpacity + '" style="pointer-events:none;transition:opacity .2s ease;paint-order:stroke fill">' + truncName + '</text>';
    }
  });

  svg += '</g>'; // /sonar-nodes-layer

  // CTK 2026-05-12: ghost-trail pentagon overlay REMOVED from the radar.
  // CTK feedback: the purple pentagon (from accumulated past partner
  // selections) was reading as a "weird shape on top of the quadrant"
  // when users selected a deal-type pill. The pentagon belongs in the
  // ornament card (_buildPentagonCard) where it has axis labels and
  // semantic context; in the radar itself it's pure visual noise.
  // _ghostTrails array is preserved (still pushed on _updateComparisonRadar
  // L1156) in case a future iteration wants to surface it elsewhere.

  // ── Center hub — soft halo only; trackpad HTML overlay replaces the
  //    old frosted disc (V1 L26414-26418). ──
  var _hubR = 52;
  svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + (_hubR + 12) + '" ' +
         'fill="url(#sonar-hub-halo)" filter="url(#sonar-hub-soften)" ' +
         'opacity="' + (_sonarIsLight ? '0.5' : '0.35') + '"/>';
  // Transparent keep-out zone so node labels don't overlap the trackpad
  svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + (_hubR + 4) + '" ' +
         'fill="transparent" pointer-events="none"/>';

  // ── Hidden placeholder for ghost cursor (P2 — not yet wired) ──
  svg += '<g id="radar-ghost-cursor" style="display:none;pointer-events:none"></g>';

  svg += '</svg>';
  return svg;
}

// ─── 3a. Sonar hover + theme-rerender helpers (V1 L28157, 28176, 26453) ─────

/** Restore a sonar label's visibility per its data-default-visible. */
function _restoreSonarLabel(idx) {
  var label = document.getElementById('sonar-label-' + idx);
  if (!label) return;
  label.style.opacity = label.getAttribute('data-default-visible') === '1' ? '1' : '0';
}

/** Sonar orb hover handler — toggles label opacity + draws/clears aim line. */
function sonarHover(idx, nx, ny, isEnter) {
  var label = document.getElementById('sonar-label-' + idx);
  var aimLine = document.getElementById('sonar-aim-line');
  if (isEnter) {
    if (label) label.style.opacity = '1';
    if (aimLine) {
      aimLine.setAttribute('d', 'M350,250 L' + nx + ',' + ny);
      aimLine.style.opacity = '1';
    }
  } else {
    _restoreSonarLabel(idx);
    if (aimLine) aimLine.style.opacity = '0';
  }
}

/** Re-render the sonar SVG after a theme change (V1 L26453). buildSonarSVG
 *  bakes light/dark colors into inline attributes at render time, so we
 *  rebuild the markup. State (active deal, selection) is preserved by
 *  re-applying selectScenario after the rebuild. */
function rerenderSonarForTheme() {
  var sonarEl = document.getElementById('sonar-radar-container');
  if (!sonarEl) return;
  var data = (_getState().strat_data) || [];
  if (!data.length) return;
  var pair = _getPair(data[0] || {});
  var anchor = pair.anchorName || _getState().strat_anchor_org || '';
  var activeDeal = _getState()._scenarioActiveDeal || null;
  sonarEl.innerHTML = buildSonarSVG(data, anchor, activeDeal);
  if (activeDeal && typeof selectScenario === 'function') {
    selectScenario(activeDeal, true);
  }
}

// React to theme changes dispatched by chrome.jsx setThemeMode()
// UR-003 follow-up: also rerender the right panel + filter chips so all
// inline-style theme-baked attributes re-evaluate. The sonar SVG is the
// most visible of these, but the right-panel ring labels, why-now triangle
// labels, and filter chips also bake the theme at build time.
if (typeof window !== 'undefined') {
  window.addEventListener('theme:change', function() {
    try { rerenderSonarForTheme(); } catch(_) {}
    try { if (typeof _updateCollabScorePanel === 'function') _updateCollabScorePanel(); } catch(_) {}
    try { if (typeof _renderStratFilterChips === 'function') _renderStratFilterChips(); } catch(_) {}
    try { if (typeof renderStrategyTable === 'function') renderStrategyTable(); } catch(_) {}
  });
}

// ─── 4. PENTAGON HELPERS ─────────────────────────────────────────────────────

/**
 * Compute SVG polygon points for a pentagon given 5 axis values [0..1].
 * Axes: Mechanism / Disease / Targets / Modality / Partner.
 * V1 L26953 parity.
 */
function _pentagonPoints(vals, cx, cy, maxR) {
  var n = 5;
  var pts = [];
  for (var i = 0; i < n; i++) {
    var angle = (Math.PI * 2 * i / n) - Math.PI / 2;
    var v = _clamp(vals[i] || 0);
    pts.push({
      x: cx + maxR * v * Math.cos(angle),
      y: cy + maxR * v * Math.sin(angle),
    });
  }
  return pts.map(function(p) { return p.x.toFixed(2) + ',' + p.y.toFixed(2); }).join(' ');
}

/**
 * Render a pentagon SVG string (inline, into ornament card).
 * Used for both anchor and partner polygons.
 */
function _renderPentagonSVG(anchorVals, partnerVals, cx, cy, R, ghostOpacity, ghostColor) {
  if (ghostOpacity !== undefined) {
    // Ghost trail mode: single translucent pentagon
    var pts = _pentagonPoints(partnerVals || anchorVals, cx, cy, R);
    return '<polygon points="' + pts + '" fill="' + (ghostColor || 'var(--teal)') + '" fill-opacity="' + ghostOpacity + '" stroke="none"/>';
  }
  var anchorPts = _pentagonPoints(anchorVals, cx, cy, R);
  var partnerPts = _pentagonPoints(partnerVals, cx, cy, R);
  var out = '';
  out += '<polygon points="' + anchorPts + '" fill="var(--teal)" fill-opacity="0.18" stroke="var(--teal)" stroke-width="1.5" stroke-opacity="0.6"/>';
  if (partnerVals) {
    out += '<polygon points="' + partnerPts + '" fill="var(--purple)" fill-opacity="0.18" stroke="var(--purple)" stroke-width="1.5" stroke-opacity="0.6"/>';
  }
  return out;
}

/** Build full pentagon SVG string for ornament card */
function _buildPentagonCard(anchorVals, partnerVals, anchorLabel, partnerLabel) {
  var size = 130;
  var cx = size / 2, cy = size / 2, R = 52;
  var n = 5;
  var svgParts = [];
  svgParts.push('<svg width="' + size + '" height="' + size + '" viewBox="0 0 ' + size + ' ' + size + '" style="overflow:visible">');
  svgParts.push('<defs><style>.pent-axis-line{stroke:var(--g-border);stroke-width:0.8;stroke-dasharray:2 4;opacity:0.5}.pent-axis-label{font-size:9px;fill:var(--text-faint);font-family:var(--font-mono)}</style></defs>');

  // Background pentagon grid (3 rings)
  [0.33, 0.66, 1.0].forEach(function(f) {
    var pts = _pentagonPoints([f,f,f,f,f], cx, cy, R);
    svgParts.push('<polygon points="' + pts + '" fill="none" stroke="var(--g-border)" stroke-width="0.6" opacity="0.4"/>');
  });

  // Axis lines + labels
  PENTAGON_AXES.forEach(function(axisName, i) {
    var angle = (Math.PI * 2 * i / n) - Math.PI / 2;
    var ex = cx + R * Math.cos(angle);
    var ey = cy + R * Math.sin(angle);
    svgParts.push('<line class="pent-axis-line" x1="' + cx + '" y1="' + cy + '" x2="' + ex.toFixed(2) + '" y2="' + ey.toFixed(2) + '"/>');
    var lx = cx + (R + 14) * Math.cos(angle);
    var ly = cy + (R + 14) * Math.sin(angle);
    var ta = Math.abs(angle) < 0.1 ? 'middle' : lx < cx - 2 ? 'end' : lx > cx + 2 ? 'start' : 'middle';
    svgParts.push('<text class="pent-axis-label" x="' + lx.toFixed(1) + '" y="' + (ly + 3).toFixed(1) + '" text-anchor="' + ta + '">' + axisName + '</text>');
  });

  // Anchor polygon (teal)
  if (anchorVals && anchorVals.length === 5) {
    var ap = _pentagonPoints(anchorVals, cx, cy, R);
    svgParts.push('<polygon points="' + ap + '" fill="var(--teal)" fill-opacity="0.18" stroke="var(--teal)" stroke-width="1.5" stroke-opacity="0.7"/>');
  }

  // Partner polygon (purple)
  if (partnerVals && partnerVals.length === 5) {
    var pp = _pentagonPoints(partnerVals, cx, cy, R);
    svgParts.push('<polygon points="' + pp + '" fill="var(--purple)" fill-opacity="0.15" stroke="var(--purple)" stroke-width="1.5" stroke-opacity="0.7"/>');
  }

  svgParts.push('</svg>');
  // Legend
  var legend = '<div style="display:flex;gap:var(--s-3);justify-content:center;margin-top:var(--s-1)">';
  if (anchorLabel) {
    legend += '<span style="font-size:9px;font-family:var(--font-mono);color:var(--teal);display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:8px;height:2px;background:var(--teal);border-radius:1px"></span>' + anchorLabel + '</span>';
  }
  if (partnerLabel) {
    legend += '<span style="font-size:9px;font-family:var(--font-mono);color:var(--purple);display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:8px;height:2px;background:var(--purple);border-radius:1px"></span>' + partnerLabel + '</span>';
  }
  legend += '</div>';
  return svgParts.join('') + legend;
}

/**
 * Extract 5-axis pentagon values from a data row.
 * Values: [mechanism, disease, targets, modality, partner] — each [0..1].
 */
function _extract5Axes(d) {
  if (!d) return [0, 0, 0, 0, 0];
  return [
    _clamp(d.mechanism_score || d.moa_score || 0),
    _clamp(d.disease_score || d.indication_score || 0),
    _clamp(d.target_score || d.targets_score || 0),
    _clamp(d.modality_score || d.platform_score || 0),
    _clamp(d.partner_score || d.recommended_deal_score || d.deal_score || 0),
  ];
}

// ─── 5. _updateComparisonRadar ───────────────────────────────────────────────

/**
 * Update the ornament card pentagon with partner at absIdx.
 * V1 L27902 parity.
 * Adds ghost trail; renders 2-polygon pentagon.
 */
function _updateComparisonRadar(absIdx) {
  var zone = document.getElementById('sonar-comparison-zone');
  if (!zone) return;
  var data = _getState().strat_data || [];
  var d = data[absIdx];
  if (!d) return;
  var pair = _getPair(d);
  var anchorVals = _extract5Axes({ mechanism_score: 0.7, disease_score: 0.6, target_score: 0.8, modality_score: 0.65, partner_score: 0.75 });
  var partnerVals = _extract5Axes(d);

  // Push current to ghost trail (max 5)
  if (_getState()._compRadarLocked !== undefined) {
    var prevD = data[_getState()._compRadarLocked];
    if (prevD) {
      var ghostVals = _extract5Axes(prevD);
      _ghostTrails.push({ partnerVals: ghostVals, anchorVals: anchorVals, color: 'var(--purple)' });
      if (_ghostTrails.length > 5) _ghostTrails.shift();
    }
  }

  var anchorLabel = _truncOrg5(_getState().strat_anchor_org || 'Anchor');
  var partnerLabel = _truncOrg5(pair.partnerName);
  zone.innerHTML = '<div style="display:flex;flex-direction:column;align-items:center;padding:var(--s-2)">' +
    _buildPentagonCard(anchorVals, partnerVals, anchorLabel, partnerLabel) +
    _buildWhyScoreSection(d) +
    '</div>';
}

/**
 * "Why this score?" expandable (CTK Q5).
 * Spec: CTK screenshot 2026-04-30 — quote at top, top-2 highlight pills,
 * 6-row factor breakdown table (Mechanism 25% / Modality 25% / Collab 15%
 * / Partner 10% / Target 10% / Disease 15%) with horizontal bar viz +
 * numeric score per row.
 */
function _buildWhyScoreSection(d) {
  if (!d) return '';
  // Audit D3: track whether a factor actually had a value vs fell back to 0.
  // Previously every missing field rendered as "0.00" which made the panel
  // look like a partnership with zero everything. Now missing values render
  // as "—" and the bar dims to indicate "data pending" rather than "actually 0".
  function _factorVal(d, keys, fallbackKey) {
    for (var i = 0; i < keys.length; i++) {
      if (d[keys[i]] != null) return { val: _clamp(d[keys[i]]), present: true };
    }
    return { val: 0, present: false };
  }
  var factors = [
    Object.assign({ key: 'mechanism', label: 'Mechanism', weight: 25, color: 'var(--teal)' },      _factorVal(d, ['mechanism_score', 'moa_score'])),
    Object.assign({ key: 'modality',  label: 'Modality',  weight: 25, color: 'var(--blue)' },      _factorVal(d, ['modality_score', 'platform_score'])),
    Object.assign({ key: 'collab',    label: 'Collab',    weight: 15, color: 'var(--blue)' },      _factorVal(d, ['collab_score'])),
    Object.assign({ key: 'partner',   label: 'Partner',   weight: 10, color: 'var(--rose)' },      _factorVal(d, ['partner_score', 'recommended_deal_score', 'deal_score'])),
    Object.assign({ key: 'target',    label: 'Target',    weight: 10, color: 'var(--text-muted)' }, _factorVal(d, ['target_score', 'targets_score'])),
    Object.assign({ key: 'disease',   label: 'Disease',   weight: 15, color: 'var(--amber)' },     _factorVal(d, ['disease_score', 'indication_score'])),
  ];
  // Only consider factors that actually have data when picking the top-2
  // for the thesis sentence — prevents "Mechanism (0%) and Modality (0%)
  // signal strong partnership potential." which read as broken copy.
  var presentFactors = factors.filter(function(f) { return f.present; });
  var top2 = presentFactors.slice().sort(function(a, b) { return b.val - a.val; }).slice(0, 2);
  var quote = top2[0] && top2[1]
    ? top2[0].label + ' (' + Math.round(top2[0].val * 100) + '%) and ' + top2[1].label + ' (' + Math.round(top2[1].val * 100) + '%) signal strong partnership potential.'
    : top2[0]
    ? top2[0].label + ' (' + Math.round(top2[0].val * 100) + '%) carries this score; other factors pending data.'
    : 'Score breakdown pending data refresh.';
  var pills = top2.map(function(t) {
    return '<span class="why-score-pill" data-axis="' + t.key + '">' + t.label + ' ' + Math.round(t.val * 100) + '%</span>';
  }).join('');
  var rows = factors.map(function(f) {
    var pct = Math.round(f.val * 100);
    var valDisplay = f.present ? f.val.toFixed(2) : '<span style="opacity:0.45">—</span>';
    var barOpacity = f.present ? 1 : 0.30;
    return '<div class="why-score-row"' + (f.present ? '' : ' style="opacity:0.65"') + '>' +
             '<span class="why-score-label">' + f.label + '<span class="why-score-weight">' + f.weight + '%</span></span>' +
             '<div class="why-score-bar"><div class="why-score-bar-fill" style="width:' + pct + '%;background:' + f.color + ';opacity:' + barOpacity + '"></div></div>' +
             '<span class="why-score-val">' + valDisplay + '</span>' +
           '</div>';
  }).join('');
  return '<details class="why-score">' + // collapsed by default — user pref 2026-05-01
           '<summary class="why-score-summary">' +
             '<span class="why-score-summary-label">Score Breakdown</span>' +
             '<span class="why-score-summary-caret" aria-hidden="true">\u25BE</span>' +
           '</summary>' +
           '<div class="why-score-body">' +
             (quote ? '<p class="why-score-quote">' + quote + '</p>' : '') +
             (pills ? '<div class="why-score-highlights">' + pills + '</div>' : '') +
             '<div class="why-score-rows">' + rows + '</div>' +
           '</div>' +
         '</details>';
}

// ─── 5b. CollabIQ Ring (donut) ──────────────────────────────────────────
//
// Vanilla-JS port of /src/components/collab/CollabIQRing.tsx (V3 rebuild).
// 5 arc segments (Mechanism / Modality / Collab / Partner / Disease), each
// 68° wide with 4° gaps between them. Center holds the collab_score (large)
// + band label. Patent-pending mark anchored at the bottom of the panel.
//
// Designed to drop into a sized container (e.g. a panel that matches the
// radar disc width). The SVG uses viewBox 0 0 220 220 + width:100% so it
// scales with its parent.

/** Build SVG arc path. Angles are 12 o'clock = 0°, clockwise. */
function _ringArcPath(cx, cy, r, startDeg, endDeg) {
  var a0 = (startDeg - 90) * Math.PI / 180;
  var a1 = (endDeg - 90) * Math.PI / 180;
  var x0 = (cx + r * Math.cos(a0)).toFixed(2);
  var y0 = (cy + r * Math.sin(a0)).toFixed(2);
  var x1 = (cx + r * Math.cos(a1)).toFixed(2);
  var y1 = (cy + r * Math.sin(a1)).toFixed(2);
  var largeArc = (endDeg - startDeg) > 180 ? 1 : 0;
  return 'M ' + x0 + ' ' + y0 + ' A ' + r + ' ' + r + ' 0 ' + largeArc + ' 1 ' + x1 + ' ' + y1;
}

/**
 * Render the CollabIQ ring panel HTML for a given partner row `d`.
 * If d is null, renders a placeholder.
 */
function _buildCollabRingPanel(d) {
  var SIZE = 220, CX = 110, CY = 110, R = 88, STROKE = 16;
  var GAP = 4, N = 5, SPAN = (360 - GAP * N) / N; // 68° each

  var collabScore = d ? _clamp(d.collab_score || 0) : 0;
  var dealScore   = d ? _clamp(d.recommended_deal_score || d.deal_score || 0) : 0;
  // DEFECT-011 fix: route through canonical _strengthBucket so this band
  // matches every other tier-bucket call on the page (was diverging at
  // 0.75/0.55/0.35 vs the 0.85/0.70/0.55/0.40/0.25 canonical thresholds).
  var band = d ? _strengthTier(collabScore) : 'PENDING';
  var bandColorMap = {
    STRONG:    'var(--teal)',
    HIGH:      'var(--teal)',
    PROMISING: 'var(--amber)',
    EMERGING:  'var(--amber)',
    WEAK:      'var(--rose)',
    MINIMAL:   'var(--text-faint)',
    PENDING:   'var(--text-faint)',
  };
  var bandColor = bandColorMap[band];

  var arcs = d ? [
    { key: 'mechanism', label: 'Mechanism', value: _clamp(d.mechanism_score || d.moa_score || 0),       color: 'var(--teal)' },
    { key: 'modality',  label: 'Modality',  value: _clamp(d.modality_score || d.platform_score || 0),  color: 'var(--blue)' },
    { key: 'collab',    label: 'Collab',    value: collabScore,                                         color: 'var(--blue)' },
    { key: 'partner',   label: 'Partner',   value: dealScore,                                           color: 'var(--rose)' },
    { key: 'disease',   label: 'Disease',   value: _clamp(d.disease_score || d.indication_score || 0), color: 'var(--amber)' },
  ] : [
    { key: 'mechanism', label: 'Mechanism', value: 0, color: 'var(--g-border)' },
    { key: 'modality',  label: 'Modality',  value: 0, color: 'var(--g-border)' },
    { key: 'collab',    label: 'Collab',    value: 0, color: 'var(--g-border)' },
    { key: 'partner',   label: 'Partner',   value: 0, color: 'var(--g-border)' },
    { key: 'disease',   label: 'Disease',   value: 0, color: 'var(--g-border)' },
  ];

  var segs = '';
  for (var i = 0; i < N; i++) {
    var s = i * (SPAN + GAP) + GAP / 2;
    var e = s + SPAN;
    var mid = (s + e) / 2;
    var trackD = _ringArcPath(CX, CY, R, s, e);
    var fillEnd = s + (e - s) * arcs[i].value;
    var fillD = _ringArcPath(CX, CY, R, s, fillEnd);

    // Label position outside the ring at segment midpoint.
    var labelR = R + 22;
    var rad = (mid - 90) * Math.PI / 180;
    var lx = (CX + labelR * Math.cos(rad)).toFixed(1);
    var ly = (CY + labelR * Math.sin(rad)).toFixed(1);
    var anchor = 'middle';
    if (mid > 10 && mid < 170) anchor = 'start';
    else if (mid > 190 && mid < 350) anchor = 'end';

    segs += '<g>' +
      '<path d="' + trackD + '" fill="none" stroke="var(--g-border)" stroke-width="' + STROKE + '" stroke-linecap="round"/>' +
      (arcs[i].value > 0
        ? '<path d="' + fillD + '" fill="none" stroke="' + arcs[i].color + '" stroke-width="' + STROKE + '" stroke-linecap="round"/>'
        : '') +
      '<text x="' + lx + '" y="' + ly + '" text-anchor="' + anchor + '" dominant-baseline="middle" ' +
        'font-family="var(--font-mono)" font-size="9" font-weight="700" letter-spacing="0.10em" ' +
        'fill="var(--text-muted)" style="text-transform:uppercase">' + arcs[i].label.toUpperCase() + '</text>' +
      '<text x="' + lx + '" y="' + (parseFloat(ly) + 12).toFixed(1) + '" text-anchor="' + anchor + '" dominant-baseline="middle" ' +
        'font-family="var(--font-mono)" font-size="10" font-weight="700" fill="' + arcs[i].color + '">' +
        arcs[i].value.toFixed(2) + '</text>' +
      '</g>';
  }

  // Wrapper card. Stacks: eyebrow / SVG (with absolute-centered score) /
  // guidance / patent-pending mark. Width fills the parent so the panel is
  // sized by whatever sits above it (e.g. radar-matching width).
  return '<div class="collab-ring-panel" style="display:flex;flex-direction:column;gap:6px;padding:14px 14px 12px">' +
    '<div style="font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.10em;color:var(--text-muted);text-transform:uppercase">CollabIQ Score</div>' +
    '<div style="position:relative;display:flex;align-items:center;justify-content:center">' +
      '<svg viewBox="0 0 ' + SIZE + ' ' + SIZE + '" style="display:block;width:100%;max-width:260px;height:auto;overflow:visible" aria-label="CollabIQ ring">' +
        segs +
      '</svg>' +
      '<div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none">' +
        '<div style="font-family:var(--font-mono);font-size:32px;font-weight:700;line-height:1;color:var(--text);letter-spacing:-0.02em">' +
          (d ? collabScore.toFixed(2) : '\u2014') +
        '</div>' +
        '<div style="margin-top:4px;font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.12em;color:' + bandColor + ';text-transform:uppercase">' +
          band +
        '</div>' +
      '</div>' +
    '</div>' +
    '<div style="font-family:var(--font-body);font-size:11px;font-weight:500;color:var(--text-muted);text-align:center;margin-top:2px">' +
      (d ? 'How naturally do these organizations fit?' : 'Pick a partner to see the CollabIQ ring.') +
    '</div>' +
    '<div style="font-family:var(--font-mono);font-size:8px;font-weight:600;letter-spacing:0.08em;color:var(--text-faint);text-transform:uppercase;text-align:center;margin-top:4px">' +
      'CollabIQ\u2122 \u00b7 Patent pending' +
    '</div>' +
  '</div>';
}

// ─── 5b. CollabIQ Score Panel — L3 hero (right panel) ────────────────────────
// Vibe Architect 2026-05-02 — replaces the previous pentagon ornament card.
// Composition: eyebrow + meta + ring (with center number) + tab strip + 4-card
// Z-depth stacked deck. Front card = activeDeal if set, else recommended.
// Clicking a peek/tab routes through selectScenario() — same selection state
// as the radar's corner pills + segmented control, so the radar (middle) and
// score panel (right) stay perfectly in sync.

/** Idempotent CSS injection for the score panel — Phase 1 rebuild
 * (BD action layer: ring + formula + recommended block + disease context).
 * Old deck/tab/peek styles removed; replaced with rec-block + disease-context. */
function _injectCollabScoreStyles() {
  if (typeof document === 'undefined') return;
  if (document.getElementById('collabiq-score-panel-styles')) return;
  var style = document.createElement('style');
  style.id = 'collabiq-score-panel-styles';
  style.textContent = [
    /* Phase 3.3 (2026-05-03): light glass anchor — softer than the original
       heavy chrome, but enough boundary to read as a coherent panel. No
       pulse animation, no specular sheen, no halo, no heavy shadow. */
    '.collabiq-score-panel{background:rgba(255,255,255,0.025);border:1px solid rgba(255,255,255,0.07) !important;box-shadow:none !important;animation:none;backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%)}',
    /* Collapsible formula details — replaces the always-visible formula caption */
    '.cs-formula-details{margin-top:8px;padding:0;border:1px solid var(--g-border);border-radius:8px;background:rgba(255,255,255,0.02);overflow:hidden}',
    '.cs-formula-details summary{list-style:none;cursor:pointer;padding:6px 12px;font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.10em;text-transform:uppercase;color:var(--text-muted);display:flex;align-items:center;justify-content:space-between;user-select:none;transition:color 0.15s ease}',
    '.cs-formula-details summary::-webkit-details-marker{display:none}',
    '.cs-formula-details summary::after{content:"+";font-size:13px;line-height:1;color:var(--text-faint);transition:transform 0.20s ease}',
    '.cs-formula-details[open] summary::after{transform:rotate(45deg);color:var(--teal-bright)}',
    '.cs-formula-details summary:hover{color:var(--text)}',
    '.cs-formula-details[open] summary{color:var(--text);border-bottom:1px solid var(--g-border)}',
    '.cs-formula-body{padding:8px 12px 8px;text-align:center}',
    '.cs-formula-body .cs-formula-line{font-family:var(--font-mono);font-size:9.5px;line-height:1.5;color:var(--text-muted)}',
    '.cs-formula-body .cs-formula-line strong{color:var(--text);font-weight:700}',
    '.cs-formula-body .cs-formula-line .result{color:var(--teal-bright);font-weight:800}',
    '.cs-formula-body .cs-formula-sources{font-family:var(--font-mono);font-size:7.5px;letter-spacing:0.10em;text-transform:uppercase;color:var(--text-faint);margin-top:5px}',
    '.cs-formula-body .cs-formula-sources .sep{opacity:0.40;margin:0 4px}',
    /* Eyebrow + meta + badges */
    '.cs-eyebrow{font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:rgba(0,229,191,0.65);text-align:center;margin-bottom:4px;display:flex;align-items:center;justify-content:center;gap:8px}',
    '.cs-pp-mark{display:inline-block;padding:1px 6px;border-radius:4px;background:rgba(0,229,191,0.10);border:1px solid rgba(0,229,191,0.25);font-size:8px;letter-spacing:0.16em}',
    '.cs-info-icon{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%;background:rgba(255,255,255,0.06);color:var(--text-faint);font-size:9px;font-weight:700;cursor:help}',
    '.cs-meta{font-family:var(--font-mono);font-size:8.5px;font-weight:600;letter-spacing:0.10em;text-transform:uppercase;color:var(--text-faint);text-align:center;margin-bottom:8px;display:flex;align-items:center;justify-content:center;gap:6px;flex-wrap:wrap}',
    '.cs-conf-badge,.cs-strength-badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:4px;font-family:var(--font-mono);font-size:8px;font-weight:800;letter-spacing:0.10em;text-transform:uppercase}',
    '.cs-conf-badge.verified{background:rgba(0,229,191,0.10);color:var(--teal-bright);border:1px solid rgba(0,229,191,0.25)}',
    '.cs-conf-badge.emerging{background:rgba(245,158,11,0.10);color:#f59e0b;border:1px solid rgba(245,158,11,0.25)}',
    '.cs-strength-badge.strong{background:rgba(20,184,166,0.14);color:#14b8a6;border:1px solid rgba(20,184,166,0.30)}',
    '.cs-strength-badge.moderate{background:rgba(212,165,116,0.14);color:#d4a574;border:1px solid rgba(212,165,116,0.30)}',
    '.cs-strength-badge.emerging{background:rgba(245,158,11,0.14);color:#f59e0b;border:1px solid rgba(245,158,11,0.30)}',
    '.cs-strength-badge.weak{background:rgba(239,68,68,0.14);color:#ef4444;border:1px solid rgba(239,68,68,0.30)}',
    '.cs-strength-badge.minimal,.cs-strength-badge.pending{background:rgba(255,255,255,0.04);color:var(--text-faint);border:1px solid var(--g-border)}',
    '.cs-snap{font-family:var(--font-mono);font-size:8px;letter-spacing:0.08em;color:var(--text-faint)}',
    /* Ring — slimmed (Phase 3.1 · 2026-05-03) */
    '.cs-ring-wrap{position:relative;width:170px;height:170px;margin:2px auto 0}',
    '.cs-ring-svg{display:block;width:100%;height:100%;overflow:visible}',
    '.cs-score-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}',
    '.cs-score-num{font-family:var(--font-mono);font-size:32px;font-weight:800;line-height:1;letter-spacing:-0.025em;color:var(--text);text-shadow:0 2px 14px rgba(0,229,191,0.20)}',
    '.cs-score-label{margin-top:3px;font-family:var(--font-mono);font-size:8px;font-weight:700;letter-spacing:0.18em;text-transform:uppercase;color:var(--text-faint)}',
    '.cs-score-anchor{text-align:center;font-family:var(--font-mono);font-size:10px;letter-spacing:0.04em;color:var(--text);margin-top:4px}',
    '.cs-score-anchor strong{color:var(--teal-bright);font-weight:700}',
    '.cs-score-pat{text-align:center;font-family:var(--font-mono);font-size:7.5px;letter-spacing:0.12em;color:var(--text-faint);text-transform:uppercase;margin-top:3px}',
    /* Slim formula caption — tighter padding */
    '.cs-formula-caption{margin-top:8px;padding:6px 10px;background:rgba(255,255,255,0.025);border:1px solid var(--g-border);border-radius:8px;text-align:center}',
    '.cs-formula-line{font-family:var(--font-mono);font-size:9.5px;line-height:1.45;color:var(--text-muted)}',
    '.cs-formula-line strong{color:var(--text);font-weight:700}',
    '.cs-formula-line .result{color:var(--teal-bright);font-weight:800}',
    '.cs-formula-sources{font-family:var(--font-mono);font-size:7.5px;letter-spacing:0.10em;text-transform:uppercase;color:var(--text-faint);margin-top:3px}',
    '.cs-formula-sources .sep{opacity:0.40;margin:0 4px}',
    /* Patent-dim subtitle (interpretive register · sentence-case PJS) */
    '.cs-dim-subtitle{margin-top:8px;text-align:center;font-family:var(--font-display,"General Sans","Plus Jakarta Sans",sans-serif);font-size:11.5px;font-weight:500;line-height:1.45;color:var(--text-muted);letter-spacing:-0.005em}',
    /* "Why this score?" decomposition panel */
    '.cs-why{margin-top:12px;padding:11px 13px 10px;background:rgba(255,255,255,0.025);border:1px solid var(--g-border);border-radius:12px}',
    '.cs-why-head{font-family:var(--font-display,"General Sans","Plus Jakarta Sans",sans-serif);font-size:11.5px;font-weight:600;line-height:1.35;color:var(--text);margin-bottom:9px;letter-spacing:-0.005em}',
    '.cs-why-bars{display:flex;flex-direction:column;gap:6px}',
    '.cs-why-bar{display:flex;align-items:center;gap:7px;font-family:var(--font-mono);font-size:9.5px}',
    '.cs-why-rank{display:inline-flex;align-items:center;justify-content:center;min-width:38px;padding:1px 5px;font-size:7.5px;font-weight:800;letter-spacing:0.10em;text-transform:uppercase;border-radius:3px;background:rgba(255,255,255,0.04);color:var(--text-faint);border:1px solid rgba(255,255,255,0.06)}',
    '.cs-why-rank.best{background:color-mix(in srgb,var(--dim-color) 14%,transparent);color:var(--dim-color);border-color:color-mix(in srgb,var(--dim-color) 30%,transparent)}',
    '.cs-why-rank.second{background:color-mix(in srgb,var(--dim-color) 8%,transparent);color:var(--dim-color);border-color:color-mix(in srgb,var(--dim-color) 18%,transparent);opacity:0.85}',
    '.cs-why-name{flex:1;color:var(--text);font-family:var(--font-display,"General Sans","Plus Jakarta Sans",sans-serif);font-size:11px;font-weight:500;letter-spacing:-0.005em;text-transform:none}',
    '.cs-why-track{flex:0 0 70px;height:4px;background:rgba(255,255,255,0.05);border-radius:2px;overflow:hidden}',
    '.cs-why-fill{height:100%;border-radius:2px;background:var(--dim-color);transition:width 0.4s ease}',
    '.cs-why-val{font-family:var(--font-mono);font-size:10px;font-weight:800;letter-spacing:-0.02em;min-width:30px;text-align:right;color:var(--dim-color)}',
    '.cs-why-formula{margin-top:9px;padding-top:7px;border-top:1px dashed rgba(255,255,255,0.08);font-family:var(--font-mono);font-size:9.5px;line-height:1.45;color:var(--text-muted);text-align:center}',
    '.cs-why-formula strong{color:var(--text);font-weight:700}',
    '.cs-why-formula .result{color:var(--teal-bright);font-weight:800}',
    '.cs-why-sources{display:flex;justify-content:space-between;gap:6px;margin-top:8px;padding-top:7px;border-top:1px dashed rgba(255,255,255,0.08)}',
    '.cs-why-src{display:flex;flex-direction:column;align-items:center;gap:2px;flex:1}',
    '.cs-why-src-icon{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:5px;font-family:var(--font-mono);font-size:9px;font-weight:800;color:var(--src-color);background:color-mix(in srgb,var(--src-color) 12%,transparent);border:1px solid color-mix(in srgb,var(--src-color) 30%,transparent)}',
    '.cs-why-src-label{font-family:var(--font-mono);font-size:7px;letter-spacing:0.08em;text-transform:uppercase;color:var(--text-faint);text-align:center;line-height:1.2}',
    /* Recommended block — slimmed */
    '.cs-rec-block{margin-top:10px;padding:10px 12px 9px;background:color-mix(in srgb,var(--card-color) 8%,rgba(255,255,255,0.04));border:1px solid color-mix(in srgb,var(--card-color) 32%,transparent);border-radius:10px;position:relative}',
    '.cs-rec-block::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;background:var(--card-color);border-top-left-radius:10px;border-top-right-radius:10px}',
    '.cs-rec-head{display:flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:8.5px;font-weight:800;letter-spacing:0.14em;text-transform:uppercase;color:var(--card-color);margin-bottom:3px}',
    '.cs-rec-head .cs-rec-dot{display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--card-color);box-shadow:0 0 6px var(--card-color)}',
    '.cs-rec-head .cs-rec-tier{margin-left:auto;padding:1px 5px;font-size:7px;letter-spacing:0.08em;border-radius:3px}',
    '.cs-rec-tier.gold{background:rgba(245,158,11,0.14);color:#f59e0b;border:1px solid rgba(245,158,11,0.30)}',
    '.cs-rec-tier.silver{background:rgba(180,200,220,0.10);color:rgba(220,230,240,0.85);border:1px solid rgba(180,200,220,0.20)}',
    '.cs-rec-tier.bronze{background:rgba(217,119,6,0.10);color:var(--amber-warm,#d4a574);border:1px solid rgba(217,119,6,0.25)}',
    '.cs-rec-row{display:flex;align-items:baseline;gap:10px;margin-bottom:4px}',
    '.cs-rec-score{font-family:var(--font-mono);font-size:24px;font-weight:800;line-height:1;letter-spacing:-0.025em;color:var(--card-color)}',
    '.cs-rec-label{font-family:var(--font-display);font-size:13px;font-weight:700;color:var(--text);letter-spacing:-0.005em}',
    '.cs-rec-rationale{font-family:var(--font-body);font-size:10px;line-height:1.45;color:var(--text-muted);margin:3px 0 6px}',
    '.cs-rec-rationale strong{color:var(--text);font-weight:600}',
    '.cs-rec-evidence{display:flex;flex-wrap:wrap;gap:4px}',
    '.cs-rec-chip{display:inline-flex;align-items:baseline;gap:4px;padding:2px 7px;font-family:var(--font-mono);font-size:8.5px;font-weight:700;background:rgba(255,255,255,0.06);border:1px solid var(--g-border);border-radius:4px;color:var(--text)}',
    '.cs-rec-chip .cs-chip-label{color:var(--text-faint);text-transform:uppercase;letter-spacing:0.06em;font-size:8px}',
    '.cs-rec-alts{display:flex;align-items:center;justify-content:space-between;margin-top:10px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.06);gap:6px}',
    '.cs-rec-alts-label{font-family:var(--font-mono);font-size:8px;font-weight:700;letter-spacing:0.10em;text-transform:uppercase;color:var(--text-faint);flex-shrink:0}',
    '.cs-rec-alts-row{display:flex;gap:5px;flex-wrap:wrap}',
    '.cs-rec-alt-pill{display:inline-flex;align-items:center;gap:4px;padding:2px 6px;font-family:var(--font-mono);font-size:9px;font-weight:700;border-radius:4px;background:rgba(255,255,255,0.04);border:1px solid var(--g-border);cursor:pointer}',
    '.cs-rec-alt-pill .cs-alt-dot{width:5px;height:5px;border-radius:50%}',
    '.cs-rec-alt-pill .cs-alt-name{color:var(--text-muted)}',
    /* Disease context */
    '.cs-disease-ctx{margin-top:10px;padding:12px 14px 10px;background:rgba(255,255,255,0.025);border:1px solid var(--g-border);border-radius:12px}',
    '.cs-dc-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}',
    '.cs-dc-title{font-family:var(--font-display);font-size:11px;font-weight:600;color:var(--text)}',
    '.cs-dc-eyebrow{font-family:var(--font-mono);font-size:8px;font-weight:700;letter-spacing:0.10em;text-transform:uppercase;color:var(--text-faint)}',
    '.cs-dc-disease-row{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}',
    '.cs-dc-disease{font-family:var(--font-display);font-size:13px;font-weight:700;color:var(--text)}',
    '.cs-dc-phase{font-family:var(--font-mono);font-size:8.5px;font-weight:800;letter-spacing:0.12em;text-transform:uppercase;padding:2px 6px;border-radius:4px}',
    '.cs-dc-phase.disrupting{background:rgba(244,63,94,0.14);color:var(--rose);border:1px solid rgba(244,63,94,0.30)}',
    '.cs-dc-phase.accelerating{background:rgba(245,158,11,0.14);color:#f59e0b;border:1px solid rgba(245,158,11,0.30)}',
    '.cs-dc-phase.emerging{background:rgba(20,184,166,0.14);color:#14b8a6;border:1px solid rgba(20,184,166,0.30)}',
    '.cs-dc-phase.dormant{background:rgba(255,255,255,0.04);color:var(--text-faint);border:1px solid rgba(255,255,255,0.10)}',
    '.cs-dc-signals{display:flex;flex-direction:column;gap:4px;margin-top:6px}',
    '.cs-dc-sig{display:flex;align-items:center;gap:8px}',
    '.cs-dc-sig-name{font-family:var(--font-body);font-size:10px;flex:1;color:var(--text)}',
    '.cs-dc-sig-track{flex:0 0 80px;height:4px;background:rgba(255,255,255,0.05);border-radius:2px;overflow:hidden}',
    '.cs-dc-sig-fill{height:100%;border-radius:2px;background:var(--rose)}',
    '.cs-dc-sig-val{font-family:var(--font-mono);font-size:9px;font-weight:800;letter-spacing:-0.02em;min-width:28px;text-align:right;color:var(--rose)}',
    '.cs-dc-foot{font-family:var(--font-mono);font-size:8px;letter-spacing:0.08em;color:var(--text-faint);margin-top:6px;text-transform:uppercase}',
    /* Why-now triangle (3-axis disruption — modality / patent / research) */
    '.cs-whynow{margin-top:10px;padding:12px 14px 10px;background:rgba(255,255,255,0.025);border:1px solid var(--g-border);border-radius:12px}',
    '.cs-wn-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}',
    '.cs-wn-title{font-family:var(--font-display);font-size:12px;font-weight:600;color:var(--text);letter-spacing:-0.005em}',
    '.cs-wn-eyebrow{font-family:var(--font-mono);font-size:8px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:var(--text-faint)}',
    '.cs-wn-body{display:flex;align-items:center;gap:12px;margin-top:4px}',
    '.cs-wn-svg-wrap{flex-shrink:0;width:120px;height:108px}',
    '.cs-wn-svg-wrap svg{display:block;width:100%;height:100%;overflow:visible}',
    '.cs-wn-takeaway{font-family:var(--font-body);font-size:10.5px;line-height:1.45;color:var(--text-muted);flex:1;letter-spacing:-0.005em}',
    '.cs-wn-takeaway strong{color:var(--text);font-weight:600}',
    '.cs-wn-cue{display:inline-block;padding:1px 7px;margin-bottom:5px;font-family:var(--font-mono);font-size:8px;font-weight:800;letter-spacing:0.12em;text-transform:uppercase;border-radius:3px}',
    '.cs-wn-cue.modality{background:rgba(168,85,247,0.10);color:#a855f7;border:1px solid rgba(168,85,247,0.28)}',
    '.cs-wn-cue.patent{background:rgba(244,63,94,0.10);color:var(--rose);border:1px solid rgba(244,63,94,0.28)}',
    '.cs-wn-cue.research{background:rgba(59,130,246,0.10);color:var(--blue);border:1px solid rgba(59,130,246,0.28)}',
    '.cs-wn-disease-row{display:flex;align-items:baseline;gap:7px;margin-bottom:6px}',
    '.cs-wn-disease{font-family:var(--font-display);font-size:12.5px;font-weight:700;color:var(--text);letter-spacing:-0.005em}',
    '.cs-wn-foot{font-family:var(--font-mono);font-size:8px;letter-spacing:0.10em;color:var(--text-faint);margin-top:8px;text-transform:uppercase;padding-top:6px;border-top:1px dashed rgba(255,255,255,0.06)}',
    /* Combined disclosure (Phase 3.7) — patent dims + deal-type formula in ONE collapsible */
    '.cs-combined-details{margin-top:10px;padding:0;border:1px solid var(--g-border);border-radius:10px;background:rgba(255,255,255,0.025);overflow:hidden}',
    '.cs-combined-details summary{list-style:none;cursor:pointer;padding:9px 14px;font-family:var(--font-display,"General Sans","Plus Jakarta Sans",sans-serif);font-size:11.5px;font-weight:600;color:var(--text);display:flex;align-items:center;justify-content:space-between;user-select:none;letter-spacing:-0.005em;transition:color 0.15s ease}',
    '.cs-combined-details summary::-webkit-details-marker{display:none}',
    '.cs-combined-details summary::after{content:"+";font-size:14px;line-height:1;color:var(--text-faint);font-family:var(--font-mono);transition:transform 0.20s ease}',
    '.cs-combined-details[open] summary::after{transform:rotate(45deg);color:var(--teal-bright)}',
    '.cs-combined-details summary:hover{color:var(--text)}',
    '.cs-combined-details[open] summary{border-bottom:1px solid var(--g-border)}',
    '.cs-combined-body{padding:12px 14px}',
    '.cs-cb-section{display:flex;flex-direction:column;gap:8px}',
    '.cs-cb-section-divider{margin-top:12px;padding-top:12px;border-top:1px dashed rgba(255,255,255,0.08)}',
    '.cs-cb-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:9px;font-family:var(--font-mono);font-size:8.5px;font-weight:800;letter-spacing:0.14em;text-transform:uppercase;color:var(--text-muted)}',
    '.cs-cb-formula{font-family:var(--font-mono);font-size:7.5px;font-weight:700;letter-spacing:0.06em;color:var(--text-faint);text-transform:none;font-style:italic}',
    /* ── Partner tooltip (radar hover preview card) ── */
    '.partner-tooltip{position:fixed;width:280px;padding:12px 14px 10px;background:rgba(7,12,23,0.96);border:1px solid rgba(255,255,255,0.12);border-radius:10px;backdrop-filter:blur(20px) saturate(160%);-webkit-backdrop-filter:blur(20px) saturate(160%);box-shadow:0 18px 48px rgba(0,0,0,0.65),inset 0 1px 0 rgba(255,255,255,0.06);z-index:1000;pointer-events:none;opacity:0;transition:opacity 150ms ease-out}',
    '.partner-tooltip.visible{opacity:1}',
    '.ptt-header{display:flex;align-items:baseline;justify-content:space-between;gap:8px}',
    '.ptt-name{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text);letter-spacing:-0.01em}',
    '.ptt-overall{font-family:var(--font-mono);font-size:16px;font-weight:800;letter-spacing:-0.02em;color:var(--teal-bright)}',
    '.ptt-meta{display:flex;gap:5px;flex-wrap:wrap;margin-top:4px;align-items:center}',
    '.ptt-strength,.ptt-inferred,.ptt-cat{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-family:var(--font-mono);font-size:7.5px;font-weight:800;letter-spacing:0.10em;text-transform:uppercase}',
    '.ptt-strength.strong{background:rgba(20,184,166,0.14);color:#14b8a6;border:1px solid rgba(20,184,166,0.30)}',
    '.ptt-strength.moderate{background:rgba(212,165,116,0.14);color:#d4a574;border:1px solid rgba(212,165,116,0.30)}',
    '.ptt-strength.emerging{background:rgba(245,158,11,0.14);color:#f59e0b;border:1px solid rgba(245,158,11,0.30)}',
    '.ptt-strength.weak{background:rgba(239,68,68,0.14);color:#ef4444;border:1px solid rgba(239,68,68,0.30)}',
    '.ptt-strength.minimal,.ptt-strength.pending{background:rgba(255,255,255,0.04);color:var(--text-faint);border:1px solid var(--g-border)}',
    '.ptt-inferred{background:rgba(180,200,220,0.10);color:rgba(220,230,240,0.85);border:1px dashed rgba(180,200,220,0.40)}',
    '.ptt-cat{background:rgba(255,255,255,0.04);color:var(--text-muted);border:1px solid var(--g-border)}',
    '.ptt-divider{height:1px;background:rgba(255,255,255,0.08);margin:9px 0}',
    '.ptt-section-label{font-family:var(--font-mono);font-size:7.5px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--text-faint);margin-bottom:5px}',
    '.ptt-bars{display:flex;flex-direction:column;gap:4px}',
    '.ptt-bar{display:flex;align-items:center;gap:6px}',
    '.ptt-bar-label{font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:var(--text-muted);width:55px}',
    '.ptt-bar-track{flex:1;height:3px;background:rgba(255,255,255,0.05);border-radius:2px;overflow:hidden}',
    '.ptt-bar-fill{height:100%;border-radius:2px}',
    '.ptt-bar-val{font-family:var(--font-mono);font-size:9.5px;font-weight:800;letter-spacing:-0.02em;min-width:26px;text-align:right}',
    '.ptt-rec{margin-top:8px;padding:5px 8px;border-radius:5px;font-family:var(--font-mono);font-size:9px;font-weight:800;letter-spacing:0.10em;text-transform:uppercase;display:inline-block;background:color-mix(in srgb,var(--rec-color) 10%,transparent);border:1px solid color-mix(in srgb,var(--rec-color) 35%,transparent);color:var(--rec-color)}',
    '.ptt-rationale{font-family:var(--font-body);font-size:10px;line-height:1.4;color:var(--text-muted);margin-top:6px}',
    '.ptt-cta{font-family:var(--font-mono);font-size:8.5px;letter-spacing:0.10em;color:var(--text-faint);margin-top:8px;text-align:center;text-transform:uppercase}',
    '.ptt-cta strong{color:var(--teal-bright);font-weight:700}',
    '.ptt-hint{font-family:var(--font-mono);font-size:8px;letter-spacing:0.12em;color:var(--text-faint);margin-top:8px;text-align:center;text-transform:uppercase;opacity:0.55;border-top:1px dashed rgba(255,255,255,0.06);padding-top:6px}',
    /* ─── Light-mode theme overrides — 2026-05-05 ───────────────────────────
       The block above bakes rgba(255,255,255,X) into every panel/divider/track
       background which renders invisibly on the light theme's white surface.
       These overrides re-bind the same selectors using slate-tinted alphas so
       the score panel reads with V2-grade contrast in light mode. Dark mode
       continues to use the originals (lower specificity loses to the override
       only when [data-theme="light"] is on <html>). */
    '[data-theme="light"] .collabiq-score-panel{background:rgba(15,23,42,0.025);border:1px solid var(--g-border) !important}',
    '[data-theme="light"] .cs-formula-details{background:rgba(15,23,42,0.02)}',
    '[data-theme="light"] .cs-info-icon{background:rgba(15,23,42,0.06)}',
    '[data-theme="light"] .cs-strength-badge.minimal,[data-theme="light"] .cs-strength-badge.pending{background:rgba(15,23,42,0.04)}',
    '[data-theme="light"] .cs-formula-caption{background:rgba(15,23,42,0.025)}',
    '[data-theme="light"] .cs-why{background:rgba(15,23,42,0.025)}',
    '[data-theme="light"] .cs-why-rank{background:rgba(15,23,42,0.04);border:1px solid rgba(15,23,42,0.08)}',
    '[data-theme="light"] .cs-why-track{background:rgba(15,23,42,0.06)}',
    '[data-theme="light"] .cs-why-formula{border-top:1px dashed rgba(15,23,42,0.10)}',
    '[data-theme="light"] .cs-why-sources{border-top:1px dashed rgba(15,23,42,0.10)}',
    '[data-theme="light"] .cs-rec-block{background:color-mix(in srgb,var(--card-color) 6%,rgba(15,23,42,0.025))}',
    '[data-theme="light"] .cs-rec-chip{background:rgba(15,23,42,0.05)}',
    '[data-theme="light"] .cs-rec-alts{border-top:1px solid rgba(15,23,42,0.08)}',
    '[data-theme="light"] .cs-rec-alt-pill{background:rgba(15,23,42,0.04)}',
    '[data-theme="light"] .cs-disease-ctx{background:rgba(15,23,42,0.025)}',
    '[data-theme="light"] .cs-dc-phase.dormant{background:rgba(15,23,42,0.05);border:1px solid rgba(15,23,42,0.10)}',
    '[data-theme="light"] .cs-dc-sig-track{background:rgba(15,23,42,0.06)}',
    '[data-theme="light"] .cs-whynow{background:rgba(15,23,42,0.025)}',
    '[data-theme="light"] .cs-wn-foot{border-top:1px dashed rgba(15,23,42,0.10)}',
    '[data-theme="light"] .cs-combined-details{background:rgba(15,23,42,0.025)}',
    '[data-theme="light"] .cs-cb-section-divider{border-top:1px dashed rgba(15,23,42,0.10)}',
    /* Partner-tooltip — entirely dark navy in original; rebuild as a light glass card */
    '[data-theme="light"] .partner-tooltip{background:rgba(255,255,255,0.96);border:1px solid rgba(15,23,42,0.12);box-shadow:0 18px 48px rgba(15,23,42,0.18),inset 0 1px 0 rgba(255,255,255,0.85)}',
    '[data-theme="light"] .ptt-divider{background:rgba(15,23,42,0.08)}',
    '[data-theme="light"] .ptt-strength.minimal,[data-theme="light"] .ptt-strength.pending{background:rgba(15,23,42,0.05)}',
    '[data-theme="light"] .ptt-cat{background:rgba(15,23,42,0.05)}',
    '[data-theme="light"] .ptt-bar-track{background:rgba(15,23,42,0.08)}',
    '[data-theme="light"] .ptt-hint{border-top:1px dashed rgba(15,23,42,0.10)}',
    '[data-theme="light"] .ptt-inferred{background:rgba(71,85,105,0.10);color:rgba(51,65,85,0.85);border:1px dashed rgba(71,85,105,0.40)}',
    '[data-theme="light"] .cs-rec-tier.silver{background:rgba(71,85,105,0.08);color:rgba(51,65,85,0.85);border:1px solid rgba(71,85,105,0.20)}',
    /* single-axis fallback bar (used when only one disruption signal is available) */
    '[data-theme="light"] .cs-wn-singletrack{background:rgba(15,23,42,0.08) !important}'
  ].join('\n');
  document.head.appendChild(style);
}

/** Map deal key -> CSS color var (uses --sonar-* palette). */
function _csDealColor(key) {
  return 'var(--sonar-' + _sonarVarSuffix(key) + ')';
}

/** Static info per deal type — labels (short used in tab strip / chips). */
var _CS_DEAL_INFO = {
  'CO_DEVELOPMENT':       { label: 'Co-Development', short: 'Co-Dev' },
  'PLATFORM_PARTNERSHIP': { label: 'R&D Collab',     short: 'R&D'    },
  'PORTFOLIO_EXPANSION':  { label: 'M&A',            short: 'M&A'    },
  'IN_LICENSING':         { label: 'In-Licensing',   short: 'In-Lic' }
};

/** ─── Patent-pending CollabIQ dimensions ──────────────────────────────────
 * Per FRONTEND_IP_DESIGN_GUIDE.md the right-panel ring renders the FOUR
 * patent dimensions (not deal types — those live on the sonar radar).
 *  - Partner Alignment    (teal)   structural org-pair fit
 *  - Target Convergence   (blue)   shared biological targets
 *  - Disease Overlap      (amber)  therapeutic coverage intersection
 *  - Modality Fit         (purple) technology differentiation
 * Score = 0.70 × best dimension + 0.30 × second dimension.
 * Colors mirror cdev/codev for Partner, sonar-blue for Target, amber for
 * Disease, purple for Modality. Hard-coded so theme tokens don't drift.
 */
var _CS_DIM_INFO = {
  'PARTNER_ALIGNMENT':  { label: 'Partner Alignment',  short: 'Partner',  color: '#0d9488', source: 'ClinicalTrials.gov', icon: 'P' },
  'TARGET_CONVERGENCE': { label: 'Target Convergence', short: 'Target',   color: '#3b82f6', source: 'UniProt',            icon: 'T' },
  'DISEASE_OVERLAP':    { label: 'Disease Overlap',    short: 'Disease',  color: '#d97706', source: 'MONDO',              icon: 'D' },
  'MODALITY_FIT':       { label: 'Modality Fit',       short: 'Modality', color: '#7c3aed', source: 'DrugBank',           icon: 'M' }
};

/** Color hex for a patent dim key. Used by ring SVG + bars + chips. */
function _csDimColor(key) {
  return (_CS_DIM_INFO[key] && _CS_DIM_INFO[key].color) || '#64748b';
}

/** Compute the four patent-dimension scores for a row.
 * Prefers V2 backend fields when present; falls back to log-normalized
 * counts of shared_targets / shared_diseases / shared_moas / shared_trials
 * when V2 backend isn't yet live (per BACKEND_HANDOFF_NOTES §4). */
function _csPatentDims(d) {
  if (!d) return { PARTNER_ALIGNMENT: 0, TARGET_CONVERGENCE: 0, DISEASE_OVERLAP: 0, MODALITY_FIT: 0 };

  // V2 backend: return as-is when fields land
  if (d.partner_alignment_dim != null && d.target_convergence_dim != null) {
    return {
      PARTNER_ALIGNMENT:  _clamp(d.partner_alignment_dim),
      TARGET_CONVERGENCE: _clamp(d.target_convergence_dim),
      DISEASE_OVERLAP:    _clamp(d.disease_overlap_dim),
      MODALITY_FIT:       _clamp(d.modality_comp_dim)
    };
  }

  // Fallback proxies. Log-normalize counts so a few dozen patents/trials
  // doesn't saturate the bar; saturation cap k tuned per dim from the
  // distribution we see in the live API (Sanofi cohort spans 0–~50).
  var nTargets  = parseInt(d.shared_targets,  10) || 0;
  var nMoas     = parseInt(d.shared_moas,     10) || 0;
  var nDiseases = parseInt(d.shared_diseases, 10) || 0;
  var nTrials   = parseInt(d.shared_trials,   10) || 0;
  var nPatents  = parseInt(d.shared_patents,  10) || 0;
  function logNorm(x, k) { if (x <= 0) return 0; return Math.min(1, Math.log10(1 + x) / Math.log10(1 + k)); }

  return {
    PARTNER_ALIGNMENT:  logNorm(nTrials + nPatents, 50),
    TARGET_CONVERGENCE: logNorm(nTargets,           20),
    DISEASE_OVERLAP:    logNorm(nDiseases,          20),
    MODALITY_FIT:       logNorm(nMoas,              15)
  };
}

/** CollabIQ score (patent formula). 0.70 × strongest + 0.30 × second. */
function _csCollabIQScore(d) {
  if (d && d.collabiq_score != null) return _clamp(d.collabiq_score);
  var dims = _csPatentDims(d);
  var sorted = Object.keys(dims).map(function(k) { return dims[k]; })
    .sort(function(a, b) { return b - a; });
  return _clamp(0.70 * (sorted[0] || 0) + 0.30 * (sorted[1] || 0));
}

/** Generate rationale text for a deal type from the row's sub-factor scores.
 *  Falls back to a static template if sub-factors aren't present.
 *  Templates per scoring_methodology.md / partneriq_evidence_chain.md. */
function _csRationale(dealKey, d) {
  if (!d) return '';
  function v(k, fb) { return (typeof d[k] === 'number' ? d[k] : (fb != null ? fb : 0)); }
  switch (dealKey) {
    case 'CO_DEVELOPMENT':
      return 'Independent overlap on target (' + v('sf_shared_target_signal', 0.7).toFixed(2) +
             ') and modality differentiation (' + v('sf_modality_differentiation', 0.65).toFixed(2) +
             ') — split-risk co-development is the natural fit.';
    case 'PLATFORM_PARTNERSHIP':
      return 'Mechanism complementarity (' + v('sf_complementarity_score', 0.65).toFixed(2) +
             ') plus MOA diversity (' + v('sf_moa_diversity', 0.55).toFixed(2) +
             ') — parallel R&D programs across both pipelines.';
    case 'PORTFOLIO_EXPANSION':
      return 'Complementary diseases (' + v('sf_comp_diseases_signal', 0.65).toFixed(2) +
             ') and TA adjacency (' + v('sf_ta_adjacency', 0.55).toFixed(2) +
             ') — accretive acquisition target.';
    case 'IN_LICENSING':
      return 'Stage asymmetry (' + v('sf_stage_asymmetry', 0.65).toFixed(2) +
             ') plus disease coverage gap (' + v('sf_disease_coverage_complement', 0.55).toFixed(2) +
             ') — license advanced asset to fill the portfolio.';
    default: return '';
  }
}

/** Evidence tier from breadth + depth (per partneriq_evidence_chain.md). */
function _csEvidenceTier(d) {
  if (!d) return 'BRONZE';
  var br = d.evidence_breadth || 0, de = d.evidence_depth || 0;
  if (br >= 4 && de >= 10) return 'GOLD';
  if (br >= 2 && de >= 5)  return 'SILVER';
  return 'BRONZE';
}

/** Snapshot date — read from window.DASHIQ.snapshotDate if exposed, else today. */
function _csSnapshot() {
  if (window.DASHIQ && window.DASHIQ.snapshotDate) return window.DASHIQ.snapshotDate;
  // Fallback: build a yyyy-mm-dd from local date
  var d = new Date();
  var m = String(d.getMonth() + 1).padStart(2, '0');
  var dd = String(d.getDate()).padStart(2, '0');
  return d.getFullYear() + '-' + m + '-' + dd;
}

/**
 * Build the inner HTML for the right-panel CollabIQ Score block.
 * Phase 1 rebuild — replaces deck/tab strip with rec block + disease context.
 */
function _buildCollabScorePanel(d, activeDeal, anchorOrg) {
  function _v(key) { return d ? _clamp(d[key]) : 0; }
  var scores = {
    CO_DEVELOPMENT:       _v('co_development_score'),
    PLATFORM_PARTNERSHIP: _v('platform_partnership_score'),
    PORTFOLIO_EXPANSION:  _v('portfolio_expansion_score'),
    IN_LICENSING:         _v('in_licensing_score')
  };
  // Strategy score = 0.7 × best + 0.3 × second (canonical formula).
  // Prefer V2 strategy_score; fall back to recommended_deal_score, then collab_iq/collab_score.
  var strategyScore = d ? _clamp(d.strategy_score || d.recommended_deal_score || d.collab_iq_score || d.collab_score || 0) : 0;
  var recDeal = (d && d.recommended_deal_type) || 'CO_DEVELOPMENT';

  // Strength + confidence — categorical from backend, with score-derived fallback.
  // DEFECT-011 fix: route through canonical _strengthTier (was 0.70/0.50/0.35/0.20
  // — now 0.85/0.70/0.55/0.40/0.25 to match every other tier-bucket call).
  var strength = (d && d.strategy_strength) || (d ? _strengthTier(strategyScore) : 'PENDING');
  var confidence = (d && d.confidence_tier) || 'EMERGING';

  // Partner name resolution
  var partnerName = '';
  if (d) {
    var pair = (typeof _getPair === 'function') ? _getPair(d) : { partnerName: d.org_b_name || '' };
    partnerName = _fixOrgName(pair.partnerName || d.org_b_name || '');
  }
  var aOrg = anchorOrg ? _fixOrgName(anchorOrg) : 'Anchor';

  // ── Patent dimensions (FRONTEND_IP_DESIGN_GUIDE) ─────────────────────────
  // Right-panel ring renders the four PATENT dimensions (Partner Alignment /
  // Target Convergence / Disease Overlap / Modality Fit). Deal-type
  // breakdown lives on the sonar radar. CollabIQ score = patent formula.
  var dims = _csPatentDims(d);
  var collabiqScore = _csCollabIQScore(d);
  var sortedDims = Object.keys(dims).map(function(k) { return { k: k, v: dims[k] }; })
    .sort(function(a, b) { return b.v - a.v; });
  var bestDim = sortedDims[0], secondDim = sortedDims[1];

  // Sorted deal scores still used by tooltip / fallback paths in this fn.
  var sortedDeals = Object.keys(scores).map(function(k) { return { k: k, v: scores[k] }; })
    .sort(function(a, b) { return b.v - a.v; });
  var bestDealDim = sortedDeals[0], secondDealDim = sortedDeals[1];

  // UR-003 fix: theme-aware ring backdrop + label color. Was hardcoded
  // white-on-white in light mode (track paths used stroke="#fff", labels
  // used rgba(230,237,247,0.55)). Read theme at build time so the right
  // panel stays legible against the light/dark page surface.
  var _ringIsLight = document.documentElement.getAttribute('data-theme') === 'light';
  var _ringTrackStroke = _ringIsLight ? 'rgba(15,23,42,0.92)' : '#fff';
  var _ringLabelFill = _ringIsLight ? 'rgba(15,23,42,0.65)' : 'rgba(230,237,247,0.55)';
  // Patent-dim ring: NW=Partner, NE=Target, SE=Disease, SW=Modality.
  function _ringDimSegment(d_path, dimKey) {
    var s = dims[dimKey];
    var color = _csDimColor(dimKey);
    var dasharray = (s * 150).toFixed(1) + ' 1000';
    return '<path d="' + d_path + '" stroke="' + color + '" stroke-dasharray="' + dasharray + '"/>';
  }
  function _ringLabelOnly(labelTxt, lx, ly, anchor) {
    return '<text x="' + lx + '" y="' + ly + '" fill="' + _ringLabelFill + '" text-anchor="' + anchor + '">' + labelTxt + '</text>';
  }

  // ── Build HTML ───────────────────────────────────────────────────────────
  var html = '';

  // Eyebrow row — patent-aligned. "CollabIQ™" only (Strategy moved to sonar).
  html += '<div class="cs-eyebrow">' +
            '<span class="cs-pp-mark">PATENT PENDING</span>' +
            'CollabIQ™' +
            '<span class="cs-info-icon" title="CollabIQ Score = 0.70 × strongest dimension + 0.30 × second strongest. Four patent-pending dimensions: Partner Alignment, Target Convergence, Disease Overlap, Modality Fit.">?</span>' +
          '</div>';

  // Meta row — strength badge only.
  // Phase 3.3: dropped VERIFIED + snapshot date per principal feedback —
  // keep the meta row visually clean. Confidence stays in the data-grounded
  // tooltip on hover; date moves to a global "Data as of {date}" footer
  // (handled elsewhere if/when added).
  if (d) {
    html += '<div class="cs-meta">' +
              '<span class="cs-strength-badge ' + strength.toLowerCase() + '" title="strategy_strength · ' + strength + '">' + strength + '</span>' +
            '</div>';
  } else {
    html += '<div class="cs-meta">' +
              '<span class="cs-strength-badge pending">PENDING</span>' +
            '</div>';
  }

  // Ring — 4 patent-dim arcs. NW=Partner, NE=Target, SE=Disease, SW=Modality.
  // Replaces the deal-type ring (deal types live on the sonar radar).
  html += '<div class="cs-ring-wrap">';
  html += '<svg class="cs-ring-svg" viewBox="0 0 240 240" aria-hidden="true">';
  html += '<g stroke-width="11" stroke-linecap="round" fill="none" opacity="' + (_ringIsLight ? '0.10' : '0.18') + '">' +
            '<path d="M 120 24 A 96 96 0 0 0 24 120" stroke="' + _ringTrackStroke + '"/>' +
            '<path d="M 216 120 A 96 96 0 0 0 120 24" stroke="' + _ringTrackStroke + '"/>' +
            '<path d="M 120 216 A 96 96 0 0 0 216 120" stroke="' + _ringTrackStroke + '"/>' +
            '<path d="M 24 120 A 96 96 0 0 0 120 216" stroke="' + _ringTrackStroke + '"/>' +
          '</g>';
  html += '<g stroke-width="11" stroke-linecap="round" fill="none">';
  html += _ringDimSegment('M 120 24 A 96 96 0 0 0 24 120',  'PARTNER_ALIGNMENT');
  html += _ringDimSegment('M 216 120 A 96 96 0 0 0 120 24', 'TARGET_CONVERGENCE');
  html += _ringDimSegment('M 120 216 A 96 96 0 0 0 216 120','DISEASE_OVERLAP');
  html += _ringDimSegment('M 24 120 A 96 96 0 0 0 120 216', 'MODALITY_FIT');
  html += '</g>';
  // Cardinal labels — short patent-dim names.
  html += '<g font-family="JetBrains Mono, monospace" font-size="8" font-weight="700" letter-spacing="1.2">';
  html += _ringLabelOnly('PARTNER',  36,  28,  'end');
  html += _ringLabelOnly('TARGET',   204, 28,  'start');
  html += _ringLabelOnly('DISEASE',  204, 218, 'start');
  html += _ringLabelOnly('MODALITY', 36,  218, 'end');
  html += '</g>';
  html += '</svg>';
  html += '<div class="cs-score-center">' +
            '<div class="cs-score-num">' + (d ? collabiqScore.toFixed(2) : '—') + '</div>' +
            '<div class="cs-score-label">CollabIQ™</div>' +
          '</div>';
  html += '</div>';

  // Subtitle (interpretive register · sentence-case PJS)
  html += '<div class="cs-dim-subtitle">How naturally do these organizations fit?</div>';

  // Anchor row + patent pending mark
  html += '<div class="cs-score-anchor"><strong>' + aOrg + '</strong> ↔ <strong>' + (partnerName || '—') + '</strong></div>';
  html += '<div class="cs-score-pat">Patent pending</div>';

  // ── Phase 3.7 (2026-05-03) restructure ───────────────────────────────────
  // Previously the right panel rendered, in order:
  //   1. always-visible "What makes this partnership work?" 4-bar panel
  //   2. collapsible "Deal-type breakdown" formula disclosure
  //   3. always-visible Why-now? disruption triangle
  // CTK feedback: panel was too tall (dead space between radar and table)
  // and the always-visible 4-bar panel duplicated info already encoded in
  // the patent-dim ring above.
  // New order: Why-now? triangle renders FIRST (now sits just under the
  // patent-pending microtype, where the always-visible 4-bar panel was),
  // then a SINGLE combined disclosure holding BOTH the patent-dim
  // breakdown AND the deal-type breakdown — both collapsed by default.
  // The combined disclosure is appended LATER, after the disease-context
  // block, so the visual order is: ring → subtitle → anchor → patent-pending
  // → Why-now triangle → combined "What's behind this score?" disclosure.

  // Phase 3.4 (2026-05-03): rec block REMOVED from right panel.
  // Was: full recommended-deal card with score + rationale + chips + alternatives.
  // Now: that data lives in the per-org hover tooltip (rich card on radar
  // hover) + the partnership table's "Recommended" column with deal pill.
  // The 4-arc ring above visually encodes which deal type leads (longest
  // arc = recommended). Right panel is now purely: ring + anchor + formula
  // disclosure + disease context. No redundancy with the hover tooltip.
  //
  // To restore: re-add the cs-rec-block construction (CSS still present in
  // _injectCollabScoreStyles for backward compat).

  // ── Disease context block ────────────────────────────────────────────────
  // Field-name reconciliation (Phase 3.5 · 2026-05-03):
  //   The live /strategy-iq/search payload uses different field names than
  //   the original backend spec. Real fields are:
  //     - disruption_context_disease  (top disrupting disease for this pair)
  //     - disruption_context_score    (signal strength for that disease)
  //     - disruption_modifier         (overall modifier applied to scores)
  //     - top_shared_diseases         (PLURAL; pipe- or comma-separated list)
  //     - shared_diseases             (count, sometimes a list)
  //   The previous gate checked disruption_signals_json / disruption_urgency
  //   / top_shared_disease — none of which exist on live rows, so the block
  //   never rendered. New gate accepts the live names AND the legacy names
  //   so it works against any payload shape.
  var _hasCtx = d && (
    d.disruption_signals_json || d.disruption_urgency ||
    d.disruption_context_disease || d.disruption_context_score ||
    d.disruption_modifier || d.top_shared_disease || d.top_shared_diseases ||
    d.shared_diseases || d.top_shared_tas
  );
  if (_hasCtx) {
    // Top disease — try every known field name in priority order.
    function _firstFromList(v) {
      if (!v) return '';
      var s = String(v);
      // Split on either pipe or comma.
      var parts = s.indexOf('|') >= 0 ? s.split('|') : s.split(',');
      return (parts[0] || '').trim();
    }
    var topDisease =
      d.disruption_context_disease ||
      _firstFromList(d.top_shared_diseases) ||
      d.top_shared_disease ||
      _firstFromList(d.shared_diseases);
    var topTA = _firstFromList(d.top_shared_tas);
    var phase = d.disruption_phase || '';
    var phaseCls = phase.toLowerCase();

    // Build the signal list. Three priority paths:
    //   1. disruption_signals_json (rich array — V2 backend)
    //   2. disruption_context_score + disruption_modifier (live API today)
    //   3. disruption_urgency (legacy single signal)
    var signals = [];
    if (d.disruption_signals_json) {
      try {
        var parsed = (typeof d.disruption_signals_json === 'string') ? JSON.parse(d.disruption_signals_json) : d.disruption_signals_json;
        if (Array.isArray(parsed)) signals = parsed.slice(0, 3);
      } catch (e) { /* malformed json — fall through */ }
    }
    if (signals.length === 0) {
      // Synthesize from live fields. disruption_context_score is the per-
      // disease disruption signal; disruption_modifier is the score adj.
      if (d.disruption_context_score != null) {
        signals.push({ label: 'Disruption signal', score: _clamp(d.disruption_context_score) });
      }
      if (d.disruption_modifier != null) {
        signals.push({ label: 'Score modifier', score: _clamp(Math.abs(d.disruption_modifier)) });
      }
      if (signals.length === 0 && d.disruption_urgency != null) {
        signals.push({ label: 'Disruption urgency', score: _clamp(d.disruption_urgency) });
      }
    }

    if (topDisease || signals.length > 0 || topTA) {
      // ── Why now? triangle — BD-MEANINGFUL AXES ──────────────────────────
      // CTK 2026-05-12 redesign: replace abstract Modality/Patent/Research
      // axes with three questions a BD/VC audience actually asks of a
      // partnership row:
      //
      //   URGENCY     (top · rose)   — Why now? Patent cliff / LOE pressure
      //                                on the disease. Per-disease signal
      //                                from disruption_signals.patent_cliff.
      //   OPPORTUNITY (left · purple)— What's the prize? Platform shift +
      //                                cross-TA expansion on the disease.
      //                                Mean of disruption_signals.modality_migration
      //                                and cross_ta_expansion.
      //   EXECUTION   (right · teal) — Can they actually do it? Joint
      //                                capability across the partner pair —
      //                                normalized blend of n_co_trials,
      //                                n_shared_targets, n_shared_patents.
      //                                Pair-level (NOT per-disease) by design.
      //
      // The triangle renders synchronously with pair-level fallbacks; an
      // async enrichment pass (_enrichWhyNowFromDisruption, fires after
      // panel mount) replaces URGENCY + OPPORTUNITY with per-disease values
      // from /v2/disruption-signals and the takeaway text with the dominant
      // signal's pre-written `partner_frame` BD narrative.
      function _disKey(d, keys) {
        for (var i = 0; i < keys.length; i++) {
          if (d[keys[i]] != null) return _clamp(d[keys[i]]);
        }
        return null;
      }
      var ctxScore = (d.disruption_context_score != null) ? _clamp(d.disruption_context_score) : null;

      // URGENCY — synchronous fallback from pair-level fields. The async
      // enrichment overrides with the disease's patent_cliff strength.
      var urgScore = _disKey(d, ['patent_cliff_pressure', 'sf_patent_cliff_pressure', 'sf_patent_cliff_pressure_signal', 'patent_cliff', 'patent_cliff_signal']);
      if (urgScore == null && d.disruption_modifier != null) {
        // disruption_modifier ranges 0.85..1.15. Map (1.0 -> 0, 1.15 -> 1.0)
        // so a flat 1.0 modifier reads as "no urgency" not "max urgency".
        urgScore = _clamp((parseFloat(d.disruption_modifier) - 1.0) / 0.15);
      }
      if (urgScore == null) urgScore = (ctxScore != null ? ctxScore : 0);

      // OPPORTUNITY — synchronous fallback (modality_migration as proxy).
      var oppScore = _disKey(d, ['modality_migration', 'sf_modality_migration', 'sf_modality_migration_signal']);
      if (oppScore == null) oppScore = (ctxScore != null ? ctxScore : 0);

      // EXECUTION — pair-level only, ALWAYS available (every strategy_iq
      // row has these structural counts). Normalize each component to 0..1
      // against a defensible cap, then average. Caps chosen from p95 of
      // the strategy_iq distribution to avoid saturating on outliers:
      //   n_co_trials cap 50, n_shared_targets cap 100, n_shared_patents cap 50.
      var _coT = Math.min((parseFloat(d.n_co_trials) || 0) / 50, 1);
      var _shT = Math.min((parseFloat(d.n_shared_targets) || 0) / 100, 1);
      var _shP = Math.min((parseFloat(d.n_shared_patents) || parseFloat(d.shared_patents) || 0) / 50, 1);
      var execScore = (_coT + _shT + _shP) / 3;

      // Triangle geometry — same equilateral as before:
      //   top    (URGENCY):     ( 60, 18 )
      //   left   (OPPORTUNITY): ( 17, 90 )
      //   right  (EXECUTION):   (103, 90 )
      // Center at (60, 66).
      var TX = 60, TY = 18;
      var LX = 17, LY = 90;
      var RX = 103, RY = 90;
      var CX = 60, CY = 66;
      function _ptOnAxis(vx, vy, s) {
        return [
          (CX + (vx - CX) * s).toFixed(2),
          (CY + (vy - CY) * s).toFixed(2)
        ];
      }
      var pTop   = _ptOnAxis(TX, TY, urgScore);  // URGENCY top
      var pLeft  = _ptOnAxis(LX, LY, oppScore);  // OPPORTUNITY left
      var pRight = _ptOnAxis(RX, RY, execScore); // EXECUTION right
      var plotPts = pTop[0] + ',' + pTop[1] + ' ' + pRight[0] + ',' + pRight[1] + ' ' + pLeft[0] + ',' + pLeft[1];

      // Pick the dominant axis — drives the cue pill + takeaway lead.
      var axes = [
        { key: 'urgency',     label: 'Urgency',     val: urgScore,  color: 'var(--rose)' },
        { key: 'opportunity', label: 'Opportunity', val: oppScore,  color: '#a855f7' },
        { key: 'execution',   label: 'Execution',   val: execScore, color: 'var(--teal)' }
      ].sort(function(a, b) { return b.val - a.val; });
      var dom = axes[0];
      var cueClass = dom.key;
      var cueText = dom.label + ' ' + dom.val.toFixed(2);

      // Synchronous takeaway — will be replaced by partner_frame text from
      // the disruption_signals row once _enrichWhyNowFromDisruption resolves.
      function _etlQuarterLabel() {
        var ts = (window.STATE && window.STATE.last_etl_at) || '';
        var dt = ts ? new Date(ts) : null;
        if (!dt || isNaN(dt.getTime())) dt = new Date();
        var q = Math.floor(dt.getUTCMonth() / 3) + 1;
        var yy = dt.getUTCFullYear() % 100;
        return 'Q' + q + '/' + (yy < 10 ? '0' + yy : yy);
      }
      var takeawayLead = {
        urgency:     'LOE / patent cliff pressure across shared portfolio',
        opportunity: 'platform shift + cross-TA expansion across shared portfolio',
        execution:   'joint clinical + IP capacity (pair-level)'
      }[dom.key] || 'multiple disruption signals across shared portfolio';
      var takeaway = takeawayLead + ' · ' + _etlQuarterLabel() + ' window';

      // CTK 2026-05-12: triangle is now PER-PAIR (not per-disease). Tag
      // with org names so the post-mount enrichment can call
      // /v2/disruption-signals/pair and aggregate signals across the
      // pair's top shared diseases. Different pair -> different aggregate
      // -> different triangle shape. No disease label printed (triangle IS
      // the pair's portfolio disruption fingerprint, not one disease).
      var _whynowOrgA = (d && (d.org_a_name || d.org_b_name)) || '';
      var _whynowOrgB = (d && d.org_a_name && d.org_b_name)
        ? (d.org_b_name === _whynowOrgA ? d.org_a_name : d.org_b_name)
        : '';
      var _whynowPairAttrs = (_whynowOrgA && _whynowOrgB)
        ? ' data-org-a="' + _escHTML(_whynowOrgA) + '" data-org-b="' + _escHTML(_whynowOrgB) + '"'
        : '';
      html += '<div class="cs-whynow"' + _whynowPairAttrs + '>';
      html +=   '<div class="cs-wn-head">';
      html +=     '<span class="cs-wn-title">Why now?</span>';
      html +=     '<span class="cs-wn-eyebrow">Pair disruption fingerprint</span>';
      html +=   '</div>';
      html +=   '<div class="cs-wn-body">';

      {
        // 3-axis triangle: URGENCY (top, rose) · OPPORTUNITY (left, purple)
        // · EXECUTION (right, teal). Theme-aware grid strokes.
        var _whynowIsLight = document.documentElement.getAttribute('data-theme') === 'light';
        var _whynowGridStroke = _whynowIsLight ? 'rgba(15,23,42,0.14)' : 'rgba(255,255,255,0.10)';
        var _whynowAxisStroke = _whynowIsLight ? 'rgba(15,23,42,0.10)' : 'rgba(255,255,255,0.06)';
        html +=     '<div class="cs-wn-svg-wrap">';
        html +=       '<svg viewBox="0 0 120 108" aria-hidden="true">';
        // 3 nested gridline triangles (100%, 66%, 33%)
        html +=         '<g stroke="' + _whynowGridStroke + '" stroke-width="0.7" fill="none">';
        html +=           '<polygon points="' + TX + ',' + TY + ' ' + RX + ',' + RY + ' ' + LX + ',' + LY + '"/>';
        var g66 = [
          _ptOnAxis(TX, TY, 0.66), _ptOnAxis(RX, RY, 0.66), _ptOnAxis(LX, LY, 0.66)
        ];
        html +=           '<polygon points="' + g66[0][0]+','+g66[0][1]+' '+g66[1][0]+','+g66[1][1]+' '+g66[2][0]+','+g66[2][1] + '"/>';
        var g33 = [
          _ptOnAxis(TX, TY, 0.33), _ptOnAxis(RX, RY, 0.33), _ptOnAxis(LX, LY, 0.33)
        ];
        html +=           '<polygon points="' + g33[0][0]+','+g33[0][1]+' '+g33[1][0]+','+g33[1][1]+' '+g33[2][0]+','+g33[2][1] + '"/>';
        html +=         '</g>';
        // Axis lines from center to each vertex
        html +=         '<g stroke="' + _whynowAxisStroke + '" stroke-width="0.5">';
        html +=           '<line x1="' + CX + '" y1="' + CY + '" x2="' + TX + '" y2="' + TY + '"/>';
        html +=           '<line x1="' + CX + '" y1="' + CY + '" x2="' + RX + '" y2="' + RY + '"/>';
        html +=           '<line x1="' + CX + '" y1="' + CY + '" x2="' + LX + '" y2="' + LY + '"/>';
        html +=         '</g>';
        // Plotted polygon (rose tint matches dominant urgency axis vibe)
        html +=         '<polygon class="cs-wn-plot" points="' + plotPts + '" ' +
                          'fill="rgba(244,63,94,0.20)" stroke="#f43f5e" stroke-width="1.5" stroke-linejoin="round"/>';
        // Vertex dots in axis colors
        html +=         '<circle class="cs-wn-vtx cs-wn-vtx-urg" cx="' + pTop[0] + '" cy="' + pTop[1] + '" r="2.6" fill="#f43f5e"/>';
        html +=         '<circle class="cs-wn-vtx cs-wn-vtx-opp" cx="' + pLeft[0] + '" cy="' + pLeft[1] + '" r="2.2" fill="#a855f7"/>';
        html +=         '<circle class="cs-wn-vtx cs-wn-vtx-exe" cx="' + pRight[0] + '" cy="' + pRight[1] + '" r="2.2" fill="#0d9488"/>';
        // Axis labels — theme-aware
        var _whynowLabelFill = _whynowIsLight ? 'rgba(15,23,42,0.65)' : 'rgba(230,237,247,0.55)';
        html +=         '<g font-family="JetBrains Mono, monospace" font-size="6.4" font-weight="700" letter-spacing="0.6">';
        html +=           '<text x="60" y="6" text-anchor="middle" fill="' + _whynowLabelFill + '">URGENCY</text>';
        html +=           '<text class="cs-wn-val-urg" x="60" y="13" text-anchor="middle" fill="#f43f5e">' + urgScore.toFixed(2) + '</text>';
        html +=           '<text x="13" y="100" text-anchor="middle" fill="' + _whynowLabelFill + '">OPPORTUNITY</text>';
        html +=           '<text class="cs-wn-val-opp" x="13" y="107" text-anchor="middle" fill="#a855f7">' + oppScore.toFixed(2) + '</text>';
        html +=           '<text x="106" y="100" text-anchor="middle" fill="' + _whynowLabelFill + '">EXECUTION</text>';
        html +=           '<text class="cs-wn-val-exe" x="106" y="107" text-anchor="middle" fill="#0d9488">' + execScore.toFixed(2) + '</text>';
        html +=         '</g>';
        html +=       '</svg>';
        html +=     '</div>';
      }

      html +=     '<div class="cs-wn-takeaway">';
      html +=       '<span class="cs-wn-cue ' + cueClass + '">' + cueText + '</span><br/>';
      html +=       '<span class="cs-wn-takeaway-body">' + takeaway + '</span>';
      html +=     '</div>';
      html +=   '</div>';
      html +=   '<div class="cs-wn-foot">Source · disruption_signals (per disease) + strategy_iq (pair execution)</div>';
      html += '</div>';
    }
  }

  // ── Combined disclosure: "What's behind this score?" ─────────────────────
  // ONE collapsible holding both:
  //   (a) Patent-dimension breakdown — 4 ranked bars (was always-visible)
  //   (b) Deal-type breakdown formula — strategy_score formula (was disclosure)
  // Hidden by default so the right panel reads tight against the radar.
  if (d) {
    html += '<details class="cs-combined-details">';
    html +=   '<summary>What’s behind this score?</summary>';
    html +=   '<div class="cs-combined-body">';
    // (a) Patent dim breakdown
    html +=     '<div class="cs-cb-section">';
    html +=       '<div class="cs-cb-section-head">Patent dimensions <span class="cs-cb-formula">0.70 × best + 0.30 × second</span></div>';
    html +=       '<div class="cs-why-bars">';
    sortedDims.forEach(function(dm, i) {
      var info = _CS_DIM_INFO[dm.k];
      var rankCls = i === 0 ? 'best' : (i === 1 ? 'second' : '');
      var rankTxt = i === 0 ? 'BEST · 70%' : (i === 1 ? 'SECOND · 30%' : '');
      html += '<div class="cs-why-bar" style="--dim-color:' + info.color + '">';
      if (rankTxt) {
        html += '<span class="cs-why-rank ' + rankCls + '">' + rankTxt + '</span>';
      } else {
        html += '<span class="cs-why-rank">—</span>';
      }
      html +=   '<span class="cs-why-name">' + info.label + '</span>';
      html +=   '<div class="cs-why-track"><div class="cs-why-fill" style="width:' + (dm.v * 100).toFixed(0) + '%"></div></div>';
      html +=   '<span class="cs-why-val">' + dm.v.toFixed(2) + '</span>';
      html += '</div>';
    });
    html +=       '</div>';
    html +=     '</div>';
    // (b) Deal-type breakdown
    html +=     '<div class="cs-cb-section cs-cb-section-divider">';
    html +=       '<div class="cs-cb-section-head">Deal-type breakdown <span class="cs-cb-formula">sonar radar source</span></div>';
    html +=       '<div class="cs-formula-line">' +
                    '<strong>0.70 × ' + bestDealDim.v.toFixed(2) + '</strong> (' + _CS_DEAL_INFO[bestDealDim.k].label + ') ' +
                    '<strong>+ 0.30 × ' + secondDealDim.v.toFixed(2) + '</strong> (' + _CS_DEAL_INFO[secondDealDim.k].label + ') = ' +
                    '<span class="result">' + strategyScore.toFixed(2) + '</span>' +
                  '</div>';
    html +=     '</div>';
    html +=   '</div>';
    html += '</details>';
  }

  return html;
}

/** Build the rich hover-tooltip HTML for a partner row. */
function _buildPartnerTooltipContent(d) {
  if (!d) return '';
  var pair = (typeof _getPair === 'function') ? _getPair(d) : { partnerName: d.org_b_name || '' };
  var name = _fixOrgName(pair.partnerName || d.org_b_name || '');
  var strategyScore = _clamp(d.strategy_score || d.recommended_deal_score || d.collab_iq_score || d.collab_score || 0);
  var strength = (d.strategy_strength || 'PENDING').toUpperCase();
  var recDeal = d.recommended_deal_type || 'CO_DEVELOPMENT';
  var recInfo = _CS_DEAL_INFO[recDeal] || _CS_DEAL_INFO.CO_DEVELOPMENT;
  var recColor = _csDealColor(recDeal);
  var isInferred = d.partner_origin === 'INFERRED';
  var cat = d.org_b_category || '';

  var dims = [
    { key: 'CO_DEVELOPMENT',       score: _clamp(d.co_development_score) },
    { key: 'PLATFORM_PARTNERSHIP', score: _clamp(d.platform_partnership_score) },
    { key: 'PORTFOLIO_EXPANSION',  score: _clamp(d.portfolio_expansion_score) },
    { key: 'IN_LICENSING',         score: _clamp(d.in_licensing_score) }
  ].sort(function(a, b) { return b.score - a.score; });

  var bars = '';
  dims.forEach(function(dm) {
    var info = _CS_DEAL_INFO[dm.key];
    var c = _csDealColor(dm.key);
    bars += '<div class="ptt-bar">' +
            '<span class="ptt-bar-label">' + info.short + '</span>' +
            '<div class="ptt-bar-track"><div class="ptt-bar-fill" style="width:' + (dm.score * 100).toFixed(0) + '%;background:' + c + '"></div></div>' +
            '<span class="ptt-bar-val" style="color:' + c + '">' + dm.score.toFixed(2) + '</span>' +
            '</div>';
  });

  var html = '<div class="ptt-header">' +
               '<span class="ptt-name">' + name + '</span>' +
               '<span class="ptt-overall">' + strategyScore.toFixed(2) + '</span>' +
             '</div>';
  html += '<div class="ptt-meta">' +
            '<span class="ptt-strength ' + strength.toLowerCase() + '">' + strength + '</span>' +
            (isInferred ? '<span class="ptt-inferred">INFERRED</span>' : '') +
            (cat ? '<span class="ptt-cat">' + cat + '</span>' : '') +
          '</div>';
  html += '<div class="ptt-divider"></div>';
  html += '<div class="ptt-section-label">Deal-type fit</div>';
  html += '<div class="ptt-bars">' + bars + '</div>';
  html += '<div class="ptt-rec" style="--rec-color:' + recColor + '">Recommended · ' + recInfo.label + '</div>';
  html += '<p class="ptt-rationale">' + _csRationale(recDeal, d) + '</p>';
  html += '<div class="ptt-hint">Click the dot to lock this partner</div>';
  return html;
}

/** Show the rich partner tooltip. Called from radar mouseover handler. */
function _showPartnerTooltip(idx, evt) {
  var data = _getState().strat_data || [];
  var d = data[idx];
  if (!d) return;
  var tt = document.getElementById('partner-tooltip');
  if (!tt) {
    tt = document.createElement('div');
    tt.id = 'partner-tooltip';
    tt.className = 'partner-tooltip';
    document.body.appendChild(tt);
  }
  tt.innerHTML = _buildPartnerTooltipContent(d);
  _movePartnerTooltip(evt);
  tt.classList.add('visible');
}

/** Move the tooltip — called on mousemove for cursor-following behavior. */
function _movePartnerTooltip(evt) {
  var tt = document.getElementById('partner-tooltip');
  if (!tt || !evt) return;
  var rect = tt.getBoundingClientRect();
  var pad = 16;
  var x = evt.clientX, y = evt.clientY;
  // Default: tooltip to the right of cursor, above
  var placeX = x + pad;
  if (placeX + rect.width > window.innerWidth - pad) {
    placeX = x - rect.width - pad; // flip left if would clip right edge
  }
  if (placeX < pad) placeX = pad;
  var placeY = y - rect.height - pad;
  if (placeY < pad) placeY = y + pad; // flip below if clips top
  tt.style.left = placeX + 'px';
  tt.style.top = placeY + 'px';
}

/** Hide the partner tooltip. Called from mouseout. */
function _hidePartnerTooltip() {
  var tt = document.getElementById('partner-tooltip');
  if (tt) tt.classList.remove('visible');
}

window._showPartnerTooltip = _showPartnerTooltip;
window._movePartnerTooltip = _movePartnerTooltip;
window._hidePartnerTooltip = _hidePartnerTooltip;

/** Re-render the right panel from current state. */
function _updateCollabScorePanel() {
  var mount = document.getElementById('collabiq-score-panel-mount');
  if (!mount) return;
  var data = _getState().strat_data || [];
  var lockedIdx = _getState()._compRadarLocked;
  var d = (lockedIdx !== undefined && lockedIdx !== null) ? data[lockedIdx] : null;
  if (!d && data.length) {
    var sorted = data.slice().sort(function(a, b) {
      return (_clamp(b.collab_iq_score || b.collab_score || 0)) - (_clamp(a.collab_iq_score || a.collab_score || 0));
    });
    d = sorted[0];
  }
  var activeDeal = _getState()._scenarioActiveDeal || null;
  var anchorOrg = _getState().strat_anchor_org || '';
  mount.innerHTML = _buildCollabScorePanel(d, activeDeal, anchorOrg);
  // CTK 2026-05-12: post-mount enrichment — swap pair-level URGENCY +
  // OPPORTUNITY for disease-specific values from /v2/disruption-signals
  // and rewrite the takeaway with the dominant signal's partner_frame.
  _enrichWhyNowFromDisruption(mount);
}

/**
 * Async enrichment for the Why-now triangle (PAIR mode).
 *
 * Synchronous render uses pair-level fallbacks (sf_modality_migration_signal,
 * etc.) which only expose ONE of the 5 canonical disruption signal types.
 * After mount, fetch /v2/disruption-signals/pair which aggregates ALL 5
 * disruption signals across the pair's top shared diseases via JOIN to
 * the disruption_signals table. Replace URGENCY and OPPORTUNITY with
 * those pair-aggregated values; EXECUTION stays as the synchronous
 * pair-level capacity score (n_co_trials + n_shared_targets + n_shared_patents).
 *
 * Each partner row has a different top_shared_diseases list -> different
 * JOIN result -> different aggregate -> different triangle shape. Truly
 * dynamic per row, derived entirely from real disease-level disruption
 * signals already in DuckDB.
 */
function _enrichWhyNowFromDisruption(rootEl) {
  if (!rootEl || !window.DASHIQ || !window.DASHIQ.fetchDisruptionSignals) return;
  var nodes = rootEl.querySelectorAll('.cs-whynow[data-org-a][data-org-b]');
  for (var i = 0; i < nodes.length; i++) (function (wn) {
    var orgA = wn.getAttribute('data-org-a');
    var orgB = wn.getAttribute('data-org-b');
    if (!orgA || !orgB) return;
    window.DASHIQ.fetchDisruptionSignals({ org_a: orgA, org_b: orgB })
      .then(function (resp) {
        var sigs = (resp && resp.signals) || {};
        var pc = sigs.patent_cliff;
        var mm = sigs.modality_migration;
        var ct = sigs.cross_ta_expansion;
        var ne = sigs.new_entrant_surge;
        var mo = sigs.moa_convergence;

        // URGENCY = pair's aggregated patent_cliff signal across shared diseases.
        var urg = (pc && typeof pc.strength === 'number') ? pc.strength : null;
        // OPPORTUNITY = mean of modality_migration + cross_ta_expansion
        // (platform shift + TA boundary expansion — together they describe
        // "where the field is moving" across the pair's shared portfolio).
        var oppParts = [];
        if (mm && typeof mm.strength === 'number') oppParts.push(mm.strength);
        if (ct && typeof ct.strength === 'number') oppParts.push(ct.strength);
        var opp = oppParts.length ? oppParts.reduce(function(a,b){return a+b;},0) / oppParts.length : null;
        // EXECUTION stays pair-capacity (synchronous), but we ALSO have
        // pair-aggregated pressure signals — surface them as a secondary
        // read by averaging new_entrant_surge + moa_convergence and
        // blending 50/50 with synchronous execution if both exist.
        var pressureParts = [];
        if (ne && typeof ne.strength === 'number') pressureParts.push(ne.strength);
        if (mo && typeof mo.strength === 'number') pressureParts.push(mo.strength);
        // Inversion: high pressure = LOW execution-room. Subtract from 1.
        var pressureMean = pressureParts.length ? pressureParts.reduce(function(a,b){return a+b;},0) / pressureParts.length : null;

        function _ptOnAxis(vx, vy, s) { return [(60 + (vx - 60) * s).toFixed(2), (66 + (vy - 66) * s).toFixed(2)]; }
        var svg = wn.querySelector('svg');
        if (svg) {
          var execText = wn.querySelector('.cs-wn-val-exe');
          var execScoreSync = execText ? parseFloat(execText.textContent) || 0 : 0;
          // Execution combines pair capacity (sync) with inverse pressure
          // (pair-aggregated). If both available, average. If only sync,
          // use as-is. If only pressure, invert it.
          var execScore;
          if (pressureMean != null && execScoreSync > 0) {
            execScore = (execScoreSync + (1 - pressureMean)) / 2;
          } else if (pressureMean != null) {
            execScore = 1 - pressureMean;
          } else {
            execScore = execScoreSync;
          }
          var urgUsed = (urg != null) ? urg : (parseFloat((wn.querySelector('.cs-wn-val-urg')||{}).textContent) || 0);
          var oppUsed = (opp != null) ? opp : (parseFloat((wn.querySelector('.cs-wn-val-opp')||{}).textContent) || 0);
          var pTop   = _ptOnAxis(60, 18, urgUsed);
          var pLeft  = _ptOnAxis(17, 90, oppUsed);
          var pRight = _ptOnAxis(103, 90, execScore);
          var plot = svg.querySelector('.cs-wn-plot');
          if (plot) plot.setAttribute('points', pTop[0]+','+pTop[1]+' '+pRight[0]+','+pRight[1]+' '+pLeft[0]+','+pLeft[1]);
          var vUrg = svg.querySelector('.cs-wn-vtx-urg'); if (vUrg) { vUrg.setAttribute('cx', pTop[0]);   vUrg.setAttribute('cy', pTop[1]); }
          var vOpp = svg.querySelector('.cs-wn-vtx-opp'); if (vOpp) { vOpp.setAttribute('cx', pLeft[0]);  vOpp.setAttribute('cy', pLeft[1]); }
          var vExe = svg.querySelector('.cs-wn-vtx-exe'); if (vExe) { vExe.setAttribute('cx', pRight[0]); vExe.setAttribute('cy', pRight[1]); }
          if (urg != null) { var tUrg = svg.querySelector('.cs-wn-val-urg'); if (tUrg) tUrg.textContent = urg.toFixed(2); }
          if (opp != null) { var tOpp = svg.querySelector('.cs-wn-val-opp'); if (tOpp) tOpp.textContent = opp.toFixed(2); }
          var tExe = svg.querySelector('.cs-wn-val-exe'); if (tExe) tExe.textContent = execScore.toFixed(2);
        }

        // Re-pick dominant axis with enriched values.
        var execText2 = wn.querySelector('.cs-wn-val-exe');
        var execScore2 = execText2 ? parseFloat(execText2.textContent) || 0 : 0;
        var enriched = [
          { key: 'urgency',     label: 'Urgency',     val: (urg != null ? urg : (parseFloat((wn.querySelector('.cs-wn-val-urg')||{}).textContent) || 0)) },
          { key: 'opportunity', label: 'Opportunity', val: (opp != null ? opp : (parseFloat((wn.querySelector('.cs-wn-val-opp')||{}).textContent) || 0)) },
          { key: 'execution',   label: 'Execution',   val: execScore2 }
        ].sort(function(a,b){return b.val - a.val;});
        var dom = enriched[0];
        var cueEl = wn.querySelector('.cs-wn-cue');
        if (cueEl) {
          cueEl.className = 'cs-wn-cue ' + dom.key;
          cueEl.textContent = dom.label + ' ' + dom.val.toFixed(2);
        }
      })
      .catch(function () { /* backend down — keep synchronous fallback values */ });
  })(nodes[i]);
}

window._enrichWhyNowFromDisruption = _enrichWhyNowFromDisruption;

// ─── 6. _onPartnerHover / _onPartnerHoverOut ─────────────────────────────────

/**
 * Preview pentagon on ranked-list hover.
 * V1 L28157 parity.
 */
function _onPartnerHover(absIdx) {
  var locked = _getState()._compRadarLocked;
  if (locked !== undefined && locked !== null) return; // keep lock
  _updateComparisonRadar(absIdx);
}

/** Restore locked partner or blank on mouseout */
function _onPartnerHoverOut() {
  var locked = _getState()._compRadarLocked;
  if (locked !== undefined && locked !== null) {
    _updateComparisonRadar(locked);
  }
}

// ─── 7. _selectRankedPartner ─────────────────────────────────────────────────

/**
 * Lock a ranked-list row and open evidence panel.
 * V1 L26928 parity.
 */
function _selectRankedPartner(absIdx, rowEl) {
  // Clear previous is-active
  var prev = document.querySelectorAll('#strat-rank-list .rank-row.is-active');
  prev.forEach(function(el) { el.classList.remove('is-active'); });

  if (rowEl) rowEl.classList.add('is-active');

  _getState()._compRadarLocked = absIdx;
  _getState()._strat_selected_partner = ((_getState().strat_data || [])[absIdx] || {}).org_b_name || '';
  _updateComparisonRadar(absIdx);
  if (typeof showStrategyDetail === 'function') showStrategyDetail(absIdx, false);
}

// ─── 8. _renderRankedList ────────────────────────────────────────────────────

/**
 * Render top-7 ranked partner list into #strat-rank-list.
 * V1 L26852 parity. Top 7 NOT top 10.
 * Filtered by activeDeal when set; sorted by deal score desc.
 */
function _renderRankedList(results, activeDeal) {
  var container = document.getElementById('strat-rank-list');
  if (!container) return;

  var data = Array.isArray(results) ? results : [];

  // Sort by deal score desc
  var sorted = data.slice().sort(function(a, b) {
    return _clamp(b.recommended_deal_score || b.deal_score || 0) - _clamp(a.recommended_deal_score || a.deal_score || 0);
  });

  // Filter by activeDeal if set
  if (activeDeal) {
    sorted = sorted.filter(function(d) { return d.recommended_deal_type === activeDeal; });
  }

  // Top 7
  var top7 = sorted.slice(0, 7);

  // Update header
  var headerDeal = document.getElementById('strat-rank-header-deal');
  var headerTag = document.getElementById('strat-rank-header-tag');
  if (headerDeal) {
    headerDeal.textContent = activeDeal
      ? (DEAL_TYPES.find(function(dt) { return dt.key === activeDeal; }) || {}).label || 'Top Partners'
      : 'Top Partners';
  }
  if (headerTag) {
    headerTag.textContent = activeDeal ? 'Ranked by deal score' : '';
  }

  if (!top7.length) {
    container.innerHTML = '<div style="padding:var(--s-4);text-align:center;color:var(--text-faint);font-size:var(--text-xs)">No partners in this quadrant</div>';
    return;
  }

  // Map sorted index back to absolute index in original results array
  var html = '';
  top7.forEach(function(d, rank) {
    var absIdx = data.indexOf(d);
    if (absIdx < 0) absIdx = rank; // fallback
    var pair = _getPair(d);
    var name = _fixOrgName(pair.partnerName || d.org_b_name || '');
    var score = _clamp(d.recommended_deal_score || d.deal_score || 0);
    var collab = _clamp(d.collab_iq_score || d.collab_score || 0);
    var cat = d.org_b_category || d.org_category || '';
    var isActive = _getState()._compRadarLocked === absIdx;
    var scoreColor = _dealScoreColor(score);

    html += '<div class="rank-row' + (isActive ? ' is-active' : '') + '" ' +
            'data-abs-idx="' + absIdx + '" ' +
            'onclick="_selectRankedPartner(' + absIdx + ', this)" ' +
            'onmouseenter="_onPartnerHover(' + absIdx + ')" ' +
            'onmouseleave="_onPartnerHoverOut()" ' +
            'style="display:flex;flex-direction:column;gap:2px;padding:var(--s-2) var(--s-3);cursor:pointer;border-bottom:1px solid var(--g-border);min-height:44px;transition:background var(--t-fast)">';

    html += '<div style="display:flex;align-items:center;gap:var(--s-2)">';
    html += '<span style="font-size:10px;font-family:var(--font-mono);color:var(--text-faint);min-width:16px;text-align:right">' + (rank + 1) + '</span>';
    html += '<span style="font-weight:600;font-size:12px;color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + _orgXLink(name, 'strategy') + '</span>';
    if (cat) html += '<span class="prim-pill prim-pill-soft prim-pill-accent-slate" style="font-size:9px;padding:1px 5px;flex-shrink:0">' + cat + '</span>';
    html += '</div>';

    html += '<div style="display:flex;align-items:center;gap:var(--s-3);padding-left:24px">';
    html += '<span style="font-size:11px;font-family:var(--font-mono);font-weight:700;color:var(--' + scoreColor + ')">' + _fmt2(score) + '</span>';
    html += '<span style="font-size:10px;color:var(--text-faint)">CollabIQ ' + Math.round(collab * 100) + '%</span>';
    html += '</div>';
    html += '</div>';
  });

  container.innerHTML = html;
}

// ─── 9. EVIDENCE PANEL ───────────────────────────────────────────────────────

/**
 * Build evidence panel HTML for a data row.
 * V1 L29032+ parity.
 */
function _buildEvidenceHTML(d) {
  var pair = _getPair(d);
  var anchorName = _fixOrgName(pair.anchorName);
  var partnerName = _fixOrgName(pair.partnerName);
  var evSummary = parseEvidenceSummary(d.evidence_summary);
  var evBreadth = evSummary.length;
  var evDepth = evSummary.reduce(function(acc, e) {
    return acc + (e.entities ? e.entities.length : 0);
  }, 0);

  // Confidence tier
  var tier = 'bronze', tierLabel = 'BRONZE', tierText = 'Directional signal';
  if (evBreadth >= 4 && evDepth >= 10) { tier = 'gold'; tierLabel = 'GOLD'; tierText = 'Multi-source'; }
  else if (evBreadth >= 2 && evDepth >= 5) { tier = 'silver'; tierLabel = 'SILVER'; tierText = 'Moderate corroboration'; }

  // Missing sources (GAPS)
  var required = ['ClinicalTrials', 'DrugBank_MOA', 'DrugBank_Targets', 'USPTO', 'MechPeer'];
  var present = evSummary.map(function(e) { return e.srcKey; });
  var gaps = required.filter(function(k) { return present.indexOf(k) < 0; });

  var h = '';

  // Header
  h += '<div style="display:flex;align-items:center;gap:var(--s-2);margin-bottom:var(--s-3)">';
  h += '<span style="font-size:var(--text-sm);font-weight:700;color:var(--text)">Evidence</span>';
  h += '<span class="prim-badge prim-badge-sm prim-badge-tier-' + tier + '">' + tierLabel + '</span>';
  h += '<em style="font-size:var(--text-xs);color:var(--text-faint);font-style:italic">' + tierText + '</em>';
  h += '</div>';

  // Deal recommendation
  var arc = DEAL_ARCHETYPE_COPY[d.recommended_deal_type] || {};
  h += '<div style="margin-bottom:var(--s-3);padding:var(--s-2) var(--s-3);background:var(--g-spatial);border-radius:var(--r-md);border:1px solid var(--g-border)">';
  h += '<div style="font-size:10px;font-family:var(--font-mono);color:var(--text-faint);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px">Recommended</div>';
  h += dealBadge(d.recommended_deal_type);
  if (d.recommended_deal_score) {
    h += ' <span style="font-size:11px;font-family:var(--font-mono);font-weight:700;color:var(--' + _dealScoreColor(d.recommended_deal_score) + ')">' + _fmt2(d.recommended_deal_score) + '</span>';
  }
  if (arc.blurb) {
    h += '<p style="font-size:12px;color:var(--text-muted);margin:6px 0 0;line-height:1.5">' + arc.blurb + '</p>';
  }
  h += '</div>';

  // Evidence cards keyed on the score-ring axes (Partner Alignment /
  // Target Convergence / Modality Fit / Disease Overlap). Pre-fix this
  // loop was per-source (one card per ClinicalTrials/USPTO/etc), which
  // showed a different framing than the four-dim score ring above the
  // panel. Cards now align top-to-bottom on the same four axes.
  var DIM_KEYS = ['PARTNER_ALIGNMENT', 'TARGET_CONVERGENCE', 'MODALITY_FIT', 'DISEASE_OVERLAP'];
  // Map evidence source keys to one of the four dimensions. Sources the
  // current API ships are anchored here; the placeholder path covers any
  // dimension whose bucket comes back empty.
  var SRC_TO_DIM = {
    'ClinicalTrials':   'PARTNER_ALIGNMENT',
    'USPTO':            'PARTNER_ALIGNMENT',
    'MechPeer':         'PARTNER_ALIGNMENT',
    'DrugBank_Targets': 'TARGET_CONVERGENCE',
    'PubMed':           'TARGET_CONVERGENCE',
    'DrugBank_MOA':     'MODALITY_FIT',
    'FDALabels':        'DISEASE_OVERLAP',
  };
  var dimScores = (typeof _csPatentDims === 'function') ? _csPatentDims(d) : {};
  // Bucket evSummary entries onto the four dims.
  var byDim = { PARTNER_ALIGNMENT: [], TARGET_CONVERGENCE: [], MODALITY_FIT: [], DISEASE_OVERLAP: [] };
  for (var _i = 0; _i < evSummary.length; _i++) {
    var _ev = evSummary[_i];
    var _dimKey = SRC_TO_DIM[_ev.srcKey || ''];
    if (_dimKey && byDim[_dimKey]) byDim[_dimKey].push(_ev);
  }

  DIM_KEYS.forEach(function(dimKey) {
    var info = (typeof _CS_DIM_INFO !== 'undefined' && _CS_DIM_INFO[dimKey]) || { label: dimKey, color: '#64748b' };
    var dimScore = _clamp(dimScores[dimKey] || 0);
    var bucket = byDim[dimKey] || [];
    var hasSignals = bucket.length > 0;
    var dotColor = hasSignals
      ? (dimScore >= 0.7 ? 'var(--teal)' : dimScore >= 0.4 ? 'var(--amber)' : 'var(--rose)')
      : 'var(--text-faint)';

    h += '<div class="ev-chain-card" style="padding:var(--s-2) var(--s-3);margin-bottom:var(--s-2);background:var(--g-spatial);border-radius:var(--r-md);border:1px solid var(--g-border)">';
    h += '<div style="display:flex;align-items:center;gap:var(--s-2);margin-bottom:4px">';
    h += '<span style="width:8px;height:8px;border-radius:50%;background:' + dotColor + ';flex-shrink:0;display:inline-block"></span>';
    h += '<strong style="font-size:11px;color:var(--text)">' + info.label + '</strong>';
    h += '<span style="font-size:10px;font-family:var(--font-mono);font-weight:600;color:' + info.color + ';margin-left:auto">' + Math.round(dimScore * 100) + '%</span>';
    h += '</div>';

    if (!hasSignals) {
      h += '<p style="font-size:11px;color:var(--text-faint);font-style:italic;margin:0;line-height:1.45">no detailed signals yet for this dimension</p>';
    } else {
      // Aggregate narratives + entities from the bucket. Show the highest-
      // confidence narrative, plus up to 5 unique entities across sources.
      var sorted = bucket.slice().sort(function(a, b) { return (b.confidence || 0) - (a.confidence || 0); });
      var topNarr = '';
      for (var i = 0; i < sorted.length; i++) {
        if (sorted[i].narrative) { topNarr = sorted[i].narrative; break; }
      }
      if (topNarr) {
        h += '<p style="font-size:11px;color:var(--text-muted);margin:0 0 4px;line-height:1.45">' + topNarr + '</p>';
      }
      var seen = {};
      var entSet = [];
      sorted.forEach(function(ev) {
        var ents = Array.isArray(ev.entities) ? ev.entities : [];
        ents.forEach(function(ent) {
          if (entSet.length >= 5) return;
          var key = String(ent || '').trim();
          if (!key || seen[key]) return;
          seen[key] = 1;
          entSet.push(key);
        });
      });
      if (entSet.length) {
        h += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
        entSet.forEach(function(ent) {
          h += '<span class="prim-pill prim-pill-soft prim-pill-accent-teal" style="font-size:9px">' + ent + '</span>';
        });
        h += '</div>';
      }
    }
    h += '</div>';
  });

  // GAPS box
  if (gaps.length) {
    h += '<div style="margin-bottom:var(--s-3);padding:var(--s-2) var(--s-3);background:color-mix(in srgb,var(--rose) 8%,var(--g-ground));border-radius:var(--r-md);border:1px solid color-mix(in srgb,var(--rose) 25%,transparent)">';
    h += '<div style="font-size:10px;font-family:var(--font-mono);font-weight:700;color:var(--rose);margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">Gaps</div>';
    h += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
    gaps.forEach(function(g) {
      h += '<span class="prim-pill prim-pill-soft prim-pill-accent-rose" style="font-size:9px">' + _sourceLabel(g) + '</span>';
    });
    h += '</div>';
    h += '</div>';
  }

  // KG footer — DEFECT-004 fix: read the actual fields backend ships
  // (shared_diseases, evidence.{indications,targets,patents}, org_*_target_count
  // / org_*_patent_count) instead of the never-shipped `indication_count` /
  // `target_count` / `kg_updated` field names. Falls back across multiple
  // shapes for robustness across HERO snapshots vs live SQL.
  var ev = d.evidence || {};
  var kgDate = d.kg_updated || d.updated_at || (window.STATE && window.STATE.last_etl_at) || '';
  var indCount = d.shared_diseases
                 || ev.indications
                 || d.indication_count
                 || d.indications
                 || 0;
  var targetCount = d.shared_targets
                    || ev.targets
                    || d.target_count
                    || d.targets
                    || Math.max(d.org_a_target_count || 0, d.org_b_target_count || 0);
  var patentCount = d.shared_patents
                    || ev.patents
                    || d.patent_count
                    || d.patents
                    || 0;
  h += '<div style="margin-top:var(--s-3);padding:var(--s-2) var(--s-3);background:var(--g-spatial);border-radius:var(--r-md);border:1px solid var(--g-border)">';
  if (kgDate) {
    h += '<div style="font-size:10px;color:var(--text-faint);margin-bottom:4px">Updated ' + kgDate + '</div>';
  }
  // Audit D4: when all three counts are zero we previously rendered
  //   "0 indications · 0 targets · 0 patents"
  // which reads as a stale/empty data state in front of investors. Now we
  // show a graceful "pending data" line instead — visibly different and
  // clearly signals that the absence is data freshness, not a real null.
  var totalCounts = (indCount || 0) + (targetCount || 0) + (patentCount || 0);
  if (totalCounts > 0) {
    h += '<div style="font-size:10px;font-family:var(--font-mono);color:var(--text-faint)">' +
         indCount + ' indications · ' + targetCount + ' targets · ' + patentCount + ' patents</div>';
  } else {
    h += '<div style="font-size:10px;font-family:var(--font-mono);color:var(--text-faint);font-style:italic">' +
         'Evidence counts pending data refresh</div>';
  }
  h += '</div>';

  // Data sources footer (teal box)
  h += '<div style="margin-top:var(--s-2);padding:var(--s-2) var(--s-3);background:color-mix(in srgb,var(--teal) 8%,var(--g-ground));border-radius:var(--r-md);border:1px solid color-mix(in srgb,var(--teal) 25%,transparent)">';
  h += '<div style="font-size:9px;font-family:var(--font-mono);color:var(--teal);font-weight:700;margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">Data Sources</div>';
  h += '<div style="font-size:9px;color:var(--text-muted)">ClinicalTrials.gov · FDA FAERS · DrugCentral · USPTO · UMLS · CollabIQ™ · PubMed · FDA Orange Book</div>';
  h += '</div>';

  // Disclaimer
  h += '<p style="font-size:9px;color:var(--text-faint);font-style:italic;margin:var(--s-2) 0 0;text-align:center">Representative signal — full evidence set available on request.</p>';

  return h;
}

// ─── 10. openStratDrawer / closePartDrawer ───────────────────────────────────

/**
 * Open evidence panel (desktop inline) or mobile drawer.
 * V1 L28888 parity.
 * isMobile check is dynamic (NOT hardcoded).
 */
function openStratDrawer(d) {
  var isMobile = window.innerWidth <= 768;
  var pair = _getPair(d);
  var evHTML = _buildEvidenceHTML(d);
  var title = _fixOrgName(pair.anchorName) + ' × ' + _fixOrgName(pair.partnerName);

  if (isMobile) {
    // Mobile drawer path
    var drawerTitle = document.getElementById('part-drawer-title');
    var drawerBody = document.getElementById('part-drawer-body');
    var drawer = document.getElementById('part-drawer');
    if (drawerTitle) drawerTitle.textContent = title;
    if (drawerBody) drawerBody.innerHTML = evHTML;
    if (drawer) drawer.classList.add('open');
    _showMobileBackdrop();
  } else {
    // Desktop inline evidence panel
    var panel = document.getElementById('strat-evidence-panel');
    if (!panel) return;
    panel.innerHTML = '';
    var inner = document.createElement('div');
    inner.className = 'ev-panel-inner';
    inner.style.cssText = 'display:flex;flex-direction:column;height:100%;overflow:hidden';

    var head = document.createElement('div');
    head.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:var(--s-3) var(--s-4);border-bottom:1px solid var(--g-border);flex-shrink:0';
    head.innerHTML = '<div id="strat-ev-title" style="font-size:var(--text-sm);font-weight:700;color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + title + '</div>';

    var closeBtn = document.createElement('button');
    closeBtn.setAttribute('aria-label', 'Close evidence panel');
    closeBtn.style.cssText = 'cursor:pointer;font-size:18px;color:var(--text-muted);line-height:1;min-width:44px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--r-sm);transition:all var(--t-fast);background:transparent;border:none';
    closeBtn.innerHTML = typeof window.CloseIcon !== 'undefined'
      ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
      : '×';
    closeBtn.onclick = function() { closePartDrawer(); };
    head.appendChild(closeBtn);
    inner.appendChild(head);

    var body = document.createElement('div');
    body.id = 'strat-ev-body';
    body.style.cssText = 'flex:1;overflow-y:auto;padding:var(--s-4)';
    body.innerHTML = evHTML;
    inner.appendChild(body);
    panel.appendChild(inner);
    panel.style.width = '340px';
    panel.style.flexShrink = '0';
    panel.style.display = 'flex';
  }
}

/**
 * Close evidence panel / drawer.
 * V1 L21440 parity.
 */
function closePartDrawer() {
  var isMobile = window.innerWidth <= 768;
  if (isMobile) {
    var drawer = document.getElementById('part-drawer');
    if (drawer) drawer.classList.remove('open');
    _hideMobileBackdrop();
  }
  var panel = document.getElementById('strat-evidence-panel');
  if (panel) {
    panel.innerHTML = '';
    panel.style.width = '0';
    panel.style.display = 'none';
  }
  // Clear lock
  _getState()._compRadarLocked = null;
}

// ─── 11. showStrategyDetail ───────────────────────────────────────────────────

/**
 * Open evidence panel for table row at absIdx.
 * V1 L28817 parity.
 * Re-renders table to highlight selected row.
 */
function showStrategyDetail(idx, isAuto) {
  var data = _getState().strat_data || [];
  var d = data[idx];
  if (!d) return;

  _getState()._strat_selected_idx = idx;
  _getState()._strat_selected_partner = d.org_b_name || '';
  _getState()._compRadarLocked = idx;

  // Update comparison radar with ghost trails
  _updateComparisonRadar(idx);

  // Load org radar for disruption data
  _loadOrgRadar(d);

  // Re-render table to highlight row
  renderStrategyTable();

  // Vibe Architect 2026-05-02 — refresh right-panel CollabIQ Score + deck
  // (replaces the prior #collab-ring-mount innerHTML update; the small donut
  //  beneath the radar was removed in favor of the supersized version on the right.)
  _updateCollabScorePanel();

  // Open drawer
  openStratDrawer(d);

  // Scroll selected row into view on manual click
  if (!isAuto) {
    var rowEl = document.querySelector('[data-abs-idx="' + idx + '"]');
    if (!rowEl) {
      rowEl = document.querySelector('tr[data-stratidx="' + idx + '"]');
    }
    if (rowEl) rowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  }
}

// ─── 12. _loadOrgRadar ───────────────────────────────────────────────────────

/**
 * Load org radar data for disruption + ghost trail.
 * V1 L28407 parity — uses rankCompete side-load.
 */
function _loadOrgRadar(d, targetId) {
  if (!d) return;
  var pair = _getPair(d);
  var orgB = pair.partnerName || d.org_b_name || '';
  if (!orgB) return;
  // Side-load compete rank for partner (disruption data)
  if (window.DASHIQ && typeof window.DASHIQ.rankCompete === 'function') {
    window.DASHIQ.rankCompete({ org: orgB, limit: 1 }).then(function(res) {
      if (!res) return;
      var rows = Array.isArray(res) ? res : (res.results || res.data || []);
      if (rows[0] && rows[0].disruption_modifier) {
        d._disruption_modifier = rows[0].disruption_modifier;
      }
    }).catch(function() {});
  }
}

// ─── 13. renderStrategyTable ─────────────────────────────────────────────────

/**
 * Render the Partnership Opportunities table.
 * V1 L28662 parity.
 * Page size 10. Columns: PARTNER / DEAL SCORE / RECOMMENDED / CollabIQ™.
 * Sort by recommended_deal_score desc default.
 * Pagination uses window.primitives.Pagination (React) rendered into a container.
 */
/** Render the deal-type filter chips above the table.
 *  Phase 3 · Vibe Architect 2026-05-03. Counts come from the UNFILTERED cohort
 *  so the chip badges always show the full distribution. Click → toggles
 *  _stratDealChip (and clears if same chip clicked twice). */
function _renderStratFilterChips() {
  var mount = document.getElementById('strat-filter-chips');
  if (!mount) return;
  var data = _getState().strat_data || [];
  var activeChip = _getState()._stratDealChip || null;
  var counts = data.reduce(function(acc, d) {
    var k = d.recommended_deal_type || 'UNKNOWN';
    acc[k] = (acc[k] || 0) + 1;
    return acc;
  }, {});
  var total = data.length;
  // Chip styling — audit D2: 0-count chips now render disabled (faded +
  // not clickable) so users don't tap into a dead filter.
  // Theme-aware bg + countBadge bg — inline style attrs override CSS, so the
  // light/dark swap has to happen at build time. Mirrors the _sonarIsLight
  // pattern used by the radar block (L363).
  var _chipIsLight = document.documentElement.getAttribute('data-theme') === 'light';
  var _chipBaseBg = _chipIsLight ? 'rgba(15,23,42,0.04)' : 'rgba(255,255,255,0.04)';
  var _chipBadgeBg = _chipIsLight ? 'rgba(15,23,42,0.06)' : 'rgba(255,255,255,0.06)';
  var _chipActiveColor = _chipIsLight ? '#0d8f86' : 'var(--teal-bright)';
  var baseStyle = 'display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:100px;font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;cursor:pointer;border:1px solid var(--g-border);background:' + _chipBaseBg + ';color:var(--text-muted);user-select:none';
  var activeStyle = ';background:rgba(0,229,191,0.10);color:' + _chipActiveColor + ';border-color:rgba(13,143,134,0.40)';
  var disabledStyle = ';opacity:0.40;cursor:not-allowed;pointer-events:none';
  function chipHTML(key, label, count, color, isActive) {
    var isEmpty = (key !== '' && (count === 0 || count == null));
    var dot = color ? '<span style="width:5px;height:5px;border-radius:50%;background:' + color + ';display:inline-block"></span>' : '';
    var countBadge = '<span style="padding:0 5px;background:' + _chipBadgeBg + ';border-radius:6px;font-size:9px;opacity:0.85">' + count + '</span>';
    var styleStack = baseStyle + (isActive ? activeStyle : '') + (isEmpty ? disabledStyle : '');
    var clickAttr = isEmpty ? '' : 'onclick="_setStratChip(\'' + key + '\')" ';
    return '<button type="button" class="strat-filter-chip' + (isActive ? ' active' : '') + (isEmpty ? ' empty' : '') + '" data-chip="' + key + '" ' +
           clickAttr +
           (isEmpty ? 'aria-disabled="true" tabindex="-1" ' : '') +
           'style="' + styleStack + '">' +
             dot +
             '<span>' + label + '</span>' +
             countBadge +
           '</button>';
  }
  var html = '';
  html += chipHTML('', 'All', total, null, !activeChip);
  html += chipHTML('CO_DEVELOPMENT', 'Co-Dev', counts.CO_DEVELOPMENT || 0, _csDealColor('CO_DEVELOPMENT'), activeChip === 'CO_DEVELOPMENT');
  html += chipHTML('PLATFORM_PARTNERSHIP', 'R&D', counts.PLATFORM_PARTNERSHIP || 0, _csDealColor('PLATFORM_PARTNERSHIP'), activeChip === 'PLATFORM_PARTNERSHIP');
  html += chipHTML('PORTFOLIO_EXPANSION', 'M&A', counts.PORTFOLIO_EXPANSION || 0, _csDealColor('PORTFOLIO_EXPANSION'), activeChip === 'PORTFOLIO_EXPANSION');
  html += chipHTML('IN_LICENSING', 'License', counts.IN_LICENSING || 0, _csDealColor('IN_LICENSING'), activeChip === 'IN_LICENSING');
  mount.innerHTML = html;
}

/** Toggle the table's deal-type filter chip. Empty string clears. Re-renders table. */
function _setStratChip(key) {
  var current = _getState()._stratDealChip || null;
  // Clicking same chip → clear; else set
  _getState()._stratDealChip = (current === key || key === '') ? null : key;
  _getState()._stratPage = 0;
  _renderStratFilterChips();
  renderStrategyTable();
}
window._setStratChip = _setStratChip;

function renderStrategyTable() {
  var wrap = document.getElementById('strat-table-wrap');
  if (!wrap) return;

  // Refresh filter chips alongside the table render
  _renderStratFilterChips();

  var data = _getState().strat_data || [];
  var activeDeal = _getState()._scenarioActiveDeal || null;
  var dealChip = _getState()._stratDealChip || null;
  var sortCol = _getState()._stratSortCol || 'recommended_deal_score';
  var sortDir = _getState()._stratSortDir || 'desc';
  var page = _getState()._stratPage || 0;
  var selectedIdx = _getState()._strat_selected_idx;
  var PAGE_SIZE = 10;

  // Filter by active deal type
  var filtered = data.filter(function(d, i) {
    if (dealChip && d.recommended_deal_type !== dealChip) return false;
    return true;
  });

  // Sort
  filtered.sort(function(a, b) {
    var av = parseFloat(a[sortCol] || 0);
    var bv = parseFloat(b[sortCol] || 0);
    return sortDir === 'desc' ? bv - av : av - bv;
  });

  var totalItems = filtered.length;
  var totalPages = Math.max(1, Math.ceil(totalItems / PAGE_SIZE));
  if (page >= totalPages) page = 0;
  _getState()._stratPage = page;
  var pageData = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);

  // Build result count badge
  var anchorOrg = _getState().strat_anchor_org || '';
  var taLabel = _getState().strat_ta || '';
  var countBadge = document.getElementById('piq-result-count');
  if (countBadge) {
    countBadge.textContent = totalItems + ' results' + (taLabel ? ' · ' + taLabel : '');
  }

  if (!filtered.length) {
    wrap.innerHTML = _stratEmptyHTML();
    return;
  }

  // Helper: sort icon
  function _sortIcon(col) {
    if (sortCol !== col) return ' <span style="opacity:0.3;font-size:10px">⇕</span>';
    return ' <span style="font-size:10px;color:var(--teal)">' + (sortDir === 'asc' ? '↑' : '↓') + '</span>';
  }

  var thStyle = 'padding:var(--s-2) var(--s-3);font-family:var(--font-mono);font-size:var(--text-3xs);font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-faint);text-align:center;white-space:nowrap;cursor:pointer;user-select:none;border-bottom:1px solid var(--g-border)';

  var h = '<table id="strat-matches-tbl" style="width:100%;border-collapse:collapse">';
  h += '<thead><tr>';
  h += '<th style="' + thStyle + ';text-align:center;max-width:180px" onclick="sortStratCol(\'org_b_name\')">PARTNER' + _sortIcon('org_b_name') + '</th>';
  h += '<th style="' + thStyle + ';text-align:center">';
  if (typeof window.Tooltip !== 'undefined') {
    h += '<span title="Best deal fit score for this pair">DEAL SCORE' + _sortIcon('recommended_deal_score') + '</span>';
  } else {
    h += 'DEAL SCORE' + _sortIcon('recommended_deal_score');
  }
  h += '</th>';
  h += '<th style="' + thStyle + ';text-align:center" onclick="sortStratCol(\'recommended_deal_type\')">RECOMMENDED' + _sortIcon('recommended_deal_type') + '</th>';
  h += '<th style="' + thStyle + ';text-align:center" onclick="sortStratCol(\'collab_iq_score\')">CollabIQ™' + _sortIcon('collab_iq_score') + '</th>';
  h += '</tr></thead><tbody>';

  // Build absolute-index map from filtered back to original data
  pageData.forEach(function(d) {
    var absIdx = data.indexOf(d);
    var isSelected = selectedIdx === absIdx;
    var pair = _getPair(d);
    var partnerName = _fixOrgName(pair.partnerName || d.org_b_name || '');
    var dealScore = _clamp(d.recommended_deal_score || d.deal_score || 0);
    var collabScore = _clamp(d.collab_iq_score || d.collab_score || 0);
    var cat = d.org_b_category || d.org_category || '';
    var hasDisruption = d.disruption_modifier > 1.05 || d._disruption_modifier > 1.05;
    var evBreadth = parseEvidenceSummary(d.evidence_summary).length;
    var evDepth = parseEvidenceSummary(d.evidence_summary).reduce(function(acc, e) { return acc + (e.entities ? e.entities.length : 0); }, 0);

    // DEFECT-009 fix: previously read d.mechanism_pct + d.platform_diff_pct,
    // neither of which exist on the live API. Real fields:
    //   mechanism_score (0-1) and modality_complementarity_score (0-1).
    // top_shared_targets is the canonical shipped name for the targets fallback
    // (legacy `shared_targets` is a count, not a list).
    var dealThesis = '';
    var mechScore = d.mechanism_score != null ? d.mechanism_score : d.mechanism_pct;
    var modScore  = d.modality_complementarity_score != null ? d.modality_complementarity_score : d.platform_diff_pct;
    if (mechScore != null) dealThesis += Math.round(_clamp(mechScore) * 100) + '% mechanism';
    if (modScore != null) dealThesis += (dealThesis ? ' · ' : '') + Math.round(_clamp(modScore) * 100) + '% modality fit';
    if (!dealThesis) {
      var tgts = d.top_shared_targets || d.shared_targets;
      if (tgts) {
        // top_shared_targets is pipe-delimited; show first 3
        var tStr = String(tgts);
        var parts = tStr.indexOf('|') >= 0 ? tStr.split('|') : tStr.split(',');
        dealThesis = 'Targets: ' + parts.slice(0, 3).map(function(s) { return s.trim(); }).filter(Boolean).join(', ');
      }
    }

    var catPill = cat ? '<span class="prim-pill prim-pill-soft prim-pill-accent-slate" style="font-size:9px;margin-left:4px">' + cat + '</span>' : '';
    var disruptDot = hasDisruption ? '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--purple);margin-left:4px;vertical-align:middle" title="Disruption signal"></span>' : '';
    var confPill = typeof confidenceTierPill === 'function'
      ? '' // React-rendered separately; for innerHTML table we skip React
      : '';

    var scoreColor = _dealScoreColor(dealScore);
    var collabColor = _collabIQColor(collabScore);
    var collabLabel = _collabIQLabel(collabScore);

    h += '<tr class="clickable' + (isSelected ? ' row-hl' : '') + '" ' +
         'onclick="showStrategyDetail(' + absIdx + ')" ' +
         'data-abs-idx="' + absIdx + '" ' +
         'data-stratidx="' + absIdx + '" ' +
         'data-stratdeal="' + (d.recommended_deal_type || '') + '" ' +
         'data-stratpartner="' + partnerName.replace(/"/g, '&quot;') + '" ' +
         'style="cursor:pointer;border-bottom:1px solid var(--g-border);transition:background var(--t-fast)" ' +
         'onmouseover="this.style.background=\'var(--g-spatial)\';_onPartnerHover(' + absIdx + ')" ' +
         'onmouseout="this.style.background=\'transparent\';_onPartnerHoverOut()">';

    // PARTNER cell
    h += '<td style="max-width:180px;text-align:center;vertical-align:middle;padding:var(--s-2) var(--s-3)">';
    h += '<div style="font-weight:600;font-size:12px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center">' +
         _orgXLink(partnerName, 'strategy') + catPill + disruptDot + '</div>';
    if (dealThesis) {
      h += '<div style="font-size:10px;color:var(--text-faint);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + dealThesis + '</div>';
    }
    h += '</td>';

    // DEAL SCORE cell
    h += '<td style="text-align:center;vertical-align:middle;padding:var(--s-2) var(--s-3)">';
    h += '<div style="font-family:var(--font-mono);font-size:13px;font-weight:700;color:var(--' + scoreColor + ')">' + _fmt2(dealScore) + '</div>';
    h += '<div style="font-size:9px;color:var(--text-faint)">' + _stratScoreLabel(dealScore) + '</div>';
    h += '</td>';

    // RECOMMENDED cell
    h += '<td style="text-align:center;vertical-align:middle;padding:var(--s-2) var(--s-3)">' + dealBadge(d.recommended_deal_type) + '</td>';

    // CollabIQ™ cell
    h += '<td style="text-align:center;vertical-align:middle;padding:var(--s-2) var(--s-3)">';
    h += '<div style="font-family:var(--font-mono);font-size:13px;font-weight:700;color:var(--' + collabColor + ')">' + _fmt2(collabScore) + '</div>';
    h += '<div style="font-size:9px;color:var(--text-faint)">' + collabLabel + '</div>';
    h += '</td>';

    h += '</tr>';
  });

  h += '</tbody></table>';

  // Pagination placeholder — React Pagination renders into a container
  h += '<div id="strat-pagination-mount" style="padding:var(--s-2) 0"></div>';

  wrap.innerHTML = h;

  // Mount React Pagination into the placeholder
  _mountPagination(page, totalPages, totalItems);

  // Wire sort handlers
  window.sortStratCol = function(col) {
    if (_getState()._stratSortCol === col) {
      _getState()._stratSortDir = _getState()._stratSortDir === 'asc' ? 'desc' : 'asc';
    } else {
      _getState()._stratSortCol = col;
      _getState()._stratSortDir = 'desc';
    }
    _getState()._stratPage = 0;
    renderStrategyTable();
  };
}

/** Mount React Pagination component into strat-pagination-mount.
 *  UR-014 fix: renderStrategyTable() does `wrap.innerHTML = h` which destroys
 *  any prior mount node. The previous code cached a single root on window
 *  but never reset it when the host node was replaced — React 18 logged
 *  "createRoot() on a container that has already been passed to createRoot()"
 *  on every page change. Fix: cache the root on the mount element itself
 *  (lifecycle = same as the node), reuse if same node, create new if node
 *  was replaced.
 */
function _mountPagination(page, totalPages, totalItems) {
  var mount = document.getElementById('strat-pagination-mount');
  if (!mount) return;
  if (totalPages <= 1) { mount.innerHTML = ''; return; }
  var Pagination = window.Pagination || (window.primitives && window.primitives.Pagination);
  if (!Pagination) return;
  var root = mount.__reactRoot || null;
  if (!root) {
    if (window.ReactDOM && window.ReactDOM.createRoot) {
      root = window.ReactDOM.createRoot(mount);
      mount.__reactRoot = root;
    } else if (window.ReactDOM) {
      // React 16/17 compat — legacy ReactDOM.render handles re-renders cleanly.
      window.ReactDOM.render(
        React.createElement(Pagination, {
          page: page + 1,
          totalPages: totalPages,
          totalItems: totalItems,
          pageSize: 10,
          onChange: function(p) {
            _getState()._stratPage = p - 1;
            renderStrategyTable();
          },
        }),
        mount
      );
      return;
    }
  }
  if (root && root.render) {
    root.render(React.createElement(Pagination, {
      page: page + 1,
      totalPages: totalPages,
      totalItems: totalItems,
      pageSize: 10,
      onChange: function(p) {
        _getState()._stratPage = p - 1;
        renderStrategyTable();
      },
    }));
  }
}

/** Empty state HTML for the table */
function _stratEmptyHTML() {
  var orgB = _getState().strat_org_b;
  if (orgB) {
    // Comparison pair with no data — premium upsell
    var _orgAName = _fixOrgName(_getState().strat_anchor_org || '');
    var _orgBName = _fixOrgName(orgB);
    return '<div style="max-width:480px;margin:0 auto;text-align:center;padding:var(--s-8) var(--s-4)">' +
           '<div style="font-size:16px;font-weight:700;color:var(--text);margin-bottom:8px">' + _orgAName + ' × ' + _orgBName + '</div>' +
           '<p style="font-size:var(--text-sm);color:var(--text-muted);margin-bottom:var(--s-4)">Full partnership intelligence not available for this pair in your current plan.</p>' +
           '<button class="diq-search-btn" onclick="loadStrategyData()" style="min-height:44px">Request Full Access →</button>' +
           '</div>';
  }
  return '<div class="prim-empty" style="padding:var(--s-8)">' +
         '<p class="prim-empty-title">No Partnership Opportunities Found</p>' +
         '<p class="prim-empty-sub">Try adjusting your search or selecting a different anchor organization.</p>' +
         '</div>';
}

// ─── 14. selectScenario ───────────────────────────────────────────────────────

/**
 * Quadrant filter — syncs all controls and re-renders.
 * V1 L27485 parity.
 * Called by wedge / corner pill / segmented btn / trackpad sector.
 *
 * // v1: autoplay quadrant cycle deferred to follow-up iteration
 */
function selectScenario(dealType, isAuto) {
  var prev = _getState()._scenarioActiveDeal;

  // Toggle off if same deal
  if (prev === dealType && !isAuto) {
    _getState()._scenarioActiveDeal = null;
    _getState()._stratDealChip = null;
  } else {
    _getState()._scenarioActiveDeal = dealType;
    _getState()._stratDealChip = dealType;
  }

  var activeDeal = _getState()._scenarioActiveDeal;

  // Sync corner pills (class name: .active, matches V1 + JS toggle direction)
  document.querySelectorAll('.sonar-corner-pill').forEach(function(el) {
    var d = el.dataset.deal;
    el.classList.toggle('active', d === activeDeal);
  });

  // Sync segmented buttons (class name: .active)
  document.querySelectorAll('.deal-segmented-btn').forEach(function(el) {
    var d = el.dataset.deal;
    el.classList.toggle('active', d === activeDeal);
    el.setAttribute('aria-selected', d === activeDeal ? 'true' : 'false');
    // Move indicator
    if (d === activeDeal) {
      var indicator = document.getElementById('deal-segmented-indicator');
      if (indicator) {
        indicator.style.left = el.offsetLeft + 'px';
        indicator.style.width = el.offsetWidth + 'px';
      }
    }
  });

  // Sync trackpad sectors (class name: .active)
  document.querySelectorAll('.tp-sector').forEach(function(el) {
    el.classList.toggle('active', el.dataset.deal === activeDeal);
  });

  // Set data-active-deal on the sonar SVG wrapper so CSS rules
  // [data-active-deal] .sonar-node { opacity: 0.18 } cascade.
  var sonarSvg = document.getElementById('sonar-radar-svg');
  if (sonarSvg) {
    if (activeDeal) {
      sonarSvg.setAttribute('data-active-deal', activeDeal);
    } else {
      sonarSvg.removeAttribute('data-active-deal');
    }
  }

  // Re-render the sonar SVG so wedge fills + label visibility update
  // (theme-aware per-wedge opacity + per-quadrant label gating).
  var sonarContainer = document.getElementById('sonar-radar-container');
  var data = _getState().strat_data || [];
  if (sonarContainer && data.length) {
    var anchor = _getState().strat_anchor_org || 'Sanofi';
    sonarContainer.innerHTML = buildSonarSVG(data, anchor, activeDeal);
  }

  // Re-render ranked list + table
  _renderRankedList(data, activeDeal);
  renderStrategyTable();

  // Vibe Architect 2026-05-02 — re-render right-panel deck (front card follows activeDeal)
  _updateCollabScorePanel();
}

// ─── 15. stratSwapOrgs ───────────────────────────────────────────────────────

/**
 * Swap anchor ↔ comparison org inputs.
 * V1 L25627 parity.
 */
function stratSwapOrgs() {
  var a = _getState().strat_anchor_org || '';
  var b = _getState().strat_org_b || '';
  _getState().strat_anchor_org = b;
  _getState().strat_org_b = a;

  // Update DOM inputs
  var inpA = document.getElementById('strat-org-a');
  var inpB = document.getElementById('strat-org-b');
  if (inpA) inpA.value = _getState().strat_anchor_org;
  if (inpB) inpB.value = _getState().strat_org_b || '';

  if (_getState().strat_anchor_org) loadStrategyData();
}

// ─── 16. loadStrategyData ────────────────────────────────────────────────────

/**
 * Fire strategy-iq search and repaint.
 * V1 L25638 parity.
 * 30s timeout via DASHIQ.searchStrategy.
 */
function loadStrategyData() {
  var panel = _getPanel();
  if (!panel) return;

  var anchor = _getState().strat_anchor_org || '';
  if (!anchor) {
    _getState().strat_anchor_org = 'Sanofi';
    anchor = 'Sanofi';
  }

  var orgB = _getState().strat_org_b || '';
  var dealType = _getState()._scenarioActiveDeal || '';

  // Show loader
  // UR-014 fix: createRoot warning + duplicate-root leak. Cache the React 18
  // root on the mount element so we re-use it across loadStrategyData calls
  // (Analyze click, deal-chip change, etc.). Previously every call created a
  // new createRoot on the same DOM node — React 18 logs a console error
  // ("called createRoot() on a container that has already been passed to
  // createRoot()") and the previous root leaks listeners.
  var heroSection = document.getElementById('sonar-hero-container');
  var loaderMount = document.getElementById('strat-loader-mount');
  if (loaderMount) {
    if (window.ReactDOM && window.Loader) {
      var lRoot = loaderMount.__reactRoot;
      if (!lRoot && window.ReactDOM.createRoot) {
        lRoot = window.ReactDOM.createRoot(loaderMount);
        loaderMount.__reactRoot = lRoot;
      }
      if (lRoot) {
        lRoot.render(React.createElement(window.Loader, {
          label: 'EVALUATING PARTNERS',
          detail: 'Scanning partnership landscape for ' + anchor,
        }));
      }
    }
    loaderMount.style.display = 'flex';
    loaderMount.style.justifyContent = 'center';
  }
  if (heroSection) heroSection.style.opacity = '0.4';

  // DEFECT-010 fix: limit=50 cap was truncating 661 Sanofi pairs to 50.
  // Bump to 500 (well under MAX_LIMIT=200 server-side soft cap; server clamps).
  var params = { org_name: anchor, limit: 500 };
  if (orgB) params.org_b = orgB;
  if (dealType) params.deal_type = dealType;

  window.DASHIQ.searchStrategy(params)
    .then(function(res) {
      if (loaderMount) loaderMount.style.display = 'none';
      if (heroSection) heroSection.style.opacity = '1';

      var results = Array.isArray(res) ? res : (res.results || res.data || []);

      // DEFECT-017 fix: fuzzy self-pair detection. Anchor "Sanofi" must drop
      // legal-entity variants like "Sanofi-belgium Nv-" / "S. A. Labaz-sanofi".
      // Normalize to lowercase + strip non-alphanum + take first 5 chars.
      function _orgKey(s) {
        return (s || '').toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 5);
      }
      var anchorKey = _orgKey(anchor);

      var excludeOrgs = ['Torrent', 'Teva', 'Cipla', 'Sun Pharma', 'Mylan', 'Hikma'];
      results = results.filter(function(d) {
        var partnerName = (d.org_b_name || d.partner_name || '').toLowerCase();
        if (!partnerName) return false;
        // Self-pair (fuzzy)
        if (anchorKey && _orgKey(partnerName) === anchorKey) return false;
        // Generics manufacturers
        for (var i = 0; i < excludeOrgs.length; i++) {
          if (partnerName.indexOf(excludeOrgs[i].toLowerCase()) >= 0) return false;
        }
        // DEFECT-002 fix: removed ACADEMIC/GOVERNMENT/HOSPITAL/FOUNDATION/
        // UNIVERSITY exclusion. NIH consortia, university tech-transfer
        // licensing, hospital-led trials are real BD targets in pharma.
        // Stanford, NIH, UCSF, Cleveland Clinic, Mass General were all
        // silently dropped by this filter. If a category filter is
        // wanted, expose as a user-toggleable chip — not a hardcoded drop.
        return true;
      });

      // DEFECT-018 fix: sort first, then walk linearly. Previous O(n²)
      // mid-iteration filter caused unstable ordering on tied scores.
      results.sort(function(a, b) {
        var sa = _clamp(a.recommended_deal_score || a.deal_score || 0);
        var sb = _clamp(b.recommended_deal_score || b.deal_score || 0);
        return sb - sa;
      });
      var seen = {};
      var deduped = [];
      results.forEach(function(d) {
        var key = (d.org_b_name || d.partner_name || '').toLowerCase();
        if (!key || seen[key]) return;
        seen[key] = true;
        deduped.push(d);
      });

      _getState().strat_data = deduped;
      _getState()._stratPage = 0;

      renderStrategyProfile(deduped);

      // Auto-open top partner after 200ms when >600px wide
      if (window.innerWidth > 600 && deduped.length) {
        setTimeout(function() {
          var autoIdx = 0;
          _getState()._compRadarLocked = autoIdx;
          showStrategyDetail(autoIdx, true);
        }, 200);
      }
    })
    .catch(function(err) {
      if (loaderMount) loaderMount.style.display = 'none';
      if (heroSection) heroSection.style.opacity = '1';
      var isTimeout = err && (err.status === 0 || (err.message && err.message.indexOf('timeout') >= 0));
      var errorTitle = isTimeout ? 'Strategy Data Timeout' : 'Strategy Data Unavailable';
      var errorDetail = err ? (err.message || String(err)) : 'Unable to load partnership data.';

      var errMount = document.getElementById('strat-error-mount');
      if (errMount && window.ReactDOM && window.ErrorCard) {
        // UR-014 fix: cache root to avoid createRoot duplicate warning.
        var errRoot = errMount.__reactRoot;
        if (!errRoot && window.ReactDOM.createRoot) {
          errRoot = window.ReactDOM.createRoot(errMount);
          errMount.__reactRoot = errRoot;
        }
        if (errRoot) {
          errRoot.render(React.createElement(window.ErrorCard, {
            title: errorTitle,
            detail: errorDetail,
            onRetry: loadStrategyData,
          }));
          errMount.style.display = 'block';
        }
      }
    });
}

// ─── 17. renderStrategyProfile (post-data repaint) ───────────────────────────

/**
 * Rebuild hero section + table after data loads.
 * V1 L27071 parity.
 */
function renderStrategyProfile(results) {
  var data = Array.isArray(results) ? results : [];
  var anchor = _getState().strat_anchor_org || 'Sanofi';
  var activeDeal = _getState()._scenarioActiveDeal || null;

  // Rebuild sonar SVG
  var sonarContainer = document.getElementById('sonar-radar-container');
  if (sonarContainer) {
    sonarContainer.innerHTML = buildSonarSVG(data, anchor, activeDeal);
  }

  // Rebuild ranked list
  _renderRankedList(data, activeDeal);

  // Render table
  renderStrategyTable();

  // If revisiting, restore last latched deal
  var keepDeal = _getState()._scenarioActiveDeal;
  if (keepDeal) {
    _getState()._scenarioActiveDeal = null;
    try { selectScenario(keepDeal, true); } catch(e) {}
  }

  // Update comparison zone placeholder if no lock
  var zone = document.getElementById('sonar-comparison-zone');
  if (zone && (_getState()._compRadarLocked === undefined || _getState()._compRadarLocked === null)) {
    zone.innerHTML = _comparisonZonePlaceholder();
  }
}

/** Placeholder content for comparison zone (no partner selected) */
function _comparisonZonePlaceholder() {
  // Pentagon placeholder glyph (gray pentagonal outline)
  var cx = 65, cy = 60, R = 42, n = 5;
  var pts = _pentagonPoints([1,1,1,1,1], cx, cy, R);
  var svgGlyph = '<svg width="130" height="120" viewBox="0 0 130 120" style="opacity:0.12">' +
    '<polygon points="' + pts + '" fill="none" stroke="var(--text-faint)" stroke-width="1.5"/>' +
    '</svg>';
  return '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-3)">' +
    svgGlyph +
    '<span style="font-size:10px;font-family:var(--font-mono);color:var(--text-faint)">Hover a partner to compare</span>' +
    '</div>';
}

// ─── 18. setupOrgAutocomplete ─────────────────────────────────────────────────

/**
 * Wire org autocomplete for anchor + comparison inputs.
 * V1 L25598 parity.
 */
function _setupOrgAutocomplete(inputId, acId, stateKey) {
  var inp = document.getElementById(inputId);
  var ac = document.getElementById(acId);
  if (!inp || !ac) return;

  // INT-011/022 fix: avoid inline-onclick string injection (apostrophes,
  // backslashes, and quotes in org names like "L'Oréal" or 'Sanofi "Group"'
  // would break JS-in-string-in-attribute serialization). Use a delegated
  // listener on the dropdown that reads the org name from a `data-org-name`
  // attribute (HTML-escaped, no JS escape needed). Also wire keyboard arrows
  // + Enter (INT-001 fix).
  function _escAttr(s) {
    return String(s == null ? '' : s)
      .replace(/&/g, '&amp;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;');
  }
  function _commitPick(name) {
    if (!name) return;
    inp.value = name;
    if (window.STATE) window.STATE[stateKey] = name;
    ac.innerHTML = '';
    ac.style.display = 'none';
    // Fire a synthetic input event for any listeners depending on it.
    try { inp.dispatchEvent(new Event('change', { bubbles: true })); } catch(_) {}
  }

  // Delegated click handler on the dropdown (one-time wire).
  if (!ac.__orgAcWired) {
    ac.__orgAcWired = true;
    ac.addEventListener('mousedown', function(e) {
      // mousedown fires before blur — suppress blur-close by preventDefault.
      var item = e.target && e.target.closest && e.target.closest('.ac-item');
      if (!item) return;
      e.preventDefault();
      _commitPick(item.getAttribute('data-org-name'));
    });
  }

  // UR-013 fix: search dropdown "shifts weird" when typing.
  // Root causes addressed:
  //   (1) Every keystroke fired a separate fetch with no debounce. Rapid
  //       typing → multiple inflight requests → late responses overwrite
  //       fresher results, causing the dropdown to flip between item sets
  //       (visible "shift").
  //   (2) Result count varied (1, 8, 5, 8, ...) → dropdown height bounced
  //       on each render. We give the dropdown a stable wrapper so the
  //       outer surface stops flexing while items reshuffle inside.
  // Fix:
  //   - 180ms debounce on the input event.
  //   - Per-input request generation token; stale responses are dropped.
  //   - Use container's existing min-width:220px from CSS plus a padded
  //     inner list — items shrink/grow inside a stable shell.
  ac.__acDebounce = ac.__acDebounce || null;
  ac.__acGen = ac.__acGen || 0;
  inp.addEventListener('input', function() {
    var q = inp.value.trim();
    if (ac.__acDebounce) { clearTimeout(ac.__acDebounce); ac.__acDebounce = null; }
    if (q.length < 2) {
      ac.__acGen++;
      ac.innerHTML = '';
      ac.style.display = 'none';
      return;
    }
    var myGen = ++ac.__acGen;
    ac.__acDebounce = setTimeout(function() {
      if (!window.DASHIQ || typeof window.DASHIQ.fetchOrgs !== 'function') return;
      window.DASHIQ.fetchOrgs({ q: q }).then(function(res) {
        // Drop stale response — only the newest generation gets to render.
        if (myGen !== ac.__acGen) return;
        var items = Array.isArray(res) ? res : (res.orgs || res.results || []);
        items = items.slice(0, 8);
        if (!items.length) { ac.innerHTML = ''; ac.style.display = 'none'; return; }
        ac.innerHTML = items.map(function(org) {
          var name = org.name || org.org_name || String(org);
          var nameAttr = _escAttr(name);
          return '<div class="ac-item" data-org-name="' + nameAttr + '" ' +
                 'style="padding:var(--s-2) var(--s-3);cursor:pointer;font-size:var(--text-sm);color:var(--text);transition:background var(--t-fast);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" ' +
                 'onmouseover="this.style.background=\'var(--g-frosted)\'" ' +
                 'onmouseout="this.style.background=\'transparent\'">' + nameAttr + '</div>';
        }).join('');
        ac.style.display = 'block';
      }).catch(function() {
        if (myGen !== ac.__acGen) return;
        ac.innerHTML = '';
        ac.style.display = 'none';
      });
    }, 180);
  });

  // INT-001 fix: Enter commits top result; Escape closes dropdown.
  inp.addEventListener('keydown', function(e) {
    if (ac.style.display !== 'block') return;
    if (e.key === 'Enter') {
      var first = ac.querySelector('.ac-item');
      if (first) {
        e.preventDefault();
        _commitPick(first.getAttribute('data-org-name'));
      }
    } else if (e.key === 'Escape') {
      ac.style.display = 'none';
    }
  });

  inp.addEventListener('blur', function() {
    setTimeout(function() { ac.style.display = 'none'; }, 150);
  });
}

// ─── 19. renderStrategy (main entry) ─────────────────────────────────────────

/**
 * Main render entry — called by chrome.jsx switchModule on first mount.
 * V1 L25571 parity.
 * Defaults strat_subview='partners', strat_anchor_org='Sanofi'.
 */
function renderStrategy() {
  var panel = _getPanel();
  if (!panel) return;

  // Defaults
  if (!_getState().strat_subview) _getState().strat_subview = 'partners';
  if (!_getState().strat_anchor_org) _getState().strat_anchor_org = 'Sanofi';

  var anchor = _getState().strat_anchor_org;
  var orgB = _getState().strat_org_b || '';
  var activeDeal = _getState()._scenarioActiveDeal || null;

  // Build panel HTML
  var html = _buildStrategyPanelHTML(anchor, orgB, activeDeal);
  panel.innerHTML = html;

  // Wire autocompletes
  _setupOrgAutocomplete('strat-org-a', 'strat-org-a-ac', 'strat_anchor_org');
  _setupOrgAutocomplete('strat-org-b', 'strat-org-b-ac', 'strat_org_b');

  // Wire Analyze button
  var analyzeBtn = document.getElementById('strat-analyze-btn');
  if (analyzeBtn) {
    analyzeBtn.onclick = function() {
      var inpA = document.getElementById('strat-org-a');
      var inpB = document.getElementById('strat-org-b');
      if (inpA && inpA.value.trim()) _getState().strat_anchor_org = inpA.value.trim();
      if (inpB) _getState().strat_org_b = inpB.value.trim();
      loadStrategyData();
    };
  }

  // Wire swap button
  var swapBtn = document.getElementById('strat-swap-btn');
  if (swapBtn) swapBtn.onclick = stratSwapOrgs;

  // Tap hint auto-dismiss (5s)
  setTimeout(function() {
    var hint = document.getElementById('radar-tap-hint');
    if (hint) hint.style.opacity = '0';
  }, 5000);

  // Load data
  loadStrategyData();
}

// ─── 20. _buildStrategyPanelHTML ─────────────────────────────────────────────

/**
 * Build the full panel HTML string.
 * Assembles all subcomponents: search row, xmod island, hero block,
 * ornament card, sonar disc, corner pills, trackpad, segmented control,
 * table section, evidence panel.
 */
function _buildStrategyPanelHTML(anchor, orgB, activeDeal) {
  var anchorVal = anchor || '';
  var orgBVal = orgB || '';
  var nubLabel = _truncOrg5(anchor);

  // ── Search + xmod row ──
  var searchRow = '<div class="search-xmod-row" style="display:flex;flex-direction:column;align-items:center;gap:var(--s-2);padding:var(--s-3) var(--s-4)">';

  // Cross-Intelligence island first — populated by app.jsx Tier 5 refreshAllXmodBars
  searchRow += '<div id="xmod-island-wrap-strategy" class="xmod-island-wrap"></div>';

  // Search container — single anchor org input, compact
  var SEARCH_ICON = '<svg class="diq-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>';
  searchRow += '<div class="diq-search-container" style="max-width:480px">';
  searchRow += '<div class="diq-search-row">';
  searchRow += '<span class="diq-search-label">Explore by</span>';
  searchRow += '<button class="diq-mode-tab on" disabled aria-label="Search mode: Organizations" style="cursor:default">Org</button>';

  // Anchor input (with magnifier icon)
  searchRow += '<div class="diq-search-input-wrap" style="flex:1;min-width:0">';
  searchRow += SEARCH_ICON;
  searchRow += '<input id="strat-org-a" type="text" class="diq-search-input" aria-label="Search organization" placeholder="Organization..." value="' + anchorVal.replace(/"/g,'&quot;') + '" autocomplete="off">';
  searchRow += '<div id="strat-org-a-ac" class="diq-search-dropdown ac-dropdown" style="display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:300"></div>';
  searchRow += '</div>';

  // Hidden org-b kept for state compatibility
  searchRow += '<input id="strat-org-b" type="hidden" value="' + orgBVal.replace(/"/g,'&quot;') + '">';

  // Analyze CTA
  searchRow += '<button id="strat-analyze-btn" class="diq-search-btn" type="button">Analyze</button>';

  searchRow += '</div>'; // .diq-search-row
  searchRow += '</div>'; // .diq-search-container

  searchRow += '</div>'; // .search-xmod-row

  // ── Loader + error mount ──
  var stateSection = '<div id="strat-loader-mount" style="display:none;padding:var(--s-6)"></div>';
  stateSection += '<div id="strat-error-mount" style="display:none;padding:var(--s-4)"></div>';

  // ── Hero block: sonar-hero-container ──
  // Phase B parity fix — V1 layout (PARTNERIQ_a-2.md L48):
  //   #sonar-hero-container (radar+ornament side-by-side) → .deal-segmented → #strat-layout-wrap
  // Previously V2 nested .deal-segmented INSIDE the hero next to the disc, which
  // left a vertical dead-zone whenever the ornament card (tall) was taller than
  // the disc+segmented column. Now segmented is a sibling AFTER the hero.
  // gap:0 = monolith effect (PIQ-17.28 / PIQ-22.2).
  // align-items:stretch so disc column matches ornament card height.
  var hero = '<div id="sonar-hero-container" style="display:flex;flex-direction:row-reverse;gap:0;padding:0 var(--s-4) var(--s-2);align-items:stretch">';

  // ── RIGHT panel (Vibe Architect 2026-05-02): CollabIQ Score + 4-card stack ──
  // Replaces the previous pentagon ornament. The radar in the middle remains
  // the patent-pending instrument; this right panel is the calc's executive
  // readout (ring + big number) plus a Z-depth stacked deck of the 4 deal
  // recommendations. #collabiq-score-panel-mount is the rebuild target on data
  // updates (selectScenario, showStrategyDetail). #sonar-comparison-zone is
  // retained as a hidden node so legacy hover-update code paths don't break.
  _injectCollabScoreStyles();
  // Phase 3.3 (2026-05-03): light-glass anchor restored, ws-card class still
  // dropped so we don't get double-styling.
  hero += '<div id="sonar-ornament-card" class="collabiq-score-panel" style="width:380px;flex-shrink:0;align-self:flex-start;display:flex;flex-direction:column;position:relative;padding:14px 14px 12px;border-radius:18px">';
  hero += '<div id="collabiq-score-panel-mount" style="display:flex;flex-direction:column;flex:1">';
  hero +=   _buildCollabScorePanel(null, activeDeal, anchor);
  hero += '</div>';
  hero += '<div id="sonar-comparison-zone" style="display:none" aria-hidden="true"></div>';
  hero += '</div>'; // /sonar-ornament-card

  // ── RIGHT ornament (LEFT in DOM / visual RIGHT): sonar disc + controls ──
  hero += '<div class="sonar-glass-disc-wrap" style="flex:1;display:flex;flex-direction:column;gap:var(--s-2);min-width:0">';

  // Sonar disc (with corner pills via overflow:visible)
  // Vibe Architect 2026-05-03 — constrain pill-anchor to the disc's actual
  // square dimensions so .sonar-corner-pill (positioned at 12-16% of parent)
  // lands on the disc edges, not the wider panel corners.
  hero += '<div style="position:relative;width:100%;max-width:440px;aspect-ratio:1/1;margin:0 auto">';
  hero += '<div id="sonar-radar-container" style="position:relative;width:100%;height:100%;overflow:visible">';
  // Radar SVG will be injected by renderStrategyProfile
  hero += '</div>';

  // ── Corner pills (NW / NE / SW / SE) ──
  // 4% inset from disc corners; outline rest, solid matte active.
  // Phase 3.7 (2026-05-03): Per-deal color RESTORED on the pillbox
  // filter UI (corner pills + segmented control below). The rim
  // arcs around the radar disc stay neutral — those carried the
  // "score = deal type" implication. The pills are filter affordances,
  // and color-coded buttons help users recognize "Co-Dev = teal,
  // Licensing = amber" at a glance, which is a usability win.
  DEAL_TYPES.forEach(function(dt) {
    var isAct = activeDeal === dt.key;
    hero += '<button type="button" class="sonar-corner-pill ' + dt.corner + (isAct ? ' active' : '') + '" ' +
            'data-deal="' + dt.key + '" ' +
            'onclick="selectScenario(\'' + dt.key + '\')" ' +
            'style="--deal-clr:var(--sonar-' + _sonarVarSuffix(dt.key) + ')">' +
            dt.cornerLabel + '</button>';
  });

  // ── Trackpad center hub (54×54 circle, V1 L1116-1340 parity) ──
  // 4 .tp-sector children w/ rotated border-radius (50% on outer corner,
  // 4px on inner three) producing the iconic "pie slice with one curved
  // edge" silhouette. Sectors are clear glass; per-sector --tp-glow
  // accent var drives hover/active glow.
  hero += '<div class="deal-trackpad" data-active-deal="' + (activeDeal || '') + '">';
  DEAL_TYPES.forEach(function(dt) {
    var isAct = activeDeal === dt.key;
    hero += '<div class="tp-sector' + (isAct ? ' active' : '') + '" data-deal="' + dt.key + '" ' +
            'onclick="selectScenario(\'' + dt.key + '\')" ' +
            'title="' + dt.label + '"></div>';
  });
  // Center nub — origin org name, 5-char truncated. Mint-glass ring
  // (light) / navy w/ cyan ring (dark). CSS in modules.css.
  hero += '<div class="tp-center-nub"><span class="tp-center-nub-label">' + nubLabel + '</span></div>';
  hero += '</div>'; // /deal-trackpad

  // ── Tap hint coach mark (auto-dismiss 5s, see below) ──
  hero += '<div id="radar-tap-hint" style="position:absolute;bottom:8%;left:50%;transform:translateX(-50%);font-size:10px;font-family:var(--font-mono);color:var(--text-faint);pointer-events:none;white-space:nowrap;transition:opacity 0.5s;z-index:4">Click any quadrant to filter →</div>';

  hero += '</div>'; // /relative wrapper

  // ── iOS segmented control — moved INSIDE disc-wrap, directly under the radar
  //    (Vibe Architect 2026-05-03). Previously sat as a sibling of the hero
  //    which left a gap between the radar and the control. Now it docks under
  //    the disc as a single visual instrument.
  hero += '<div style="padding:8px 0 0;display:flex;justify-content:center">';
  hero += '<div id="deal-segmented" class="deal-segmented" role="tablist">';
  hero += '<div id="deal-segmented-indicator"></div>';
  // Phase 3.7: segmented buttons get per-deal color back (filter UI).
  DEAL_TYPES.forEach(function(dt) {
    var isAct = activeDeal === dt.key;
    hero += '<button type="button" class="deal-segmented-btn' + (isAct ? ' active' : '') + '" ' +
            'data-deal="' + dt.key + '" ' +
            'role="tab" aria-selected="' + (isAct ? 'true' : 'false') + '" ' +
            'onclick="selectScenario(\'' + dt.key + '\')" ' +
            'style="--deal-clr:var(--sonar-' + _sonarVarSuffix(dt.key) + ')">' +
            dt.cornerLabel + '</button>';
  });
  hero += '</div>'; // /deal-segmented
  hero += '</div>'; // /segmented wrapper inside disc-wrap

  hero += '</div>'; // /sonar-glass-disc-wrap
  hero += '</div>'; // /sonar-hero-container

  // ── Bottom layout: table + evidence panel ──
  var bottom = '<div id="strat-layout-wrap" style="display:flex;flex-direction:row;gap:0;align-items:flex-start;padding:0 var(--s-4) var(--s-6);min-height:0">';

  // Table section
  bottom += '<div class="ws-card" style="flex:1;min-width:0;overflow:hidden">';
  bottom += '<div style="display:flex;align-items:center;justify-content:space-between;padding:var(--s-3) var(--s-4);border-bottom:1px solid var(--g-border)">';
  bottom += '<span style="font-size:var(--text-sm);font-weight:700;color:var(--text)">Partnership Opportunities</span>';
  bottom += '<span id="piq-result-count" style="font-size:10px;font-family:var(--font-mono);color:var(--text-faint)">—</span>';
  bottom += '</div>';
  // Deal-type filter chips (Phase 3 · Vibe Architect 2026-05-03). Populated by _renderStratFilterChips().
  bottom += '<div id="strat-filter-chips" style="display:flex;flex-wrap:wrap;gap:6px;padding:var(--s-2) var(--s-4);border-bottom:1px solid var(--g-border)"></div>';
  bottom += '<div id="strat-table-wrap" style="overflow-x:auto"></div>';
  bottom += '</div>'; // /ws-card table

  // Evidence panel (desktop inline; hidden on mobile)
  // Phase B — evidence panel given proper card surface so it doesn't disappear
  // into the dark page background. Class .ws-card supplies frosted bg + border.
  bottom += '<div id="strat-evidence-panel" class="ws-card" style="width:0;flex-shrink:0;overflow:hidden;transition:width 0.3s ease;display:none;margin-left:var(--s-3)"></div>';

  bottom += '</div>'; // /strat-layout-wrap

  return searchRow + stateSection + hero + bottom;
}

// ─── 21. _onPartnerClick (sonar orb click helper) ───────────────────────────

/**
 * Handle sonar orb click — lock in selected partner.
 * Called from SVG onclick before showStrategyDetail.
 */
function _onPartnerClick(absIdx) {
  _getState()._compRadarLocked = absIdx;
  // Highlight corresponding ranked-list row
  var rows = document.querySelectorAll('#strat-rank-list .rank-row');
  rows.forEach(function(row) {
    var ri = parseInt(row.dataset.absIdx, 10);
    row.classList.toggle('is-active', ri === absIdx);
  });
}

// ─── 22. Wire global window exports ──────────────────────────────────────────

// Expose _onPartnerClick and _onPartnerHoverOut (called from SVG inline handlers)
window._onPartnerClick = _onPartnerClick;
window._onPartnerHoverOut = _onPartnerHoverOut;
window.sonarHover = sonarHover;
window.rerenderSonarForTheme = rerenderSonarForTheme;

Object.assign(window, {
  renderStrategy: renderStrategy,
  loadStrategyData: loadStrategyData,
  showStrategyDetail: showStrategyDetail,
  openStratDrawer: openStratDrawer,
  closePartDrawer: closePartDrawer,
  selectScenario: selectScenario,
  stratSwapOrgs: stratSwapOrgs,
  _renderRankedList: _renderRankedList,
  _updateComparisonRadar: _updateComparisonRadar,
  _selectRankedPartner: _selectRankedPartner,
  _onPartnerHover: _onPartnerHover,
  buildSonarSVG: buildSonarSVG,
  // Also expose helper (used by competeiq/whitespaceiq cross-navigation)
  parseEvidenceSummary: parseEvidenceSummary,
  dealBadge: dealBadge,
});
