// Spellstash Card Search — tell it what you're looking for, in your own
// words. The AI reads Scryfall's actual syntax rulebook
// (data/scryfall-syntax-reference.txt, captured verbatim) and writes the
// query; results land as a fast image grid with a quiet mark on the cards
// you already own. Prices and actions live in the popup, where intent
// shifts from scanning to deciding. Contract: docs/CARD_SEARCH_PLAN.md
// plus the 8-04 redesign ruling. Raw Scryfall syntax runs untouched.
//
// NO in-page mic and NO auto-search (Dwayne 8-04): the iPhone keyboard's
// own dictation covers voice, and every search fires only from an explicit
// Search tap (or Enter / a tapped example) so translate credits are never
// burned by a page acting on its own.
const { useState, useEffect, useRef, useCallback } = React;
const { CardPopup, OwnThisModal, WishlistModal, ClipIcon } = window.ssCardPopup;

const T = {
  accent: "var(--cm-accent)",
  accentFg: "var(--cm-accent-fg)",
  accentSoft: "var(--cm-accent-soft)",
  bg: "var(--bg)", bg2: "var(--bg-2)", bg3: "var(--bg-3)",
  line: "var(--line)",
  ink: "var(--ink)", ink2: "var(--ink-2)", ink3: "var(--ink-3)", ink4: "var(--ink-4)",
  good: "var(--good)",
  serif: "var(--cm-serif)", sans: "var(--cm-sans)", mono: "var(--cm-mono)",
};

// Permanent event names (surface prefix convention). Never rename.
function track(name, props) {
  try { window.plausible && window.plausible(name, { props: props || {} }); } catch (e) { /* best effort */ }
}

// ── Recent searches (last 5 Scryfall SYNTAX strings, not the raw ask) ────
// Persisted so leaving the page and coming back keeps your last searches one
// tap away. Syntax only by design (Dwayne 8-09): the natural-language text is
// personal and disposable, the compiled query is the reusable thing.
const RECENTS_KEY = "ss-lookup-recents";
const RECENTS_MAX = 5;
function loadRecents() {
  try {
    const raw = JSON.parse(localStorage.getItem(RECENTS_KEY) || "[]");
    return Array.isArray(raw) ? raw.filter((s) => typeof s === "string" && s.trim()).slice(0, RECENTS_MAX) : [];
  } catch (e) { return []; }
}
function pushRecent(list, syntax) {
  const s = (syntax || "").trim();
  if (!s) return list;
  // De-dupe (move-to-front), cap at 5.
  const next = [s, ...list.filter((x) => x !== s)].slice(0, RECENTS_MAX);
  try { localStorage.setItem(RECENTS_KEY, JSON.stringify(next)); } catch (e) { /* best effort */ }
  return next;
}

// ── Page-scoped buy list (same anatomy as the Brewer/Synergy pair) ───────
const PICKS_KEY = "ss-lookup-picks";
function loadPicks() {
  try {
    const raw = JSON.parse(localStorage.getItem(PICKS_KEY) || "[]");
    return Array.isArray(raw) ? raw.filter((p) => p && p.name) : [];
  } catch (e) { return []; }
}
function picksToPlainList(picks) {
  return picks.map((p) => "1 " + p.name).join("\n");
}
function ckBulkBuy(picks) {
  const plain = picksToPlainList(picks);
  try { navigator.clipboard && navigator.clipboard.writeText(plain); } catch (e) { /* best effort */ }
  const form = document.createElement("form");
  form.method = "POST";
  form.action = "https://www.cardkingdom.com/builder?partner=spellstash&utm_source=spellstash&utm_medium=affiliate&utm_campaign=card-search";
  form.target = "_blank";
  const input = document.createElement("input");
  input.type = "hidden";
  input.name = "c";
  input.value = plain;
  form.appendChild(input);
  document.body.appendChild(form);
  form.submit();
  form.remove();
}

function PickBar({ picks, copied, onBuyAll, onCopy, onClear }) {
  if (!picks.length) return null;
  return (
    <div className="cm-bulkbar" role="region" aria-label="Buy list">
      <span className="cm-mono" style={{ color: T.accent, fontWeight: 700 }}>
        <ClipIcon filled /> {picks.length} clipped
      </span>
      <button type="button" className="cm-btn cm-btn--primary cm-btn--sm" onClick={onBuyAll}>Buy all at CK</button>
      <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" onClick={onCopy}>{copied ? "✓ Copied" : "Copy"}</button>
      <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" onClick={onClear}>Clear</button>
    </div>
  );
}

