// Dependency graph — force/radial/matrix variants
// Deterministic positions so it's static hi-fi

function seededNodes() {
  // 60 nodes clustered — names themed as Zope/Plone artifacts
  const names = [
    'portal_skins', 'portal_workflow', 'portal_types', 'portal_catalog', 'portal_membership',
    'portal_actions', 'portal_properties', 'acl_users', 'MailHost', 'PatientRecord',
    'AdmissionForm', 'DischargeSummary', 'billing_export', 'hl7_router', 'icd10_lookup',
    'LabResult', 'RadiologyOrder', 'MedicationList', 'ward_roster', 'shift_handover',
    'consent_form', 'bed_allocation', 'triage_script', 'emr_sync', 'pharmacy_queue',
    'pdf_template', 'barcode_render', 'auth_handler', 'session_cache', 'ldap_bridge',
    'Diagnosis', 'Referral', 'Appointment', 'PatientChart', 'VitalSigns',
    'sendMail', 'printLabel', 'archiveRecord', 'purgeSession', 'rebuildIndex',
    'Catalog.py', 'Folder.py', 'ObjectManager', 'SkinTool', 'ZCatalog',
    'customer_portal', 'ward_dashboard', 'nursing_station', 'admit_workflow', 'exportCSV',
    'ledger.py', 'invoice_gen', 'insurance_api', 'claim_router', 'diagnostic_code',
    'signoffScript', 'auditLog', 'soapHandler', 'xmlRPCBridge', 'legacyAdapter',
  ];
  const nodes = names.map((name, i) => {
    const cluster = i < 14 ? 0 : i < 30 ? 1 : i < 44 ? 2 : 3;
    // stable pseudo-random
    const r1 = ((i * 9301 + 49297) % 233280) / 233280;
    const r2 = ((i * 7919 + 104729) % 233280) / 233280;
    return { id: i, name, cluster, r1, r2,
      risk: ((i * 31 + 13) % 100),
      ttw: (i * 17) % 10 < 4, // edited through the web
      size: 3 + ((i * 13) % 6),
    };
  });
  const edges = [];
  for (let i = 0; i < nodes.length; i++) {
    const degree = 2 + ((i * 7) % 5);
    for (let k = 0; k < degree; k++) {
      let target = (i + (k + 1) * 7 + (i % 5)) % nodes.length;
      if (target === i) target = (i + 1) % nodes.length;
      edges.push([i, target]);
    }
  }
  return { nodes, edges };
}

const GRAPH_DATA = seededNodes();

