/* Top-level app with view routing, state, real midi/dsp polling, tweaks */

const { useState: useStateApp, useEffect: useEffectApp, useMemo: useMemoApp } = React;

function buildInitialChannel(ch){
  return {
    id: ch.id,
    map: ch.map,
    midiCh: ch.defaultMidi != null ? ch.defaultMidi : 0,
    pg: true,                       // accept incoming Program Change → swap soundmap
    volume: -3 + Math.random()*6,
    pan: ch.id===0?-15: ch.id===4?-8: ch.id===5?0: ch.id===6?20: ch.id===7?12: 0,
    tune: 0,
    sends: 20,
    mute: false,
    solo: false,
    eq: JSON.parse(JSON.stringify(EQ_DEFAULT)),
    eqOut: 0,
    pre:  JSON.parse(JSON.stringify(FX_PRE)),
    post: JSON.parse(JSON.stringify(FX_POST)),
    // Tempo-locked loop bank state. `loopBank` is the bank name (or null when
    // none loaded). `activeLoopPc` mirrors the engine's currently-playing
    // Program Change slot; `pendingLoopPc` is a swap requested by the user
    // that the engine will apply on the next bar boundary.
    loopBank: null,
    activeLoopPc: null,
    pendingLoopPc: null,
    loopArmed: false,
  };
}

function buildInitialState(){
  const channels = {};
  CHANNELS.forEach(ch=> channels[ch.id] = buildInitialChannel(ch));
  return {
    view: 'mixer',          // 'mixer' | 'edit' | 'master'
    editingChannel: null,
    selectedChannel: 0,
    channels,
    master: {
      volume: -1,
      headroom: 6,
      pan: 0,
      eq: { low:{gain:1.5,freq:100}, mid:{gain:-1,freq:1200}, high:{gain:2,freq:8000} },
      fx: JSON.parse(JSON.stringify(MASTER_FX)),
    }
  };
}

function MidiPortPopup({ onClose, midiInLabel }){
  const [ports, setPorts] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  const fetchPorts = React.useCallback(function(){
    setLoading(true);
    if(TauriAPI.available){
      TauriAPI.getMidiPorts().then(function(list){
        setPorts(Array.isArray(list) ? list : []);
        setLoading(false);
      }).catch(function(){
        setPorts([]);
        setLoading(false);
      });
    } else {
      setPorts([]);
      setLoading(false);
    }
  }, []);

  React.useEffect(function(){ fetchPorts(); }, []);

  const handleSelect = function(portName){
    if(TauriAPI.available){
      TauriAPI.selectMidiPort(portName).catch(function(){});
    }
    onClose();
  };

  const hasConnection = ports && ports.some(function(p){ return p.is_connected; });

  return (
    <>
      <div onClick={onClose} style={{position:'absolute',inset:0,background:'rgba(0,0,0,0.45)',zIndex:40}}/>
      <div style={{position:'absolute',top:46,right:'auto',left:480,width:240,maxHeight:400,background:'var(--bg-1)',border:'1px solid var(--line)',borderRadius:10,padding:12,zIndex:41,boxShadow:'0 20px 60px rgba(0,0,0,0.7)',display:'flex',flexDirection:'column',gap:10}}>
        <div className="row gap-8" style={{justifyContent:'space-between',alignItems:'center'}}>
          <div style={{fontFamily:'Rajdhani',fontWeight:700,fontSize:12,letterSpacing:'.2em',textTransform:'uppercase',color:'var(--ink)'}}>MIDI INPUT DEVICE</div>
          <div className="row gap-6">
            <button onClick={fetchPorts} style={{background:'none',border:'1px solid var(--line)',color:'var(--ink-mute)',cursor:'pointer',fontSize:10,borderRadius:4,padding:'3px 7px',fontFamily:'JetBrains Mono',letterSpacing:'.06em'}}>REFRESH</button>
            <button onClick={onClose} style={{background:'none',border:'none',color:'var(--ink-mute)',cursor:'pointer',fontSize:14}}>✕</button>
          </div>
        </div>

        <div style={{overflowY:'auto',display:'flex',flexDirection:'column',gap:4,flex:1}}>
          {loading && (
            <div style={{textAlign:'center',color:'var(--ink-mute)',fontSize:10,padding:'18px 0',fontFamily:'JetBrains Mono'}}>
              scanning ports…
            </div>
          )}
          {!loading && (!ports || ports.length===0) && (
            <div style={{textAlign:'center',color:'var(--ink-mute)',fontSize:10,padding:'18px 0',fontFamily:'JetBrains Mono'}}>
              No MIDI devices found
            </div>
          )}
          {!loading && ports && ports.map(function(port, i){
            return (
              <div key={i} className={"midi-popup-row"+(port.is_connected?' is-connected':'')}
                   onClick={function(){ handleSelect(port.name); }}>
                {port.name}
              </div>
            );
          })}
        </div>

        {hasConnection && (
          <button className="tb-btn" style={{width:'100%',justifyContent:'center',color:'var(--accent-danger)'}}
                  onClick={function(){ handleSelect(''); }}>
            DISCONNECT
          </button>
        )}
      </div>
    </>
  );
}

