// prototype.jsx — main shell + boot + pill-choice + blue-pill flow
// Composes <MatrixRain> + <CRTOverlay> + screens. State machine drives
// which screen is visible: boot → choice → terminal | bluepill.

const { useState: usePS, useEffect: useFX, useRef: useRf, useCallback: useCB, useMemo: useMM } = React;

// ── Typewriter hook ─────────────────────────────────────────────────────
// Reveals one char at a time. Calls onDone when finished.
function useTypewriter(text, speed = 28, start = true, onDone) {
  const [out, setOut] = usePS('');
  useFX(() => {
    if (!start) return;
    setOut('');
    let i = 0;
    let cancelled = false;
    const tick = () => {
      if (cancelled) return;
      i += 1;
      setOut(text.slice(0, i));
      if (i < text.length) {
        setTimeout(tick, speed + Math.random() * speed * 0.6);
      } else if (onDone) {
        setTimeout(onDone, 200);
      }
    };
    setTimeout(tick, 80);
    return () => { cancelled = true; };
  }, [text, start, speed]);
  return out;
}

// ── Boot screen ─────────────────────────────────────────────────────────
function BootScreen({ theme, onDone }) {
  const c = theme;
  const lines = c.bootLines;
  const [lineIdx, setLineIdx] = usePS(0);
  const [done, setDone] = usePS(false);

  // Auto-advance: each line types out then advance to next.
  useFX(() => {
    if (lineIdx >= lines.length) {
      setTimeout(() => setDone(true), 300);
      setTimeout(onDone, 800);
    }
  }, [lineIdx, lines.length, onDone]);

  return (
    <div style={{
      position: 'absolute',
      inset: 0,
      background: c.bgDeep,
      color: c.fg,
      fontFamily: c.fontMono,
      fontSize: 18,
      lineHeight: 1.7,
      padding: '24% 8% 0',
      zIndex: 20,
      opacity: done ? 0 : 1,
      transition: 'opacity 0.6s',
      pointerEvents: 'none',
    }}>
      {lines.slice(0, lineIdx).map((l, i) => (
        <div key={i} style={{ textShadow: `0 0 6px ${c.fg}`, opacity: 0.85 }}>{l}</div>
      ))}
      {lineIdx < lines.length && (
        <TypingLine
          text={lines[lineIdx]}
          theme={c}
          speed={lineIdx === 0 ? 65 : 30}
          onDone={() => setLineIdx((i) => i + 1)}
        />
      )}
    </div>
  );
}

function TypingLine({ text, theme, speed, onDone }) {
  const out = useTypewriter(text, speed, true, onDone);
  return (
    <div style={{ color: theme.fgGlow, textShadow: `0 0 8px ${theme.fg}` }}>
      {out}
      <span style={{
        display: 'inline-block',
        width: 10,
        height: 18,
        background: theme.fgGlow,
        marginLeft: 4,
        verticalAlign: '-3px',
        animation: 'cursor-blink 1s steps(2) infinite',
        boxShadow: `0 0 8px ${theme.fg}`,
      }} />
    </div>
  );
}

// ── Classified File title ─────────────────────────────────────────────
// 4 variants of the classified-dossier landing card. All share the same
// green frame chrome so they read as one design system — only the body
// content/composition changes.
//
//   'dossier'   — trimmed minimal: subject + codename + 1-line status
//   'wakeup'    — "Wake up, Neo..." film opening line, typed live, loops
//                 through the three iconic Matrix terminal lines
//   'matrixhas' — ASCII-numbered stack of the three film quotes + subject row
//   'operator'  — Tank-style operator screen: dim column-number grid behind
//                 a tight classified card

// Common: redaction bar of N "█" chars in the brand green.
function Redacted({ chars = 8, theme }) {
  const c = theme;
  return (
    <span style={{
      color: c.fg,
      textShadow: `0 0 6px ${c.fg}`,
      letterSpacing: 1,
      opacity: 0.92,
    }}>{'█'.repeat(chars)}</span>
  );
}

// One field row. Value can be any node — we render it inline.
function CFField({ theme, label, value }) {
  const c = theme;
  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: '110px 1fr',
      gap: 14,
      padding: '5px 0',
      fontSize: 13,
      alignItems: 'baseline',
    }}>
      <div style={{
        color: c.fgDim,
        textTransform: 'uppercase',
        letterSpacing: 3,
        fontSize: 11,
      }}>{label}</div>
      <div>{value}</div>
    </div>
  );
}

// Shared frame chrome (border, header strip, glow) — every variant inherits.
function CFFrame({ theme, docId = '#0A1F2', date = '1999.03.31', headerRight = null, children }) {
  const c = theme;
  return (
    <div style={{
      position: 'relative',
      marginBottom: 40,
      fontFamily: c.fontMono,
      color: c.fg,
      width: 620,
      background: 'rgba(0,0,0,0.66)',
      border: `1px solid ${c.fgDim}`,
      boxShadow: `0 0 26px rgba(0,255,65,0.18), inset 0 0 40px rgba(0,0,0,0.55)`,
    }}>
      <div style={{
        display: 'flex',
        alignItems: 'center',
        gap: 14,
        padding: '12px 22px',
        background: 'rgba(0,255,65,0.06)',
        borderBottom: `1px solid ${c.fgDim}`,
        fontSize: 12,
        letterSpacing: 4,
        textTransform: 'uppercase',
        color: c.fgGlow,
        textShadow: `0 0 6px ${c.fg}`,
      }}>
        <span style={{ color: c.accent, fontSize: 14, textShadow: `0 0 10px ${c.accent}` }}>▪▪▪</span>
        <span style={{ fontWeight: 700 }}>classified file</span>
        <span style={{ flex: 1, textAlign: 'center', color: c.fgDim, letterSpacing: 3 }}>
          doc {docId}
        </span>
        <span style={{ color: c.fgDim }}>{headerRight || date}</span>
      </div>
      <div style={{ padding: '18px 26px 18px' }}>{children}</div>
    </div>
  );
}

