=== FXScreen (block library) ===
function FXScreen({ state, connected }) {
  const [fxList, setFxList] = useState(null);
  const [loading, setLoading] = useState(true);
  const [showAdd, setShowAdd] = useState(false);
  const [paramSchemas, setParamSchemas] = useState({});

  // Load block param schemas
  const loadParams = useCallback(async (fxType) => {
    if (paramSchemas[fxType]) return paramSchemas[fxType];
    try {
      const data = await apiGet(`/api/block-params/${fxType}`);
      setParamSchemas(prev => ({ ...prev, [fxType]: data.params || [] }));
      return data.params || [];
    } catch (e) { return []; }
  }, [paramSchemas]);

  useEffect(() => {
    if (!connected || !state?.current_preset) {
      // Offline: show a simple empty state
      setFxList([]);
      setLoading(false);
      return;
    }

    // Load active preset's chain from presets API
    apiGet('/api/presets').then(async data => {
      const banks = data.banks || [];
      const cp = state.current_preset;
      const preset = banks[cp.bank]?.presets?.[cp.program];
      if (preset?.chain) {
        const blocks = await Promise.all(preset.chain.map(async (b, i) => {
          const params = await loadParams(b.fx_type);
          return {
            id: i + 1,
            title: FX_TYPE_LABELS[b.fx_type] || b.fx_type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
            type: b.fx_type,
            bypassed: b.bypass || !b.enabled,
            params: params,
          };
        }));
        setFxList(blocks);
      }
      setLoading(false);
    }).catch(() => {
      setFxList([]);
      setLoading(false);
    });
  }, [connected, state, loadParams]);

  const toggle = (id) => setFxList(prev => prev ? prev.map(f => f.id === id ? { ...f, bypassed: !f.bypassed } : f) : prev);

  const addBlock = async (fxType) => {
    setShowAdd(false);
    const params = await loadParams(fxType);
    setFxList(prev => [...(prev || []), {
      id: Date.now(),
      title: FX_TYPE_LABELS[fxType] || fxType,
      type: fxType,
      bypassed: false,
      params: params,
    }]);
  };

  if (loading) return <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}><div className="loader" /></div>;

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <div className="screen-header">
        <div className="screen-title">FX Chain {(fxList?.length || 0) > 0 ? `(${fxList.length})` : ''}</div>
        <button className="btn btn-ghost btn-sm" onClick={() => setShowAdd(true)}>+ Add Block</button>
      </div>
      <div className="screen-body">
        {/* Signal path */}
        {fxList && fxList.length > 0 && (
          <div style={{ display: "flex", alignItems: "center", gap: 4, padding: "4px 2px", overflowX: "auto" }}>
            {["IN", ...fxList.map(f => f.type.toUpperCase()), "OUT"].map((s, i, arr) => (
              <div key={i} style={{ display: "flex", alignItems: "center", gap: 4, flexShrink: 0 }}>
                <div style={{ padding: "3px 8px", borderRadius: 4, fontSize: 10, fontWeight: 700,
                  background: s === "IN" || s === "OUT" ? T.blueDim : T.amberDim,
                  color: s === "IN" || s === "OUT" ? T.blue : T.amber,
                  border: `1px solid ${(s === "IN" || s === "OUT" ? T.blue : T.amber)}60` }}>{s}</div>
                {i < arr.length - 1 && <div style={{ width: 12, height: 2, background: T.border, borderRadius: 1 }} />}
              </div>
            ))}
          </div>
        )}
        {(!fxList || fxList.length === 0) ? (
          <div style={{ textAlign: "center", padding: 30, color: T.textDim, fontSize: 14 }}>
            No FX blocks in the chain.<br />
            <button className="btn btn-ghost btn-sm" style={{ marginTop: 10 }} onClick={() => setShowAdd(true)}>+ Add your first block</button>
          </div>
        ) : (
          fxList.map(f => (
            <FXBlock key={f.id} {...f} active={!f.bypassed} onToggle={() => toggle(f.id)} />
          ))
        )}
      </div>

      {/* Add Block slide panel */}
      {showAdd && (
        <SlidePanel onClose={() => setShowAdd(false)}>
          <div className="screen-header">
            <div className="screen-title">Add FX Block</div>
          </div>
          <div className="screen-body" style={{ gap: 4 }}>
            {Object.entries(FX_TYPE_LABELS).map(([key, label]) => (
              <div key={key} className="preset-row" onClick={() => addBlock(key)} style={{ borderRadius: 6 }}>
                <span className="badge badge-blue" style={{ width: 60, flexShrink: 0 }}>{key.split('_')[0]}</span>
                <span style={{ flex: 1, fontSize: 13 }}>{label}</span>
                <span style={{ color: T.textDim }}>+</span>
              </div>
            ))}
          </div>
        </SlidePanel>
      )}
    </div>
  );
}

