// V2/src/app.jsx
// Tier 5 — Root React app.
// Wires:
//   - ?embed=true detection (synchronous, BEFORE React mounts → no FOUC)
//   - Embed-mode + dormant/activated entry-animation styles (one-time injection)
//   - postMessage handshake (COLLABIQ_READY post / COLLABIQ_ACTIVATE listen)
//     using window.DASHIQ.postReady() / window.DASHIQ.onActivate(cb) helpers
//   - 5-step entry animation sequence on `activated` class:
//       orbs bloom → chrome fade → radar spin-up → table rows stagger → evidence slide
//   - Cross-module intelligence bar (refreshAllXmodBars) wired to chrome:switch CustomEvent
//   - Mobile shell toggle at viewport ≤ 768px
//   - <Chrome/> mount via ReactDOM.createRoot (React 18, idempotent)
//     Chrome owns the activeModule conditional render, including exploreiq →
//     <ExploreIQModule/> (kg/explorer.html iframe) for the schema explorer.
//   - <TweaksPanel/> with useTweaks(window.__TWEAK_DEFAULTS) → CSS vars on <html>
//
// Loaded LAST in V2/index.html. By script-execution time, on window:
//   React, ReactDOM, useTweaks, TweaksPanel + tweak atoms,
//   DASHIQ (postReady/onActivate/cache/preload), Chrome, ExploreIQModule,
//   switchModule, MODULES, MODULE_KEYS, DEFAULT_MODULE, STATE,
//   icons (HandshakeIcon/TargetIcon/MapIcon/...),
//   primitives (Badge/Pill/Loader/...),
//   plus per-module render functions written by Tier 4.
// No `import` / `export` statements.