// Tiny one-line "red protocol required" status — shared by 3 variants.
function CFRedProtocol({ theme }) {
  const c = theme;
  return (
    <div style={{
      fontSize: 12, color: c.fgDim, letterSpacing: 3,
      textTransform: 'uppercase', marginTop: 4,
    }}>
      <span style={{ color: c.redPill, textShadow: `0 0 6px ${c.redPill}` }}>●</span>
      {'  red protocol required.'}
    </div>
  );
}

function ClassifiedFile({ theme, variant = 'dossier' }) {
  if (variant === 'wakeup')    return <CFWakeUp    theme={theme} />;
  if (variant === 'matrixhas') return <CFMatrixHas theme={theme} />;
  if (variant === 'operator')  return <CFOperator  theme={theme} />;
  return <CFDossier theme={theme} />;
}

// ── DecryptingDossier ────────────────────────────────────────────
// Renders the SAME chrome and field layout as <CFDossier> — same CFFrame,
// same subject/codename rows, same red-protocol footer. The redacted
// values animate in instead of being a static row of █ blocks. Once the
// animation completes, the visual output is IDENTICAL to CFDossier so the
// hand-off to the live <PillChoice> requires no element swap and the file
// never jumps position.
//
// Variants:
//   'decrypt-lock'    — chars churn ~1.1s, then lock left-to-right per row
//   'decrypt-scan'    — a horizontal green scan-line sweeps top to bottom
//                       across the field area, locking each row as it passes
//   'decrypt-buildup' — frame clip-path opens from a thin centre slit, then
//                       chars churn briefly, then lock left-to-right
function DecryptingDossier({ theme, variant, onResolved }) {
  const c = theme;
  const charset = (RAIN_CHARS[c.chars] || RAIN_CHARS.classic).split('');
  const pick = () => charset[Math.floor(Math.random() * charset.length)];
  const fields = [
    { label: 'subject',  len: 9 },
    { label: 'codename', len: 7 },
  ];

  const [tick, setTick]     = usePS(0);                 // churns katakana
  const [locked, setLocked] = usePS([0, 0]);            // chars locked per field (lock variant)
  const [scanY, setScanY]   = usePS(0);                 // scan progress 0..1 (scan variant)
  const [buildK, setBuildK] = usePS(variant === 'decrypt-buildup' ? 0 : 1);

  // Hold the latest onResolved in a ref so the animation effect below can
  // call it without listing it as a dep. Otherwise every parent re-render
  // (e.g. when the user hovers a pill) passes a fresh () => setResolved
  // function reference, the effect re-runs, and the buildup restarts from
  // the beginning — which the user noticed as "the intro keeps reloading
  // every time I hover".
  const onResolvedRef = useRf(onResolved);
  onResolvedRef.current = onResolved;

  useFX(() => {
    const churn = setInterval(() => setTick((x) => x + 1), 70);
    const t0 = performance.now();
    let raf;
    const cleanups = [() => clearInterval(churn)];

    const loop = () => {
      const e = performance.now() - t0;

      if (variant === 'decrypt-buildup') {
        setBuildK(Math.min(1, e / 700));
      }

      if (variant === 'decrypt-scan') {
        if (e > 250) {
          const s = Math.min(1, (e - 250) / 1700);
          setScanY(s);
          if (s >= 1) {
            setLocked([9, 7]);
            setTimeout(() => onResolvedRef.current && onResolvedRef.current(), 350);
            return;
          }
        }
        raf = requestAnimationFrame(loop);
        return;
      }

      // 'decrypt-lock' and 'decrypt-buildup' both lock chars L→R after a
      // churn hold. buildup waits longer so the frame can finish opening.
      const lockStartMs = variant === 'decrypt-buildup' ? 1500 : 1100;
      if (e > lockStartMs) {
        const ls = e - lockStartMs;
        const next = [
          Math.min(9, Math.floor(ls / 80)),
          Math.min(7, Math.floor(Math.max(0, ls - 200) / 80)),
        ];
        setLocked(next);
        if (next[0] >= 9 && next[1] >= 7) {
          setTimeout(() => onResolvedRef.current && onResolvedRef.current(), 350);
          return;
        }
      }
      raf = requestAnimationFrame(loop);
    };
    loop();
    cleanups.push(() => cancelAnimationFrame(raf));

    return () => cleanups.forEach((fn) => fn());
  }, [variant]);

  // Wrapper just carries the clip/opacity animation. No layout role —
  // centring is handled by the parent's column-flex (alignItems:center)
  // in PillChoice.
  const wrapStyle = variant === 'decrypt-buildup' ? {
    clipPath: `inset(0 ${(1 - buildK) * 50}% 0 ${(1 - buildK) * 50}%)`,
    opacity: buildK,
  } : {};

  // For scan variant: lock entire field once scan passes its row band.
  // Thresholds picked so subject row (top) locks around 45% scan, codename
  // row (just below) locks at ~85%.
  const isScanLocked = (fi) => {
    if (variant !== 'decrypt-scan') return false;
    return scanY > (fi === 0 ? 0.45 : 0.85);
  };

  return (
    <div style={wrapStyle}>
      <CFFrame theme={c}>
        <div style={{ position: 'relative' }}>
          {fields.map((f, fi) => (
            <div key={f.label} style={{
              display: 'grid', gridTemplateColumns: '110px 1fr',
              gap: 14, padding: '5px 0', fontSize: 13, alignItems: 'baseline',
            }}>
              <div style={{
                color: c.fgDim, textTransform: 'uppercase',
                letterSpacing: 3, fontSize: 11,
              }}>{f.label}</div>
              <div style={{ letterSpacing: 1, fontFamily: c.fontMono }}>
                {Array.from({ length: f.len }, (_, i) => {
                  const isLocked = variant === 'decrypt-scan'
                    ? isScanLocked(fi)
                    : i < locked[fi];
                  if (isLocked) {
                    return <span key={i} style={{
                      color: c.fg,
                      textShadow: `0 0 6px ${c.fg}`,
                      letterSpacing: 1,
                      opacity: 0.92,
                    }}>█</span>;
                  }
                  return <span key={i} style={{
                    color: c.accent,
                    textShadow: `0 0 10px ${c.accent}`,
                    opacity: 0.85,
                  }}>{pick()}</span>;
                })}
                {/* force re-render on tick so random chars churn */}
                <span style={{ display: 'none' }}>{tick}</span>
              </div>
            </div>
          ))}

          {variant === 'decrypt-scan' && scanY > 0 && scanY < 1 && (
            <div style={{
              position: 'absolute',
              left: -20, right: -20,
              top: `${scanY * 100}%`,
              height: 2,
              background: c.fgGlow,
              boxShadow: `0 0 14px ${c.fg}, 0 0 30px ${c.fg}`,
              pointerEvents: 'none',
            }} />
          )}
        </div>

        <div style={{ height: 6 }} />
        <CFRedProtocol theme={c} />
      </CFFrame>
    </div>
  );
}