// 3. MODELS SCREEN ─────────────────────────────────────────────


=== ModelsScreen ===
function ModelsScreen({ state, connected, refresh }) {
  const [models, setModels] = useState([]);
  const [current, setCurrent] = useState(null);
  const [loading, setLoading] = useState(true);
  const [searching, setSearching] = useState(false);
  const [searchQ, setSearchQ] = useState('');
  const [searchResults, setSearchResults] = useState([]);
  const [showSearch, setShowSearch] = useState(false);
  const [uploading, setUploading] = useState(false);
  const fileRef = useRef(null);

  const loadModels = useCallback(async () => {
    if (!connected) { setLoading(false); return; }
    try {
      const data = await apiGet('/api/models');
      setModels(data.models || []);
      setCurrent(data.current);
    } catch (e) { /* ignore */ }
    setLoading(false);
  }, [connected]);

  useEffect(() => { loadModels(); }, [loadModels]);

  const handleLoad = async (path) => {
    try { await apiPost('/api/models/load', { path }); await loadModels(); refresh(); } catch (e) { alert('Failed to load model'); }
  };

  const handleUnload = async () => {
    try { await apiPost('/api/models/unload'); setCurrent(null); await loadModels(); refresh(); } catch (e) { alert('Failed to unload'); }
  };

  const handleUpload = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setUploading(true);
    try {
      await apiUpload('/api/models/upload', file);
      await loadModels();
    } catch (err) { alert('Upload failed'); }
    setUploading(false);
  };

  const handleSearch = useCallback(async () => {
    if (!searchQ.trim()) return;
    setSearching(true);
    try {
      const data = await apiGet(`/api/models/tonedownload/search?q=${encodeURIComponent(searchQ)}`);
      setSearchResults(data.results || []);
    } catch (e) { alert('Search failed'); }
    setSearching(false);
  }, [searchQ]);

  const handleInstall = async (result) => {
    try {
      await apiPost('/api/models/tonedownload/install', {
        download_url: result.download_url,
        name: result.name || result.filename,
        id: result.id,
        size: result.size,
        pack_name: result.pack_name,
        display_name: result.name,
      });
      await loadModels();
      refresh();
    } catch (e) { alert('Install failed'); }
  };

  if (loading) return <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}><div className="loader" /></div>;

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <div className="screen-header">
        <div className="screen-title">NAM Models ({models.length})</div>
        <div style={{ display: "flex", gap: 6 }}>
          <button className="btn btn-ghost btn-sm" onClick={() => setShowSearch(!showSearch)}>
            {showSearch ? "Local" : "Search"}
          </button>
          <button className="btn btn-primary btn-sm" onClick={() => fileRef.current?.click()}>
            {uploading ? "..." : "+ Upload"}
          </button>
          <input ref={fileRef} type="file" accept=".nam" style={{ display: "none" }} onChange={handleUpload} />
        </div>
      </div>

      {showSearch && (
        <div style={{ padding: "8px 12px", borderBottom: `1px solid ${T.border}` }}>
          <div className="search-bar">
            <span style={{ color: T.textDim }}>🔍</span>
            <input type="text" placeholder="Search Tone3000…" value={searchQ}
              onChange={e => setSearchQ(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearch()} />
            <button className="btn btn-primary btn-sm" onClick={handleSearch} disabled={searching}>
              {searching ? "..." : "Go"}
            </button>
          </div>
          {searchResults.length > 0 && (
            <div style={{ marginTop: 8, maxHeight: 240, overflow: "auto" }}>
              {searchResults.map(r => (
                <div key={r.id} className="preset-row" onClick={() => handleInstall(r)} style={{ borderBottom: `1px solid ${T.border}`, borderRadius: 0 }}>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 13 }}>{r.name}</div>
                    <div style={{ fontSize: 10, color: T.textDim }}>{r.author} · {r.size_display} · {r.architecture}</div>
                  </div>
                  <button className="btn btn-primary btn-sm">Get</button>
                </div>
              ))}
            </div>
          )}
        </div>
      )}

      <div className="screen-body">
        {current && (
          <div className="card-sm">
            <div className="section-label" style={{ marginBottom: 6 }}>Loaded Model</div>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <div className="nam-chip">{current}</div>
              <button className="btn btn-danger btn-sm" onClick={handleUnload}>Unload</button>
            </div>
          </div>
        )}

        {models.length === 0 ? (
          <div style={{ textAlign: "center", padding: 30, color: T.textDim, fontSize: 14 }}>
            No NAM models found.<br />Upload a .nam file or search Tone3000.
          </div>
        ) : (
          models.map(m => (
            <div key={m.name} className={`preset-row ${m.loaded ? "active" : ""}`}>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 13, fontWeight: 500 }}>{m.name}</div>
                <div style={{ fontSize: 10, color: T.textDim }}>{m.architecture} · {m.size_mb} MB</div>
              </div>
              {m.loaded ? (
                <span className="badge badge-green">Loaded</span>
              ) : (
                <button className="btn btn-ghost btn-sm" onClick={() => handleLoad(m.path)}>Load</button>
              )}
            </div>
          ))
        )}
      </div>
    </div>
  );
}