function DependencyGraph({ style = 'force', width = 620, height = 420, highlight = null, compact = false }) {
  const { nodes, edges } = GRAPH_DATA;
  const positions = useMemo(() => {
    if (style === 'radial') {
      // concentric rings by cluster
      const rings = [0.15, 0.32, 0.52, 0.72];
      return nodes.map((n) => {
        const ringR = rings[n.cluster] * Math.min(width, height);
        const angle = (n.r1 * Math.PI * 2) + n.cluster * 0.3;
        return {
          x: width / 2 + Math.cos(angle) * ringR,
          y: height / 2 + Math.sin(angle) * ringR,
        };
      });
    }
    if (style === 'matrix') {
      const cols = 10, rows = 6;
      const cellW = (width - 40) / cols;
      const cellH = (height - 40) / rows;
      return nodes.map((n, i) => ({
        x: 20 + (i % cols) * cellW + cellW / 2,
        y: 20 + Math.floor(i / cols) * cellH + cellH / 2,
      }));
    }
    // force — pre-baked blobby layout by cluster
    const centers = [
      { x: width * 0.28, y: height * 0.32 },
      { x: width * 0.70, y: height * 0.28 },
      { x: width * 0.30, y: height * 0.72 },
      { x: width * 0.72, y: height * 0.72 },
    ];
    return nodes.map((n) => {
      const c = centers[n.cluster];
      const spread = 110;
      return {
        x: c.x + (n.r1 - 0.5) * spread,
        y: c.y + (n.r2 - 0.5) * spread,
      };
    });
  }, [style, width, height, nodes]);

  if (style === 'matrix') {
    // Adjacency matrix style
    const cols = 10, rows = 6;
    const cellSize = Math.min((width - 40) / cols, (height - 40) / rows);
    return (
      <svg width={width} height={height} style={{ display: 'block' }}>
        <rect width={width} height={height} fill="var(--bg-1)"/>
        {nodes.map((n, i) => {
          const col = i % cols, row = Math.floor(i / cols);
          const x = 20 + col * cellSize;
          const y = 20 + row * cellSize;
          const fill = n.ttw ? 'var(--rust)' : n.risk > 70 ? 'var(--bone)' : 'var(--slate)';
          const opacity = 0.3 + (n.risk / 100) * 0.7;
          return (
            <g key={i}>
              <rect x={x + 1} y={y + 1} width={cellSize - 2} height={cellSize - 2} fill={fill} opacity={opacity}/>
              {n.ttw && <rect x={x + 1} y={y + 1} width={cellSize - 2} height={cellSize - 2} fill="none" stroke="var(--rust)" strokeWidth="0.5"/>}
            </g>
          );
        })}
        {/* Row/col labels */}
        {[...Array(cols)].map((_, i) => (
          <line key={`cg-${i}`} x1={20 + i * cellSize} y1={16} x2={20 + i * cellSize} y2={22} stroke="var(--line-2)"/>
        ))}
      </svg>
    );
  }

  return (
    <svg width={width} height={height} style={{ display: 'block' }}>
      <rect width={width} height={height} fill="var(--bg-1)"/>
      {/* edges */}
      <g stroke="var(--line-2)" strokeWidth="0.5" fill="none" opacity="0.7">
        {edges.map(([a, b], i) => {
          const pa = positions[a], pb = positions[b];
          const isHi = highlight != null && (a === highlight || b === highlight);
          return (
            <line
              key={i}
              x1={pa.x} y1={pa.y}
              x2={pb.x} y2={pb.y}
              stroke={isHi ? 'var(--bone)' : 'var(--line-2)'}
              strokeWidth={isHi ? 1 : 0.5}
              opacity={isHi ? 0.9 : 0.5}
            />
          );
        })}
      </g>
      {/* nodes */}
      <g>
        {nodes.map((n, i) => {
          const p = positions[i];
          const fill = n.ttw ? 'var(--rust)' : n.risk > 70 ? 'var(--bone)' : n.risk > 40 ? 'var(--slate)' : 'var(--lichen)';
          const stroke = n.ttw ? 'var(--rust)' : 'var(--ink-dim)';
          return (
            <g key={i}>
              <circle cx={p.x} cy={p.y} r={n.size} fill={fill} opacity={0.85} stroke={stroke} strokeWidth="0.5"/>
              {n.ttw && (
                <circle cx={p.x} cy={p.y} r={n.size + 3} fill="none" stroke="var(--rust)" strokeWidth="0.5" opacity="0.6"/>
              )}
            </g>
          );
        })}
      </g>
      {/* sparse labels on biggest/ttw nodes */}
      {!compact && (
        <g fontFamily="var(--mono)" fontSize="9.5" fill="var(--muted)">
          {nodes.filter((n) => n.ttw && n.size >= 6).slice(0, 8).map((n) => {
            const p = positions[n.id];
            return (
              <text key={n.id} x={p.x + 8} y={p.y + 3}>{n.name}</text>
            );
          })}
        </g>
      )}
    </svg>
  );
}

// ---------- Risk stratigraphy + coupling heat strip ----------
// Designed for the dependency-section "executive view".
// Left: per-cluster horizontal stacked bars. Right: top-15 coupling list.
// Below: one-sentence summary. Raw force graph collapses into <details>.

const RISK_CLUSTERS = [
  // Each row: sync + div + ttw === total (invariant checked by validateDemoFixture)
  // Totals: total=145, ttw=125 (86%), div=8, sync=12
  { label: 'Portal Tools',     code: 'portal_*',        total: 28, sync:  2, div:  2, ttw: 24 },
  { label: 'Custom Scripts',   code: 'Script (Python)', total: 52, sync:  4, div:  3, ttw: 45 },
  { label: 'Page Templates',   code: 'ZPT / DTML',      total: 31, sync:  3, div:  2, ttw: 26 },
  { label: 'External Methods', code: 'ExternalMethod',  total: 18, sync:  1, div:  1, ttw: 16 },
  { label: 'DTML Documents',   code: 'DTML Method',     total: 16, sync:  2, div:  0, ttw: 14 },
];

