/* global React, WaxSeal, Pin, ConfBar, Tag, Sev, Divider */
// Watson rail - Evening dark theme matching Figma 1:1743
// Marcus Chen scenario. ALL behavioral logic preserved.

const { useState: uSW, useRef: uRW } = React;

// ── Clue entries with hypothesis-weight vectors ──────────────────────
const EVIDENCE = [
  {
    id: 'P02:47', t: '02:47', day: 0.116,
    label: 'Odd-hours login - corp IP 10.14.22.88',
    src: 'Okta / SIEM', tone: 'mustard',
    supports: { insider: 8, hijack: 8, authorized: 0 },
  },
  {
    id: 'P03:41', t: '03:41', day: 0.154,
    label: 'MFA fatigue accept - 6 push prompts',
    src: 'Okta MFA', tone: 'mustard',
    supports: { insider: 6, hijack: 14, authorized: -8 },
  },
  {
    id: 'P04:08', t: '04:08', day: 0.172,
    label: 'DB query spike - customers_pii - 412k rows',
    src: 'prod-db-01', tone: 'brick',
    supports: { insider: 22, hijack: 4, authorized: 6 },
  },
  {
    id: 'P04:23', t: '04:23', day: 0.183,
    label: 'Sensitive-label access - PII data class',
    src: 'DLP / CASB', tone: 'brick',
    supports: { insider: 14, hijack: 10, authorized: 4 },
  },
  {
    id: 'P04:51', t: '04:51', day: 0.202,
    label: 'Personal Dropbox upload - 2.3 GB egress',
    src: 'Netskope CASB', tone: 'brick',
    supports: { insider: 20, hijack: 14, authorized: -14 },
  },
  {
    id: 'P05:12', t: '05:12', day: 0.213,
    label: 'Unauthorized API call - payment service 200 requests',
    src: 'AWS API Gateway', tone: 'mustard',
    supports: { insider: 8, hijack: 14, authorized: -6 },
  },
  {
    id: 'P05:17', t: '05:17', day: 0.221,
    label: 'AWS IAM key created - personal account',
    src: 'AWS CloudTrail', tone: 'brick',
    supports: { insider: 18, hijack: 14, authorized: -12 },
  },
];

const EVIDENCE_TWIST = [
  {
    id: 'PT14:23', t: '14:23', day: 1.599,
    label: 'New device fingerprint - Windows/Chrome vs macOS/Safari',
    src: 'Endpoint Telemetry', tone: 'mustard',
    supports: { insider: 0, hijack: 30, authorized: 0 },
  },
  {
    id: 'PT16:05', t: '16:05', day: 1.670,
    label: 'Geo anomaly - Bucharest RO parallel session',
    src: 'GeoIP / Auth logs', tone: 'mustard',
    supports: { insider: 0, hijack: 30, authorized: -8 },
  },
  {
    id: 'PT05:45', t: '05:45', day: 0.240,
    label: 'Credential stuffing attack - 500 attempts',
    src: 'Okta', tone: 'mustard',
    supports: { insider: 0, hijack: 26, authorized: -10 },
  },
];