// Variant A — trimmed minimal dossier (default)
function CFDossier({ theme }) {
  const c = theme;
  return (
    <CFFrame theme={c}>
      <CFField theme={c} label="subject"  value={<Redacted chars={9} theme={c} />} />
      <CFField theme={c} label="codename" value={<Redacted chars={7} theme={c} />} />
      <div style={{ height: 6 }} />
      <CFRedProtocol theme={c} />
    </CFFrame>
  );
}

// Variant B — Wake up, Neo... live typing
function CFWakeUp({ theme }) {
  const c = theme;
  const lines = ['Wake up, Neo...', 'The Matrix has you...', 'Follow the white rabbit.'];
  const [idx, setIdx] = usePS(0);
  const [shown, setShown] = usePS('');
  useFX(() => {
    let i = 0;
    setShown('');
    const text = lines[idx];
    const iv = setInterval(() => {
      i += 1;
      setShown(text.slice(0, i));
      if (i >= text.length) {
        clearInterval(iv);
        setTimeout(() => setIdx((n) => (n + 1) % lines.length), 1800);
      }
    }, 70);
    return () => clearInterval(iv);
  }, [idx]);
  return (
    <CFFrame theme={c}>
      <div style={{
        fontSize: 18, color: c.fgGlow,
        textShadow: `0 0 10px ${c.fg}, 0 0 22px ${c.fg}`,
        letterSpacing: 1, marginBottom: 16, minHeight: 26,
      }}>
        {shown}
        <span style={{
          display: 'inline-block', width: 9, height: 16,
          background: c.fgGlow, marginLeft: 4, verticalAlign: '-3px',
          animation: 'cursor-blink 1s steps(2) infinite',
          boxShadow: `0 0 8px ${c.fg}`,
        }} />
      </div>
      <div style={{ borderTop: `1px dashed ${c.fgDim}`, paddingTop: 12 }}>
        <CFField theme={c} label="subject" value={<Redacted chars={9} theme={c} />} />
        <CFRedProtocol theme={c} />
      </div>
    </CFFrame>
  );
}

// Variant C — stacked film quotes as transmission lines
function CFMatrixHas({ theme }) {
  const c = theme;
  const quotes = [
    'Wake up, Neo...',
    'The Matrix has you...',
    'Follow the white rabbit.',
  ];
  return (
    <CFFrame theme={c} headerRight="transmission">
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
        {quotes.map((q, i) => (
          <div key={q} style={{
            display: 'flex', gap: 12, alignItems: 'baseline', fontSize: 14,
          }}>
            <span style={{ color: c.fgDim, width: 24, textAlign: 'right' }}>
              {String(i + 1).padStart(2, '0')}
            </span>
            <span style={{ color: c.fgDim }}>»</span>
            <span style={{ color: c.fgGlow, textShadow: `0 0 6px ${c.fg}` }}>{q}</span>
          </div>
        ))}
      </div>
      <div style={{
        borderTop: `1px dashed ${c.fgDim}`,
        paddingTop: 12,
        display: 'flex',
        alignItems: 'baseline',
        gap: 14,
      }}>
        <div style={{
          color: c.fgDim, textTransform: 'uppercase',
          letterSpacing: 3, fontSize: 11, width: 90,
        }}>subject</div>
        <Redacted chars={9} theme={c} />
        <div style={{ flex: 1 }} />
        <div style={{ fontSize: 11, color: c.redPill, textShadow: `0 0 6px ${c.redPill}`, letterSpacing: 2 }}>
          ● red protocol
        </div>
      </div>
    </CFFrame>
  );
}

// Variant D — operator-screen vibe: dim column-number grid behind the card
function CFOperator({ theme }) {
  const c = theme;
  return (
    <div style={{ position: 'relative' }}>
      <div style={{
        position: 'absolute',
        inset: '-30px -70px',
        fontFamily: c.fontMono,
        fontSize: 10,
        color: c.fgDim,
        opacity: 0.42,
        pointerEvents: 'none',
        display: 'flex',
        flexWrap: 'wrap',
        gap: 4,
        overflow: 'hidden',
        letterSpacing: 1,
      }}>
        {Array.from({ length: 60 }, (_, i) => (
          <span key={i}>{`[${String(i).padStart(2, '0')}]`}</span>
        ))}
      </div>
      <CFFrame theme={c} headerRight="operator 7">
        <CFField theme={c} label="subject" value={<Redacted chars={9} theme={c} />} />
        <CFField theme={c} label="alias"   value={<Redacted chars={6} theme={c} />} />
        <div style={{ height: 4 }} />
        <CFRedProtocol theme={c} />
      </CFFrame>
    </div>
  );
}