// Canonical demo fixture — single source of truth for all preview sections.
// Derived from RISK_CLUSTERS above: total=145, ttw=125 (86%), div=8, sync=12.
// dashboard.jsx reads window.FOSSYL_DEMO for all preview fallback values.
const _RC_TTW   = RISK_CLUSTERS.reduce((s, c) => s + c.ttw,   0);
const _RC_TOTAL = RISK_CLUSTERS.reduce((s, c) => s + c.total, 0);
const FOSSYL_DEMO = Object.freeze({
  total:    _RC_TOTAL,                                          // 145
  ttw:      _RC_TTW,                                           // 125
  diverged: RISK_CLUSTERS.reduce((s, c) => s + c.div,   0),   // 8
  synced:   RISK_CLUSTERS.reduce((s, c) => s + c.sync,  0),   // 12
  ttwPct:   Math.round(_RC_TTW / _RC_TOTAL * 100),             // 86
  edges:    481,
  elapsed:  52,
  deathKb:  187,   // estimated reclaim for 23 dead-code candidates
});
window.FOSSYL_DEMO = FOSSYL_DEMO;

function validateDemoFixture() {
  const DEV = window.location.hostname === 'localhost' || window.location.hostname.startsWith('127.');
  const D = window.FOSSYL_DEMO;
  const errors = [];
  const fail = (msg) => { console.error('[demo-fixture]', msg); errors.push(msg); };

  // 1. Cluster-level: sync+div+ttw===total
  RISK_CLUSTERS.forEach(c => {
    if (c.sync + c.div + c.ttw !== c.total)
      fail(`cluster "${c.label}": sync+div+ttw=${c.sync+c.div+c.ttw} ≠ total=${c.total}`);
  });

  // 2. Top-level bucket sums and range
  if (D.ttw + D.diverged + D.synced !== D.total)
    fail(`ttw+diverged+synced=${D.ttw+D.diverged+D.synced} ≠ total=${D.total}`);
  if (D.ttw > D.total)
    fail(`ttw=${D.ttw} > total=${D.total}`);

  // 3. Displayed untracked % is derived, not hardcoded
  const expectedPct = Math.round(D.ttw / D.total * 100);
  if (D.ttwPct !== expectedPct)
    fail(`ttwPct=${D.ttwPct} ≠ round(ttw/total*100)=${expectedPct}`);

  // 4. Risk formula weights (set by dashboard.jsx §03 IIFE)
  const W = window.FOSSYL_RISK_WEIGHTS;
  if (W) {
    if (Math.abs(W.in + W.ch + W.ttw - 1.0) > 1e-9)
      fail(`FOSSYL_RISK_WEIGHTS sum=${W.in+W.ch+W.ttw} ≠ 1.0`);
    if (W.in !== 0.6 || W.ch !== 0.2 || W.ttw !== 0.2)
      fail(`FOSSYL_RISK_WEIGHTS expected 0.6/0.2/0.2 got ${W.in}/${W.ch}/${W.ttw}`);
  } else {
    fail('FOSSYL_RISK_WEIGHTS not set — dashboard.jsx §03 IIFE not loaded?');
  }

  // 5. §03 coupling rows: formula correctness and raw-score monotonicity
  const rows = window.FOSSYL_COUPLING_ROWS;
  if (rows && W) {
    const score = r => W.in*r.in + W.ch*r.ch + W.ttw*r.ttw;
    const maxRaw = Math.max(...rows.map(score));
    rows.forEach(r => {
      const expected = Math.round(score(r) / maxRaw * 100);
      if (r.risk !== expected)
        fail(`§03 ${r.n}: risk=${r.risk} ≠ formula gives ${expected}`);
    });
    for (let i = 0; i < rows.length - 1; i++) {
      const rawA = score(rows[i]), rawB = score(rows[i+1]);
      if (rawA < rawB)
        fail(`§03 raw score not monotonic: ${rows[i].n}(${rawA.toFixed(2)}) < ${rows[i+1].n}(${rawB.toFixed(2)})`);
    }
  } else if (!rows) {
    fail('FOSSYL_COUPLING_ROWS not set — dashboard.jsx not loaded?');
  }

  // 6. sendReferralFax inDeg single-sourced: COUPLING_TOP == §03 == §01
  const faxTop = COUPLING_TOP.find(r => r.name === 'sendReferralFax');
  const fax03  = rows && rows.find(r => r.n === 'sendReferralFax');
  const fax01  = window.FOSSYL_TTW_TABLE && window.FOSSYL_TTW_TABLE.find(r => String(r[1]).includes('sendReferralFax'));
  if (faxTop && fax03 && faxTop.inDeg !== fax03.in)
    fail(`sendReferralFax inDeg COUPLING_TOP=${faxTop.inDeg} ≠ §03=${fax03.in}`);
  if (faxTop && fax01 && faxTop.inDeg !== fax01[4])
    fail(`sendReferralFax inDeg COUPLING_TOP=${faxTop.inDeg} ≠ §01 callers=${fax01[4]}`);
  if (fax03 && fax01 && fax03.in !== fax01[4])
    fail(`sendReferralFax inDeg §03=${fax03.in} ≠ §01=${fax01[4]}`);

  // 7. §04 version similarities: all in [0.86, 1.0)
  const vgroups = window.FOSSYL_VERSION_GROUPS;
  if (vgroups) {
    vgroups.forEach(g => {
      g.sims.forEach(s => {
        if (s <= 0 || s >= 1.0) fail(`§04 ${g.n}: sim=${s} not in (0, 1.0)`);
        if (s < 0.86) fail(`§04 ${g.n}: sim=${s} below threshold 0.86`);
      });
      if (g.sims.length !== g.copies) fail(`§04 ${g.n}: sims.length=${g.sims.length} ≠ copies=${g.copies}`);
    });
  }

  // 8. Highly-coupled count: exec summary == card == COUPLING_TOP list length
  const hc = window.FOSSYL_HIGHLY_COUPLED;
  if (hc !== undefined) {
    const cardCount = (window.COUPLING_TOP || []).filter(c => c.inDeg >= 5).length;
    const listLen   = (window.COUPLING_TOP || []).length;
    if (hc !== cardCount) fail(`highly_coupled exec_summary=${hc} ≠ filter_count=${cardCount}`);
    if (hc !== listLen)   fail(`highly_coupled exec_summary=${hc} ≠ COUPLING_TOP.length=${listLen}`);
  }

  // 9. §6 upload-toggle and §6 report-header copy must both embed FOSSYL_SIGNATURES_PAYLOAD_DESC
  //    and enumerate exactly the field types in the extractor allow-list.
  //    Required tokens mirror the five categories in intent_inferrer.py's allow-lists.
  //    Forbidden tokens catch descriptions that claim to send things the extractor strips.
  const PAYLOAD_REQUIRED = [
    '## ',        // Zope ## header directives (Parameters, Returns, Bind, …)
    'import',     // import / from-import module names
    'signature',  // function and class signatures
    'METAL',      // METAL macro/slot names (ZPT)
  ];
  const PAYLOAD_FORBIDDEN = ['docstring'];

  const toggleCopy = window.FOSSYL_AI_TOGGLE_COPY || '';
  const kickerCopy = window.FOSSYL_AI_KICKER_COPY || '';
  if (toggleCopy && kickerCopy) {
    for (const tok of PAYLOAD_REQUIRED) {
      if (!toggleCopy.includes(tok))
        fail(`§6 copy drift: upload toggle missing required payload token "${tok}"`);
      if (!kickerCopy.includes(tok))
        fail(`§6 copy drift: §6 kicker missing required payload token "${tok}"`);
    }
    for (const tok of PAYLOAD_FORBIDDEN) {
      if (toggleCopy.toLowerCase().includes(tok))
        fail(`§6 copy drift: upload toggle contains forbidden token "${tok}" — extractor strips it`);
      if (kickerCopy.toLowerCase().includes(tok))
        fail(`§6 copy drift: §6 kicker contains forbidden token "${tok}" — extractor strips it`);
    }
    // Both strings embed the same shared payload description constant
    const payloadDesc = window.FOSSYL_SIGNATURES_PAYLOAD_DESC || '';
    if (payloadDesc && !toggleCopy.includes(payloadDesc))
      fail('§6 copy drift: upload toggle does not embed FOSSYL_SIGNATURES_PAYLOAD_DESC');
    if (payloadDesc && !kickerCopy.includes(payloadDesc))
      fail('§6 copy drift: §6 kicker does not embed FOSSYL_SIGNATURES_PAYLOAD_DESC');
  }

  // 10. Printed formula label reflects actual weight constants
  const formula = window.FOSSYL_RISK_FORMULA;
  if (W && formula) {
    if (!formula.includes(`${W.in}·in`))    fail(`formula label missing ${W.in}·in`);
    if (!formula.includes(`${W.ch}·churn`)) fail(`formula label missing ${W.ch}·churn`);
    if (!formula.includes(`${W.ttw}·ttw`))  fail(`formula label missing ${W.ttw}·ttw`);
  } else if (W && !formula) {
    fail('FOSSYL_RISK_FORMULA not set — §03 IIFE not loaded?');
  }

  if (errors.length === 0) {
    console.log('[demo-fixture] all invariants pass');
  } else if (DEV) {
    throw new Error(`[demo-fixture] ${errors.length} invariant(s) failed:\n${errors.join('\n')}`);
  }
}
window.validateDemoFixture = validateDemoFixture;

