const { useState, useEffect, useLayoutEffect, useMemo, useRef } = React;

// Shared "back button closes the modal" hook for full-screen overlays.
// On mobile (esp. iOS Safari) a modal that doesn't touch history means the
// hardware/Safari Back button navigates the whole page away (out to home)
// instead of just dismissing the sheet. Each modal calls this on mount: it
// pushes a throwaway history entry, and a Back press pops it -> onClose. If the
// modal is instead closed in-app (× / done), we quietly consume the entry we
// added so Back doesn't require an extra press. Unique per instance so nested
// modals each own their own entry.
window.BBR_useModalBack = function (onClose) {
  const cbRef = React.useRef(onClose);
  cbRef.current = onClose;
  React.useEffect(() => {
    const id = "m" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
    let popped = false;
    // Explicitly re-assert the current URL (path+search+hash) as pushState's
    // 3rd arg — matching InviteFriend's already-proven D-NAV-01 fix. Omitting
    // it (as this hook used to) relies on the browser defaulting to "no
    // change", which real-device iOS Safari testing showed isn't reliable:
    // QA found the Back button still left to home on the modals using this
    // hook, while Invite's explicit-URL version closed cleanly.
    const url = location.pathname + location.search + location.hash;
    try { window.history.pushState({ bbrModal: id }, "", url); } catch (e) { return; }
    // Pass viaBack=true so a caller can tell an OS/Safari Back press (the entry
    // is already popped) from an in-app close (× / backdrop, which still needs
    // its pushed entry consumed on unmount). Existing callers ignore the arg.
    const onPop = () => { popped = true; if (cbRef.current) cbRef.current(true); };
    window.addEventListener("popstate", onPop);
    return () => {
      window.removeEventListener("popstate", onPop);
      // Closed in-app (not via Back): drop the entry we pushed if it's still on top.
      if (!popped && window.history.state && window.history.state.bbrModal === id) {
        try { window.history.back(); } catch (e) {}
      }
    };
  }, []);
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "vermillion",
  "grain": "on"
}/*EDITMODE-END*/;

const ACCENTS = {
  vermillion: "oklch(0.62 0.19 28)",
  ochre: "oklch(0.72 0.15 85)",
  ink: "#17130f",
  forest: "oklch(0.50 0.12 155)",
  cobalt: "oklch(0.48 0.18 250)",
  plum: "oklch(0.45 0.18 330)",
};

