// ─────────────────────────────────────────────────────────────────
// KEY AUTO-DETECTION
//
// Real musical-key detection on uploaded audio via Web Audio.
// Krumhansl-Schmuckler key-finding algorithm.
//
// Pipeline:
//   1. decodeAudioData  → Float32Array (mono)
//   2. downsample       → 22.05 kHz (Nyquist over piano range)
//   3. windowed FFT     → magnitude spectrum per 4096-sample frame
//   4. chromagram       → fold spectrum into 12 pitch classes
//   5. summed chroma    → average over track (skip silence)
//   6. Krumhansl corr.  → match against 24 key profiles (12 maj + 12 min)
//   7. Camelot mapping  → wheel notation (1A..12A / 1B..12B)
//
// EXPORT:
//   window.KeyDetect.analyzeFile(File)            → { key, mode, camelot, confidence, chroma, alternates }
//   window.KeyDetect.analyzeBuffer(AudioBuffer)
//   <KeyDetectBadge result={…}/>                  — small chip
//   <KeyDetectModal file={…} onAccept onReject onClose/>
//   <AudioAutoDetectModal file={…} ...>           — combined Tempo + Key
// ─────────────────────────────────────────────────────────────────
(function(){
  const _S = React.useState;
  const _E = React.useEffect;

  const TARGET_SR = 22050;
  const FFT_SIZE = 4096;
  const HOP = 2048;
  const A4 = 440;
  const KEYS = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];

  // Krumhansl-Kessler key profiles (1990) — perceptual weights for each scale degree
  const MAJOR_PROFILE = [6.35,2.23,3.48,2.33,4.38,4.09,2.52,5.19,2.39,3.66,2.29,2.88];
  const MINOR_PROFILE = [6.33,2.68,3.52,5.38,2.60,3.53,2.54,4.75,3.98,2.69,3.34,3.17];

  // Camelot wheel: maj/min → wheel position (1A..12A minor, 1B..12B major)
  // Order of fifths: C, G, D, A, E, B, F#, C#, G#, D#, A#, F (each step = +7 semitones mod 12)
  const CAMELOT_MAJOR = { 'C':'8B','G':'9B','D':'10B','A':'11B','E':'12B','B':'1B','F#':'2B','C#':'3B','G#':'4B','D#':'5B','A#':'6B','F':'7B' };
  const CAMELOT_MINOR = { 'A':'8A','E':'9A','B':'10A','F#':'11A','C#':'12A','G#':'1A','D#':'2A','A#':'3A','F':'4A','C':'5A','G':'6A','D':'7A' };

  // ─── Audio ───────────────────────────────────────────────────
  function getAudioCtx() {
    if (window.__keyDetectCtx) return window.__keyDetectCtx;
    const C = window.AudioContext || window.webkitAudioContext;
    if (!C) return null;
    window.__keyDetectCtx = new C();
    return window.__keyDetectCtx;
  }
  async function decodeFile(file) {
    const ctx = getAudioCtx();
    if (!ctx) throw new Error('Web Audio not supported');
    const buf = await file.arrayBuffer();
    return new Promise((res, rej) => ctx.decodeAudioData(buf.slice(0), res, rej));
  }

  // ─── Mono + downsample ───────────────────────────────────────
  function monoDownsample(audioBuffer, targetSr) {
    const sr = audioBuffer.sampleRate, ch = audioBuffer.numberOfChannels, len = audioBuffer.length;
    const mono = new Float32Array(len);
    for (let c = 0; c < ch; c++) {
      const data = audioBuffer.getChannelData(c);
      for (let i = 0; i < len; i++) mono[i] += data[i] / ch;
    }
    const ratio = sr / targetSr, newLen = Math.floor(len / ratio);
    const out = new Float32Array(newLen);
    for (let i = 0; i < newLen; i++) {
      const idx = i * ratio, i0 = Math.floor(idx), f = idx - i0;
      out[i] = mono[i0] * (1 - f) + (mono[i0 + 1] || 0) * f;
    }
    return { samples: out, sampleRate: targetSr };
  }

  // ─── FFT (radix-2, in-place, real input) ─────────────────────
  function fftMag(real) {
    const N = real.length;
    const im = new Float32Array(N);
    // bit-reverse
    let j = 0;
    for (let i = 1; i < N; i++) {
      let bit = N >> 1;
      for (; j & bit; bit >>= 1) j ^= bit;
      j ^= bit;
      if (i < j) { [real[i], real[j]] = [real[j], real[i]]; [im[i], im[j]] = [im[j], im[i]]; }
    }
    // Cooley-Tukey
    for (let size = 2; size <= N; size <<= 1) {
      const half = size >> 1;
      const tableStep = -2 * Math.PI / size;
      for (let i = 0; i < N; i += size) {
        for (let k = 0; k < half; k++) {
          const angle = tableStep * k;
          const cos = Math.cos(angle), sin = Math.sin(angle);
          const tr = real[i + k + half] * cos - im[i + k + half] * sin;
          const ti = real[i + k + half] * sin + im[i + k + half] * cos;
          real[i + k + half] = real[i + k] - tr;
          im[i + k + half] = im[i + k] - ti;
          real[i + k] += tr;
          im[i + k] += ti;
        }
      }
    }
    // magnitudes
    const mag = new Float32Array(N >> 1);
    for (let i = 0; i < mag.length; i++) mag[i] = Math.sqrt(real[i] * real[i] + im[i] * im[i]);
    return mag;
  }

  // Hann window
  function hann(N) {
    const w = new Float32Array(N);
    for (let i = 0; i < N; i++) w[i] = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / (N - 1));
    return w;
  }

  // ─── Chromagram ──────────────────────────────────────────────
  function chromagram(samples, sr) {
    const window = hann(FFT_SIZE);
    const chroma = new Float32Array(12);
    let frames = 0;
    const buf = new Float32Array(FFT_SIZE);

    // Precompute pitch class for each FFT bin
    const binToPc = new Int8Array(FFT_SIZE >> 1);
    for (let k = 1; k < FFT_SIZE >> 1; k++) {
      const freq = k * sr / FFT_SIZE;
      if (freq < 65 || freq > 2093) { binToPc[k] = -1; continue; } // C2..C7 piano range
      const midi = 69 + 12 * Math.log2(freq / A4);
      const pc = ((Math.round(midi) % 12) + 12) % 12;
      binToPc[k] = pc;
    }

    for (let off = 0; off + FFT_SIZE < samples.length; off += HOP) {
      // Skip near-silent frames
      let energy = 0;
      for (let i = 0; i < FFT_SIZE; i++) {
        const v = samples[off + i];
        energy += v * v;
        buf[i] = v * window[i];
      }
      const rms = Math.sqrt(energy / FFT_SIZE);
      if (rms < 0.005) continue;

      const mag = fftMag(buf);
      for (let k = 1; k < mag.length; k++) {
        const pc = binToPc[k];
        if (pc >= 0) chroma[pc] += mag[k];
      }
      frames++;
    }
    if (frames === 0) return null;
    // Normalize
    let max = 0;
    for (let i = 0; i < 12; i++) if (chroma[i] > max) max = chroma[i];
    if (max > 0) for (let i = 0; i < 12; i++) chroma[i] /= max;
    return chroma;
  }

  // ─── Pearson correlation ─────────────────────────────────────
  function correlate(a, b) {
    const n = a.length;
    let am = 0, bm = 0;
    for (let i = 0; i < n; i++) { am += a[i]; bm += b[i]; }
    am /= n; bm /= n;
    let num = 0, da = 0, db = 0;
    for (let i = 0; i < n; i++) {
      const av = a[i] - am, bv = b[i] - bm;
      num += av * bv;
      da += av * av;
      db += bv * bv;
    }
    return num / (Math.sqrt(da * db) || 1);
  }

  // ─── Krumhansl-Schmuckler key matching ───────────────────────
  function findKey(chroma) {
    const scores = [];
    for (let i = 0; i < 12; i++) {
      // Rotate profile to start at root i
      const majRot = MAJOR_PROFILE.slice(i).concat(MAJOR_PROFILE.slice(0, i));
      const minRot = MINOR_PROFILE.slice(i).concat(MINOR_PROFILE.slice(0, i));
      scores.push({ key: KEYS[i], mode: 'Major', score: correlate(chroma, majRot) });
      scores.push({ key: KEYS[i], mode: 'Minor', score: correlate(chroma, minRot) });
    }
    scores.sort((a, b) => b.score - a.score);
    return scores;
  }

  // ─── Main analyzer ───────────────────────────────────────────
  async function analyzeBuffer(audioBuffer, onProgress) {
    const prog = (phase, pct) => {
      onProgress && onProgress(phase, pct);
      window.dispatchEvent(new CustomEvent('astro-key-detect-progress', { detail: { phase, pct } }));
    };
    prog('decoding', 0.1);
    const { samples, sampleRate } = monoDownsample(audioBuffer, TARGET_SR);
    prog('chromagram', 0.45);
    const chroma = chromagram(samples, sampleRate);
    if (!chroma) {
      prog('done', 1);
      return { key: null, mode: null, confidence: 0, chroma: null, alternates: [] };
    }
    prog('matching', 0.85);
    const ranked = findKey(chroma);
    const top = ranked[0], second = ranked[1];
    // Confidence: gap between top and second
    const gap = top.score - second.score;
    const conf = Math.max(0.3, Math.min(0.99, 0.55 + gap * 2.5));
    const camelot = top.mode === 'Major' ? CAMELOT_MAJOR[top.key] : CAMELOT_MINOR[top.key];
    const alternates = ranked.slice(1, 5).map(r => ({
      key: r.key, mode: r.mode, score: r.score,
      camelot: r.mode === 'Major' ? CAMELOT_MAJOR[r.key] : CAMELOT_MINOR[r.key],
    }));
    prog('done', 1);
    return {
      key: top.key, mode: top.mode, camelot,
      confidence: conf,
      chroma: Array.from(chroma),
      alternates,
      analyzedAt: Date.now(),
    };
  }

  async function analyzeFile(file, onProgress) {
    const audioBuffer = await decodeFile(file);
    return analyzeBuffer(audioBuffer, onProgress);
  }

  // ─── React: BADGE ────────────────────────────────────────────
  function KeyDetectBadge({ result }) {
    if (!result || !result.key) return null;
    return (
      <div style={{
        display: 'inline-flex', alignItems: 'center', gap: 8,
        padding: '4px 10px', border: '1px solid var(--rule)', background: 'var(--bg-2)',
      }}>
        <span className="ff-mono num" style={{ fontSize: 14, fontWeight: 500 }}>{result.key} {result.mode === 'Major' ? 'maj' : 'min'}</span>
        <span className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)' }}>· {result.camelot}</span>
        <span className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)' }}>· {Math.round(result.confidence * 100)}%</span>
      </div>
    );
  }

  // ─── Chroma bar ──────────────────────────────────────────────
  function ChromaBar({ chroma, rootKey }) {
    if (!chroma) return null;
    const max = Math.max(...chroma);
    return (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: 2, alignItems: 'flex-end', height: 60 }}>
        {chroma.map((v, i) => (
          <div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
            <div style={{ flex: 1, width: '100%', display: 'flex', alignItems: 'flex-end' }}>
              <div style={{ width: '100%', height: `${(v / max) * 100}%`, background: KEYS[i] === rootKey ? 'var(--ink)' : 'var(--ink-3)', minHeight: 1 }}/>
            </div>
            <span className="ff-mono" style={{ fontSize: 8, color: KEYS[i] === rootKey ? 'var(--ink)' : 'var(--ink-3)' }}>{KEYS[i]}</span>
          </div>
        ))}
      </div>
    );
  }

  // ─── React: MODAL (key-only) ─────────────────────────────────
  function KeyDetectModal({ file, onAccept, onReject, onClose }) {
    const [phase, setPhase] = _S('init');
    const [pct, setPct] = _S(0);
    const [result, setResult] = _S(null);
    const [chosen, setChosen] = _S(null); // {key, mode, camelot}
    const [err, setErr] = _S(null);

    _E(() => {
      let cancelled = false;
      (async () => {
        try {
          setPhase('decoding'); setPct(0.05);
          const r = await analyzeFile(file, (p, n) => {
            if (cancelled) return;
            setPhase(p); setPct(n);
          });
          if (cancelled) return;
          setResult(r);
          setChosen({ key: r.key, mode: r.mode, camelot: r.camelot });
          setPhase('done');
        } catch (e) {
          if (!cancelled) { setErr(e.message || 'Analysis failed'); setPhase('error'); }
        }
      })();
      return () => { cancelled = true; };
    }, [file]);

    const accept = () => { onAccept && onAccept({ ...result, ...chosen }); onClose && onClose(); };
    const reject = () => { onReject && onReject(); onClose && onClose(); };

    return (
      <div onClick={onClose} style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
      }}>
        <div onClick={e => e.stopPropagation()} style={{
          background: 'var(--paper)', border: '1px solid var(--rule)',
          width: 580, maxWidth: '92vw', padding: 28,
        }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
            <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.14em', color: 'var(--ink-3)' }}>KEY AUTO-DETECT</span>
            <button onClick={onClose} style={{ background: 'transparent', border: 0, color: 'var(--ink-3)', cursor: 'pointer', fontSize: 18 }}>×</button>
          </div>
          <div style={{ fontSize: 18, fontWeight: 500, marginBottom: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name}</div>
          <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 22 }}>
            Krumhansl-Schmuckler · 12-bin chromagram · analyzed locally
          </div>

          {phase !== 'done' && phase !== 'error' && (
            <div style={{ marginBottom: 22 }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>
                {phase.toUpperCase()} · {Math.round(pct * 100)}%
              </div>
              <div style={{ width: '100%', height: 4, background: 'var(--bg-2)' }}>
                <div style={{ height: '100%', width: `${pct * 100}%`, background: 'var(--ink)', transition: 'width 0.2s' }}/>
              </div>
            </div>
          )}

          {phase === 'error' && (
            <div style={{ padding: 16, border: '1px solid var(--rule)', background: 'var(--bg-2)', marginBottom: 22 }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: '#a32a18', marginBottom: 6 }}>ANALYSIS FAILED</div>
              <div style={{ fontSize: 12, color: 'var(--ink-2)' }}>{err}</div>
            </div>
          )}

          {phase === 'done' && result && result.key && (
            <>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginBottom: 4 }}>
                <span className="ff-mono num" style={{ fontSize: 56, fontWeight: 500, letterSpacing: '-0.02em', lineHeight: 1 }}>
                  {chosen.key} <span style={{ fontSize: 32, color: 'var(--ink-3)' }}>{chosen.mode === 'Major' ? 'maj' : 'min'}</span>
                </span>
                <span className="ff-mono" style={{ fontSize: 14, color: 'var(--ink-3)' }}>· Camelot {chosen.camelot}</span>
              </div>
              <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 22 }}>
                {Math.round(result.confidence * 100)}% confidence
              </div>

              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>CHROMA DISTRIBUTION</div>
              <div style={{ padding: 12, border: '1px solid var(--rule)', background: 'var(--bg-2)', marginBottom: 22 }}>
                <ChromaBar chroma={result.chroma} rootKey={chosen.key}/>
              </div>

              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>ALTERNATES</div>
              <div style={{ display: 'flex', gap: 6, marginBottom: 22, flexWrap: 'wrap' }}>
                {result.alternates.slice(0, 4).map((a, i) => {
                  const sel = chosen.key === a.key && chosen.mode === a.mode;
                  return (
                    <button key={i} onClick={() => setChosen({ key: a.key, mode: a.mode, camelot: a.camelot })} style={{
                      padding: '6px 12px', cursor: 'pointer',
                      background: sel ? 'var(--ink)' : 'transparent',
                      color: sel ? 'var(--bg)' : 'var(--ink)',
                      border: '1px solid ' + (sel ? 'var(--ink)' : 'var(--rule)'),
                      fontFamily: 'inherit',
                    }}>
                      <span className="ff-mono num" style={{ fontSize: 12, fontWeight: 500 }}>{a.key} {a.mode === 'Major' ? 'maj' : 'min'}</span>
                      <span className="ff-mono" style={{ fontSize: 9, opacity: 0.7, marginLeft: 6 }}>{a.camelot}</span>
                    </button>
                  );
                })}
              </div>

              <div style={{ display: 'flex', gap: 8 }}>
                <button onClick={accept} className="ff-mono upper" style={{
                  flex: 1, fontSize: 11, padding: '12px 18px', letterSpacing: '.14em',
                  background: 'var(--ink)', color: 'var(--bg)', border: 0, cursor: 'pointer',
                }}>SAVE {chosen.key} {chosen.mode === 'Major' ? 'MAJ' : 'MIN'}</button>
                <button onClick={reject} className="ff-mono upper" style={{
                  fontSize: 11, padding: '12px 18px', letterSpacing: '.14em',
                  background: 'transparent', color: 'var(--ink)', border: '1px solid var(--rule)', cursor: 'pointer',
                }}>SKIP</button>
              </div>
            </>
          )}
        </div>
      </div>
    );
  }

  // ─── React: COMBINED MODAL (Tempo + Key in one pass) ─────────
  function AudioAutoDetectModal({ file, onAccept, onReject, onClose }) {
    const [phase, setPhase] = _S('decoding');
    const [pct, setPct] = _S(0.05);
    const [tempoResult, setTempoResult] = _S(null);
    const [keyResult, setKeyResult] = _S(null);
    const [chosenBpm, setChosenBpm] = _S(null);
    const [chosenKey, setChosenKey] = _S(null);
    const [err, setErr] = _S(null);

    _E(() => {
      let cancelled = false;
      (async () => {
        try {
          // Decode once, share buffer between both analyzers
          const ctx = getAudioCtx();
          if (!ctx) throw new Error('Web Audio not supported');
          const buf = await file.arrayBuffer();
          if (cancelled) return;
          const audioBuffer = await new Promise((res, rej) => ctx.decodeAudioData(buf.slice(0), res, rej));
          if (cancelled) return;

          // Tempo first (faster)
          setPhase('tempo'); setPct(0.15);
          const t = await window.TempoDetect.analyzeBuffer(audioBuffer, (p, n) => {
            if (!cancelled) setPct(0.15 + n * 0.35);
          });
          if (cancelled) return;
          setTempoResult(t); setChosenBpm(t.bpm);

          // Then key
          setPhase('key'); setPct(0.55);
          const k = await analyzeBuffer(audioBuffer, (p, n) => {
            if (!cancelled) setPct(0.55 + n * 0.4);
          });
          if (cancelled) return;
          setKeyResult(k);
          setChosenKey({ key: k.key, mode: k.mode, camelot: k.camelot });
          setPhase('done'); setPct(1);
        } catch (e) {
          if (!cancelled) { setErr(e.message || 'Analysis failed'); setPhase('error'); }
        }
      })();
      return () => { cancelled = true; };
    }, [file]);

    const accept = () => {
      onAccept && onAccept({
        bpm: chosenBpm,
        bpmConfidence: tempoResult && tempoResult.confidence,
        halfTime: tempoResult && tempoResult.halfTime,
        key: chosenKey && chosenKey.key,
        mode: chosenKey && chosenKey.mode,
        camelot: chosenKey && chosenKey.camelot,
        keyConfidence: keyResult && keyResult.confidence,
      });
      onClose && onClose();
    };
    const reject = () => { onReject && onReject(); onClose && onClose(); };

    return (
      <div onClick={onClose} style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
      }}>
        <div onClick={e => e.stopPropagation()} style={{
          background: 'var(--paper)', border: '1px solid var(--rule)',
          width: 640, maxWidth: '92vw', padding: 28,
        }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
            <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.14em', color: 'var(--ink-3)' }}>AUDIO AUTO-DETECT · TEMPO + KEY</span>
            <button onClick={onClose} style={{ background: 'transparent', border: 0, color: 'var(--ink-3)', cursor: 'pointer', fontSize: 18 }}>×</button>
          </div>
          <div style={{ fontSize: 18, fontWeight: 500, marginBottom: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name}</div>
          <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 22 }}>
            {(file.size / 1024 / 1024).toFixed(2)} MB · onset autocorr + chromagram · all local
          </div>

          {phase !== 'done' && phase !== 'error' && (
            <div style={{ marginBottom: 22 }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>
                {phase.toUpperCase()} · {Math.round(pct * 100)}%
              </div>
              <div style={{ width: '100%', height: 4, background: 'var(--bg-2)' }}>
                <div style={{ height: '100%', width: `${pct * 100}%`, background: 'var(--ink)', transition: 'width 0.2s' }}/>
              </div>
            </div>
          )}

          {phase === 'error' && (
            <div style={{ padding: 16, border: '1px solid var(--rule)', background: 'var(--bg-2)', marginBottom: 22 }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: '#a32a18', marginBottom: 6 }}>ANALYSIS FAILED</div>
              <div style={{ fontSize: 12, color: 'var(--ink-2)' }}>{err}</div>
            </div>
          )}

          {phase === 'done' && tempoResult && keyResult && (
            <>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18, marginBottom: 22 }}>
                {/* Tempo */}
                <div style={{ border: '1px solid var(--rule)', padding: 18 }}>
                  <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)' }}>TEMPO</span>
                  <div className="ff-mono num" style={{ fontSize: 44, fontWeight: 500, letterSpacing: '-0.02em', lineHeight: 1, marginTop: 8, marginBottom: 4 }}>{chosenBpm}<span style={{ fontSize: 14, color: 'var(--ink-3)', marginLeft: 6 }}>BPM</span></div>
                  <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)', marginBottom: 12 }}>{Math.round(tempoResult.confidence * 100)}% conf · {tempoResult.halfTime ? 'half-time feel' : 'straight'}</div>
                  <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                    {tempoResult.candidates.map((c, i) => (
                      <button key={i} onClick={() => setChosenBpm(c.bpm)} className="ff-mono" style={{
                        fontSize: 11, padding: '4px 8px', cursor: 'pointer',
                        background: chosenBpm === c.bpm ? 'var(--ink)' : 'transparent',
                        color: chosenBpm === c.bpm ? 'var(--bg)' : 'var(--ink)',
                        border: '1px solid ' + (chosenBpm === c.bpm ? 'var(--ink)' : 'var(--rule)'),
                      }}>
                        {c.bpm} <span style={{ opacity: 0.6, fontSize: 8, marginLeft: 3 }}>{c.primary ? '' : c.kind === 'half' ? '½' : '×2'}</span>
                      </button>
                    ))}
                  </div>
                </div>

                {/* Key */}
                <div style={{ border: '1px solid var(--rule)', padding: 18 }}>
                  <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)' }}>KEY</span>
                  <div className="ff-mono num" style={{ fontSize: 44, fontWeight: 500, letterSpacing: '-0.02em', lineHeight: 1, marginTop: 8, marginBottom: 4 }}>
                    {chosenKey.key}<span style={{ fontSize: 22, color: 'var(--ink-3)', marginLeft: 4 }}>{chosenKey.mode === 'Major' ? 'maj' : 'min'}</span>
                  </div>
                  <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)', marginBottom: 12 }}>{Math.round(keyResult.confidence * 100)}% conf · Camelot {chosenKey.camelot}</div>
                  <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                    {keyResult.alternates.slice(0, 3).map((a, i) => {
                      const sel = chosenKey.key === a.key && chosenKey.mode === a.mode;
                      return (
                        <button key={i} onClick={() => setChosenKey({ key: a.key, mode: a.mode, camelot: a.camelot })} className="ff-mono" style={{
                          fontSize: 11, padding: '4px 8px', cursor: 'pointer',
                          background: sel ? 'var(--ink)' : 'transparent',
                          color: sel ? 'var(--bg)' : 'var(--ink)',
                          border: '1px solid ' + (sel ? 'var(--ink)' : 'var(--rule)'),
                        }}>
                          {a.key}{a.mode === 'Major' ? 'maj' : 'min'}
                        </button>
                      );
                    })}
                  </div>
                </div>
              </div>

              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>CHROMA · 12 PITCH CLASSES</div>
              <div style={{ padding: 12, border: '1px solid var(--rule)', background: 'var(--bg-2)', marginBottom: 22 }}>
                <ChromaBar chroma={keyResult.chroma} rootKey={chosenKey.key}/>
              </div>

              <div style={{ display: 'flex', gap: 8 }}>
                <button onClick={accept} className="ff-mono upper" style={{
                  flex: 1, fontSize: 11, padding: '12px 18px', letterSpacing: '.14em',
                  background: 'var(--ink)', color: 'var(--bg)', border: 0, cursor: 'pointer',
                }}>SAVE {chosenBpm} BPM · {chosenKey.key} {chosenKey.mode === 'Major' ? 'MAJ' : 'MIN'}</button>
                <button onClick={reject} className="ff-mono upper" style={{
                  fontSize: 11, padding: '12px 18px', letterSpacing: '.14em',
                  background: 'transparent', color: 'var(--ink)', border: '1px solid var(--rule)', cursor: 'pointer',
                }}>SKIP</button>
              </div>
            </>
          )}
        </div>
      </div>
    );
  }

  // ─── Public ──────────────────────────────────────────────────
  window.KeyDetect = { analyzeFile, analyzeBuffer };
  window.KeyDetectBadge = KeyDetectBadge;
  window.KeyDetectModal = KeyDetectModal;
  window.AudioAutoDetectModal = AudioAutoDetectModal;

  console.log('[KeyDetect] loaded · Krumhansl-Schmuckler · 24 keys · Camelot wheel');
})();