// 4. IRS SCREEN ────────────────────────────────────────────────


=== IRsScreen ===
function IRsScreen({ state, connected, refresh }) {
  const [irs, setIrs] = useState([]);
  const [current, setCurrent] = useState(null);
  const [loading, setLoading] = useState(true);
  const [searching, setSearching] = useState(false);
  const [searchQ, setSearchQ] = useState('');
  const [searchResults, setSearchResults] = useState([]);
  const [showSearch, setShowSearch] = useState(false);
  const [uploading, setUploading] = useState(false);
  const fileRef = useRef(null);

  const loadIrs = useCallback(async () => {
    if (!connected) { setLoading(false); return; }
    try {
      const data = await apiGet('/api/irs');
      setIrs(data.irs || []);
      setCurrent(data.current);
    } catch (e) { /* ignore */ }
    setLoading(false);
  }, [connected]);

  useEffect(() => { loadIrs(); }, [loadIrs]);

  const handleLoad = async (path) => {
    try { await apiPost('/api/irs/load', { path }); await loadIrs(); refresh(); } catch (e) { alert('Failed to load IR'); }
  };

  const handleUnload = async () => {
    try { await apiPost('/api/irs/unload'); setCurrent(null); await loadIrs(); refresh(); } catch (e) { alert('Failed to unload'); }
  };

  const handleUpload = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setUploading(true);
    try {
      await apiUpload('/api/irs/upload', file);
      await loadIrs();
    } catch (err) { alert('Upload failed'); }
    setUploading(false);
  };

  const handleSearch = useCallback(async () => {
    if (!searchQ.trim()) return;
    setSearching(true);
    try {
      const data = await apiGet(`/api/irs/tonedownload/search?q=${encodeURIComponent(searchQ)}`);
      setSearchResults(data.results || []);
    } catch (e) { alert('Search failed'); }
    setSearching(false);
  }, [searchQ]);

  const handleInstall = async (result) => {
    try {
      await apiPost('/api/irs/tonedownload/install', {
        download_url: result.download_url,
        name: result.name || result.filename,
        id: result.id,
        size: result.size,
        display_name: result.name,
      });
      await loadIrs();
      refresh();
    } catch (e) { alert('Install failed'); }
  };

  if (loading) return <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}><div className="loader" /></div>;

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <div className="screen-header">
        <div className="screen-title">IR Files ({irs.length})</div>
        <div style={{ display: "flex", gap: 6 }}>
          <button className="btn btn-ghost btn-sm" onClick={() => setShowSearch(!showSearch)}>
            {showSearch ? "Local" : "Search"}
          </button>
          <button className="btn btn-primary btn-sm" onClick={() => fileRef.current?.click()}>
            {uploading ? "..." : "+ Upload"}
          </button>
          <input ref={fileRef} type="file" accept=".wav" style={{ display: "none" }} onChange={handleUpload} />
        </div>
      </div>

      {showSearch && (
        <div style={{ padding: "8px 12px", borderBottom: `1px solid ${T.border}` }}>
          <div className="search-bar">
            <span style={{ color: T.textDim }}>🔍</span>
            <input type="text" placeholder="Search Tone3000 IRs…" value={searchQ}
              onChange={e => setSearchQ(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearch()} />
            <button className="btn btn-primary btn-sm" onClick={handleSearch} disabled={searching}>
              {searching ? "..." : "Go"}
            </button>
          </div>
          {searchResults.length > 0 && (
            <div style={{ marginTop: 8, maxHeight: 240, overflow: "auto" }}>
              {searchResults.map(r => (
                <div key={r.id} className="preset-row" onClick={() => handleInstall(r)} style={{ borderBottom: `1px solid ${T.border}`, borderRadius: 0 }}>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 13 }}>{r.name}</div>
                    <div style={{ fontSize: 10, color: T.textDim }}>{r.author} · {r.size_display}</div>
                  </div>
                  <button className="btn btn-primary btn-sm">Get</button>
                </div>
              ))}
            </div>
          )}
        </div>
      )}

      <div className="screen-body">
        {current && (
          <div className="card-sm">
            <div className="section-label" style={{ marginBottom: 6 }}>Loaded IR</div>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <div className="nam-chip" style={{ color: T.green, borderColor: `${T.green}55`, background: `${T.green}15` }}>{current}</div>
              <button className="btn btn-danger btn-sm" onClick={handleUnload}>Unload</button>
            </div>
          </div>
        )}

        {irs.length === 0 ? (
          <div style={{ textAlign: "center", padding: 30, color: T.textDim, fontSize: 14 }}>
            No IR files found.<br />Upload a .wav file or search Tone3000.
          </div>
        ) : (
          irs.map(ir => (
            <div key={ir.name} className={`preset-row ${ir.loaded ? "active" : ""}`}>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 13, fontWeight: 500 }}>{ir.name}</div>
                <div style={{ fontSize: 10, color: T.textDim }}>{ir.sample_rate} Hz · {ir.length_ms} ms · {ir.num_taps} taps</div>
              </div>
              {ir.loaded ? (
                <span className="badge badge-green">Loaded</span>
              ) : (
                <button className="btn btn-ghost btn-sm" onClick={() => handleLoad(ir.path)}>Load</button>
              )}
            </div>
          ))
        )}
      </div>
    </div>
  );
}