function PillChoice({ theme, onRed, onBlue, hovered, setHovered, titleVariant = 'wakeup', pillShape = 'edgelit', introVariant = null }) {
  const c = theme;
  const introIsDecrypt = introVariant && introVariant.startsWith('decrypt-');
  const [resolved, setResolved] = usePS(!introIsDecrypt);
  const [appeared, setAppeared] = usePS(false);
  useFX(() => {
    const t = setTimeout(() => setAppeared(true), 40);
    return () => clearTimeout(t);
  }, []);
  const showPills = introIsDecrypt ? resolved : appeared;
  return (
    // Outer: full-artboard flex that places its single inner column dead
    // centre via `place-items: center`. This split — an outer flex that
    // centers ONE child + an inner flex-column that lays out all the
    // visible elements — is what makes the file, pills and hint share
    // exactly the same vertical axis. Don't collapse them back into one;
    // the asymmetric "file slightly left, pills slightly right" bug came
    // from per-child centering each picking a slightly different base.
    <div style={{
      position: 'absolute', inset: 0, zIndex: 10,
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      pointerEvents: 'none',
      color: c.fg,
      fontFamily: c.font,
    }}>
      <div style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        opacity: introIsDecrypt ? 1 : (appeared ? 1 : 0),
        transform: introIsDecrypt ? 'translateY(0)' : (appeared ? 'translateY(0)' : 'translateY(10px)'),
        transition: 'opacity 0.7s ease-out, transform 0.7s ease-out',
      }}>
        {introIsDecrypt ? (
          <DecryptingDossier
            theme={c}
            variant={introVariant}
            onResolved={() => setResolved(true)}
          />
        ) : (
          <ClassifiedFile theme={c} variant={titleVariant} />
        )}

        {/* Pills — visibility:hidden takes layout space so the file above
            does not shift when pills appear / disappear. Only opacity
            animates so the reveal is a true fade-in. */}
        <div style={{
          display: 'flex', gap: 36,
          alignItems: 'center',
          pointerEvents: showPills ? 'auto' : 'none',
          opacity: showPills ? 1 : 0,
          visibility: showPills ? 'visible' : 'hidden',
          transition: 'opacity 0.8s ease-out 0.1s',
        }}>
          <PillButton
            color={c.bluePill} glow={c.bluePillGlow}
            theme={c} shape={pillShape}
            onClick={onBlue}
            onHover={(h) => setHovered(h ? 'blue' : null)}
          />
          <PillButton
            color={c.redPill} glow={c.redPillGlow}
            theme={c} shape={pillShape}
            onClick={onRed}
            onHover={(h) => setHovered(h ? 'red' : null)}
          />
        </div>

        {/* hint */}
        <div style={{
          marginTop: 28,
          fontSize: 11,
          letterSpacing: 4,
          color: c.fgDim,
          textTransform: 'uppercase',
          fontFamily: c.fontMono,
          opacity: (showPills && !hovered) ? 0.7 : 0,
          visibility: showPills ? 'visible' : 'hidden',
          transition: 'opacity 0.5s',
        }}>
          choose carefully. one choice cannot be undone.
        </div>
      </div>
    </div>
  );
}

// PillButton — 5 different shape/finish variants, all share the same
// pure-visual interface (no caption, no label, no inner dot). Identity
// reads only through colour + shape. The pill is the message.
//
//   'edgelit'  — dark capsule with a thin coloured rim + soft outer halo
//   'glossy'   — a real-pharmacy pill: solid colour body with white sheen
//   'vertical' — vertical capsule, two-tone gradient down the long axis
//   'holohud'  — angular hexagonal HUD shape with corner brackets, sci-fi
//   'orb'      — pure plasma sphere, no border, just colour + glow
function PillButton(props) {
  const { shape = 'edgelit' } = props;
  if (shape === 'glossy')   return <PillGlossy   {...props} />;
  if (shape === 'vertical') return <PillVertical {...props} />;
  if (shape === 'holohud')  return <PillHoloHud  {...props} />;
  if (shape === 'orb')      return <PillOrb      {...props} />;
  return <PillEdgeLit {...props} />;
}

// Shared hover hook to keep the variants concise.
function usePillHover(onHover) {
  const [hover, setHover] = usePS(false);
  return [hover, {
    onMouseEnter: () => { setHover(true);  onHover && onHover(true); },
    onMouseLeave: () => { setHover(false); onHover && onHover(false); },
  }];
}

// 1) Edge-lit capsule — dark body, coloured rim, outer halo
function PillEdgeLit({ color, glow, onClick, onHover }) {
  const [hover, hp] = usePillHover(onHover);
  return (
    <button onClick={onClick} {...hp} style={{
      width: 220, height: 56,
      borderRadius: 999,
      border: `1.5px solid ${color}`,
      cursor: 'pointer', outline: 'none', padding: 0,
      background: hover
        ? `radial-gradient(ellipse at center, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.7) 55%, ${glow} 100%)`
        : `radial-gradient(ellipse at center, rgba(0,0,0,0.92) 0%, rgba(0,0,0,0.78) 55%, rgba(0,0,0,0.55) 100%)`,
      boxShadow: hover
        ? `0 0 26px ${glow}, 0 0 60px ${glow}, inset 0 0 18px ${glow}, inset 0 0 2px ${color}`
        : `0 0 12px ${glow}, inset 0 0 12px ${glow}, inset 0 0 1px ${color}`,
      transform: hover ? 'translateY(-2px)' : 'none',
      transition: 'all 0.22s ease-out',
      position: 'relative',
    }}>
      <span style={{
        position: 'absolute', top: 4, left: 32, right: 32, height: 10,
        borderRadius: 999,
        background: 'linear-gradient(180deg, rgba(255,255,255,0.18), rgba(255,255,255,0) 90%)',
        pointerEvents: 'none',
      }} />
    </button>
  );
}