function ClipDrawer({ picks, copied, onCopy, onBuyAll, onRemove, onClear }) {
  const [open, setOpen] = useState(false);
  if (!picks.length) return null;
  return (
    <div style={{ position: "fixed", top: 74, right: 12, zIndex: 60 }}>
      <button type="button" onClick={() => setOpen((o) => !o)}
        aria-expanded={open} aria-label={"Your buy list: " + picks.length + " clipped"}
        style={{
          display: "inline-flex", alignItems: "center", gap: 6,
          background: T.accent, color: T.accentFg, border: "none",
          borderRadius: 999, padding: "0.5rem 0.8rem", minHeight: 44,
          font: "700 13px " + T.mono, cursor: "pointer",
          boxShadow: "0 4px 14px rgba(0,0,0,0.25)",
        }}>
        <ClipIcon filled /> {picks.length}
      </button>
      {open && (
        <div style={{
          position: "absolute", right: 0, marginTop: 8, width: "min(86vw, 320px)",
          background: T.bg2, border: "1px solid " + T.line, borderRadius: 12,
          boxShadow: "0 10px 30px rgba(0,0,0,0.3)", padding: "0.6rem",
          maxHeight: "60vh", overflowY: "auto",
        }}>
          <div style={{ font: "700 11px " + T.mono, color: T.ink3, textTransform: "uppercase", letterSpacing: "0.06em", padding: "0.2rem 0.3rem" }}>
            Your buy list
          </div>
          {picks.map((p) => (
            <div key={p.name} style={{ display: "flex", alignItems: "center", gap: 8, padding: "0.35rem 0.3rem", borderBottom: "1px dashed " + T.line }}>
              <span style={{ flex: 1, minWidth: 0, overflowWrap: "anywhere", color: T.ink, fontSize: "0.9rem" }}>{p.name}</span>
              <button type="button" onClick={() => onRemove(p)} aria-label={"Remove " + p.name}
                style={{ border: "1px solid " + T.line, background: "transparent", color: T.ink3, borderRadius: 999, width: 44, height: 44, cursor: "pointer", flexShrink: 0 }}>✕</button>
            </div>
          ))}
          <div style={{ display: "flex", gap: 8, marginTop: "0.6rem", flexWrap: "wrap" }}>
            <button type="button" className="cm-btn cm-btn--primary cm-btn--sm" style={{ minHeight: 44 }} onClick={onBuyAll}>Buy all at CK</button>
            <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44 }} onClick={onCopy}>{copied ? "✓ Copied" : "Copy"}</button>
            <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44 }} onClick={onClear}>Clear</button>
          </div>
        </div>
      )}
    </div>
  );
}

// Raw syntax runs untouched; anything else goes through the translator.
function looksLikeSyntax(q) {
  return /[:{}<>=]|^!/.test(q);
}

// Natural-language examples: each one runs through the real translator, so
// the blank page teaches the whole trick.
const STARTERS = [
  "Elves for a Sultai commander deck with tap abilities",
  "Cheap board wipes for commander",
  "Dragons under five mana that fly",
  "Artifacts that make treasure tokens",
];

// Did-you-mean for zero-result nameish queries, off the cheap autocomplete.
function DidYouMean({ query, onPick }) {
  const [names, setNames] = useState([]);
  useEffect(() => {
    let aborted = false;
    setNames([]);
    const q = String(query || "").trim();
    if (q.length < 3 || /[:<>=]/.test(q)) return;
    (async () => {
      try {
        const r = await window.ssAuth.authedFetch("/api/cards/autocomplete?q=" + encodeURIComponent(q.slice(0, 30)));
        if (!r.ok || aborted) return;
        const arr = await r.json();
        if (!aborted) setNames((Array.isArray(arr) ? arr : []).slice(0, 3));
      } catch (e) { /* garnish */ }
    })();
    return () => { aborted = true; };
  }, [query]);
  if (!names.length) return null;
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem", alignItems: "center", margin: "0.4rem 0" }}>
      <span style={{ font: "600 12px " + T.mono, color: T.ink3 }}>Did you mean</span>
      {names.map((n) => (
        <button key={n} type="button" onClick={() => onPick(n)}
          style={{ border: "1.5px solid " + T.line, background: "transparent", color: T.ink, borderRadius: 999, padding: "0.4rem 0.8rem", font: "600 12.5px " + T.mono, cursor: "pointer", minHeight: 44 }}>
          {n}
        </button>
      ))}
    </div>
  );
}