const HYPOTHESES = [
  {
    id: 'insider',
    name: 'Insider exfiltration by Marcus Chen',
    detail: 'Marcus Chen, privileged DBA, is intentionally copying customer PII to personal cloud infrastructure. Odd-hours login, MFA fatigue, targeted PII query, personal Dropbox upload, and AWS key creation form a coherent exfil chain.',
    supporting: [
      'Odd-hours login 02:47 UTC from unknown IP (evasion timing)',
      'MFA fatigue accept after 6 push prompts (social-engineering success)',
      'Prod DB query spike on customers_pii (412k rows, aligns with exfil window)',
      'Personal Dropbox upload (clear exfil channel, data outside org)',
      'AWS IAM key in personal account (infrastructure for sustained movement)',
    ],
    counter: [
      'No prior history of suspicious DB activity in personnel file',
      'Parallel session from Bucharest complicates solo-actor hypothesis',
    ],
    base: 24,
    conf: 24,
    delta: 0,
    nextStep: 'Cross-check AWS key creation timestamp against login sessions and upload completion time.',
  },
  {
    id: 'hijack',
    name: 'Account hijack via stolen session',
    detail: "Marcus Chen's credentials were compromised. An external actor is using his active sessions to query production and stage data, impersonating him. New device fingerprint and Bucharest geo anomaly are the key signals.",
    supporting: [
      'Different device fingerprint (Windows/Chrome vs known macOS/Safari)',
      'Parallel session from Bucharest, Romania (impossible travel)',
      'MFA fatigue attack (adversary overwhelmed Marcus until acceptance)',
    ],
    counter: [
      'Marcus has not reported credential compromise or unusual account activity',
      'Personal Dropbox and AWS key require knowledge of personal cloud credentials',
      'Query patterns are too polished for a noisy external attacker',
    ],
    base: 12,
    conf: 12,
    delta: 0,
    badge: 'New device fingerprint arrived 14 min ago',
    nextStep: 'Interview Marcus Chen to determine if he recalls the 02:47 UTC session or unusual MFA prompts.',
  },
  {
    id: 'authorized',
    name: 'Authorized backup / migration project',
    detail: 'Customer data export is part of a legitimate, undocumented internal initiative. Marcus was asked informally to copy data to a staging environment for migration or analytics.',
    supporting: [
      'PII table access and sensitive-label read are consistent with migration prep (approved DBA use case)',
      'AWS key creation could reflect staging infrastructure provisioning',
    ],
    counter: [
      'Personal Dropbox and personal AWS account contradict any sanctioned initiative',
      'Odd-hours, MFA fatigue, and impossible parallel session incompatible with documented project',
      'No ticket, no change request, no approval chain in ITSM or IAM audit logs',
    ],
    base: 25,
    conf: 25,
    delta: 0,
    nextStep: 'Query change management system for any active migration that might have enlisted Marcus.',
  },
];

// Watson narrative constants
const NARRATIVE_1PIN = `The 02:47 login from a corporate IP bears marking, though the hour is curious and the device fingerprint warrants attention. One cannot yet discern whether this represents Marcus Chen at his desk in an unusual moment, or perhaps the account accessed by another hand entirely. The facts are thin: a timestamp, an IP address, and nothing more. Until further evidence emerges, the trail remains obscured by multiple possibilities. What does the MFA history reveal?`;

const NARRATIVE_3PIN = `Marcus Chen accepted a multi-factor prompt at 03:41, after five prior rejections within three minutes. The fatigue pattern is deliberate. Twenty-two minutes earlier, a login arrived at 02:47 from a corporate IP at an hour no calendar event explains. The 04:08 query against customers_pii on prod-db-01 returned four hundred and twelve thousand rows.\n\nThis alone is not yet a story. But if the next pin is the Dropbox upload, I will commit it to a draft.`;

const NARRATIVE = `The evidence now forms a coherent narrative of insider exfiltration, supported by the progression from the 02:47 login through the MFA fatigue accepts, the targeted burst against customers_pii, the privileged PII label access, the personal Dropbox upload, and finally the AWS IAM key creation. Marcus Chen's account exhibits the full signature of methodical data theft, each step building upon the last in a sequence too deliberate for accident.`;

// ── Pure recompute function - PRESERVED ──────────────────────────────
function recomputeHypotheses(clues, hypotheses) {
  return hypotheses.map(h => {
    const delta = clues.reduce((sum, c) => sum + (c.supports && c.supports[h.id] != null ? c.supports[h.id] : 0), 0);
    const raw = h.base + delta;
    const conf = Math.max(0, Math.min(95, raw));
    const supportingCount = clues.filter(c => c.supports && (c.supports[h.id] || 0) > 0).length;
    return { ...h, conf, raw, delta, supportingCount };
  }).sort((a, b) => b.raw - a.raw);
}