// 2) Glossy pharmacy pill — solid colour, white sheen, looks like a Tylenol
function PillGlossy({ color, glow, onClick, onHover }) {
  const [hover, hp] = usePillHover(onHover);
  return (
    <button onClick={onClick} {...hp} style={{
      width: 220, height: 60,
      borderRadius: 999,
      border: 'none', cursor: 'pointer', outline: 'none', padding: 0,
      background: `linear-gradient(180deg,
        #ffffff 0%, ${color} 18%, ${color} 70%, rgba(0,0,0,0.4) 100%)`,
      boxShadow: hover
        ? `0 0 24px ${glow}, 0 0 50px ${glow}, inset 0 -10px 18px rgba(0,0,0,0.35), inset 0 2px 4px rgba(255,255,255,0.7)`
        : `0 4px 18px rgba(0,0,0,0.5), 0 0 8px ${glow}, inset 0 -8px 14px rgba(0,0,0,0.35), inset 0 2px 3px rgba(255,255,255,0.6)`,
      transform: hover ? 'translateY(-2px) scale(1.03)' : 'none',
      transition: 'all 0.22s ease-out',
      position: 'relative',
      overflow: 'hidden',
    }}>
      {/* main top gloss highlight */}
      <span style={{
        position: 'absolute', top: 6, left: 24, right: 24, height: 16,
        borderRadius: 999,
        background: 'linear-gradient(180deg, rgba(255,255,255,0.85), rgba(255,255,255,0) 90%)',
        filter: 'blur(0.4px)',
        pointerEvents: 'none',
      }} />
      {/* center seam line, like a real pill */}
      <span style={{
        position: 'absolute', left: '50%', top: 14, bottom: 14,
        width: 1, background: 'rgba(0,0,0,0.25)',
        boxShadow: '1px 0 0 rgba(255,255,255,0.18)',
        pointerEvents: 'none',
      }} />
    </button>
  );
}

// 3) Vertical capsule — stood up, two-tone
function PillVertical({ color, glow, onClick, onHover }) {
  const [hover, hp] = usePillHover(onHover);
  return (
    <button onClick={onClick} {...hp} style={{
      width: 64, height: 180,
      borderRadius: 999,
      border: `1px solid ${color}`,
      cursor: 'pointer', outline: 'none', padding: 0,
      // top half white-ish (lighter tint), bottom half coloured — reads as a
      // two-tone capsule like the iconic blue-and-white meds
      background: `linear-gradient(180deg,
        rgba(240,240,240,0.95) 0%,
        rgba(220,220,220,0.92) 48%,
        ${color} 52%,
        ${color} 100%)`,
      boxShadow: hover
        ? `0 0 30px ${glow}, 0 0 70px ${glow}, inset 0 0 18px rgba(0,0,0,0.25), inset 0 2px 6px rgba(255,255,255,0.5)`
        : `0 6px 22px rgba(0,0,0,0.55), 0 0 14px ${glow}, inset 0 0 14px rgba(0,0,0,0.2), inset 0 2px 4px rgba(255,255,255,0.45)`,
      transform: hover ? 'translateY(-4px) rotate(2deg)' : 'rotate(0deg)',
      transition: 'all 0.25s ease-out',
      position: 'relative',
    }}>
      {/* vertical highlight line */}
      <span style={{
        position: 'absolute', left: 8, top: 16, bottom: 16, width: 7,
        borderRadius: 999,
        background: 'linear-gradient(180deg, rgba(255,255,255,0.7), rgba(255,255,255,0) 80%)',
        filter: 'blur(0.5px)', pointerEvents: 'none',
      }} />
    </button>
  );
}

// 4) Holo HUD — angular hexagonal sci-fi button with corner brackets
function PillHoloHud({ color, glow, onClick, onHover }) {
  const [hover, hp] = usePillHover(onHover);
  // 6-sided hexagonal shape via clip-path, plus 4 corner-bracket overlays.
  const hexClip = 'polygon(8% 0, 92% 0, 100% 50%, 92% 100%, 8% 100%, 0 50%)';
  return (
    <button onClick={onClick} {...hp} style={{
      width: 220, height: 64,
      border: 'none', cursor: 'pointer', outline: 'none', padding: 0,
      background: 'transparent',
      position: 'relative',
      transform: hover ? 'translateY(-2px) scale(1.03)' : 'none',
      transition: 'all 0.22s ease-out',
    }}>
      {/* Hex body */}
      <div style={{
        position: 'absolute', inset: 0,
        clipPath: hexClip,
        background: hover
          ? `linear-gradient(135deg, rgba(0,0,0,0.7), ${glow})`
          : 'rgba(0,0,0,0.6)',
        boxShadow: hover
          ? `0 0 28px ${glow}, 0 0 70px ${glow}`
          : `0 0 14px ${glow}`,
      }} />
      {/* Outline via inner shadow on a 1px shrunken clip — emulated with a
          second layer slightly inset */}
      <div style={{
        position: 'absolute', inset: 0,
        clipPath: hexClip,
        border: `1.5px solid ${color}`,
        // border won't follow clip-path — we fake it with a thin coloured
        // ring drawn by background-clip below
        background: `linear-gradient(${color}, ${color})`,
        WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
        WebkitMaskComposite: 'xor', maskComposite: 'exclude',
        padding: '1.5px', boxSizing: 'border-box',
        pointerEvents: 'none',
      }} />
      {/* 4 corner brackets */}
      {[
        { top: -3, left: -3,  rot: 0   },
        { top: -3, right: -3, rot: 90  },
        { bottom: -3, right: -3, rot: 180 },
        { bottom: -3, left: -3, rot: 270 },
      ].map((p, i) => (
        <span key={i} style={{
          position: 'absolute',
          top: p.top, bottom: p.bottom, left: p.left, right: p.right,
          width: 12, height: 12,
          borderTop: `2px solid ${color}`,
          borderLeft: `2px solid ${color}`,
          transform: `rotate(${p.rot}deg)`,
          boxShadow: `0 0 6px ${glow}`,
          pointerEvents: 'none',
        }} />
      ))}
      {/* Central tick / dash mark */}
      <span style={{
        position: 'absolute', top: '50%', left: '50%',
        transform: 'translate(-50%, -50%)',
        width: 28, height: 2,
        background: color,
        boxShadow: `0 0 8px ${color}`,
        pointerEvents: 'none',
      }} />
    </button>
  );
}

