// Spellstash — the shared card popup (docs/CARD_SEARCH_PLAN.md phase 1).
// ONE implementation of the card display system: big art with prev/next
// arrows walking the DISPLAY order, DFC flip, EDHREC stat chips, live stash
// locations with per-copy quick-move, Own this filing, Want it wishlist,
// clip, and the Card Kingdom buy path. Extracted from the Bracket Brewer's
// battle-tested components; Card Search is the first consumer, existing
// pages migrate one per session (never copy-paste an 8th clone).
//
// No build step: loaded as its own Babel script AFTER spellstash-auth.jsx
// and BEFORE the page script. Exposes window.ssCardPopup = { CardPopup,
// OwnThisModal, WishlistModal, ClipIcon }. Analytics stay page-owned:
// every component takes callbacks (onBuyClick, onTrack) instead of firing
// events itself, so each surface keeps its own permanent event names.
(() => {
const { useState, useEffect, useRef, useCallback } = React;

// Body-scroll lock shared by every modal in this file. Stacked modals close
// in one React commit in sibling order, so naive capture-and-restore leaves
// the page frozen (the review's repro: popup restores '', then Own this
// restores the 'hidden' it captured at mount). A counter makes the LAST
// unlock restore scrolling no matter the unmount order.
let overflowLocks = 0;
function lockScroll() {
  overflowLocks += 1;
  document.body.style.overflow = "hidden";
}
function unlockScroll() {
  overflowLocks = Math.max(0, overflowLocks - 1);
  if (overflowLocks === 0) document.body.style.overflow = "";
}

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)",
};

function ClipIcon({ filled }) {
  return (
    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" aria-hidden="true"
      style={{ verticalAlign: "-2px" }}
      stroke="currentColor" strokeWidth={filled ? 2.7 : 2} strokeLinecap="round" strokeLinejoin="round">
      <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
    </svg>
  );
}

