// ============ DECK EMPTY PORTAL ============
// "Mix anything on the internet" empty state. Replaces the old single-purpose
// drop zone with a source picker that advertises the breadth of djanything.com.
// Each row is a real, working source: paste a URL → YouTube/SoundCloud; click →
// file picker, demos, or Spotify panel. The deck letter sits in the corner; the
// whole card is one tab-stop on the URL field, with discrete buttons below.
function DeckEmptyPortal({ side, onPickFile, onPasteUrl, onLoadDemos, loadingDemos, onOpenSpotify, queueHasTracks, onLoadFromQueue, queue }) {
  const [url, setUrl] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const submit = async () => {
    const v = url.trim();
    if (!v || submitting) return;
    setSubmitting(true);
    try { await onPasteUrl?.(v); setUrl(''); }
    finally { setSubmitting(false); }
  };
  // Auto-detect source from the URL the user typed/pasted, so the YouTube /
  // SoundCloud row visibly highlights as they paste — making the "mix the
  // internet" promise feel responsive, not just declarative.
  const detected =
    /youtube\.com|youtu\.be/i.test(url) ? 'youtube' :
    /soundcloud\.com/i.test(url) ? 'soundcloud' :
    /spotify\.com/i.test(url) ? 'spotify' : null;

  // Up to 4 ready tracks already analyzed in the queue → 1-click load
  const readyTracks = (queue || [])
    .filter(q => q.buffer && !q.loading && !q.analyzing && q.bpm)
    .slice(0, 4);

  return (
    <div className={`deck-portal deck-portal-${side}`}>
      <div className="deck-portal-letter">{side.toUpperCase()}</div>

      <div className="deck-portal-headline">
        What should Deck {side.toUpperCase()} play?
      </div>
      <div className="deck-portal-tagline">The internet is your record crate.</div>

      {/* Unified URL field — paste from anywhere on the web */}
      <form
        className={`deck-portal-url ${detected || ''}`}
        onSubmit={(e) => { e.preventDefault(); submit(); }}
      >
        <span className="dpu-icon" aria-hidden="true">
          {detected === 'youtube' ? <YouTubeMark /> :
           detected === 'soundcloud' ? <SoundCloudMark /> :
           detected === 'spotify' ? <SpotifyMark /> :
           <SearchMark />}
        </span>
        <input
          type="text"
          placeholder="Paste a YouTube or SoundCloud link…"
          value={url}
          onChange={(e) => setUrl(e.target.value)}
          aria-label={`Paste a track URL for Deck ${side.toUpperCase()}`}
        />
        <button
          type="submit"
          className="dpu-go"
          disabled={!url.trim() || submitting}
        >
          {submitting ? 'Loading…' : 'Load →'}
        </button>
      </form>

      {/* Source rows: each one a real wired-up affordance */}
      <div className="deck-portal-sources">
        <SourceRow
          color="#FF0033"
          icon={<YouTubeMark />}
          name="YouTube"
          note="Beat-matched"
        />
        <SourceRow
          color="#FF7700"
          icon={<SoundCloudMark />}
          name="SoundCloud"
          note="Streaming"
        />
        <SourceRow
          color="#1DB954"
          icon={<SpotifyMark />}
          name="Spotify"
          note="Browse library"
          onClick={onOpenSpotify}
          clickable
        />
        <SourceRow
          color="#1ee0ff"
          icon={<UploadMark />}
          name="Upload"
          note="MP3 / WAV"
          onClick={onPickFile}
          clickable
        />
      </div>

      {/* Footer: ready-now suggestions OR demo loader */}
      <div className="deck-portal-suggest">
        {readyTracks.length > 0 ? (
          <>
            <div className="dps-label">Ready in your queue</div>
            <div className="dps-tiles">
              {readyTracks.map((tr) => (
                <button
                  key={tr.id}
                  className="dps-tile"
                  onClick={() => onLoadFromQueue?.(side, tr)}
                  title={`Load "${tr.title}" on Deck ${side.toUpperCase()}`}
                >
                  {tr.thumb
                    ? <img src={tr.thumb} alt="" />
                    : <div className="dps-tile-placeholder" />}
                  <div className="dps-tile-meta">
                    <div className="dps-tile-title">{tr.title}</div>
                    <div className="dps-tile-bpm">{tr.bpm ? `${Math.round(tr.bpm)} BPM` : '—'}</div>
                  </div>
                </button>
              ))}
            </div>
          </>
        ) : (
          <button
            className="dps-demos"
            onClick={onLoadDemos}
            disabled={loadingDemos}
          >
            <Icon name="sparkles" size={12} />
            <span>{loadingDemos ? 'Loading demos…' : 'Try 4 demo tracks'}</span>
          </button>
        )}
      </div>
    </div>
  );
}

// Brand marks (drawn inline so we don't depend on icon names that don't exist)
const YouTubeMark = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
    <path d="M23 7.5s-.2-1.5-.9-2.2c-.8-.9-1.8-.9-2.2-1C17 4 12 4 12 4s-5 0-7.9.3c-.4 0-1.4.1-2.2 1C1.2 6 1 7.5 1 7.5S.8 9.3.8 11.1v1.6c0 1.8.2 3.6.2 3.6s.2 1.5.9 2.2c.8.9 1.9.9 2.4 1 1.7.2 7.7.3 7.7.3s5 0 7.9-.3c.4 0 1.4-.1 2.2-1 .7-.7.9-2.2.9-2.2s.2-1.8.2-3.6v-1.6c0-1.8-.2-3.6-.2-3.6zM9.7 14.6V8.4l6.4 3.1-6.4 3.1z"/>
  </svg>
);
const SoundCloudMark = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
    <path d="M2 14v3M4 12v5M6 11v6M8 10v7M10 9v8M12 8v9M14 7v10c4 0 7-2 7-5s-3-5-7-5z"
          stroke="currentColor" strokeWidth="1.6" fill="none" strokeLinecap="round"/>
  </svg>
);
const SpotifyMark = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
    <circle cx="12" cy="12" r="10"/>
    <path d="M7 9c3-1 8-1 11 1M7.5 12.5c2.5-.7 6.5-.5 9 1M8 16c2-.5 5-.4 7 .8"
          stroke="#000" strokeWidth="1.6" fill="none" strokeLinecap="round"/>
  </svg>
);
const UploadMark = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
       strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
    <polyline points="17 8 12 3 7 8"/>
    <line x1="12" y1="3" x2="12" y2="15"/>
  </svg>
);
const SearchMark = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
       strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <circle cx="11" cy="11" r="7"/>
    <line x1="21" y1="21" x2="16.65" y2="16.65"/>
  </svg>
);