// 5) Plasma orb — pure sphere of colour and glow, no border. Most abstract.
function PillOrb({ color, glow, onClick, onHover }) {
  const [hover, hp] = usePillHover(onHover);
  return (
    <button onClick={onClick} {...hp} style={{
      width: 100, height: 100,
      borderRadius: '50%',
      border: 'none', cursor: 'pointer', outline: 'none', padding: 0,
      // Off-centre radial so it reads as a 3D sphere
      background: hover
        ? `radial-gradient(circle at 35% 32%, rgba(255,255,255,0.85) 0%, ${color} 30%, ${color} 55%, rgba(0,0,0,0.55) 100%)`
        : `radial-gradient(circle at 35% 32%, rgba(255,255,255,0.65) 0%, ${color} 35%, ${color} 60%, rgba(0,0,0,0.6) 100%)`,
      boxShadow: hover
        ? `0 0 30px ${glow}, 0 0 70px ${glow}, 0 0 120px ${glow}`
        : `0 0 18px ${glow}, 0 0 40px ${glow}`,
      transform: hover ? 'translateY(-3px) scale(1.06)' : 'none',
      transition: 'all 0.25s ease-out',
      position: 'relative',
    }}>
      {/* tiny specular highlight */}
      <span style={{
        position: 'absolute',
        top: '20%', left: '24%',
        width: 12, height: 8,
        borderRadius: '50%',
        background: 'rgba(255,255,255,0.85)',
        filter: 'blur(2px)',
        pointerEvents: 'none',
      }} />
    </button>
  );
}

// ── Glitch transition ─────────────────────────────────────────────────
// When the user takes the red pill we play a cinematic glitch sequence,
// then a slow white-to-black fade into the file system. Timings:
//   0–300ms      — hard glitch, no message yet (slice tears + RGB-split bars)
//   300–1800ms   — messages cycle one at a time (about 380ms each)
//   1800–2100ms  — quick white wash
//   2100–2600ms  — white fades to black (calls onDone partway so file
//                   system can render and fade in underneath)
function GlitchTransition({ theme, onDone, width, height }) {
  const c = theme;
  const [phase, setPhase] = usePS(0);  // 0 hard glitch / 1 word cycle / 2 wash / 3 fade
  const [wordIdx, setWordIdx] = usePS(0);
  const [tick, setTick] = usePS(0);

  const MESSAGES = ['BREACH', 'WAKE_UP', 'PROTOCOL=RED', 'WHO ARE YOU?'];

  useFX(() => {
    const ticker = setInterval(() => setTick((t) => t + 1), 55);
    // Tighter timeline: get to the file system in ~1.4s total so we don't
    // make the user wait. Wash + fade stays smooth but quick.
    const tPhase1 = setTimeout(() => setPhase(1), 260);   // start showing words
    const tPhase2 = setTimeout(() => setPhase(2), 1100);  // start white wash
    const tPhase3 = setTimeout(() => setPhase(3), 1320);  // start fade-out
    const tDone   = setTimeout(() => onDone(),   1500);   // hand off to file system
    // Cycle the messages every ~280ms once we hit phase 1.
    const tWord1 = setTimeout(() => setWordIdx(1), 540);
    const tWord2 = setTimeout(() => setWordIdx(2), 820);
    const tWord3 = setTimeout(() => setWordIdx(3), 1080);
    return () => {
      clearInterval(ticker);
      [tPhase1, tPhase2, tPhase3, tDone, tWord1, tWord2, tWord3].forEach(clearTimeout);
    };
  }, [onDone]);

  // Build slice tears that shift horizontally each tick (phase 0 + 1).
  const slices = [];
  const sliceCount = 14;
  for (let i = 0; i < sliceCount; i++) {
    const top = (i / sliceCount) * 100;
    const h2 = 100 / sliceCount;
    const offset = ((tick * 31 + i * 17) % 60) - 30;
    slices.push(
      <div key={i} style={{
        position: 'absolute',
        left: 0, right: 0,
        top: `${top}%`,
        height: `${h2 + 0.5}%`,
        transform: `translateX(${offset}px)`,
        background: `repeating-linear-gradient(
          to bottom,
          rgba(0,255,65,${0.04 + Math.random() * 0.12}) 0px,
          rgba(0,0,0,0.4) 2px,
          rgba(0,255,65,${0.02 + Math.random() * 0.06}) 4px
        )`,
        mixBlendMode: 'screen',
        opacity: 0.85,
      }} />
    );
  }

  // Background per phase. Phase 3 fades white → transparent so the file
  // system already mounted underneath can read through.
  let bg;
  if (phase === 2) bg = 'rgba(220,255,220,0.95)';
  else if (phase === 3) bg = 'rgba(0,0,0,0)';
  else bg = 'rgba(0,0,0,0.35)';

  return (
    <div style={{
      position: 'absolute', inset: 0,
      zIndex: 80,
      background: bg,
      transition: phase === 3 ? 'background 0.45s ease-in' : 'background 0.18s',
      pointerEvents: 'none',
      overflow: 'hidden',
    }}>
      {phase < 2 && slices}
      {phase === 1 && (
        <div style={{
          position: 'absolute',
          inset: 0,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          fontFamily: c.fontMono,
          fontSize: 56,
          fontWeight: 700,
          letterSpacing: 12,
          textTransform: 'uppercase',
        }}>
          {/* RGB-split layers */}
          <span style={{
            position: 'absolute', color: '#ff2240',
            transform: `translate(${(tick * 7) % 14 - 7}px, ${(tick * 5) % 10 - 5}px)`,
            mixBlendMode: 'screen', opacity: 0.85,
          }}>{MESSAGES[wordIdx]}</span>
          <span style={{
            position: 'absolute', color: '#3aa0ff',
            transform: `translate(${(tick * 11) % 14 - 7}px, ${(tick * 9) % 10 - 5}px)`,
            mixBlendMode: 'screen', opacity: 0.85,
          }}>{MESSAGES[wordIdx]}</span>
          <span style={{
            position: 'relative', color: c.fgGlow,
            textShadow: `0 0 18px ${c.fg}, 0 0 38px ${c.fg}`,
          }}>{MESSAGES[wordIdx]}</span>
        </div>
      )}
    </div>
  );
}