// Clean-path routing. Each page has a real URL (so it's individually
// crawlable / prerendered); the Vercel SPA rewrite serves index.html for any
// path without a static file, and this reads the path back into a route.
const SEG_TO_ROUTE = {
  list: "list", leaderboards: "leaderboards", legal: "legal", help: "help",
  "pressing-guide": "pressing-guide", account: "account", collection: "collection",
  // /today — Album of the Day's own URL, so the email, a bio link and the nav have
  // somewhere to point that is always the current record. Not prerendered: the pick
  // turns over at midnight UTC, so a static page would be wrong for most of the day.
  today: "today",
  // /vote — the ballot's canonical home and what the Wednesday email's per-recipient vote
  // links land on. Not /deep-dives: the ballot lived at the bottom of that page from June and
  // drew 5 nominations in 8 weeks, and affiliate_clicks has never recorded one interaction
  // from a deep-dives surface. The archive gets an embed; this gets the traffic.
  vote: "vote",
  // /ambassadors — the record-shop referral programme waitlist. Store-facing, so
  // it's reachable from the footer rather than the main nav, but it's a real,
  // prerendered, crawlable URL that must read back into its own route.
  ambassadors: "ambassadors",
  // Deep Dives pillar. The archive segment is read from the shared config so a
  // rename (e.g. liner-notes) only happens in data/deepdives_config.js, plus a
  // back-compat alias kept here so old links keep resolving after a rename.
  [(window.BBR_DEEPDIVES && window.BBR_DEEPDIVES.ROUTE) || "deep-dives"]: "deep-dives",
  "deep-dives-admin": "deep-dives-admin",
};
const DD_ARTICLE_SEG = (window.BBR_DEEPDIVES && window.BBR_DEEPDIVES.ARTICLE) || "deep-dive";
// Read the current URL path into a route descriptor (null => home).
function bbrParsePath() {
  const segs = (location.pathname || "/").split("/").filter(Boolean);
  if (!segs.length) return bbrParseLegacyHash();   // "/" — but honour old hash links
  if (segs[0] === "album" && segs[1]) return { route: "album", slug: decodeURIComponent(segs[1]) };
  if (segs[0] === DD_ARTICLE_SEG && segs[1]) return { route: "deep-dive", slug: decodeURIComponent(segs[1]) };
  if (segs[0] === "collector" && segs[1]) return { route: "public-collection", publicId: decodeURIComponent(segs[1]) };
  if (SEG_TO_ROUTE[segs[0]]) return { route: SEG_TO_ROUTE[segs[0]] };
  return null; // unknown path -> home (SPA), keeps a friendly 404-free fallback
}
// Back-compat: resolve the old hash deep links (#album=, #collection=, #list…)
// so previously-shared URLs still land on the right page.
function bbrParseLegacyHash() {
  const h = (location.hash || "").replace(/^#/, "");
  if (!h) return null;
  if (h.startsWith("collection=")) return { route: "public-collection", publicId: decodeURIComponent(h.slice(11)) };
  if (h.startsWith("album=")) return { route: "album", slug: decodeURIComponent(h.slice(6)) };
  const legacy = { mycollection: "collection", list: "list", leaderboards: "leaderboards", legal: "legal", "pressing-guide": "pressing-guide", account: "account" };
  if (legacy[h]) return { route: legacy[h] };
  return null;
}
// Build the clean path for the current route ("/" = home).
function bbrRouteToPath(route, albumSlug, publicUser, deepDiveSlug) {
  if (route === "home") return "/";
  if (route === "album" && albumSlug) return "/album/" + encodeURIComponent(albumSlug);
  if (route === "deep-dive" && deepDiveSlug) return "/" + DD_ARTICLE_SEG + "/" + encodeURIComponent(deepDiveSlug);
  if (route === "deep-dives") return "/" + ((window.BBR_DEEPDIVES && window.BBR_DEEPDIVES.ROUTE) || "deep-dives");
  if (route === "collection") return "/collection";
  if (route === "public-collection" && publicUser) return "/collector/" + encodeURIComponent(publicUser.id);
  return "/" + route; // list | leaderboards | legal | pressing-guide | account | deep-dives-admin
}

// Per-route <title> + meta description, kept in sync as the SPA navigates.
// Collector first, canon second — kept in step with DESC in gen_seo.py. The SPA
// rewrites the meta description on every navigation, so leaving the old
// publication-first copy here would have undone the prerendered tag the moment
// the app booted, and a crawler that runs JS would still have seen the old one.
const BBR_SITE_DESC = "Add the vinyl you own and find out what it is worth: a free market valuation on every record, a value history for your whole shelf, and a leaderboard to climb. Behind it sits the hundred greatest albums ever made, every placement defended in an essay with pressing guides, session detail and scoring data.";
function bbrSetMetaTag(selector, attr, value) {
  const el = document.head.querySelector(selector);
  if (el) el.setAttribute(attr, value);
}
function bbrApplyRouteMeta(route, album, publicUser) {
  let title, desc;
  if (route === "album" && album) {
    const dek = (window.ESSAYS_FULL && window.ESSAYS_FULL[album.slug] && window.ESSAYS_FULL[album.slug].essay && window.ESSAYS_FULL[album.slug].essay.dek) || "";
    title = album.title + " — " + album.artist + " (№ " + album.rank + ") | The Lead-In";
    // Mirrors album_desc() in gen_seo.py. It used to return the essay dek verbatim,
    // which meant a crawler that runs JS saw an entirely different description from
    // the prerendered tag it had just replaced — and neither mentioned the collection.
    desc = album.title + " by " + album.artist + " (" + album.year + ") — what a copy is worth, the pressing to own, and the case for № " + album.rank + (dek ? ": " + dek : ".");
  } else if (route === "deep-dive" && album) {
    const ddLabel = (window.BBR_DEEPDIVES && window.BBR_DEEPDIVES.LABEL) || "Deep Dives";
    const dek = (window.ESSAYS_FULL && window.ESSAYS_FULL[album.slug] && window.ESSAYS_FULL[album.slug].essay && window.ESSAYS_FULL[album.slug].essay.dek) || "";
    title = album.title + " — " + album.artist + " | " + ddLabel + " | The Lead-In";
    const ddTail = album.excerpt || dek || "";
    desc = album.title + " by " + album.artist + (album.year ? " (" + album.year + ")" : "") + " — what a copy is worth and which pressing to own, in a full Lead-In dossier" + (ddTail ? ": " + ddTail : ".");
  } else if (route === "deep-dives") {
    const ddLabel = (window.BBR_DEEPDIVES && window.BBR_DEEPDIVES.LABEL) || "Deep Dives";
    title = ddLabel + " | The Lead-In";
    desc = "Full Lead-In dossiers on records beyond the ranked hundred — which pressing is worth owning, what a copy costs now, and the essay behind it. A new " + ddLabel.replace(/s$/, "").toLowerCase() + " every week.";
  } else if (route === "list") {
    title = "The 100 Greatest Albums, Ranked 100→1 | The Lead-In";
    desc = "The Lead-In's definitive ranking of the hundred greatest albums, 100 down to 1 — mark the ones you own and see how much of the canon is already on your shelf.";
  } else if (route === "leaderboards") {
    title = "Collector Leaderboards | The Lead-In";
    desc = "See how collectors rank by what their shelves are worth, by genre, and by how much of the hundred they own — then add your records and find out where you land. Collections are private by default.";
  } else if (route === "collection") {
    title = "My Collection — Track & Value Your Vinyl | The Lead-In";
    desc = "Add a record and it arrives priced at its market median, graded for condition and tracked week to week. Free to start, and a Discogs library imports in one tap.";
  } else if (route === "pressing-guide") {
    title = "Pressing Guides | The Lead-In";
    desc = "Which pressing to buy and which to avoid — runout etchings, labels, reissues and mastering notes, so you can tell which edition you own and what it is worth.";
  } else if (route === "public-collection") {
    title = ((publicUser && publicUser.name) ? publicUser.name + "'s collection" : "A collector's collection") + " | The Lead-In";
    desc = "A public vinyl collection on The Lead-In — every record priced at its market median and graded for condition. Build yours and it can read the same way.";
  } else if (route === "account") {
    title = "Account settings | The Lead-In";
    desc = BBR_SITE_DESC;
  } else if (route === "today") {
    // The album is in the title deliberately: this URL gets shared and pasted, and
    // "Album of the Day" alone tells a reader nothing about whether to click.
    const t = window.bbrAlbumOfDay && window.bbrAlbumOfDay(0);
    title = t ? ("Today: " + t.title + " — " + t.artist + " | The Lead-In")
              : "Album of the Day | The Lead-In";
    desc = t
      ? ("Today from the hundred greatest albums: " + t.title + " by " + t.artist +
         " (" + t.year + "), ranked №" + t.rank + ". Read the case for it, then add it to your collection.")
      : "One record a day from the hundred greatest albums, chosen by the date. Read the case for it, then add it to your collection.";
  } else if (route === "vote") {
    title = "Vote for next week\u2019s Deep Dive | The Lead-In";
    desc = "A shortlist of records goes up every Wednesday and the one with the most votes gets written up in full \u2014 the essay, the pressing guide, the history. One vote each, no account needed.";
  } else if (route === "help") {
    title = "Help & Support | The Lead-In";
    desc = "How-to guides, frequently asked questions and contact details for The Lead-In — adding records, valuations, leaderboards, importing your collection and more.";
  } else if (route === "ambassadors") {
    title = "Ambassador Programme — Partner with The Lead-In | For Record Shops";
    desc = "Independent record shops: earn a 50/50 share of subscription revenue from customers you refer. We track what a collection is worth, we don't sell records — so there's no competition. Join the founding waitlist.";
  } else if (route === "legal") {
    title = "Legal | The Lead-In";
    desc = BBR_SITE_DESC;
  } else {
    title = "The Lead-In — Value the Vinyl You Own, Record by Record";
    desc = BBR_SITE_DESC;
  }
  document.title = title;
  bbrSetMetaTag('meta[name="description"]', "content", desc);
  bbrSetMetaTag('meta[property="og:title"]', "content", title);
  bbrSetMetaTag('meta[property="og:description"]', "content", desc);
  bbrSetMetaTag('meta[name="twitter:title"]', "content", title);
  bbrSetMetaTag('meta[name="twitter:description"]', "content", desc);
  // Canonical + og:url track the real path now that pages have their own URLs.
  const url = "https://www.theleadin.com" + bbrRouteToPath(route, album && album.slug, publicUser, album && album.slug);
  bbrSetMetaTag('link[rel="canonical"]', "href", url);
  bbrSetMetaTag('meta[property="og:url"]', "content", url);
}

// Sitewide first-record activation nudge. Shows for a logged-in member who has
// zero records, on every route EXCEPT /collection (which has its own empty-state
// checklist). Reads the live count from BBR_store (reactive: disappears the
// instant they add one); waits for collectionV2Loaded so it never flashes before
// the load settles. Dismissible per-user, persisted in localStorage.
// High-intent content surfaces the nudge is allowed on — deliberately NOT
// /collection (has its own empty-state checklist) nor utility routes (account,
// help, legal, pressing-guide, public collections).
const FRB_ROUTES = ["home", "album", "deep-dive", "deep-dives", "list", "leaderboards"];
function FirstRecordBanner({ user, route, onCollection }) {
  const [, force] = useState(0);
  const [dismissed, setDismissed] = useState(false);
  useEffect(() => {
    if (!window.BBR_store) return;
    return window.BBR_store.subscribe(() => force(n => n + 1));
  }, []);
  const uid = user && user.id;
  const dkey = uid ? "bbr_frb_dismissed_" + uid : null;
  useEffect(() => {
    try { setDismissed(dkey ? localStorage.getItem(dkey) === "1" : false); } catch (e) { setDismissed(false); }
  }, [dkey]);
  const st = window.BBR_store;
  const visible = !!user && FRB_ROUTES.includes(route) && !!st && st.collectionV2Loaded &&
    (st.collectionV2 || []).length === 0 && !dismissed;
  // While the banner shows, flag the body so the floating feedback launcher
  // (bottom-right, shared on Leaderboards) lifts clear of the bar instead of
  // overlapping its controls.
  useEffect(() => {
    document.body.classList.toggle("frb-active", visible);
    return () => document.body.classList.remove("frb-active");
  }, [visible]);
  if (!visible) return null;
  const dismiss = () => {
    try { if (dkey) localStorage.setItem(dkey, "1"); } catch (e) {}
    setDismissed(true);
  };
  return (
    <div className="frb" role="region" aria-label="Finish setting up your collection">
      <div className="frb-in">
        <span className="frb-badge" aria-hidden="true">◆</span>
        <span className="frb-txt"><b>Finish setting up.</b> Add your first record for a live valuation and your place on the leaderboard.</span>
        <button type="button" className="frb-cta" onClick={onCollection}>Add a record →</button>
        <button type="button" className="frb-x" onClick={dismiss} aria-label="Dismiss">×</button>
      </div>
    </div>
  );
}

function App() {
  const [route, setRoute] = useState("home"); // 'home' | 'list' | 'album' | 'collection' | 'deep-dives' | 'deep-dive'
  const [albumSlug, setAlbumSlug] = useState(null);
  const [deepDiveSlug, setDeepDiveSlug] = useState(null);
  const [tweakOn, setTweakOn] = useState(false);
  const [tweaks, setTweaks] = useState(TWEAK_DEFAULTS);

  // ---- My Collection: auth + demo state ----
  const [user, setUser] = useState(null);                     // null = logged out (real Supabase session)
  const [authReady, setAuthReady] = useState(false);          // false until the initial session check resolves
  const [dataMode, setDataMode] = useState("populated");        // 'populated' | 'empty'
  const [authView, setAuthView] = useState(null);              // null|'login'|'signup'|'forgot'
  const [recovery, setRecovery] = useState(false);             // true => show the set-new-password form (arrived via reset link)
  const [gate, setGate] = useState(null);                       // null|'collection'|'wishlist'
  const [publicUser, setPublicUser] = useState(null);           // {id, name} for the public collection view

  const albums = window.ALBUMS;

  // Restore from localStorage
  useEffect(() => {
    // Routing is driven by the URL path so every page is its own crawlable URL
    // and a refresh keeps you put. A bare "/" (no descriptor) falls through to
    // the default 'home'. Old #hash deep links are honoured for back-compat.
    const parsed = bbrParsePath();
    if (parsed) {
      if (parsed.route === "public-collection") { setPublicUser({ id: parsed.publicId, name: "" }); setRoute("public-collection"); }
      else if (parsed.route === "album") { setAlbumSlug(parsed.slug); setRoute("album"); }
      else if (parsed.route === "deep-dive") { setDeepDiveSlug(parsed.slug); setRoute("deep-dive"); }
      else setRoute(parsed.route);
    }
    const saved = {};
    ["accent", "grain"].forEach(k => {
      const v = localStorage.getItem("bbr:" + k);
      if (v) saved[k] = v;
    });
    if (Object.keys(saved).length) setTweaks(t => ({ ...t, ...saved }));
    // (auth is restored from the real Supabase session, see the effect below)
    const dm = localStorage.getItem("bbr:mc:dm");
    if (dm === "empty" || dm === "populated") setDataMode(dm);
  }, []);

  // ---- real Supabase auth: hydrate session on mount + subscribe to changes ----
  useEffect(() => {
    if (!window.BBR_auth) { setAuthReady(true); return; }
    let unsub = null;
    const sync = (u) => {
      setUser(u);
      if (window.BBR_store) {
        if (u && u.id) window.BBR_store.onLogin(u.id);
        else window.BBR_store.onLogout();
      }
    };
    window.BBR_auth.current().then((u) => { if (u) sync(u); }).finally(() => setAuthReady(true));
    unsub = window.BBR_auth.onChange((u) => { sync(u); setAuthReady(true); });
    return () => { if (unsub) unsub(); };
  }, []);

  // ---- password recovery: if the user arrived via a reset-email link, show
  // the set-new-password form instead of dropping them into their account. The
  // latch in supabase.js catches the PASSWORD_RECOVERY event even if it fired
  // before this mounts; we also subscribe in case it fires after. ----
  useEffect(() => {
    if (!window.BBR_auth || !window.BBR_auth.onRecovery) return;
    if (window.BBR_auth.recoveryPending()) setRecovery(true);
    return window.BBR_auth.onRecovery(() => setRecovery(true));
  }, []);
  // Keep the URL hash in sync with the current page so a refresh restores
  // where you are AND browser back/forward step through in-app pages. Each
  // in-app navigation pushes a history entry; when back/forward fires popstate
  // the browser has already updated the hash, so target === location.hash here
  // and we skip the push (no loop). Skip the first pass: on mount the hash IS
  // the source of truth (the restore effect reads it).
  const hashSyncReady = React.useRef(false);
  // How many in-app history entries we've pushed this session. Lets a "back"
  // button fall back to the homepage when there's nothing of ours to go back to
  // (e.g. the visitor deep-linked straight onto a page) instead of leaving the site.
  const navDepthRef = React.useRef(0);
  useEffect(() => {
    if (!hashSyncReady.current) { hashSyncReady.current = true; return; }
    const target = bbrRouteToPath(route, albumSlug, publicUser, deepDiveSlug);
    if (location.pathname !== target) {
      // strip any legacy hash when we push a clean path
      history.pushState(null, "", target + location.search);
      navDepthRef.current += 1;
    } else if (location.hash) {
      history.replaceState(null, "", target + location.search);
    }
  }, [route, albumSlug, publicUser, deepDiveSlug]);
  // Back/forward: apply the route encoded in the (browser-updated) path.
  useEffect(() => {
    const onPop = () => {
      if (navDepthRef.current > 0) navDepthRef.current -= 1;
      const parsed = bbrParsePath();
      if (!parsed) { setRoute("home"); setAlbumSlug(null); return; }
      if (parsed.route === "public-collection") { setPublicUser({ id: parsed.publicId, name: "" }); setRoute("public-collection"); }
      else if (parsed.route === "album") { setAlbumSlug(parsed.slug); setRoute("album"); }
      else if (parsed.route === "deep-dive") { setDeepDiveSlug(parsed.slug); setRoute("deep-dive"); }
      else { setAlbumSlug(null); setRoute(parsed.route); }
    };
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);
  // 'account' is the only strictly logged-in route — if a refresh lands there
  // without a session, send the visitor home rather than show a blank page.
  useEffect(() => {
    if (authReady && !user && route === "account") { setRoute("home"); setAlbumSlug(null); }
  }, [authReady, user, route]);
  useEffect(() => {
    Object.entries(tweaks).forEach(([k, v]) => localStorage.setItem("bbr:" + k, v));
  }, [tweaks]);
  useEffect(() => { localStorage.setItem("bbr:mc:dm", dataMode); }, [dataMode]);
  // setMode only affects guests now; logged-in data is driven by onLogin/onLogout.
  useEffect(() => { if (window.BBR_store && !user) window.BBR_store.setMode(); }, [dataMode, user]);

  // Apply accent + grain to document
  useEffect(() => {
    document.documentElement.style.setProperty("--accent", ACCENTS[tweaks.accent] || ACCENTS.vermillion);
    document.body.classList.toggle("grain", tweaks.grain !== "off");
  }, [tweaks.accent, tweaks.grain]);

  // Tweak mode
  useEffect(() => {
    const onMsg = (e) => {
      const d = e.data || {};
      if (d.type === "__activate_edit_mode") setTweakOn(true);
      if (d.type === "__deactivate_edit_mode") setTweakOn(false);
    };
    window.addEventListener("message", onMsg);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", onMsg);
  }, []);

  const setKey = (k, v) => {
    setTweaks(t => ({ ...t, [k]: v }));
    window.parent.postMessage({ type: "__edit_mode_set_keys", edits: { [k]: v } }, "*");
  };

  // Remember where the user was on the list, so returning from a deep dive
  // lands them back at the same scroll position instead of jumping to the top.
  const listScrollRef = useRef(0);
  const restoreListScrollRef = useRef(false);

  const goHome = () => { setRoute("home"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goList = () => {
    // Coming back from an album → restore the saved list scroll position.
    restoreListScrollRef.current = (route === "album");
    setRoute("list"); setAlbumSlug(null);
    if (!restoreListScrollRef.current) window.scrollTo(0, 0);
  };
  const goAlbum = (slugOrAlbum) => {
    // Capture the list scroll position before leaving so we can return to it.
    if (route === "list") listScrollRef.current = window.scrollY;
    const slug = typeof slugOrAlbum === "string" ? slugOrAlbum : slugOrAlbum.slug;
    setAlbumSlug(slug); setRoute("album"); window.scrollTo(0, 0);
  };
  // Published so surfaces several layers down can navigate to an album without a
  // callback threaded through components that have no other interest in it — the
  // two "gaps in the 100" grids (HomeManifesto's GapsBand, CollectionDashboard_v2)
  // used to be external buy links and are now internal album links. Re-assigned on
  // every commit so the closure never goes stale over `route`. Anything reading it
  // guards on its existence and falls back to a plain href, so a hard navigation is
  // always the worst case.
  useEffect(() => { window.BBR_goAlbum = goAlbum; });

  // After the list re-mounts, restore the saved scroll position if we arrived
  // here via "back" from a deep dive. A couple of rAF passes lets row heights
  // settle (cover art, etc.) before we snap to the target offset.
  useLayoutEffect(() => {
    if (route !== "list" || !restoreListScrollRef.current) return;
    restoreListScrollRef.current = false;
    const target = listScrollRef.current;
    window.scrollTo(0, target);
    let n = 0;
    const settle = () => {
      window.scrollTo(0, target);
      if (++n < 3) requestAnimationFrame(settle);
    };
    requestAnimationFrame(settle);
  }, [route]);
  const goCollection = () => { setRoute("collection"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goLegal = () => { setRoute("legal"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goHelp = () => { setRoute("help"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goToday = () => { setRoute("today"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goVote = () => { setRoute("vote"); setAlbumSlug(null); window.scrollTo(0, 0); };
  // Return to wherever the visitor came from. If they navigated within the app
  // we step back through history (so it lands on the actual previous page);
  // if they deep-linked straight in, there's nothing of ours behind us, so go home.
  const goBack = () => { if (navDepthRef.current > 0) window.history.back(); else goHome(); };
  const goPressingGuide = () => { setRoute("pressing-guide"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goLeaderboards = () => { setRoute("leaderboards"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goPublicCollection = (uid, name) => { setPublicUser({ id: uid, name: name || "" }); setRoute("public-collection"); window.scrollTo(0, 0); };
  const goAccount = () => { setRoute("account"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goDeepDives = () => { setRoute("deep-dives"); setAlbumSlug(null); setDeepDiveSlug(null); window.scrollTo(0, 0); };
  const goAmbassadors = () => { setRoute("ambassadors"); setAlbumSlug(null); window.scrollTo(0, 0); };
  const goDeepDive = (slugOrRow) => {
    const slug = typeof slugOrRow === "string" ? slugOrRow : (slugOrRow && slugOrRow.slug);
    if (!slug) return;
    setDeepDiveSlug(slug); setRoute("deep-dive"); setAlbumSlug(null); window.scrollTo(0, 0);
  };
  window.__nav = { goHome, goList, goAlbum, goCollection, goLegal, goHelp, goBack, goPressingGuide, goLeaderboards, goPublicCollection, goAccount, goDeepDives, goDeepDive, goAmbassadors };

  // ---- auth helpers ----
  // Why a signup lands where it does. The split is about WHERE the thing they
  // wanted to do lives. collection/wishlist put a record on their shelf, so
  // /collection is the payoff and the right place to land. But streak, nominate,
  // vote and comment all act on the page the reader is already on — the ballot,
  // the discussion, the Album of the Day card — so sending them to an empty
  // /collection abandons the very action that made them sign up. Those return in
  // place. The auth overlay sits over the current route without changing it, so
  // "return in place" is simply: don't navigate.
  //
  // We remember the intent that opened the gate in a ref, not state: it is read
  // once inside an event handler and must not trigger a re-render or be caught
  // mid-transition.
  const pendingIntentRef = useRef(null);
  // Intents whose action lives on the current page — landing on /collection would
  // strand the reader away from what they were signing up to do.
  const STAY_IN_PLACE_INTENTS = ["streak", "nominate", "vote", "comment"];
  // keepIntent is passed only by the gate's own login/signup buttons, which
  // carry the intent forward. Every other entry to the auth overlay — the nav
  // "Sign in", a homepage CTA — clears it, so a later unrelated sign-in falls
  // back to the default /collection landing rather than honouring a stale intent.
  const openAuth = (mode, keepIntent) => { if (!keepIntent) pendingIntentRef.current = null; setAuthView(mode); };
  const onAuthed = (u) => {
    const intent = pendingIntentRef.current;
    pendingIntentRef.current = null;
    setUser(u || null); setAuthView(null); setGate(null);
    if (STAY_IN_PLACE_INTENTS.includes(intent)) return;
    goCollection();
  };
  const signOut = async () => { try { if (window.BBR_auth) await window.BBR_auth.signOut(); } catch (e) {} setUser(null); goCollection(); };
  const requireAuth = (intent) => { if (user) return true; pendingIntentRef.current = intent || "collection"; setGate(intent || "collection"); return false; };
  window.__bbrRequireAuth = requireAuth; // available to essay pages etc.

  const currentAlbum = albumSlug ? albums.find(a => a.slug === albumSlug) : albums[0];

  // Per-route document title + meta description (for JS-rendering crawlers and
  // for the browser tab). The static <head> + #root fallback cover no-JS bots.
  useEffect(() => { bbrApplyRouteMeta(route, route === "album" ? currentAlbum : null, publicUser); },
    [route, albumSlug, currentAlbum, publicUser]);

  return (
    <>
      <TopNav
        route={route} onHome={goHome} onList={goList} onCollection={goCollection}
        onLeaderboards={goLeaderboards} onDeepDives={goDeepDives} onHelp={goHelp} onToday={goToday}
        currentRank={currentAlbum?.rank}
        user={user} onSignIn={() => openAuth("login")} onSignOut={signOut} onSettings={goAccount}
      />

      <FirstRecordBanner user={user} route={route} onCollection={goCollection} />

      {route === "home" && (
        <>
        {/* A daily reason to open the site: the same record as that morning's email,
            plus the visit streak. Above the manifesto because a returning visitor
            should see what changed today before the standing pitch. */}
        {/* user + authReady drive the streak block's signed-out state. authReady
            matters: userId resolves asynchronously, so without it a signed-in
            collector would see "sign in to start a streak" flash over their own
            streak on every load. */}
        {window.TodayCard ? <TodayCard onAlbum={goAlbum} onRequireAuth={requireAuth} user={user} authReady={authReady} /> : null}
        <Home albums={albums} onAlbum={goAlbum} onList={goList} user={user} authReady={authReady} onCollection={goCollection} onLeaderboards={goLeaderboards} onAuthOpen={openAuth} />
        </>
      )}
      {route === "vote" && window.BallotSection && (
        <BallotSection variant="page" user={user} onRequireAuth={requireAuth}
                       magic={window.bbrBallotMagic && window.bbrBallotMagic()} />
      )}
      {route === "today" && window.TodayPage && (
        <TodayPage onAlbum={goAlbum} onHome={goHome} onList={goList} onRequireAuth={requireAuth} user={user} />
      )}
      {route === "list" && (
        <ListPage albums={albums} onAlbum={goAlbum} onRequireAuth={requireAuth} />
      )}
      {route === "album" && currentAlbum && (
        <AlbumPage
          album={currentAlbum} onHome={goHome} onList={goList} onAlbum={goAlbum} all={albums}
          user={user} onGate={(intent) => setGate(intent || "collection")}
        />
      )}
      {route === "deep-dives" && window.DeepDives && (
        <DeepDives albums={albums} onDeepDive={goDeepDive} onHome={goHome}
                   user={user} onRequireAuth={requireAuth} />
      )}
      {route === "deep-dive" && deepDiveSlug && window.DeepDiveArticle && (
        <DeepDiveArticle
          slug={deepDiveSlug} all={albums} user={user}
          onAlbum={goAlbum} onHome={goHome} onDeepDives={goDeepDives} onDeepDive={goDeepDive}
          onGate={(intent) => setGate(intent || "collection")}
        />
      )}
      {route === "deep-dives-admin" && window.DeepDivesAdmin && (
        <DeepDivesAdmin onHome={goHome} onDeepDives={goDeepDives} />
      )}
      {route === "collection" && (
        <MyCollection
          albums={albums} user={user} dataMode={dataMode}
          onAuthOpen={openAuth} onGate={(intent) => setGate(intent || "collection")}
        />
      )}
      {window.FeedbackWidget && ["collection", "leaderboards", "pressing-guide", "help"].includes(route) && (
        <FeedbackWidget page={route} user={user} />
      )}
      {/* The newsletter popup is deliberately NOT mounted. Over its last reported
          week it was shown 59 times, dismissed 40 and submitted 0 — and on the
          homepage its dwell trigger fired straight over the top of the Album of
          the Day card, i.e. over the one surface with something to say. The ask it
          made has moved to the end of an album essay, where a reader has actually
          read one first (components/AlbumPage.jsx). The component is left in the
          tree rather than deleted so re-enabling it is one line if that turns out
          to be wrong. */}
      {route === "legal" && window.Legal && (
        <Legal onHome={goHome} />
      )}
      {route === "help" && window.Help && (
        <Help
          onHome={goHome}
          onBack={goBack}
          onCollection={goCollection}
          onPressingGuide={goPressingGuide}
          onLeaderboards={goLeaderboards}
          onList={goList}
        />
      )}
      {route === "pressing-guide" && window.PressingGuide && (
        <PressingGuide onHome={goHome} onCollection={goCollection} />
      )}
      {route === "ambassadors" && window.Ambassadors && (
        <Ambassadors />
      )}
      {route === "leaderboards" && window.Leaderboards_v2 && (
        <Leaderboards_v2 albums={albums} currentUserId={user && user.id} onViewCollection={goPublicCollection} />
      )}
      {route === "public-collection" && window.PublicCollectionV2 && publicUser && (
        <PublicCollectionV2 userId={publicUser.id} displayName={publicUser.name} albums={albums} onBack={goLeaderboards} />
      )}
      {route === "account" && user && window.AccountSettings && (
        <AccountSettings user={user} onBack={goCollection} onSignOut={signOut} onViewPublic={goPublicCollection} />
      )}

      {window.Footer && (
        <Footer
          onLegal={goLegal}
          onHelp={goHelp}
          onPressingGuide={goPressingGuide}
          onList={goList}
          onCollection={goCollection}
          onAmbassadors={goAmbassadors}
        />
      )}

      {/* Auth screens. Recovery (set-new-password) takes precedence: the user
          followed a reset link and must be able to actually change their password. */}
      {recovery ? (
        <AuthScreen
          mode="reset"
          onMode={(m) => { if (window.BBR_auth) window.BBR_auth.clearRecovery(); setRecovery(false); setAuthView(m); }}
          onAuthed={(u) => { if (window.BBR_auth) window.BBR_auth.clearRecovery(); setRecovery(false); onAuthed(u); }}
          onClose={() => { if (window.BBR_auth) window.BBR_auth.clearRecovery(); setRecovery(false); }}
        />
      ) : authView && (
        <AuthScreen mode={authView} onMode={setAuthView} onAuthed={onAuthed} onClose={() => setAuthView(null)} />
      )}

      {/* Signup gate */}
      {gate && (
        <SignupGate
          intent={gate}
          onSignup={() => { setGate(null); openAuth("signup", true); }}
          onLogin={() => { setGate(null); openAuth("login", true); }}
          onClose={() => { pendingIntentRef.current = null; setGate(null); }}
        />
      )}

      {tweakOn && (
        <div className="tweaks" onClick={(e) => e.stopPropagation()}>
          <h4>Tweaks</h4>
          <div className="t-row">
            <label>Accent color</label>
            <div className="t-opts" style={{ flexWrap: "wrap" }}>
              {Object.keys(ACCENTS).map(k => (
                <button
                  key={k}
                  className={tweaks.accent === k ? "active" : ""}
                  onClick={() => setKey("accent", k)}
                  title={k}
                  style={{ minWidth: 0, padding: "6px 8px" }}
                >
                  <span style={{ display: "inline-block", width: 10, height: 10, background: ACCENTS[k], marginRight: 6, verticalAlign: "middle" }} />
                  {k}
                </button>
              ))}
            </div>
          </div>
          <div className="t-row">
            <label>Paper grain</label>
            <div className="t-opts">
              <button className={tweaks.grain !== "off" ? "active" : ""} onClick={() => setKey("grain", "on")}>On</button>
              <button className={tweaks.grain === "off" ? "active" : ""} onClick={() => setKey("grain", "off")}>Off</button>
            </div>
          </div>
        </div>
      )}
    </>
  );
}

function AccountMenu({ user, onCollection, onSettings, onSignOut }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);
  return (
    <div className="nav-acct" ref={ref}>
      {/* A clickable div gave no keyboard access at all, so a signed-in user
          could not reach Sign out without a mouse. */}
      <button
        type="button"
        className="nav-avatar"
        onClick={() => setOpen(o => !o)}
        aria-expanded={open}
        aria-haspopup="menu"
        aria-label={"Account menu for " + user.name}
      >
        <span className="av">{user.initials}</span>
        <span className="nm">{user.name.split(" ")[0]}</span>
      </button>
      {open && (
        <div className="nav-menu" role="menu">
          <div className="nav-menu-head">
            <div className="nm">{user.name}</div>
            <div className="em">{user.email}</div>
          </div>
          <a role="menuitem" href="/collection" onClick={(e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); setOpen(false); onCollection(); }}>
            <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4"><circle cx="8" cy="8" r="6" /><circle cx="8" cy="8" r="1.6" /></svg>
            My Collection
          </a>
          <a role="menuitem" href="/account" onClick={(e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); setOpen(false); onSettings(); }}>
            <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4"><circle cx="8" cy="8" r="2.2" /><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.4 1.4M11.6 11.6L13 13M13 3l-1.4 1.4M4.4 11.6L3 13" /></svg>
            Account settings
          </a>
          <button type="button" role="menuitem" className="sep signout" onClick={() => { setOpen(false); onSignOut(); }}>
            <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4"><path d="M6 2H3v12h3M10 5l3 3-3 3M13 8H6" /></svg>
            Sign out
          </button>
        </div>
      )}
    </div>
  );
}

function TopNav({ route, onHome, onList, onCollection, onLeaderboards, onDeepDives, onHelp, onToday, currentRank, user, onSignIn, onSignOut, onSettings }) {
  const [menuOpen, setMenuOpen] = React.useState(false);
  const navRef = React.useRef(null);

  // --nav-h is what every page under a fixed header uses to clear it:
  //   .album-page  { padding-top: var(--nav-h) }
  //   .album-crumbs{ position: sticky; top: var(--nav-h) }
  // plus the dossier rails and the mobile pill nav. It was three hardcoded
  // guesses — 64px, 56px under 640, 50px under 380 — at a height that is not
  // constant: the bar is sized by its own content (the 44px burger appears below
  // ~960px), by breakpoint padding, and by env(safe-area-inset-top) on a notched
  // phone. Measured against the real bar it was wrong at every width:
  //
  //     1280px  real 68  var 64  ->  4px of the crumbs bar under the header
  //      900px  real 73  var 64  ->  9px
  //      633px  real 69  var 56  -> 13px
  //      375px  real 65  var 50  -> 15px
  //
  // which is exactly the reported "the Return to The Hundred bar is partly hidden
  // by the header", worst on the phone. So measure it instead of guessing, and
  // keep it correct through resize, rotation, the menu opening and font swap.
  // The CSS values stay as the pre-hydration fallback.
  React.useEffect(() => {
    const el = navRef.current;
    if (!el) return;
    const apply = () => {
      const h = Math.round(el.getBoundingClientRect().height);
      // Clamped, because this variable offsets whole pages: if the mobile drawer
      // ever stops being position:fixed and starts growing the bar instead, an
      // unclamped read would push every album page down by the height of the menu.
      // 40-120px covers every real bar (measured 65-73) and refuses the absurd.
      if (h >= 40 && h <= 120) document.documentElement.style.setProperty("--nav-h", h + "px");
    };
    apply();
    let ro = null;
    if (typeof ResizeObserver === "function") { ro = new ResizeObserver(apply); ro.observe(el); }
    window.addEventListener("resize", apply, { passive: true });
    window.addEventListener("orientationchange", apply);
    // Fonts land after first paint and change the bar's height with them.
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(apply).catch(() => {});
    return () => {
      if (ro) ro.disconnect();
      window.removeEventListener("resize", apply);
      window.removeEventListener("orientationchange", apply);
    };
  }, []);
  const ddLabel = (window.BBR_DEEPDIVES && window.BBR_DEEPDIVES.LABEL) || "Deep Dives";
  // Wrap each nav action so tapping a link also closes the mobile menu.
  const go = (fn) => () => { setMenuOpen(false); if (fn) fn(); };
  // Real hrefs, not onClick-only <a>. Href-less anchors aren't focusable and are
  // announced as plain text, so the whole nav was invisible to keyboards and
  // screen readers. navTo() keeps SPA routing on a plain left-click while letting
  // cmd/ctrl/middle-click open a new tab, and leaves a crawlable link behind.
  const navTo = (href, fn) => ({
    href,
    onClick: (e) => {
      if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
      e.preventDefault();
      go(fn)();
    },
  });
  return (
    <nav ref={navRef} className={"topnav" + (menuOpen ? " topnav--open" : "")}>
      <a className="brand" {...navTo("/", onHome)} aria-label="The Lead-In, home">
        <img className="brand-mark" src="/brand-mark.png?v=1" alt="" />
        <span className="brand-name"><em className="brand-art">The</em>Lead-In</span>
      </a>

      <button
        className="nav-burger"
        aria-label={menuOpen ? "Close menu" : "Open menu"}
        aria-expanded={menuOpen}
        onClick={() => setMenuOpen((v) => !v)}
      >
        <span /><span /><span />
      </button>

      <div className={"links" + (menuOpen ? " links--open" : "")} style={{ alignItems: "center" }}>
        <a className={route === "home" ? "active" : ""} aria-current={route === "home" ? "page" : undefined} {...navTo("/", onHome)}>Home</a>
        <a className={route === "today" ? "active" : ""} aria-current={route === "today" ? "page" : undefined} {...navTo("/today", onToday)}>Today</a>
        <a className={route === "list" ? "active" : ""} aria-current={route === "list" ? "page" : undefined} {...navTo("/list", onList)}>The List 100→1</a>
        <a className={(route === "deep-dives" || route === "deep-dive") ? "active" : ""} aria-current={(route === "deep-dives" || route === "deep-dive") ? "page" : undefined} {...navTo("/deep-dives", onDeepDives)}>{ddLabel}</a>
        <a className={route === "collection" ? "active" : ""} aria-current={route === "collection" ? "page" : undefined} {...navTo("/collection", onCollection)}>My Collection</a>
        <a className={route === "leaderboards" ? "active" : ""} aria-current={route === "leaderboards" ? "page" : undefined} {...navTo("/leaderboards", onLeaderboards)}>Leaderboards</a>
        <a className={route === "help" ? "active" : ""} aria-current={route === "help" ? "page" : undefined} {...navTo("/help", onHelp)}>Help</a>
        {user
          ? <AccountMenu user={user} onCollection={go(onCollection)} onSettings={go(onSettings)} onSignOut={go(onSignOut)} />
          : <button className="nav-signin" onClick={go(onSignIn)}>Sign in</button>}
      </div>
    </nav>
  );
}

// Error boundary: a crash in any subtree shows a fallback instead of
// blanking the entire site. Logs the error so the console still shows it.
class BBRErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { error: null }; }
  static getDerivedStateFromError(error) { return { error }; }
  componentDidCatch(error, info) { console.error("[BBR] caught render error:", error, info); }
  render() {
    if (this.state.error) {
      return (
        <div style={{ maxWidth: 560, margin: "12vh auto", padding: "0 24px", fontFamily: "var(--sans, system-ui)", color: "var(--ink, #17130f)" }}>
          <h1 style={{ fontFamily: "var(--serif, Georgia)", fontSize: 28, marginBottom: 12 }}>Something went wrong on this page.</h1>
          <p style={{ lineHeight: 1.6, opacity: 0.8 }}>The rest of the site is fine, try reloading, or head back to the homepage. If it keeps happening, the details are in the browser console.</p>
          <button onClick={() => { location.hash = ""; location.reload(); }}
            style={{ marginTop: 16, padding: "10px 18px", border: "1px solid currentColor", background: "transparent", color: "inherit", borderRadius: 6, cursor: "pointer" }}>
            Reload the site
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// createRoot() clears #root's children, which removes the prerendered .seo-shell
// the visitor has been reading. Marking <html> ready also drops the loading bar
// (see html:not(.app-ready)::after in index.html).
ReactDOM.createRoot(document.getElementById("root")).render(
  <BBRErrorBoundary><App /></BBRErrorBoundary>
);
// A timer, not rAF: rAF is paused in background tabs, which would leave the
// loading bar spinning over a fully-booted app.
setTimeout(() => document.documentElement.classList.add("app-ready"), 0);