function SourceRow({ color, icon, name, note, onClick, clickable }) {
  const Tag = clickable ? 'button' : 'div';
  return (
    <Tag
      className={`source-row ${clickable ? 'clickable' : ''}`}
      onClick={onClick}
      style={{ '--source-color': color }}
      type={clickable ? 'button' : undefined}
    >
      <span className="sr-icon" style={{ color }}>{icon}</span>
      <span className="sr-name">{name}</span>
      <span className="sr-note">{note}</span>
      {clickable && <span className="sr-arrow">→</span>}
    </Tag>
  );
}

// ============ DECK ============
function Deck({ side, state, onChange, otherState, onOtherChange, automix, isMaster, masterPinned, onSetMaster, onLoadFiles, queue, onLoadFromQueue, quantize, phaseErrMs, onBeatJump, onAlignNow, onYoutubeUrl, onLoadDemos, loadingDemos, onOpenSpotify }) {
  const isA = side === 'a';
  const [fxOpen, setFxOpen] = useState(true);
  const [loopsOpen, setLoopsOpen] = useState(true);
  const [advOpen, setAdvOpen] = useState(false); // ⚙ advanced tray (grid-nudge + ±4bar jumps + key-sync hint)
  const [dropActive, setDropActive] = useState(false);
  const fileInputRef = useRef(null);
  // Auto-pop the advanced tray when phase drift is sustained > 25ms (likely bad grid)
  const driftRef = useRef({ since: 0, last: 0 });
  useEffect(() => {
    if (phaseErrMs == null || !state.synced) { driftRef.current.since = 0; return; }
    const now = Date.now();
    if (Math.abs(phaseErrMs) > 25) {
      if (!driftRef.current.since) driftRef.current.since = now;
      else if (now - driftRef.current.since > 2000 && !advOpen) {
        setAdvOpen(true); // hint to user: open the grid-nudge tools
        driftRef.current.since = 0;
      }
    } else {
      driftRef.current.since = 0;
    }
  }, [phaseErrMs, state.synced, advOpen]);

  const t = state.track;
  const empty = !t;

  // Tap tempo: collect timestamps, derive BPM from intervals.
  const tapTimesRef = useRef([]);
  const tapTimeoutRef = useRef(null);
  const [tapPreview, setTapPreview] = useState(null);
  const handleTap = () => {
    if (!t) return;
    const now = performance.now();
    const arr = tapTimesRef.current;
    // Reset if last tap was >2s ago (new sequence)
    if (arr.length && now - arr[arr.length - 1] > 2000) arr.length = 0;
    arr.push(now);
    // Need at least 3 taps (2 intervals) to estimate.
    if (arr.length >= 3) {
      // Use median interval over the most recent up to 8 taps for stability.
      const recent = arr.slice(-8);
      const intervals = [];
      for (let i = 1; i < recent.length; i++) intervals.push(recent[i] - recent[i - 1]);
      intervals.sort((a, b) => a - b);
      const med = intervals[Math.floor(intervals.length / 2)];
      let bpm = 60000 / med;
      // Clamp to sensible range
      while (bpm < 70) bpm *= 2;
      while (bpm > 200) bpm /= 2;
      setTapPreview(Math.round(bpm * 10) / 10);
    }
    // Auto-commit after 2s of no taps
    if (tapTimeoutRef.current) clearTimeout(tapTimeoutRef.current);
    tapTimeoutRef.current = setTimeout(() => {
      const a = tapTimesRef.current;
      if (a.length >= 3) {
        const recent = a.slice(-8);
        const intervals = [];
        for (let i = 1; i < recent.length; i++) intervals.push(recent[i] - recent[i - 1]);
        intervals.sort((x, y) => x - y);
        const med = intervals[Math.floor(intervals.length / 2)];
        let bpm = 60000 / med;
        while (bpm < 70) bpm *= 2;
        while (bpm > 200) bpm /= 2;
        bpm = Math.round(bpm * 10) / 10;
        onChange({ ...state, track: { ...t, bpm, __origBpm: bpm } });
      }
      tapTimesRef.current = [];
      setTapPreview(null);
    }, 1800);
  };
  const halveBpm = () => {
    if (!t || !t.bpm) return;
    const next = Math.round((t.bpm / 2) * 10) / 10;
    onChange({ ...state, track: { ...t, bpm: next, __origBpm: next } });
  };
  const doubleBpm = () => {
    if (!t || !t.bpm) return;
    const next = Math.round((t.bpm * 2) * 10) / 10;
    onChange({ ...state, track: { ...t, bpm: next, __origBpm: next } });
  };

  // ---- Deck-targeted drop zone ----
  // Accepts: (1) audio files dropped from OS, (2) queue-track drags.
  const handleDeckDrop = (e) => {
    e.preventDefault();
    setDropActive(false);
    const files = e.dataTransfer.files;
    const trackId =
      e.dataTransfer.getData('application/x-djanything-track-id') ||
      e.dataTransfer.getData('text/x-djanything-track-id') ||
      e.dataTransfer.getData('text/x-mixfm-track-id');
    if (files && files.length) {
      onLoadFiles?.(side, files);
    } else if (trackId && queue) {
      const tr = queue.find(q => q.id === trackId);
      if (tr) onLoadFromQueue?.(side, tr);
    }
  };
  const handleDeckDragOver = (e) => {
    // Only react to drags carrying audio files OR our queue mime type.
    const dt = e.dataTransfer;
    const hasFile = dt.types?.includes('Files');
    const hasTrack =
      dt.types?.includes('application/x-djanything-track-id') ||
      dt.types?.includes('text/x-djanything-track-id') ||
      dt.types?.includes('text/x-mixfm-track-id');
    if (!hasFile && !hasTrack) return;
    e.preventDefault();
    dt.dropEffect = 'copy';
    if (!dropActive) setDropActive(true);
  };
  const handleDeckDragLeave = (e) => {
    // Only deactivate when leaving the deck root, not crossing children.
    if (e.currentTarget.contains(e.relatedTarget)) return;
    setDropActive(false);
  };
  const openPicker = () => fileInputRef.current?.click();

  const updateEQ = (k, v) => onChange({ ...state, eq: { ...state.eq, [k]: v } });
  const updateFilter = (v) => onChange({ ...state, filter: v });
  const updateFx = (fx, patch) => onChange({ ...state, fx: { ...state.fx, [fx]: { ...state.fx[fx], ...patch } } });

  const togglePlay = () => onChange({ ...state, playing: !state.playing });
  const setCue = () => {
    if (empty) return;
    if (state.cue == null || Math.abs(state.cue - state.progress) > 0.001) {
      onChange({ ...state, cue: state.progress, playing: false });
    } else {
      onChange({ ...state, progress: state.cue, playing: true });
    }
  };
  const syncBlocked = state.track?.source === 'soundcloud' || otherState.track?.source === 'soundcloud';
  const syncWaiting = !!(state.track?.analyzing || otherState.track?.analyzing);
  const syncMissingBpm = !!(state.track && otherState.track && (!state.track.bpm || !otherState.track.bpm));
  const syncDisabled = empty || !otherState.track || syncBlocked || syncWaiting || syncMissingBpm;
  const toggleSync = (e) => {
    if (syncDisabled) return;
    const next = !state.synced;
    // Shift+SYNC = key-sync mode: tempo-match AND adjust playback rate so the
    // resulting pitch lands on the master's key. We flag it on the deck state;
    // the rate effect picks it up.
    const wantKeySync = !!(e && e.shiftKey);

    // If user has pinned an explicit master, respect it: only the NON-master
    // deck can be a slave. Tapping SYNC on the master is a no-op.
    if (next && masterPinned) return;

    // Auto-master rule: the LIVE (playing) deck stays master. The deck being
    // cued adapts. If user hits SYNC on the live deck while the other is paused,
    // flip — slave the OTHER deck so the live track's tempo/pitch never changes.
    if (next && state.playing && !otherState.playing) {
      if (onOtherChange) onOtherChange({ ...otherState, synced: true, keySync: wantKeySync });
      onChange({ ...state, synced: false, keySync: false });
      return;
    }

    // Normal case: this deck becomes slave; ensure other isn't also synced
    onChange({ ...state, synced: next, keySync: next ? wantKeySync : false });
    if (next && otherState.synced && onOtherChange) {
      onOtherChange({ ...otherState, synced: false, keySync: false });
    }
  };
  const toggleKeylock = () => {
    onChange({ ...state, keylock: !state.keylock });
  };

  // Rate / semitone preview when SYNC is engaged
  const syncInfo = useMemo(() => {
    if (!state.synced || !state.track || !otherState.track) return null;
    const rate = otherState.track.bpm / state.track.bpm;
    const semis = 12 * Math.log2(rate);
    // Key clash detection (Camelot)
    const ka = state.track.key, kb = otherState.track.key;
    let clash = null;
    const m1 = ka?.match(/(\d+)([AB])/), m2 = kb?.match(/(\d+)([AB])/);
    if (m1 && m2) {
      if (ka === kb) clash = 'perfect';
      else {
        const diff = Math.abs(parseInt(m1[1]) - parseInt(m2[1]));
        const letterSame = m1[2] === m2[2];
        if (letterSame && (diff <= 1 || diff === 11)) clash = 'compatible';
        else if (!letterSame && diff === 0) clash = 'compatible';
        else clash = 'clash';
      }
    }
    return { rate, semis, clash };
  }, [state.synced, state.track, otherState.track]);

  const setLoopLen = (bars) => {
    if (empty || !t) return;
    if (state.loop && state.loop.bars === bars) {
      onChange({ ...state, loop: null });
      return;
    }
    // --- Beat-grid quantization (like Rekordbox Q) ---
    // Snap the loop start to the nearest beat ahead of the playhead,
    // so the loop always begins on a grid line.
    const bpm = t.bpm || 120;
    const durSec = t.duration || 180;
    const firstBeat = t.firstBeat || 0; // seconds offset of 1st beat
    const beatSec = 60 / bpm;
    const barSec = 4 * beatSec;
    const loopSec = bars * barSec; // 1 bar = 4 beats

    // Current playhead in seconds relative to first beat
    const playSec = state.progress * durSec;
    const sinceFirst = playSec - firstBeat;
    // Snap to division (or beat as fallback). With quantize OFF, no snap.
    const qOn = quantize?.on !== false;
    const divBeats = qOn ? ((quantize?.division || 0.25) * 4) : 0; // beats per division
    let startSec;
    if (!qOn || divBeats === 0) {
      startSec = playSec;
    } else {
      const stepSec = divBeats * beatSec;
      const stepIdx = sinceFirst / stepSec;
      const nearest = Math.round(stepIdx);
      const snapped = (Math.abs(stepIdx - nearest) * stepSec < 0.025) ? nearest : Math.ceil(stepIdx);
      startSec = firstBeat + snapped * stepSec;
    }
    const endSec = Math.min(durSec, startSec + loopSec);
    const start = startSec / durSec;
    const end = endSec / durSec;
    onChange({
      ...state,
      loop: { start, end, bars, beats: bars * 4, startSec, endSec },
      progress: start,
    });
  };

  // ---- Instant beat-loop: starts at NOW, runs for N beats, no quantize wait ----
  const triggerBeatLoop = (beats) => {
    if (empty || !t) return;
    if (state.loop && state.loop.beats === beats) {
      onChange({ ...state, loop: null });
      return;
    }
    const bpm = t.bpm || 120;
    const durSec = t.duration || 180;
    const beatSec = 60 / bpm;
    const playSec = state.progress * durSec;
    const startSec = playSec; // no quantize on beat loops — they fire instantly
    const endSec = Math.min(durSec, startSec + beats * beatSec);
    onChange({
      ...state,
      loop: { start: startSec / durSec, end: endSec / durSec, bars: beats / 4, beats, startSec, endSec, instant: true },
    });
  };

  const fmtTime = (p) => {
    if (!t) return '--:--';
    const total = t.duration || 210;
    const secs = Math.floor(p * total);
    const m = Math.floor(secs / 60);
    const s = secs % 60;
    return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
  };

  const dbLabel = (v) => {
    const db = (v - 0.5) * 24;
    return `${db >= 0 ? '+' : ''}${db.toFixed(1)} dB`;
  };
  const filterLabel = (v) => {
    if (Math.abs(v - 0.5) < 0.03) return 'OFF';
    return v < 0.5 ? `LP ${Math.round((0.5 - v) * 200)}%` : `HP ${Math.round((v - 0.5) * 200)}%`;
  };

  const effectiveBpm = state.synced && otherState.track ? otherState.track.bpm : (t?.bpm || 0);

  return (
    <div
      className={`panel deck ${side} ${dropActive ? 'drop-active' : ''} ${empty ? 'is-empty' : ''}`}
      data-screen-label={`Deck ${side.toUpperCase()}`}
      onDragOver={handleDeckDragOver}
      onDragLeave={handleDeckDragLeave}
      onDrop={handleDeckDrop}
    >
      <input
        ref={fileInputRef}
        type="file"
        accept="audio/*"
        multiple
        style={{ display: 'none' }}
        onChange={(e) => { if (e.target.files?.length) onLoadFiles?.(side, e.target.files); e.target.value = ''; }}
      />
      <div className="deck-drop-overlay" aria-hidden="true">
        <div className="deck-drop-overlay-inner">
          <div className="deck-drop-icon">⤓</div>
          <div className="deck-drop-text">DROP TO LOAD ON DECK {side.toUpperCase()}</div>
        </div>
      </div>
      {empty ? (
        // ---- "Mix the internet" empty state ----
        // Beginners shouldn't see a generic "load a file" prompt. They should
        // see the breadth of sources djanything.com unifies — YouTube, SoundCloud,
        // Spotify, files — as a clickable picker, with paste-anywhere as a
        // first-class action. Each row shows the source's brand color and an
        // honest one-line capability note.
        <DeckEmptyPortal
          side={side}
          onPickFile={openPicker}
          onPasteUrl={(url) => onYoutubeUrl?.(url)}
          onLoadDemos={onLoadDemos}
          loadingDemos={loadingDemos}
          onOpenSpotify={onOpenSpotify}
          queueHasTracks={queue && queue.length > 0}
          onLoadFromQueue={onLoadFromQueue}
          queue={queue}
        />
      ) : (
        <>
          <div className="deck-head">
            <div className="deck-label">{side.toUpperCase()}</div>
            <div className="deck-track">
              <div className="deck-title">{t.title}</div>
              <div className="deck-artist">{t.artist}</div>
            </div>
            <div className="deck-meta">
              <div
                className={`pill bpm ${state.synced && t?.bpm && Math.abs(effectiveBpm - t.bpm) > 0.05 ? 'shifted' : ''}`}
                title={
                  state.synced && t?.bpm && Math.abs(effectiveBpm - t.bpm) > 0.05
                    ? `Synced to ${effectiveBpm.toFixed(1)} BPM (track was ${t.bpm.toFixed(1)})`
                    : 'Tempo of this deck'
                }
              >
                {effectiveBpm ? effectiveBpm.toFixed(1) : '---'} <span style={{opacity:0.6, fontSize:'0.7em'}}>BPM</span>
                {state.synced && t?.bpm && Math.abs(effectiveBpm - t.bpm) > 0.05 && (
                  <span className="bpm-was">← {t.bpm.toFixed(1)}</span>
                )}
              </div>
              <div className="pill key">{t?.key || '--'}</div>
            </div>
          </div>

          <Waveform
            trackId={`${side}-${t?.id || 'empty'}`}
            progress={state.progress}
            onScrub={(p) => onChange({ ...state, progress: p })}
            cue={state.cue}
            loop={state.loop}
            seed={t?.seed || 0}
            empty={empty}
            time={fmtTime(state.progress)}
            duration={fmtTime(1)}
            bpm={effectiveBpm}
            firstBeat={t?.firstBeat || 0}
          />
        </>
      )}

      {/* All deck controls below are hidden when empty — the portal IS the deck
          when there's no track. As soon as a track lands, the full mixer
          surface appears. */}
      {!empty && (<>

      {/* ===== Transport row 1 — PRIMARY: PLAY, CUE, MATCH, MASTER, Fix beats ===== */}
      <div className="transport">
        <Tip text={state.playing ? 'Pause the track.' : 'Start playing the track.'}>
          <button className={`t-btn play ${state.playing ? 'playing' : ''}`} onClick={togglePlay} disabled={empty}>
            <Icon name={state.playing ? 'pause' : 'play'} size={16} />
            {state.playing ? 'PLAYING' : 'PLAY'}
          </button>
        </Tip>
        <Tip text={<><strong>Cue point</strong>. Pause and click here to mark the spot. Hold this while paused to preview that spot. Click while paused to jump back to it.</>}>
          <button className="t-btn cue" onClick={setCue} disabled={empty}>
            <Icon name="cue" size={14} /> CUE
          </button>
        </Tip>
        <Tip text={syncBlocked
          ? 'Beat-matching is tempo-lock only for Web Audio tracks. Try a YouTube link or upload an MP3.'
          : syncWaiting
            ? 'Beat analysis is still running. MATCH unlocks when the BPM and beat grid are ready.'
          : syncMissingBpm
            ? 'This track needs a detected BPM before it can tempo-lock.'
          : (state.synced
            ? (state.keySync ? <>Beats <em>and</em> keys are matched to the other deck. Click to unlock.</> : <>Beats matched to the other deck. Click to unlock.</>)
            : <>Auto-match this song's tempo to the other deck. Hold <strong>Shift</strong> to also match the musical key.</>)}>
          <button
            className={`t-btn sync ${state.synced ? 'active' : ''} ${state.keySync ? 'keysync' : ''}`}
            onClick={(e) => toggleSync(e)}
            disabled={syncDisabled}
          >
            {!state.synced && <Icon name="sync" size={11} />} {state.synced ? (state.keySync ? 'KEY+SYNC' : 'SYNCED') : 'MATCH'}
          </button>
        </Tip>
        <Tip text={masterPinned
          ? 'This deck is locked as the master. Other decks follow its tempo. Click to unlock.'
          : (isMaster
            ? 'This deck is the master right now (the other deck syncs to it). Click to lock it as master.'
            : 'Make this deck the master. Other decks will tempo-match this one when you hit MATCH.')}>
          <button
            className={`t-btn master-btn ${isMaster ? 'active' : ''} ${masterPinned ? 'pinned' : ''}`}
            onClick={onSetMaster}
            disabled={empty}
          >
            {isMaster ? '◆ MASTER' : 'MASTER'}
          </button>
        </Tip>
        <Tip text={
          (state.synced && Math.abs(phaseErrMs || 0) > 25)
            ? <>Beats are <strong>off by {Math.abs(phaseErrMs).toFixed(0)}ms</strong>. Click to auto-snap and open the manual fix tools (Tap, Half/Double, beat-1 nudge).</>
            : <>Open the manual fix tools — Tap along, Half / Double, or nudge beat 1. Auto-snap also runs if synced.</>
        }>
          <button
            className={`fixbeats-trigger ${(state.synced && Math.abs(phaseErrMs || 0) > 25) ? 'alert' : ''} ${advOpen ? 'open' : ''}`}
            onClick={() => {
              const wasOpen = advOpen;
              setAdvOpen(!wasOpen);
              if (!wasOpen && state.synced && onAlignNow) onAlignNow();
            }}
            disabled={empty || !t || t.analyzing}
            aria-label="Toggle fix beats panel"
          >
            <span className="fb-dot" />
            {(state.synced && Math.abs(phaseErrMs || 0) > 25)
              ? <>Fix beats <span className="fb-amt">({Math.abs(phaseErrMs).toFixed(0)}ms)</span></>
              : 'Fix beats'}
          </button>
        </Tip>
      </div>

      {/* ===== Transport row 2 — SECONDARY: beat-aligned skip ===== */}
      <div className="transport-skip">
        <span className="ts-label">Skip</span>
        <Tip text="Skip back 4 bars (16 beats), staying on the beat.">
          <button className="ts-btn" onClick={() => onBeatJump?.(-16)} disabled={empty || !t || t.analyzing} aria-label="Jump back 4 bars">← 4 bars</button>
        </Tip>
        <Tip text="Skip back 1 bar (4 beats), staying on the beat.">
          <button className="ts-btn" onClick={() => onBeatJump?.(-4)} disabled={empty || !t || t.analyzing} aria-label="Jump back 1 bar">← 1 bar</button>
        </Tip>
        <Tip text="Skip forward 1 bar (4 beats), staying on the beat.">
          <button className="ts-btn" onClick={() => onBeatJump?.(4)} disabled={empty || !t || t.analyzing} aria-label="Jump forward 1 bar">1 bar →</button>
        </Tip>
        <Tip text="Skip forward 4 bars (16 beats), staying on the beat.">
          <button className="ts-btn" onClick={() => onBeatJump?.(16)} disabled={empty || !t || t.analyzing} aria-label="Jump forward 4 bars">4 bars →</button>
        </Tip>
      </div>

      {/* ===== Sync status — single friendly pill by default; details on hover/expand ===== */}
      {syncInfo && (
        <div className={`sync-bar ${syncInfo.clash || ''} ${state.synced ? 'on' : 'off'}`}>
          <div className="sync-bar-main">
            {state.synced ? (
              <span className={`sync-state lock-${state.synced && phaseErrMs != null ? (Math.abs(phaseErrMs) < 5 ? 'tight' : Math.abs(phaseErrMs) < 25 ? 'near' : 'drift') : 'tight'}`}>
                <span className="sync-led" />
                {state.synced && phaseErrMs != null && Math.abs(phaseErrMs) >= 25 ? 'Beats drifting' : 'Beats matched'}
              </span>
            ) : (
              <span className="sync-state idle">Tap MATCH to lock beats</span>
            )}
            {syncInfo.clash === 'perfect' && <span className="sync-key good">♪ Same key</span>}
            {syncInfo.clash === 'compatible' && <span className="sync-key good">♪ Sounds good together</span>}
            {syncInfo.clash === 'clash' && <span className="sync-key bad" title={`The two songs are in keys that often clash (${t?.key} vs ${otherState.track?.key}). Hold Shift while clicking MATCH to also adjust pitch.`}>⚠ Keys clash</span>}
          </div>
          {/* Detail chips — collapsed unless tray open */}
          {advOpen && (
            <div className="sync-bar-detail">
              <span className="sync-chip rate">×{syncInfo.rate.toFixed(3)}</span>
              <span className={`sync-chip pitch ${Math.abs(syncInfo.semis) > 2 ? 'warn' : ''}`}>
                {syncInfo.semis >= 0 ? '+' : ''}{syncInfo.semis.toFixed(2)} st
              </span>
              {state.synced && phaseErrMs != null && (
                <span
                  className={`sync-chip phase ${Math.abs(phaseErrMs) < 5 ? 'lock' : Math.abs(phaseErrMs) < 25 ? 'near' : 'drift'}`}
                  title="How perfectly the beats are lined up right now, in milliseconds. Under 5ms = rock-solid lock. Over 25ms = drifting (try the Beatgrid tools)."
                >
                  Δ {phaseErrMs >= 0 ? '+' : ''}{phaseErrMs.toFixed(0)}ms
                </span>
              )}
              <button
                className={`sync-chip keylock-btn ${state.keylock ? 'on' : ''}`}
                onClick={toggleKeylock}
                title={state.keylock
                  ? 'Pitch lock is ON. The song stays in its original key even when you change its speed. Most songs sound better this way.'
                  : 'Pitch lock is OFF. Speeding up the song also raises its pitch (vinyl-style). Click to turn pitch lock back on.'}
              >
                {state.keylock ? '🔒 KEYLOCK' : '🔓 FREE PITCH'}
              </button>
            </div>
          )}
        </div>
      )}

      {/* ===== "Fix beats" panel — replaces the dense ⚙ tray. ===== */}
      {advOpen && !empty && t && !t.analyzing && (
        <div className="fixbeats-panel">
          <div className="fixbeats-header">
            <div className="fixbeats-title">Fix beats</div>
            <Tip text="Close this panel.">
              <button className="fixbeats-close" onClick={() => setAdvOpen(false)} aria-label="Close">×</button>
            </Tip>
          </div>

          {/* CARD 1 — BPM */}
          <div className="fb-card">
            <div className="fb-card-q">Is the BPM right?</div>
            <div className="fb-card-hint">
              If the song feels too slow or too fast, the auto-detector probably doubled or halved the tempo. Use Half / Double, or tap along with the kick.
            </div>
            <div className="fb-card-actions">
              <Tip text={<>Halve the BPM. Use this if the track sounds <strong>twice as fast</strong> as the number says (e.g. 174 → 87).</>}>
                <button className="fb-btn" onClick={halveBpm} disabled={empty || !t?.bpm}>Half</button>
              </Tip>
              <Tip text={<>Double the BPM. Use this if the track sounds <strong>twice as slow</strong> as the number says (e.g. 87 → 174).</>}>
                <button className="fb-btn" onClick={doubleBpm} disabled={empty || !t?.bpm}>Double</button>
              </Tip>
              <Tip text={<>Tap this button along with the kick drum 4 or more times. The BPM is set automatically about 2 seconds after your last tap.</>}>
                <button
                  className={`fb-btn primary ${tapPreview ? 'tapping' : ''}`}
                  onClick={handleTap}
                  disabled={empty}
                >Tap along{tapPreview != null ? ` · ${tapPreview.toFixed(1)}` : ''}</button>
              </Tip>
              <span className="fb-readout">Now: <strong>{(t?.bpm || 0).toFixed(1)}</strong> BPM</span>
            </div>
          </div>

          {/* CARD 2 — Beat 1 position */}
          <div className="fb-card">
            <div className="fb-card-q">Is beat 1 in the right place?</div>
            <div className="fb-card-hint">
              The yellow lines on the waveform mark where each bar starts. They should land on the kick drum. If they're off, nudge them or pause on a kick and mark it.
            </div>
            <div className="fb-card-actions">
              <Tip text="Shift beat 1 fifty milliseconds earlier (big step).">
                <button className="fb-btn icon-btn" onClick={() => onChange({ ...state, track: { ...t, firstBeat: Math.max(0, (t.firstBeat || 0) - 0.05) } })}>«</button>
              </Tip>
              <Tip text="Shift beat 1 five milliseconds earlier (fine step).">
                <button className="fb-btn icon-btn" onClick={() => onChange({ ...state, track: { ...t, firstBeat: Math.max(0, (t.firstBeat || 0) - 0.005) } })}>‹</button>
              </Tip>
              <Tip text="Shift beat 1 five milliseconds later (fine step).">
                <button className="fb-btn icon-btn" onClick={() => onChange({ ...state, track: { ...t, firstBeat: (t.firstBeat || 0) + 0.005 } })}>›</button>
              </Tip>
              <Tip text="Shift beat 1 fifty milliseconds later (big step).">
                <button className="fb-btn icon-btn" onClick={() => onChange({ ...state, track: { ...t, firstBeat: (t.firstBeat || 0) + 0.05 } })}>»</button>
              </Tip>
              <Tip text={<>Pause the song with the playhead on a kick drum, then click this. <strong>That moment becomes beat 1</strong> and the whole grid snaps to it.</>}>
                <button
                  className="fb-btn primary"
                  onClick={() => onChange({ ...state, track: { ...t, firstBeat: state.progress * (t.duration || 0) } })}
                >Mark beat 1 here</button>
              </Tip>
              <span className="fb-readout">Now: <strong>{(((t.firstBeat || 0) * 1000) | 0)}ms</strong></span>
            </div>
            {t.analyzeConfidence != null && t.analyzeConfidence < 0.3 && (
              <div className="fb-warn">⚠ Auto-detect was unsure on this track — please double-check the grid.</div>
            )}
          </div>
        </div>
      )}

      <div className="knob-row">
        <Knob value={state.eq.high} onChange={(v) => updateEQ('high', v)} label="HIGH" displayValue={dbLabel(state.eq.high)} colorHue={350} />
        <Knob value={state.eq.mid} onChange={(v) => updateEQ('mid', v)} label="MID" displayValue={dbLabel(state.eq.mid)} colorHue={295} />
        <Knob value={state.eq.low} onChange={(v) => updateEQ('low', v)} label="LOW" displayValue={dbLabel(state.eq.low)} colorHue={200} />
        <Knob value={state.filter} onChange={updateFilter} label="FILTER" displayValue={filterLabel(state.filter)} colorHue={150} className="filter" />
      </div>

      <div className="section-head">
        <div className="section-title">Loops</div>
        <button className="collapse-btn" onClick={() => setLoopsOpen(!loopsOpen)} aria-label={loopsOpen ? 'Hide loops' : 'Show loops'} aria-expanded={loopsOpen}>
          <Icon name={loopsOpen ? 'chevron-up' : 'chevron-down'} size={12} />
        </button>
      </div>
      {loopsOpen && (
        <div className="loop-rack" title="Click any number to loop that many beats from where the song is right now. Click the same button again to unloop.">
          {[
            // Sub-beat loops — chops within a single beat for stutter / glitch effects
            { beats: 0.125, l: '⅛',  unit: 'BEAT' },
            { beats: 0.25,  l: '¼',  unit: 'BEAT' },
            { beats: 0.5,   l: '½',  unit: 'BEAT' },
            // Whole-beat loops — the workhorses for build-ups and transitions
            { beats: 1,  l: '1', unit: 'BEAT' },
            { beats: 2,  l: '2', unit: 'BEATS' },
            { beats: 4,  l: '4', unit: 'BEATS' },
            { beats: 8,  l: '8', unit: 'BEATS' },
          ].map(({ beats, l, unit }) => (
            <button
              key={beats}
              className={`loop-btn ${state.loop?.beats === beats ? 'active' : ''}`}
              onClick={() => triggerBeatLoop(beats)}
              disabled={empty}
              title={beats < 1
                ? `Loop ${l} of a beat (~${Math.round(beats * (60 / (t?.bpm || 120)) * 1000)}ms at ${t?.bpm?.toFixed(0) || '?'} BPM). Use for stutter/glitch effects. Click again to release.`
                : `Loop ${beats} beat${beats > 1 ? 's' : ''} (≈ ${(beats / 4).toFixed(beats < 4 ? 2 : 0)} bar${beats >= 4 ? 's' : ''}). Click again to release.`}
            >
              {l} <span className="loop-btn-unit">{unit}</span>
            </button>
          ))}
        </div>
      )}

      <div className="section-head">
        <div className="section-title">FX Rack</div>
        <button className="collapse-btn" onClick={() => setFxOpen(!fxOpen)} aria-label={fxOpen ? 'Hide FX' : 'Show FX'} aria-expanded={fxOpen}>
          <Icon name={fxOpen ? 'chevron-up' : 'chevron-down'} size={12} />
        </button>
      </div>
      {fxOpen && (
        <div className="fx-rack">
          {[
            { key: 'reverb', label: 'Reverb', hue: 200 },
            { key: 'delay', label: 'Delay', hue: 240 },
            { key: 'echo', label: 'Echo', hue: 280 },
            { key: 'flanger', label: 'Flanger', hue: 320 },
            { key: 'crush', label: 'Crush', hue: 30 },
          ].map(({ key, label, hue }) => {
            const fx = state.fx[key];
            return (
              <button
                key={key}
                className={`fx-btn ${fx.on ? 'active' : ''}`}
                onClick={(e) => {
                  // top half of btn => toggle, bottom half => adjust amount
                  const r = e.currentTarget.getBoundingClientRect();
                  const rel = (e.clientY - r.top) / r.height;
                  if (rel < 0.7) {
                    updateFx(key, { on: !fx.on });
                  }
                }}
                onWheel={(e) => {
                  e.preventDefault();
                  const delta = e.deltaY > 0 ? -0.05 : 0.05;
                  updateFx(key, { amount: Math.max(0, Math.min(1, fx.amount + delta)) });
                }}
                disabled={empty}
                title={`${label} effect — click to turn on or off, scroll over the button to set how strong it is.`}
              >
                <div className="fx-icon" style={{ background: `radial-gradient(circle, oklch(0.75 0.18 ${hue}), oklch(0.5 0.2 ${hue}))`, borderRadius: 4 }} />
                {label}
                <div className="fx-amount" style={{ width: `${fx.amount * 100}%`, opacity: fx.on ? 1 : 0.3 }} />
              </button>
            );
          })}
        </div>
      )}
      {fxOpen && state.fx.delay?.on && (
        <div className="fx-sync-row" title="Choose how the Delay echoes are timed: locked to the beat (musical) or set to a free time you choose.">
          <span className="fxs-label">DELAY TIME</span>
          <button
            className={`fxs-toggle ${state.fx.delay?.beatSync ? 'on' : ''}`}
            onClick={() => updateFx('delay', { beatSync: !state.fx.delay?.beatSync })}
            disabled={empty}
            title={state.fx.delay?.beatSync
              ? 'TEMPO mode: Delay echoes follow the beat (musical). Pick a beat division below.'
              : 'FREE mode: Delay uses a fixed time, not tied to the song’s beat.'}
          >
            {state.fx.delay?.beatSync ? '♪ TEMPO' : 'FREE'}
          </button>
          {state.fx.delay?.beatSync && (
            <div className="fxs-divs" title="Pick how often the delay echoes (in beats). 1/4 = quarter note, 1/8 = eighth, etc.">
              {[
                { v: 0.0625, l: '1/16', tip: 'Sixteenth notes — fast, almost a buzz' },
                { v: 0.125,  l: '1/8',  tip: 'Eighth notes — quick “ping” echoes' },
                { v: 0.25,   l: '1/4',  tip: 'Quarter notes — echoes match the kick drum (most common)' },
                { v: 0.5,    l: '1/2',  tip: 'Half notes — wide, slow echoes' },
                { v: 1,      l: '1 BAR',tip: 'Whole bar — huge dub-style echoes' },
              ].map(({ v, l, tip }) => (
                <button
                  key={v}
                  className={`fxs-div ${(state.fx.delay?.beatDivision || 0.25) === v ? 'active' : ''}`}
                  onClick={() => updateFx('delay', { beatDivision: v, beatSync: true })}
                  disabled={empty}
                  title={tip}
                >{l}</button>
              ))}
            </div>
          )}
        </div>
      )}
      {fxOpen && <FXPad state={state} onChange={onChange} empty={empty} />}
      </>)}
    </div>
  );
}

// ============ X/Y FX TOUCHPAD ============
function FXPad({ state, onChange, empty }) {
  const padRef = useRef(null);
  const [pos, setPos] = useState(null);
  const [latched, setLatched] = useState(false);
  const [activeFx, setActiveFx] = useState('reverb');

  useEffect(() => {
    const on = Object.entries(state.fx).find(([, v]) => v.on);
    if (on && !latched) setActiveFx(on[0]);
  }, [state.fx, latched]);

  const apply = (x, y) => {
    onChange({
      ...state,
      filter: x,
      fx: { ...state.fx, [activeFx]: { ...state.fx[activeFx], on: true, amount: 1 - y } },
    });
  };

  const release = () => {
    if (latched) return;
    setPos(null);
    onChange({
      ...state,
      filter: 0.5,
      fx: { ...state.fx, [activeFx]: { ...state.fx[activeFx], on: false } },
    });
  };

  const handleMove = (clientX, clientY) => {
    const r = padRef.current?.getBoundingClientRect();
    if (!r) return;
    const x = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
    const y = Math.max(0, Math.min(1, (clientY - r.top) / r.height));
    setPos({ x, y });
    apply(x, y);
  };

  const onPointerDown = (e) => {
    if (empty) return;
    e.currentTarget.setPointerCapture?.(e.pointerId);
    handleMove(e.clientX, e.clientY);
  };
  const onPointerMove = (e) => {
    if (empty) return;
    if (e.buttons === 0 && !latched) return;
    handleMove(e.clientX, e.clientY);
  };
  const onPointerUp = (e) => {
    if (empty) return;
    e.currentTarget.releasePointerCapture?.(e.pointerId);
    if (!latched) release();
  };

  const fxLabels = { reverb: 'REVERB', delay: 'DELAY', echo: 'ECHO', flanger: 'FLANGER', crush: 'CRUSH' };

  return (
    <div className="fxpad-wrap">
      <div className="fxpad-head">
        <div className="fxpad-title">X/Y PAD</div>
        <div className="fxpad-controls">
          <select
            className="fxpad-select"
            value={activeFx}
            onChange={(e) => setActiveFx(e.target.value)}
            disabled={empty}
            title="Pick which effect this pad controls. Drag the pad up = more effect, sideways = filter sweep."
          >
            {Object.entries(fxLabels).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
          </select>
          <button
            className={`fxpad-latch ${latched ? 'on' : ''}`}
            onClick={() => {
              const next = !latched;
              setLatched(next);
              if (!next) release();
            }}
            disabled={empty}
            title={latched
              ? 'Latched: the effect stays on after you let go of the pad. Click again to release.'
              : 'Hold mode (default): the effect stops as soon as you release the pad. Click to latch it instead.'}
          >
            {latched ? '● LATCHED' : '○ LATCH'}
          </button>
        </div>
      </div>
      <div
        ref={padRef}
        className={`fxpad ${empty ? 'empty' : ''} ${pos ? 'active' : ''}`}
        title="Touch and drag to play with effects. Up/down = how strong the effect is. Left/right = filter (left cuts highs, right cuts lows). Lift to release — unless LATCH is on."
        style={pos ? {
          '--glow-x': `${pos.x * 100}%`,
          '--glow-y': `${pos.y * 100}%`,
          // Color shifts subtly with Y position: cyan at top (strong FX) → magenta at bottom
          '--glow-hue': `${210 - pos.y * 90}`,
        } : undefined}
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerUp}
      >
        <div className="fxpad-grid" />
        {pos && <div className="fxpad-glow" />}
        <div className="fxpad-axis-x"><span>LP</span><span>FILTER</span><span>HP</span></div>
        <div className="fxpad-axis-y"><span>MAX</span><span>{fxLabels[activeFx]}</span><span>0</span></div>
        {pos && (
          <div className="fxpad-dot" style={{ left: `${pos.x * 100}%`, top: `${pos.y * 100}%` }}>
            <div className="fxpad-crosshair-h" />
            <div className="fxpad-crosshair-v" />
            <div className="fxpad-dot-inner" />
          </div>
        )}
        {empty && <div className="fxpad-empty-label">Load a track to use X/Y pad</div>}
        {!empty && !pos && <div className="fxpad-hint">Click &amp; drag · Latch to hold · Tap on mobile</div>}
      </div>
    </div>
  );
}

// ============ MIXER ============
function Mixer({ deckA, deckB, mixer, onMixerChange, vuA, vuB, quantize, onQuantizeChange, masterDeck }) {
  const setVol = (side, v) => onMixerChange({ ...mixer, [`vol${side.toUpperCase()}`]: v });
  const setCross = (v) => onMixerChange({ ...mixer, crossfader: v });
  const setMaster = (v) => onMixerChange({ ...mixer, master: v });

  // Key match (Camelot-style close detection)
  const keyMatch = useMemo(() => {
    if (!deckA.track || !deckB.track) return null;
    const ka = deckA.track.key, kb = deckB.track.key;
    if (ka === kb) return 'perfect';
    // extract number and letter
    const m1 = ka?.match(/(\d+)([AB])/), m2 = kb?.match(/(\d+)([AB])/);
    if (!m1 || !m2) return null;
    const diff = Math.abs(parseInt(m1[1]) - parseInt(m2[1]));
    if (diff <= 1 || diff === 11) return 'compatible';
    return 'clash';
  }, [deckA.track, deckB.track]);

  const bpmMatch = useMemo(() => {
    if (!deckA.track || !deckB.track) return null;
    const a = deckA.synced ? deckB.track.bpm : deckA.track.bpm;
    const b = deckB.track.bpm;
    const diff = Math.abs(a - b);
    if (diff < 0.1) return 'perfect';
    if (diff < 2) return 'close';
    return 'off';
  }, [deckA, deckB]);

  // Master deck info — used for the center BPM clock and dual waveform.
  // If neither deck is explicitly master, fall back to whichever has a track.
  const effMaster = masterDeck || (deckA.track ? 'a' : (deckB.track ? 'b' : null));
  const masterTrack = effMaster === 'a' ? deckA.track : (effMaster === 'b' ? deckB.track : null);
  const bothLoaded = !!(deckA.track && deckB.track);
  const bpmDiff = bothLoaded ? Math.abs((deckA.track.bpm || 0) - (deckB.track.bpm || 0)) : null;
  const locked = !!(deckA.synced || deckB.synced);

  return (
    <div className="panel mixer" data-screen-label="Mixer">
      {/* DUAL WAVEFORM — bird's-eye stacked overview, A on top, B mirrored */}
      <DualWaveform deckA={deckA} deckB={deckB} masterBpm={masterTrack?.bpm} />

      <div className="mixer-head">
        <div className="mixer-title">Mixer</div>
        <div
          className={`match-indicator ${(keyMatch === 'perfect' || keyMatch === 'compatible') && (bpmMatch === 'perfect' || bpmMatch === 'close') ? 'ok' : ''}`}
          title={!deckA.track || !deckB.track
            ? 'Load a song on each deck to check if they’ll sound good together'
            : keyMatch === 'perfect' ? 'Both songs are in the same key — they’ll blend perfectly.'
            : keyMatch === 'compatible' ? 'These keys are musically compatible — your blend will sound smooth.'
            : keyMatch === 'clash' ? 'These keys often clash. The blend may sound dissonant. Tip: hold Shift on MATCH to fix the pitch.'
            : ''}
        >
          <span className="dot" />
          {!deckA.track || !deckB.track ? 'Load both' :
            (keyMatch === 'perfect' ? 'KEY MATCH' :
             keyMatch === 'compatible' ? 'HARMONIC' :
             keyMatch === 'clash' ? 'KEY CLASH' : '')}
        </div>
      </div>

      {/* CENTER BPM — pulse-on-beat, large, glowing when locked */}
      <CenterBpm
        masterBpm={masterTrack?.bpm}
        masterFirstBeat={masterTrack?.firstBeat || 0}
        masterSide={effMaster}
        bothLoaded={bothLoaded}
        bpmDiff={bpmDiff}
        locked={locked}
      />

      {/* MASTER badge */}
      <div
        className={`master-tag inline ${effMaster === 'a' ? 'a' : effMaster === 'b' ? 'b' : 'none'}`}
        title={effMaster
          ? `Deck ${effMaster.toUpperCase()} is the master — the other deck syncs its tempo to this one when you hit MATCH.`
          : 'No master deck yet. Whichever deck plays first becomes the master automatically.'}
      >
        MASTER · {effMaster ? effMaster.toUpperCase() : '—'}
      </div>

      {/* BEAT CLOCKS — A and B, sit directly above their faders */}
      <div className="beat-clock-row">
        <BeatClock
          side="a" bpm={deckA.track?.bpm} firstBeat={deckA.track?.firstBeat}
          deckColor="var(--accent-cyan)" label="A"
          isMaster={effMaster === 'a'} synced={locked && bpmDiff != null && bpmDiff < 0.5}
        />
        <BeatClock
          side="b" bpm={deckB.track?.bpm} firstBeat={deckB.track?.firstBeat}
          deckColor="var(--accent-pink)" label="B"
          isMaster={effMaster === 'b'} synced={locked && bpmDiff != null && bpmDiff < 0.5}
        />
      </div>

      <div className="fader-row compact">
        <div className="fader-col">
          <div className="fader-stack">
            <div className="vu-meter" title="Live loudness meter for Deck A"><div className="vu-fill" style={{ height: `${vuA * 100}%` }} /></div>
            <Fader value={mixer.volA} onChange={(v) => setVol('a', v)} className="fader-a" />
          </div>
        </div>
        <div className="fader-col">
          <div className="fader-stack">
            <Fader value={mixer.volB} onChange={(v) => setVol('b', v)} className="fader-b" />
            <div className="vu-meter" title="Live loudness meter for Deck B"><div className="vu-fill" style={{ height: `${vuB * 100}%` }} /></div>
          </div>
        </div>
      </div>

      <div className="crossfader-wrap" title="Drag this to slide between Deck A (left) and Deck B (right). Center = both decks playing at once.">
        <div className="crossfader-labels">
          <span className="a" title="Slide left to play only Deck A">◄ A</span>
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-3)' }}>
            {mixer.crossfader < 0.45 ? 'A' : mixer.crossfader > 0.55 ? 'B' : 'MID'}
          </span>
          <span className="b" title="Slide right to play only Deck B">B ►</span>
        </div>
        <Fader orientation="horizontal" value={mixer.crossfader} onChange={setCross} />
      </div>

      {/* Quantize — moved to the bottom; less prominent now that BPM/clocks
          are the headline. Toggle + division as a tight inline strip. */}
      <div className="quantize-panel slim">
        <button
          className={`q-toggle ${quantize?.on ? 'on' : ''}`}
          onClick={() => onQuantizeChange?.({ ...quantize, on: !quantize?.on })}
          title={quantize?.on
            ? 'Beat-snap is ON. Anything you do (loops, cues, beat-jumps) will line up perfectly with the beat — even if your timing is slightly off. Recommended for beginners.'
            : 'Beat-snap is OFF. Loops and cues fire exactly where you click, on or off the beat. Use this for off-grid creative effects.'}
        >
          <span className="q-led" /> Q {quantize?.on ? 'ON' : 'OFF'}
        </button>
        <div className="q-divs" title="How fine the beat-snap is. 1 = whole bar, ¼ = single beat (default), ¹⁄₁₆ = very precise.">
          {[
            { v: 1,    l: '1',     tip: 'Snap to whole bars (4 beats) — huge, slow steps' },
            { v: 0.5,  l: '½',     tip: 'Snap to half-bars (2 beats)' },
            { v: 0.25, l: '¼',     tip: 'Snap to single beats — the standard setting' },
            { v: 0.125,l: '⅛',    tip: 'Snap to half-beats — tighter, more rhythmic' },
            { v: 0.0625, l: '¹⁄₁₆', tip: 'Snap to 16th notes — very precise, almost no snapping at all' },
          ].map(({ v, l, tip }) => (
            <button
              key={v}
              className={`q-div ${(quantize?.division || 0.25) === v ? 'active' : ''}`}
              onClick={() => onQuantizeChange?.({ ...quantize, division: v, on: true })}
              disabled={!quantize?.on}
              title={tip}
            >{l}</button>
          ))}
        </div>
      </div>

      <div style={{ flex: 1 }} />

      <div className="master-row">
        <div className="master-knob-wrap" title="Master volume — controls how loud everything coming out of the speakers is.">
          <Knob value={mixer.master} onChange={setMaster} label="MASTER" displayValue={`${Math.round(mixer.master * 100)}%`} size={44} colorHue={150} />
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { Deck, Mixer });