// 5. PRESETS SCREEN ────────────────────────────────────────────


=== PresetsScreen ===
function PresetsScreen({ state, connected, refresh }) {
  const [banks, setBanks] = useState([]);
  const [selectedBank, setSelectedBank] = useState(0);
  const [loading, setLoading] = useState(true);
  const [saveName, setSaveName] = useState('');
  const [showSave, setShowSave] = useState(false);
  const [deleting, setDeleting] = useState(null);

  const loadPresets = useCallback(async () => {
    if (!connected) {
      setBanks([{ name: "Default", number: 0, presets: [
        { name: "Edge of Breakup", bank: 0, program: 0 },
        { name: "Modern High Gain", bank: 0, program: 1 },
        null, null,
      ]}]);
      setLoading(false);
      return;
    }
    try {
      const data = await apiGet('/api/presets');
      setBanks(data.banks || []);
    } catch (e) { /* ignore */ }
    setLoading(false);
  }, [connected]);

  useEffect(() => { loadPresets(); }, [loadPresets]);

  const handleActivate = async (bank, program) => {
    try {
      await apiPost(`/api/presets/${bank}/${program}/activate`);
      await loadPresets();
      refresh();
    } catch (e) { alert('Failed to load preset'); }
  };

  const handleSave = async () => {
    if (!saveName.trim()) return;
    const currentBank = state?.current_preset?.bank ?? 0;
    const currentProgram = state?.current_preset?.program ?? 0;
    try {
      await apiPut(`/api/presets/${currentBank}/${currentProgram}`, { name: saveName });
      setShowSave(false);
      await loadPresets();
    } catch (e) { alert('Failed to save'); }
  };

  const handleNewPreset = async () => {
    // Find the first empty slot, or create at next program number
    const bank = selectedBank;
    const bankData = banks[bank];
    const presets = bankData?.presets || [];
    let nextProg = presets.findIndex(p => p == null);
    if (nextProg === -1) nextProg = presets.length;
    const name = `New Preset ${nextProg + 1}`;
    try {
      await apiPut(`/api/presets/${bank}/${nextProg}`, { name });
      await loadPresets();
      await apiPost(`/api/presets/${bank}/${nextProg}/activate`);
      refresh();
    } catch (e) { alert('Failed to create preset'); }
  };

  const handleDelete = async (bank, program) => {
    try {
      await apiDelete(`/api/presets/${bank}/${program}`);
      setDeleting(null);
      await loadPresets();
    } catch (e) { alert('Failed to delete'); }
  };

  if (loading) return <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}><div className="loader" /></div>;

  const currentPreset = state?.current_preset;
  const bankData = banks[selectedBank];
  const presets = bankData?.presets || [];

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <div className="screen-header">
        <div className="screen-title">Presets</div>
        <div style={{ display: "flex", gap: 6 }}>
          <button className="btn btn-ghost btn-sm" onClick={() => setShowSave(true)}>Save</button>
          <button className="btn btn-primary btn-sm" onClick={handleNewPreset}>+ New</button>
        </div>
      </div>

      <div className="screen-body">
        {/* Bank selector */}
        {banks.length > 1 && (
          <div className="toggle">
            {banks.map((b, i) => (
              <button key={b.number} className={`toggle-btn ${selectedBank === i ? "active" : ""}`}
                onClick={() => setSelectedBank(i)}>{b.name}</button>
            ))}
          </div>
        )}

        {/* Current preset indicator */}
        {currentPreset && (
          <div className="card-sm" style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 11, color: T.textDim }}>Current</div>
              <div style={{ fontSize: 14, fontWeight: 600, color: T.amber }}>
                {currentPreset.name}
                <span className="mono" style={{ fontSize: 10, color: T.textDim, marginLeft: 8 }}>
                  Bank {currentPreset.bank} · Prog {currentPreset.program}
                </span>
              </div>
            </div>
          </div>
        )}

        {/* Preset list */}
        {presets.length === 0 ? (
          <div style={{ textAlign: "center", padding: 30, color: T.textDim, fontSize: 14 }}>No presets in this bank.</div>
        ) : (
          presets.map((p, i) => {
            if (!p) return null;
            const isActive = currentPreset?.bank === p.bank && currentPreset?.program === p.program;
            const isDeleting = deleting === `${p.bank}-${p.program}`;
            return (
              <div key={i} className={`preset-row ${isActive ? "active" : ""}`}
                onClick={() => !isDeleting && handleActivate(p.bank, p.program)}>
                <div className="mono" style={{ width: 28, height: 28, borderRadius: 6, display: "flex",
                  alignItems: "center", justifyContent: "center", fontSize: 12, fontWeight: 700,
                  background: isActive ? `${T.amber}22` : T.surface,
                  color: isActive ? T.amber : T.textDim,
                  border: `1px solid ${isActive ? T.amber+"60" : T.border}` }}>{p.program + 1}</div>
                <div style={{ flex: 1 }}>
                  <div style={{ fontSize: 13, fontWeight: 600 }}>{p.name}</div>
                  <div style={{ fontSize: 10, color: T.textDim }}>Bank {p.bank} · Program {p.program}</div>
                </div>
                {isActive && <span style={{ color: T.amber, fontSize: 16 }}>●</span>}
                {isDeleting ? (
                  <div style={{ display: "flex", gap: 4 }}>
                    <button className="btn btn-danger btn-sm" onClick={e => { e.stopPropagation(); handleDelete(p.bank, p.program); }}>Confirm</button>
                    <button className="btn btn-ghost btn-sm" onClick={e => { e.stopPropagation(); setDeleting(null); }}>Cancel</button>
                  </div>
                ) : (
                  <button className="btn btn-ghost btn-sm" style={{ color: T.red }}
                    onClick={e => { e.stopPropagation(); setDeleting(`${p.bank}-${p.program}`); }}>Delete</button>
                )}
              </div>
            );
          })
        )}
      </div>

      {/* Save dialog */}
      {showSave && (
        <SlidePanel onClose={() => setShowSave(false)}>
          <div className="screen-header">
            <div className="screen-title">Save Preset</div>
          </div>
          <div className="screen-body">
            <div className="search-bar">
              <input type="text" placeholder="Preset name…" value={saveName}
                onChange={e => setSaveName(e.target.value)} autoFocus
                onKeyDown={e => e.key === 'Enter' && handleSave()} />
            </div>
            <div style={{ display: "flex", gap: 8 }}>
              <button className="btn btn-primary" style={{ flex: 1 }} onClick={handleSave}>Save</button>
              <button className="btn btn-ghost" onClick={() => setShowSave(false)}>Cancel</button>
            </div>
          </div>
        </SlidePanel>
      )}
    </div>
  );
}

// 6. SETTINGS SCREEN ───────────────────────────────────────────