function validateRealStats(stats) {
  const DEV = window.location.hostname === 'localhost' || window.location.hostname.startsWith('127.');
  const errors = [];
  const fail = (msg) => { console.error('[real-stats]', msg); errors.push(msg); };

  const ttw       = stats.ttw      || 0;
  const div       = stats.diverged || 0;
  const sync      = stats.synced   || 0;
  const totalZodb = ttw + div + sync;

  if (ttw > totalZodb)
    fail(`ttw=${ttw} > total_zodb=${totalZodb} — TTW cannot exceed total ZODB objects`);

  if (totalZodb > 0) {
    const pct = Math.round(ttw / totalZodb * 100);
    if (pct < 0 || pct > 100)
      fail(`computed ttwPct=${pct} out of range for ttw=${ttw}, total_zodb=${totalZodb}`);
  }

  if (errors.length === 0) console.log('[real-stats] all invariants pass');
  else if (DEV) throw new Error(`[real-stats] ${errors.length} invariant(s) failed:\n${errors.join('\n')}`);
}
window.validateRealStats = validateRealStats;

const COUPLING_TOP = [
  { name: 'admitPatient',           type: 'Script (Python)', inDeg: 41, ttw: true,  risk: 'critical' },
  { name: 'portal_catalog',         type: 'ZCatalog',        inDeg: 38, ttw: false, risk: 'high'     },
  { name: 'PatientRecord',          type: 'Content type',    inDeg: 33, ttw: false, risk: 'high'     },
  { name: 'billing_export',         type: 'Script (Python)', inDeg: 29, ttw: true,  risk: 'critical' },
  { name: 'sendReferralFax',        type: 'Script (Python)', inDeg: 23, ttw: true,  risk: 'critical' },
  { name: 'hl7_router',             type: 'External Method', inDeg: 22, ttw: false, risk: 'high'     },
  { name: 'auth_handler',           type: 'Script (Python)', inDeg: 21, ttw: true,  risk: 'critical' },
  { name: 'ward_dashboard',         type: 'Page Template',   inDeg: 18, ttw: false, risk: 'medium'   },
  { name: 'icd10_lookup',           type: 'Script (Python)', inDeg: 17, ttw: true,  risk: 'high'     },
  { name: 'discharge_summary',      type: 'DTML Method',     inDeg: 15, ttw: true,  risk: 'high'     },
  { name: 'consent_form',           type: 'Page Template',   inDeg: 13, ttw: false, risk: 'medium'   },
  { name: 'ledger.py',              type: 'External Method', inDeg: 12, ttw: false, risk: 'medium'   },
  { name: 'admit_workflow',         type: 'Script (Python)', inDeg: 11, ttw: true,  risk: 'high'     },
  { name: 'archiveRecord',          type: 'Script (Python)', inDeg:  9, ttw: true,  risk: 'medium'   },
  { name: 'pdf_template',           type: 'Page Template',   inDeg:  8, ttw: false, risk: 'low'      },
];