function narrativeForRanked(ranked, clues) {
  const top = ranked[0];
  const n = clues.length;
  const ids = new Set(clues.map(c => c.id));

  if (!top || n === 0) {
    return `The case file is open and empty. Forty-one signals are streaming from Okta, prod-db-01, DLP, Netskope, AWS, and the endpoint telemetry. None has yet been promoted to evidence.\n\nThe ledger to my right shows the last few hours of Marcus Chen's account. I am watching, but I will not commit a sentence to paper until Sherlock pins the first clue.`;
  }

  if (n === 1) {
    const only = clues[0];
    if (only.id === 'P02:47') {
      return `One clue, and a quiet one. A login at 02:47 UTC from a corporate IP. The address is known, the device is known. The hour is not.\n\nNo calendar event, no on-call rotation, no scheduled patch window justifies this session. It could be Marcus working late on a problem he did not record. It could also be someone else, in possession of his credentials. With one clue I cannot tell. I am writing nothing yet.`;
    }
    return `A single thread on the desk. Watson holds it loosely while waiting for a second.`;
  }

  if (top.id === 'insider' && n === 2) {
    return `Two clues now bear on the same hour. The 02:47 login was followed at 03:41 by a multi-factor accept after six prior rejections. That pattern is deliberate; an account holder does not normally weather six prompts in three minutes by accident.\n\nThe shape of the early evidence reads as social engineering, but it could be self-inflicted. Watson is leaning, not committed.`;
  }

  if (top.id === 'insider' && n >= 3 && n <= 4) {
    return `The picture is gathering. The 02:47 login, the MFA fatigue accept, and the 04:08 burst against customers_pii on prod-db-01 are three steps that fit a single actor moving with patience. Four hundred and twelve thousand rows is not a curious peek; it is a deliberate read.\n\nIf the next pin is the personal Dropbox upload or an AWS key creation, this draft becomes the working hypothesis.`;
  }

  if (top.id === 'insider' && n >= 5) {
    return `The evidence now forms a coherent narrative of insider exfiltration, supported by the progression from the 02:47 login through the MFA fatigue accepts, the targeted burst against customers_pii, the privileged PII label access, the personal Dropbox upload, and finally the AWS IAM key creation. Marcus Chen's account exhibits the full signature of methodical data theft, each step building upon the last in a sequence too deliberate for accident.\n\nA single dissonance remains: the new device fingerprint that surfaced at 14:23 has not been explained. Until it is, the twin hypothesis of account hijack lingers.`;
  }

  if (top.id === 'hijack' && n < 4) {
    return `Watson is watching for hijack signals: device fingerprint, geo anomaly, credential stuffing. The weight of current clues tilts toward a compromised account, but the case is thin.\n\nPin the new device fingerprint, the Bucharest session, or the credential stuffing pattern to sharpen this thread.`;
  }

  if (top.id === 'hijack' && n >= 4) {
    const hasFingerprint = ids.has('PT14:23');
    const hasBucharest = ids.has('PT16:05');
    const hasStuffing = ids.has('PT05:45');
    const surfaced = [
      hasFingerprint && 'a Windows/Chrome fingerprint where macOS/Safari has always lived',
      hasBucharest && 'a parallel session in Bucharest two flying hours from his desk',
      hasStuffing && 'five hundred credential stuffing attempts on his account',
    ].filter(Boolean).join(', ');
    return `A second hand on the same operator. ${surfaced || 'The hijack signals'} point away from Marcus and toward an attacker using his credentials. The 02:47 session may have been the moment of capture, the MFA fatigue the moment of opening.\n\nInsider exfiltration remains plausible because the data flowed to a personal account, but Marcus may not have been at the keys. Sherlock should determine whether Marcus recalls the early session.`;
  }

  if (top.id === 'authorized') {
    return `Could this be a legitimate migration? Marcus has root on prod-db-01. The pattern is consistent with bulk export for analytics or staging. Watson would expect a change ticket and an approval chain. The change management system shows neither.\n\nThis is the quiet hypothesis, the one that needs a single absence to fall. Until a change request surfaces, it sits at the bottom of the list.`;
  }

  return NARRATIVE;
}

// ── Clue number circle (dark red wax dot per Figma) ───────────────────
function ClueNumber({ n }) {
  return (
    <div style={{
      width: 16, height: 16, borderRadius: '50%',
      background: '#952711',
      border: '1px solid rgba(239,231,210,0.12)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      fontFamily: 'var(--sans)', fontSize: 9, fontWeight: 600,
      color: '#f2c0b6',
      flexShrink: 0,
    }}>{n}</div>
  );
}