// ── Wishlist modal ────────────────────────────────────────────────────
// Move gap cards you don't own onto a wishlist. Mirrors the collection
// page's deck-completer flow: pick an existing wishlist (is_wishlist
// containers) or create one inline, then one Add button does the work.
// LAW: selecting a radio never fires anything; only Add acts. Wishlist rows
// are name-only wants and never count toward the free cap (server-side).
function WishlistModal({ cards, onAdded, onClose, onTrack }) {
  const [wishlists, setWishlists] = useState(null); // null = loading
  const [target, setTarget] = useState(null);       // container id string, or "new"
  const [newName, setNewName] = useState("");
  const [adding, setAdding] = useState(false);
  const [added, setAdded] = useState(null);         // { count, name } after success
  const [error, setError] = useState(null);
  const [upgradeUrl, setUpgradeUrl] = useState(null);

  // Fetch the user's wishlist containers when the modal opens.
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await window.ssAuth.authedFetch("/api/containers");
        const rows = r.ok ? await r.json() : [];
        if (cancelled) return;
        const lists = (Array.isArray(rows) ? rows : []).filter((c) => c && c.is_wishlist && !c.archived_at);
        setWishlists(lists);
        setTarget(lists.length ? String(lists[0].id) : "new");
      } catch (e) {
        if (!cancelled) { setWishlists([]); setTarget("new"); }
      }
    })();
    return () => { cancelled = true; };
  }, []);

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    lockScroll();
    return () => {
      window.removeEventListener("keydown", onKey);
      unlockScroll();
    };
  }, [onClose]);

  const add = async () => {
    if (adding || added || !cards.length || target == null) return;
    setAdding(true);
    setError(null);
    try {
      let targetId;
      let targetName;
      if (target === "new") {
        const name = newName.trim() || "Wishlist";
        const r = await window.ssAuth.authedFetch("/api/containers", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ name, kind: "other", is_wishlist: true }),
        });
        const body = await r.json().catch(() => ({}));
        if (!r.ok) {
          const err = new Error(body.error || "HTTP " + r.status);
          err.upgrade_url = body.upgrade_url || null;
          throw err;
        }
        targetId = body.id;
        targetName = body.name || name;
      } else {
        targetId = parseInt(target, 10);
        const found = (wishlists || []).find((w) => w.id === targetId);
        targetName = found ? found.name : "your wishlist";
      }
      // The bulk endpoint caps at 200 items per request; chunk larger adds.
      for (let i = 0; i < cards.length; i += 200) {
        const items = cards.slice(i, i + 200).map((c) => ({
          oracle_id: c.oracle_id,
          container_id: targetId,
          quantity: 1,
        }));
        const r = await window.ssAuth.authedFetch("/api/instances/bulk", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ items }),
        });
        if (!r.ok) {
          const body = await r.json().catch(() => ({}));
          const err = new Error(body.error || "HTTP " + r.status);
          err.upgrade_url = body.upgrade_url || null;
          throw err;
        }
      }
      setAdded({ count: cards.length, name: targetName });
      // Once per successful add batch.
      if (onTrack) onTrack(cards.length);
      onAdded(cards.map((c) => c.oracle_id));
    } catch (e) {
      setError(e.message || "Couldn't add to the wishlist. Try again.");
      setUpgradeUrl(e.upgrade_url || null);
    }
    setAdding(false);
  };

  const radioStyle = (active) => ({
    display: "flex", alignItems: "center", gap: "0.55rem",
    minHeight: 44, padding: "0 0.6rem", boxSizing: "border-box",
    border: "1px solid " + (active ? T.accent : T.line),
    background: active ? T.accentSoft : "transparent",
    borderRadius: 8, cursor: "pointer",
    color: T.ink, fontSize: "0.92rem",
  });

  return (
    <div className="cm-modal-backdrop" onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Add to a wishlist"
        style={{
          width: "min(92vw, 420px)", background: T.bg2, border: "1px solid " + T.line,
          borderRadius: 12, padding: "1.1rem", boxSizing: "border-box",
          maxHeight: "calc(100dvh - 40px)", overflowY: "auto", overscrollBehavior: "contain",
        }}>
        <div style={{ fontFamily: T.serif, fontWeight: 700, fontSize: "1.1rem", color: T.ink }}>Add to a wishlist</div>
        <div style={{ font: "600 12px " + T.mono, color: T.ink3, marginTop: "0.25rem" }}>
          {cards.length === 1 ? cards[0].name : cards.length.toLocaleString() + " cards"}
        </div>
        {added ? (
          <div style={{ marginTop: "0.9rem" }}>
            <div style={{ color: T.ink, fontSize: "0.95rem" }}>
              Added {added.count.toLocaleString()} to {added.name}.
            </div>
            <a href="/collection?view=wishlist"
              style={{ display: "inline-block", marginTop: "0.5rem", color: T.accent, font: "600 13px " + T.sans }}>
              See your wishlist
            </a>
            <div style={{ marginTop: "0.9rem" }}>
              <button type="button" className="cm-btn cm-btn--primary" style={{ minHeight: 44 }} onClick={onClose}>Done</button>
            </div>
          </div>
        ) : (
          <>
            {wishlists == null && (
              <p style={{ color: T.ink3, fontSize: "0.9rem", margin: "0.75rem 0 0" }}>Loading your wishlists…</p>
            )}
            {wishlists != null && (
              <div role="radiogroup" aria-label="Pick a wishlist"
                style={{ marginTop: "0.75rem", display: "flex", flexDirection: "column", gap: "0.35rem" }}>
                {/* New wishlist leads the list. At the bottom of a long list
                    on a phone, the create option and its name box sat under
                    the keyboard, which read as "can't create a container". */}
                <label style={radioStyle(target === "new")}>
                  <input type="radio" name="ss-wishlist-target"
                    checked={target === "new"}
                    onChange={() => setTarget("new")} />
                  + New wishlist
                </label>
                {target === "new" && (
                  <input value={newName} onChange={(e) => setNewName(e.target.value)}
                    placeholder="Wishlist" aria-label="New wishlist name" autoComplete="off"
                    style={{
                      minHeight: 44, boxSizing: "border-box", width: "100%",
                      border: "2px solid " + T.ink, borderRadius: 8, background: T.bg,
                      padding: "0 12px", font: "600 15px " + T.sans, color: T.ink,
                    }} />
                )}
                {wishlists.map((w) => (
                  <label key={w.id} style={radioStyle(target === String(w.id))}>
                    <input type="radio" name="ss-wishlist-target"
                      checked={target === String(w.id)}
                      onChange={() => setTarget(String(w.id))} />
                    {w.name}
                  </label>
                ))}
              </div>
            )}
            {error && (
              <p className="cm-radar-status-msg is-err" style={{ margin: "0.6rem 0 0" }}>
                {error}
                {upgradeUrl && (
                  <a href={upgradeUrl} style={{ marginLeft: 8, color: "var(--cm-accent)", fontWeight: 700 }}>See Pro</a>
                )}
              </p>
            )}
            <div style={{
              display: "flex", gap: "0.5rem", marginTop: "0.9rem",
              position: "sticky", bottom: "-1.1rem", background: T.bg2,
              padding: "0.6rem 0 1.1rem", marginBottom: "-1.1rem",
            }}>
              <button type="button" className="cm-btn cm-btn--primary" style={{ minHeight: 44 }}
                onClick={add} disabled={adding || wishlists == null || !cards.length}>
                {adding
                  ? "Adding…"
                  : (target === "new" ? "Create + add " : "Add ") + cards.length + (cards.length === 1 ? " card" : " cards")}
              </button>
              <button type="button" className="cm-btn cm-btn--ghost" style={{ minHeight: 44 }}
                onClick={onClose} disabled={adding}>
                Cancel
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ── Own this ──────────────────────────────────────────────────────────
// The untracked-bulk fix: the user KNOWS they own a "missing" card, so file
// it into a real container right here (pick one or create a box inline) and
// the check flips it to owned on the spot. Mirrors WishlistModal's shape;
// targets real containers, never wishlists.
function OwnThisModal({ card, onOwned, onClose, onTrack }) {
  const [containers, setContainers] = useState(null); // null = loading
  const [target, setTarget] = useState(null);
  const [newName, setNewName] = useState("");
  const [adding, setAdding] = useState(false);
  const [added, setAdded] = useState(null); // { name } after success
  const [error, setError] = useState(null);
  const [upgradeUrl, setUpgradeUrl] = useState(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await window.ssAuth.authedFetch("/api/containers");
        const rows = r.ok ? await r.json() : [];
        if (cancelled) return;
        const list = (Array.isArray(rows) ? rows : []).filter((c) => c && !c.is_wishlist && !c.archived_at);
        setContainers(list);
        setTarget(list.length ? String(list[0].id) : "new");
      } catch (e) {
        if (!cancelled) { setContainers([]); setTarget("new"); }
      }
    })();
    return () => { cancelled = true; };
  }, []);

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    lockScroll();
    return () => {
      window.removeEventListener("keydown", onKey);
      unlockScroll();
    };
  }, [onClose]);

  const add = async () => {
    if (adding || added || !card || !card.oracle_id || target == null) return;
    setAdding(true);
    setError(null);
    try {
      let targetId; let targetName; let targetKind = "box";
      if (target === "new") {
        const name = newName.trim() || "Storage Box";
        const r = await window.ssAuth.authedFetch("/api/containers", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ name, kind: "box" }),
        });
        const body = await r.json().catch(() => ({}));
        if (!r.ok) {
          const err = new Error(body.error || "HTTP " + r.status);
          err.upgrade_url = body.upgrade_url || null;
          throw err;
        }
        targetId = body.id;
        targetName = body.name || name;
      } else {
        targetId = parseInt(target, 10);
        const found = (containers || []).find((c) => c.id === targetId);
        targetName = found ? found.name : "your container";
        targetKind = found ? found.kind : "box";
      }
      const r = await window.ssAuth.authedFetch("/api/instances", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ oracle_id: card.oracle_id, container_id: targetId, quantity: 1 }),
      });
      const body = await r.json().catch(() => ({}));
      if (!r.ok) {
        const err = new Error(body.error || "HTTP " + r.status);
        err.upgrade_url = body.upgrade_url || null;
        throw err;
      }
      setAdded({ name: targetName });
      if (onTrack) onTrack(card);
      onOwned(card, targetId, targetName, targetKind);
    } catch (e) {
      setError(e.message || "Couldn't add the card. Try again.");
      setUpgradeUrl(e.upgrade_url || null);
    }
    setAdding(false);
  };

  const radioStyle = (active) => ({
    display: "flex", alignItems: "center", gap: "0.55rem",
    minHeight: 44, padding: "0 0.6rem", boxSizing: "border-box",
    border: "1px solid " + (active ? T.accent : T.line),
    background: active ? T.accentSoft : "transparent",
    borderRadius: 8, cursor: "pointer",
    color: T.ink, fontSize: "0.92rem",
  });

  return (
    <div className="cm-modal-backdrop" onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="I own this card"
        style={{
          width: "min(92vw, 420px)", background: T.bg2, border: "1px solid " + T.line,
          borderRadius: 12, padding: "1.1rem", boxSizing: "border-box",
          maxHeight: "calc(100dvh - 40px)", overflowY: "auto", overscrollBehavior: "contain",
        }}>
        <div style={{ fontFamily: T.serif, fontWeight: 700, fontSize: "1.1rem", color: T.ink }}>Where does it live?</div>
        <div style={{ font: "600 12px " + T.mono, color: T.ink3, marginTop: "0.25rem" }}>{card.name}</div>
        {added ? (
          <div style={{ marginTop: "0.9rem" }}>
            <div style={{ color: T.ink, fontSize: "0.95rem" }}>
              Filed into {added.name}. It counts as owned now.
            </div>
            <div style={{ marginTop: "0.9rem" }}>
              <button type="button" className="cm-btn cm-btn--primary" style={{ minHeight: 44 }} onClick={onClose}>Done</button>
            </div>
          </div>
        ) : (
          <>
            {containers == null && (
              <p style={{ color: T.ink3, fontSize: "0.9rem", margin: "0.75rem 0 0" }}>Loading your containers…</p>
            )}
            {containers != null && (
              <div role="radiogroup" aria-label="Pick a container"
                style={{ marginTop: "0.75rem", display: "flex", flexDirection: "column", gap: "0.35rem" }}>
                {/* New box leads for the same phone-keyboard reason as the
                    wishlist modal. */}
                <label style={radioStyle(target === "new")}>
                  <input type="radio" name="ss-ownthis-target"
                    checked={target === "new"}
                    onChange={() => setTarget("new")} />
                  + New box
                </label>
                {target === "new" && (
                  <input value={newName} onChange={(e) => setNewName(e.target.value)}
                    placeholder="Storage Box" aria-label="New box name" autoComplete="off"
                    style={{
                      minHeight: 44, boxSizing: "border-box", width: "100%",
                      border: "2px solid " + T.ink, borderRadius: 8, background: T.bg,
                      padding: "0 12px", font: "600 15px " + T.sans, color: T.ink,
                    }} />
                )}
                {containers.map((c) => (
                  <label key={c.id} style={radioStyle(target === String(c.id))}>
                    <input type="radio" name="ss-ownthis-target"
                      checked={target === String(c.id)}
                      onChange={() => setTarget(String(c.id))} />
                    {c.name} <span style={{ color: T.ink3, font: "600 11px " + T.mono }}>({c.kind})</span>
                  </label>
                ))}
              </div>
            )}
            {error && (
              <p className="cm-radar-status-msg is-err" style={{ margin: "0.6rem 0 0" }}>
                {error}
                {upgradeUrl && (
                  <a href={upgradeUrl} style={{ marginLeft: 8, color: "var(--cm-accent)", fontWeight: 700 }}>See Pro</a>
                )}
              </p>
            )}
            <div style={{
              display: "flex", gap: "0.5rem", marginTop: "0.9rem",
              position: "sticky", bottom: "-1.1rem", background: T.bg2,
              padding: "0.6rem 0 1.1rem", marginBottom: "-1.1rem",
            }}>
              <button type="button" className="cm-btn cm-btn--primary" style={{ minHeight: 44 }}
                onClick={add} disabled={adding || containers == null}>
                {adding ? "Adding…" : (target === "new" ? "Create the box + file it" : "I own this, file it")}
              </button>
              <button type="button" className="cm-btn cm-btn--ghost" style={{ minHeight: 44 }} onClick={onClose}>Cancel</button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}


// ── Owned-copy actions: live locations + quick-move ──────────────────────
// Owning a card shouldn't dead-end the popup (Dwayne 8-03). Pulls the live
// locations (with instance ids) from /api/find and offers the same
// POST /api/instances/:id/move quick-move the Radar popup has. Containers
// load once per open; wishlists are excluded as move targets (a real copy
// moving into a want-list would stop counting as owned).
function StashActions({ card }) {
  const [find, setFind] = useState(null);
  const [containers, setContainers] = useState([]);
  const [moveFor, setMoveFor] = useState(null); // instance_id with the picker open
  const [busy, setBusy] = useState(false);

  const reload = useCallback(async () => {
    try {
      const r = await window.ssAuth.authedFetch(
        "/api/find?oracle_id=" + encodeURIComponent(card.oracle_id || "") +
        "&q=" + encodeURIComponent(card.name || "") + "&strict=1");
      if (r.ok) setFind(await r.json());
    } catch (e) { /* the static line below covers the failure */ }
  }, [card.oracle_id, card.name]);

  useEffect(() => {
    let aborted = false;
    setFind(null); setMoveFor(null);
    (async () => {
      await reload();
      try {
        const c = await window.ssAuth.authedFetch("/api/containers");
        if (!aborted && c.ok) {
          const arr = await c.json();
          setContainers((Array.isArray(arr) ? arr : []).filter((x) => x && !x.archived_at && !x.is_wishlist));
        }
      } catch (e) { /* no targets, Move buttons just hide */ }
    })();
    return () => { aborted = true; };
  }, [reload]);

  const doMove = async (instanceId, toId) => {
    if (!toId || busy) return;
    setBusy(true);
    try {
      const r = await window.ssAuth.authedFetch("/api/instances/" + instanceId + "/move", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ to_container_id: Number(toId) }),
      });
      if (r.ok) await reload();
    } catch (e) { /* row stays put */ }
    setBusy(false);
    setMoveFor(null);
  };

  const locs = find && Array.isArray(find.locations) ? find.locations : null;
  if (!locs) {
    // Static fallback while the live read loads (or if it fails).
    return card.locations && card.locations.length > 0 ? (
      <div style={{ font: "600 12.5px " + T.mono, color: T.good, marginTop: "0.4rem" }}>
        In your stash: {card.locations.map((l) => l.container_name + (l.quantity > 1 ? " \u00d7" + l.quantity : "")).join(" \u00b7 ")}
      </div>
    ) : null;
  }
  return (
    <div style={{ marginTop: "0.5rem" }}>
      <div style={{ font: "600 11.5px " + T.mono, color: T.good, textTransform: "uppercase", letterSpacing: "0.06em" }}>In your stash</div>
      {locs.map((loc, i) => {
        const targets = containers.filter((c) => c.id !== loc.container_id);
        return (
          <div key={(loc.instance_id || i) + "-" + i} style={{ padding: "0.35rem 0", borderBottom: "1px dashed " + T.line }}>
            <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", justifyContent: "space-between" }}>
              <span style={{ font: "600 12.5px " + T.mono, color: T.ink, minWidth: 0, overflowWrap: "anywhere" }}>
                {loc.container_name} <span style={{ color: T.ink3 }}>({loc.kind}) ×{loc.quantity}{loc.finish && loc.finish !== "nonfoil" ? " " + loc.finish : ""}</span>
              </span>
              {loc.instance_id && targets.length > 0 && moveFor !== loc.instance_id && (
                <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44, flexShrink: 0 }}
                  disabled={busy} onClick={() => setMoveFor(loc.instance_id)}>
                  Move
                </button>
              )}
            </div>
            {moveFor === loc.instance_id && (
              <div style={{ marginTop: "0.35rem", display: "flex", gap: "0.4rem", alignItems: "center" }}>
                <select disabled={busy} defaultValue="" onChange={(e) => doMove(loc.instance_id, e.target.value)}
                  style={{ flex: 1, minWidth: 0, boxSizing: "border-box", minHeight: 44, padding: "0.35rem", background: T.bg, border: "1px solid " + T.line, borderRadius: 6, color: T.ink, fontSize: "0.85rem" }}>
                  <option value="" disabled>{busy ? "Moving\u2026" : "Move to\u2026"}</option>
                  {targets.map((c) => <option key={c.id} value={c.id}>{c.name} ({c.kind})</option>)}
                </select>
                <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44 }}
                  disabled={busy} onClick={() => setMoveFor(null)}>Cancel</button>
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

// ── Card popup (plain mode) ───────────────────────────────────────────
// Same lightbox chrome as synergy.jsx's SynergyCardModal in its plain
// commander mode: just the card, readable, with prev/next arrows walking the
// list it was opened from. Gap and spice rows and the commander hero all
// open here.
function CardPopup({ cards, startIndex, onClose, onWant, onOwnThis, isWishlisted, isClipped, onToggleClip, onBuyClick, suspendKeys }) {
  const list = Array.isArray(cards) && cards.length ? cards : [];
  const [idx, setIdx] = useState(() => Math.min(Math.max(startIndex || 0, 0), Math.max(list.length - 1, 0)));
  // Re-clamp when the list shrinks under an open popup (a filter toggled
  // behind it); card must always be a real entry.
  const safeIdx = Math.min(idx, Math.max(list.length - 1, 0));
  const card = list[safeIdx] || {};
  const hasPrev = idx > 0;
  const hasNext = idx < list.length - 1;
  const goPrev = useCallback(() => setIdx((i) => Math.max(0, i - 1)), []);
  const goNext = useCallback(() => setIdx((i) => Math.min(list.length - 1, i + 1)), [list.length]);

  // Swipe forward/back (Dwayne 8-06, same pattern as the collection
  // lightbox): horizontal drag pages, taps and vertical scrolls pass through.
  const swipeStart = useRef(null);
  const onSwipeTouchStart = (e) => {
    if (e.touches && e.touches.length === 1) {
      swipeStart.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
    }
  };
  const onSwipeTouchEnd = (e) => {
    const s = swipeStart.current;
    swipeStart.current = null;
    if (!s || list.length < 2 || !e.changedTouches || !e.changedTouches.length) return;
    const dx = e.changedTouches[0].clientX - s.x;
    const dy = e.changedTouches[0].clientY - s.y;
    if (Math.abs(dx) < 48 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
    if (dx < 0) goNext(); else goPrev();
  };

  useEffect(() => {
    const onKey = (e) => {
      // The wishlist modal stacks above this popup; its keys win while open.
      if (suspendKeys) return;
      if (e.key === "Escape") onClose();
      else if (e.key === "ArrowLeft") goPrev();
      else if (e.key === "ArrowRight") goNext();
    };
    window.addEventListener("keydown", onKey);
    lockScroll();
    return () => { window.removeEventListener("keydown", onKey); unlockScroll(); };
  }, [onClose, goPrev, goNext, suspendKeys]);

  const img = card.image_normal || card.image_small;

  // Double-faced cards: Scryfall serves the back face at the same URL with
  // /back/ in place of /front/. Probe on card change; single-faced cards 404
  // the probe and no flip button appears. Same pattern as the collection
  // lightbox.
  const [showBack, setShowBack] = useState(false);
  const [hasBack, setHasBack] = useState(false);
  const backSrc = img && img.indexOf("/front/") >= 0 ? img.replace("/front/", "/back/") : null;
  useEffect(() => {
    setShowBack(false);
    setHasBack(false);
    if (!backSrc) return;
    let live = true;
    const probe = new Image();
    probe.onload = () => { if (live) setHasBack(true); };
    probe.src = backSrc;
    return () => { live = false; };
  }, [backSrc]);

  return (
    <div className="cm-modal-backdrop" onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} onTouchStart={onSwipeTouchStart} onTouchEnd={onSwipeTouchEnd} style={{ position: "relative", maxWidth: "min(90vw, 440px)", width: "100%", touchAction: "pan-y" }}>
        <button className="cm-card-lightbox-close" onClick={onClose} aria-label="Close"
          style={{ position: "absolute", top: 6, right: 6, zIndex: 40, background: "rgba(20,16,10,0.65)", color: "#fff", border: "none", borderRadius: 999, width: 32, height: 32, lineHeight: "32px", textAlign: "center", cursor: "pointer" }}>✕</button>
        <div className="cm-card-lightbox"
          style={{ maxWidth: "min(90vw, 440px)", maxHeight: "calc(100dvh - 40px)", overflowY: "auto", overscrollBehavior: "contain", paddingBottom: "max(16px, env(safe-area-inset-bottom))" }}>
        <div className="cm-card-lightbox-imgwrap">
          {img
            ? <img src={showBack && hasBack && backSrc ? backSrc : img} alt={card.name} className="cm-card-lightbox-img"
                style={{ maxWidth: "min(90vw, 440px)", maxHeight: "58dvh", width: "auto", height: "auto", objectFit: "contain", display: "block", margin: "0 auto" }} />
            : <div className="cm-card-lightbox-img" style={{ minHeight: 300, background: T.bg3 }} />}
          {list.length > 1 && (
            <>
              <button type="button" className="cm-card-lightbox-nav cm-card-lightbox-nav--prev" onClick={goPrev} disabled={safeIdx === 0} aria-label="Previous card">‹</button>
              <button type="button" className="cm-card-lightbox-nav cm-card-lightbox-nav--next" onClick={goNext} disabled={safeIdx >= list.length - 1} aria-label="Next card">›</button>
              <span className="cm-card-lightbox-count cm-mono">{safeIdx + 1} / {list.length}</span>
            </>
          )}
        </div>
        <div className="cm-card-lightbox-history">
          {hasBack && (
            <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44, marginBottom: "0.5rem" }}
              onClick={() => setShowBack((s) => !s)}>
              {showBack ? "Show front ⟲" : "Flip to back ⟳"}
            </button>
          )}
          <div style={{ fontFamily: T.serif, fontSize: "1.05rem", color: T.ink, fontWeight: 700 }}>{card.name}</div>
          {card.type_line && (
            <div style={{ font: "600 12px " + T.mono, color: T.ink3, marginTop: "0.15rem" }}>{card.type_line}</div>
          )}
          {card.sub && (
            <div style={{ font: "600 12px " + T.mono, color: T.ink3, marginTop: "0.15rem" }}>{card.sub}</div>
          )}
          {(typeof card.synergy_pct === "number" || typeof card.inclusion_rate === "number") && (
            <div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap", marginTop: "0.4rem" }}>
              {typeof card.synergy_pct === "number" && (
                <span style={{ border: "1px solid " + T.line, borderRadius: 999, padding: "0.18rem 0.55rem", font: "600 11.5px " + T.mono, color: T.ink3 }}>
                  {card.synergy_pct}% synergy
                </span>
              )}
              {typeof card.inclusion_rate === "number" && (
                <span style={{ border: "1px solid " + T.line, borderRadius: 999, padding: "0.18rem 0.55rem", font: "600 11.5px " + T.mono, color: T.ink3 }}>
                  in {card.inclusion_rate}% of decks
                </span>
              )}
            </div>
          )}
          {card.owned && <StashActions card={card} />}
          {card.buy_url && (
            <div style={{ marginTop: "0.6rem" }}>
              {/* Owned cards keep the buy path, just not the loud one: a
                  second copy is a legitimate want (Dwayne 8-03). */}
              <a className={card.owned ? "cm-btn cm-btn--ghost cm-btn--sm" : "cm-btn cm-btn--primary"}
                style={{ minHeight: 44, textDecoration: "none", display: "inline-flex", alignItems: "center" }}
                href={card.buy_url} target="_blank" rel="noopener noreferrer"
                onClick={() => { if (onBuyClick) onBuyClick(card); }}>
                {(card.owned ? "Buy another \u00b7 " : "") + (card.ck_price != null ? "$" + card.ck_price.toFixed(2) + " at Card Kingdom" : "Buy at Card Kingdom")}
              </a>
            </div>
          )}
          {card.clippable && onToggleClip && (() => {
            const popClipped = isClipped ? isClipped(card.name) : false;
            return (
              <div style={{ marginTop: "0.6rem" }}>
                <button type="button" className="cm-btn cm-btn--sm" style={{
                    minHeight: 44,
                    background: popClipped ? T.accent : "transparent",
                    color: popClipped ? T.accentFg : T.ink3,
                    border: "1px solid " + (popClipped ? T.accent : T.line),
                    borderRadius: 8,
                  }}
                  aria-pressed={popClipped} aria-label={popClipped ? "Remove from your buy list" : "Clip to your buy list"}
                  title={popClipped ? "On your buy list. Tap to remove." : "Clip to your buy list"}
                  onClick={() => onToggleClip(card)}>
                  <ClipIcon filled={popClipped} />{popClipped ? " ✓" : ""}
                </button>
              </div>
            );
          })()}
          {card.wantable && (onWant || onOwnThis) && (
            <div style={{ marginTop: "0.6rem", display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
              {onOwnThis && (
                <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44 }}
                  onClick={() => onOwnThis(card)}
                  title="Already own a copy that isn't tracked? File it into a container.">
                  Own this
                </button>
              )}
              {onWant && (isWishlisted && isWishlisted(card.oracle_id) ? (
                <button type="button" className="cm-btn cm-btn--ghost" style={{ minHeight: 44 }} disabled>
                  ✓ On your wishlist
                </button>
              ) : (
                <button type="button" className="cm-btn cm-btn--ghost cm-btn--sm" style={{ minHeight: 44 }}
                  onClick={() => onWant(card)}>
                  Want it
                </button>
              ))}
            </div>
          )}
        </div>
        </div>
      </div>
    </div>
  );
}


window.ssCardPopup = { CardPopup, OwnThisModal, WishlistModal, ClipIcon };
})();
