// ===========================================================
// Leaderboards_v2 + PublicCollectionV2.
// Public (works logged-out). Computed client-side from the public
// view + profiles so each row can show the collector's CROWN JEWEL
// cover and "collecting since" date. (Small dataset; if it grows,
// move aggregation back to the leaderboard_totals() function.)
// Reuses globals: Sleeve, AnimatedMoney.
// ===========================================================
const { useState: useLbState, useEffect: useLbEffect, useMemo: useLbMemo } = React;

function lbGbp(n) { return "£" + Math.round(Number(n) || 0).toLocaleString("en-GB"); }
function lbGrade(code) { return code ? code.replace("_PLUS", "+") : "—"; }
function lbSince(iso) {
  if (!iso) return null;
  const d = String(iso).slice(0, 10).split("-");
  const mo = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][parseInt(d[1], 10) - 1] || "";
  return mo + " " + d[0];
}
function lbAlbumFor(r, albums) {
  if (!r) return null;
  return r.slug ? (albums.find(a => a.slug === r.slug) || { title: r.title, artist: r.artist, year: r.year, rank: -1, slug: r.slug })
                : { title: r.title, artist: r.artist, year: r.year, rank: -1, slug: null };
}
function CrownIcon({ size = 13 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style={{ verticalAlign: "-1px" }}>
      <path d="M2 8l4.5 3.5L12 4l5.5 7.5L22 8l-2 11H4L2 8z" />
    </svg>
  );
}

function lbMoverChip(delta) {
  if (delta == null) return null;
  if (delta === 0) return <span className="lb-mv flat" title="No change since last week">&ndash;</span>;
  if (delta > 0) return <span className="lb-mv up" title={"Up " + delta + " since last week"}>&#9650; {delta}</span>;
  return <span className="lb-mv down" title={"Down " + (-delta) + " since last week"}>&#9660; {-delta}</span>;
}

// Count-up number that animates from 0 when scrolled into view (reuses the
// homepage HdCountUp when present; falls back to a static formatted number).
function LbNum({ value }) {
  return window.HdCountUp ? <HdCountUp value={Math.round(Number(value) || 0)} /> : <>{(Math.round(Number(value) || 0)).toLocaleString("en-GB")}</>;
}

// Cover art for non-canon (manual) records, fetched once from /api/cover
// (Discogs, with an iTunes fallback) and cached in localStorage so a record is
// only ever looked up once. Canon picks use their on-disk /covers file instead.
const LB_COVER_CACHE = (() => {
  try { return JSON.parse(window.localStorage.getItem("bbr_cover_cache") || "{}"); } catch (e) { return {}; }
})();
function lbCoverKey(r) { return ((r && r.artist) || "") + "|" + ((r && r.title) || ""); }
function lbFetchCover(r) {
  const key = lbCoverKey(r);
  if (key in LB_COVER_CACHE) return Promise.resolve(LB_COVER_CACHE[key]);
  const q = new URLSearchParams({ artist: r.artist || "", title: r.title || "" });
  if (r.year) q.set("year", String(r.year));
  if (r.discogs_release_id) q.set("release_id", String(r.discogs_release_id));
  return fetch("/api/cover?" + q.toString())
    .then(res => (res.ok ? res.json() : null))
    .then(d => {
      const url = (d && d.coverUrl) || null;
      LB_COVER_CACHE[key] = url; // cache misses too, so we don't refetch them
      try { window.localStorage.setItem("bbr_cover_cache", JSON.stringify(LB_COVER_CACHE)); } catch (e) {}
      return url;
    })
    .catch(() => null);
}