// ── Clues Collected (was "Pocket") ────────────────────────────────────
function Pocket({ pins = [], compact, animateLast, onUnpin, onFocus }) {
  const [hoverIdx, setHoverIdx] = uSW(null);

  return (
    <div>
      {/* "CLUES COLLECTED" - Geist SemiBold 12px tracking 1px #9f8aa6 */}
      <div style={{
        fontFamily: 'var(--sans)', fontSize: 12, fontWeight: 600,
        letterSpacing: '1px', textTransform: 'uppercase',
        color: 'var(--plum)',
        marginBottom: 10,
      }}>Clues Collected</div>

      {pins.length === 0 ? (
        <div style={{
          fontFamily: 'var(--sans)',
          fontSize: 12.5, lineHeight: 1.6, color: 'var(--ink-mute)',
          padding: '12px 0',
        }}>
          No clues collected. Click a row in the alerts ledger to pin it here.
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column' }}>
          {pins.map((p, i) => (
            <div key={p.id}>
              <div
                onMouseEnter={() => setHoverIdx(i)}
                onMouseLeave={() => setHoverIdx(null)}
                onClick={() => onFocus && onFocus(p)}
                style={{
                  display: 'flex', alignItems: 'flex-start', gap: 8,
                  padding: '8px 0',
                  animation: animateLast && i === pins.length - 1 ? 'pin-in .45s cubic-bezier(.2,.7,.3,1)' : undefined,
                  cursor: onFocus ? 'pointer' : 'default',
                }}
              >
                <div style={{ paddingTop: 2 }}>
                  <ClueNumber n={i + 1} />
                </div>

                {/* Label + source */}
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontFamily: 'var(--sans)', fontSize: 12.5, lineHeight: 1.35, color: 'var(--ink)' }}>
                    {p.label}
                  </div>
                  {!compact && (
                    <div style={{ fontFamily: 'var(--sans)', fontSize: 10, color: 'var(--ink-mute)', marginTop: 2 }}>
                      {p.src}
                    </div>
                  )}
                </div>

                {/* Timestamp + trash on hover */}
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
                  <span style={{ fontFamily: 'var(--sans)', fontSize: 10.5, color: 'var(--ink-mute)' }}>{p.t}</span>
                  {onUnpin && hoverIdx === i && (
                    <button
                      onClick={(e) => { e.stopPropagation(); onUnpin(p.id); }}
                      title="Remove clue"
                      style={{
                        border: 'none', background: 'var(--brick-wash)',
                        color: 'var(--brick)', width: 16, height: 16, padding: 0,
                        cursor: 'pointer', borderRadius: 3,
                        fontFamily: 'var(--mono)', fontSize: 11, lineHeight: 1,
                        display: 'flex', alignItems: 'center', justifyContent: 'center',
                        flexShrink: 0,
                      }}
                    >x</button>
                  )}
                </div>
              </div>
              {i < pins.length - 1 && (
                <div style={{ height: 1, background: 'var(--rule)' }} />
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Hypothesis card - Figma dark style ───────────────────────────────
function HypothesisCard({ h, rank, top, expanded: expandedProp, onToggle, showBadge }) {
  const [localExpanded, setLocalExpanded] = uSW(false);
  const isExpanded = expandedProp !== undefined ? expandedProp : localExpanded;

  function handleClick() {
    if (onToggle) { onToggle(h.id); } else { setLocalExpanded(v => !v); }
  }

  const delta = typeof h.delta === 'number' ? h.delta : 0;
  const arrow = delta > 0 ? '▲' : delta < 0 ? '▼' : '';
  // Figma: positive delta = #c9826f (brick), negative = #8aa9a6 (teal-ish)
  const dColor = delta > 0 ? 'var(--brick)' : delta < 0 ? 'var(--teal)' : 'var(--ink-mute)';

  const confPct = typeof h.conf === 'number' && h.conf <= 1 ? Math.round(h.conf * 100) : Math.round(h.conf);
  const supportingCount = h.supportingCount != null ? h.supportingCount : (h.evidence || 0);
  const showBadgeActual = showBadge !== undefined ? showBadge : (h.badge ? true : false);

  // Figma: top = bg var(--plum-wash) = #251d34, border var(--plum-soft) = #463854
  //        others = transparent bg, border rgba(239,231,210,0.15)
  return (
    <div
      onClick={handleClick}
      style={{
        background: top ? 'var(--plum-wash)' : 'transparent',
        border: top ? '1px solid var(--plum-soft)' : '1px solid rgba(239,231,210,0.15)',
        borderRadius: 8,
        padding: '13px 15px',
        cursor: 'pointer',
        transition: 'background 0.12s',
        marginBottom: 8,
      }}
    >
      {showBadgeActual && h.badge && (
        <div style={{
          marginBottom: 8, padding: '4px 8px',
          background: 'var(--mustard-wash)', border: '1px solid var(--mustard-soft)',
          fontFamily: 'var(--sans)', fontSize: 9.5, fontWeight: 500, color: 'var(--mustard)',
          display: 'flex', alignItems: 'center', gap: 6, borderRadius: 4,
        }}>
          <span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--mustard)', flexShrink: 0 }} />
          {h.badge}
        </div>
      )}

      {/* No.N + confidence + arrow */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
        <span style={{
          fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: 12, color: 'var(--plum)',
        }}>No.{rank}</span>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ fontFamily: 'var(--sans)', fontSize: 11, fontWeight: 600, color: 'var(--ink)' }}>
            {confPct}<span style={{ fontWeight: 400, color: 'var(--ink-mute)' }}>%</span>
          </span>
          {/* Figma uses -scale-y-100 on the up-arrow to show it as a trending indicator */}
          {delta > 0 && <span style={{ fontSize: 9, color: 'var(--ink-mute)', display: 'inline-block', transform: 'scaleY(-1)' }}>{'▲'}</span>}
          {delta < 0 && <span style={{ fontSize: 9, color: 'var(--ink-mute)' }}>{'▼'}</span>}
        </div>
      </div>

      {/* Hypothesis name */}
      <div style={{
        fontFamily: 'var(--sans)', fontSize: 13, fontWeight: 600,
        color: 'var(--ink)', lineHeight: 1.25, marginBottom: 8,
      }}>{h.name}</div>

      {/* Confidence bar */}
      <div style={{ background: 'var(--parchment-2)', height: 4, borderRadius: 2, overflow: 'hidden', marginBottom: 6 }}>
        <div style={{ width: `${confPct}%`, height: '100%', background: 'var(--plum)', borderRadius: 2, transition: 'width .5s' }} />
      </div>

      {/* "N pinned - supports" + delta */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingTop: 2 }}>
        <span style={{ fontFamily: 'var(--sans)', fontSize: 10, color: 'var(--ink-mute)' }}>
          {supportingCount} pinned - supports
        </span>
        {delta !== 0 && (
          <span style={{ fontFamily: 'var(--sans)', fontSize: 10, color: dColor }}>
            {arrow} {delta > 0 ? '+' : ''}{delta} pts
          </span>
        )}
      </div>

      {/* Expanded supporting/counter/next-step - mustard section headings per Figma */}
      {isExpanded && h.supporting && (
        <div style={{ marginTop: 14 }}>
          <div style={{
            fontFamily: 'var(--sans)', fontSize: 11, fontWeight: 600,
            letterSpacing: '0.14em', textTransform: 'uppercase',
            color: 'var(--mustard)', marginBottom: 8,
          }}>Supporting</div>
          <ul style={{ margin: 0, padding: '0 0 0 14px', listStyleType: 'disc' }}>
            {h.supporting.map((s, i) => (
              <li key={i} style={{ fontFamily: 'var(--sans)', fontSize: 12, lineHeight: 1.45, color: 'var(--ink)', marginBottom: 6 }}>{s}</li>
            ))}
          </ul>
          {h.counter && h.counter.length > 0 && (
            <>
              <div style={{
                fontFamily: 'var(--sans)', fontSize: 11, fontWeight: 600,
                letterSpacing: '0.14em', textTransform: 'uppercase',
                color: 'var(--mustard)', marginTop: 14, marginBottom: 8,
              }}>Tensions</div>
              <ul style={{ margin: 0, padding: '0 0 0 14px', listStyleType: 'disc' }}>
                {h.counter.map((c, i) => (
                  <li key={i} style={{ fontFamily: 'var(--sans)', fontSize: 12, lineHeight: 1.45, color: 'var(--ink)', marginBottom: 6 }}>{c}</li>
                ))}
              </ul>
            </>
          )}
          {h.nextStep && (
            <div style={{ marginTop: 14 }}>
              <div style={{
                fontFamily: 'var(--sans)', fontSize: 11, fontWeight: 600,
                letterSpacing: '0.14em', textTransform: 'uppercase',
                color: 'var(--mustard)', marginBottom: 8,
              }}>Next step</div>
              <div style={{ fontFamily: 'var(--sans)', fontSize: 12, lineHeight: 1.45, color: 'var(--ink)' }}>{h.nextStep}</div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── Watson's Note ─────────────────────────────────────────────────────
function Narrative({ ranked, clues, text, writing, height }) {
  const displayText = text != null ? text : narrativeForRanked(ranked || [], clues || []);
  return (
    <div style={{ position: 'relative' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
        <span style={{
          fontFamily: 'var(--sans)', fontSize: 12, fontWeight: 600,
          letterSpacing: '1px', textTransform: 'uppercase', color: 'var(--plum)',
        }}>Watson's Note</span>
        {writing && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
            <span style={{ width: 5, height: 5, borderRadius: 2.5, background: 'var(--plum)', flexShrink: 0 }} />
            <span style={{ fontFamily: 'var(--sans)', fontSize: 9.5, color: 'var(--plum)' }}>writing</span>
          </div>
        )}
      </div>
      <div style={{
        fontFamily: 'var(--sans)',
        fontSize: 13, lineHeight: 1.6, color: 'var(--ink-soft)',
        whiteSpace: 'pre-wrap',
        height, overflow: height ? 'hidden' : 'visible',
        position: 'relative',
      }}>
        {displayText}
        {writing && (
          <span style={{
            display: 'inline-block', width: 6, height: 13,
            background: 'var(--plum)',
            verticalAlign: '-2px', marginLeft: 1,
            animation: 'cursor-blink 1s steps(2) infinite',
          }} />
        )}
        {height && (
          <div style={{
            position: 'absolute', left: 0, right: 0, bottom: 0, height: 32,
            background: 'linear-gradient(transparent, var(--paper))',
          }} />
        )}
      </div>
    </div>
  );
}

// ── Full Watson left rail ──────────────────────────────────────────────
function WatsonRail({
  pins = [],
  ranked,
  hypos,
  narrative,
  writing = true,
  animateLastPin,
  narrativeHeight = 200,
  onUnpin,
  onFocus,
}) {
  const computedRanked = ranked || (hypos ? hypos : HYPOTHESES);
  const displayPins = pins;
  const [expandedIds, setExpandedIds] = uSW(() => new Set());

  function handleToggle(id) {
    setExpandedIds(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  }

  const pinnedIds = new Set(displayPins.map(p => p.id));
  const twistPinned = pinnedIds.has('PT14:23');

  const watsonIconUrl = 'https://www.figma.com/api/mcp/asset/b2aa8beb-4e64-43e6-ae32-16588678c7ba';

  return (
    <div style={{ width: '100%', display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>

      {/* Header: Watson icon + "Watson" italic + "YOUR POCKET COPILOT" */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 10,
        padding: '20px 22px 16px',
        flexShrink: 0,
      }}>
        <div style={{ width: 24, height: 22, flexShrink: 0 }}>
          <img src={watsonIconUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }}
            onError={(e) => { e.target.style.display = 'none'; }}
          />
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 6 }}>
          <span style={{
            fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: 17,
            color: 'var(--ink)', lineHeight: 1,
          }}>Watson</span>
          <span style={{
            fontFamily: 'var(--sans)', fontWeight: 400, fontSize: 9.5,
            color: 'var(--ink-mute)', letterSpacing: '0.04em', textTransform: 'uppercase',
            lineHeight: 1.2,
          }}>YOUR POCKET COPILOT</span>
        </div>
      </div>

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

        {/* Clues Collected */}
        <div style={{ padding: '0 22px 14px', flexShrink: 0 }}>
          <Pocket pins={displayPins} animateLast={animateLastPin} onUnpin={onUnpin} onFocus={onFocus} />
        </div>

        {/* Hypotheses, progressively revealed */}
        {(() => {
          const n = displayPins.length;
          if (n === 0) {
            return (
              <div style={{
                padding: '15px 22px 16px',
                borderTop: '1px solid var(--rule)',
                flexShrink: 0,
              }}>
                <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 10 }}>
                  <span style={{
                    fontFamily: 'var(--sans)', fontSize: 12, fontWeight: 600,
                    letterSpacing: '1px', textTransform: 'uppercase', color: 'var(--plum)',
                  }}>Hypotheses</span>
                  <span style={{ fontFamily: 'var(--sans)', fontSize: 10, color: 'var(--ink-mute)' }}>waiting</span>
                </div>
                <div style={{
                  fontFamily: 'var(--serif)', fontStyle: 'italic',
                  fontSize: 12.5, lineHeight: 1.55, color: 'var(--ink-mute)',
                }}>
                  No theory yet. Pin a clue and I will begin to form one.
                </div>
              </div>
            );
          }
          const visibleCount = n === 1 ? 1 : n <= 2 ? 2 : 3;
          const visible = computedRanked.slice(0, visibleCount);
          return (
            <div style={{
              padding: '15px 22px 16px',
              borderTop: '1px solid var(--rule)',
              flexShrink: 0,
            }}>
              <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 8 }}>
                <span style={{
                  fontFamily: 'var(--sans)', fontSize: 12, fontWeight: 600,
                  letterSpacing: '1px', textTransform: 'uppercase', color: 'var(--plum)',
                }}>Hypotheses</span>
                <span style={{ fontFamily: 'var(--sans)', fontSize: 10, color: 'var(--ink-mute)' }}>
                  {n < 3 ? `${visibleCount} so far` : 'ranked - live'}
                </span>
              </div>
              {visible.map((h, i) => (
                <HypothesisCard
                  key={h.id}
                  h={h}
                  rank={i + 1}
                  top={i === 0}
                  expanded={i === 0 ? !expandedIds.has(h.id) : expandedIds.has(h.id)}
                  onToggle={handleToggle}
                  showBadge={h.id === 'hijack' && !twistPinned}
                />
              ))}
              {visibleCount < 3 && (
                <div style={{
                  marginTop: 10,
                  fontFamily: 'var(--sans)', fontSize: 11, lineHeight: 1.5,
                  color: 'var(--ink-faint)', fontStyle: 'italic',
                }}>
                  More clues will surface alternative readings.
                </div>
              )}
            </div>
          );
        })()}

        {/* Watson's Note */}
        <div style={{
          padding: '13px 22px 22px',
          borderTop: '1px solid var(--rule)',
        }}>
          <Narrative
            ranked={computedRanked}
            clues={displayPins}
            text={narrative}
            writing={writing}
            height={narrativeHeight}
          />
        </div>

      </div>
    </div>
  );
}

// ── Inject keyframes ──────────────────────────────────────────────────
if (typeof document !== 'undefined' && !document.getElementById('watson-keyframes')) {
  const s = document.createElement('style');
  s.id = 'watson-keyframes';
  s.textContent = `
    @keyframes pin-in {
      0%   { opacity: 0; transform: translateX(-10px) scale(.96); }
      40%  { opacity: 1; transform: translateX(0) scale(1); }
      100% { opacity: 1; transform: translateX(0) scale(1); }
    }
    @keyframes cursor-blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
  `;
  document.head.appendChild(s);
}

Object.assign(window, {
  Pocket, HypothesisCard, Narrative, WatsonRail,
  EVIDENCE, EVIDENCE_TWIST, HYPOTHESES,
  NARRATIVE, NARRATIVE_1PIN, NARRATIVE_3PIN,
  recomputeHypotheses, narrativeForRanked,
});