function LookupApp() {
  const [session, sessionLoading] = window.ssAuth.useSession();
  const [text, setText] = useState("");
  const [result, setResult] = useState(null);
  const [searching, setSearching] = useState(false);
  const [translating, setTranslating] = useState(false);
  const [error, setError] = useState(null);
  const [moreError, setMoreError] = useState(null);
  const [page, setPage] = useState(1);
  const [modal, setModal] = useState(null); // { idx }
  const [showTips, setShowTips] = useState(false);
  // The syntax the loaded pages actually ran under, shown to the user and
  // editable in place (the AI's work stays transparent and correctable).
  const [ranSyntax, setRanSyntax] = useState("");
  const [editingSyntax, setEditingSyntax] = useState(false);
  const [syntaxDraft, setSyntaxDraft] = useState("");

  const [picks, setPicks] = useState(loadPicks);
  const [recents, setRecents] = useState(loadRecents);
  const [copied, setCopied] = useState(false);
  useEffect(() => {
    try { localStorage.setItem(PICKS_KEY, JSON.stringify(picks)); } catch (e) { /* best effort */ }
  }, [picks]);
  const isClipped = useCallback((name) => picks.some((p) => p.name === name), [picks]);
  const toggleClip = useCallback((card) => {
    if (!card || !card.name) return;
    track("CardSearchClip", { card: card.name });
    setPicks((prev) => prev.some((p) => p.name === card.name)
      ? prev.filter((p) => p.name !== card.name)
      : [...prev, { name: card.name, oracle_id: card.oracle_id || null, buy_url: card.buy_url || null }]);
  }, []);
  const copyPicks = useCallback(() => {
    try {
      if (navigator.clipboard) navigator.clipboard.writeText(picksToPlainList(picks));
      setCopied(true);
      setTimeout(() => setCopied(false), 1600);
    } catch (e) { /* best effort */ }
  }, [picks]);

  const [wishlisted, setWishlisted] = useState(() => new Set());
  const [wishlistModal, setWishlistModal] = useState(null);
  const [ownModal, setOwnModal] = useState(null);

  // Public page: signed-out visitors search everything; the ownership layer
  // (owned marks, Own this, Want it) is the reason to sign up, not a wall.
  useEffect(() => {
    if (sessionLoading) return;
    track("CardSearchView", { signed: session ? "yes" : "no" });
  }, [session, sessionLoading]);

  // Run a KNOWN syntax string (from the translator, raw input, or the edit
  // box). All pages of one list run under one syntax.
  const runSyntax = useCallback(async (syntax, p) => {
    const pageNum = p || 1;
    setSearching(true);
    if (pageNum === 1) { setError(null); setResult(null); setMoreError(null); }
    else setMoreError(null);
    try {
      const r = await window.ssAuth.authedFetch(
        "/api/lookup/search?q=" + encodeURIComponent(syntax) + "&page=" + pageNum + "&order=name");
      const body = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(body.error || "HTTP " + r.status);
      setRanSyntax(syntax);
      setPage(pageNum);
      setResult((prev) => pageNum > 1 && prev
        ? { ...body, cards: [...prev.cards, ...body.cards] }
        : body);
      // Remember the syntax on a fresh search that actually found cards, so an
      // empty/typo query never clutters the recents.
      if (pageNum === 1 && body.total_cards > 0) setRecents((r) => pushRecent(r, syntax));
      track("CardSearchQuery", { page: pageNum, results: body.total_cards });
    } catch (e) {
      if (pageNum === 1) { setError(e.message || "Search failed. Try again."); setResult(null); }
      else setMoreError(e.message || "Couldn't load more. Try again.");
    }
    setSearching(false);
  }, []);

  // The one entry point: raw syntax runs as-is, plain words go through the
  // translator first.
  const commit = useCallback(async (askOverride) => {
    const ask = String(typeof askOverride === "string" ? askOverride : text).trim();
    if (!ask || searching || translating) return;
    setEditingSyntax(false);
    if (looksLikeSyntax(ask)) {
      runSyntax(ask, 1);
      return;
    }
    setTranslating(true);
    setError(null);
    setResult(null);
    try {
      const r = await window.ssAuth.authedFetch("/api/lookup/translate", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ask }),
      });
      const body = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(body.error || "HTTP " + r.status);
      track("CardSearchTranslate", { ask_len: ask.length });
      setTranslating(false);
      runSyntax(body.syntax, 1);
    } catch (e) {
      setTranslating(false);
      setError(e.message || "Could not translate that. Add more detail, or type Scryfall syntax directly.");
    }
  }, [text, searching, translating, runSyntax]);

  const allCards = result && Array.isArray(result.cards) ? result.cards : [];
  // Grid order IS popup order: one flat array, no re-sorting.
  const popupCards = allCards.map((c) => ({
    name: c.name, image_normal: c.image_normal || c.image_small,
    oracle_id: c.oracle_id, type_line: c.type_line,
    owned: c.owned, locations: [],
    ck_price: c.ck_price, buy_url: c.buy_url,
    wantable: !!session && !c.owned && !!c.oracle_id,
    clippable: true,
  }));

  const body = () => {
    if (sessionLoading) return <p className="cm-radar-status-msg">Loading…</p>;
    return (
      <>
        <header className="cm-radar-head">
          <div className="cm-radar-head-main">
            <div className="cm-eyebrow">Card Search <span className="cm-beta-chip">BETA</span></div>
            <h1 className="cm-h1 cm-h1--sm">CARD SEARCH</h1>
            <p className="cm-radar-lead">
              Tell it what you are looking for, in your own words. It writes the
              search, checks every result against your stash, and marks what you
              already own.
            </p>
          </div>
        </header>

        {!session && (
          <div style={{
            border: "1.5px solid " + T.accent, background: T.accentSoft,
            borderRadius: 10, padding: "0.7rem 0.9rem", margin: "0 0 0.8rem",
            color: T.ink, fontSize: "0.92rem", lineHeight: 1.5,
          }}>
            Searching is free, no account needed. <a href="/login?signup=1&next=%2Flookup" style={{ color: T.accent, fontWeight: 700 }}>Sign up free</a> and
            every result gets checked against your own collection: green marks on
            cards you already own, so you never buy a card twice.
          </div>
        )}
        <div className="cm-radar-addbar syn-box">
          <div style={{ position: "relative" }}>
            <textarea
              value={text}
              disabled={searching || translating}
              onChange={(e) => setText(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); commit(); } }}
              placeholder="Tell us what you are looking for. Example: elves for a Sultai commander deck with tap abilities"
              aria-label="Tell us what you are looking for"
              rows={4}
              style={{
                width: "100%", boxSizing: "border-box", minHeight: 128, resize: "vertical",
                border: "2px solid " + T.ink, borderRadius: 10,
                background: T.bg, padding: "12px 14px",
                font: "600 16px " + T.sans, color: T.ink, lineHeight: 1.4,
              }}
            />
          </div>
          <div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginTop: "0.6rem", flexWrap: "wrap" }}>
            <button type="button" className="cm-btn cm-btn--primary syn-check" style={{ marginTop: 0 }}
              disabled={searching || translating || !text.trim()}
              onClick={() => commit()}>
              {translating ? "Writing the search…" : searching ? "Searching…" : "Search"}
            </button>
            <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44 }}
              onClick={() => setShowTips((s) => !s)} aria-expanded={showTips}>
              {showTips ? "Hide tips" : "Tips"}
            </button>
          </div>
          {showTips && (
            <div style={{ marginTop: "0.6rem", padding: "0.7rem 0.9rem", background: T.bg, border: "1px solid " + T.line, borderRadius: 10, color: T.ink2, fontSize: "0.9rem", lineHeight: 1.55 }}>
              The more detail, the better the search. Name the card type (creature,
              instant, artifact), the deck or format it is for ("commander legal",
              "standard"), colors or a commander's colors, and what the card should
              do. Sets work too ("from the Fallout set"). If you know Scryfall
              syntax, type it raw and it runs untouched.
            </div>
          )}
        </div>

        {recents.length > 0 && !searching && !translating && (
          <div style={{ marginTop: "1rem" }}>
            <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: "0.5rem" }}>
              <span style={{ font: "700 11.5px " + T.mono, color: T.ink3, textTransform: "uppercase", letterSpacing: "0.06em" }}>
                Recent searches
              </span>
              <button type="button"
                onClick={() => { setRecents([]); try { localStorage.removeItem(RECENTS_KEY); } catch (e) {} }}
                style={{ border: "none", background: "transparent", color: T.ink3, font: "600 11px " + T.mono, cursor: "pointer", padding: "0.2rem 0.3rem" }}>
                Clear
              </button>
            </div>
            <div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem" }}>
              {recents.map((syntax) => (
                <button key={syntax} type="button"
                  title={"Run " + syntax}
                  onClick={() => { setText(""); runSyntax(syntax, 1); }}
                  style={{ border: "1.5px solid " + T.accentSoft, background: T.bg2, color: T.ink2, borderRadius: 999, padding: "0.5rem 0.9rem", font: "600 12px " + T.mono, cursor: "pointer", minHeight: 44, textAlign: "left", maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  {syntax}
                </button>
              ))}
            </div>
          </div>
        )}

        {!result && !searching && !translating && !error && (
          <div style={{ marginTop: "1rem" }}>
            <div style={{ font: "700 11.5px " + T.mono, color: T.ink3, textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: "0.5rem" }}>
              Try one
            </div>
            <div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem" }}>
              {STARTERS.map((q) => (
                <button key={q} type="button"
                  onClick={() => { setText(q); commit(q); }}
                  style={{ border: "1.5px solid " + T.line, background: "transparent", color: T.ink, borderRadius: 999, padding: "0.5rem 0.9rem", font: "600 12.5px " + T.mono, cursor: "pointer", minHeight: 44, textAlign: "left" }}>
                  {q}
                </button>
              ))}
            </div>
          </div>
        )}

        {translating && <p className="cm-radar-status-msg">Reading your ask and writing the search…</p>}
        {searching && page === 1 && <p className="cm-radar-status-msg">Searching all of Magic…</p>}
        {error && <p className="cm-radar-status-msg is-err">{error}</p>}

        {result && !error && (
          <div className="cm-radar-listwrap">
            {ranSyntax && (
              <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", flexWrap: "wrap", margin: "0.7rem 0 0" }}>
                <span style={{ font: "600 11.5px " + T.mono, color: T.ink3 }}>Searched as</span>
                {editingSyntax ? (
                  <>
                    <input value={syntaxDraft} onChange={(e) => setSyntaxDraft(e.target.value)}
                      onKeyDown={(e) => { if (e.key === "Enter") { setEditingSyntax(false); runSyntax(syntaxDraft.trim() || ranSyntax, 1); } }}
                      aria-label="Edit the search syntax"
                      style={{ flex: 1, minWidth: 180, minHeight: 44, boxSizing: "border-box", background: T.bg, color: T.ink, border: "1.5px solid " + T.accent, borderRadius: 8, padding: "0 10px", font: "600 13px " + T.mono }} />
                    <button type="button" className="cm-btn cm-btn--primary cm-btn--sm" style={{ minHeight: 44 }}
                      onClick={() => { setEditingSyntax(false); runSyntax(syntaxDraft.trim() || ranSyntax, 1); }}>
                      Run
                    </button>
                  </>
                ) : (
                  <>
                    <code style={{ font: "600 12.5px " + T.mono, color: T.ink2, background: T.bg2, border: "1px solid " + T.line, borderRadius: 6, padding: "0.3rem 0.55rem", overflowWrap: "anywhere" }}>{ranSyntax}</code>
                    <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44 }}
                      onClick={() => { setSyntaxDraft(ranSyntax); setEditingSyntax(true); }}>
                      Edit
                    </button>
                  </>
                )}
              </div>
            )}
            {Array.isArray(result.warnings) && result.warnings.length > 0 && (
              <p className="cm-radar-status-msg is-warn">{result.warnings.join(" ")}</p>
            )}
            {result.verdict_degraded && (
              <p className="cm-radar-status-msg is-warn">The ownership check hiccuped, so owned marks may be missing. Refresh to re-check.</p>
            )}
            <div style={{ font: "600 12.5px " + T.mono, color: T.ink3, margin: "0.7rem 0 0.3rem" }}>
              {result.total_cards.toLocaleString()} {result.total_cards === 1 ? "card" : "cards"}
              {result.total_cards > allCards.length ? " · first " + allCards.length.toLocaleString() + " loaded" : ""}
              {session && allCards.length > 0 ? " · " + allCards.filter((c) => c.owned).length.toLocaleString() + " already in your stash" : ""}
            </div>
            {allCards.length === 0 && (
              <>
                <p className="cm-radar-status-msg">No card matches that. Add more detail, or check the spelling.</p>
                <DidYouMean query={ranSyntax}
                  onPick={(name) => { setText(name); runSyntax("!\"" + name.replace(/"/g, "") + "\"", 1); }} />
              </>
            )}
            <div style={{
              display: "grid",
              gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))",
              gap: 10, marginTop: "0.7rem",
            }}>
              {allCards.map((c, i) => (
                <button key={(c.oracle_id || c.name) + "-" + i} type="button"
                  onClick={() => { track("CardSearchCardOpen", { card: c.name }); setModal({ idx: i }); }}
                  aria-label={"Open " + c.name + (c.owned ? ", in your stash" : "")}
                  style={{ position: "relative", border: "none", background: "transparent", padding: 0, cursor: "pointer", borderRadius: 8 }}>
                  {(c.image_normal || c.image_small)
                    ? <img src={c.image_normal || c.image_small} alt={c.name} loading="lazy"
                        style={{ width: "100%", display: "block", borderRadius: "4.75% / 3.5%", background: T.bg3 }} />
                    : <div style={{ width: "100%", aspectRatio: "5 / 7", background: T.bg3, borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", color: T.ink3, font: "600 12px " + T.mono, padding: 6, overflowWrap: "anywhere" }}>{c.name}</div>}
                  {c.owned && (
                    <span aria-hidden="true" title="In your stash"
                      style={{
                        position: "absolute", top: 6, right: 6, width: 22, height: 22,
                        borderRadius: 999, background: T.good, color: "#fff",
                        display: "inline-flex", alignItems: "center", justifyContent: "center",
                        font: "700 12px " + T.sans, boxShadow: "0 1px 5px rgba(0,0,0,0.45)", opacity: 0.94,
                      }}>✓</span>
                  )}
                </button>
              ))}
            </div>
            {moreError && <p className="cm-radar-status-msg is-err">{moreError}</p>}
            {result.has_more && (
              <div style={{ margin: "1rem 0" }}>
                <button type="button" className="cm-btn cm-btn--ghost" style={{ minHeight: 44 }}
                  disabled={searching}
                  onClick={() => runSyntax(ranSyntax, page + 1)}>
                  {searching ? "Loading…" : "Show more"}
                </button>
              </div>
            )}
          </div>
        )}

        <p className="cm-radar-note">
          Search powered by Scryfall (scryfall.com). Spellstash is not produced by or endorsed by Scryfall.
          Prices from Card Kingdom, refreshed daily. Spellstash may earn a commission on Card Kingdom purchases.
          Spellstash is unaffiliated with Wizards of the Coast. Magic: The Gathering and all card data are
          property of Wizards of the Coast LLC.
        </p>
      </>
    );
  };

  return (
    <window.ssShell.AppShell active="lookup" session={session}>
      <main className="cm-radar" style={picks.length ? { paddingBottom: "5.5rem" } : undefined}>
        {body()}
      </main>
      {modal != null && popupCards.length > 0 && (
        <CardPopup cards={popupCards} startIndex={modal.idx}
          onClose={() => setModal(null)}
          isClipped={isClipped}
          onToggleClip={toggleClip}
          isWishlisted={(oid) => wishlisted.has(oid)}
          onWant={(card) => setWishlistModal({ cards: [{ oracle_id: card.oracle_id, name: card.name }] })}
          onOwnThis={(card) => setOwnModal({ name: card.name, oracle_id: card.oracle_id })}
          onBuyClick={(card) => track("CardSearchBuyClick", { card: card.name, price: card.ck_price, owned: card.owned ? "yes" : "no" })}
          suspendKeys={wishlistModal != null || ownModal != null} />
      )}
      {wishlistModal != null && (
        <WishlistModal cards={wishlistModal.cards}
          onAdded={(ids) => setWishlisted((prev) => { const next = new Set(prev); ids.forEach((id) => next.add(id)); return next; })}
          onClose={() => setWishlistModal(null)}
          onTrack={(count) => track("CardSearchWishlistAdd", { count })} />
      )}
      {ownModal != null && (
        <OwnThisModal card={ownModal}
          onOwned={(card) => {
            setResult((prev) => prev
              ? { ...prev, cards: prev.cards.map((c) => c.name === card.name ? { ...c, owned: true } : c) }
              : prev);
          }}
          onClose={() => setOwnModal(null)}
          onTrack={(card) => track("CardSearchOwnThis", { card: card.name })} />
      )}
      <PickBar picks={picks} copied={copied}
        onBuyAll={() => ckBulkBuy(picks)}
        onCopy={copyPicks}
        onClear={() => setPicks([])} />
      <ClipDrawer picks={picks} copied={copied}
        onBuyAll={() => ckBulkBuy(picks)}
        onCopy={copyPicks}
        onRemove={(cardPick) => setPicks((prev) => prev.filter((x) => x.name !== cardPick.name))}
        onClear={() => setPicks([])} />
    </window.ssShell.AppShell>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<LookupApp />);
