/* global React, Icon, I, Chip, ACCENT, CopyButton, Variant, SkeletonRow, CONSENSIO_DATA */

const { useState: useState2, useEffect: useEffect2, useMemo: useMemo2, useRef: useRef2 } = React;
const D2 = window.CONSENSIO_DATA;

// ── Family detail ────────────────────────────────────────────────────────────
function FamilyScreen({ navigate, viewState = 'normal' }) {
  const r = D2.rowe;
  const proband = r.proband;
  const [localState, setLocalState] = useState2(viewState);
  const [replyOpen, setReplyOpen] = useState2(false);
  const [replyText, setReplyText] = useState2('');
  const [replySent, setReplySent] = useState2(false);
  useEffect2(() => { setLocalState(viewState); }, [viewState]);

  const sendReply = () => {
    if (!replyText.trim()) return;
    const reply = { caseId: r.id, to: 'Dr. R. Mahalingam', message: replyText, timestamp: new Date().toISOString() };
    // localStorage fallback
    try {
      const stored = JSON.parse(localStorage.getItem('consensio_clinician_replies') || '[]');
      stored.push(reply);
      localStorage.setItem('consensio_clinician_replies', JSON.stringify(stored));
    } catch (_) {}
    // Supabase persist
    if (window.__sb && window.__sbUser) {
      window.__sb.from('variant_replies').insert({
        user_id: window.__sbUser.id, case_id: r.id,
        recipient: 'Dr. R. Mahalingam', message: replyText,
      }).then(() => {});
    }
    audit('clinician_reply_sent', r.id, { requester: 'Dr. R. Mahalingam', messageLen: replyText.length });
    setReplyText('');
    setReplyOpen(false);
    setReplySent(true);
    setTimeout(() => setReplySent(false), 3000);
  };

  if (localState === 'error') {
    return (
      <div style={{ padding: 32, maxWidth: 1080, margin: '0 auto' }}>
        <ErrorState resource="case" detail={`Case ${r.id}`}
          onRetry={() => setLocalState('normal')} />
      </div>
    );
  }
  if (localState === 'loading') {
    return (
      <div style={{ padding: 32, maxWidth: 1480, margin: '0 auto' }}>
        <div style={{ marginBottom: 24 }}>
          <div className="skeleton" style={{ height: 20, width: 240, marginBottom: 8 }} />
          <div className="skeleton" style={{ height: 12, width: 320 }} />
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16 }}>
          <ListSkeleton rows={3} cols={[120, 240, 100]} />
          <ListSkeleton rows={3} cols={[120, 100, 60, 60, 80]} />
        </div>
      </div>
    );
  }

  return (
    <div style={{ padding: 32, maxWidth: 1480, margin: '0 auto' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }}>
            <h1 className="t-page" style={{ margin: 0 }}>{(window.maskPHI || (s => s))(r.family)} family</h1>
            <Chip tone="info">In review</Chip>
            <Chip tone="warning">High priority</Chip>
          </div>
          <div className="t-meta">
            <span className="mono">{(window.maskPHI || (s => s))(r.id)}</span> · 4 members · trio WGS · enrolled Mar 14, 2026
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="btn"><Icon d={I.external} size={13} />Open report</button>
          <button className="btn btn-primary" onClick={() => navigate('variants')}>
            Open variants <Icon d={I.chevronR} size={13} />
          </button>
        </div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16 }}>
        {/* Left: AI publication summary + phenotype */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <AiSummaryCard ai={r.aiSummary} />

          <section className="surface" style={{ padding: 0, overflow: 'hidden' }}>
            <header style={{ padding: '12px 16px', borderBottom: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between' }}>
              <div className="t-card">Phenotype · HPO</div>
              <span className="t-meta">{r.phenotype.length} terms · Mahalingam et al.</span>
            </header>
            <table className="cx">
              <thead>
                <tr>
                  <th>HPO</th><th>Term</th><th>Onset</th>
                </tr>
              </thead>
              <tbody>
                {r.phenotype.map(p => (
                  <tr key={p.hpo} style={{ cursor: 'default' }}>
                    <td className="mono" style={{ color: 'var(--accent-info)' }}>
                      <HPOTerm hpoId={p.hpo} label={p.hpo} />
                    </td>
                    <td>{p.term}</td>
                    <td className="t-meta">{p.onset}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </section>
        </div>

        {/* Right: pedigree + members + actions */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <PedigreeCard />

          <section className="surface" style={{ padding: 0, overflow: 'hidden' }}>
            <header style={{ padding: '12px 16px', borderBottom: '1px solid var(--border)' }}>
              <div className="t-card">Family members</div>
            </header>
            <table className="cx">
              <thead>
                <tr><th>Member</th><th>ID</th><th>Sex</th><th>Age</th><th>Seq</th></tr>
              </thead>
              <tbody>
                <tr style={{ cursor: 'default' }}>
                  <td style={{ fontWeight: 500 }}>
                    {proband.label}
                    <span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: 3, background: 'var(--accent-danger)', marginLeft: 8, verticalAlign: 'middle' }} title="Affected" />
                  </td>
                  <td className="mono">{proband.id}</td>
                  <td>{proband.sex}</td>
                  <td>{proband.age}</td>
                  <td><Chip tone="info" dot={false}>{proband.sequenced}</Chip></td>
                </tr>
                {r.members.map(m => (
                  <tr key={m.id} style={{ cursor: 'default' }}>
                    <td>{m.label}</td>
                    <td className="mono">{m.id}</td>
                    <td>{m.sex}</td>
                    <td>{m.age}</td>
                    <td>{m.sequenced === '—' ? <span className="t-meta">—</span> : <Chip tone="neutral" dot={false}>{m.sequenced}</Chip>}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </section>

          <section className="surface" style={{ padding: 16 }}>
            <div className="t-card" style={{ marginBottom: 12 }}>Quick actions</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              <button className="btn" style={{ height: 32, justifyContent: 'flex-start' }}>
                <Icon d={I.bolt} size={13} />Trigger reanalysis
              </button>
              <button className="btn" style={{ height: 32, justifyContent: 'flex-start' }}
                      onClick={() => setReplyOpen(true)}>
                <Icon d={I.user} size={13} />Reply to clinician request
              </button>
              <button className="btn" style={{ height: 32, justifyContent: 'flex-start' }}
                      onClick={() => audit('presentation_generated', r.id)}>
                <Icon d={I.paper} size={13} />Generate draft report
              </button>
            </div>
          </section>
        </div>
      </div>

      {/* Clinician reply modal */}
      {replyOpen && (
        <Modal onClose={() => setReplyOpen(false)}>
          <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border)' }}>
            <div className="t-card">Reply to Dr. R. Mahalingam</div>
            <div className="t-meta" style={{ marginTop: 4 }}>
              Re: {r.id} — Update on functional evidence requested by May 24
            </div>
          </div>
          <div style={{ padding: 20 }}>
            <textarea
              value={replyText}
              onChange={e => setReplyText(e.target.value)}
              placeholder="Type your reply..."
              rows={6}
              style={{
                width: '100%', resize: 'vertical', padding: 10,
                background: 'var(--bg-base)', border: '1px solid var(--border)',
                borderRadius: 6, color: 'var(--text-primary)', fontSize: 13,
                fontFamily: 'inherit', boxSizing: 'border-box',
              }}
            />
            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
              <button className="btn" onClick={() => setReplyOpen(false)}>Cancel</button>
              <button className="btn btn-primary" onClick={sendReply} disabled={!replyText.trim()}>
                <Icon d={I.check} size={13} />Send reply
              </button>
            </div>
          </div>
        </Modal>
      )}

      {/* Reply sent toast */}
      {replySent && (
        <div style={{
          position: 'fixed', left: '50%', bottom: 24, transform: 'translateX(-50%)',
          background: 'var(--bg-raised)', border: '1px solid var(--border-hover)',
          borderRadius: 6, padding: '10px 16px',
          display: 'flex', alignItems: 'center', gap: 8,
          boxShadow: '0 8px 32px rgba(0,0,0,0.4)', zIndex: 200,
        }}>
          <Icon d={I.check} size={14} />
          <span style={{ fontSize: 13 }}>Reply sent to Dr. R. Mahalingam</span>
        </div>
      )}
    </div>
  );
}

// AI publication summary card — note authentication-gated share link
function AiSummaryCard({ ai }) {
  return (
    <section className="surface" style={{ padding: 0, overflow: 'hidden', position: 'relative' }}>
      {/* AI-tinted left border */}
      <div style={{
        position: 'absolute', left: 0, top: 0, bottom: 0, width: 3,
        background: 'linear-gradient(180deg, var(--accent-info), var(--accent-paper))',
      }} />
      <header style={{ padding: '14px 16px 12px 19px', borderBottom: '1px solid var(--border)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
          <Icon d={I.bolt} size={14} />
          <div className="t-card">AI publication summary</div>
          <AiBadge label="AI" />
        </div>
        <div className="t-meta">{ai.model} · generated {ai.generatedAt}</div>
      </header>

      <div style={{ padding: '16px 16px 16px 19px' }}>
        <p style={{ margin: '0 0 16px', fontSize: 13, lineHeight: 1.55, color: 'var(--text-primary)' }}>
          {ai.headline}
        </p>
        <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
          {ai.points.map((p, i) => (
            <li key={i} style={{ display: 'flex', gap: 10, fontSize: 13 }}>
              <span style={{
                width: 18, height: 18, borderRadius: 4, flexShrink: 0, marginTop: 1,
                background: `color-mix(in oklab, ${ACCENT[p.tone]} 22%, transparent)`,
                color: ACCENT[p.tone],
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>
                <Icon d={p.tone === 'paper' ? I.paper : p.tone === 'request' ? I.user : p.tone === 'warning' ? I.alert : I.bolt} size={11} />
              </span>
              <span style={{ color: 'var(--text-secondary)', lineHeight: 1.55 }}>{p.text}</span>
            </li>
          ))}
        </ul>
      </div>

      <footer style={{
        padding: '12px 16px 12px 19px',
        borderTop: '1px solid var(--border)',
        background: 'var(--bg-raised)',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      }}>
        <div className="t-meta">
          <Icon d={I.alert} size={12} /> AI output requires analyst review before clinical action.
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="btn btn-sm">Mark reviewed</button>
          <button className="btn btn-sm">Regenerate</button>
        </div>
      </footer>
    </section>
  );
}

// Pedigree — purely data-driven. Accepts PedigreeNode[]:
// { id, label, sex: 'M'|'F'|'U', affected, carrier, deceased, generation, x, y, parents: string[] }
function PedigreeCard({ nodes = D2.pedigree.nodes }) {
  // Compute parent connector lines per generation
  const byId = Object.fromEntries(nodes.map(n => [n.id, n]));
  const childGroups = {};
  nodes.forEach(n => {
    if (n.parents && n.parents.length === 2) {
      const key = [...n.parents].sort().join('|');
      (childGroups[key] = childGroups[key] || []).push(n);
    }
  });

  const renderShape = (n) => {
    const fill = n.affected ? 'var(--accent-danger)' : 'var(--bg-surface)';
    const stroke = n.affected ? 'var(--accent-danger)' : 'var(--text-secondary)';
    const w = 28, h = 28;
    const sw = 1.5;
    const cx = n.x, cy = n.y;
    let shape;
    if (n.sex === 'M') {
      shape = <rect x={cx - w/2} y={cy - h/2} width={w} height={h} fill={fill} stroke={stroke} strokeWidth={sw} />;
    } else if (n.sex === 'F') {
      shape = <circle cx={cx} cy={cy} r={w/2} fill={fill} stroke={stroke} strokeWidth={sw} />;
    } else {
      // diamond for unknown
      shape = <polygon points={`${cx},${cy-h/2} ${cx+w/2},${cy} ${cx},${cy+h/2} ${cx-w/2},${cy}`} fill={fill} stroke={stroke} strokeWidth={sw} />;
    }
    return (
      <g key={n.id}>
        {/* carrier: diagonal half-fill via clipPath */}
        {n.carrier && !n.affected && (
          <g clipPath={`url(#clip-${n.id})`}>
            {n.sex === 'M' && <rect x={cx - w/2} y={cy - h/2} width={w} height={h} fill="var(--text-secondary)" opacity="0.45" />}
            {n.sex === 'F' && <circle cx={cx} cy={cy} r={w/2} fill="var(--text-secondary)" opacity="0.45" />}
          </g>
        )}
        <defs>
          <clipPath id={`clip-${n.id}`}>
            <polygon points={`${cx - w/2},${cy + h/2} ${cx + w/2},${cy - h/2} ${cx + w/2},${cy + h/2}`} />
          </clipPath>
        </defs>
        {shape}
        {/* deceased: diagonal line through */}
        {n.deceased && (
          <line x1={cx - w/2 - 4} y1={cy + h/2 + 4} x2={cx + w/2 + 4} y2={cy - h/2 - 4}
                stroke="var(--text-primary)" strokeWidth="1.5" />
        )}
        {/* proband arrow */}
        {n.proband && (
          <g stroke="var(--accent-danger)" strokeWidth="1.5" fill="none">
            <path d={`M${cx - w/2 - 14} ${cy + h/2 + 8} L${cx - w/2 - 2} ${cy + h/2 - 4}`} />
            <path d={`M${cx - w/2 - 6} ${cy + h/2 - 6} L${cx - w/2 - 2} ${cy + h/2 - 4} L${cx - w/2 - 4} ${cy + h/2}`} />
          </g>
        )}
        {/* id label above */}
        <text x={cx} y={cy - h/2 - 6} textAnchor="middle" fontSize="10"
              fill={n.affected ? 'var(--text-primary)' : 'var(--text-secondary)'}
              fontFamily="var(--font-mono)">
          {window.maskPHI ? window.maskPHI(n.id) : n.id}
        </text>
        {/* role label below */}
        <text x={cx} y={cy + h/2 + 12} textAnchor="middle" fontSize="10"
              fill="var(--text-primary)">{n.label}</text>
        {n.age && (
          <text x={cx} y={cy + h/2 + 22} textAnchor="middle" fontSize="9"
                fill="var(--text-tertiary)">{n.age}</text>
        )}
      </g>
    );
  };

  return (
    <section className="surface" style={{ padding: 16 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 12 }}>
        <div className="t-card">Pedigree</div>
        <button className="btn btn-ghost btn-sm">Expand <Icon d={I.external} size={11} /></button>
      </div>
      <svg width="100%" height="200" viewBox="0 0 280 200" style={{ display: 'block' }}>
        {/* Connector lines per couple */}
        <g stroke="var(--text-tertiary)" strokeWidth="1" fill="none">
          {Object.entries(childGroups).map(([key, children]) => {
            const [p1Id, p2Id] = key.split('|');
            const p1 = byId[p1Id], p2 = byId[p2Id];
            if (!p1 || !p2) return null;
            const midX = (p1.x + p2.x) / 2;
            const parentY = (p1.y + p2.y) / 2;
            const childY = children[0].y - 14 - 8;
            return (
              <g key={key}>
                <line x1={p1.x + 14} y1={p1.y} x2={p2.x - 14} y2={p2.y} />
                <line x1={midX} y1={parentY} x2={midX} y2={childY} />
                <line x1={Math.min(...children.map(c => c.x))} y1={childY}
                      x2={Math.max(...children.map(c => c.x))} y2={childY} />
                {children.map(c => (
                  <line key={c.id} x1={c.x} y1={childY} x2={c.x} y2={c.y - 14} />
                ))}
              </g>
            );
          })}
        </g>

        {/* Generation labels */}
        {[...new Set(nodes.map(n => n.generation))].sort().map((g, i) => {
          const sample = nodes.find(n => n.generation === g);
          return (
            <text key={g} x={8} y={sample.y + 3}
                  fontSize="10" fill="var(--text-tertiary)" fontFamily="var(--font-mono)">
              {'I'.repeat(g)}
            </text>
          );
        })}

        {nodes.map(renderShape)}

        {/* Legend */}
        <g transform="translate(216, 28)">
          <circle cx="6" cy="6" r="5" fill="var(--accent-danger)" />
          <text x="16" y="9" fontSize="10" fill="var(--text-secondary)">Affected</text>
          <circle cx="6" cy="24" r="5" fill="var(--bg-surface)" stroke="var(--text-secondary)" />
          <text x="16" y="27" fontSize="10" fill="var(--text-secondary)">Unaffected</text>
          <circle cx="6" cy="42" r="5" fill="var(--bg-surface)" stroke="var(--text-secondary)" />
          <path d="M2 47 L10 37" stroke="var(--text-secondary)" strokeWidth="1" opacity="0.6" />
          <text x="16" y="45" fontSize="10" fill="var(--text-secondary)">Carrier</text>
          <circle cx="6" cy="60" r="5" fill="var(--bg-surface)" stroke="var(--text-secondary)" />
          <line x1="0" y1="65" x2="12" y2="55" stroke="var(--text-primary)" strokeWidth="1.5" />
          <text x="16" y="63" fontSize="10" fill="var(--text-secondary)">Deceased</text>
        </g>
      </svg>
    </section>
  );
}

// ── Variants Screen ──────────────────────────────────────────────────────────
function VariantsScreen() {
  const [selectedRank, setSelectedRank] = useState2(1);
  const [classifying, setClassifying] = useState2(false);
  const [pendingChange, setPendingChange] = useState2(null);
  const [pendingClass, setPendingClass] = useState2(null);
  const [chatOpen, setChatOpen] = useState2(true);
  const [flashRank, setFlashRank] = useState2(null);
  const [tagPopover, setTagPopover] = useState2(null); // { rowId, anchor }
  const [acmgPopover, setAcmgPopover] = useState2(false); // for candidate
  const [pendingTagEdits, setPendingTagEdits] = useState2({}); // rowId -> { tags, timer }
  const [loading, setLoading] = useState2(true);
  const [showExcluded, setShowExcluded] = useState2(false);
  const [acmgOverrides, setAcmgOverrides] = useState2(() => {
    try { return JSON.parse(localStorage.getItem('consensio_acmg_overrides') || '{}'); } catch (_) { return {}; }
  });
  const [tagOverrides, setTagOverrides] = useState2(() => {
    try { return JSON.parse(localStorage.getItem('consensio_tag_overrides') || '{}'); } catch (_) { return {}; }
  });

  const selected = D2.variants.find(v => v.rank === selectedRank);

  // Evidence rows for the selected candidate
  const allEvidence = useMemo2(
    () => (D2.evidenceFixtures || []).filter(r => r.variantRank === selectedRank),
    [selectedRank]
  );

  const getRowTags = (r) => pendingTagEdits[r.id]?.tags ?? tagOverrides[r.id] ?? r.tags;

  const evidenceRows = useMemo2(() => {
    if (showExcluded) return allEvidence;
    return allEvidence.filter(r => !getRowTags(r).includes('excluded'));
  }, [allEvidence, showExcluded, pendingTagEdits, tagOverrides]);

  const excludedCount = allEvidence.length - evidenceRows.length;

  // Listen for chat-triggered row flashes
  useEffect2(() => {
    const handler = (e) => {
      const rank = e.detail;
      setSelectedRank(rank);
      setFlashRank(rank);
      setTimeout(() => setFlashRank(null), 800);
    };
    window.addEventListener('consensio:flash-variant', handler);
    return () => window.removeEventListener('consensio:flash-variant', handler);
  }, []);

  // On mount: merge Supabase ACMG overrides into local state
  useEffect2(() => {
    if (!window.__sb || !window.__sbUser) return;
    window.__sb.from('acmg_overrides')
      .select('variant_key,classification')
      .eq('sample_id', 'rowe-demo')
      .eq('user_id', window.__sbUser.id)
      .then(({ data }) => {
        if (!data) return;
        const remote = {};
        data.forEach(r => { remote[r.variant_key] = r.classification; });
        setAcmgOverrides(prev => ({ ...remote, ...prev }));
      });
    window.__sb.from('tag_overrides')
      .select('row_id,tags')
      .eq('sample_id', 'rowe-demo')
      .eq('user_id', window.__sbUser.id)
      .then(({ data }) => {
        if (!data) return;
        const remote = {};
        data.forEach(r => { remote[r.row_id] = r.tags; });
        setTagOverrides(prev => ({ ...remote, ...prev }));
      });
  }, []);

  // Auto-save with 10s undo window — commit to localStorage + Supabase on expiry
  useEffect2(() => {
    if (!pendingChange) return;
    const t = setTimeout(() => {
      setAcmgOverrides(prev => {
        const next = { ...prev, [pendingChange.rank]: pendingChange.to };
        try { localStorage.setItem('consensio_acmg_overrides', JSON.stringify(next)); } catch (_) {}
        if (window.__sb && window.__sbUser) {
          window.__sb.from('acmg_overrides').upsert({
            user_id: window.__sbUser.id, sample_id: 'rowe-demo',
            variant_key: String(pendingChange.rank), classification: pendingChange.to,
          }, { onConflict: 'user_id,sample_id,variant_key' }).then(() => {});
        }
        return next;
      });
      setPendingChange(null);
      setPendingClass(null);
    }, 10000);
    return () => clearTimeout(t);
  }, [pendingChange]);

  // Per-variant evidence load (skeleton, never spinner)
  useEffect2(() => {
    setLoading(true);
    audit('ai_evidence_view', `${selected.gene}:${selected.hgvs}`);
    const t = setTimeout(() => setLoading(false), 600);
    return () => clearTimeout(t);
  }, [selectedRank]);

  const applyClassification = (newClass) => {
    setPendingChange({ rank: selectedRank, from: selected.acmg, to: newClass, at: Date.now() });
    setPendingClass(newClass);
    setClassifying(false);
    audit('variant_classification_change', `${selected.gene}:${selected.hgvs}`, { from: selected.acmg, to: newClass });
  };
  const undo = () => { setPendingChange(null); setPendingClass(null); };

  const displayClass = (pendingChange && pendingChange.rank === selectedRank)
    ? pendingChange.to
    : (acmgOverrides[selectedRank] || selected.acmg);

  const ACMG_OPTIONS = [
    { key: 'Pathogenic',        tone: 'danger'  },
    { key: 'Likely Pathogenic', tone: 'warning' },
    { key: 'VUS',               tone: 'neutral' },
    { key: 'Likely Benign',     tone: 'info'    },
    { key: 'Benign',            tone: 'success' },
  ];

  // Tag commit with 10s undo — commit to localStorage + Supabase on expiry
  const onChangeRowTags = (rowId, nextTags) => {
    setPendingTagEdits(prev => {
      const existing = prev[rowId];
      if (existing?.timer) clearTimeout(existing.timer);
      const timer = setTimeout(() => {
        setPendingTagEdits(p => {
          const n = { ...p };
          const committed = n[rowId]?.tags;
          delete n[rowId];
          if (committed !== undefined) {
            setTagOverrides(t => {
              const next = { ...t, [rowId]: committed };
              try { localStorage.setItem('consensio_tag_overrides', JSON.stringify(next)); } catch (_) {}
              if (window.__sb && window.__sbUser) {
                window.__sb.from('tag_overrides').upsert({
                  user_id: window.__sbUser.id, sample_id: 'rowe-demo',
                  variant_key: 'rowe', row_id: rowId, tags: committed,
                }, { onConflict: 'user_id,sample_id,row_id' }).then(() => {});
              }
              return next;
            });
          }
          return n;
        });
      }, 10000);
      return { ...prev, [rowId]: { tags: nextTags, timer } };
    });
  };

  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
      {/* Header */}
      <div style={{ padding: '20px 32px 16px', borderBottom: '1px solid var(--border)', background: 'var(--bg-base)' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
          <div>
            <h1 className="t-page" style={{ margin: 0 }}>Variants · Rowe family</h1>
            <div className="t-meta" style={{ marginTop: 4 }}>
              1,842 called · 47 high-impact · <strong style={{ color: 'var(--accent-info)' }}>{D2.variants.length} candidates</strong> after Consensio prioritization
            </div>
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <button className="btn"><Icon d={I.filter} size={13} />Filters · 6</button>
            <button className="btn" onClick={() => {
              const header = 'Gene\tHGVS\tProtein\tACMG\tOMIM\tInheritance\tgnomAD_AF';
              const rows = D2.variants.map(v => [
                v.gene, v.hgvs, v.protein,
                acmgOverrides[v.rank] || v.acmg,
                v.omim, v.inheritance, v.gnomadAf,
              ].join('\t'));
              const tsv = [header, ...rows].join('\n');
              const a = document.createElement('a');
              a.href = URL.createObjectURL(new Blob([tsv], { type: 'text/tab-separated-values' }));
              a.download = 'rowe_candidates.tsv';
              a.click();
              URL.revokeObjectURL(a.href);
              audit('case_export', 'rowe_variants', { format: 'tsv', count: D2.variants.length });
            }}><Icon d={I.external} size={13} />Export VCF</button>
            <button className={`btn${chatOpen ? ' btn-primary' : ''}`} onClick={() => setChatOpen(!chatOpen)}>
              <Icon d={I.bolt} size={13} />AI assistant
              <span className="kbd" style={{ marginLeft: 4 }}>⌘J</span>
            </button>
            <button className="btn btn-primary"><Icon d={I.check} size={13} />Finalize report</button>
          </div>
        </div>
      </div>

      {/* Main + chat layout */}
      <div style={{
        flex: 1, display: 'grid',
        gridTemplateColumns: chatOpen ? 'minmax(0, 1fr) 380px' : 'minmax(0, 1fr)',
        minHeight: 0,
      }}>
        <main style={{ overflow: 'hidden', display: 'flex', flexDirection: 'column', minWidth: 0 }}>
          {/* Candidate strip */}
          <CandidatesStrip
            variants={D2.variants}
            selectedRank={selectedRank}
            onSelect={setSelectedRank}
            displayClass={displayClass}
            pendingChange={pendingChange}
            onReclassify={() => setClassifying(true)}
            flashRank={flashRank}
          />

          {/* Virtualized evidence table */}
          <div style={{ flex: 1, padding: '12px 32px 24px', overflow: 'hidden', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <div className="t-label">Evidence for variant {selectedRank}</div>
                <AiBadge label="AI-prioritized" />
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                {excludedCount > 0 && (
                  <button className="btn btn-ghost btn-sm" onClick={() => setShowExcluded(s => !s)}>
                    {showExcluded ? 'Hide' : 'Show'} excluded ({excludedCount})
                  </button>
                )}
                <span className="t-meta">
                  {loading ? '—' : `${evidenceRows.length} rows · ${evidenceRows.filter(r => r.new).length} new`}
                </span>
              </div>
            </div>

            <VirtualizedEvidenceTable
              rows={evidenceRows}
              loading={loading}
              pendingTagEdits={pendingTagEdits}
              tagOverrides={tagOverrides}
              onOpenTagPopover={(rowId, anchor) => setTagPopover({ rowId, anchor })}
              onOpenNote={(rowId) => {
                audit('note_edit', rowId);
              }}
            />
          </div>
        </main>

        {chatOpen && (
          <aside style={{
            borderLeft: '1px solid var(--border)',
            background: 'var(--bg-base)',
            overflow: 'hidden',
            display: 'flex', flexDirection: 'column',
          }}>
            <AiChatPanel variant={selected} onClose={() => setChatOpen(false)} />
          </aside>
        )}
      </div>

      {/* Undo toast for classification */}
      {pendingChange && (
        <div style={{
          position: 'fixed', left: '50%', bottom: 24, transform: 'translateX(-50%)',
          background: 'var(--bg-raised)', border: '1px solid var(--border-hover)',
          borderRadius: 6, padding: '10px 14px',
          display: 'flex', alignItems: 'center', gap: 12,
          boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
          zIndex: 100, animation: 'fadeIn 200ms ease',
        }}>
          <Icon d={I.check} size={14} />
          <div>
            <div style={{ fontSize: 13 }}>
              Variant {pendingChange.rank} reclassified: <strong>{pendingChange.from}</strong> → <strong>{pendingChange.to}</strong>
            </div>
            <div className="t-meta">Auto-saves in 10s · undo to revert</div>
          </div>
          <button className="btn btn-sm" onClick={undo}>Undo</button>
        </div>
      )}

      {/* Per-row tag undo toast */}
      {Object.keys(pendingTagEdits).length > 0 && (
        <div style={{
          position: 'fixed', left: 32, bottom: 24,
          background: 'var(--bg-raised)', border: '1px solid var(--border-hover)',
          borderRadius: 6, padding: '8px 12px',
          display: 'flex', alignItems: 'center', gap: 10,
          boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
          zIndex: 100, maxWidth: 380,
        }}>
          <Icon d={I.clock} size={12} />
          <div style={{ flex: 1, fontSize: 12 }}>
            {Object.keys(pendingTagEdits).length} evidence row{Object.keys(pendingTagEdits).length > 1 ? 's' : ''} tagged · saving in 10s
          </div>
          <button className="btn btn-sm" onClick={() => {
            Object.values(pendingTagEdits).forEach(p => p.timer && clearTimeout(p.timer));
            setPendingTagEdits({});
          }}>Undo all</button>
        </div>
      )}

      {/* Classify modal */}
      {classifying && (
        <Modal onClose={() => setClassifying(false)}>
          <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border)' }}>
            <div className="t-card">Reclassify variant</div>
            <div className="t-meta" style={{ marginTop: 4 }}>
              <span className="mono">{selected.gene} {selected.hgvs} {selected.protein}</span>
            </div>
          </div>
          <div style={{ padding: 20 }}>
            <div className="t-label" style={{ marginBottom: 12 }}>ACMG classification</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {ACMG_OPTIONS.map(o => (
                <button key={o.key}
                  onClick={() => applyClassification(o.key)}
                  style={{
                    display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    background: 'var(--bg-raised)', border: '1px solid var(--border)',
                    color: 'var(--text-primary)',
                    padding: '12px 16px', borderRadius: 6, cursor: 'pointer',
                    fontSize: 13, fontFamily: 'inherit',
                  }}>
                  <span><Chip tone={o.tone}>{o.key}</Chip></span>
                  {o.key === selected.acmg && <span className="t-meta">current</span>}
                </button>
              ))}
            </div>
            <div className="t-meta" style={{ marginTop: 16, padding: 12, background: 'var(--bg-base)', borderRadius: 6, border: '1px solid var(--border)' }}>
              <Icon d={I.alert} size={12} /> Reclassification is logged with timestamp and reviewer ID. A 10-second undo window will appear after save.
            </div>
          </div>
        </Modal>
      )}

      {/* Tag popover */}
      {tagPopover && (
        <TagPopover
          anchor={tagPopover.anchor}
          tags={pendingTagEdits[tagPopover.rowId]?.tags ?? tagOverrides[tagPopover.rowId] ?? (allEvidence.find(r => r.id === tagPopover.rowId)?.tags || [])}
          onChange={(next) => onChangeRowTags(tagPopover.rowId, next)}
          onClose={() => setTagPopover(null)}
        />
      )}
    </div>
  );
}

// ── Candidate variants strip ────────────────────────────────────────────────
function CandidatesStrip({ variants, selectedRank, onSelect, displayClass, pendingChange, onReclassify, flashRank }) {
  return (
    <div style={{
      padding: '16px 32px',
      borderBottom: '1px solid var(--border)',
      background: 'var(--bg-surface)',
    }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 10 }}>
        <div className="t-label">Candidate variants ({variants.length})</div>
        <span className="t-meta">Click a card to load its evidence below</span>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
        {variants.map(v => {
          const isSel = v.rank === selectedRank;
          const flashing = flashRank === v.rank;
          const cls = isSel ? displayClass : v.acmg;
          const tone = cls === 'Pathogenic' ? 'danger' : cls === 'Likely Pathogenic' ? 'warning' : cls === 'VUS' ? 'neutral' : cls === 'Likely Benign' ? 'info' : 'success';
          return (
            <button key={v.rank}
                    onClick={() => onSelect(v.rank)}
                    style={{
                      appearance: 'none', cursor: 'pointer', textAlign: 'left',
                      background: isSel ? 'color-mix(in oklab, var(--accent-info) 12%, var(--bg-raised))' : 'var(--bg-raised)',
                      border: '1px solid ' + (isSel ? 'color-mix(in oklab, var(--accent-info) 50%, transparent)' : 'var(--border)'),
                      borderRadius: 6, padding: 12, color: 'var(--text-primary)',
                      transition: 'border-color 120ms ease, background 120ms ease',
                      animation: flashing ? 'rowFlash 800ms ease-out' : undefined,
                      fontFamily: 'inherit',
                    }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
                  <span style={{ fontSize: 11, color: 'var(--text-tertiary)', fontWeight: 600 }}>#{v.rank}</span>
                  <span className="mono" style={{ fontSize: 13, fontWeight: 600 }}>{v.gene}</span>
                </div>
                <span className="mono" style={{ fontSize: 11, color: 'var(--text-secondary)' }}>{v.aiScore.toFixed(2)}</span>
              </div>
              <div className="mono" style={{ fontSize: 11, color: 'var(--text-secondary)', marginBottom: 8, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                {v.hgvs} <span style={{ color: 'var(--text-tertiary)' }}>{v.protein}</span>
              </div>
              <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                <Chip tone={tone}>{cls}</Chip>
                {pendingChange && pendingChange.rank === v.rank && (
                  <Chip tone="warning" dot={false} style={{ fontSize: 9 }}>UNDO 10s</Chip>
                )}
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ── Virtualized evidence table ───────────────────────────────────────────────
// Grid rows (no <table>), fixed pixel column widths, manual virtualization,
// overscan 5, calc(100vh - 280px) container height, opacity-pulse skeleton.
function VirtualizedEvidenceTable({ rows, loading, pendingTagEdits, tagOverrides = {}, onOpenTagPopover, onOpenNote }) {
  const ROW_HEIGHT = 44;
  const OVERSCAN = 5;
  // Column widths from spec: checkbox | PMID | Year | Variant | Mut.type | Inh. | Zyg. | Pheno. | Assay | Match | Tags | Note
  const COLS = [32, 80, 52, 220, 100, 90, 72, 140, 110, 90, 120, 48];
  const GRID = COLS.map(w => w + 'px').join(' ');
  const TOTAL_W = COLS.reduce((a, b) => a + b, 0); // 1154
  const HEADERS = ['', 'PMID', 'Year', 'Variant', 'Mut. type', 'Inherit.', 'Zyg.', 'Phenotype', 'Assay', 'Match', 'Tags', ''];

  const scrollRef = useRef2(null);
  const [scrollTop, setScrollTop] = useState2(0);
  const [containerH, setContainerH] = useState2(640);
  const [allSelected, setAllSelected] = useState2(false);
  const [selectedSet, setSelectedSet] = useState2(new Set());

  useEffect2(() => {
    if (!scrollRef.current) return;
    const el = scrollRef.current;
    const measure = () => setContainerH(el.clientHeight);
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  // Reset scroll on rows change
  useEffect2(() => {
    if (scrollRef.current) scrollRef.current.scrollTop = 0;
    setScrollTop(0);
  }, [rows]);

  const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
  const end = Math.min(rows.length, Math.ceil((scrollTop + containerH) / ROW_HEIGHT) + OVERSCAN);
  const visible = rows.slice(start, end);
  const offsetY = start * ROW_HEIGHT;

  const toggleRow = (id) => {
    const next = new Set(selectedSet);
    next.has(id) ? next.delete(id) : next.add(id);
    setSelectedSet(next);
  };

  const matchTone = (m) => m === 'High' ? 'success' : m === 'Medium' ? 'warning' : 'neutral';

  return (
    <div className="surface" style={{
      padding: 0, overflow: 'hidden',
      display: 'flex', flexDirection: 'column',
      flex: 1, minHeight: 0,
    }}>
      {/* Horizontal scroll wrapper for narrow viewports — table is 1154px wide */}
      <div style={{ overflow: 'auto', display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
        {/* Header row */}
        <div style={{
          display: 'grid', gridTemplateColumns: GRID,
          background: 'var(--bg-raised)',
          borderBottom: '1px solid var(--border)',
          width: TOTAL_W, minWidth: TOTAL_W,
          position: 'sticky', top: 0, zIndex: 2,
        }}>
          {HEADERS.map((h, i) => (
            <div key={i} style={{
              padding: '10px 12px',
              fontSize: 11, fontWeight: 500, letterSpacing: 0.04, textTransform: 'uppercase',
              color: 'var(--text-secondary)',
              whiteSpace: 'nowrap', overflow: 'hidden',
              textAlign: i === COLS.length - 1 ? 'center' : (i === 9 ? 'right' : 'left'),
              display: 'flex', alignItems: 'center', gap: 6,
              borderRight: i < HEADERS.length - 1 ? '1px solid var(--border)' : 'none',
            }}>
              {i === 0 ? (
                <input type="checkbox"
                  checked={allSelected}
                  onChange={(e) => {
                    setAllSelected(e.target.checked);
                    setSelectedSet(e.target.checked ? new Set(rows.map(r => r.id)) : new Set());
                  }}
                  style={{ accentColor: 'var(--accent-info)' }} />
              ) : h}
            </div>
          ))}
        </div>

        {/* Body — scrollable container with virtualized children */}
        <div
          ref={scrollRef}
          onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
          style={{
            height: 'calc(100vh - 280px)',
            maxHeight: 'calc(100vh - 280px)',
            minHeight: 220,
            overflowY: 'auto', overflowX: 'hidden',
            width: TOTAL_W, minWidth: TOTAL_W,
            background: 'var(--bg-surface)',
          }}>
          {loading ? (
            // 6 skeleton rows, matching column structure exactly
            <div>
              {Array.from({ length: 6 }).map((_, r) => (
                <div key={r} style={{
                  display: 'grid', gridTemplateColumns: GRID,
                  height: ROW_HEIGHT, alignItems: 'center',
                  borderBottom: '1px solid var(--border)',
                }}>
                  {COLS.map((w, c) => (
                    <div key={c} style={{ padding: '0 12px' }}>
                      <div className="skeleton-pulse" style={{
                        height: c === 0 ? 12 : 10,
                        width: c === 0 ? 12 : Math.max(20, Math.min(w - 32, 100)),
                        margin: c === 0 ? '0 auto' : 0,
                      }} />
                      {c === 3 && (
                        <div className="skeleton-pulse" style={{ height: 8, width: 80, marginTop: 4 }} />
                      )}
                    </div>
                  ))}
                </div>
              ))}
            </div>
          ) : rows.length === 0 ? (
            <div style={{ padding: 32, textAlign: 'center' }}>
              <span className="t-meta">No evidence rows for this candidate.</span>
            </div>
          ) : (
            // Virtualized: total-height spacer + translated visible window
            <div style={{ height: rows.length * ROW_HEIGHT, position: 'relative' }}>
              <div style={{
                position: 'absolute', top: 0, left: 0, right: 0,
                transform: `translateY(${offsetY}px)`,
              }}>
                {visible.map((r) => {
                  const tags = pendingTagEdits[r.id]?.tags ?? tagOverrides[r.id] ?? r.tags;
                  const isExcluded = tags.includes('excluded');
                  const isPending = !!pendingTagEdits[r.id];
                  const isSel = selectedSet.has(r.id);
                  return (
                    <div key={r.id} style={{
                      display: 'grid', gridTemplateColumns: GRID,
                      height: ROW_HEIGHT, alignItems: 'center',
                      borderBottom: '1px solid var(--border)',
                      background: isSel
                        ? 'color-mix(in oklab, var(--accent-info) 14%, var(--bg-surface))'
                        : isPending
                          ? 'color-mix(in oklab, var(--accent-warning) 8%, var(--bg-surface))'
                          : 'transparent',
                      opacity: isExcluded ? 0.5 : 1,
                      textDecoration: isExcluded ? 'line-through' : 'none',
                      transition: 'background 120ms ease',
                      cursor: 'default',
                    }}
                    onMouseEnter={e => { if (!isSel && !isPending) e.currentTarget.style.background = 'var(--bg-raised)'; }}
                    onMouseLeave={e => { if (!isSel && !isPending) e.currentTarget.style.background = 'transparent'; }}>
                      {/* checkbox */}
                      <div style={{ padding: '0 8px', textAlign: 'center' }}>
                        <input type="checkbox" checked={isSel} onChange={() => toggleRow(r.id)}
                               style={{ accentColor: 'var(--accent-info)' }} />
                      </div>
                      {/* PMID */}
                      <div style={{ padding: '0 12px', fontSize: 11, overflow: 'hidden' }}>
                        {r.pmid ? (
                          <a href={`https://pubmed.ncbi.nlm.nih.gov/${r.pmid}`}
                             target="_blank" rel="noopener noreferrer"
                             className="mono"
                             onClick={e => e.stopPropagation()}
                             style={{ color: 'var(--accent-info)', textDecoration: 'none' }}>
                            {r.pmid}
                          </a>
                        ) : (
                          <span className="t-meta mono">{r.source.slice(0, 8)}</span>
                        )}
                      </div>
                      {/* Year */}
                      <div className="mono" style={{ padding: '0 12px', fontSize: 12, color: 'var(--text-secondary)' }}>{r.year}</div>
                      {/* Variant — never truncated; container scrolls if too narrow */}
                      <div className="mono" style={{ padding: '0 12px', fontSize: 12, overflowX: 'auto', whiteSpace: 'nowrap' }}>
                        <span style={{ fontWeight: 600 }}>{r.gene}</span>
                        <span style={{ color: 'var(--text-secondary)' }}> {r.hgvs}</span>
                      </div>
                      {/* Mut. type */}
                      <div style={{ padding: '0 12px', fontSize: 12, color: 'var(--text-secondary)' }}>{r.mutType}</div>
                      {/* Inheritance */}
                      <div style={{ padding: '0 12px', fontSize: 12, color: 'var(--text-secondary)' }}>{r.inheritance}</div>
                      {/* Zygosity */}
                      <div style={{ padding: '0 12px', fontSize: 12, color: 'var(--text-secondary)' }}>{r.zygosity}</div>
                      {/* Phenotype */}
                      <div style={{ padding: '0 12px', fontSize: 12, color: 'var(--text-primary)' }}>{r.phenotype}</div>
                      {/* Assay */}
                      <div style={{ padding: '0 12px', fontSize: 12, color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.assay}</div>
                      {/* Match score */}
                      <div style={{ padding: '0 12px', textAlign: 'right' }}>
                        <Chip tone={matchTone(r.match)} dot={false} style={{ fontSize: 10 }}>{r.match}</Chip>
                      </div>
                      {/* Tags (popover) */}
                      <div style={{ padding: '0 6px' }}>
                        <button
                          onClick={(ev) => {
                            ev.stopPropagation();
                            const rect = ev.currentTarget.getBoundingClientRect();
                            onOpenTagPopover(r.id, rect);
                          }}
                          style={{
                            appearance: 'none', cursor: 'pointer',
                            background: 'transparent',
                            border: '1px dashed var(--border)',
                            borderRadius: 4, padding: '2px 4px',
                            display: 'flex', flexWrap: 'wrap', gap: 3,
                            minHeight: 22, width: '100%',
                            color: 'var(--text-primary)', textAlign: 'left',
                          }}>
                          {tags.length === 0 ? (
                            <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>+ Tag</span>
                          ) : (
                            tags.map(t => <TagBadge key={t} tag={t} />)
                          )}
                        </button>
                      </div>
                      {/* Note action */}
                      <div style={{ padding: 0, textAlign: 'center' }}>
                        <button
                          onClick={(e) => { e.stopPropagation(); onOpenNote(r.id); }}
                          className="btn btn-ghost btn-sm"
                          title="Add note"
                          style={{ padding: '0 6px', height: 22, position: 'relative' }}>
                          <Icon d={I.paper} size={11} />
                          {r.notes > 0 && (
                            <span style={{
                              position: 'absolute', top: -2, right: 0,
                              width: 6, height: 6, borderRadius: 3,
                              background: 'var(--accent-info)',
                            }} />
                          )}
                        </button>
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Footer stats */}
      <div style={{
        padding: '8px 16px', borderTop: '1px solid var(--border)',
        background: 'var(--bg-raised)',
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        fontSize: 11, color: 'var(--text-secondary)', flexShrink: 0,
      }}>
        <div style={{ display: 'flex', gap: 16 }}>
          <span>{rows.length} rows</span>
          {selectedSet.size > 0 && <span>{selectedSet.size} selected</span>}
          <span>Virtualized · rendering rows {start}–{Math.min(end, rows.length)} (overscan {OVERSCAN})</span>
        </div>
        <div>Row height {ROW_HEIGHT}px · table width {TOTAL_W}px</div>
      </div>
    </div>
  );
}

// AI score bar
function AiScoreBar({ score }) {
  const pct = Math.round(score * 100);
  const color = score >= 0.8 ? 'var(--accent-success)'
              : score >= 0.5 ? 'var(--accent-info)'
              : score >= 0.3 ? 'var(--accent-warning)'
              : 'var(--text-tertiary)';
  return (
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, justifyContent: 'flex-end' }}>
      <span className="mono" style={{ color, fontWeight: 600, fontVariantNumeric: 'tabular-nums', minWidth: 28, textAlign: 'right' }}>
        {(score).toFixed(2)}
      </span>
      <div style={{ width: 40, height: 6, background: 'var(--bg-base)', borderRadius: 3, overflow: 'hidden' }}>
        <div style={{ width: `${pct}%`, height: '100%', background: color, borderRadius: 3 }} />
      </div>
    </div>
  );
}

// Evidence drawer — skeleton rows while loading
function EvidenceDrawer({ variant, displayClass, onClassify }) {
  const [loading, setLoading] = useState2(true);
  const [activeTab, setActiveTab] = useState2('evidence');

  // Simulate per-variant load (skeleton, never a spinner)
  useEffect2(() => {
    setLoading(true);
    // Audit: AI evidence view (HIPAA access log)
    audit('ai_evidence_view', `${variant.gene}:${variant.hgvs}`);
    const t = setTimeout(() => setLoading(false), 600);
    return () => clearTimeout(t);
  }, [variant.rank]);

  return (
    <>
      {/* Header */}
      <div style={{ padding: 20, borderBottom: '1px solid var(--border)' }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
          <div style={{ minWidth: 0, flex: 1 }}>
            <div className="t-label" style={{ marginBottom: 6 }}>Variant {variant.rank}</div>
            <div style={{ fontSize: 15, fontWeight: 600, lineHeight: 1.4 }}>
              <span className="mono">{variant.gene}</span>
              <span className="t-meta" style={{ marginLeft: 8, fontWeight: 400 }}>{variant.condition}</span>
            </div>
            <div className="mono" style={{ marginTop: 8, fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.6, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
              {variant.hgvs} · {variant.protein}
              <CopyButton value={`${variant.gene} ${variant.hgvs} ${variant.protein}`} />
            </div>
          </div>
        </div>

        <div style={{ marginTop: 16, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          <Chip tone={displayClass === 'Pathogenic' ? 'danger' : displayClass === 'Likely Pathogenic' ? 'warning' : displayClass === 'VUS' ? 'neutral' : displayClass === 'Likely Benign' ? 'info' : 'success'}>
            {displayClass}
          </Chip>
          {variant.acmgCriteria.map(c => (
            <span key={c} className="mono" style={{
              fontSize: 11, padding: '2px 6px', borderRadius: 3,
              background: 'var(--bg-raised)', color: 'var(--text-secondary)',
              border: '1px solid var(--border)',
            }}>{c}</span>
          ))}
        </div>

        <div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
          <button className="btn btn-primary" style={{ flex: 1 }} onClick={onClassify}>Reclassify</button>
          <button className="btn"
                  onClick={() => audit('note_edit', `${variant.gene}:${variant.hgvs}`)}>
            <Icon d={I.paper} size={13} />Add note
          </button>
        </div>
      </div>

      {/* Tabs */}
      <div style={{ display: 'flex', gap: 0, padding: '0 20px', borderBottom: '1px solid var(--border)' }}>
        {[
          { key: 'evidence', label: 'AI evidence', badge: 6 },
          { key: 'predictions', label: 'In silico' },
          { key: 'genomic', label: 'Genomic' },
        ].map(t => (
          <button key={t.key} onClick={() => setActiveTab(t.key)}
            style={{
              background: 'transparent', border: 0, cursor: 'pointer',
              padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 6,
              fontSize: 12, fontWeight: 500,
              color: activeTab === t.key ? 'var(--text-primary)' : 'var(--text-secondary)',
              borderBottom: `2px solid ${activeTab === t.key ? 'var(--accent-info)' : 'transparent'}`,
              marginBottom: -1,
            }}>
            {t.label}
            {t.badge != null && (
              <span style={{
                fontSize: 10, padding: '0 5px', borderRadius: 7, lineHeight: '14px',
                background: 'var(--bg-raised)', color: 'var(--text-secondary)',
              }}>{t.badge}</span>
            )}
          </button>
        ))}
      </div>

      {/* Body */}
      <div style={{ padding: 20, flex: 1 }}>
        {activeTab === 'evidence' && (
          <EvidenceTable loading={loading} />
        )}
        {activeTab === 'predictions' && (
          <PredictionsPanel variant={variant} />
        )}
        {activeTab === 'genomic' && (
          <GenomicPanel variant={variant} />
        )}
      </div>
    </>
  );
}

function EvidenceTable({ loading }) {
  const cols = [180, 220, 70, 80, 60]; // skeleton widths
  const [editingRow, setEditingRow] = useState2(null); // 'ev-1' etc.
  const [popover, setPopover] = useState2(null); // { kind: 'acmg'|'tags', rowId, anchor }
  const [pendingEdits, setPendingEdits] = useState2({}); // rowId -> { criteriaState, tags, snapshot, at, timer }
  const [showExcluded, setShowExcluded] = useState2(false);

  // Build current per-row state, with optimistic pending overlays
  const rowState = (row) => {
    const pending = pendingEdits[row.id];
    if (pending) {
      return {
        criteria: pending.criteria,
        criteriaState: pending.criteriaState,
        tags: pending.tags,
      };
    }
    return {
      criteria: row.criteria || [],
      criteriaState: Object.fromEntries((row.criteria || []).map(c => [c, 'met'])),
      tags: row.tags || [],
    };
  };

  // Commit/Undo (10s window per spec)
  const commitPending = (rowId, patch) => {
    setPendingEdits(prev => {
      const existing = prev[rowId];
      if (existing && existing.timer) clearTimeout(existing.timer);
      const merged = {
        ...(existing || {
          criteria: (D2.evidence.find(e => e.id === rowId) || {}).criteria || [],
          criteriaState: Object.fromEntries(((D2.evidence.find(e => e.id === rowId) || {}).criteria || []).map(c => [c, 'met'])),
          tags: (D2.evidence.find(e => e.id === rowId) || {}).tags || [],
          at: Date.now(),
        }),
        ...patch,
        at: Date.now(),
      };
      merged.timer = setTimeout(() => {
        setPendingEdits(p => { const n = { ...p }; delete n[rowId]; return n; });
        // In real app: emit audit event + persist
      }, 10000);
      return { ...prev, [rowId]: merged };
    });
  };

  const onChangeCriteria = (rowId, nextState) => {
    const criteriaList = Object.entries(nextState).filter(([, v]) => v === 'met').map(([k]) => k);
    commitPending(rowId, { criteriaState: nextState, criteria: criteriaList });
  };
  const onChangeTags = (rowId, nextTags) => {
    commitPending(rowId, { tags: nextTags });
  };

  const evidence = D2.evidence;
  const excludedCount = evidence.filter(e => (rowState(e).tags || []).includes('excluded')).length;
  const visible = evidence.filter(e => showExcluded || !(rowState(e).tags || []).includes('excluded'));

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
        <div className="t-label">Sources cited by Consensio</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          {!loading && excludedCount > 0 && (
            <button className="btn btn-ghost btn-sm" onClick={() => setShowExcluded(s => !s)}>
              {showExcluded ? 'Hide' : 'Show'} excluded ({excludedCount})
            </button>
          )}
          <span className="t-meta">{loading ? '—' : `${visible.length} sources · 2 new`}</span>
        </div>
      </div>

      <div style={{
        background: 'var(--bg-base)',
        border: '1px solid var(--border)',
        borderRadius: 6,
        overflow: 'hidden',
      }}>
        <table className="cx" style={{ background: 'transparent', tableLayout: 'fixed', width: '100%' }}>
          <colgroup>
            <col style={{ width: 152 }} />
            <col />
            <col style={{ width: 100 }} />
            <col style={{ width: 100 }} />
            <col style={{ width: 48 }} />
          </colgroup>
          <thead>
            <tr>
              <th style={{ background: 'var(--bg-raised)' }}>Source</th>
              <th style={{ background: 'var(--bg-raised)' }}>Assertion</th>
              <th style={{ background: 'var(--bg-raised)' }}>ACMG</th>
              <th style={{ background: 'var(--bg-raised)' }}>Tags</th>
              <th style={{ background: 'var(--bg-raised)', textAlign: 'right' }}>Score</th>
            </tr>
          </thead>
          <tbody>
            {loading ? (
              <>{[0,1,2,3,4,5].map(i => <SkeletonRow key={i} cols={cols} />)}</>
            ) : (
              visible.map((e) => {
                const st = rowState(e);
                const isExcluded = (st.tags || []).includes('excluded');
                const isPending = !!pendingEdits[e.id];
                return (
                  <tr key={e.id}
                      style={{
                        cursor: 'default',
                        opacity: isExcluded ? 0.5 : 1,
                        background: isPending ? 'color-mix(in oklab, var(--accent-warning) 8%, transparent)' : undefined,
                      }}
                      className="hover-row">
                    <td style={{ verticalAlign: 'top' }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' }}>
                        <Icon d={e.kind === 'paper' ? I.paper : e.kind === 'db' ? I.flask : I.star} size={11} />
                        <span style={{ fontSize: 11, fontWeight: 500 }}>{e.source}</span>
                        {e.new && (
                          <span style={{
                            fontSize: 9, padding: '1px 4px', borderRadius: 3,
                            background: 'var(--accent-paper)', color: '#fff', fontWeight: 700, letterSpacing: 0.04,
                          }}>NEW</span>
                        )}
                      </div>
                      <div className="mono t-meta" style={{ marginTop: 2, fontSize: 10 }}>
                        {e.pmid ? `PMID ${e.pmid}` : e.id_label}
                      </div>
                    </td>
                    <td style={{
                      color: 'var(--text-secondary)', fontSize: 12, lineHeight: 1.5,
                      textDecoration: isExcluded ? 'line-through' : 'none',
                    }}>
                      {e.assertion}
                    </td>
                    {/* ACMG criteria cell */}
                    <td style={{ verticalAlign: 'top' }}>
                      <button
                        onClick={(ev) => {
                          ev.stopPropagation();
                          const rect = ev.currentTarget.getBoundingClientRect();
                          setPopover({ kind: 'acmg', rowId: e.id, anchor: rect });
                        }}
                        style={{
                          appearance: 'none', cursor: 'pointer',
                          background: 'transparent', border: '1px dashed var(--border)',
                          borderRadius: 4, padding: '3px 4px',
                          display: 'flex', flexWrap: 'wrap', gap: 3,
                          minHeight: 22, width: '100%',
                          color: 'var(--text-primary)', textAlign: 'left',
                        }}
                        title="Edit ACMG criteria">
                        {st.criteria.length === 0 ? (
                          <span style={{ fontSize: 10, color: 'var(--text-tertiary)', padding: '1px 2px' }}>+ Add</span>
                        ) : (
                          st.criteria.map(code => (
                            <AcmgChip key={code} code={code} state={st.criteriaState[code] || 'met'} onClick={() => {}} />
                          ))
                        )}
                      </button>
                    </td>
                    {/* Tags cell */}
                    <td style={{ verticalAlign: 'top' }}>
                      <button
                        onClick={(ev) => {
                          ev.stopPropagation();
                          const rect = ev.currentTarget.getBoundingClientRect();
                          setPopover({ kind: 'tags', rowId: e.id, anchor: rect });
                        }}
                        style={{
                          appearance: 'none', cursor: 'pointer',
                          background: 'transparent', border: '1px dashed var(--border)',
                          borderRadius: 4, padding: '3px 4px',
                          display: 'flex', flexWrap: 'wrap', gap: 3,
                          minHeight: 22, width: '100%',
                          color: 'var(--text-primary)', textAlign: 'left',
                        }}>
                        {st.tags.length === 0 ? (
                          <span style={{ fontSize: 10, color: 'var(--text-tertiary)', padding: '1px 2px' }}>+ Tag</span>
                        ) : (
                          st.tags.map(t => <TagBadge key={t} tag={t} />)
                        )}
                      </button>
                    </td>
                    <td className="mono" style={{ textAlign: 'right', fontSize: 11, color: 'var(--text-secondary)', verticalAlign: 'top' }}>
                      {e.score.toFixed(2)}
                    </td>
                  </tr>
                );
              })
            )}
          </tbody>
        </table>
      </div>

      {!loading && (
        <div className="t-meta" style={{ marginTop: 12, padding: 10, background: 'var(--bg-base)', border: '1px solid var(--border)', borderRadius: 4, display: 'flex', alignItems: 'center', gap: 8 }}>
          <AiBadge label="AI" size="xs" />
          <span>Evidence retrieved by Consensio Reasoner v3.2. Always verify primary sources before classification.</span>
        </div>
      )}

      {/* Pending edits toast (per-row 10s undo) */}
      {Object.keys(pendingEdits).length > 0 && (
        <div style={{
          position: 'fixed', left: 'calc(50% - 200px)', bottom: 80,
          background: 'var(--bg-raised)', border: '1px solid var(--border-hover)',
          borderRadius: 6, padding: '8px 12px',
          display: 'flex', alignItems: 'center', gap: 10,
          boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
          zIndex: 100,
          maxWidth: 420,
        }}>
          <Icon d={I.clock} size={12} />
          <div style={{ flex: 1, fontSize: 12 }}>
            {Object.keys(pendingEdits).length} evidence row{Object.keys(pendingEdits).length > 1 ? 's' : ''} updated · saving in 10s
          </div>
          <button className="btn btn-sm" onClick={() => {
            Object.values(pendingEdits).forEach(p => p.timer && clearTimeout(p.timer));
            setPendingEdits({});
          }}>Undo all</button>
        </div>
      )}

      {/* Popovers */}
      {popover && popover.kind === 'acmg' && (
        <AcmgCriteriaPopover
          anchor={popover.anchor}
          criteriaState={rowState(D2.evidence.find(r => r.id === popover.rowId)).criteriaState}
          dict={D2.acmgCriteria}
          onChange={(next) => onChangeCriteria(popover.rowId, next)}
          onClose={() => setPopover(null)}
        />
      )}
      {popover && popover.kind === 'tags' && (
        <TagPopover
          anchor={popover.anchor}
          tags={rowState(D2.evidence.find(r => r.id === popover.rowId)).tags}
          onChange={(next) => onChangeTags(popover.rowId, next)}
          onClose={() => setPopover(null)}
        />
      )}
    </div>
  );
}

function PredictionsPanel({ variant }) {
  const items = [
    { tool: 'CADD',         score: variant.cadd, scale: '0–40' },
    { tool: 'REVEL',        score: variant.revel.toFixed(2), scale: '0.00–1.00' },
    { tool: 'SpliceAI',     score: '0.04', scale: '0.00–1.00' },
    { tool: 'AlphaMissense',score: '0.91', scale: '0.00–1.00' },
    { tool: 'PrimateAI',    score: '0.82', scale: '0.00–1.00' },
    { tool: 'PhyloP100',    score: '7.41', scale: '−20 to 10' },
  ];
  return (
    <div>
      <div className="t-label" style={{ marginBottom: 12 }}>In silico predictions</div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
        {items.map(it => (
          <div key={it.tool} className="raised" style={{ padding: 10 }}>
            <div className="t-meta">{it.tool}</div>
            <div className="mono" style={{ fontSize: 15, fontWeight: 600, marginTop: 4 }}>{it.score}</div>
            <div className="t-meta" style={{ fontSize: 10 }}>{it.scale}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

function GenomicPanel({ variant }) {
  return (
    <div>
      <div className="t-label" style={{ marginBottom: 12 }}>Genomic context</div>
      <table className="cx" style={{ background: 'transparent' }}>
        <tbody>
          {[
            ['Chromosome',  `chr${variant.chrom}`,  true],
            ['Reference',   'GRCh38',               false],
            ['Gene',        variant.gene,           true],
            ['Transcript',  'NM_003482.3',          true],
            ['Exon',        '11 of 54',             false],
            ['Zygosity',    variant.zygosity,       false],
            ['Inheritance', variant.inheritance,    false],
            ['OMIM',        variant.omim,           true],
            ['gnomAD AF',   variant.gnomad,         true],
          ].map(([k, v, mono]) => (
            <tr key={k} style={{ cursor: 'default' }}>
              <td style={{ width: 110, color: 'var(--text-secondary)', borderBottom: 'none', padding: '6px 0', fontSize: 12 }}>{k}</td>
              <td className={mono ? 'mono' : ''} style={{ borderBottom: 'none', padding: '6px 0', fontSize: 13 }}>{v}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// Generic modal
function Modal({ children, onClose }) {
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(10,14,20,0.6)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      zIndex: 200, backdropFilter: 'blur(2px)',
    }}>
      <div onClick={e => e.stopPropagation()}
           className="surface"
           style={{
             background: 'var(--bg-raised)',
             width: 480, maxWidth: '90vw',
             borderRadius: 8, overflow: 'hidden',
             boxShadow: '0 24px 64px rgba(0,0,0,0.5)',
             border: '1px solid var(--border-hover)',
           }}>
        {children}
      </div>
    </div>
  );
}

// ── Updates / Reanalysis feed ────────────────────────────────────────────────
// ── Reanalysis / Pipeline Runs screen ────────────────────────────────────────
const GH_REPO = 'cansincomert/genomixLLM';
const GH_WORKFLOW = 'reanalysis.yml';

function fmtElapsed(s) {
  if (s == null) return '—';
  if (s < 3600) return `${Math.round(s / 60)}m`;
  return `${(s / 3600).toFixed(1)}h`;
}

function fmtDate(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}

function AcmgBar({ acmg }) {
  const total = Object.values(acmg).reduce((s, v) => s + v, 0);
  if (!total) return <span className="t-meta">—</span>;
  const segs = [
    { key: 'pathogenic',        label: 'P',   color: 'var(--accent-danger)'   },
    { key: 'likely_pathogenic', label: 'LP',  color: 'var(--accent-warning)'  },
    { key: 'vus',               label: 'VUS', color: 'var(--text-tertiary)'    },
    { key: 'likely_benign',     label: 'LB',  color: 'var(--accent-info)'     },
    { key: 'benign',            label: 'B',   color: 'var(--accent-success)'  },
  ].filter(s => acmg[s.key] > 0);
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
      <div style={{ display: 'flex', height: 6, borderRadius: 3, overflow: 'hidden', width: 80, flexShrink: 0 }}>
        {segs.map(s => (
          <div key={s.key} style={{ width: `${(acmg[s.key] / total) * 100}%`, background: s.color }} />
        ))}
      </div>
      {segs.map(s => (
        <span key={s.key} className="mono" style={{ fontSize: 10, color: s.color }}>
          {s.label} {acmg[s.key]}
        </span>
      ))}
    </div>
  );
}

function TriggerModal({ onClose }) {
  const [sampleId, setSampleId]     = useState2('');
  const [skipS0, setSkipS0]         = useState2(true);
  const [ghToken, setGhToken]       = useState2(() => localStorage.getItem('gh_pat') || '');
  const [status, setStatus]         = useState2(null); // null | 'sending' | 'ok' | 'err'
  const [errMsg, setErrMsg]         = useState2('');

  const SAMPLE_RUN_LOCAL = {
    '56':'E250182783','57':'E250182783','58':'E250182783','59':'E250182783',
    '60':'E250182783','61':'E250182783','62':'E250182783','63':'E250182783',
    '64':'E250182783','65':'E250182783','66':'E250182783','67':'E250182783',
    '68':'E250182783','69':'E250182783','70':'E250182783','71':'E250182783',
    '72':'E250182783','73':'E250182783',
    '289':'E250182783','290':'E250182783','291':'E250182783','292':'E250182783',
    '293':'E250182783','294':'E250182783','295':'E250182783','296':'E250182783',
    '297':'E250182783','298':'E250182783','299':'E250182783','300':'E250182783',
    '301':'E250182783',
    '99':'E250162786','108':'E250162786','109':'E250162786',
    '209':'E250099309','350':'E250159595',
  };

  const trigger = async () => {
    if (!sampleId || !ghToken) return;
    if (ghToken) localStorage.setItem('gh_pat', ghToken);
    setStatus('sending');
    try {
      const res = await fetch(
        `https://api.github.com/repos/${GH_REPO}/actions/workflows/${GH_WORKFLOW}/dispatches`,
        {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${ghToken}`,
            Accept: 'application/vnd.github.v3+json',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            ref: 'main',
            inputs: {
              sample_id: sampleId,
              run_id: SAMPLE_RUN_LOCAL[sampleId] || 'E250182783',
              skip_sprint0: String(skipS0),
              all_impacts: 'true',
            },
          }),
        }
      );
      if (res.status === 204) {
        setStatus('ok');
        audit('reanalysis_triggered', sampleId);
      } else {
        const body = await res.json().catch(() => ({}));
        setErrMsg(body.message || `HTTP ${res.status}`);
        setStatus('err');
      }
    } catch (e) {
      setErrMsg(e.message);
      setStatus('err');
    }
  };

  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 300,
      display: 'flex', alignItems: 'center', justifyContent: 'center' }}
      onClick={onClose}>
      <div style={{ background: 'var(--bg-surface)', border: '1px solid var(--border)',
        borderRadius: 10, width: 420, overflow: 'hidden' }}
        onClick={e => e.stopPropagation()}>
        <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--border)',
          fontWeight: 600, fontSize: 14, display: 'flex', justifyContent: 'space-between' }}>
          Trigger reanalysis
          <button onClick={onClose} style={{ background: 'none', border: 0, cursor: 'pointer',
            color: 'var(--text-tertiary)', fontSize: 16, lineHeight: 1 }}>×</button>
        </div>
        <div style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 14 }}>
          {status === 'ok' ? (
            <div style={{ textAlign: 'center', padding: '24px 0' }}>
              <div style={{ fontSize: 28, marginBottom: 8 }}>✓</div>
              <div style={{ fontWeight: 600, marginBottom: 4 }}>Workflow dispatched</div>
              <div className="t-meta">Sample {sampleId} — check GitHub Actions for progress.</div>
              <button className="btn btn-primary" style={{ marginTop: 16 }} onClick={onClose}>Close</button>
            </div>
          ) : (
            <>
              <div>
                <div className="t-label" style={{ marginBottom: 6 }}>Sample ID</div>
                <input value={sampleId} onChange={e => setSampleId(e.target.value)}
                  placeholder="e.g. 289"
                  style={{ width: '100%', padding: '8px 10px', background: 'var(--bg-base)',
                    border: '1px solid var(--border)', borderRadius: 6,
                    color: 'var(--text-primary)', fontSize: 13, fontFamily: 'var(--font-mono)',
                    boxSizing: 'border-box' }} />
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <input type="checkbox" id="skipS0" checked={skipS0}
                  onChange={e => setSkipS0(e.target.checked)}
                  style={{ accentColor: 'var(--accent-info)' }} />
                <label htmlFor="skipS0" style={{ fontSize: 13, cursor: 'pointer' }}>
                  Skip Sprint 0 (VCF already exists)
                </label>
              </div>
              <div>
                <div className="t-label" style={{ marginBottom: 6 }}>
                  GitHub PAT <span className="t-meta">(workflow scope — stored locally)</span>
                </div>
                <input type="password" value={ghToken} onChange={e => setGhToken(e.target.value)}
                  placeholder="ghp_…"
                  style={{ width: '100%', padding: '8px 10px', background: 'var(--bg-base)',
                    border: '1px solid var(--border)', borderRadius: 6,
                    color: 'var(--text-primary)', fontSize: 13, fontFamily: 'var(--font-mono)',
                    boxSizing: 'border-box' }} />
              </div>
              {status === 'err' && (
                <div style={{ fontSize: 12, color: 'var(--accent-danger)', padding: '8px 10px',
                  background: 'color-mix(in oklab, var(--accent-danger) 12%, transparent)',
                  borderRadius: 6 }}>
                  {errMsg}
                </div>
              )}
              <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
                <button className="btn" onClick={onClose}>Cancel</button>
                <button className="btn btn-primary"
                  disabled={!sampleId || !ghToken || status === 'sending'}
                  onClick={trigger}>
                  {status === 'sending' ? 'Dispatching…' : <><Icon d={I.bolt} size={13} />Run pipeline</>}
                </button>
              </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

function UpdatesScreen() {
  const [runs, setRuns]         = useState2(null);
  const [expanded, setExpanded] = useState2(new Set());
  const [tab, setTab]           = useState2('history');   // 'history' | 'trigger'
  const [triggerOpen, setTriggerOpen] = useState2(false);

  useEffect2(() => {
    fetch('patient_data/json/pipeline_runs.json')
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d) setRuns(d.runs.slice().reverse()); }) // newest first
      .catch(() => {});
  }, []);

  const toggleExpand = id => setExpanded(prev => {
    const next = new Set(prev);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });

  const TAB_STYLE = active => ({
    background: 'transparent', border: 0, cursor: 'pointer',
    padding: '10px 16px', fontSize: 13, fontWeight: 500,
    color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
    borderBottom: `2px solid ${active ? 'var(--accent-info)' : 'transparent'}`,
    marginBottom: -1,
  });

  return (
    <div style={{ padding: 32, maxWidth: 1100, margin: '0 auto' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 20 }}>
        <div>
          <h1 className="t-page" style={{ margin: 0 }}>Reanalysis</h1>
          <div className="t-meta" style={{ marginTop: 4 }}>
            Pipeline run history · LLM consensus results · Trigger new runs
          </div>
        </div>
        <button className="btn btn-primary" onClick={() => setTriggerOpen(true)}>
          <Icon d={I.bolt} size={13} />Trigger reanalysis
        </button>
      </div>

      {/* Tabs */}
      <div style={{ display: 'flex', borderBottom: '1px solid var(--border)', marginBottom: 20 }}>
        <button style={TAB_STYLE(tab === 'history')} onClick={() => setTab('history')}>Run history</button>
        <button style={TAB_STYLE(tab === 'actions')} onClick={() => setTab('actions')}>
          GitHub Actions
          <span style={{ fontSize: 10, marginLeft: 6, padding: '1px 5px', borderRadius: 8,
            background: 'var(--bg-raised)', color: 'var(--text-secondary)' }}>↗</span>
        </button>
      </div>

      {tab === 'actions' ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <div className="surface" style={{ padding: 20 }}>
            <div className="t-card" style={{ marginBottom: 8 }}>Setup: self-hosted runner</div>
            <div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.7, marginBottom: 12 }}>
              The reanalysis workflow dispatches to a <span className="mono" style={{ fontSize: 12 }}>self-hosted</span> runner
              on the genomics workstation. Register it once:
            </div>
            <pre style={{ background: 'var(--bg-base)', borderRadius: 6, padding: '10px 14px',
              fontSize: 12, overflowX: 'auto', color: 'var(--text-primary)', lineHeight: 1.6 }}>
{`# On the genomics workstation:
cd ~/actions-runner
./config.sh --url https://github.com/cansincomert/genomixLLM \\
            --token <runner-token-from-github>
./run.sh`}
            </pre>
            <div className="t-meta" style={{ marginTop: 10 }}>
              Get the runner token at GitHub → genomixLLM → Settings → Actions → Runners → New self-hosted runner
            </div>
          </div>
          <div className="surface" style={{ padding: 20 }}>
            <div className="t-card" style={{ marginBottom: 8 }}>GitHub PAT for UI dispatch</div>
            <div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.7, marginBottom: 8 }}>
              The "Trigger reanalysis" button needs a classic PAT with <span className="mono" style={{ fontSize: 12 }}>repo</span> +{' '}
              <span className="mono" style={{ fontSize: 12 }}>workflow</span> scopes.
              Generate one at <span className="mono" style={{ fontSize: 12 }}>github.com → Settings → Developer settings → Personal access tokens</span>.
              The token is stored only in your browser's localStorage.
            </div>
          </div>
        </div>
      ) : !runs ? (
        <ListSkeleton rows={2} cols={[120, 80, 80, 120, 120, 80]} />
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {runs.map((run, ri) => {
            const isExp = expanded.has(ri);
            const sampleList = Object.entries(run.samples || {});
            const totalP  = sampleList.reduce((s, [, v]) => s + (v.acmg?.pathogenic || 0), 0);
            const totalLP = sampleList.reduce((s, [, v]) => s + (v.acmg?.likely_pathogenic || 0), 0);
            const totalVUS = sampleList.reduce((s, [, v]) => s + (v.acmg?.vus || 0), 0);
            return (
              <div key={ri} className="surface" style={{ padding: 0, overflow: 'hidden' }}>
                {/* Run header */}
                <div style={{ padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 16,
                  cursor: 'pointer', borderBottom: isExp ? '1px solid var(--border)' : 'none' }}
                  onClick={() => toggleExpand(ri)}>
                  <div style={{ width: 8, height: 8, borderRadius: 4, flexShrink: 0,
                    background: run.all_ok ? 'var(--accent-success)' : 'var(--accent-danger)' }} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontWeight: 600, fontSize: 13 }}>
                      Run {ri + 1} · {run.run_id}
                    </div>
                    <div className="mono t-meta" style={{ fontSize: 10 }}>{run.log_file}</div>
                  </div>
                  <div style={{ display: 'flex', gap: 24, flexShrink: 0 }}>
                    <div style={{ textAlign: 'center' }}>
                      <div className="t-label" style={{ fontSize: 10 }}>Date</div>
                      <div className="mono" style={{ fontSize: 12 }}>{fmtDate(run.started_at)}</div>
                    </div>
                    <div style={{ textAlign: 'center' }}>
                      <div className="t-label" style={{ fontSize: 10 }}>Samples</div>
                      <div className="mono" style={{ fontSize: 12 }}>{run.total_samples}</div>
                    </div>
                    <div style={{ textAlign: 'center' }}>
                      <div className="t-label" style={{ fontSize: 10 }}>Duration</div>
                      <div className="mono" style={{ fontSize: 12 }}>{fmtElapsed(run.total_elapsed_s)}</div>
                    </div>
                    <div style={{ textAlign: 'center' }}>
                      <div className="t-label" style={{ fontSize: 10 }}>Pathogenic</div>
                      <div className="mono" style={{ fontSize: 12, color: totalP ? 'var(--accent-danger)' : 'inherit' }}>
                        {totalP} P · {totalLP} LP
                      </div>
                    </div>
                    <div style={{ textAlign: 'center' }}>
                      <div className="t-label" style={{ fontSize: 10 }}>VUS</div>
                      <div className="mono" style={{ fontSize: 12 }}>{totalVUS}</div>
                    </div>
                    <div style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>
                      {isExp ? '▲' : '▼'}
                    </div>
                  </div>
                </div>

                {/* Expanded per-sample table */}
                {isExp && (
                  <table className="cx" style={{ fontSize: 12 }}>
                    <thead>
                      <tr>
                        <th>Sample</th>
                        <th>Sprint 0</th>
                        <th>Sprints 1–3</th>
                        <th style={{ textAlign: 'right' }}>Duration</th>
                        <th style={{ textAlign: 'right' }}>Assessed</th>
                        <th>ACMG consensus</th>
                      </tr>
                    </thead>
                    <tbody>
                      {sampleList.map(([sid, s]) => (
                        <tr key={sid}>
                          <td className="mono" style={{ fontWeight: 600 }}>Sample {sid}</td>
                          <td>
                            <Chip tone={s.sprint0 === 'ok' ? 'success' : 'neutral'}>
                              {s.sprint0 === 'ok' ? 'OK' : s.sprint0}
                            </Chip>
                          </td>
                          <td>
                            <Chip tone={s.sprint13 === 'ok' ? 'success' : 'neutral'}>
                              {s.sprint13 === 'ok' ? 'OK' : s.sprint13}
                            </Chip>
                          </td>
                          <td className="mono" style={{ textAlign: 'right' }}>
                            {fmtElapsed(s.elapsed_s)}
                          </td>
                          <td className="mono" style={{ textAlign: 'right', color: 'var(--text-secondary)' }}>
                            {s.assessed ?? '—'}
                          </td>
                          <td><AcmgBar acmg={s.acmg || {}} /></td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                )}
              </div>
            );
          })}
        </div>
      )}

      {triggerOpen && <TriggerModal onClose={() => setTriggerOpen(false)} />}
    </div>
  );
}

// ── Admin ────────────────────────────────────────────────────────────────────
const ROLE_LABELS = {
  org_admin:       { label: 'Org Admin',        tone: 'danger'  },
  doctor:          { label: 'Doctor',            tone: 'info'    },
  lab_analyst:     { label: 'Lab Analyst',       tone: 'paper'   },
  variant_analyst: { label: 'Variant Analyst',   tone: 'warning' },
  viewer:          { label: 'Viewer',            tone: 'neutral' },
};
const ALL_ROLES = ['org_admin','doctor','lab_analyst','variant_analyst','viewer'];

function MembersTab({ user }) {
  const [members, setMembers]       = useState2(null);
  const [addOpen, setAddOpen]       = useState2(false);
  const [newEmail, setNewEmail]     = useState2('');
  const [newRole, setNewRole]       = useState2('variant_analyst');
  const [newName, setNewName]       = useState2('');
  const [newTitle, setNewTitle]     = useState2('');
  const [addErr, setAddErr]         = useState2('');
  const [adding, setAdding]         = useState2(false);

  const fetchMembers = () => {
    if (!window.__sb || !user?.orgId) return;
    window.__sb.from('organization_members')
      .select('id,email,role,display_name,title,department,is_active,activated_at,invited_at,last_login_at')
      .eq('org_id', user.orgId)
      .order('invited_at', { ascending: false })
      .then(({ data }) => setMembers(data || []));
  };

  useEffect2(() => { fetchMembers(); }, [user?.orgId]);

  const updateRole = async (memberId, role) => {
    await window.__sb.from('organization_members').update({ role }).eq('id', memberId);
    setMembers(prev => prev.map(m => m.id === memberId ? { ...m, role } : m));
  };

  const toggleActive = async (memberId, current) => {
    await window.__sb.from('organization_members').update({ is_active: !current }).eq('id', memberId);
    setMembers(prev => prev.map(m => m.id === memberId ? { ...m, is_active: !current } : m));
  };

  const sendReset = async (email) => {
    const redirectTo = `${window.location.origin}${window.location.pathname}`;
    const { error } = await window.__sb.auth.resetPasswordForEmail(email, { redirectTo });
    if (error) alert('Failed to send reset link: ' + error.message);
    else alert(`Password reset link sent to ${email}`);
  };

  const deleteMember = async (memberId, email) => {
    if (!confirm(`Remove ${email} from the organization? This cannot be undone.`)) return;
    await window.__sb.from('organization_members').delete().eq('id', memberId);
    setMembers(prev => prev.filter(m => m.id !== memberId));
  };

  const addMember = async () => {
    if (!newEmail.trim()) { setAddErr('Email is required.'); return; }
    setAdding(true); setAddErr('');
    const { data: { session } } = await window.__sb.auth.getSession();
    const res = await fetch(
      `${window.__sb.supabaseUrl}/functions/v1/invite-member`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${session.access_token}`,
          'apikey': window.__sb.supabaseKey,
        },
        body: JSON.stringify({
          email:        newEmail.trim().toLowerCase(),
          role:         newRole,
          display_name: newName.trim() || newEmail.split('@')[0],
          title:        newTitle.trim() || null,
        }),
      }
    );
    const result = await res.json();
    setAdding(false);
    if (!res.ok) { setAddErr(result.error || 'Failed to invite member.'); return; }
    setAddOpen(false); setNewEmail(''); setNewName(''); setNewTitle(''); setNewRole('variant_analyst');
    fetchMembers();
  };

  if (!window.__sb || !user?.orgId) {
    return (
      <div style={{ padding: 24, textAlign: 'center' }} className="t-meta">
        Member management requires Supabase + an organization. Configure <span className="mono" style={{ fontSize: 11 }}>supabase-config.js</span> and create an organization.
      </div>
    );
  }

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
        <div className="t-meta">{members ? `${members.length} member${members.length !== 1 ? 's' : ''} in this organization` : 'Loading…'}</div>
        {user?.role === 'org_admin' && (
          <button className="btn btn-primary" onClick={() => setAddOpen(true)}>
            <Icon d={I.user} size={13} />Add member
          </button>
        )}
      </div>

      {addOpen && (
        <div className="surface" style={{ padding: 16, marginBottom: 16, border: '1px solid var(--accent-info)', borderRadius: 8 }}>
          <div style={{ fontWeight: 600, fontSize: 13, marginBottom: 12 }}>New member</div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 10 }}>
            {[
              { label: 'Email *', value: newEmail, set: setNewEmail, type: 'email', ph: 'analyst@clinic.com' },
              { label: 'Display name', value: newName, set: setNewName, type: 'text', ph: 'Dr. Smith' },
              { label: 'Title / credential', value: newTitle, set: setNewTitle, type: 'text', ph: 'MD, PhD…' },
            ].map(f => (
              <div key={f.label}>
                <div className="t-label" style={{ marginBottom: 5, fontSize: 10 }}>{f.label}</div>
                <input type={f.type} value={f.value} onChange={e => f.set(e.target.value)} placeholder={f.ph}
                  style={{ width: '100%', padding: '7px 9px', background: 'var(--bg-base)', border: '1px solid var(--border)', borderRadius: 6, color: 'var(--text-primary)', fontSize: 12, boxSizing: 'border-box' }} />
              </div>
            ))}
            <div>
              <div className="t-label" style={{ marginBottom: 5, fontSize: 10 }}>Role *</div>
              <select value={newRole} onChange={e => setNewRole(e.target.value)}
                style={{ width: '100%', padding: '7px 9px', background: 'var(--bg-base)', border: '1px solid var(--border)', borderRadius: 6, color: 'var(--text-primary)', fontSize: 12 }}>
                {ALL_ROLES.map(r => <option key={r} value={r}>{ROLE_LABELS[r].label}</option>)}
              </select>
            </div>
          </div>
          {addErr && <div style={{ color: 'var(--accent-danger)', fontSize: 12, marginBottom: 8 }}>{addErr}</div>}
          <div className="t-meta" style={{ marginBottom: 10, fontSize: 11 }}>
            An invite email will be sent automatically to this address.
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <button className="btn" onClick={() => setAddOpen(false)}>Cancel</button>
            <button className="btn btn-primary" onClick={addMember} disabled={adding || !newEmail}>
              {adding ? 'Adding…' : 'Add member'}
            </button>
          </div>
        </div>
      )}

      {!members ? (
        <ListSkeleton rows={4} cols={[160, 120, 80, 100, 80]} />
      ) : members.length === 0 ? (
        <div style={{ padding: 32, textAlign: 'center' }} className="t-meta">No members yet.</div>
      ) : (
        <div className="surface" style={{ padding: 0, overflow: 'hidden' }}>
          <table className="cx" style={{ fontSize: 12 }}>
            <thead>
              <tr>
                <th>Name / Email</th>
                <th>Role</th>
                <th>Title</th>
                <th>Status</th>
                <th>Last login</th>
                <th>Invited</th>
                {user?.role === 'org_admin' && <th>Actions</th>}
              </tr>
            </thead>
            <tbody>
              {members.map(m => {
                const statusTone = !m.activated_at ? 'neutral' : m.is_active ? 'success' : 'danger';
                const statusLabel = !m.activated_at ? 'Pending' : m.is_active ? 'Active' : 'Deactivated';
                const fmt = d => d ? new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: '2-digit' }) : '—';
                const fmtTime = d => d ? new Date(d).toLocaleString('en-GB', { day: 'numeric', month: 'short', year: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—';
                return (
                <tr key={m.id} style={{ opacity: m.is_active ? 1 : 0.45 }}>
                  <td>
                    <div style={{ fontWeight: 500 }}>{m.display_name || m.email.split('@')[0]}</div>
                    <div className="mono" style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>{m.email}</div>
                  </td>
                  <td>
                    {user?.role === 'org_admin' ? (
                      <select value={m.role} onChange={e => updateRole(m.id, e.target.value)}
                        style={{ background: 'var(--bg-base)', border: '1px solid var(--border)', borderRadius: 4, color: 'var(--text-primary)', fontSize: 11, padding: '3px 6px' }}>
                        {ALL_ROLES.map(r => <option key={r} value={r}>{ROLE_LABELS[r].label}</option>)}
                      </select>
                    ) : (
                      <Chip tone={ROLE_LABELS[m.role]?.tone || 'neutral'}>{ROLE_LABELS[m.role]?.label || m.role}</Chip>
                    )}
                  </td>
                  <td style={{ color: 'var(--text-secondary)' }}>{m.title || '—'}</td>
                  <td><Chip tone={statusTone}>{statusLabel}</Chip></td>
                  <td className="mono" style={{ color: 'var(--text-tertiary)', fontSize: 11 }}>{fmtTime(m.last_login_at)}</td>
                  <td className="mono" style={{ color: 'var(--text-tertiary)', fontSize: 11 }}>{fmt(m.invited_at)}</td>
                  {user?.role === 'org_admin' && (
                    <td>
                      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                        <button className="btn btn-ghost btn-sm"
                          style={{ fontSize: 11, color: m.is_active ? 'var(--accent-warning)' : 'var(--accent-success)' }}
                          onClick={() => toggleActive(m.id, m.is_active)}>
                          {m.is_active ? 'Deactivate' : 'Reactivate'}
                        </button>
                        <button className="btn btn-ghost btn-sm"
                          style={{ fontSize: 11 }}
                          onClick={() => sendReset(m.email)}>
                          Reset pwd
                        </button>
                        <button className="btn btn-ghost btn-sm"
                          style={{ fontSize: 11, color: 'var(--accent-danger)' }}
                          onClick={() => deleteMember(m.id, m.email)}>
                          Remove
                        </button>
                      </div>
                    </td>
                  )}
                </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

function AdminScreen({ user }) {
  const [tab, setTab]           = useState2('members');
  const [auditLog, setAuditLog] = useState2([]);
  const [ghPat, setGhPat]       = useState2(() => localStorage.getItem('gh_pat') || '');
  const [patSaved, setPatSaved] = useState2(false);

  useEffect2(() => {
    try {
      const stored = JSON.parse(localStorage.getItem('consensio_audit_log') || '[]');
      setAuditLog(stored.slice(-100).reverse());
    } catch (_) {}
  }, []);

  const savePat = () => {
    if (ghPat.trim()) localStorage.setItem('gh_pat', ghPat.trim());
    else localStorage.removeItem('gh_pat');
    setPatSaved(true);
    setTimeout(() => setPatSaved(false), 2000);
  };

  const totalCases = D2.projects.reduce((s, p) => s + p.cases, 0);
  const analysts = ['L. Aldana', 'M. Otieno', 'J. Park', 'S. Vetrova'];
  const sbStatus = window.__sbConfigured ? 'Configured' : 'Not configured (using SHA-256 fallback)';

  const TAB_STYLE = active => ({
    background: 'transparent', border: 0, cursor: 'pointer',
    padding: '10px 16px', fontSize: 13, fontWeight: 500,
    color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
    borderBottom: `2px solid ${active ? 'var(--accent-info)' : 'transparent'}`,
    marginBottom: -1,
  });

  return (
    <div style={{ padding: 32, maxWidth: 1080, margin: '0 auto' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 20 }}>
        <div>
          <h1 className="t-page" style={{ margin: 0 }}>Admin</h1>
          <div className="t-meta" style={{ marginTop: 4 }}>
            {user?.org || 'Nevagen Genetik Merkezi'} · member access, integrations, audit log
          </div>
        </div>
        {user?.role && <Chip tone={ROLE_LABELS[user.role]?.tone || 'neutral'}>{ROLE_LABELS[user.role]?.label || user.role}</Chip>}
      </div>

      {/* Tabs */}
      <div style={{ display: 'flex', borderBottom: '1px solid var(--border)', marginBottom: 24 }}>
        <button style={TAB_STYLE(tab === 'members')}      onClick={() => setTab('members')}>Members</button>
        <button style={TAB_STYLE(tab === 'integrations')} onClick={() => setTab('integrations')}>Integrations</button>
        <button style={TAB_STYLE(tab === 'audit')}        onClick={() => setTab('audit')}>Audit log</button>
      </div>

      {/* Members tab */}
      {tab === 'members' && <MembersTab user={user} />}

      {/* Integrations tab */}
      {tab === 'integrations' && (
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          {/* GitHub PAT */}
          <div className="surface" style={{ padding: 16 }}>
            <div style={{ fontWeight: 600, fontSize: 13, marginBottom: 6 }}>
              GitHub PAT
              <span className="t-meta" style={{ marginLeft: 8, fontWeight: 400 }}>
                workflow:dispatch scope · stored in localStorage
              </span>
            </div>
            <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
              <input
                type="password" value={ghPat}
                onChange={e => setGhPat(e.target.value)}
                placeholder="ghp_…"
                style={{ flex: 1, padding: '7px 10px', background: 'var(--bg-base)',
                  border: '1px solid var(--border)', borderRadius: 6,
                  color: 'var(--text-primary)', fontSize: 12, fontFamily: 'var(--font-mono)' }}
              />
              <button className="btn btn-primary btn-sm" onClick={savePat}>
                {patSaved ? '✓ Saved' : 'Save'}
              </button>
            </div>
            <div className="t-meta" style={{ marginTop: 8, fontSize: 11 }}>
              Used by Reanalysis → Trigger reanalysis modal. Create at GitHub → Settings → Developer settings → Personal access tokens → Classic → <span className="mono">repo, workflow</span> scopes.
            </div>
          </div>

          {/* Supabase */}
          <div className="surface" style={{ padding: 16 }}>
            <div style={{ fontWeight: 600, fontSize: 13, marginBottom: 6 }}>
              Supabase
              <Chip tone={window.__sbConfigured ? 'success' : 'neutral'} style={{ marginLeft: 8 }}>
                {window.__sbConfigured ? 'Connected' : 'Not configured'}
              </Chip>
            </div>
            <div className="t-meta" style={{ fontSize: 12, lineHeight: 1.65 }}>
              {window.__sbConfigured
                ? <>Auth and ACMG/tag overrides persist to Supabase. Shared state across analysts.</>
                : <>Edit <span className="mono" style={{ fontSize: 11 }}>supabase-config.js</span> with your project URL + anon key,
                   then run <span className="mono" style={{ fontSize: 11 }}>supabase-schema.sql</span> in the Supabase SQL editor.
                   Until then, overrides are stored in this browser only.</>
              }
            </div>
          </div>
        </div>
      )}

      {/* Audit log tab */}
      {tab === 'audit' && (
        <div className="surface" style={{ padding: 0, overflow: 'hidden' }}>
          <div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
            <div className="t-card">Audit log</div>
            <div className="t-meta">Last {Math.min(auditLog.length, 100)} events</div>
          </div>
          {auditLog.length === 0 ? (
            <div style={{ padding: 24, textAlign: 'center' }} className="t-meta">
              No audit events recorded yet. Actions like classification changes, exports, and AI views will appear here.
            </div>
          ) : (
            <table className="cx" style={{ fontSize: 12 }}>
              <thead>
                <tr>
                  <th>Timestamp</th>
                  <th>User</th>
                  <th>Action</th>
                  <th>Resource</th>
                </tr>
              </thead>
              <tbody>
                {auditLog.map((e, i) => (
                  <tr key={i}>
                    <td className="mono" style={{ color: 'var(--text-tertiary)', whiteSpace: 'nowrap' }}>
                      {new Date(e.timestamp).toLocaleString()}
                    </td>
                    <td>{e.userId}</td>
                    <td><Chip tone="neutral">{e.action}</Chip></td>
                    <td className="mono" style={{ color: 'var(--text-secondary)' }}>{e.resourceId}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      )}
    </div>
  );
}

// ── AI Chat side rail ────────────────────────────────────────────────────────
function AiChatPanel({ variant, onClose }) {
  const [messages, setMessages] = useState2([
    { role: 'ai', text: `I've reviewed the candidate variants for the Rowe family. Variant 1 (KMT2D c.2504delT) shows the strongest signal — frameshift in an LOF-intolerant gene with absent gnomAD frequency and recent direct evidence from Eldridge 2025. Ask me anything about the evidence, alternative interpretations, or paper summaries.`,
      refs: [{ rank: 1, label: 'KMT2D c.2504delT' }] },
  ]);
  const [input, setInput] = useState2('');
  const [streaming, setStreaming] = useState2(false);
  const [streamingText, setStreamingText] = useState2('');
  const [unavailable, setUnavailable] = useState2(false);
  const scrollRef = React.useRef(null);

  // Auto-scroll on new content
  React.useEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    }
  }, [messages, streamingText]);

  const suggestions = [
    'Summarize Eldridge 2025',
    'Compare with variant 2',
    'Alternative diagnoses?',
    'Functional evidence gaps',
  ];

  const send = async (text) => {
    if (!text.trim() || streaming) return;
    const userMsg = { role: 'user', text };
    setMessages(m => [...m, userMsg]);
    setInput('');
    setStreaming(true);
    setStreamingText('');
    setUnavailable(false);

    // 8s timeout per AI unavailable contract
    let timedOut = false;
    const timeout = setTimeout(() => { timedOut = true; }, 8000);

    try {
      const prompt = `You are Consensio, a clinical genomics assistant helping a geneticist analyze the Rowe family case. The currently-selected variant is ${variant.gene} ${variant.hgvs} ${variant.protein} (ACMG: ${variant.acmg}, OMIM: ${variant.omim}). Answer concisely (under 90 words) and in clinical language. The user asks: ${text}`;
      const result = await window.claude.complete(prompt);
      clearTimeout(timeout);
      if (timedOut) throw new Error('timeout');

      // Simulate streaming display — render character-by-character
      const full = result || '';
      for (let i = 1; i <= full.length; i++) {
        setStreamingText(full.slice(0, i));
        // small variable delay
        await new Promise(r => setTimeout(r, full[i] === ' ' ? 8 : 6));
      }
      setMessages(m => [...m, {
        role: 'ai',
        text: full,
        refs: text.match(/variant 2|MYT1L/i) ? [{ rank: 2, label: 'MYT1L c.1456G>A' }] : null,
      }]);
      setStreamingText('');
    } catch (err) {
      clearTimeout(timeout);
      console.warn('AI assistant unavailable:', err);
      setUnavailable(true);
      setStreamingText('');
    } finally {
      setStreaming(false);
    }
  };

  return (
    <>
      {/* Header */}
      <div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <Icon d={I.bolt} size={14} />
            <div style={{ fontSize: 13, fontWeight: 600 }}>AI assistant</div>
            <span className="mono" style={{
              fontSize: 11, padding: '2px 6px', borderRadius: 3,
              background: 'var(--bg-raised)', color: 'var(--text-secondary)',
            }}>{variant.gene}</span>
          </div>
          <button className="btn btn-ghost btn-sm" onClick={onClose} style={{ padding: '0 4px' }}>
            <Icon d={I.x} size={12} />
          </button>
        </div>
        <div style={{
          marginTop: 8, padding: '6px 8px',
          background: 'color-mix(in oklab, var(--accent-info) 12%, transparent)',
          borderRadius: 4, fontSize: 11, color: 'var(--text-secondary)',
          border: '1px solid color-mix(in oklab, var(--accent-info) 22%, transparent)',
        }}>
          Context: <span className="mono" style={{ color: 'var(--text-primary)' }}>
            {variant.gene} · {variant.hgvs}
          </span>
        </div>
      </div>

      {/* Messages */}
      <div ref={scrollRef} style={{
        flex: 1, overflow: 'auto', padding: '16px',
        display: 'flex', flexDirection: 'column', gap: 12,
      }}>
        {messages.map((m, i) => (
          <ChatMessage key={i} message={m} />
        ))}
        {streaming && streamingText && (
          <ChatMessage message={{ role: 'ai', text: streamingText }} streaming />
        )}
        {streaming && !streamingText && (
          <div style={{ display: 'flex', gap: 4, color: 'var(--text-tertiary)', padding: '4px 0' }}>
            <span className="ai-dot" style={{ animationDelay: '0ms' }} />
            <span className="ai-dot" style={{ animationDelay: '150ms' }} />
            <span className="ai-dot" style={{ animationDelay: '300ms' }} />
          </div>
        )}
      </div>

      {/* Suggestions */}
      {!streaming && messages.length < 3 && (
        <div style={{ padding: '0 16px 12px', display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          {suggestions.map(s => (
            <button key={s} onClick={() => send(s)}
              className="btn btn-sm" style={{ background: 'transparent', borderColor: 'var(--border)' }}>
              {s}
            </button>
          ))}
        </div>
      )}

      {/* Input or unavailable banner */}
      {unavailable ? (
        <div style={{
          padding: 12, margin: 12,
          background: 'color-mix(in oklab, var(--accent-warning) 14%, var(--bg-surface))',
          border: '1px solid color-mix(in oklab, var(--accent-warning) 32%, transparent)',
          borderRadius: 6, fontSize: 12,
        }}>
          <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
            <Icon d={I.alert} size={13} />
            <div style={{ flex: 1 }}>
              <div style={{ fontWeight: 500, color: 'var(--text-primary)', marginBottom: 4 }}>
                AI assistant temporarily unavailable
              </div>
              <div style={{ color: 'var(--text-secondary)', lineHeight: 1.5 }}>
                You can still use the evidence table and take notes manually.
              </div>
              <button className="btn btn-sm" style={{ marginTop: 8 }}
                      onClick={() => { setUnavailable(false); setInput(''); }}>
                Retry
              </button>
            </div>
          </div>
        </div>
      ) : (
        <form onSubmit={e => { e.preventDefault(); send(input); }}
              style={{
                padding: 12, borderTop: '1px solid var(--border)',
                background: 'var(--bg-surface)', flexShrink: 0,
              }}>
          <div style={{
            display: 'flex', alignItems: 'flex-end', gap: 8,
            background: 'var(--bg-raised)', border: '1px solid var(--border)',
            borderRadius: 6, padding: 8,
          }}>
            <textarea
              value={input}
              onChange={e => setInput(e.target.value)}
              onKeyDown={e => {
                if (e.key === 'Enter' && !e.shiftKey) {
                  e.preventDefault(); send(input);
                }
              }}
              placeholder="Ask about evidence, papers, ACMG criteria…"
              disabled={streaming}
              rows={1}
              style={{
                flex: 1, resize: 'none', background: 'transparent', border: 0,
                outline: 'none', fontSize: 13, lineHeight: 1.5, color: 'var(--text-primary)',
                maxHeight: 120,
              }} />
            <button type="submit" disabled={streaming || !input.trim()}
              className="btn btn-primary btn-sm" style={{ flexShrink: 0 }}>
              {streaming ? '…' : 'Send'}
            </button>
          </div>
          <div className="t-meta" style={{ marginTop: 6, fontSize: 10 }}>
            <Icon d={I.alert} size={10} /> Verify all AI claims against primary sources before clinical action.
          </div>
        </form>
      )}

      <style>{`
        .ai-dot {
          display: inline-block;
          width: 6px; height: 6px; border-radius: 3px;
          background: var(--text-tertiary);
          animation: ai-pulse 1.2s ease-in-out infinite;
        }
        @keyframes ai-pulse {
          0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
          40% { opacity: 1; transform: scale(1); }
        }
        @keyframes ai-cursor {
          0%, 100% { opacity: 1; }
          50% { opacity: 0.2; }
        }
        .ai-cursor::after {
          content: '▌';
          display: inline;
          margin-left: 2px;
          color: var(--accent-info);
          animation: ai-cursor 0.8s steps(1) infinite;
        }
      `}</style>
    </>
  );
}

function ChatMessage({ message, streaming }) {
  const isUser = message.role === 'user';
  return (
    <div style={{
      display: 'flex',
      justifyContent: isUser ? 'flex-end' : 'flex-start',
    }}>
      <div style={{
        maxWidth: '88%',
        padding: '10px 12px',
        borderRadius: isUser ? '10px 10px 2px 10px' : '10px 10px 10px 2px',
        background: isUser ? 'var(--bg-raised)' : 'color-mix(in oklab, var(--accent-info) 8%, var(--bg-base))',
        border: isUser ? '1px solid var(--border)' : '1px solid color-mix(in oklab, var(--accent-info) 20%, transparent)',
        fontSize: 13, lineHeight: 1.55,
        color: 'var(--text-primary)',
        wordBreak: 'break-word',
      }}>
        <span className={streaming ? 'ai-cursor' : ''}>{message.text}</span>
        {message.refs && message.refs.length > 0 && (
          <div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 4 }}>
            {message.refs.map(r => (
              <button key={r.rank} className="btn btn-sm" style={{
                background: 'color-mix(in oklab, var(--accent-info) 14%, transparent)',
                borderColor: 'color-mix(in oklab, var(--accent-info) 32%, transparent)',
                color: 'var(--accent-info)',
                fontSize: 11,
              }}
              onClick={() => {
                // Flash highlight the row (interpreted by parent via custom event)
                window.dispatchEvent(new CustomEvent('consensio:flash-variant', { detail: r.rank }));
              }}>
                <Icon d={I.flask} size={10} />Variant {r.rank} · {r.label}
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, {
  FamilyScreen, VariantsScreen, UpdatesScreen, AdminScreen,
  AiChatPanel,
});

// ── Filter picker popover (used by GenomicsScreen table headers) ─────────────
function FilterPicker({ col, label, rect, options, active, onToggle, onClear, onClose }) {
  const ref = useRef2(null);
  const [pick, setPick] = useState2('');

  useEffect2(() => {
    const onDown = e => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const onKey  = e => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown',   onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('keydown',   onKey);
    };
  }, []);

  const left = Math.min(rect.left, window.innerWidth - 248);
  const top  = rect.bottom + 4;
  const q = pick.trim().toLowerCase();
  const visible = q ? options.filter(o => String(o.value).toLowerCase().includes(q)) : options;

  return (
    <div ref={ref} style={{
      position: 'fixed', left, top, zIndex: 200,
      background: 'var(--bg-raised)',
      border: '1px solid var(--border-hover)',
      borderRadius: 8, padding: 6,
      minWidth: 220, maxHeight: 340,
      display: 'flex', flexDirection: 'column',
      boxShadow: '0 12px 32px rgba(0,0,0,0.5)',
    }}>
      {/* Header */}
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        padding: '4px 8px 8px', borderBottom: '1px solid var(--border)', marginBottom: 4, flexShrink: 0,
      }}>
        <span className="t-label" style={{ fontSize: 10 }}>Filter · {label}</span>
        {active?.size > 0 && (
          <button className="btn btn-ghost btn-sm"
            style={{ fontSize: 10, padding: '1px 6px', color: 'var(--accent-info)' }}
            onClick={onClear}>
            Clear ({active.size})
          </button>
        )}
      </div>
      {/* Type-to-search */}
      <div style={{ padding: '0 4px 6px', flexShrink: 0 }}>
        <input
          autoFocus
          type="search"
          placeholder={`Search ${label}…`}
          value={pick}
          onChange={e => setPick(e.target.value)}
          style={{
            width: '100%', boxSizing: 'border-box',
            appearance: 'none', background: 'var(--bg-overlay)',
            border: '1px solid var(--border)', borderRadius: 5,
            padding: '4px 8px', fontSize: 11,
            color: 'var(--text-primary)', outline: 'none', fontFamily: 'inherit',
          }}
          onFocus={e => e.target.style.borderColor = 'var(--accent-info)'}
          onBlur={e => e.target.style.borderColor = 'var(--border)'}
        />
      </div>
      {/* Options */}
      <div style={{ overflowY: 'auto', flex: 1 }}>
        {visible.length === 0 && (
          <div className="t-meta" style={{ padding: '8px 12px', fontSize: 11 }}>No matches</div>
        )}
        {visible.map(({ value, count }) => {
          const checked = active?.has(value) || false;
          return (
            <label key={value} style={{
              display: 'flex', alignItems: 'center', gap: 8,
              padding: '5px 8px', cursor: 'pointer', borderRadius: 4, userSelect: 'none',
            }}
            onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-overlay)'}
            onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
              <input type="checkbox" checked={checked} onChange={() => onToggle(value)}
                style={{ accentColor: 'var(--accent-info)', cursor: 'pointer', flexShrink: 0 }} />
              <span style={{ flex: 1, fontSize: 12, color: checked ? 'var(--text-primary)' : 'var(--text-secondary)' }}>
                {value || '(none)'}
              </span>
              <span className="mono" style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>{count}</span>
            </label>
          );
        })}
      </div>
    </div>
  );
}

// ── Variant detail drawer (slide-in from right) ───────────────────────────────
function VariantDetailDrawer({ variant: v, onClose }) {
  useEffect2(() => {
    const onKey = e => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);

  const impactTone = imp =>
    ({ HIGH: 'danger', MODERATE: 'warning', LOW: 'info', MODIFIER: 'neutral' }[imp] || 'neutral');

  // Build external resource URLs from variant fields
  const [refAllele, altAllele] = (v.allele_string || '/').split('/');
  const gene = v.gene_symbol || '';
  const gnomadUrl  = `https://gnomad.broadinstitute.org/variant/${v.chr}-${v.start}-${refAllele}-${altAllele}?dataset=gnomad_r4`;
  const clinvarUrl = `https://www.ncbi.nlm.nih.gov/clinvar/?term=${encodeURIComponent(gene)}[gene]`;
  const omimUrl    = `https://www.omim.org/search?search=${encodeURIComponent(gene)}`;

  return (
    <div style={{
      position: 'fixed', right: 0, top: 48,
      height: 'calc(100vh - 48px)', width: 480,
      background: 'var(--bg-surface)',
      borderLeft: '1px solid var(--border-hover)',
      boxShadow: '-8px 0 32px rgba(0,0,0,0.4)',
      zIndex: 150,
      display: 'flex', flexDirection: 'column',
      animation: 'vddSlideIn 200ms ease-out',
    }}>
      <style>{`@keyframes vddSlideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }`}</style>

      {/* Drawer header */}
      <div style={{
        padding: '12px 16px', borderBottom: '1px solid var(--border)',
        display: 'flex', alignItems: 'flex-start', gap: 10, flexShrink: 0,
        background: 'var(--bg-raised)',
      }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4, flexWrap: 'wrap' }}>
            <span className="mono" style={{ fontSize: 15, fontWeight: 700 }}>{gene || '—'}</span>
            {v.impact && (
              <Chip tone={impactTone(v.impact)} dot={false} style={{ fontSize: 10, padding: '1px 6px' }}>
                {v.impact}
              </Chip>
            )}
          </div>
          <div className="mono t-meta" style={{ fontSize: 11 }}>
            {v.chr}:{v.start?.toLocaleString()} · {v.allele_string} · {v.variant_class}
          </div>
        </div>
        <button className="btn btn-ghost btn-sm" onClick={onClose} style={{ flexShrink: 0, marginTop: 2 }}>
          <Icon d={I.x} size={13} />
        </button>
      </div>

      {/* Scrollable body */}
      <div style={{ flex: 1, overflowY: 'auto', padding: 16, display: 'flex', flexDirection: 'column', gap: 16 }}>

        {/* 1. Coordinates */}
        <section>
          <div className="t-label" style={{ marginBottom: 8 }}>Coordinates</div>
          <div className="raised" style={{ padding: 12, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
            {[
              { label: 'Chromosome', value: v.chr },
              { label: 'Position',   value: v.start?.toLocaleString() },
              { label: 'End',        value: v.end?.toLocaleString() },
              { label: 'Allele',     value: v.allele_string },
              { label: 'Class',      value: v.variant_class },
              { label: 'Assembly',   value: 'GRCh38' },
            ].map(({ label, value }) => (
              <div key={label}>
                <div className="t-label" style={{ fontSize: 9, marginBottom: 2 }}>{label}</div>
                <div className="mono" style={{ fontSize: 12 }}>{value || '—'}</div>
              </div>
            ))}
          </div>
        </section>

        {/* 2. Transcript details */}
        <section>
          <div className="t-label" style={{ marginBottom: 8 }}>Transcript Details</div>
          <div className="raised" style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
            {[
              { label: 'MANE / Transcript',       value: v.transcript_id,                                             mono: true  },
              { label: 'HGVSc',                   value: buildHgvsc(v.allele_string, v.cds_start),                  mono: true  },
              { label: 'HGVSp / Protein Change',  value: buildHgvsp(v.amino_acids, v.protein_start) || v.amino_acids, mono: true  },
              { label: 'Consequence',              value: (v.consequence_terms || []).join(', '),                    mono: false },
              { label: 'Most Severe',              value: v.most_severe_consequence,                                 mono: false },
              { label: 'Exon',                     value: formatExon(v.exon),                                        mono: true  },
              { label: 'Codons',                   value: v.codons,                                                  mono: true  },
              { label: 'Biotype',                  value: v.biotype,                                                 mono: false },
            ].filter(r => r.value).map(({ label, value, mono }) => (
              <div key={label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12 }}>
                <span className="t-meta" style={{ fontSize: 11, flexShrink: 0 }}>{label}</span>
                <span className={mono ? 'mono' : ''} style={{ fontSize: 12, textAlign: 'right', wordBreak: 'break-all' }}>
                  {value}
                </span>
              </div>
            ))}
          </div>
        </section>

        {/* 3. ClinVar classifications */}
        {v.clinvar_sig?.length > 0 && (
          <section>
            <div className="t-label" style={{ marginBottom: 8 }}>ClinVar Classifications</div>
            <div className="raised" style={{ padding: 12, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {v.clinvar_sig.map((sig, i) => {
                const tone = (sig.includes('pathogenic') && !sig.includes('benign'))
                  ? (sig.includes('likely') ? 'warning' : 'danger')
                  : sig === 'uncertain_significance' ? 'neutral' : 'success';
                return (
                  <Chip key={i} tone={tone} dot={true} style={{ fontSize: 11 }}>
                    {sig.replace(/_/g, ' ')}
                  </Chip>
                );
              })}
            </div>
          </section>
        )}

        {/* 4. External links */}
        {gene && (
          <section>
            <div className="t-label" style={{ marginBottom: 8 }}>External Resources</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {[
                { label: 'ClinVar', url: clinvarUrl, detail: `${gene} gene variants` },
                { label: 'gnomAD',  url: gnomadUrl,  detail: `${v.chr}:${v.start} ${v.allele_string}` },
                { label: 'OMIM',    url: omimUrl,    detail: `${gene} gene search` },
              ].map(({ label, url, detail }) => (
                <a key={label} href={url} target="_blank" rel="noopener noreferrer"
                   style={{
                     display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                     padding: '8px 12px', borderRadius: 6,
                     background: 'var(--bg-raised)', border: '1px solid var(--border)',
                     textDecoration: 'none',
                     transition: 'border-color 100ms, background 100ms',
                   }}
                   onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--border-hover)'; e.currentTarget.style.background = 'var(--bg-overlay)'; }}
                   onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'var(--bg-raised)'; }}>
                  <div>
                    <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)' }}>{label}</div>
                    <div className="t-meta" style={{ fontSize: 11, marginTop: 1 }}>{detail}</div>
                  </div>
                  <Icon d={I.external} size={12} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }} />
                </a>
              ))}
            </div>
          </section>
        )}

      </div>
    </div>
  );
}

// ── Clinical nomenclature helpers ─────────────────────────────────────────────
const AA1TO3 = {
  A:'Ala',R:'Arg',N:'Asn',D:'Asp',C:'Cys',Q:'Gln',E:'Glu',G:'Gly',
  H:'His',I:'Ile',L:'Leu',K:'Lys',M:'Met',F:'Phe',P:'Pro',S:'Ser',
  T:'Thr',W:'Trp',Y:'Tyr',V:'Val','*':'Ter',X:'Ter',
};

function buildHgvsp(amino_acids, protein_start) {
  if (!amino_acids || protein_start == null) return null;
  const [ref1, alt1] = amino_acids.split('/');
  if (!ref1 || !alt1) return null;
  const ref3 = AA1TO3[ref1.toUpperCase()] || ref1;
  const alt3 = AA1TO3[alt1.toUpperCase()] || alt1;
  return `p.${ref3}${protein_start}${alt3}`;
}

function buildHgvsc(allele_string, cds_start) {
  if (cds_start == null || !allele_string) return null;
  const [ref, alt] = allele_string.split('/');
  if (!ref || !alt) return null;
  if (ref.length === 1 && alt.length === 1 && ref !== '-' && alt !== '-')
    return `c.${cds_start}${ref}>${alt}`;
  if (alt === '-' || alt.length < ref.length) return `c.${cds_start}del`;
  if (ref === '-' || ref.length < alt.length) return `c.${cds_start}ins`;
  return `c.${cds_start}${ref}>${alt}`;
}

function formatExon(exon) {
  if (!exon) return null;
  const n = String(exon).split('/')[0];
  return n ? `Exon ${n}` : null;
}

// ── Sample → sequencing run mapping (derived from data/raw/ folder names) ─────
const SAMPLE_RUN = {
  '56':'E250182783','57':'E250182783','58':'E250182783','59':'E250182783',
  '60':'E250182783','61':'E250182783','62':'E250182783','63':'E250182783',
  '64':'E250182783','65':'E250182783','66':'E250182783','67':'E250182783',
  '68':'E250182783','69':'E250182783','70':'E250182783','71':'E250182783',
  '72':'E250182783','73':'E250182783',
  '289':'E250182783','290':'E250182783','291':'E250182783','292':'E250182783',
  '293':'E250182783','294':'E250182783','295':'E250182783','296':'E250182783',
  '297':'E250182783','298':'E250182783','299':'E250182783','300':'E250182783',
  '301':'E250182783',
  '99':'E250162786','108':'E250162786','109':'E250162786',
  '209':'E250099309',
  '350':'E250159595',
};
const sampleLabel = id => `${SAMPLE_RUN[id] || '?'} · ${id}`;

// ── Genomics Lab screen ──────────────────────────────────────────────────────
function GenomicsScreen({ initialSample } = {}) {
  const SAMPLES = [
    '56','57','58','59','60','61','62','63','64','65','66','67','68','69','70','71','72','73',
    '289','290','291','292','293','294','295','296','297','298','299','300','301',
    '99','108','109','209','350',
  ];
  const IMPACT_ORDER = { HIGH: 0, MODERATE: 1, LOW: 2, MODIFIER: 3 };
  const PAGE_SIZE = 50;

  // Column definitions — filterable:true marks columns eligible for value filtering
  const ALL_COLS = [
    { col: 'chr',                     label: 'Chr',         w: 56                   },
    { col: 'start',                   label: 'Position',    w: 96                   },
    { col: 'allele_string',           label: 'Allele',      w: 72                   },
    { col: 'variant_class',           label: 'Class',       w: 68,  filterable: true },
    { col: 'gene_symbol',             label: 'Gene',        w: 90,  filterable: true },
    { col: 'most_severe_consequence', label: 'Consequence', w: 160, filterable: true },
    { col: 'impact',                  label: 'Impact',      w: 90,  filterable: true },
    { col: 'transcript_id',           label: 'MANE / Tx',   w: 140                  },
    { col: 'hgvsc',                   label: 'HGVSc',       w: 120                  },
    { col: 'amino_acids',             label: 'HGVSp',       w: 120                  },
    { col: 'codons',                  label: 'Codons',      w: 88                   },
    { col: 'exon',                    label: 'Exon',        w: 68                   },
    { col: 'clinvar_sig',             label: 'ClinVar',     w: 130, filterable: true },
  ];

  // ── State ──────────────────────────────────────────────────────────────────
  const [selectedRun,    setSelectedRun]    = useState2(() => initialSample ? (SAMPLE_RUN[initialSample] || null) : null);
  const [selectedSample, setSelectedSample] = useState2(() => initialSample || null);
  const [sampleData, setSampleData]         = useState2(null);
  const [loading, setLoading]               = useState2(false);
  const [error, setError]                   = useState2(null);
  const [reportTab, setReportTab]           = useState2('fastp');
  const [varPage, setVarPage]               = useState2(0);
  const [sortCol, setSortCol]               = useState2('impact');  // null = unsorted
  const [sortDir, setSortDir]               = useState2('asc');     // 'asc' | 'desc' | null
  const [hiddenCols, setHiddenCols]         = useState2(new Set());
  const [colPickerOpen, setColPickerOpen]   = useState2(false);
  const colPickerRef                        = useRef2(null);
  // Drill-down drawer
  const [selectedVariant, setSelectedVariant] = useState2(null);
  // Column filters: { col: Set<string> } — empty Set or absent key = no filter
  const [activeFilters, setActiveFilters]     = useState2({});
  // Global text search
  const [searchText, setSearchText]           = useState2('');
  // Which column's filter popover is open + its trigger rect
  const [filterPickerCol, setFilterPickerCol]   = useState2(null);
  const [filterPickerRect, setFilterPickerRect] = useState2(null);

  // ── Data load ──────────────────────────────────────────────────────────────
  useEffect2(() => {
    if (!selectedSample) { setSampleData(null); setLoading(false); setError(null); return; }
    let cancelled = false;
    setLoading(true);
    setError(null);
    setSampleData(null);
    setVarPage(0);
    setActiveFilters({});
    setSelectedVariant(null);

    audit('genomics_sample_view', `sample_${selectedSample}`);

    fetch(`patient_data/json/display_data/sample_${selectedSample}_display.json`)
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then(data => {
        if (!cancelled) { setSampleData(data); setLoading(false); }
      })
      .catch(err => {
        if (!cancelled) { setError(err.message); setLoading(false); }
      });

    return () => { cancelled = true; };
  }, [selectedSample]);

  // ── Close column picker on outside click ──────────────────────────────────
  useEffect2(() => {
    if (!colPickerOpen) return;
    const onDown = e => {
      if (colPickerRef.current && !colPickerRef.current.contains(e.target))
        setColPickerOpen(false);
    };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [colPickerOpen]);

  // ── Derived: distinct filter options per filterable column ─────────────────
  const filterOptions = useMemo2(() => {
    if (!sampleData?.variants) return {};
    const filterableCols = ALL_COLS.filter(c => c.filterable).map(c => c.col);
    const result = {};
    for (const col of filterableCols) {
      const counts = {};
      for (const v of sampleData.variants) {
        const vals = col === 'clinvar_sig'
          ? (v.clinvar_sig || [])
          : [String(v[col] || '')].filter(Boolean);
        for (const val of vals) {
          counts[val] = (counts[val] || 0) + 1;
        }
      }
      result[col] = Object.entries(counts)
        .map(([value, count]) => ({ value, count }))
        .sort((a, b) => b.count - a.count);
    }
    return result;
  }, [sampleData]);

  // ── Derived: sorted variants ───────────────────────────────────────────────
  const sortedVariants = useMemo2(() => {
    if (!sampleData?.variants) return [];
    if (!sortDir) return [...sampleData.variants];
    const copy = [...sampleData.variants];
    copy.sort((a, b) => {
      if (sortCol === 'impact') {
        const diff = (IMPACT_ORDER[a.impact] ?? 4) - (IMPACT_ORDER[b.impact] ?? 4);
        return sortDir === 'asc' ? diff : -diff;
      }
      const av = String(a[sortCol] ?? '');
      const bv = String(b[sortCol] ?? '');
      const diff = av.localeCompare(bv);
      return sortDir === 'asc' ? diff : -diff;
    });
    return copy;
  }, [sampleData, sortCol, sortDir]);

  // ── Derived: filtered variants (active filters + text search applied) ───────
  const filteredVariants = useMemo2(() => {
    let result = sortedVariants;

    // Column checkbox filters
    const hasColFilters = Object.values(activeFilters).some(s => s.size > 0);
    if (hasColFilters) {
      result = result.filter(v => {
        for (const [col, selected] of Object.entries(activeFilters)) {
          if (!selected.size) continue;
          if (col === 'clinvar_sig') {
            if (!(v.clinvar_sig || []).some(s => selected.has(s))) return false;
          } else {
            if (!selected.has(String(v[col] || ''))) return false;
          }
        }
        return true;
      });
    }

    // Global text search
    const q = searchText.trim().toLowerCase();
    if (q) {
      result = result.filter(v =>
        (v.gene_symbol || '').toLowerCase().includes(q) ||
        (v.most_severe_consequence || '').toLowerCase().includes(q) ||
        (v.impact || '').toLowerCase().includes(q) ||
        (v.amino_acids || '').toLowerCase().includes(q) ||
        (v.transcript_id || '').toLowerCase().includes(q) ||
        (v.chr || '').toLowerCase().includes(q) ||
        String(v.start || '').includes(q) ||
        (v.allele_string || '').toLowerCase().includes(q) ||
        (v.variant_class || '').toLowerCase().includes(q) ||
        (v.clinvar_sig || []).some(s => s.toLowerCase().includes(q)) ||
        (buildHgvsp(v.amino_acids, v.protein_start) || '').toLowerCase().includes(q) ||
        (buildHgvsc(v.allele_string, v.cds_start) || '').toLowerCase().includes(q)
      );
    }

    return result;
  }, [sortedVariants, activeFilters, searchText]);

  const pageCount = Math.ceil(filteredVariants.length / PAGE_SIZE);
  const pageRows  = filteredVariants.slice(varPage * PAGE_SIZE, (varPage + 1) * PAGE_SIZE);
  const hasActiveFilters = Object.values(activeFilters).some(s => s.size > 0) || searchText.trim().length > 0;

  // ── Helpers ────────────────────────────────────────────────────────────────
  const impactTone = imp =>
    ({ HIGH: 'danger', MODERATE: 'warning', LOW: 'info', MODIFIER: 'neutral' }[imp] || 'neutral');

  // Three-state sort: asc → desc → null(default) → asc …
  const onSortCol = col => {
    if (sortCol !== col) {
      setSortCol(col); setSortDir('asc');
    } else if (sortDir === 'asc') {
      setSortDir('desc');
    } else if (sortDir === 'desc') {
      setSortCol(null); setSortDir(null);
    } else {
      setSortCol(col); setSortDir('asc');
    }
    setVarPage(0);
  };

  const toggleCol = col => {
    setHiddenCols(prev => {
      const next = new Set(prev);
      if (next.has(col)) next.delete(col); else next.add(col);
      return next;
    });
  };

  // Open a filter picker anchored to the clicked <th>
  const openFilterPicker = (col, e) => {
    e.stopPropagation(); // prevent column sort from firing
    const rect = e.currentTarget.closest('th').getBoundingClientRect();
    setFilterPickerCol(col);
    setFilterPickerRect(rect);
    setColPickerOpen(false); // close column visibility picker if open
  };

  // Toggle a single value in a column's active filter Set
  const toggleFilterValue = (col, value) => {
    setActiveFilters(prev => {
      const curr = new Set(prev[col] || []);
      if (curr.has(value)) curr.delete(value); else curr.add(value);
      const next = { ...prev };
      if (curr.size) next[col] = curr; else delete next[col];
      return next;
    });
    setVarPage(0);
  };

  const clearColFilter = col => {
    setActiveFilters(prev => { const n = { ...prev }; delete n[col]; return n; });
    setVarPage(0);
  };

  // Sort indicator shown on every column header
  const SortIndicator = ({ col: c }) => {
    if (sortCol === c && sortDir) {
      return (
        <Icon d={I.chevronD} size={10} style={{
          display: 'inline-block', verticalAlign: 'middle',
          transform: sortDir === 'desc' ? 'rotate(180deg)' : 'none',
          marginLeft: 4, color: 'var(--accent-info)', transition: 'transform 150ms',
        }} />
      );
    }
    return (
      <svg width="8" height="12" viewBox="0 0 8 12" fill="currentColor" aria-hidden="true"
           style={{ display: 'inline-block', verticalAlign: 'middle', marginLeft: 4, opacity: 0.25 }}>
        <path d="M4 0L7 4H1L4 0Z" />
        <path d="M4 12L1 8H7L4 12Z" />
      </svg>
    );
  };

  const visibleCols   = ALL_COLS.filter(c => !hiddenCols.has(c.col));
  const tableMinWidth = visibleCols.reduce((s, c) => s + c.w, 0);

  return (
    <React.Fragment>
    <div style={{ padding: 32, maxWidth: 1520, margin: '0 auto' }}>

      {/* Page header */}
      <div style={{ marginBottom: 24 }}>
        <h1 className="t-page" style={{ margin: 0 }}>Genomics Lab</h1>
        <div className="t-meta" style={{ marginTop: 4 }}>
          WES variant explorer · {SAMPLES.length} samples across {Object.keys(SAMPLES.reduce((a,s)=>({...a,[SAMPLE_RUN[s]||'?']:1}),{})).length} sequencing runs · VEP GRCh38
        </div>
      </div>

      {/* Cascading selectors — run then sample */}
      {(() => {
        const runGroups = SAMPLES.reduce((acc, s) => {
          const run = SAMPLE_RUN[s] || 'Other';
          (acc[run] = acc[run] || []).push(s);
          return acc;
        }, {});
        const runs = Object.keys(runGroups).sort();
        const selStyle = {
          appearance: 'none', cursor: 'pointer',
          background: 'var(--bg-raised)',
          border: '1px solid var(--border)',
          borderRadius: 8,
          color: 'var(--text-primary)',
          padding: '10px 40px 10px 14px',
          fontSize: 13,
          fontFamily: 'var(--font-mono)',
          minWidth: 300,
          backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")`,
          backgroundRepeat: 'no-repeat',
          backgroundPosition: 'right 12px center',
        };
        return (
          <div style={{ display: 'flex', gap: 16, alignItems: 'flex-end', marginBottom: 24, flexWrap: 'wrap' }}>
            <div>
              <div className="t-label" style={{ marginBottom: 7, fontSize: 11 }}>Sequencing Run</div>
              <select
                value={selectedRun || ''}
                onChange={e => {
                  const r = e.target.value || null;
                  setSelectedRun(r);
                  setSelectedSample(null);
                  setSampleData(null);
                  setError(null);
                }}
                style={selStyle}
              >
                <option value="">— select run —</option>
                {runs.map(run => (
                  <option key={run} value={run}>{run} · {runGroups[run].length} samples</option>
                ))}
              </select>
            </div>
            {selectedRun && (
              <div>
                <div className="t-label" style={{ marginBottom: 7, fontSize: 11 }}>Sample</div>
                <select
                  value={selectedSample || ''}
                  onChange={e => setSelectedSample(e.target.value || null)}
                  style={selStyle}
                >
                  <option value="">— select sample —</option>
                  {(runGroups[selectedRun] || []).map(s => (
                    <option key={s} value={s}>Sample {s}</option>
                  ))}
                </select>
              </div>
            )}
          </div>
        );
      })()}

      {/* Error state */}
      {selectedSample && error && (
        <ErrorState
          resource="genomics data"
          detail={`Could not load sample_${selectedSample}_display.json: ${error}. Ensure the Python parser has been run and the server is started with python3 -m http.server.`}
          onRetry={() => setSelectedSample(s => s)}
        />
      )}

      {/* QC Summary card */}
      {selectedSample && !error && (loading || sampleData) && (
        <section className="surface" style={{ padding: 16, marginBottom: 16 }}>
          <div className="t-card" style={{ marginBottom: 12 }}>
            QC Summary · {sampleLabel(selectedSample)}
          </div>
          {loading ? (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              {[7, 8].map((cols, ri) => (
                <div key={ri} style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: 10 }}>
                  {Array.from({ length: cols }).map((_, i) => (
                    <div key={i} className="raised" style={{ padding: 12 }}>
                      <div className="skeleton" style={{ height: 9, width: '65%', marginBottom: 8, borderRadius: 3 }} />
                      <div className="skeleton" style={{ height: 14, width: '45%', borderRadius: 3 }} />
                    </div>
                  ))}
                </div>
              ))}
            </div>
          ) : (() => {
            const q = sampleData.qc;
            const techScore = q.technical_confidence;
            const clinScore = q.clinical_confidence;
            const confColor = (s) => s == null ? 'var(--fg-muted)' : s >= 90 ? 'var(--accent-success)' : s >= 70 ? 'var(--accent-warning)' : 'var(--accent-danger)';
            const row1 = [
              { label: 'Total reads',      value: q.total_reads?.toLocaleString() ?? '—' },
              { label: 'Read length',      value: q.read1_mean_length != null ? `${q.read1_mean_length} bp` : '—' },
              { label: 'Q20 rate',         value: q.q20_rate != null ? `${q.q20_rate.toFixed(1)}%` : '—' },
              { label: 'Q30 rate',         value: q.q30_rate != null ? `${q.q30_rate.toFixed(1)}%` : '—' },
              { label: 'GC content',       value: q.gc_content != null ? `${q.gc_content.toFixed(1)}%` : '—' },
              { label: 'Duplication',      value: q.duplication_rate != null ? `${q.duplication_rate.toFixed(2)}%` : '—' },
              { label: 'Insert size peak', value: q.insert_size_peak != null ? `${q.insert_size_peak} bp` : '—' },
            ];
            const row2 = [
              { label: 'Mean depth',       value: q.mean_depth != null ? `${q.mean_depth.toFixed(0)}×` : '—' },
              { label: 'On-target reads',  value: q.on_target_rate != null ? `${q.on_target_rate.toFixed(1)}%` : '—' },
              { label: 'Breadth ≥1×',      value: q.breadth_pct != null ? `${q.breadth_pct.toFixed(1)}%` : '—' },
              { label: '≥20× coverage',    value: q.coverage_20x_pct != null ? `${q.coverage_20x_pct.toFixed(1)}%` : '—' },
              { label: '≥30× coverage',    value: q.coverage_30x_pct != null ? `${q.coverage_30x_pct.toFixed(1)}%` : '—' },
              { label: '≥100× coverage',   value: q.coverage_100x_pct != null ? `${q.coverage_100x_pct.toFixed(1)}%` : '—' },
              { label: 'Tech confidence',  value: techScore != null ? `${techScore.toFixed(1)}/100` : '—', color: confColor(techScore) },
              { label: 'Clin confidence',  value: clinScore != null ? `${clinScore.toFixed(1)}/100` : '—', color: confColor(clinScore) },
            ];
            return (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                {[row1, row2].map((row, ri) => (
                  <div key={ri} style={{ display: 'grid', gridTemplateColumns: `repeat(${row.length}, 1fr)`, gap: 10 }}>
                    {row.map((stat, i) => (
                      <div key={i} className="raised" style={{ padding: 12 }}>
                        <div className="t-label" style={{ marginBottom: 5, fontSize: 10 }}>{stat.label}</div>
                        <div className="mono" style={{ fontWeight: 600, fontSize: 14, color: stat.color }}>{stat.value}</div>
                      </div>
                    ))}
                  </div>
                ))}
              </div>
            );
          })()}
          {!loading && sampleData && (
            <div className="t-meta" style={{ marginTop: 10, fontSize: 11 }}>
              {sampleData.variants_included?.toLocaleString()} variants shown
              {sampleData.variants_included < sampleData.total_variants_in_file && ` (top by clinical priority)`}
              {' '}· {sampleData.total_variants_in_file?.toLocaleString()} total in VCF
              &nbsp;·&nbsp;
              {hasActiveFilters
                ? <span style={{ color: 'var(--accent-info)' }}>
                    {filteredVariants.length.toLocaleString()} shown (filtered from {sampleData.variants_included?.toLocaleString()})
                  </span>
                : sampleData.variants_included >= sampleData.total_high_moderate
                  ? <span style={{ color: 'var(--accent-success)' }}>all {sampleData.variants_included?.toLocaleString()} shown</span>
                  : <span>top {sampleData.variants_included?.toLocaleString()} shown by clinical priority</span>
              }
              &nbsp;·&nbsp; {sampleData.filter_applied} filter applied
            </div>
          )}
        </section>
      )}

      {/* ── Variant metrics table ──────────────────────────────────────────── */}
      {selectedSample && !error && (loading || sampleData) && (
        <section className="surface" style={{ padding: 0, marginBottom: 16 }}>

          {/* Table toolbar */}
          <header style={{
            padding: '10px 16px', borderBottom: '1px solid var(--border)',
            display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8,
          }}>
            <div className="t-card" style={{ flex: 1, minWidth: 0 }}>
              Variant Metrics
              {sampleData ? ` · ${filteredVariants.length.toLocaleString()} variant${filteredVariants.length !== 1 ? 's' : ''}` : ''}
              {hiddenCols.size > 0 && (
                <span className="t-meta" style={{ fontSize: 10, marginLeft: 8 }}>
                  ({hiddenCols.size} col{hiddenCols.size > 1 ? 's' : ''} hidden)
                </span>
              )}
            </div>

            {/* Global text search */}
            <div style={{ position: 'relative', flexShrink: 0 }}>
              <Icon d={I.search} size={12} style={{
                position: 'absolute', left: 8, top: '50%', transform: 'translateY(-50%)',
                color: 'var(--text-tertiary)', pointerEvents: 'none',
              }} />
              <input
                type="search"
                placeholder="Search gene, HGVSc, consequence…"
                value={searchText}
                onChange={e => { setSearchText(e.target.value); setVarPage(0); }}
                style={{
                  appearance: 'none', background: 'var(--bg-overlay)',
                  border: '1px solid var(--border)', borderRadius: 6,
                  padding: '5px 8px 5px 26px', fontSize: 12,
                  color: 'var(--text-primary)', width: 240,
                  outline: 'none', fontFamily: 'inherit',
                }}
                onFocus={e => e.target.style.borderColor = 'var(--accent-info)'}
                onBlur={e => e.target.style.borderColor = 'var(--border)'}
              />
            </div>

            <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>

              {/* Clear active column filters + search */}
              {hasActiveFilters && (
                <button className="btn btn-ghost btn-sm"
                  onClick={() => { setActiveFilters({}); setSearchText(''); setVarPage(0); }}
                  style={{ color: 'var(--accent-info)', gap: 4 }}>
                  <Icon d={I.x} size={11} /> Clear filters
                </button>
              )}

              {/* Column visibility picker */}
              <div style={{ position: 'relative' }} ref={colPickerRef}>
                <button
                  className="btn btn-ghost btn-sm"
                  onClick={() => setColPickerOpen(o => !o)}
                  style={{ gap: 5 }}
                  title="Manage columns"
                >
                  <Icon d={I.cog} size={12} />
                  Columns
                  {hiddenCols.size > 0 && (
                    <span style={{
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                      width: 16, height: 16, borderRadius: 8,
                      background: 'var(--accent-info)', color: '#fff',
                      fontSize: 9, fontWeight: 700, marginLeft: 2,
                    }}>
                      {ALL_COLS.length - hiddenCols.size}
                    </span>
                  )}
                </button>

                {colPickerOpen && (
                  <div style={{
                    position: 'absolute', right: 0, top: 'calc(100% + 4px)',
                    background: 'var(--bg-raised)', border: '1px solid var(--border-hover)',
                    borderRadius: 8, padding: 6, zIndex: 120, minWidth: 176,
                    boxShadow: '0 8px 32px rgba(0,0,0,0.45)',
                  }}>
                    <div style={{
                      display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                      padding: '4px 8px 8px', borderBottom: '1px solid var(--border)', marginBottom: 4,
                    }}>
                      <span className="t-label" style={{ fontSize: 10 }}>Toggle columns</span>
                      <button className="btn btn-ghost btn-sm" style={{ fontSize: 10, padding: '1px 6px' }}
                        onClick={() => setHiddenCols(new Set())}>
                        Reset
                      </button>
                    </div>
                    {ALL_COLS.map(({ col, label }) => {
                      const visible = !hiddenCols.has(col);
                      return (
                        <label key={col} style={{
                          display: 'flex', alignItems: 'center', gap: 8,
                          padding: '6px 8px', cursor: 'pointer', borderRadius: 5,
                        }}
                        onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-overlay)'}
                        onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                          <input type="checkbox" checked={visible} onChange={() => toggleCol(col)}
                            style={{ accentColor: 'var(--accent-info)', cursor: 'pointer' }} />
                          <span style={{ fontSize: 12, color: visible ? 'var(--text-primary)' : 'var(--text-tertiary)' }}>
                            {label}
                          </span>
                        </label>
                      );
                    })}
                  </div>
                )}
              </div>

              {/* Pagination controls */}
              {pageCount > 1 && (
                <React.Fragment>
                  <div style={{ width: 1, height: 16, background: 'var(--border)', margin: '0 2px' }} />
                  <span className="t-meta" style={{ fontSize: 11 }}>
                    Page {varPage + 1} / {pageCount}
                  </span>
                  <button className="btn btn-ghost btn-sm" disabled={varPage === 0}
                    onClick={() => setVarPage(p => p - 1)}>
                    <Icon d={I.chevronL} size={11} />
                  </button>
                  <button className="btn btn-ghost btn-sm" disabled={varPage >= pageCount - 1}
                    onClick={() => setVarPage(p => p + 1)}>
                    <Icon d={I.chevronR} size={11} />
                  </button>
                </React.Fragment>
              )}
            </div>
          </header>

          {/* Horizontally scrollable table */}
          <div style={{ overflowX: 'auto', overflowY: 'visible' }}>
            <table className="cx" style={{ minWidth: Math.max(tableMinWidth, 600) }}>
              <thead>
                <tr>
                  {visibleCols.map(({ col, label, w, filterable }) => {
                    const isFiltered = activeFilters[col]?.size > 0;
                    return (
                      <th key={col}
                          style={{ width: w, cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}
                          onClick={() => onSortCol(col)}>
                        {label}
                        <SortIndicator col={col} />
                        {filterable && (
                          <button
                            onClick={e => openFilterPicker(col, e)}
                            title={`Filter by ${label}`}
                            style={{
                              appearance: 'none', border: 0, cursor: 'pointer',
                              background: 'transparent', padding: '0 2px', marginLeft: 2,
                              verticalAlign: 'middle',
                              color: isFiltered ? 'var(--accent-info)' : 'currentColor',
                              opacity: isFiltered ? 1 : 0.35,
                              display: 'inline-flex', alignItems: 'center', gap: 2,
                            }}>
                            <Icon d={I.filter} size={9} />
                            {isFiltered && (
                              <span style={{
                                display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                                width: 13, height: 13, borderRadius: '50%',
                                background: 'var(--accent-info)', color: '#fff',
                                fontSize: 8, fontWeight: 700,
                              }}>
                                {activeFilters[col].size}
                              </span>
                            )}
                          </button>
                        )}
                      </th>
                    );
                  })}
                </tr>
              </thead>
              <tbody>
                {loading
                  ? Array.from({ length: 10 }).map((_, i) => (
                      <SkeletonRow key={i} cols={visibleCols.map(c => c.w)} />
                    ))
                  : pageRows.map((v, i) => {
                    const isSelected = selectedVariant === v;
                    const clinvarChips = (v.clinvar_sig || []).slice(0, 2).map((sig, j) => {
                      const tone = (sig.includes('pathogenic') && !sig.includes('benign'))
                        ? (sig.includes('likely') ? 'warning' : 'danger')
                        : sig === 'uncertain_significance' ? 'neutral' : 'success';
                      return (
                        <Chip key={j} tone={tone} dot={false}
                              style={{ fontSize: 9, padding: '1px 5px', marginRight: 2 }}>
                          {sig.replace(/_/g, ' ').replace('significance', 'sig.')}
                        </Chip>
                      );
                    });

                    const cells = {
                      chr:                     <td key="chr"    className="mono" style={{ fontSize: 11 }}>{v.chr?.replace('chr', '') || '—'}</td>,
                      start:                   <td key="start"  className="mono" style={{ fontSize: 11 }}>{v.start?.toLocaleString() ?? '—'}</td>,
                      allele_string:           <td key="allele" className="mono" style={{ fontSize: 11 }}>{v.allele_string || '—'}</td>,
                      variant_class:           <td key="class"  style={{ fontSize: 11 }}>{v.variant_class || '—'}</td>,
                      gene_symbol:             <td key="gene"><span className="mono" style={{ fontWeight: 600, fontSize: 12 }}>{v.gene_symbol || '—'}</span></td>,
                      most_severe_consequence: <td key="cons"   style={{ fontSize: 11, color: 'var(--text-secondary)', maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={(v.consequence_terms || []).join(', ')}>{(v.consequence_terms || []).join(', ') || '—'}</td>,
                      impact:                  <td key="impact">{v.impact ? <Chip tone={impactTone(v.impact)} dot={false} style={{ fontSize: 10, padding: '1px 6px' }}>{v.impact}</Chip> : <span className="t-meta">—</span>}</td>,
                      transcript_id:           <td key="tx"     className="mono" style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>{v.transcript_id || '—'}</td>,
                      hgvsc:                   <td key="hgvsc"  className="mono" style={{ fontSize: 11 }}>{buildHgvsc(v.allele_string, v.cds_start) || '—'}</td>,
                      amino_acids:             <td key="aa"     className="mono" style={{ fontSize: 11 }}>{buildHgvsp(v.amino_acids, v.protein_start) || '—'}</td>,
                      codons:                  <td key="codons" className="mono" style={{ fontSize: 11 }}>{v.codons || '—'}</td>,
                      exon:                    <td key="exon"   className="mono" style={{ fontSize: 11 }}>{formatExon(v.exon) || '—'}</td>,
                      clinvar_sig:             <td key="cv">{clinvarChips.length ? <div style={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>{clinvarChips}</div> : <span className="t-meta">—</span>}</td>,
                    };

                    return (
                      <tr key={`${v.chr}-${v.start}-${i}`}
                          className={isSelected ? 'selected' : ''}
                          onClick={() => setSelectedVariant(isSelected ? null : v)}>
                        {visibleCols.map(({ col }) => cells[col])}
                      </tr>
                    );
                  })
                }
              </tbody>
            </table>
          </div>

          {/* Bottom pagination */}
          {!loading && pageCount > 1 && (
            <div style={{
              padding: '8px 16px', borderTop: '1px solid var(--border)',
              display: 'flex', justifyContent: 'flex-end', gap: 8, alignItems: 'center',
            }}>
              <span className="t-meta" style={{ fontSize: 11 }}>Page {varPage + 1} / {pageCount}</span>
              <button className="btn btn-ghost btn-sm"
                disabled={varPage === 0} onClick={() => setVarPage(p => p - 1)}>
                <Icon d={I.chevronL} size={11} /> Prev
              </button>
              <button className="btn btn-ghost btn-sm"
                disabled={varPage >= pageCount - 1} onClick={() => setVarPage(p => p + 1)}>
                Next <Icon d={I.chevronR} size={11} />
              </button>
            </div>
          )}
        </section>
      )}

      {/* ── Embedded HTML reports ─────────────────────────────────────────── */}
      {selectedSample && !error && sampleData && (
        <section className="surface" style={{ padding: 0, overflow: 'hidden' }}>
          <div style={{ display: 'flex', borderBottom: '1px solid var(--border)' }}>
            {[
              { key: 'fastp',  label: 'fastp QC Report' },
              { key: 'visual', label: 'Visual Analysis Report' },
            ].map(tab => (
              <button key={tab.key} onClick={() => setReportTab(tab.key)} style={{
                appearance: 'none', border: 0, cursor: 'pointer',
                padding: '10px 18px', fontSize: 13, fontWeight: 500, fontFamily: 'inherit',
                background: 'transparent',
                color: reportTab === tab.key ? 'var(--text-primary)' : 'var(--text-secondary)',
                borderBottom: `2px solid ${reportTab === tab.key ? 'var(--accent-info)' : 'transparent'}`,
                marginBottom: -1, transition: 'color 100ms, border-color 100ms',
              }}>
                {tab.label}
              </button>
            ))}
            <div style={{ flex: 1 }} />
            <div className="t-meta" style={{ alignSelf: 'center', paddingRight: 16, fontSize: 11 }}>
              {sampleLabel(selectedSample)} · GRCh38
            </div>
          </div>
          <div style={{ position: 'relative', height: 680, background: '#ffffff', borderRadius: '0 0 8px 8px' }}>
            <iframe
              src={`patient_data/reports/sample_${selectedSample}_fastp.html`}
              title={`fastp QC report · sample ${selectedSample}`}
              style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 'none', background: '#ffffff', display: reportTab === 'fastp' ? 'block' : 'none' }}
              loading="lazy"
            />
            <iframe
              src={`patient_data/reports/sample_${selectedSample}.visual_report.html`}
              title={`Visual analysis report · sample ${selectedSample}`}
              style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 'none', background: '#ffffff', display: reportTab === 'visual' ? 'block' : 'none' }}
              loading="lazy"
            />
          </div>
        </section>
      )}

    </div>

    {/* ── Filter picker — fixed-position popover anchored to column header ── */}
    {filterPickerCol && filterPickerRect && (
      <FilterPicker
        col={filterPickerCol}
        label={ALL_COLS.find(c => c.col === filterPickerCol)?.label || filterPickerCol}
        rect={filterPickerRect}
        options={filterOptions[filterPickerCol] || []}
        active={activeFilters[filterPickerCol]}
        onToggle={val => toggleFilterValue(filterPickerCol, val)}
        onClear={() => clearColFilter(filterPickerCol)}
        onClose={() => { setFilterPickerCol(null); setFilterPickerRect(null); }}
      />
    )}

    {/* ── Variant detail drawer + backdrop ─────────────────────────────────── */}
    {selectedVariant && (
      <React.Fragment>
        <div
          onClick={() => setSelectedVariant(null)}
          style={{ position: 'fixed', inset: 0, zIndex: 149, background: 'rgba(0,0,0,0.28)' }}
        />
        <VariantDetailDrawer
          variant={selectedVariant}
          onClose={() => setSelectedVariant(null)}
        />
      </React.Fragment>
    )}

    </React.Fragment>
  );
}

// ── Keyboard help modal ──────────────────────────────────────────────────────
function HelpModal({ onClose, onTriggerSessionWarning }) {
  const shortcuts = [
    { group: 'Navigation', items: [
      ['⌘K', 'Open command palette'],
      ['⌘↑/↓', 'Navigate case rows'],
      ['Esc', 'Close panel / modal'],
    ]},
    { group: 'Variant workspace', items: [
      ['⌘E', 'Toggle evidence panel'],
      ['⌘J', 'Toggle AI assistant'],
      ['⌘⇧C', 'Reclassify selected variant'],
      ['⌘⇧E', 'Toggle excluded rows'],
    ]},
    { group: 'Global', items: [
      ['?', 'Show this reference'],
      ['⌘F', 'Focus global search'],
      ['⌘L', 'Lock session'],
      ['⌘.', 'Toggle PHI mask'],
    ]},
  ];
  return (
    <Modal onClose={onClose}>
      <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <div>
          <div className="t-card">Keyboard shortcuts</div>
          <div className="t-meta" style={{ marginTop: 4 }}>WCAG 2.1 AA — every action has a keyboard path</div>
        </div>
        <button className="btn btn-ghost btn-sm" onClick={onClose} style={{ padding: '0 4px' }}>
          <Icon d={I.x} size={12} />
        </button>
      </div>
      <div style={{ padding: 20, display: 'flex', flexDirection: 'column', gap: 18 }}>
        {shortcuts.map(g => (
          <section key={g.group}>
            <div className="t-label" style={{ marginBottom: 8 }}>{g.group}</div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr auto', rowGap: 6, columnGap: 16 }}>
              {g.items.map(([k, d]) => (
                <React.Fragment key={k}>
                  <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{d}</div>
                  <div style={{ display: 'flex', gap: 4 }}>
                    {k.split(/([⌘⇧↑↓]|\+)/g).filter(Boolean).map((part, i) => (
                      <span key={i} className="kbd">{part}</span>
                    ))}
                  </div>
                </React.Fragment>
              ))}
            </div>
          </section>
        ))}
        {onTriggerSessionWarning && (
          <div style={{
            padding: 10, background: 'var(--bg-surface)',
            border: '1px dashed var(--border)', borderRadius: 4,
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          }}>
            <span className="t-meta">Test session timeout (15min idle in prod)</span>
            <button className="btn btn-sm" onClick={onTriggerSessionWarning}>
              <Icon d={I.clock} size={11} />Trigger warning
            </button>
          </div>
        )}
      </div>
    </Modal>
  );
}

// ── Session timeout countdown modal ──────────────────────────────────────────
function SessionTimeoutModal({ onStay, onSignOut }) {
  const [n, setN] = useState2(120);
  useEffect2(() => {
    if (n <= 0) { onSignOut && onSignOut(); return; }
    const t = setTimeout(() => setN(x => x - 1), 1000);
    return () => clearTimeout(t);
  }, [n]);
  const m = Math.floor(n / 60), s = n % 60;
  const label = m > 0 ? `${m}:${String(s).padStart(2, '0')}` : `${s} seconds`;
  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(10,14,20,0.7)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      zIndex: 500, backdropFilter: 'blur(4px)',
    }}>
      <div style={{
        width: 440, maxWidth: '90vw',
        background: 'var(--bg-raised)',
        border: '1px solid var(--border-hover)',
        borderRadius: 10, overflow: 'hidden',
        boxShadow: '0 24px 64px rgba(0,0,0,0.5)',
      }}>
        <div style={{ padding: '20px 24px', borderBottom: '1px solid var(--border)' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
            <div style={{
              width: 28, height: 28, borderRadius: 14,
              background: 'color-mix(in oklab, var(--accent-warning) 18%, transparent)',
              color: 'var(--accent-warning)',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>
              <Icon d={I.clock} size={14} />
            </div>
            <div style={{ fontSize: 15, fontWeight: 600 }}>Session about to expire</div>
          </div>
          <div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.55 }}>
            Your session will expire in{' '}
            <span className="mono" style={{ color: 'var(--accent-warning)', fontWeight: 600 }}>
              {label}
            </span>
            . Any unsaved variant edits are held in the 10-second undo window — once your session ends those are lost.
          </div>
        </div>
        {/* Progress bar */}
        <div style={{ height: 3, background: 'var(--bg-base)', overflow: 'hidden' }}>
          <div style={{
            height: '100%', width: `${(n / 120) * 100}%`,
            background: n < 30 ? 'var(--accent-danger)' : 'var(--accent-warning)',
            transition: 'width 1s linear',
          }} />
        </div>
        <div style={{
          padding: 16, display: 'flex', gap: 8, justifyContent: 'flex-end',
          background: 'var(--bg-surface)',
        }}>
          <button className="btn" onClick={onSignOut}>Sign out</button>
          <button className="btn btn-primary" onClick={onStay} autoFocus>
            Stay signed in
          </button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  FamilyScreen, VariantsScreen, UpdatesScreen, AdminScreen, GenomicsScreen,
  AiChatPanel, HelpModal, CommandPalette, SessionTimeoutModal,
});

// ── Command palette (⌘K) ─────────────────────────────────────────────────────
function CommandPalette({ navigate, onClose }) {
  const [q, setQ] = useState2('');
  const inputRef = React.useRef(null);
  useEffect2(() => { inputRef.current?.focus(); }, []);

  // Local index — pages, cases, families, genes
  const items = useMemo2(() => {
    const cs = D2.cases.map(c => ({
      kind: 'case', key: 'case-' + c.id,
      label: `${c.family} family`,
      detail: c.phenotype,
      meta: c.id,
      to: 'family',
    }));
    const genes = ['KMT2D', 'MYT1L', 'ANKRD11', 'COL2A1', 'WDR45'].map(g => ({
      kind: 'gene', key: 'gene-' + g,
      label: g, detail: 'Open variants for gene', to: 'variants',
    }));
    const pages = [
      { kind: 'page', key: 'p-dashboard', label: 'Dashboard',          to: 'dashboard' },
      { kind: 'page', key: 'p-projects',  label: 'Projects',           to: 'projects' },
      { kind: 'page', key: 'p-cases',     label: 'Cases (RD Program)', to: 'cases' },
      { kind: 'page', key: 'p-updates',   label: 'Reanalysis feed',    to: 'updates' },
      { kind: 'page', key: 'p-admin',     label: 'Admin',              to: 'admin' },
    ];
    return [...pages, ...cs, ...genes];
  }, []);

  const filtered = useMemo2(() => {
    if (!q.trim()) return items;
    const tokens = q.toLowerCase().split(/\s+/);
    return items.filter(it =>
      tokens.every(tok =>
        (it.label + ' ' + (it.detail || '') + ' ' + (it.meta || '')).toLowerCase().includes(tok)
      )
    );
  }, [items, q]);

  const [hover, setHover] = useState2(0);
  useEffect2(() => { setHover(0); }, [q]);

  const onSelect = (it) => {
    if (it.to) navigate(it.to);
  };

  const onKey = (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); setHover(h => Math.min(h + 1, filtered.length - 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setHover(h => Math.max(h - 1, 0)); }
    else if (e.key === 'Enter') { e.preventDefault(); filtered[hover] && onSelect(filtered[hover]); }
  };

  const KIND_META = {
    page: { label: 'Page',   icon: I.folder, tone: 'neutral' },
    case: { label: 'Case',   icon: I.flask,  tone: 'info' },
    gene: { label: 'Gene',   icon: I.star,   tone: 'paper' },
  };

  return (
    <div onClick={onClose}
         style={{
           position: 'fixed', inset: 0, background: 'rgba(10,14,20,0.55)',
           display: 'flex', justifyContent: 'center', alignItems: 'flex-start', paddingTop: 96,
           zIndex: 400, backdropFilter: 'blur(2px)',
         }}>
      <div onClick={(e) => e.stopPropagation()}
           style={{
             width: 580, maxWidth: '90vw',
             background: 'var(--bg-raised)',
             border: '1px solid var(--border-hover)',
             borderRadius: 10, overflow: 'hidden',
             boxShadow: '0 24px 64px rgba(0,0,0,0.5)',
           }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 10,
          padding: '14px 16px', borderBottom: '1px solid var(--border)',
        }}>
          <Icon d={I.search} size={14} />
          <input
            ref={inputRef}
            value={q}
            onChange={(e) => setQ(e.target.value)}
            onKeyDown={onKey}
            placeholder="Search cases, families, genes, pages…"
            style={{
              flex: 1, border: 0, outline: 'none', background: 'transparent',
              fontSize: 15, color: 'var(--text-primary)',
            }} />
          <span className="kbd">Esc</span>
        </div>
        <div style={{ maxHeight: 360, overflow: 'auto', padding: 6 }}>
          {filtered.length === 0 ? (
            <div style={{ padding: '20px 16px', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>
              No matches
            </div>
          ) : filtered.map((it, i) => {
            const km = KIND_META[it.kind];
            return (
              <button key={it.key}
                onMouseEnter={() => setHover(i)}
                onClick={() => onSelect(it)}
                style={{
                  width: '100%', appearance: 'none', textAlign: 'left',
                  display: 'flex', alignItems: 'center', gap: 12,
                  padding: '10px 12px', borderRadius: 5,
                  background: hover === i ? 'var(--bg-overlay)' : 'transparent',
                  border: 0, color: 'var(--text-primary)', cursor: 'pointer',
                  fontFamily: 'inherit',
                }}>
                <span style={{
                  width: 24, height: 24, borderRadius: 4, flexShrink: 0,
                  background: `color-mix(in oklab, ${ACCENT[km.tone]} 18%, transparent)`,
                  color: ACCENT[km.tone],
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>
                  <Icon d={km.icon} size={12} />
                </span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8 }}>
                    <span className={it.kind === 'gene' ? 'mono' : ''}>{it.label}</span>
                    {it.meta && <span className="mono t-meta">{it.meta}</span>}
                  </div>
                  {it.detail && (
                    <div className="t-meta" style={{ marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                      {it.detail}
                    </div>
                  )}
                </div>
                <span className="t-meta" style={{ textTransform: 'uppercase', fontSize: 10, letterSpacing: 0.04 }}>{km.label}</span>
              </button>
            );
          })}
        </div>
        <div style={{
          padding: '8px 14px', borderTop: '1px solid var(--border)',
          background: 'var(--bg-surface)',
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          fontSize: 11, color: 'var(--text-tertiary)',
        }}>
          <div style={{ display: 'flex', gap: 12 }}>
            <span><span className="kbd">↑</span> <span className="kbd">↓</span> navigate</span>
            <span><span className="kbd">↵</span> open</span>
          </div>
          <span>{filtered.length} result{filtered.length === 1 ? '' : 's'}</span>
        </div>
      </div>
    </div>
  );
}