function RiskStratigraphy({ height = 360 }) {
  const totalArtifacts = RISK_CLUSTERS.reduce((a, c) => a + c.total, 0);
  const totalTtw = RISK_CLUSTERS.reduce((a, c) => a + c.ttw, 0);
  const ttwPct = Math.round((totalTtw / totalArtifacts) * 100);
  const fiveOrMore = COUPLING_TOP.filter((c) => c.inDeg >= 5).length;
  window.FOSSYL_HIGHLY_COUPLED = fiveOrMore;
  const maxTotal = Math.max(...RISK_CLUSTERS.map((c) => c.total));

  return (
    <div className="col g-0">
      <div className="row" style={{ border: '1px solid var(--line)', background: 'var(--bg-1)', alignItems: 'stretch' }}>
        {/* LEFT — Risk stratification bars */}
        <div style={{ flex: 1.15, padding: '20px 24px', borderRight: '1px solid var(--line)' }}>
          <div className="row jc-b ai-c" style={{ marginBottom: 18 }}>
            <span className="caps">A — Risk by artifact cluster</span>
            <span className="mono" style={{ fontSize: 10.5, color: 'var(--muted)' }}>demo reference · {totalArtifacts} ZODB objects · 5 clusters</span>
          </div>
          <div className="col g-14">
            {RISK_CLUSTERS.map((c) => (
              <ClusterBar key={c.label} cluster={c} maxTotal={maxTotal} />
            ))}
          </div>
          <div className="row g-16" style={{ marginTop: 22, paddingTop: 14, borderTop: '1px dashed var(--line)' }}>
            <LegendDot color="var(--lichen)" label="synchronized · in git"/>
            <LegendDot color="var(--bone)"   label="diverged · drift between fs and zodb"/>
            <LegendDot color="var(--rust)"   label="TTW-only · not in version control"/>
          </div>
        </div>

        {/* RIGHT — Coupling heat strip */}
        <div style={{ flex: 1, padding: '20px 24px' }}>
          <div className="row jc-b ai-c" style={{ marginBottom: 18 }}>
            <span className="caps">B — Top-15 coupling — what breaks if you touch it</span>
            <span className="mono" style={{ fontSize: 10.5, color: 'var(--muted)' }}>sorted by in-degree</span>
          </div>
          <div className="col g-0" style={{ borderTop: '1px solid var(--line)' }}>
            {COUPLING_TOP.map((c, i) => (
              <CouplingRow key={c.name} item={c} rank={i + 1} maxIn={COUPLING_TOP[0].inDeg}/>
            ))}
          </div>
          {/* Panel B legend — dot color = risk tier, independent of panel A VCS-status colors */}
          <div className="row g-16" style={{ marginTop: 12, paddingTop: 10, borderTop: '1px dashed var(--line)' }}>
            <LegendDot color="var(--rust)"   label="critical risk"/>
            <LegendDot color="var(--bone)"   label="high risk"/>
            <LegendDot color="var(--slate)"  label="medium risk"/>
            <LegendDot color="var(--lichen)" label="low risk"/>
          </div>
        </div>
      </div>

      {/* Executive summary */}
      <div style={{
        padding: '20px 28px',
        background: 'var(--bg-1)',
        border: '1px solid var(--line)',
        borderTop: 'none',
        display: 'grid',
        gridTemplateColumns: '180px 1fr',
        gap: 28,
        alignItems: 'start',
      }}>
        <div>
          <span className="caps">Executive summary</span>
          <div className="mono" style={{ fontSize: 10.5, color: 'var(--muted)', marginTop: 6 }}>auto-generated · §2.0</div>
        </div>
        <p className="serif" style={{
          margin: 0, fontSize: 18, lineHeight: 1.5, color: 'var(--ink)',
          fontWeight: 300, letterSpacing: '-0.005em',
          textWrap: 'pretty',
        }}>
          <span className="rust" style={{ fontWeight: 500 }}>{ttwPct}% of live production code</span> exists only in the database with no version control.
          {' '}<span className="bone" style={{ fontWeight: 500 }}>{fiveOrMore} artifacts</span> are depended on by five or more other scripts —
          changing any of these without a full audit risks cascading failures across patient admission, billing, and discharge workflows.
        </p>
      </div>
    </div>
  );
}