// ── Blue pill — "you stay in the matrix" sequence ───────────────────────
// Plays a short typing sequence, then redirects to GitHub. Includes an
// "actually, I changed my mind" button that returns to choice. Visually
// the rain calms / dims, the screen washes blue.
function BluePillScene({ theme, onCancel }) {
  const c = theme;
  const lines = [
    '> blue_pill accepted.',
    '> rewinding...',
    '> reality_check.disable()',
    '> nothing happened. you saw nothing.',
    '> redirecting to a place you already knew...',
  ];
  const [idx, setIdx] = usePS(0);
  const [countdown, setCountdown] = usePS(5);

  useFX(() => {
    if (idx >= lines.length) {
      const iv = setInterval(() => setCountdown((n) => n - 1), 1000);
      return () => clearInterval(iv);
    }
  }, [idx, lines.length]);

  useFX(() => {
    if (idx >= lines.length && countdown <= 0) {
      window.top.location.href = PROFILE.github;
    }
  }, [countdown, idx, lines.length]);

  return (
    <div style={{
      position: 'absolute',
      inset: 0,
      background: 'radial-gradient(circle at 50% 45%, rgba(58,160,255,0.18), rgba(0,0,0,0.92) 70%)',
      zIndex: 30,
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
      color: c.fg,
      fontFamily: c.fontMono,
      padding: 40,
      animation: 'blue-wash 0.6s ease-out',
    }}>
      <div style={{
        fontSize: 11,
        letterSpacing: 8,
        textTransform: 'uppercase',
        color: 'rgba(58,160,255,0.9)',
        marginBottom: 28,
        textShadow: '0 0 8px rgba(58,160,255,0.8)',
      }}>
        ─── you took the blue pill ───
      </div>

      <div style={{
        width: 520,
        maxWidth: '92%',
        background: 'rgba(0,0,0,0.5)',
        border: '1px solid rgba(58,160,255,0.35)',
        boxShadow: '0 0 30px rgba(58,160,255,0.25), inset 0 0 30px rgba(58,160,255,0.08)',
        padding: '22px 26px',
        fontSize: 15,
        lineHeight: 1.7,
        color: '#cfe8ff',
      }}>
        {lines.slice(0, idx).map((l, i) => (
          <div key={i} style={{ opacity: 0.75 }}>{l}</div>
        ))}
        {idx < lines.length && (
          <TypingLine
            text={lines[idx]}
            theme={{ ...c, fg: '#9ec9ff', fgGlow: '#cfe8ff' }}
            speed={30}
            onDone={() => setIdx((i) => i + 1)}
          />
        )}
        {idx >= lines.length && (
          <div style={{ marginTop: 18, color: '#9ec9ff', textShadow: '0 0 8px rgba(58,160,255,0.7)' }}>
            → opening github.com/OleksiukStepan in <b>{Math.max(0, countdown)}</b>s...
          </div>
        )}
      </div>

      <div style={{ marginTop: 28, display: 'flex', gap: 12 }}>
        <button
          onClick={onCancel}
          style={{
            border: '1px solid rgba(58,160,255,0.55)',
            background: 'rgba(58,160,255,0.08)',
            color: '#cfe8ff',
            fontFamily: c.fontMono,
            fontSize: 12,
            padding: '8px 18px',
            cursor: 'pointer',
            letterSpacing: 2,
            textTransform: 'uppercase',
            textShadow: '0 0 8px rgba(58,160,255,0.6)',
            boxShadow: '0 0 12px rgba(58,160,255,0.25)',
          }}
        >
          ↩ wait — I changed my mind
        </button>
        {idx >= lines.length && (
          <a
            href={PROFILE.github}
            target="_top"
            style={{
              border: '1px solid rgba(58,160,255,0.85)',
              background: 'rgba(58,160,255,0.18)',
              color: '#ffffff',
              fontFamily: c.fontMono,
              fontSize: 12,
              padding: '8px 18px',
              cursor: 'pointer',
              letterSpacing: 2,
              textTransform: 'uppercase',
              textDecoration: 'none',
              boxShadow: '0 0 18px rgba(58,160,255,0.5)',
            }}
          >→ open github now</a>
        )}
      </div>

      <div style={{
        position: 'absolute',
        bottom: 28,
        left: 0,
        right: 0,
        textAlign: 'center',
        fontSize: 10,
        letterSpacing: 4,
        color: 'rgba(58,160,255,0.6)',
        textTransform: 'uppercase',
      }}>
        you remember nothing of this screen.
      </div>
    </div>
  );
}