(function () {
  // ── Embed detection (synchronous — runs before React mounts) ────────────────
  const isEmbed = new URLSearchParams(location.search).has('embed');
  if (isEmbed) document.documentElement.classList.add('embed-mode');

  // ── One-time style injection: embed CSS + dormant/activated keyframes ───────
  // We inject from JS rather than amend tokens.css/base.css (already committed)
  // so all entry-animation knobs live alongside the JS that drives them.
  const STYLE_ID = 'collabiq-app-styles';
  if (!document.getElementById(STYLE_ID)) {
    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = [
      // Embed-mode visual contract
      '.embed-mode body { background: transparent !important; }',
      '.embed-mode .atmo { opacity: 0.4; }',
      '.embed-mode #chrome-nav,',
      '.embed-mode .chrome-topbar { display: none !important; }',
      // Dormant state — pre-activation: elements rendered but visually hushed
      'html:not(.activated) .atmo-orb { opacity: 0; }',
      'html:not(.activated) .chrome-topbar,',
      'html:not(.activated) .chrome-sidebar { opacity: 0; }',
      'html:not(.activated) #panel-strategy .sonar-glass-disc-wrap {',
      '  transform: scale(0.96);',
      '  opacity: 0.6;',
      '}',
      'html:not(.activated) #panel-strategy table.iq-table tbody tr { opacity: 0; }',
      'html:not(.activated) #strat-evidence-panel { opacity: 0; }',
      // Activated state — entry animation sequence
      // 1) Orbs bloom (600ms)
      'html.activated .atmo-orb {',
      '  animation: app-orb-bloom 600ms var(--ease-out, ease-out) forwards;',
      '}',
      // 2) Chrome fades (400ms after 400ms delay)
      'html.activated .chrome-topbar,',
      'html.activated .chrome-sidebar {',
      '  animation: app-chrome-fade 400ms 400ms var(--ease-out, ease-out) forwards;',
      '}',
      // 3) PartnerIQ radar spin-up (800ms after 600ms delay)
      'html.activated #panel-strategy .sonar-glass-disc-wrap {',
      '  animation: app-radar-spinup 800ms 600ms var(--ease-out, ease-out) forwards;',
      '}',
      // 4) Table rows stagger (50ms per row, kick-off 800ms in)
      'html.activated #panel-strategy table.iq-table tbody tr {',
      '  animation: app-row-stagger 200ms calc(800ms + (var(--row-i, 0) * 50ms)) var(--ease-out, ease-out) forwards;',
      '}',
      // 5) Evidence panel slide (500ms after 1100ms delay)
      'html.activated #strat-evidence-panel {',
      '  animation: app-panel-slide 500ms 1100ms var(--ease-out, ease-out) forwards;',
      '}',
      // Keyframes
      '@keyframes app-orb-bloom { from { opacity: 0; } to { opacity: 1; } }',
      '@keyframes app-chrome-fade { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }',
      '@keyframes app-radar-spinup { from { transform: scale(0.96) rotate(-6deg); opacity: 0.6; } to { transform: scale(1) rotate(0); opacity: 1; } }',
      '@keyframes app-row-stagger { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }',
      '@keyframes app-panel-slide { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }',
      // Cross-Intelligence island — standalone parity
      // (/workspace/cross-intelligence-bar-standalone.html). Glass capsule
      // with 24px radius, gradient background, inset highlights, glow on
      // hover, animated module-glyph icons (ws-bracket / ciq-ring /
      // piq-shake) instead of plain colored dots.
      //
      // UR-015 fix: explicitly override the static .xmod-island rule in
      // components.css:951 (display:flex; flex-wrap:wrap; padding:s-2 s-3)
      // which was making the bar EXTREMELY fat by stacking padding ON TOP
      // of the segments' fixed 38px height AND letting segments wrap to a
      // second line on narrow viewports. Pin to inline-flex, remove all
      // padding (segments self-pad), force flex-wrap:nowrap, and lock
      // height to the segment row so the bar can never grow vertically.
      '.xmod-island {',
      '  display: inline-flex !important;',
      '  align-items: center;',
      '  flex-wrap: nowrap !important;',
      '  margin: var(--s-2, 8px) 0 0;',
      '  padding: 0 !important;',
      '  height: 38px;',
      '  max-height: 38px;',
      '  border-radius: 24px;',
      '  overflow: hidden;',
      '  position: relative;',
      '  background: linear-gradient(180deg, var(--g-frosted), var(--g-ground));',
      '  border: 1px solid var(--g-border-strong);',
      '  box-shadow: var(--sh-2), inset 0 1px 0 var(--g-inset-top), inset 0 -1px 0 var(--g-inset-bottom);',
      '  backdrop-filter: blur(24px) saturate(160%);',
      '  -webkit-backdrop-filter: blur(24px) saturate(160%);',
      '  transition: box-shadow var(--t-fast) var(--ease-out);',
      '  align-self: flex-start;',
      '}',
      '.xmod-island:hover {',
      '  box-shadow: var(--sh-2), var(--sh-glow-teal), inset 0 1px 0 var(--g-inset-top), inset 0 -1px 0 var(--g-inset-bottom);',
      '}',
      '.xmod-island-segments {',
      '  display: flex;',
      '  align-items: stretch;',
      '  flex-wrap: nowrap;',
      '  height: 38px;',
      '  position: relative;',
      '}',
      '.xmod-island-segments::before {',
      "  content: 'Cross-Intelligence';",
      '  display: flex;',
      '  align-items: center;',
      '  height: 38px;',
      '  padding: 0 8px 0 14px;',
      '  font-family: var(--font-mono);',
      '  font-size: 8.5px;',
      '  font-weight: 700;',
      '  letter-spacing: 0.5px;',
      '  text-transform: uppercase;',
      '  white-space: nowrap;',
      '  flex-shrink: 0;',
      '  pointer-events: none;',
      '}',
      '[data-theme="light"] .xmod-island-segments::before { color: #475569; }',
      '[data-theme="dark"]  .xmod-island-segments::before { color: rgba(226,232,240,0.5); }',
      '.xmod-island-sep {',
      '  width: 1px;',
      '  height: 18px;',
      '  align-self: center;',
      '  flex-shrink: 0;',
      '}',
      '[data-theme="light"] .xmod-island-sep { background: rgba(0,0,0,0.1); }',
      '[data-theme="dark"]  .xmod-island-sep { background: rgba(255,255,255,0.12); }',
      '.xmod-island-seg {',
      '  display: flex;',
      '  align-items: center;',
      '  gap: 6px;',
      '  height: 38px;',
      '  padding: 0 12px;',
      '  border: 0;',
      '  background: transparent;',
      '  color: var(--text);',
      '  cursor: pointer;',
      '  white-space: nowrap;',
      '  transition: background var(--t-fast) var(--ease-out);',
      '}',
      '.xmod-island-seg:hover { background: var(--g-focus); }',
      '.xmod-island-seg:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }',
      '.xmod-island-seg.active {',
      '  background: linear-gradient(180deg, var(--g-focus), var(--g-ground));',
      '  box-shadow: var(--sh-glow-teal);',
      '}',
      // Glyph dot is now a 22x22 SVG container (standalone parity)
      '.xmod-island-seg-dot {',
      '  width: 22px; height: 22px;',
      '  display: flex; align-items: center; justify-content: center;',
      '  flex-shrink: 0;',
      '}',
      '.xmod-island-seg-dot svg { width: 22px; height: 22px; overflow: visible; }',
      '.xmod-island-seg-name {',
      '  font-family: var(--font-body);',
      '  font-size: 11px;',
      '  font-weight: 700;',
      '  white-space: nowrap;',
      '}',
      '[data-theme="light"] .xmod-island-seg-name { color: #0f172a; }',
      '[data-theme="dark"]  .xmod-island-seg-name { color: #e2e8f0; }',
      '.xmod-island-seg-ctx {',
      '  display: inline-block;',
      '  font-family: var(--font-mono);',
      '  font-size: 9px;',
      '  max-width: 160px;',
      '  overflow: hidden;',
      '  text-overflow: ellipsis;',
      '  white-space: nowrap;',
      '  margin-left: 2px;',
      '  opacity: 0.85;',
      '}',
      '[data-theme="light"] .xmod-island-seg-ctx { color: #475569; }',
      '[data-theme="dark"]  .xmod-island-seg-ctx { color: rgba(226,232,240,0.6); }',
      // ── Module-glyph SVG tokens inside xmod island — override sidebar.css defaults ──
      // sidebar.css sets .ciq-sweep { opacity: 0 } and .ws-scan-line { opacity: 0 } globally.
      // We must explicitly re-enable them in the xmod context with higher specificity.
      // NOT-DONE-6D V1 parity:
      // (1) --icon-muted-strong is defined in sidebar.css only inside
      //     .sidebar-nav. The cross-intel bar lives outside the sidebar,
      //     so the token resolved to empty → ciq-ring stroke was "none" →
      //     rings were INVISIBLE regardless of opacity. Define the token
      //     scoped to .xmod-island-seg here (per-theme), matching the
      //     sidebar's rgba values.
      // (2) Bump opacity so the rings are visibly defined at 22px display
      //     size (V1 standalone reference shows clearly drawn rings; this
      //     restores the "circular scope housing" CTK called out).
      '[data-theme="dark"]  .xmod-island-seg { --icon-muted-strong: rgba(255,255,255,0.7); --icon-muted: rgba(255,255,255,0.4); }',
      '[data-theme="light"] .xmod-island-seg { --icon-muted-strong: rgba(15,23,42,0.7);  --icon-muted: rgba(15,23,42,0.4); }',
      '.xmod-island-seg .ciq-ring   { fill: none; stroke: var(--icon-muted-strong); opacity: 0.9; }',
      '.xmod-island-seg .ciq-axis   { stroke: var(--icon-muted-strong); opacity: 0.35; }',
      '.xmod-island-seg .ciq-center { fill: var(--teal); }',
      '.xmod-island-seg .ciq-blip   { fill: var(--teal); opacity: 0.85; }',
      // CTK iter2: removed `transform-box: fill-box; transform-origin: 50% 50%`
      // which made the rotation origin resolve to the BBOX center of the sweep
      // group (beam+wedge) rather than the SVG view-box center. The sweep
      // group's bbox is asymmetric (wedge extends left-and-up from center to
      // ~22,18), so rotating around its bbox center shifted the visual sweep
      // off-center within the scope rings. V1 (cross-intelligence-bar-
      // standalone.html L396) has NO transform-box override; the inline
      // style="transform-origin:50px 50px" on the <g class="ciq-sweep">
      // group (app.jsx L339) resolves against the default view-box → exact
      // 50,50 viewBox center. Restoring V1 behaviour by removing the
      // override here.
      // CTK 2026-05-12: sidebar.css globally sets `.ciq-sweep { transform-box:
      // fill-box }` for the sidebar glyph. That fill-box cascaded into the
      // xmod-island context too, which made the inline `transform-origin:
      // 50px 50px` measure from the <g>'s tight bounding-box top-left
      // (~22,9 in viewBox units) instead of the viewBox center (50,50) —
      // visible as a sweep rotating off-center to the upper-right of the
      // CompeteIQ icon. Force `transform-box: view-box` here (with the
      // !important needed to win against the more-specific sidebar rule)
      // so the inline 50px,50px resolves against the SVG viewBox = center.
      '.xmod-island-seg .ciq-sweep  { transform-box: view-box !important; transform-origin: 50% 50% !important; opacity: 1 !important; animation: xmod-ciq-spin 2.4s linear infinite; }',
      '.xmod-island-seg .ciq-beam   { stroke: var(--teal); stroke-linecap: round; }',
      '.xmod-island-seg .ciq-wedge  { fill: var(--teal); }',
      // WS glyph
      // NOT-DONE-6D V1 parity: bump ws-inner + ws-dot opacity so the
      // interior structure (filled rect + 3 dots) is visibly defined inside
      // the corner brackets at 22px display size — V1 has a clear "dotted
      // block inside brackets" pattern; V2 previously showed only brackets
      // with a barely-visible interior (the "two faint dashes" CTK noted).
      '.xmod-island-seg .ws-bracket { stroke: var(--icon-muted-strong); stroke-width: 5; stroke-linecap: round; stroke-linejoin: round; fill: none; }',
      '.xmod-island-seg .ws-inner   { fill: var(--teal); opacity: 0.42; }',
      '.xmod-island-seg .ws-scan-line { stroke: var(--teal); stroke-width: 2.5; stroke-linecap: round; opacity: 1 !important; animation: xmod-ws-scan 2.8s ease-in-out infinite; }',
      '.xmod-island-seg .ws-dot     { fill: var(--teal); opacity: 0.95; }',
      // PIQ glyph
      '.xmod-island-seg .piq-shake  { fill: var(--icon-muted-strong); transform-box: fill-box; transform-origin: 50% 65%; }',
      '.xmod-island-seg .piq-ring-track { fill: none; stroke: var(--icon-muted); stroke-width: 2; opacity: 0.3; }',
      '.xmod-island-seg .piq-ring-prog  { fill: none; stroke: var(--teal); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 56.5; stroke-dashoffset: 20; opacity: 0.8; }',
      '.xmod-island-seg .piq-fill  { opacity: 0; }',
      '.xmod-island-seg .piq-check { opacity: 0; }',
      '.xmod-island-seg .piq-pulse { opacity: 0; }',
      '@keyframes xmod-ciq-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }',
      '@keyframes xmod-ws-scan { 0%,100% { opacity: 0; transform: translateY(-16px); } 15% { opacity: 0.8; } 50% { opacity: 0.9; transform: translateY(16px); } 85% { opacity: 0.8; } }',
    ].join('\n');
    document.head.appendChild(style);
  }

  // ── Cross-module intelligence bar ──────────────────────────────────────────
  // For each module, find its #xmod-island-wrap-{key} mount and populate with
  // contextual pills linking to the OTHER two modules. Modules that don't have
  // the wrap (compete/opp use their own internal bars) are gracefully skipped.

  const XMOD_KEYS = ['strategy', 'compete', 'opp'];

  function _esc(s) {
    if (s == null) return '';
    return String(s).replace(/'/g, "\\'").replace(/"/g, '&quot;');
  }

  function _displayOrg(orgName) {
    if (!orgName) return '';
    return (typeof window.fixOrgName === 'function')
      ? window.fixOrgName(orgName)
      : String(orgName);
  }

  function _displayTA(ta) {
    if (!ta) return '';
    return (typeof window.formatTA === 'function')
      ? window.formatTA(ta)
      : String(ta);
  }

  function _truncate(s, n) {
    const str = String(s || '');
    return str.length > n ? str.slice(0, n - 1) + '…' : str;
  }

  function _pickFirstTA(state) {
    // Try every plausible source for "the TA the user is currently exploring".
    // UR-007 fix: read state.compete_anchor (the unified Phase 4 anchor model
    // CompeteIQ uses), not just the legacy state.comp_ta. When the user
    // anchors CompeteIQ on a TA, compete_anchor = {type:'TA', value:'ONCOLOGY'}
    // — the cross-intel bar should propagate that value to WhitespaceIQ.
    const ca = state.compete_anchor || null;
    const compTAFromAnchor = (ca && ca.type === 'TA' && ca.value) ? ca.value : '';
    const candidates = [
      state.opp_ta,
      state.comp_ta,
      compTAFromAnchor,
      state.strat_ta,
    ];
    for (let i = 0; i < candidates.length; i++) {
      if (candidates[i]) return candidates[i];
    }
    // Fallback — peek at strat_data row 0 if present.
    const row0 = (state.strat_data && state.strat_data[0]) || null;
    if (row0 && row0.top_shared_tas) {
      const first = String(row0.top_shared_tas).split('|').filter(Boolean)[0];
      if (first) return first;
    }
    return '';
  }

  function _pickAnchorOrg(state) {
    // UR-007 fix: include CompeteIQ's drug-profile org context
    // (comp_drug_org is set when the user opens a Drug Profile drawer).
    // Without this, the CompeteIQ → PartnerIQ cross-intel button never
    // rendered when the user was deep in a drug profile.
    return state.strat_anchor_org
      || state.comp_selected_org
      || state.comp_drug_org
      || '';
  }

  // Module-glyph SVG markup (string form for innerHTML insertion).
  // Mirrors PIQGlyph / CIQGlyph / WSGlyph in chrome.jsx — same paths /
  // class hooks (.ciq-ring / .ws-bracket / .piq-shake) so the same CSS
  // rules (defined just above + sidebar.css) drive coloring + animation.
  const _GLYPH_SVG = {
    strategy: ''
      + '<svg viewBox="0 0 100 100" fill="none" aria-hidden="true">'
      +   '<g class="piq-shake"><g transform="translate(12 22) scale(0.30)">'
      +     '<path d="M254.3,107.91,228.78,56.85a16,16,0,0,0-21.47-7.15L182.44,62.13,130.05,48.27a8.14,8.14,0,0,0-4.1,0L73.56,62.13,48.69,49.7a16,16,0,0,0-21.47,7.15L1.7,107.9a16,16,0,0,0,7.15,21.47l27,13.51,55.49,39.63a8.06,8.06,0,0,0,2.71,1.25l64,16a8,8,0,0,0,7.6-2.1l40-40,15.08-15.08,26.42-13.21a16,16,0,0,0,7.15-21.46Zm-54.89,33.37L165,113.72a8,8,0,0,0-10.68.61C136.51,132.27,116.66,130,104,122L147.24,80h31.81l27.21,54.41Zm-41.87,41.86L99.42,168.61l-49.2-35.14,28-56L128,64.28l9.8,2.59-45,43.68-.08.09a16,16,0,0,0,2.72,24.81c20.56,13.13,45.37,11,64.91-5L188,152.66Zm-25.72,34.8a8,8,0,0,1-7.75,6.06,8.13,8.13,0,0,1-1.95-.24L80.41,213.33a7.89,7.89,0,0,1-2.71-1.25L51.35,193.26a8,8,0,0,1,9.3-13l25.11,17.94L126,208.24A8,8,0,0,1,131.82,217.94Z"/>'
      +   '</g></g>'
      +   '<circle cx="50" cy="26" r="9" class="piq-ring-track"/>'
      +   '<circle cx="50" cy="26" r="9" transform="rotate(-90 50 26)" class="piq-ring-prog"/>'
      +   '<circle cx="50" cy="26" r="9" class="piq-fill"/>'
      +   '<polyline points="45,26 48,29 55,22" class="piq-check"/>'
      +   '<circle cx="50" cy="26" r="9" class="piq-pulse"/>'
      + '</svg>',
    // NOT-DONE-6D V1 parity: 3 concentric rings (r=42, r=30, r=18) to match
    // V1 standalone reference (cross-intelligence-bar-standalone.html line
    // 525). Previously only 2 rings + low inline opacity made the icon
    // read as a "small fragment" — the spinning sweep dominated and the
    // ring scope housing disappeared. Inline opacity bumped from
    // 0.18/0.14 → 0.55/0.50/0.45 so rings remain visible after stacking
    // with the CSS .ciq-ring rule.
    compete: ''
      + '<svg viewBox="0 0 100 100" fill="none" aria-hidden="true">'
      +   '<circle class="ciq-ring" cx="50" cy="50" r="42" stroke-width="3" opacity="0.55"/>'
      +   '<circle class="ciq-ring" cx="50" cy="50" r="30" stroke-width="2.5" opacity="0.50"/>'
      +   '<circle class="ciq-ring" cx="50" cy="50" r="18" stroke-width="2" opacity="0.45"/>'
      +   '<line class="ciq-axis" x1="8"  y1="50" x2="92" y2="50" stroke-width="1.2"/>'
      +   '<line class="ciq-axis" x1="50" y1="8"  x2="50" y2="92" stroke-width="1.2"/>'
      +   '<g class="ciq-sweep" style="transform-origin:50px 50px">'
      +     '<line class="ciq-beam" x1="50" y1="50" x2="50" y2="9" stroke-width="2.5"/>'
      +     '<path class="ciq-wedge" d="M50,50 L50,9 A41,41,0,0,0,22,18 Z" opacity="0.22"/>'
      +   '</g>'
      +   '<circle class="ciq-center" cx="50" cy="50" r="4"/>'
      +   '<circle class="ciq-blip" cx="68" cy="28" r="3"/>'
      +   '<circle class="ciq-blip" cx="28" cy="64" r="2.5"/>'
      + '</svg>',
    opp: ''
      + '<svg viewBox="0 0 100 100" fill="none" aria-hidden="true">'
      +   '<rect class="ws-inner" x="24" y="24" width="52" height="52" rx="6"/>'
      +   '<g class="ws-frame-g">'
      +     '<path class="ws-bracket" d="M20,12 L12,12 L12,20"/>'
      +     '<path class="ws-bracket" d="M80,12 L88,12 L88,20"/>'
      +     '<path class="ws-bracket" d="M20,88 L12,88 L12,80"/>'
      +     '<path class="ws-bracket" d="M80,88 L88,88 L88,80"/>'
      +   '</g>'
      +   '<line class="ws-scan-line" x1="22" y1="50" x2="78" y2="50"/>'
      +   '<circle class="ws-dot" cx="38" cy="38" r="3"/>'
      +   '<circle class="ws-dot" cx="62" cy="54" r="3"/>'
      +   '<circle class="ws-dot" cx="48" cy="70" r="2.5"/>'
      + '</svg>',
  };

  // Build segment HTML for a single cross-module link.
  // Standalone parity (cross-intelligence-bar-standalone.html): each
  // .xmod-island-seg now contains an animated SVG module glyph (replaces
  // the previous flat colored dot) followed by the module name and the
  // contextual payload (truncated to 28 chars).
  function _pillHTML(target, labelText, valueText, onclickJs) {
    const safeValue = _truncate(valueText, 28);
    const safeLabel = String(labelText || '').replace(/[<>"']/g, '');
    const safeOnclick = String(onclickJs || '').replace(/"/g, '&quot;');
    const glyph = _GLYPH_SVG[target] || '';
    return ''
      + '<button type="button" class="xmod-island-seg" data-target="' + target + '"'
      + ' onclick="' + safeOnclick + '"'
      + ' aria-label="Switch to ' + safeLabel + ': ' + String(safeValue).replace(/"/g, '&quot;') + '">'
      + '<span class="xmod-island-seg-dot xmod-icon-active" aria-hidden="true">' + glyph + '</span>'
      + '<span class="xmod-island-seg-name">' + safeLabel + '</span>'
      + '<span class="xmod-island-seg-ctx">' + String(safeValue).replace(/</g, '&lt;') + '</span>'
      + '</button>';
  }

  // Build the segment HTML for one module's xmod bar.
  // UR-007 fix: ALWAYS render the two cross-module buttons, even when the
  // current page has no org/TA context. Previously the buttons silently
  // disappeared when there was no anchor — a user toggling modules from
  // a fresh load would see an empty bar with just the "Cross-Intelligence"
  // label, which read as "broken." Now the buttons are always present;
  // when there's no payload to forward, clicking switches to the target
  // module's default (no anchor mutation).
  function _xmodSegmentsFor(currentKey, state) {
    const pills = [];
    const anchorOrg = _pickAnchorOrg(state);
    const currentTA = _pickFirstTA(state);

    // Helper — emit a single pill. When ctx is empty, render a subtle
    // "open" placeholder and call switchModule without state mutation.
    function pill(target, label, ctxValue, displayTransform, onclickWithCtx) {
      const hasCtx = ctxValue && String(ctxValue).trim();
      const display = hasCtx
        ? (typeof displayTransform === 'function' ? displayTransform(ctxValue) : ctxValue)
        : 'open';
      const onclick = hasCtx
        ? onclickWithCtx(_esc(ctxValue))
        : "switchModule('" + target + "',true)";
      pills.push(_pillHTML(target, label, display, onclick));
    }

    if (currentKey === 'strategy') {
      // From PartnerIQ → CompeteIQ (anchor org), → WhitespaceIQ (current TA)
      pill('compete', 'CompeteIQ', anchorOrg, _displayOrg,
        function(orgEsc) { return "STATE.comp_selected_org='" + orgEsc + "';STATE._xmod_org=true;switchModule('compete',true)"; });
      pill('opp', 'WhiteSpace IQ', currentTA, _displayTA,
        function(taEsc) { return "STATE.opp_ta='" + taEsc + "';switchModule('opp',true)"; });
    } else if (currentKey === 'compete') {
      // From CompeteIQ → PartnerIQ (selected org), → WhitespaceIQ (current TA)
      pill('strategy', 'PartnerIQ', anchorOrg, _displayOrg,
        function(orgEsc) { return "STATE.strat_anchor_org='" + orgEsc + "';STATE.strat_subview='partners';switchModule('strategy',true)"; });
      pill('opp', 'WhiteSpace IQ', currentTA, _displayTA,
        function(taEsc) { return "STATE.opp_ta='" + taEsc + "';switchModule('opp',true)"; });
    } else if (currentKey === 'opp') {
      // From WhitespaceIQ → CompeteIQ (selected disease/TA), → PartnerIQ (top org)
      pill('compete', 'CompeteIQ', currentTA, _displayTA,
        function(taEsc) { return "STATE.comp_ta='" + taEsc + "';switchModule('compete',true)"; });
      pill('strategy', 'PartnerIQ', anchorOrg, _displayOrg,
        function(orgEsc) { return "STATE.strat_anchor_org='" + orgEsc + "';STATE.strat_subview='partners';switchModule('strategy',true)"; });
    }

    if (pills.length === 0) return '';
    // V1 shape (CTK Q4): xmod-island.expanded > xmod-island-segments (which
    // carries the "Cross-Intelligence" ::before label) > xmod-island-seg items
    // separated by xmod-island-sep dividers.
    const sep = '<span class="xmod-island-sep" aria-hidden="true"></span>';
    return '<div class="xmod-island expanded" role="group" aria-label="Cross-Intelligence">'
      + '<div class="xmod-island-segments">'
      + pills.join(sep)
      + '</div></div>';
  }

  function refreshAllXmodBars() {
    const state = window.STATE || {};
    for (let i = 0; i < XMOD_KEYS.length; i++) {
      const key = XMOD_KEYS[i];
      const wrap = document.getElementById('xmod-island-wrap-' + key);
      if (!wrap) continue;
      wrap.innerHTML = _xmodSegmentsFor(key, state);
    }
  }

  // Expose for legacy callers (per partneriq.md L95, competeiq_landscape.md L29)
  window.refreshAllXmodBars = refreshAllXmodBars;

  // Refresh after imperative switchModule() calls (from inside module code).
  // chrome.jsx _switchModuleInternal now calls refreshAllXmodBars synchronously
  // after renderFn() for sidebar/initial-mount paths, so this covers the
  // remaining path: window.switchModule('key', true) fired from module JS.
  window.addEventListener('chrome:switch', function () {
    // Small defer here because imperative switchModule() fires the event
    // BEFORE _switchModuleInternal has run renderFn() and set innerHTML.
    setTimeout(refreshAllXmodBars, 80);
  });

  // ── Mobile shell toggle ────────────────────────────────────────────────────
  // ≤768px → hide #root, show #mobile-shell. Re-evaluated on every resize.
  function _updateMobileShell() {
    const root = document.getElementById('root');
    const mobile = document.getElementById('mobile-shell');
    if (!root || !mobile) return;
    if (window.innerWidth <= 768) {
      root.hidden = true;
      mobile.hidden = false;
    } else {
      root.hidden = false;
      mobile.hidden = true;
    }
  }
  _updateMobileShell();
  window.addEventListener('resize', _updateMobileShell);

  // ── postMessage handshake ──────────────────────────────────────────────────
  // Prefer the helpers exposed by data.jsx (window.DASHIQ.postReady /
  // window.DASHIQ.onActivate). Fall back to direct postMessage with explicit
  // origin TODO if those helpers are absent.

  // Allowlist of marketing-site origins that may embed the dashboard. Add
  // staging hosts here as they come online. window.DASHIQ_ALLOWED_ORIGINS
  // (defined in V2/index.html) is consulted first so embeds can be tested
  // from local hosts without a code change. '*' is intentionally NOT in the
  // list — silent drop is the right behavior for unknown frames.
  var EMBED_ORIGIN_ALLOWLIST = (Array.isArray(window.DASHIQ_ALLOWED_ORIGINS) ? window.DASHIQ_ALLOWED_ORIGINS : [])
    .concat([
      'https://collabiqcore.com',
      'https://www.collabiqcore.com',
      'https://staging.collabiqcore.com',
    ]);
  function _isAllowedEmbedOrigin(origin) {
    if (!origin || typeof origin !== 'string') return false;
    return EMBED_ORIGIN_ALLOWLIST.indexOf(origin) !== -1;
  }

  function _postReady() {
    if (window.parent === window) return; // not embedded
    if (window.DASHIQ && typeof window.DASHIQ.postReady === 'function') {
      window.DASHIQ.postReady();
      return;
    }
    // Try each allowlisted origin. Browsers silently drop messages whose
    // targetOrigin doesn't match the parent's actual origin, so spamming the
    // allowlist is safe — only the matching one actually delivers.
    EMBED_ORIGIN_ALLOWLIST.forEach(function(origin) {
      try { window.parent.postMessage({ type: 'COLLABIQ_READY' }, origin); } catch (_) {}
    });
  }

  function _onActivate() {
    document.documentElement.classList.add('activated');
    refreshAllXmodBars();
  }

  // Wire activate listener via the data.jsx helper when available, else
  // listen directly on window.message.
  if (window.DASHIQ && typeof window.DASHIQ.onActivate === 'function') {
    window.DASHIQ.onActivate(_onActivate);
  } else {
    // Origin-pinned listener: drop messages from unknown frames so a
    // malicious embedder can't trigger our activation flow.
    window.addEventListener('message', function (e) {
      if (!e || !_isAllowedEmbedOrigin(e.origin)) return;
      if (e.data && e.data.type === 'COLLABIQ_ACTIVATE') _onActivate();
    });
  }

  // Fire COLLABIQ_READY once the preload Promise.allSettled resolves. We can't
  // hook it directly (preload returns a promise we don't get a handle to), so
  // we poll cache.stats() — when any prefetch lands, we know preload is moving.
  // Bail at 8s regardless so the parent never hangs.
  function _fireReadyWhenLoaded() {
    let posted = false;
    const tryPost = function () {
      if (posted) return false;
      const stats = window.DASHIQ
        && window.DASHIQ.cache
        && typeof window.DASHIQ.cache.stats === 'function'
        ? window.DASHIQ.cache.stats()
        : null;
      // cache.stats returns { total, live }; fire when at least one entry is live.
      const live = stats ? (stats.live || stats.total || 0) : 0;
      if (live > 0) {
        posted = true;
        _postReady();
        return true;
      }
      return false;
    };
    if (tryPost()) return;
    const iv = setInterval(function () {
      if (tryPost()) clearInterval(iv);
    }, 200);
    setTimeout(function () {
      clearInterval(iv);
      if (!posted) {
        posted = true;
        _postReady();
      }
    }, 8000);
  }

  // ── Root <App/> component ──────────────────────────────────────────────────
  function App() {
    // window.__TWEAK_DEFAULTS is set in V2/index.html — never redefined here.
    // Defaults source-of-truth: tokens.css → __TWEAK_DEFAULTS → fallback object.
    // Reading the computed --type-scale first means the tweaks panel can't
    // silently regress the Phase 1 token swap (was: typeScale:1 default
    // clobbered tokens.css's --type-scale:0.83 every render).
    const _rootCS = (typeof document !== 'undefined') ? getComputedStyle(document.documentElement) : null;
    const _cssTypeScale = _rootCS ? parseFloat(_rootCS.getPropertyValue('--type-scale')) : NaN;
    if (window.__TWEAK_DEFAULTS && Number.isFinite(_cssTypeScale)) {
      window.__TWEAK_DEFAULTS.typeScale = _cssTypeScale;
    }
    const tweakDefaults = window.__TWEAK_DEFAULTS || {
      depth: 1,
      typeScale: 1,
      layoutMode: 'canvas',
      parallax: true,
      ambient: true,
    };
    const tweakHook = (typeof window.useTweaks === 'function')
      ? window.useTweaks(tweakDefaults)
      : [tweakDefaults, function () {}];
    const tweaks = tweakHook[0];
    const setTweak = tweakHook[1];

    // Apply tweak values to CSS vars on <html> so token consumers pick them up.
    React.useEffect(function () {
      const r = document.documentElement;
      r.style.setProperty('--depth', String(tweaks.depth));
      r.style.setProperty('--type-scale', String(tweaks.typeScale));
      r.dataset.layoutMode = tweaks.layoutMode;
      r.dataset.parallax = tweaks.parallax ? 'on' : 'off';
      r.dataset.ambient = tweaks.ambient ? 'on' : 'off';
    }, [tweaks.depth, tweaks.typeScale, tweaks.layoutMode, tweaks.parallax, tweaks.ambient]);

    // Fire READY when data preload settles (or after the 8s bail-out).
    React.useEffect(function () {
      _fireReadyWhenLoaded();
    }, []);

    // When NOT embedded, auto-activate after 100ms so the entry animation plays
    // for standalone visitors (otherwise we'd sit forever in the dormant state).
    React.useEffect(function () {
      if (window.parent !== window) return undefined;
      const t = setTimeout(_onActivate, 100);
      return function () { clearTimeout(t); };
    }, []);

    // Re-populate the cross-module bar once the initial module mounts.
    React.useEffect(function () {
      const t = setTimeout(refreshAllXmodBars, 200);
      return function () { clearTimeout(t); };
    }, []);

    // ── DASH_TELEMETRY broadcaster ──────────────────────────────────────────
    // Ports the V1 telemetry pipe (index_LG_PROD.html L30870-30922) so the
    // marketing-site hero animation can align its mirror components against
    // the live dashboard iframe (radar disc, evidence drawer, sidebar).
    //
    // Two channels, both run in parallel (matching V1):
    //   1. DASH_TELEMETRY    — full per-rect payload (table/sonar/radar/evidence/sidebar/theme)
    //   2. collabiq:telemetry — normalized {sonar:{cx,cy,r}, sidebar:{x,y,w,h}} shape
    //
    // Each broadcaster signature-dedupes against its last emit so we don't
    // spam the parent on every MutationObserver tick. Listeners are torn
    // down on unmount so React Fast Refresh / hot reload doesn't leak them.
    React.useEffect(function () {
      // Only run when actually iframed AND embed mode (?embed=true). Standalone
      // visitors never have a parent that wants telemetry.
      if (window === window.parent) return undefined;

      // ── Channel 1: DASH_TELEMETRY (full payload) ───────────────────────
      let lastSig = '';
      function broadcastTelemetry() {
        const tableNode    = document.getElementById('strat-table-wrap');
        const sonarNode    = document.querySelector('.sonar-glass-disc');
        const radarNode    = document.getElementById('sonar-ornament-card');
        const evidenceNode = document.getElementById('strat-evidence-panel');
        const sidebarNode  = document.getElementById('sidebar-nav');

        const payload = {
          table:    tableNode    ? tableNode.getBoundingClientRect()    : null,
          sonar:    sonarNode    ? sonarNode.getBoundingClientRect()    : null,
          radar:    radarNode    ? radarNode.getBoundingClientRect()    : null,
          evidence: evidenceNode && evidenceNode.offsetWidth > 0 ? evidenceNode.getBoundingClientRect() : null,
          sidebar:  sidebarNode  && sidebarNode.offsetWidth  > 0 ? sidebarNode.getBoundingClientRect()  : null,
          theme:    document.documentElement.getAttribute('data-theme') || 'light',
        };

        // Lightweight signature — round rects to int + theme. Skips re-emit
        // when nothing layout-relevant changed.
        function _rectSig(r) {
          if (!r) return 'null';
          return [r.left|0, r.top|0, r.width|0, r.height|0].join(',');
        }
        const sig = [
          _rectSig(payload.table),
          _rectSig(payload.sonar),
          _rectSig(payload.radar),
          _rectSig(payload.evidence),
          _rectSig(payload.sidebar),
          payload.theme,
        ].join('|');
        if (sig === lastSig) return;
        lastSig = sig;

        try {
          window.parent.postMessage({ type: 'DASH_TELEMETRY', payload: payload }, '*');
        } catch (_) { /* cross-origin parent — ignore */ }

        // Same-page consumers (e.g. dev overlays) can listen on this event.
        try {
          window.dispatchEvent(new CustomEvent('collabiq:telemetry', { detail: payload }));
        } catch (_) {}
      }

      // ── Channel 2: collabiq:telemetry (normalized) ─────────────────────
      let lastSig2 = '';
      function postNormalizedTelemetry() {
        // The radar element carries .sonar-glass-disc; .sonar-disc is the
        // alias the marketing site expects. Either selector resolves it.
        const sonar = document.querySelector('.sonar-disc')
                   || document.querySelector('.sonar-glass-disc');
        const sb    = document.querySelector('.sidebar-nav')
                   || document.getElementById('sidebar-nav');
        if (!sonar || !sb) return;
        const s = sonar.getBoundingClientRect();
        const b = sb.getBoundingClientRect();
        const sig2 = [
          (s.left + s.width / 2)|0,
          (s.top  + s.height / 2)|0,
          (s.width / 2)|0,
          b.left|0, b.top|0, b.width|0, b.height|0,
          window.innerWidth, window.innerHeight,
        ].join('|');
        if (sig2 === lastSig2) return;
        lastSig2 = sig2;

        try {
          window.parent.postMessage({
            type:    'collabiq:telemetry',
            iframeW: window.innerWidth,
            iframeH: window.innerHeight,
            sonar:   { cx: s.left + s.width / 2, cy: s.top + s.height / 2, r: s.width / 2 },
            sidebar: { x: b.left, y: b.top, w: b.width, h: b.height },
          }, '*');
        } catch (_) {}
      }

      function broadcastBoth() {
        broadcastTelemetry();
        postNormalizedTelemetry();
      }

      // Resize / scroll listeners (passive — never blocks the main thread).
      window.addEventListener('resize', broadcastBoth, { passive: true });
      window.addEventListener('scroll', broadcastBoth, { passive: true });

      // MutationObserver — V2 builds panels via React + innerHTML inside
      // partneriq/competeiq/whitespaceiq, so DOM layout changes after mount.
      // Watch body for childList/subtree/attributes (theme flips on <html>
      // bubble through document, but we also watch documentElement to
      // pick up the data-theme swap directly).
      const bodyObserver = new MutationObserver(broadcastBoth);
      bodyObserver.observe(document.body, { childList: true, subtree: true, attributes: true });
      const htmlObserver = new MutationObserver(broadcastBoth);
      htmlObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });

      // ResizeObserver on body (covers the V1 secondary-channel trigger).
      let resizeObs = null;
      if (typeof ResizeObserver !== 'undefined') {
        resizeObs = new ResizeObserver(broadcastBoth);
        resizeObs.observe(document.body);
      }

      // Initial boot broadcast (fire after a tick so the first render lands).
      const bootT = setTimeout(broadcastBoth, 500);

      // Adaptive settle-poll — keep firing every 500ms until all source
      // widgets have non-zero bounds, then stop. Cap at 60s as a safety so
      // we never poll forever (matches V1 line 30908-30921).
      let settleCount = 0;
      const settleIv = setInterval(function () {
        broadcastBoth();
        settleCount++;
        const sonar = document.querySelector('.sonar-glass-disc') || document.querySelector('.sonar-disc');
        const allReady = ['strat-table-wrap', 'sonar-ornament-card', 'strat-evidence-panel']
          .every(function (id) { const el = document.getElementById(id); return el && el.offsetWidth > 0; })
          && sonar && sonar.offsetWidth > 0;
        if (allReady || settleCount >= 120) clearInterval(settleIv);
      }, 500);

      return function () {
        window.removeEventListener('resize', broadcastBoth);
        window.removeEventListener('scroll', broadcastBoth);
        bodyObserver.disconnect();
        htmlObserver.disconnect();
        if (resizeObs) resizeObs.disconnect();
        clearTimeout(bootT);
        clearInterval(settleIv);
      };
    }, []);

    const tweakChildren = [];
    if (typeof window.TweakSection === 'function') {
      tweakChildren.push(React.createElement(window.TweakSection, { key: 'sec-display', label: 'Display' }));
    }
    if (typeof window.TweakSlider === 'function') {
      tweakChildren.push(React.createElement(window.TweakSlider, {
        key: 'depth',
        label: 'Depth',
        value: tweaks.depth,
        min: 0,
        max: 2,
        step: 0.1,
        onChange: function (v) { setTweak('depth', v); },
      }));
      tweakChildren.push(React.createElement(window.TweakSlider, {
        key: 'type-scale',
        label: 'Type scale',
        value: tweaks.typeScale,
        min: 0.8,
        max: 1.4,
        step: 0.05,
        onChange: function (v) { setTweak('typeScale', v); },
      }));
    }
    if (typeof window.TweakRadio === 'function') {
      tweakChildren.push(React.createElement(window.TweakRadio, {
        key: 'layout-mode',
        label: 'Layout',
        value: tweaks.layoutMode,
        options: ['canvas', 'compact'],
        onChange: function (v) { setTweak('layoutMode', v); },
      }));
    }
    if (typeof window.TweakToggle === 'function') {
      tweakChildren.push(React.createElement(window.TweakToggle, {
        key: 'parallax',
        label: 'Parallax',
        value: !!tweaks.parallax,
        onChange: function (v) { setTweak('parallax', v); },
      }));
      tweakChildren.push(React.createElement(window.TweakToggle, {
        key: 'ambient',
        label: 'Ambient orbs',
        value: !!tweaks.ambient,
        onChange: function (v) { setTweak('ambient', v); },
      }));
    }

    return React.createElement(
      React.Fragment,
      null,
      (typeof window.Chrome === 'function')
        ? React.createElement(window.Chrome, null)
        : null,
      (typeof window.TweaksPanel === 'function')
        ? React.createElement(window.TweaksPanel, { title: 'CollabIQ Tweaks' }, tweakChildren)
        : null
    );
  }

  // ── ExploreIQ module wiring sanity check ───────────────────────────────────
  // Chrome (chrome.jsx) owns the activeModule conditional render and mounts
  // <ExploreIQModule/> directly into #panel-exploreiq when activeModule ===
  // 'exploreiq'. We surface a console warning here if the module did not
  // attach to window — typically a script-load-order regression in
  // V2/index.html. This is the explicit conditional referencing exploreiq
  // in app.jsx; the actual JSX mount lives inside <Chrome/>.
  if (typeof window.ExploreIQModule !== 'function') {
    console.warn('[app] window.ExploreIQModule not found — chrome.jsx must load before app.jsx for the exploreiq route to render.');
  }

  // ── Mount via ReactDOM.createRoot (React 18, idempotent) ───────────────────
  function mount() {
    const rootEl = document.getElementById('root');
    if (!rootEl) return;
    if (rootEl._reactRoot) return; // already mounted
    const root = ReactDOM.createRoot(rootEl);
    rootEl._reactRoot = root;
    root.render(React.createElement(App));
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', mount);
  } else {
    mount();
  }

  // Expose root for debugging / tests.
  window.App = App;
}());