function ClusterBar({ cluster, maxTotal }) {
  const w = (cluster.total / maxTotal) * 100;
  const segs = [
    ['var(--lichen)', cluster.sync],
    ['var(--bone)',   cluster.div],
    ['var(--rust)',   cluster.ttw],
  ];
  return (
    <div className="col g-6">
      <div className="row jc-b ai-c">
        <div className="row g-10 ai-b">
          <span style={{ fontSize: 13.5, color: 'var(--ink)', fontWeight: 500 }}>{cluster.label}</span>
          <span className="mono" style={{ fontSize: 10.5, color: 'var(--muted)' }}>{cluster.code}</span>
        </div>
        <div className="row g-10 mono" style={{ fontSize: 11 }}>
          <span style={{ color: 'var(--lichen)' }}>{cluster.sync}</span>
          <span style={{ color: 'var(--dim)' }}>·</span>
          <span style={{ color: 'var(--bone)' }}>{cluster.div}</span>
          <span style={{ color: 'var(--dim)' }}>·</span>
          <span style={{ color: 'var(--rust)' }}>{cluster.ttw}</span>
          <span style={{ color: 'var(--dim)' }}>=</span>
          <span style={{ color: 'var(--ink)' }}>{cluster.total}</span>
        </div>
      </div>
      <div style={{ width: '100%', height: 14, background: 'var(--bg)', border: '1px solid var(--line)', position: 'relative' }}>
        <div className="row g-0" style={{ width: w + '%', height: '100%' }}>
          {segs.map(([color, n], i) => (
            <div
              key={i}
              style={{
                width: (n / cluster.total) * 100 + '%',
                background: color,
                opacity: 0.85,
                borderRight: i < 2 && n > 0 ? '1px solid rgba(11,12,13,0.4)' : 'none',
              }}
            />
          ))}
        </div>
      </div>
    </div>
  );
}