function LogoContextMenu({ x, y, onClose, onRelayDialout, remoteCapable }){
  const [isFullscreen, setIsFullscreen] = React.useState(false);

  const appWindow = React.useMemo(function(){
    if(window.__TAURI__ && window.__TAURI__.window && window.__TAURI__.window.getCurrentWindow){
      try { return window.__TAURI__.window.getCurrentWindow(); }
      catch(e){ return null; }
    }
    return null;
  }, []);

  React.useEffect(function(){
    if(!appWindow) return;
    appWindow.isFullscreen().then(function(v){ setIsFullscreen(!!v); }).catch(function(){});
  }, [appWindow]);

  React.useEffect(function(){
    const onKey = function(e){ if(e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return function(){ window.removeEventListener('keydown', onKey); };
  }, [onClose]);

  const toggleFullscreen = function(){
    if(appWindow){
      appWindow.setFullscreen(!isFullscreen).catch(function(){});
    }
    onClose();
  };

  const closeWindow = function(){
    if(appWindow){
      appWindow.close().catch(function(){});
    }
    onClose();
  };

  const isDesktop = !!(window.__TAURI__ && window.__TAURI__.core);
  // Dial-out is a Pi-only desktop action (this device dials a relay). A
  // remote-capable build (Windows controller) never dials out, and the browser
  // web build (BS_WEB) has a synthetic __TAURI__ but no PiDialoutPanel, so hide
  // it there too — otherwise clicking it would crash on the missing component.
  const showDialout = isDesktop && !remoteCapable && !window.BS_WEB;

  const openDialout = function(){
    if(onRelayDialout) onRelayDialout();
    onClose();
  };

  const MENU_W = 180, MENU_H = showDialout ? 122 : 88;
  const left = Math.min(x, 1024 - MENU_W - 4);
  const top = Math.min(y, 600 - MENU_H - 4);

  const itemStyle = {
    padding:'8px 12px',
    fontFamily:'Rajdhani',
    fontSize:12,
    fontWeight:600,
    letterSpacing:'.1em',
    textTransform:'uppercase',
    color:'var(--ink)',
    cursor:'pointer',
    display:'flex',
    alignItems:'center',
    justifyContent:'space-between',
    gap:10,
    borderRadius:4,
  };

  return (
    <>
      <div onClick={onClose} onContextMenu={function(e){ e.preventDefault(); onClose(); }}
           style={{position:'absolute',inset:0,zIndex:40}}/>
      <div style={{
        position:'absolute',
        left: left,
        top: top,
        width: MENU_W,
        background:'var(--bg-1)',
        border:'1px solid var(--line)',
        borderRadius:8,
        padding:4,
        zIndex:41,
        boxShadow:'0 20px 60px rgba(0,0,0,0.7)',
        display:'flex',
        flexDirection:'column',
        gap:2,
      }}>
        <div style={itemStyle}
             onMouseEnter={function(e){ e.currentTarget.style.background='var(--bg-2)'; }}
             onMouseLeave={function(e){ e.currentTarget.style.background='transparent'; }}
             onClick={toggleFullscreen}>
          <span>Full Screen</span>
          <span style={{color:'var(--accent-purple, #9b5cff)',fontSize:13,opacity:isFullscreen?1:0}}>✓</span>
        </div>
        <div style={itemStyle}
             onMouseEnter={function(e){ e.currentTarget.style.background='var(--bg-2)'; }}
             onMouseLeave={function(e){ e.currentTarget.style.background='transparent'; }}
             onClick={closeWindow}>
          <span>Close</span>
        </div>
        {showDialout && (
          <div style={itemStyle}
               onMouseEnter={function(e){ e.currentTarget.style.background='var(--bg-2)'; }}
               onMouseLeave={function(e){ e.currentTarget.style.background='transparent'; }}
               onClick={openDialout}>
            <span>Relay dial-out…</span>
          </div>
        )}
      </div>
    </>
  );
}

/* Per-core CPU mini-gauge. Renders four fixed-width slots (one per Pi
 * core); the App-level poll mutates `data-cpu="i"` div heights + label
 * text directly via querySelector, so changing values never triggers a
 * React re-render of the topbar. */
function CpuCores(){
  return (
    <div className="tb-chip cpu-cores" title="Per-core CPU usage">
      <span className="cpu-label">CPU</span>
      {[0,1,2,3].map(function(i){
        return (
          <div key={i} className="cpu-bar-wrap">
            <div className="cpu-bar"><div className="cpu-fill" data-cpu={i}/></div>
            <span className="cpu-pct" data-cpu-pct={i}>0</span>
          </div>
        );
      })}
    </div>
  );
}

/* Top-bar transport widget. Polls the engine's tempo source / BPM every 250 ms.
 * When MIDI Clock is the source, the widget locks the BPM display to the
 * incoming clock and hides the manual controls. Otherwise the user can tap
 * tempo or type a BPM and we push it to the engine via set_master_bpm_manual.
 * Transport state is tracked optimistically — get_transport_status currently
 * always returns "running", so we display the user's last requested state. */
function TransportWidget(){
  const [status, setStatus] = React.useState({ bpm:0, source:'manual', state:'stopped' });
  const [requested, setRequested] = React.useState('stopped'); // optimistic UI
  const [manualBpm, setManualBpm] = React.useState(120);
  const taps = React.useRef([]);

  React.useEffect(function(){
    const t = setInterval(function(){
      if(!window.__TAURI__ || !window.__TAURI__.core) return;
      BSInvoke('get_transport_status')
        .then(function(s){ setStatus(s); })
        .catch(function(){});
    }, 250);
    return function(){ clearInterval(t); };
  }, []);

  const isMidi = status.source === 'midi-clock' && status.bpm > 0;
  const displayBpm = isMidi ? status.bpm : manualBpm;

  const onPlay = function(){
    setRequested('running');
    if(window.__TAURI__ && window.__TAURI__.core){
      BSInvoke('transport_play').catch(function(){});
    }
  };
  const onStop = function(){
    setRequested('stopping');
    if(window.__TAURI__ && window.__TAURI__.core){
      BSInvoke('transport_stop').catch(function(){});
    }
  };
  const onTap = function(){
    const now = performance.now();
    taps.current = taps.current.filter(function(t){ return now - t < 3000; }).concat(now);
    if (taps.current.length >= 2) {
      const ints = [];
      for (let i=1; i<taps.current.length; i++) ints.push(taps.current[i] - taps.current[i-1]);
      const avg = ints.reduce(function(a,b){ return a+b; }, 0) / ints.length;
      const bpm = Math.max(40, Math.min(240, Math.round(60000 / avg)));
      setManualBpm(bpm);
      if(window.__TAURI__ && window.__TAURI__.core){
        BSInvoke('set_master_bpm_manual', { bpm: bpm }).catch(function(){});
      }
    }
  };
  const onBpmChange = function(n){
    const clamped = Math.max(40, Math.min(240, n|0));
    setManualBpm(clamped);
    if(window.__TAURI__ && window.__TAURI__.core){
      BSInvoke('set_master_bpm_manual', { bpm: clamped }).catch(function(){});
    }
  };

  return (
    <div className={"tb-transport " + (isMidi ? 'is-midi' : 'is-manual')}>
      <div className="ttx-col">
        <div className="ttx-label">BPM</div>
        <div className="ttx-bpm">{Number(displayBpm).toFixed(1)}</div>
      </div>
      <div className="ttx-col">
        <div className="ttx-label">Source</div>
        <div className="ttx-src">
          <span className="ttx-led"/> {isMidi ? 'MIDI CLOCK' : 'MANUAL'}
        </div>
      </div>
      <div className="ttx-col">
        <div className="ttx-label">Transport</div>
        <div className="ttx-state">{requested.toUpperCase()}</div>
      </div>
      {!isMidi && (
        <div className="ttx-col">
          <button className="ttx-tap" onClick={onTap}>TAP</button>
          <input type="number" className="ttx-bpm-input" min="40" max="240"
                 value={manualBpm} onChange={function(e){ onBpmChange(Number(e.target.value)); }}/>
        </div>
      )}
      <div className="ttx-col">
        <button className="ttx-ctrl ttx-play" onClick={onPlay}>{'▶'}</button>
        <button className="ttx-ctrl ttx-stop" onClick={onStop}>{'■'}</button>
      </div>
    </div>
  );
}

function TopBar({ state, midiActive, midiLabel, onHome, onMaster, showTweaks, toggleTweaks, onSave, saveState, midiPopupOpen, onToggleMidiPopup, onLogoContextMenu, onLogoClick, webRemote, webFailures }){
  return (
    <header className="topbar" data-screen-label="Top Bar">
      <div className="logo-wrap"
           style={{cursor:'pointer'}}
           onClick={onLogoClick}
           title="Connect to a remote sampler"
           onContextMenu={function(e){
             e.preventDefault();
             const frame = document.getElementById('viewport-frame');
             if(frame){
               const rect = frame.getBoundingClientRect();
               const scale = rect.width / 1024;
               const fx = (e.clientX - rect.left) / scale;
               const fy = (e.clientY - rect.top) / scale;
               onLogoContextMenu(fx, fy);
             } else {
               onLogoContextMenu(e.clientX, e.clientY);
             }
           }}>
        <div className="logo-mark"><LogoMark size={34}/></div>
        <div className="logo-name">
          <div className="brand">BRAYER·SOUND</div>
          <div className="tagline">sound. engineered to empower.</div>
        </div>
      </div>

      {webRemote && (
        <div title={"This sampler is controlled over the web relay — " + (webFailures||0) + " relay-link drop(s) since launch"}
          style={{alignSelf:'center', marginLeft:2, background:'#c0392b', color:'#fff',
            fontFamily:'JetBrains Mono', fontSize:9.5, fontWeight:700, letterSpacing:'.12em',
            padding:'3px 8px', borderRadius:5, whiteSpace:'nowrap', boxShadow:'0 0 10px rgba(192,57,43,0.6)'}}>
          ● WebRemote
          <span style={{marginLeft:6, opacity:0.85, fontWeight:600}}>⟳ {webFailures||0}</span>
        </div>
      )}

      <div style={{width:1,height:28,background:'var(--line-soft)'}}/>

      <div className="spacer"/>

      <TransportWidget/>

      <div className={"tb-chip "+(midiActive?'midi-active':'')}
           style={{width:180,cursor:'pointer',gap:6}}
           onClick={onToggleMidiPopup}
           title={midiLabel || 'No MIDI device selected'}>
        <div className="dot"/>
        <span style={{flexShrink:0}}>MIDI IN</span>
        <span style={{
          color:'var(--ink)',
          fontWeight:700,
          flex:1,
          minWidth:0,
          overflow:'hidden',
          textOverflow:'ellipsis',
          whiteSpace:'nowrap',
          textAlign:'right',
        }}>{midiLabel || '—'}</span>
      </div>

      <CpuCores/>

      <button className={"tb-btn "+(state.view==='mixer'?'is-active':'')} onClick={onHome}>MIXER</button>
      <button className={"tb-btn "+(state.view==='master'?'is-active':'')} onClick={onMaster}>MASTER</button>
      <button className={"tb-btn save-btn "+(saveState==='saved'?'is-saved':saveState==='saving'?'is-saving':'')}
              onClick={onSave}>
        {saveState==='saving' ? (<>
          <span className="save-spin"/>SAVING…
        </>) : saveState==='saved' ? (<>
          <span style={{color:'#5ee8a8'}}>✓</span> SAVED
        </>) : (<>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{marginRight:2}}>
            <path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/>
            <polyline points="17 21 17 13 7 13 7 21"/>
            <polyline points="7 3 7 8 15 8"/>
          </svg>
          SAVE
        </>)}
      </button>
    </header>
  );
}

function App(){
  const [state, setState] = useStateApp(()=>{
    try {
      const saved = localStorage.getItem('brayer-state-v2');
      if(saved){
        const p = JSON.parse(saved);
        return {...buildInitialState(), ...p};
      }
    } catch(e){}
    return buildInitialState();
  });

  useEffectApp(()=>{
    try {
      localStorage.setItem('brayer-state-v2', JSON.stringify({
        view: state.view, editingChannel: state.editingChannel, selectedChannel: state.selectedChannel
      }));
    } catch(e){}
  }, [state.view, state.editingChannel, state.selectedChannel]);

  // On boot: if a session is starred for auto-load, restore it; otherwise push
  // the UI's default state to the engine so what the user sees matches what's
  // audible. Auto-load reads from localStorage (synchronous), so this stays a
  // single mount effect — no race against the initial sync.
  useEffectApp(()=>{
    if(!TauriAPI.available) return;
    let auto = null;
    try {
      const list = JSON.parse(localStorage.getItem('brayer-snapshots-v2')) || [];
      auto = list.find(s=>s && s.autoload && s.state) || null;
    } catch(e){}
    if(auto && auto.state && auto.state.channels && auto.state.master){
      const nextChannels = JSON.parse(JSON.stringify(auto.state.channels));
      const nextMaster = JSON.parse(JSON.stringify(auto.state.master));
      setState(s=>({...s, channels: nextChannels, master: nextMaster}));
      TauriAPI.syncStateToEngine({ channels: nextChannels, master: nextMaster });
    } else {
      TauriAPI.syncStateToEngine(state);
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Real MIDI status polling
  const [midiActivity, setMidiActivity] = useStateApp({});
  const [midiInLabel, setMidiInLabel] = useStateApp(null);

  // Top-bar polling. Three independent loops; none of them touch state
  // that re-renders the channel strips:
  //   - MIDI status (1.5 s) → topbar label + on/off boolean
  //   - per-core CPU (1 s) → DOM-direct, no React state at all
  // Per-channel meters and activity LEDs are owned by MixerScreen's own
  // 100 ms poll (DOM-direct as well).
  useEffectApp(()=>{
    let active = true;
    let lastAnyMidi = null;
    const pollMidi = ()=>{
      if(!active || !TauriAPI.available) return;
      TauriAPI.getMidiStatus().then(function(status){
        if(!active || !status) return;
        setMidiInLabel(status.connected ? (status.port_name || 'CONNECTED') : null);
        // Reduce `midiActivity` to a single boolean — flip only on real
        // transitions so App doesn't re-render every poll while playing.
        var anyActive = false;
        if(status.channel_activity){
          for (var i = 0; i < status.channel_activity.length; i++) {
            if (status.channel_activity[i]) { anyActive = true; break; }
          }
        }
        if (anyActive !== lastAnyMidi) {
          lastAnyMidi = anyActive;
          setMidiActivity(anyActive ? { _any: true } : {});
        }
      });
    };

    // Per-core CPU: DOM-direct write into the CpuCores widget (rendered
    // once by React on mount). We resolve the four `.cpu-fill` and four
    // `.cpu-pct` elements via querySelector cache so the steady-state
    // poll cost is one IPC roundtrip + 8 inline-style writes.
    let cpuCache = null;
    const ensureCpuCache = ()=>{
      if (cpuCache) return cpuCache;
      const fills = []; const pcts = [];
      for (var i = 0; i < 4; i++) {
        const f = document.querySelector('[data-cpu="'+i+'"]');
        const p = document.querySelector('[data-cpu-pct="'+i+'"]');
        if (!f || !p) return null;
        fills.push(f); pcts.push(p);
      }
      cpuCache = { fills: fills, pcts: pcts, last: [-1,-1,-1,-1] };
      return cpuCache;
    };
    const pollCpu = ()=>{
      if(!active || !TauriAPI.available) return;
      TauriAPI.getPerCoreCpu().then(function(arr){
        if(!active || !arr) return;
        const c = ensureCpuCache();
        if (!c) return;
        for (var i = 0; i < 4 && i < arr.length; i++) {
          const v = Math.max(0, Math.min(100, arr[i] || 0));
          const rounded = Math.round(v);
          // Bar height — clip-path keeps the work GPU-composited.
          c.fills[i].style.height = v + '%';
          // Numeric label — only update on integer change to keep the
          // text node stable when the percentage hovers around a value.
          if (rounded !== c.last[i]) {
            c.pcts[i].textContent = String(rounded);
            c.last[i] = rounded;
          }
        }
      });
    };

    pollMidi(); pollCpu();
    const tMidi = setInterval(pollMidi, 1500);
    const tCpu = setInterval(pollCpu, 1000);
    return ()=>{
      active = false;
      clearInterval(tMidi); clearInterval(tCpu);
      cpuCache = null;
    };
  }, []);

  // Soundmap library loading
  const [soundmapLibrary, setSoundmapLibrary] = useStateApp([]);
  const refreshSoundmaps = React.useCallback(function(){
    if(TauriAPI.available){
      TauriAPI.listSoundmaps().then(function(list){
        if(Array.isArray(list)) setSoundmapLibrary(list);
      }).catch(function(){});
    }
  }, []);
  useEffectApp(function(){
    refreshSoundmaps();
  }, []);

  React.useEffect(function(){
    RemoteLink.onReconnected = function(){ refreshSoundmaps(); };
    return function(){ RemoteLink.onReconnected = null; };
  }, [refreshSoundmaps]);

  // Step 1: Apply incoming sync + snapshot (Windows side)
  React.useEffect(function(){
    RemoteLink.onSync = function(cmd, args){ setState(function(s){ return applyCommandToState(s, cmd, args); }); };
    RemoteLink.onSnapshot = function(snap){ if(snap && typeof snap === 'object') setState(snap); };
    return function(){ RemoteLink.onSync = null; RemoteLink.onSnapshot = null; };
  }, []);

  // Library refresh: re-fetch list_soundmaps on the OTHER screen after a remote
  // library command. refreshSoundmaps is a useCallback; route through a ref so
  // the []-dep effects below never capture a stale closure.
  const refreshRef = React.useRef(refreshSoundmaps);
  React.useEffect(function(){ refreshRef.current = refreshSoundmaps; }, [refreshSoundmaps]);
  React.useEffect(function(){
    // Windows side: client gets a library_changed frame → re-fetch the list.
    RemoteLink.onLibraryChanged = function(){ if(refreshRef.current) refreshRef.current(); };
    return function(){ RemoteLink.onLibraryChanged = null; };
  }, []);
  // When the control link reaches 'connected' (web account login, Windows
  // connect, OR auto-reconnect), fetch the REMOTE soundmap library. The web
  // dashboard connect path doesn't fire LoginScreen.onConnected, so without
  // this the browser keeps its empty pre-connect list ("NO SOUNDMAPS").
  React.useEffect(function(){
    const off = RemoteLink.onChange(function(s){
      if(s.state === 'connected' && refreshRef.current){ refreshRef.current(); }
    });
    return off;
  }, []);
  React.useEffect(function(){
    // Pi side: backend emits this after a remote library command → re-fetch.
    if(!window.__TAURI__ || !window.__TAURI__.event) return;
    var un = null;
    window.__TAURI__.event.listen('soundmap-library-changed', function(){
      if(refreshRef.current) refreshRef.current();
    }).then(function(fn){ un = fn; });
    return function(){ if(un) un(); };
  }, []);

  // Step 2: Listen for Pi-side `state-sync` event (Pi applies Windows-originated changes)
  React.useEffect(function(){
    if(!window.__TAURI__ || !window.__TAURI__.event) return;
    var un = null;
    window.__TAURI__.event.listen('state-sync', function(evt){
      var p = evt && evt.payload; if(!p) return;
      setState(function(s){ return applyCommandToState(s, p.cmd, p.args || {}); });
    }).then(function(fn){ un = fn; });
    return function(){ if(un) un(); };
  }, []);

  // Step 3: Track hasRemoteClient and push snapshot once on connect (Pi side)
  React.useEffect(function(){
    if(!window.__TAURI__ || !window.__TAURI__.event) return;
    var unC = null, unD = null;
    window.__TAURI__.event.listen('remote-client-connected', function(){
      RemoteLink.hasRemoteClient = true;
      if(window.__TAURI__.core) window.__TAURI__.core.invoke('set_ui_snapshot', { snapshot: stateRef.current }).catch(function(){});
    }).then(function(fn){ unC = fn; });
    window.__TAURI__.event.listen('remote-client-disconnected', function(){
      RemoteLink.hasRemoteClient = false;
    }).then(function(fn){ unD = fn; });
    return function(){ if(unC) unC(); if(unD) unD(); };
  }, []);

  // MIDI Program Change listener:
  // backend emits `midi-program-change` on every PC; we resolve (msb,lsb,program)
  // → (category,name) using CATEGORY_BANK_BASE + alphabetical position within
  // the category, then call loadSoundmap for any sampler channel listening to
  // the same MIDI channel with PG enabled. State is read via refs so the
  // listener does not need to re-subscribe on every state change.
  const stateRef = React.useRef(state);
  const libraryRef = React.useRef(soundmapLibrary);
  React.useEffect(function(){ stateRef.current = state; });
  React.useEffect(function(){ libraryRef.current = soundmapLibrary; }, [soundmapLibrary]);

  React.useEffect(function(){
    if(!window.__TAURI__ || !window.__TAURI__.event) return;
    var unlisten = null;
    var cancelled = false;
    window.__TAURI__.event.listen('midi-program-change', function(evt){
      var p = evt && evt.payload; if(!p) return;
      var midiCh = p.midi_channel, msb = p.msb, lsb = p.lsb, program = p.program;

      // Resolve category from (msb, lsb)
      var targetCat = null;
      var bb = (typeof CATEGORY_BANK_BASE !== 'undefined') ? CATEGORY_BANK_BASE : window.CATEGORY_BANK_BASE;
      if(!bb) return;
      var keys = Object.keys(bb);
      for(var i=0;i<keys.length;i++){
        if(bb[keys[i]].msb === msb && bb[keys[i]].lsb === lsb){ targetCat = keys[i]; break; }
      }
      if(!targetCat) return;

      // Pick the soundmap whose stable program number matches the incoming PC.
      var lib = libraryRef.current || [];
      var target = lib.find(function(m){
        return (m.category || '').toUpperCase() === targetCat && (m.program | 0) === program;
      });
      if(!target) return;

      // Apply to every sampler channel listening to this MIDI channel with PG enabled
      var st = stateRef.current; if(!st) return;
      Object.keys(st.channels).forEach(function(cidStr){
        var cid = parseInt(cidStr,10);
        var cs = st.channels[cid]; if(!cs) return;
        if(cs.midiCh !== midiCh) return;
        if(cs.pg === false) return;
        if(cs.map === target.name && cs.mapCategory === target.category) return;
        TauriAPI.loadSoundmap(cid, target.category, target.name).then(function(){
          setState(function(s){
            return {...s, channels: {...s.channels, [cid]: {
              ...s.channels[cid],
              map: target.name,
              mapCategory: target.category,
              sampleParams: {},
              samples: [],
            }}};
          });
        }).catch(function(err){
          console.warn('[PG] loadSoundmap failed for ch', cid, err);
        });
      });
    }).then(function(fn){
      if(cancelled){ try { fn(); } catch(e){} } else { unlisten = fn; }
    }).catch(function(){});
    return function(){
      cancelled = true;
      if(unlisten){ try { unlisten(); } catch(e){} }
    };
  }, []);

  // Cross-bank loop PC listener: backend emits `loop-pc-arrived` on every
  // incoming Program Change. The audio engine handles the in-bank case
  // (current loop bank already contains this address → swap active loop).
  // Here we handle the cross-bank case: if the address lives in a different
  // bank, load that bank on the channel and select the requested PC. The
  // engine keeps logging "[PC] NO MATCH" when out-of-bank; that's our cue.
  // We always invoke `list_loop_banks` per event (option (b) — simple, avoids
  // hoisting per-channel bank state into App).
  // Default ChannelMap (kept in sync with `ChannelMap::default_map` in
  // `midi_handler.rs`): MIDI channels are 0-indexed on the wire.
  React.useEffect(function(){
    if(!window.__TAURI__ || !window.__TAURI__.event) return;
    var unlisten = null;
    var cancelled = false;
    var midiToSampler = { 9:0, 10:1, 8:2, 11:3, 12:4, 13:5, 14:6, 15:7 };
    window.__TAURI__.event.listen('loop-pc-arrived', function(evt){
      var p = evt && evt.payload; if(!p) return;
      var midiCh = p.midi_channel, msb = p.msb, lsb = p.lsb, pc = p.pc;
      var samplerCh = midiToSampler[midiCh];
      if(samplerCh == null) return;

      // Look up the address across ALL loop banks on disk.
      BSInvoke('list_loop_banks').then(function(banks){
        if(!Array.isArray(banks)) return;
        var match = null;
        for(var i=0;i<banks.length;i++){
          var b = banks[i];
          if(!b || !Array.isArray(b.loops)) continue;
          for(var j=0;j<b.loops.length;j++){
            var l = b.loops[j];
            if(l.bank_msb === msb && l.bank_lsb === lsb && l.pc === pc){
              match = b; break;
            }
          }
          if(match) break;
        }
        if(!match){
          // Per user spec: Silence fallback applies ONLY to channel 8 (sampler
          // index 7). Other channels are silent on unmatched PCs.
          if(samplerCh !== 7) return;

          // Unmatched PC → fallback to the Silence bank on this channel so
          // the loop goes silent immediately. Per user spec: "any unknown
          // PC plays Silence". Skip if Silence is already loaded on this
          // channel (avoid redundant reload).
          var silenceBank = null;
          for(var k=0;k<banks.length;k++){
            if(banks[k] && banks[k].name === 'Silence'){ silenceBank = banks[k]; break; }
          }
          if(!silenceBank) return; // No Silence bank present; nothing to do.
          var stS = stateRef.current;
          var csS = stS && stS.channels && stS.channels[samplerCh];
          if(csS && csS.loopBank === 'Silence') return;
          var silencePc = (silenceBank.loops && silenceBank.loops[0]) ? silenceBank.loops[0].pc : null;
          BSInvoke('load_loop_bank', {
            channel: samplerCh,
            bankName: 'Silence',
          }).then(function(){
            setState(function(s){
              var prev = s.channels[samplerCh] || {};
              return {...s, channels: {...s.channels, [samplerCh]: {
                ...prev,
                loopBank: 'Silence',
                activeLoopPc: silencePc,
                pendingLoopPc: null,
                loopArmed: true,
              }}};
            });
          }).catch(function(err){
            console.warn('[loop-pc] Silence fallback load failed:', err);
          });
          return;
        }

        // If the channel already has the matching bank loaded, the audio
        // engine has already handled the in-bank PC swap — no-op here.
        var st = stateRef.current;
        var cs = st && st.channels && st.channels[samplerCh];
        if(cs && cs.loopBank === match.name) return;

        // Load the matching bank on this channel, then select the requested
        // PC. After both succeed, optimistically update channel state so the
        // UI reflects the swap immediately.
        BSInvoke('load_loop_bank', {
          channel: samplerCh,
          bankName: match.name,
        }).then(function(){
          return BSInvoke('set_active_loop_pc', {
            channel: samplerCh,
            pc: pc,
          });
        }).then(function(){
          setState(function(s){
            var prev = s.channels[samplerCh] || {};
            return {...s, channels: {...s.channels, [samplerCh]: {
              ...prev,
              loopBank: match.name,
              activeLoopPc: pc,
              pendingLoopPc: null,
              loopArmed: true,
            }}};
          });
        }).catch(function(err){
          console.warn('[loop-pc] cross-bank load failed:', err);
        });
      }).catch(function(err){
        console.warn('[loop-pc] list_loop_banks failed:', err);
      });
    }).then(function(fn){
      if(cancelled){ try { fn(); } catch(e){} } else { unlisten = fn; }
    }).catch(function(){});
    return function(){
      cancelled = true;
      if(unlisten){ try { unlisten(); } catch(e){} }
    };
  }, []);

  // Navigation also mirrors to the other screen (web ⇄ Pi) via BSInvoke('set_view').
  const onEdit = (chId)=>{ setState(s=>({...s, view:'edit', editingChannel: chId, selectedChannel: chId})); BSInvoke('set_view', {view:'edit', channelId: chId}).catch(function(){}); };
  const onBack = ()=>{ setState(s=>({...s, view:'mixer'})); BSInvoke('set_view', {view:'mixer'}).catch(function(){}); };
  const onMaster = ()=>{ setState(s=>({...s, view:'master'})); BSInvoke('set_view', {view:'master'}).catch(function(){}); };

  // Save snapshot ("project") feedback
  const [saveState, setSaveState] = useStateApp('idle'); // idle | saving | saved
  const [saveOpen, setSaveOpen] = useStateApp(false);
  const [snapshots, setSnapshots] = useStateApp(()=>{
    try { return JSON.parse(localStorage.getItem('brayer-snapshots-v2')) || []; }
    catch(e){ return []; }
  });
  const persistSnaps = (list)=>{
    setSnapshots(list);
    try { localStorage.setItem('brayer-snapshots-v2', JSON.stringify(list)); } catch(e){}
  };
  const onSave = ()=> setSaveOpen(v=>!v);
  const saveSnapshot = (name)=>{
    const sessionName = name || ('Session '+new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}));
    setSaveState('saving');
    setSaveOpen(false);

    const doLocalSave = ()=>{
      const snap = {
        id: 'snap-'+Date.now(),
        name: sessionName,
        ts: Date.now(),
        state: JSON.parse(JSON.stringify({channels:state.channels, master:state.master})),
      };
      persistSnaps([snap, ...snapshots].slice(0,12));
      setSaveState('saved');
      setTimeout(()=> setSaveState('idle'), 1500);
    };

    if (TauriAPI.available) {
      TauriAPI.saveSession(sessionName, state.channels, state.master).then(doLocalSave).catch(doLocalSave);
    } else {
      setTimeout(doLocalSave, 500);
    }
  };
  const loadSnapshot = (snap)=>{
    const nextChannels = JSON.parse(JSON.stringify(snap.state.channels));
    const nextMaster = JSON.parse(JSON.stringify(snap.state.master));
    setState(s=>({...s, channels: nextChannels, master: nextMaster}));
    // Replay the restored state to the audio engine — session load on the backend
    // only returns JSON; nothing there touches the live engine state.
    if (TauriAPI.available) {
      TauriAPI.syncStateToEngine({ channels: nextChannels, master: nextMaster });
    }
    setSaveOpen(false);
  };
  const deleteSnapshot = (id)=>{
    const snap = snapshots.find(s=>s.id===id);
    if (TauriAPI.available && snap && snap.name) {
      TauriAPI.deleteSession(snap.name).catch(function(){});
    }
    persistSnaps(snapshots.filter(s=>s.id!==id));
  };

  // Toggle the auto-load star on a snapshot. Only one entry can be starred at
  // a time — clicking another row moves the star; clicking the starred row
  // again clears it (no auto-load on next launch).
  const toggleAutoload = (id)=>{
    persistSnaps(snapshots.map(s=>({
      ...s,
      autoload: s.id === id ? !s.autoload : false,
    })));
  };

  // Overwrite an existing snapshot's stored state with the current mixer +
  // master state. Preserves id, name, and the autoload flag; bumps timestamp.
  // Mirrors saveSnapshot's local + Tauri dual-write so a Pi-side reboot picks
  // up the new state from the backend file too.
  const overwriteSnapshot = (snap)=>{
    setSaveState('saving');
    const doLocalSave = ()=>{
      const updated = {
        ...snap,
        ts: Date.now(),
        state: JSON.parse(JSON.stringify({channels: state.channels, master: state.master})),
      };
      persistSnaps(snapshots.map(s=>s.id===snap.id ? updated : s));
      setSaveState('saved');
      setTimeout(()=> setSaveState('idle'), 1500);
    };
    if (TauriAPI.available) {
      TauriAPI.saveSession(snap.name, state.channels, state.master).then(doLocalSave).catch(doLocalSave);
    } else {
      setTimeout(doLocalSave, 500);
    }
  };

  // Tweaks
  const [tweaksOn, setTweaksOn] = useStateApp(false);
  useEffectApp(()=>{
    const onMsg = (e)=>{
      if(!e.data || typeof e.data !== 'object') return;
      if(e.data.type === '__activate_edit_mode') setTweaksOn(true);
      if(e.data.type === '__deactivate_edit_mode') setTweaksOn(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({type:'__edit_mode_available'}, '*');
    return ()=> window.removeEventListener('message', onMsg);
  }, []);

  const activeMidi = Object.keys(midiActivity).length>0;

  // MIDI port popup
  const [midiPopupOpen, setMidiPopupOpen] = useStateApp(false);
  const toggleMidiPopup = function(){ setMidiPopupOpen(function(v){ return !v; }); };

  // Logo right-click context menu
  const [logoMenu, setLogoMenu] = useStateApp(null); // null | {x:number, y:number}
  const openLogoMenu = function(x, y){ setLogoMenu({x:x, y:y}); };
  const closeLogoMenu = function(){ setLogoMenu(null); };

  // Remote mode login overlay (click the BRAYER-SOUND logo to open)
  const [loginOpen, setLoginOpen] = useStateApp(false);
  // Pi-only relay dial-out setup panel (desktop build only; from logo menu)
  const [dialoutOpen, setDialoutOpen] = useStateApp(false);
  const [remoteInfo, setRemoteInfo] = useStateApp(null); // null | {ip, state}
  // Web build: is the user's Pi currently connected to the relay? Driven by the
  // relay's device_status frames (RemoteLink.deviceOnline). Default true so the
  // "connect your Pi" prompt doesn't flash before the first status arrives.
  const [deviceOnline, setDeviceOnline] = useStateApp(true);
  // Whether THIS build may act as a remote controller (show "Connect to
  // Sampler"). The Pi (Linux) is the instrument, never a controller, so the
  // backend reports false there and the logo login overlay stays disabled.
  const [remoteCapable, setRemoteCapable] = useStateApp(false);
  React.useEffect(function(){
    const off = RemoteLink.onChange(function(s){
      setRemoteInfo(s.state === 'local' ? null : { ip: s.ip, state: s.state });
      setDeviceOnline(RemoteLink.deviceOnline !== false);
    });
    return off;
  }, []);
  React.useEffect(function(){
    if(window.__TAURI__ && window.__TAURI__.core){
      window.__TAURI__.core.invoke('is_remote_capable')
        .then(function(v){ setRemoteCapable(!!v); })
        .catch(function(){});
    }
  }, []);
  // Pi desktop: show a "WebRemote" badge while this sampler is being driven over
  // the web relay (dial-out mode == 'remote'). Poll the backend status.
  const [webRemote, setWebRemote] = useStateApp(false);
  const [webFailures, setWebFailures] = useStateApp(0);
  React.useEffect(function(){
    if(window.BS_WEB || !(window.__TAURI__ && window.__TAURI__.core)) return;
    const tick = function(){
      window.__TAURI__.core.invoke('dialout_status')
        .then(function(s){ setWebRemote(!!(s && s.mode === 'remote')); setWebFailures((s && s.failures) || 0); })
        .catch(function(){});
    };
    tick();
    const t = setInterval(tick, 1500);
    return function(){ clearInterval(t); };
  }, []);

  return (
    <div className="app">
      {remoteInfo && (
        <div style={{flexShrink:0, height:18,
          display:'flex', alignItems:'center', justifyContent:'center', gap:10,
          background: remoteInfo.state==='connected' ? 'linear-gradient(90deg,#15102a,#221542,#15102a)' : '#33191a',
          borderBottom:'1px solid rgba(155,92,255,0.35)', fontFamily:'JetBrains Mono', fontSize:9.5, color:'#cbb8ff', letterSpacing:'.1em', whiteSpace:'nowrap'}}>
          <span style={{color: remoteInfo.state==='connected' ? '#8ef0b0' : '#ffb84c', fontSize:8}}>●</span>
          {remoteInfo.state==='connected' ? 'REMOTE — ' + remoteInfo.ip : (remoteInfo.state==='reconnecting' ? 'RECONNECTING…' : 'CONNECTING…')}
          <button onClick={function(){ RemoteLink.disconnect(); }}
            style={{background:'none', border:'1px solid rgba(155,92,255,0.4)', color:'#cbb8ff', borderRadius:4, padding:'0 7px', height:13, lineHeight:'12px', cursor:'pointer', fontFamily:'JetBrains Mono', fontSize:8.5}}>
            DISCONNECT
          </button>
        </div>
      )}
      <TopBar
        state={state}
        midiActive={activeMidi}
        midiLabel={midiInLabel}
        onHome={onBack}
        onMaster={onMaster}
        onSave={onSave}
        saveState={saveState}
        midiPopupOpen={midiPopupOpen}
        onToggleMidiPopup={toggleMidiPopup}
        onLogoContextMenu={openLogoMenu}
        webRemote={webRemote}
        webFailures={webFailures}
        onLogoClick={function(){
          // Windows controller: open the "Connect to Sampler" login.
          // Pi (desktop, not remote-capable, not the browser build): open the
          // relay dial-out setup directly on a single click.
          if(remoteCapable){ setLoginOpen(true); }
          else if(window.__TAURI__ && window.__TAURI__.core && !window.BS_WEB){ setDialoutOpen(true); }
        }}
      />
      {loginOpen && <LoginScreen
        onClose={function(){ setLoginOpen(false); }}
        onConnected={function(){ refreshSoundmaps(); }}
      />}
      {dialoutOpen && <PiDialoutPanel onClose={function(){ setDialoutOpen(false); }} />}
      {window.BS_WEB && remoteInfo && remoteInfo.state === 'connected' && !deviceOnline && (
        <div style={{position:'absolute', inset:0, zIndex:180,
          display:'flex', alignItems:'center', justifyContent:'center',
          background:'rgba(8,6,16,0.82)', backdropFilter:'blur(2px)'}}>
          <div style={{width:440, textAlign:'center', background:'linear-gradient(180deg,#1a162a,#100e1a)',
            border:'1px solid rgba(155,92,255,0.4)', borderRadius:18, padding:'30px 28px',
            fontFamily:'Rajdhani', color:'#e7deff', boxShadow:'0 30px 80px rgba(0,0,0,0.6)'}}>
            <div style={{transform:'scale(1.3)', display:'inline-block', marginBottom:14}}><LogoMark size={36}/></div>
            <div style={{fontSize:11, letterSpacing:'.3em', color:'#ffb84c', marginBottom:8}}>
              <span style={{fontSize:8}}>●</span> SAMPLER OFFLINE
            </div>
            <div style={{fontSize:20, fontWeight:800, letterSpacing:'.06em', marginBottom:10}}>
              Connect your Raspberry Pi
            </div>
            <div style={{fontSize:13, lineHeight:1.6, color:'#b3a8d6'}}>
              You're signed in, but your sampler isn't on the relay yet.
              On the Pi, tap the logo → <b style={{color:'#cbb8ff'}}>RELAY CONNECTION</b> → press
              <b style={{color:'#8ef0b0'}}> CONNECT</b>. This will clear automatically once it links up.
            </div>
          </div>
        </div>
      )}
      {midiPopupOpen && (
        <MidiPortPopup onClose={function(){ setMidiPopupOpen(false); }} midiInLabel={midiInLabel}/>
      )}
      {logoMenu && (
        <LogoContextMenu x={logoMenu.x} y={logoMenu.y} onClose={closeLogoMenu}
          remoteCapable={remoteCapable}
          onRelayDialout={function(){ setDialoutOpen(true); }}/>
      )}
      {saveOpen && (
        <SnapshotsDrawer
          snapshots={snapshots}
          onClose={()=>setSaveOpen(false)}
          onSaveNew={saveSnapshot}
          onLoad={loadSnapshot}
          onDelete={deleteSnapshot}
          onToggleAutoload={toggleAutoload}
          onOverwrite={overwriteSnapshot}
        />
      )}
      <div className="screen">
        {state.view==='mixer' && (
          <div style={{width:'100%',height:'100%'}} data-screen-label="Mixer">
            <MixerScreen state={state} setState={setState} onEdit={onEdit} onOpenMaster={onMaster}/>
          </div>
        )}
        {state.view==='edit' && (
          <div style={{width:'100%',height:'100%'}} data-screen-label="Channel Edit">
            <ChannelEditScreen state={state} setState={setState} chId={state.editingChannel} onBack={onBack} midiActivity={midiActivity} soundmapLibrary={soundmapLibrary} refreshSoundmaps={refreshSoundmaps}/>
          </div>
        )}
        {state.view==='master' && (
          <div style={{width:'100%',height:'100%'}} data-screen-label="Master">
            <MasterScreen state={state} setState={setState} onBack={onBack}/>
          </div>
        )}
      </div>

      {tweaksOn && <TweaksPanel state={state} setState={setState}/>}
    </div>
  );
}

function SnapshotsDrawer({ snapshots, onClose, onSaveNew, onLoad, onDelete, onToggleAutoload, onOverwrite }){
  const [name, setName] = React.useState('');
  const fmt = (ts)=>{
    const d = new Date(ts);
    const now = Date.now();
    const diff = (now - ts)/1000;
    if(diff < 60) return 'just now';
    if(diff < 3600) return Math.floor(diff/60)+'m ago';
    if(diff < 86400) return Math.floor(diff/3600)+'h ago';
    return d.toLocaleDateString();
  };
  return (
    <>
      <div onClick={onClose} style={{position:'absolute',inset:0,background:'rgba(0,0,0,0.45)',zIndex:40}}/>
      <div style={{position:'absolute',top:46,right:10,width:320,maxHeight:510,background:'var(--bg-1)',border:'1px solid var(--line)',borderRadius:10,padding:12,zIndex:41,boxShadow:'0 20px 60px rgba(0,0,0,0.7)',display:'flex',flexDirection:'column',gap:10,fontFamily:'Rajdhani'}}>
        <div className="row gap-8" style={{justifyContent:'space-between',alignItems:'center'}}>
          <div style={{fontWeight:700,fontSize:12,letterSpacing:'.2em',textTransform:'uppercase',color:'var(--ink)'}}>Sessions</div>
          <button onClick={onClose} style={{background:'none',border:'none',color:'var(--ink-mute)',cursor:'pointer',fontSize:14}}>✕</button>
        </div>

        <div style={{background:'var(--bg-0)',border:'1px solid var(--line-soft)',borderRadius:6,padding:8,display:'flex',flexDirection:'column',gap:6}}>
          <div style={{fontFamily:'JetBrains Mono',fontSize:9,color:'var(--ink-mute)',letterSpacing:'.1em'}}>SAVE CURRENT SESSION</div>
          <div className="row gap-6">
            <input
              value={name} onChange={e=>setName(e.target.value)}
              onKeyDown={e=>{if(e.key==='Enter'){onSaveNew(name.trim()); setName('');}}}
              placeholder="Session name…"
              style={{flex:1,background:'var(--bg-2)',border:'1px solid var(--line)',borderRadius:4,padding:'6px 8px',color:'var(--ink)',fontFamily:'JetBrains Mono',fontSize:10,outline:'none'}}
            />
            <button className="tb-btn is-active" style={{minWidth:60}}
              onClick={()=>{onSaveNew(name.trim()); setName('');}}>SAVE</button>
          </div>
        </div>

        <div style={{fontFamily:'JetBrains Mono',fontSize:9,color:'var(--ink-mute)',letterSpacing:'.1em'}}>SAVED · {snapshots.length}/12</div>

        <div style={{overflowY:'auto',display:'flex',flexDirection:'column',gap:4,flex:1}}>
          {snapshots.length===0 && (
            <div style={{textAlign:'center',color:'var(--ink-mute)',fontSize:10,padding:'18px 0',fontFamily:'JetBrains Mono'}}>
              no saved sessions yet
            </div>
          )}
          {snapshots.map(s=>(
            <div key={s.id} className="row gap-6"
                 style={{padding:'7px 8px',background:'var(--bg-0)',border:'1px solid var(--line-soft)',borderRadius:5,alignItems:'center'}}>
              <button onClick={()=>onToggleAutoload(s.id)}
                      title={s.autoload ? 'Auto-load on startup (click to clear)' : 'Auto-load this session on startup'}
                      style={{
                        background:'none',
                        border:'none',
                        color: s.autoload ? '#ffb84c' : 'var(--ink-mute)',
                        cursor:'pointer',
                        fontSize:16,
                        lineHeight:1,
                        padding:'0 2px',
                        flexShrink:0,
                      }}>
                {s.autoload ? '★' : '☆'}
              </button>
              <div className="col gap-2" style={{flex:1,minWidth:0}}>
                <div style={{fontWeight:600,fontSize:12,color:'var(--ink)',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{s.name}</div>
                <div style={{fontFamily:'JetBrains Mono',fontSize:8.5,color:'var(--ink-mute)',letterSpacing:'.05em'}}>{fmt(s.ts)} · 8CH + MASTER</div>
              </div>
              <button className="tb-btn" style={{padding:'4px 8px',fontSize:9}}
                      onClick={()=>onOverwrite(s)}
                      title="Overwrite this session with current state">SAVE</button>
              <button className="tb-btn" style={{padding:'4px 8px',fontSize:9}} onClick={()=>onLoad(s)}>LOAD</button>
              <button onClick={()=>onDelete(s.id)}
                      style={{background:'none',border:'1px solid var(--line)',color:'var(--ink-mute)',borderRadius:4,padding:'4px 6px',cursor:'pointer',fontSize:11,lineHeight:1}}
                      title="Delete">✕</button>
            </div>
          ))}
        </div>
      </div>
    </>
  );
}

function TweaksPanel({ state, setState }){
  return (
    <div style={{position:'absolute',right:12,bottom:12,width:240,background:'var(--bg-1)',border:'1px solid var(--line)',borderRadius:10,padding:12,zIndex:30,boxShadow:'0 10px 30px rgba(0,0,0,0.6)',display:'flex',flexDirection:'column',gap:8}}>
      <div style={{fontFamily:'Rajdhani',fontWeight:700,fontSize:11,letterSpacing:'.2em',textTransform:'uppercase',color:'var(--ink-mute)'}}>Tweaks</div>
      <button className="tb-btn" onClick={()=>setState(s=>({...s, view:'mixer'}))}>Go to mixer</button>
      <button className="tb-btn" onClick={()=>setState(s=>({...s, view:'edit', editingChannel: s.selectedChannel ?? 0}))}>Open current channel</button>
      <button className="tb-btn" onClick={()=>setState(s=>({...s, view:'master'}))}>Open master bus</button>
      <div style={{fontFamily:'JetBrains Mono',fontSize:9,color:'var(--ink-mute)',lineHeight:1.5,marginTop:4}}>
        tap channel strips to select · tap EDIT CHANNEL to open editor
      </div>
    </div>
  );
}

/* Scale viewport to fit window */
function fitViewport(){
  const frame = document.getElementById('viewport-frame');
  if(!frame) return;
  const W = 1024, H = 600;
  const sx = window.innerWidth / W;
  const sy = window.innerHeight / H;
  const s = Math.min(sx, sy);
  frame.style.transform = `scale(${s})`;
  // Shrink the grid cell so flex layout knows the scaled size
  frame.style.width = W + 'px';
  frame.style.height = H + 'px';
  frame.style.margin = `${(H*s - H)/2}px ${(W*s - W)/2}px`;
}
window.addEventListener('resize', fitViewport);
fitViewport();
setTimeout(fitViewport, 50);
setTimeout(fitViewport, 300);

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