function Leaderboards_v2({ albums, currentUserId, onViewCollection }) {
  const [items, setItems] = useLbState(null);
  const [profiles, setProfiles] = useLbState({});
  const [snaps, setSnaps] = useLbState([]);
  const [board, setBoard] = useLbState("value"); // value | genre | canon | champions | rarest
  const [genre, setGenre] = useLbState("");
  const [err, setErr] = useLbState("");
  const [covers, setCovers] = useLbState({}); // cover-art key -> url|null (fetched for non-canon records)
  const coverReq = React.useRef(new Set());

  useLbEffect(() => {
    let cancelled = false;
    // No backend wired up (config/CDN failure) — show an empty board rather
    // than throwing on the missing client, like every other data view does.
    if (!window.BBR_supabase) { setItems([]); return; }
    // Capture this week's snapshot (self-guarded to once a week), then load.
    const cap = window.BBR_supabase.rpc ? window.BBR_supabase.rpc("capture_leaderboard_snapshot").then(() => {}, () => {}) : Promise.resolve();
    cap.then(() => Promise.all([
      window.BBR_supabase.from("public_collection_items").select("*"),
      window.BBR_supabase.from("profiles").select("id,display_name,created_at"),
      window.BBR_supabase.from("leaderboard_snapshots").select("user_id,total_value,captured_on").then(r => r, () => ({ data: [] })),
    ])).then(([itemsR, profR, snapR]) => {
      if (cancelled) return;
      if (itemsR.error) { setErr(itemsR.error.message); setItems([]); return; }
      setItems(itemsR.data || []);
      const pm = {}; (profR.data || []).forEach(p => { pm[p.id] = p; }); setProfiles(pm);
      setSnaps((snapR && snapR.data) || []);
    }).catch(() => { if (!cancelled) { setErr("Couldn't load the leaderboard. Try again shortly."); setItems([]); } });
    return () => { cancelled = true; };
  }, []);

  const genres = useLbMemo(() => Array.from(new Set((items || []).map(i => i.genre).filter(Boolean))).sort(), [items]);
  useLbEffect(() => { if (genres.length && !genre) setGenre(genres[0]); }, [genres]);

  // Previous-week rank per user, from the most recent snapshot date before today.
  const prevRank = useLbMemo(() => {
    if (!snaps.length) return {};
    const today = new Date().toISOString().slice(0, 10);
    const dates = Array.from(new Set(snaps.map(s => s.captured_on))).sort().reverse();
    const priorDate = dates.find(d => d < today);
    if (!priorDate) return {};
    const rows = snaps.filter(s => s.captured_on === priorDate).slice().sort((a, b) => Number(b.total_value) - Number(a.total_value));
    const m = {}; rows.forEach((s, i) => { m[s.user_id] = i + 1; });
    return m;
  }, [snaps]);

  // Per-user aggregates.
  const users = useLbMemo(() => {
    if (!items) return null;
    const byUser = {};
    items.forEach(it => {
      const u = byUser[it.user_id] || (byUser[it.user_id] = { user_id: it.user_id, total: 0, count: 0, canon: new Set(), genreSet: new Set(), crown: null });
      const v = Number(it.est_value) || 0;
      u.total += v; u.count += 1;
      if (it.slug) u.canon.add(it.slug);
      if (it.genre) u.genreSet.add(it.genre);
      if (!u.crown || v > (Number(u.crown.est_value) || 0)) u.crown = it;
    });
    return Object.values(byUser).map(u => {
      const p = profiles[u.user_id] || {};
      return Object.assign(u, { canon_owned: u.canon.size, genre_count: u.genreSet.size, display_name: p.display_name, member_since: p.created_at });
    });
  }, [items, profiles]);

  // Aggregate stats across every public collection (for the hero band).
  const stats = useLbMemo(() => {
    if (!items || !users) return null;
    const totalValue = items.reduce((s, r) => s + (Number(r.est_value) || 0), 0);
    const canonOwned = new Set(items.filter(i => i.slug).map(i => i.slug)).size;
    return {
      collectors: users.length,
      records: items.length,
      totalValue,
      canonOwned,
      genreCount: genres.length,
    };
  }, [items, users, genres]);

  // Genre champions — top collector (by value in that genre) per genre, with
  // that champion's single best record in the genre (for the cover).
  const champions = useLbMemo(() => {
    if (!items) return [];
    const map = {}; // genre -> user_id -> { value, count, top }
    items.forEach(it => {
      if (!it.genre) return;
      const g = map[it.genre] || (map[it.genre] = {});
      const u = g[it.user_id] || (g[it.user_id] = { value: 0, count: 0, top: null });
      const v = Number(it.est_value) || 0; u.value += v; u.count += 1;
      if (!u.top || v > (Number(u.top.est_value) || 0)) u.top = it;
    });
    return Object.keys(map).map(g => {
      const entries = Object.entries(map[g]).sort((a, b) => b[1].value - a[1].value);
      const [uid, d] = entries[0];
      return { genre: g, user_id: uid, value: d.value, count: d.count, top: d.top, contenders: entries.length, display_name: (profiles[uid] || {}).display_name };
    }).sort((a, b) => b.value - a.value);
  }, [items, profiles]);
  const genreLeaders = useLbMemo(() => new Set(champions.map(c => c.user_id)), [champions]);

  // Rarest / standout — most valuable individual records across all collections.
  const rarest = useLbMemo(
    () => (items || []).filter(r => Number(r.est_value) > 0).slice().sort((a, b) => Number(b.est_value) - Number(a.est_value)).slice(0, 12),
    [items]
  );

  const ranked = useLbMemo(() => {
    if (!users) return null;
    let list = users;
    if (board === "genre" && genre) {
      // recompute totals scoped to the selected genre
      const byUser = {};
      (items || []).forEach(it => {
        if (it.genre !== genre) return;
        const u = byUser[it.user_id] || (byUser[it.user_id] = { user_id: it.user_id, total: 0, count: 0, canon: new Set(), crown: null });
        const v = Number(it.est_value) || 0; u.total += v; u.count += 1;
        if (it.slug) u.canon.add(it.slug);
        if (!u.crown || v > (Number(u.crown.est_value) || 0)) u.crown = it;
      });
      list = Object.values(byUser).map(u => { const p = profiles[u.user_id] || {}; return Object.assign(u, { canon_owned: u.canon.size, display_name: p.display_name, member_since: p.created_at }); });
    }
    list = list.slice();
    if (board === "canon") list.sort((a, b) => (b.canon_owned - a.canon_owned) || (b.total - a.total));
    else list.sort((a, b) => b.total - a.total);
    return list;
  }, [users, items, profiles, board, genre]);

  const maxVal = ranked && ranked.length ? (board === "canon" ? 100 : (ranked[0].total || 1)) : 1;

  // Fetch real cover art for the non-canon records on display (rarest finds +
  // each collector's crown jewel). Canon picks already have on-disk covers.
  useLbEffect(() => {
    if (!items) return;
    const seen = new Set();
    const recs = [];
    const add = (r) => {
      if (!r || r.slug || !(Number(r.est_value) > 0)) return;
      const k = lbCoverKey(r);
      if (!seen.has(k)) { seen.add(k); recs.push(r); }
    };
    rarest.forEach(add);
    (users || []).forEach(u => add(u.crown));
    const todo = recs.filter(r => !coverReq.current.has(lbCoverKey(r)));
    if (!todo.length) return;
    todo.forEach(r => coverReq.current.add(lbCoverKey(r)));
    let cancelled = false;
    Promise.all(todo.map(r => lbFetchCover(r).then(url => [lbCoverKey(r), url])))
      .then(pairs => {
        if (cancelled) return;
        setCovers(prev => {
          const next = Object.assign({}, prev);
          pairs.forEach(([k, u]) => { next[k] = u; });
          return next;
        });
      });
    return () => { cancelled = true; };
  }, [items, rarest, users]);

  // Album descriptor for a record, with a fetched cover URL spliced in for
  // non-canon records (Sleeve renders album.coverUrl as an <img>).
  function lbCoverAlbum(r) {
    const a = lbAlbumFor(r, albums);
    if (a && !a.slug) {
      const u = covers[lbCoverKey(r)];
      if (u) return Object.assign({}, a, { coverUrl: u });
    }
    return a;
  }

  function podium() {
    if (!ranked || ranked.length < 3 || board === "champions" || board === "rarest") return null;
    const order = [ranked[1], ranked[0], ranked[2]];
    return (
      <div className="lb-podium">
        {order.map((r) => {
          const place = ranked.indexOf(r) + 1;
          const a = lbCoverAlbum(r.crown);
          return (
            <button className={"lb-pod lb-pod-" + place} key={r.user_id} onClick={() => onViewCollection(r.user_id, r.display_name)}>
              {place === 1 && <span className="lb-pod-shine" aria-hidden="true" />}
              <span className="lb-pod-cover">
                {a ? <Sleeve album={a} size={place === 1 ? 76 : 58} /> : null}
                <span className="lb-pod-crownico"><CrownIcon size={11} /></span>
              </span>
              <span className={"lb-pod-medal m" + place}>{place}</span>
              <span className="lb-pod-name">{r.display_name || "Collector"}</span>
              <span className="lb-pod-val">
                {board === "canon"
                  ? <><b>{r.canon_owned}</b> / 100</>
                  : <><span className="cur">£</span><LbNum value={r.total} /></>}
              </span>
              <span className="lb-pod-sub">{r.count} records · {r.canon_owned} of 100</span>
              <span className="lb-pod-bar" />
            </button>);
        })}
      </div>);
  }

  return (
    <div className="lb">
      <div className="lb-head">
        <h1 className="lb-title">Leaderboards</h1>
        <p className="lb-sub">Every collection is public. Conditions and value maths are shown openly — the board judges itself.</p>
      </div>

      {stats && (
        <div className="lb-stats">
          <div className="lb-stat">
            <div className="lb-stat-n"><LbNum value={stats.collectors} /></div>
            <div className="lb-stat-l">Collectors</div>
          </div>
          <div className="lb-stat">
            <div className="lb-stat-n"><LbNum value={stats.records} /></div>
            <div className="lb-stat-l">Records logged</div>
          </div>
          <div className="lb-stat lb-stat-hero">
            <div className="lb-stat-n"><span className="cur">£</span><LbNum value={stats.totalValue} /></div>
            <div className="lb-stat-l">Total catalogue value</div>
          </div>
          <div className="lb-stat">
            <div className="lb-stat-n"><LbNum value={stats.canonOwned} /><small> / 100</small></div>
            <div className="lb-stat-l">Of the Hundred owned</div>
          </div>
          <div className="lb-stat">
            <div className="lb-stat-n"><LbNum value={stats.genreCount} /></div>
            <div className="lb-stat-l">Genres represented</div>
          </div>
        </div>
      )}

      <div className="lb-tabs">
        <button className={"lb-tab " + (board === "value" ? "active" : "")} onClick={() => setBoard("value")}>Total value</button>
        <button className={"lb-tab " + (board === "genre" ? "active" : "")} onClick={() => setBoard("genre")}>By genre</button>
        <button className={"lb-tab " + (board === "canon" ? "active" : "")} onClick={() => setBoard("canon")}>The Hundred</button>
        <button className={"lb-tab " + (board === "champions" ? "active" : "")} onClick={() => setBoard("champions")}>Genre champions</button>
        <button className={"lb-tab " + (board === "rarest" ? "active" : "")} onClick={() => setBoard("rarest")}>Rarest finds</button>
      </div>

      {board === "genre" && genres.length > 0 && (
        <div className="lb-genre-select">
          <select value={genre} onChange={e => setGenre(e.target.value)}>
            {genres.map(g => <option key={g} value={g}>{g}</option>)}
          </select>
        </div>
      )}
      {board === "canon" && <p className="lb-note">Hardest board to game: ranked by how many of the Lead-In 100 you actually own.</p>}
      {board === "value" && <p className="lb-note">Movement (&#9650;/&#9660;) shows each collector&rsquo;s rank change since last week&rsquo;s standings.</p>}

      {/* ---- Genre champions board ---- */}
      {board === "champions" ? (
        items === null ? <div className="lb-empty">Loading&hellip;</div>
        : champions.length === 0 ? <div className="lb-empty">No genres on the board yet.</div>
        : <div className="lb-champ-grid">
            {champions.map((c, i) => {
              const a = lbAlbumFor(c.top, albums);
              return (
                <button className="lb-champ" key={c.genre} style={{ "--i": i }} onClick={() => onViewCollection(c.user_id, c.display_name)}>
                  <div className="lb-champ-cover">{a ? <Sleeve album={a} size={64} /> : null}</div>
                  <div className="lb-champ-body">
                    <div className="lb-champ-genre">{c.genre}</div>
                    <div className="lb-champ-name">{c.display_name || "Collector"}</div>
                    <div className="lb-champ-val"><span className="cur">£</span><LbNum value={c.value} /></div>
                    <div className="lb-champ-tag"><CrownIcon size={11} /> Genre champion · {c.count} record{c.count == 1 ? "" : "s"}{c.contenders > 1 ? " · beat " + (c.contenders - 1) : ""}</div>
                  </div>
                </button>);
            })}
          </div>

      /* ---- Rarest finds board ---- */
      ) : board === "rarest" ? (
        items === null ? <div className="lb-empty">Loading&hellip;</div>
        : rarest.length === 0 ? <div className="lb-empty">No valued records on the board yet.</div>
        : <div className="lb-rare-grid">
            {rarest.map((r, i) => (
              <button className={"lb-rare-card" + (i === 0 ? " top" : "")} key={r.id} style={{ "--i": i }} onClick={() => onViewCollection(r.user_id, r.display_name)}>
                <span className="lb-rare-cover"><Sleeve album={lbCoverAlbum(r)} size={150} /></span>
                <span className="lb-rare-rank">{i + 1}</span>
                <span className="lb-rare-v"><span className="cur">£</span><LbNum value={r.est_value} /></span>
                <span className="lb-rare-body">
                  <span className="lb-rare-t">{r.title}</span>
                  <span className="lb-rare-a">{r.artist}{r.year ? " · " + r.year : ""}</span>
                  <span className="lb-rare-owner">{lbGrade(r.media_condition)} · {r.display_name || "a collector"}</span>
                </span>
              </button>))}
          </div>

      /* ---- Value / genre / canon ranked board ---- */
      ) : ranked === null ? (
        <div className="lb-empty">Loading the board&hellip;</div>
      ) : err ? (
        <div className="lb-empty">Couldn't load the leaderboard: {err}</div>
      ) : ranked.length === 0 ? (
        <div className="lb-empty">No collections on the board yet. Be the first — add a record.</div>
      ) : (
        <>
          {podium()}
          <div className="lb-list">
            {ranked.map((r, i) => {
              const crownAlbum = lbCoverAlbum(r.crown);
              const barPct = board === "canon" ? (r.canon_owned / 100 * 100) : (r.total / maxVal * 100);
              const badges = [];
              if (i < 10) badges.push("Top 10");
              if (genreLeaders.has(r.user_id)) badges.push("Genre leader");
              if (r.canon_owned >= 50) badges.push("Half the canon");
              else if (r.count >= 50) badges.push("50+ records");
              const mv = board === "value" ? lbMoverChip(prevRank[r.user_id] != null ? (prevRank[r.user_id] - (i + 1)) : null) : null;
              const since = lbSince(r.member_since);
              return (
                <button className={"lb-row" + (r.user_id === currentUserId ? " me" : "") + (i < 3 ? " top" : "")} key={r.user_id} style={{ "--i": i }}
                  onClick={() => onViewCollection(r.user_id, r.display_name)}>
                  <span className={"lb-rank" + (i < 3 ? " medal r" + i : "")}>{i + 1}</span>
                  <span className="lb-crown" title={r.crown ? "Crown jewel: " + r.crown.title : ""}>
                    {crownAlbum ? <Sleeve album={crownAlbum} size={48} /> : <span className="lb-crown-blank" />}
                    <span className="lb-crown-badge"><CrownIcon size={10} /></span>
                  </span>
                  <span className="lb-name-col">
                    <span className="lb-name">{r.display_name || "Anonymous collector"}{r.user_id === currentUserId ? " (you)" : ""} {mv}</span>
                    <span className="lb-sub-line">
                      {r.canon_owned} of the Hundred
                      {r.genre_count ? " · " + r.genre_count + " genre" + (r.genre_count == 1 ? "" : "s") : ""}
                      {since ? " · since " + since : ""}
                    </span>
                    <span className="lb-badges">
                      {badges.slice(0, 3).map(b => <span className="lb-badge" key={b}>{b}</span>)}
                    </span>
                    <span className="lb-bar"><i style={{ "--w": Math.max(2, barPct) + "%" }} /></span>
                  </span>
                  <span className="lb-meta">
                    {board === "canon"
                      ? <><b>{r.canon_owned}</b> / 100</>
                      : <><b><span className="cur">£</span><LbNum value={r.total} /></b><small>{r.count} record{r.count == 1 ? "" : "s"}</small></>}
                  </span>
                </button>
              );
            })}
          </div>
        </>
      )}
    </div>
  );
}

// ---- Public collection view ------------------------------------------------
function PublicCollectionV2({ userId, displayName, albums, onBack }) {
  const [items, setItems] = useLbState(null);
  const [since, setSince] = useLbState(null);
  const [search, setSearch] = useLbState("");
  const [filterGenre, setFilterGenre] = useLbState("all");
  const [sortKey, setSortKey] = useLbState("value");
  const [covers, setCovers] = useLbState({}); // cover-art key -> url|null (non-canon records)
  const coverReq = React.useRef(new Set());

  useLbEffect(() => {
    let cancelled = false;
    setItems(null);
    window.BBR_supabase.from("public_collection_items").select("*").eq("user_id", userId)
      .order("est_value", { ascending: false, nullsFirst: false })
      .then(({ data }) => { if (!cancelled) setItems(data || []); });
    window.BBR_supabase.from("profiles").select("created_at").eq("id", userId).maybeSingle()
      .then(({ data }) => { if (!cancelled && data) setSince(lbSince(data.created_at)); });
    return () => { cancelled = true; };
  }, [userId]);

  // Fetch real cover art for this collection's non-canon (manual) records.
  useLbEffect(() => {
    if (!items) return;
    const seen = new Set();
    const todo = [];
    items.forEach(r => {
      if (r.slug) return; // canon picks use their on-disk cover
      const k = lbCoverKey(r);
      if (seen.has(k) || coverReq.current.has(k)) return;
      seen.add(k); coverReq.current.add(k); todo.push(r);
    });
    if (!todo.length) return;
    let cancelled = false;
    Promise.all(todo.map(r => lbFetchCover(r).then(url => [lbCoverKey(r), url])))
      .then(pairs => {
        if (cancelled) return;
        setCovers(prev => {
          const next = Object.assign({}, prev);
          pairs.forEach(([k, u]) => { next[k] = u; });
          return next;
        });
      });
    return () => { cancelled = true; };
  }, [items]);

  function pcCoverAlbum(r) {
    const a = lbAlbumFor(r, albums);
    if (a && !a.slug) {
      const u = covers[lbCoverKey(r)];
      if (u) return Object.assign({}, a, { coverUrl: u });
    }
    return a;
  }

  const total = (items || []).reduce((s, r) => s + (Number(r.est_value) || 0), 0);
  const canon = new Set((items || []).filter(r => r.slug).map(r => r.slug)).size;
  const name = (items && items[0] && items[0].display_name) || displayName || "Collector";

  const genres = useLbMemo(() => Array.from(new Set((items || []).map(r => r.genre).filter(Boolean))).sort(), [items]);
  const view = useLbMemo(() => {
    const list = (items || []).filter(r => {
      if (filterGenre !== "all" && r.genre !== filterGenre) return false;
      if (search && !((r.title + " " + r.artist).toLowerCase().includes(search.toLowerCase()))) return false;
      return true;
    });
    const cmp = {
      value: (a, b) => (Number(b.est_value) || 0) - (Number(a.est_value) || 0),
      artist: (a, b) => (a.artist || "").localeCompare(b.artist || ""),
      year: (a, b) => (a.year || 0) - (b.year || 0),
    }[sortKey] || ((a, b) => (Number(b.est_value) || 0) - (Number(a.est_value) || 0));
    return list.slice().sort(cmp);
  }, [items, filterGenre, search, sortKey]);

  return (
    <div className="pubcol">
      <button className="lb-back" onClick={onBack}>← Leaderboards</button>
      <div className="pubcol-head">
        <h1 className="lb-title">{name}'s collection</h1>
        <div className="pubcol-stats">
          <span className="mc-chip">{lbGbp(total)} total</span>
          <span className="mc-chip">{(items || []).length} records</span>
          <span className="mc-chip">{canon} / 100 of the Hundred</span>
          {since && <span className="mc-chip">collecting since {since}</span>}
        </div>
        <p className="lb-sub">Public collection — conditions and estimated values shown for the community to judge.</p>
      </div>

      {items === null ? (
        <div className="lb-empty">Loading…</div>
      ) : items.length === 0 ? (
        <div className="lb-empty">This collector hasn't added any records yet.</div>
      ) : (
       <>
        <div className="mc-bar">
          <div className="mc-controls" style={{ marginLeft: "auto" }}>
            <div className="mc-search">
              <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="7" cy="7" r="5" /><path d="M11 11l3.5 3.5" /></svg>
              <input placeholder="Search this collection…" value={search} onChange={e => setSearch(e.target.value)} />
            </div>
            <div className="mc-select">
              <select value={filterGenre} onChange={e => setFilterGenre(e.target.value)}>
                <option value="all">All genres</option>
                {genres.map(g => <option key={g} value={g}>{g}</option>)}
              </select>
            </div>
            <div className="mc-select">
              <select value={sortKey} onChange={e => setSortKey(e.target.value)}>
                <option value="value">Sort: Value</option>
                <option value="artist">Sort: Artist</option>
                <option value="year">Sort: Year</option>
              </select>
            </div>
          </div>
        </div>
        <div className="mc-grid">
          {view.map(r => (
            <div className="mc-card" key={r.id}>
              <div className="mc-card-cover"><Sleeve album={pcCoverAlbum(r)} size={76} /></div>
              <div className="mc-card-body">
                <div className="mc-card-top">
                  <div>
                    <div className="mc-card-title">{r.title}</div>
                    <div className="mc-card-artist">{r.artist}{r.year ? " · " + r.year : ""}</div>
                  </div>
                  <span className="mc-cond" title="Media / Sleeve">{lbGrade(r.media_condition)}<small style={{ opacity: .6 }}>/{lbGrade(r.sleeve_condition)}</small></span>
                </div>
                <div className="mc-card-meta">
                  {r.genre || "—"}
                  <span className={"v2-badge " + (r.verification_level === "scanned" ? "scanned" : "manual")}>
                    {r.verification_level === "scanned" ? "✓ scanned" : "manual"}
                  </span>
                </div>
                <div className="mc-card-foot">
                  <div>
                    <div className="mc-card-val"><span className="cur">£</span><AnimatedMoney value={Number(r.est_value) || 0} /></div>
                    <div className="mc-card-range">{Number(r.est_value) > 0 ? "est. value" : "unvalued"}</div>
                  </div>
                </div>
              </div>
            </div>
          ))}
        </div>
       </>
      )}
    </div>
  );
}

Object.assign(window, { Leaderboards_v2, PublicCollectionV2, lbFetchCover, lbCoverKey });