function CouplingRow({ item, rank, maxIn }) {
  const pct = (item.inDeg / maxIn) * 100;
  const dotColor = item.risk === 'critical' ? 'var(--rust)' : item.risk === 'high' ? 'var(--bone)' : item.risk === 'medium' ? 'var(--slate)' : 'var(--lichen)';
  return (
    <div
      className="row ai-c"
      style={{
        padding: '7px 0',
        borderBottom: '1px solid var(--line)',
        gap: 12,
      }}
    >
      <span className="mono" style={{ fontSize: 10.5, color: 'var(--dim)', width: 22, textAlign: 'right', flexShrink: 0 }}>{String(rank).padStart(2, '0')}</span>
      <div style={{ width: 7, height: 7, borderRadius: '50%', background: dotColor, flexShrink: 0 }}/>
      <div className="col" style={{ flex: 1, minWidth: 0 }}>
        <div className="row g-8 ai-c">
          <span className="mono" style={{ fontSize: 12, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.name}</span>
          {item.ttw && (
            <span style={{
              fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.08em',
              color: 'var(--rust)', border: '1px solid var(--rust-dim)', padding: '1px 5px',
              borderRadius: 1, lineHeight: 1.3,
            }}>TTW</span>
          )}
        </div>
        <span className="mono" style={{ fontSize: 10, color: 'var(--muted)' }}>{item.type}</span>
      </div>
      {/* In-degree bar */}
      <div style={{ width: 110, height: 4, background: 'var(--bg-2)', flexShrink: 0, position: 'relative' }}>
        <div style={{ position: 'absolute', inset: 0, width: pct + '%', background: dotColor, opacity: 0.85 }}/>
      </div>
      <span className="mono" style={{ fontSize: 11, color: 'var(--ink-dim)', width: 28, textAlign: 'right', flexShrink: 0 }}>{item.inDeg}</span>
    </div>
  );
}

function LegendDot({ color, label }) {
  return (
    <div className="row g-6 ai-c">
      <div style={{ width: 8, height: 8, background: color, borderRadius: 1 }}/>
      <span className="mono" style={{ fontSize: 10.5, color: 'var(--muted)' }}>{label}</span>
    </div>
  );
}

Object.assign(window, { DependencyGraph, GRAPH_DATA, RiskStratigraphy, COUPLING_TOP });

// validateDemoFixture is called from the end of dashboard.jsx, after all window globals are set.