// ── PrototypeShell — composes everything ───────────────────────────────
function PrototypeShell({ theme, width, height, tweaks, autoBoot = true, titleVariant = 'dossier', pillShape = 'edgelit', introVariant = 'bootlog', initialStage = 'choice', explorerBackdrop = 'cinema', skillsLayout = 'compact', initialPath = ['~'], titleBarVariant = 'terminal', rainVariant = 'classic' }) {
  const c = theme;
  // 'intro' | 'choice' | 'glitch' | 'red' | 'blue'
  // For decrypt-* intro variants the animation happens INSIDE PillChoice,
  // so the file stays in its final position the whole time. No separate
  // intro stage needed. Other intro variants (legacy bootlog/crt/etc) are
  // no longer used so we simplify: respect the explicit initialStage.
  const [stage, setStage] = usePS(initialStage);
  const [hovered, setHovered] = usePS(null);

  // ── Cold-boot scene controller ──────────────────────────────────
  // When introVariant starts with 'boot-' we orchestrate a scene-level
  // intro: black-out → rain fades up → file mounts (buildup animation
  // happens inside PillChoice). Three timings produce the three variants.
  // Non-boot variants behave normally: rain visible, file mounted, no
  // overlay. Artboards that start directly on a non-choice stage (e.g.
  // file-system) skip boot entirely.
  const isBoot = initialStage === 'choice' && introVariant && introVariant.startsWith('boot-');
  const [bootBlack,       setBootBlack]       = usePS(isBoot ? 1 : 0);
  const [bootRainOp,      setBootRainOp]      = usePS(isBoot ? 0 : 1);
  const [bootFileVisible, setBootFileVisible] = usePS(!isBoot);
  const [bootSurge,       setBootSurge]       = usePS(0);

  useFX(() => {
    if (!isBoot) return;
    // Per-variant timeline.
    const sched = {
      'boot-sequenced': { blackOutAt: 50,  rainAt: 200, fileAt: 1100, surgeAt: null },
      'boot-sync':      { blackOutAt: 50,  rainAt: 50,  fileAt: 250,  surgeAt: null },
      'boot-surge':     { blackOutAt: 400, rainAt: 400, fileAt: 1000, surgeAt: 200 },
    }[introVariant];
    if (!sched) return;
    const ts = [];
    // Trigger transitions by mutating state — CSS transition does the
    // visual animation. blackOut/rainIn have fixed transition durations
    // in the JSX below (800ms each).
    ts.push(setTimeout(() => setBootBlack(0), sched.blackOutAt));
    ts.push(setTimeout(() => setBootRainOp(1), sched.rainAt));
    ts.push(setTimeout(() => setBootFileVisible(true), sched.fileAt));
    if (sched.surgeAt != null) {
      // Surge: flash to full instantly then fade out via CSS transition.
      ts.push(setTimeout(() => setBootSurge(1), sched.surgeAt));
      ts.push(setTimeout(() => setBootSurge(0), sched.surgeAt + 60));
    }
    return () => ts.forEach(clearTimeout);
  }, [introVariant, isBoot]);

  // Translate hover state to rain color override (red/blue tint while a pill
  // is hovered). Rain colour is otherwise locked to the theme.
  const rainColor = useMM(() => {
    if (hovered === 'red') return c.redPill;
    if (hovered === 'blue') return c.bluePill;
    return null;
  }, [hovered, c]);

  const glitchAmt = (tweaks.glitch ?? 30) / 100;

  return (
    <div style={{
      position: 'relative',
      width,
      height,
      background: c.bg,
      overflow: 'hidden',
      isolation: 'isolate',
      cursor: stage === 'red' ? 'default' : 'crosshair',
    }}>
      {/* Rain — always present, but its opacity is animated for boot-*
          intros so the scene starts from black and the rain wakes up. */}
      <div style={{
        position: 'absolute', inset: 0,
        opacity: bootRainOp,
        transition: 'opacity 800ms ease-out',
      }}>
        <MatrixRain
          theme={c}
          width={width}
          height={height}
          colorOverride={rainColor}
          glitch={stage === 'glitch' ? 1 : glitchAmt}
          speed={stage === 'glitch' ? 2.5 : (hovered === 'red' ? 1.3 : 1)}
          paused={false}
          variant={rainVariant}
        />
      </div>

      {/* Stage content */}
      {stage === 'choice' && bootFileVisible && (
        <PillChoice
          theme={c}
          onRed={() => setStage('glitch')}
          onBlue={() => setStage('blue')}
          hovered={hovered}
          setHovered={setHovered}
          titleVariant={titleVariant}
          pillShape={pillShape}
          // Boot intros all use the decrypt-buildup animation for the file;
          // the scene-level coordination is handled here. Non-boot intros
          // pass their own variant through unchanged.
          introVariant={autoBoot ? (isBoot ? 'decrypt-buildup' : introVariant) : null}
        />
      )}
      {stage === 'glitch' && (
        <GlitchTransition
          theme={c} width={width} height={height}
          onDone={() => setStage('red')}
        />
      )}
      {stage === 'red' && (
        <FileExplorer
          theme={c}
          onBack={() => setStage('choice')}
          backdrop={explorerBackdrop}
          skillsLayout={skillsLayout}
          initialPath={initialPath}
          titleBarVariant={titleBarVariant}
        />
      )}
      {stage === 'blue' && (
        <BluePillScene theme={c} onCancel={() => setStage('choice')} />
      )}

      {/* CRT overlay — on top of everything except glitch cursor */}
      {tweaks.scanlines && (
        <CRTOverlay theme={c} scanlines intensity={1} />
      )}

      {/* Cold-boot scene overlays — only present during boot-* intros */}
      {isBoot && bootSurge > 0.01 && (
        <div style={{
          position: 'absolute', inset: 0,
          background: `radial-gradient(ellipse at center, ${c.fgGlow} 0%, ${c.fg} 30%, rgba(0,0,0,0) 75%)`,
          opacity: bootSurge,
          transition: 'opacity 400ms ease-out',
          zIndex: 70, pointerEvents: 'none',
          mixBlendMode: 'screen',
        }} />
      )}
      {isBoot && (
        <div style={{
          position: 'absolute', inset: 0,
          background: '#000',
          opacity: bootBlack,
          transition: 'opacity 800ms ease-out',
          zIndex: 65, pointerEvents: 'none',
        }} />
      )}

      {/* Glitch cursor — fades during boot to not clutter */}
      {glitchAmt > 0.05 && stage !== 'boot' && (
        <GlitchCursor color={c.cursorTrail} intensity={glitchAmt} />
      )}

      {/* Stage label badge in top-right corner — still useful as a status
          tell (awaiting input / red_pill // online / blue_pill // sealed).
          Left-corner theme badge removed per feedback. */}
      <div style={{
        position: 'absolute',
        top: 12,
        right: 14,
        fontFamily: c.fontMono,
        fontSize: 10,
        letterSpacing: 3,
        color: c.fgDim,
        zIndex: 55,
        textTransform: 'uppercase',
        textShadow: `0 0 4px ${c.fg}`,
        pointerEvents: 'none',
      }}>
        {stage === 'choice' && '> awaiting input'}
        {stage === 'red' && '> red_pill // online'}
        {stage === 'blue' && '> blue_pill // sealed'}
      </div>
    </div>
  );
}

Object.assign(window, { PrototypeShell, BootScreen, PillChoice, BluePillScene });
