/* London Free Guide — home page components.
   Render from window.LFG_CONTENT + filter/cost/kind metadata.
   Exported to window at the end. */
const { useState, useMemo, useEffect } = React;

const COST = window.LFG_COST;
const KIND = window.LFG_KIND;
const FILTERS = window.LFG_FILTERS;

/* ---- small external-link icon (replaces the bare ↗ across the site) ---- */
function ExtIcon() {
  return (
    <svg className="ext-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M14 4h6v6"></path>
      <path d="M20 4l-9 9"></path>
      <path d="M18 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6"></path>
    </svg>
  );
}

/* ---- Brand logo lockup (theme-aware: white wordmark on dark, dark on light) ---- */
function Logo() {
  return (
    <span className="logo-lockup">
      <img className="logo-img logo-dark" src="img/logo-horizontal.svg" alt="London Free Guide" width="232" height="28" />
      <img className="logo-img logo-light" src="img/logo-horizontal-light.svg" alt="London Free Guide" width="232" height="28" />
    </span>
  );
}

/* ---- light / dark theme toggle (persisted) ---- */
function ThemeToggle() {
  const [theme, setTheme] = useState(
    (typeof document !== "undefined" && document.documentElement.getAttribute("data-theme")) || "dark"
  );
  function flip() {
    const next = theme === "dark" ? "light" : "dark";
    document.documentElement.setAttribute("data-theme", next);
    try { localStorage.setItem("lfg-theme", next); } catch (e) {}
    setTheme(next);
  }
  return (
    <button className="theme-toggle" onClick={flip} aria-label={"Switch to " + (theme === "dark" ? "light" : "dark") + " mode"} title="Toggle light / dark">
      {theme === "dark" ? (
        <svg className="ti" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <circle cx="12" cy="12" r="4.1" />
          <path d="M12 2.4v2.3M12 19.3v2.3M4.6 4.6l1.7 1.7M17.7 17.7l1.7 1.7M2.4 12h2.3M19.3 12h2.3M4.6 19.4l1.7-1.7M17.7 6.3l1.7-1.7" />
        </svg>
      ) : (
        <svg className="ti" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M20.5 13.3A8 8 0 1 1 10.7 3.5a6.3 6.3 0 0 0 9.8 9.8z" />
        </svg>
      )}
    </button>
  );
}

/* ============================================================= *
 *  Membership — the one passwordless account state.
 *  useMember() subscribes to window.LFGAccount; MagicLink is the shared
 *  email-capture form every gate reuses; AccountControl is the nav entry.
 * ============================================================= */
function useMember() {
  const [m, setM] = useState(function () { return (window.LFGAccount && window.LFGAccount.get()) || null; });
  useEffect(function () {
    const h = function () { setM((window.LFGAccount && window.LFGAccount.get()) || null); };
    window.addEventListener("lfg-member-change", h);
    return function () { window.removeEventListener("lfg-member-change", h); };
  }, []);
  return m;
}

/* the shared join form — same component behind every capture moment (a spot
   tip, the saved page, the nav). Submitting subscribes the email to the
   newsletter AND sets the on-device joined flag. */
function MagicLink({ cta, note, onJoined, autoFocus }) {
  const [email, setEmail] = useState("");
  function submit(e) {
    e.preventDefault();
    const v = email.trim();
    if (!v || v.indexOf("@") < 1) return;
    /* Device-based join (launch "B+"): subscribe the email to the newsletter
       (Buttondown) AND set the on-device joined flag so insider tips unlock
       here. No login, no cross-device sync — that's the Phase 2 server piece. */
    if (window.LFGNewsletter) window.LFGNewsletter.subscribe(v);
    if (window.LFGAccount) window.LFGAccount.join(v);
    if (onJoined) onJoined(v);
  }
  return (
    <form className="ml-form" onSubmit={submit}>
      <div className="ml-row">
        <input type="email" required autoFocus={!!autoFocus} value={email}
          onChange={(e) => setEmail(e.target.value)} placeholder="your email" aria-label="Email address" />
        <button className="btn" type="submit">{cta || "Join free"}</button>
      </div>
      <div className="ml-note">{note || "Unlocks every insider tip on this device and gets you the weekly free-London email. No spam, leave any time, we never sell your email."}</div>
    </form>
  );
}

const ACCOUNT_UNLOCKS = [
  "Unlock every insider tip on this device",
  "The weekly free-London roundup by email",
  "Save spots as you browse, kept on this device"
];

/* popover body — joining */
function JoinView() {
  return (
    <div className="acc-body">
      <span className="acc-eyebrow">free · no password</span>
      <h3 className="acc-title">Join the <span className="r">list</span></h3>
      <p className="acc-sub">Drop your email to unlock every insider tip on this device and get the weekly free-London roundup. The spots, the A–Z and the guides are always free.</p>
      <MagicLink cta="Join free" autoFocus />
      <ul className="acc-perks">
        {ACCOUNT_UNLOCKS.map((u) => <li key={u}>{u}</li>)}
      </ul>
      <p className="acc-foot">Everything else stays free and open. No wall, ever.</p>
    </div>
  );
}

/* popover body — already on the list */
function MemberView({ member, onClose }) {
  return (
    <div className="acc-body">
      <span className="acc-eyebrow ok">you're in</span>
      <h3 className="acc-title">On the <span className="r">list</span></h3>
      {member.email
        ? <p className="acc-sub">Joined as <b>{member.email}</b>. Insider tips are unlocked on this device.</p>
        : <p className="acc-sub">Insider tips are unlocked on this device.</p>}

      <div className="acc-pending">
        <p>One last thing: check your inbox{member.email ? <> at <b>{member.email}</b></> : ""} to confirm the weekly email. That's the only step.</p>
      </div>

      <ul className="acc-perks on">
        {ACCOUNT_UNLOCKS.map((u) => <li key={u}>{u}</li>)}
      </ul>

      <div className="acc-actions">
        <a className="acc-link" href="Saved.html">Your saved spots →</a>
        <button className="acc-leave" onClick={() => { window.LFGAccount.leave(); onClose(); }}>Leave the list</button>
      </div>
    </div>
  );
}

/* the nav entry: dead Log-in / Sign-up replaced with one member-aware control */
function AccountControl() {
  const member = useMember();
  const [open, setOpen] = useState(false);

  useEffect(function () {
    const openH = function () { setOpen(true); };
    window.addEventListener("lfg-account-open", openH);
    return function () { window.removeEventListener("lfg-account-open", openH); };
  }, []);
  useEffect(function () {
    if (!open) return;
    const onKey = function (e) { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("keydown", onKey);
    return function () { document.removeEventListener("keydown", onKey); };
  }, [open]);

  const initial = member && member.email ? member.email.trim().charAt(0).toUpperCase() : "";

  return (
    <span className="account">
      {member ? (
        <button className="nav-member ok"
          onClick={() => setOpen(!open)} aria-label="Your list" title="You're on the list">
          <span className="nav-member-av" aria-hidden="true">{initial || "\u2713"}</span>
          <span className="nav-member-lbl">On the list</span>
        </button>
      ) : (
        <button className="btn" onClick={() => setOpen(true)}>Join free</button>
      )}

      {open && ReactDOM.createPortal(
        <React.Fragment>
          <div className="account-scrim" onClick={() => setOpen(false)}></div>
          <div className="account-pop" role="dialog" aria-label="Your list">
            <button className="account-x" aria-label="Close" onClick={() => setOpen(false)}>{"\u00d7"}</button>
            {member ? <MemberView member={member} onClose={() => setOpen(false)} /> : <JoinView />}
          </div>
        </React.Fragment>,
        document.body
      )}
    </span>
  );
}

/* ---- Top navigation: two doors only ---- */
const NAV = [
  { label: "Explore", href: "Home.html" },
  { label: "Free A–Z", href: "Free Spots.html" },
  { label: "Guides", href: "Guides.html" },
  { label: "Day Trips", href: "Day Trips.html" },
  { label: "First Time", href: "First Time.html" },
  { label: "Calendar", href: "Calendar.html" },
  { label: "Partner with us", href: "Work with us.html", accent: true }
];
/* which nav link is "active" for the current file — detail pages map back to
   their section (spot pages to Free A–Z, guide pages to Guides) */
function navActiveHref() {
  let f = "";
  try { f = decodeURIComponent((location.pathname.split("/").pop() || "")).toLowerCase(); } catch (e) {}
  if (!f || f === "home.html" || f === "index.html") return "Home.html";
  if (f === "spot.html" || f.indexOf("spot-") === 0) return "Free Spots.html";
  if (f === "guide.html" || f === "guides.html" || f.indexOf("guide-") === 0) return "Guides.html";
  const map = {
    "free spots.html": "Free Spots.html",
    "day trips.html": "Day Trips.html",
    "first time.html": "First Time.html",
    "calendar.html": "Calendar.html",
    "work with us.html": "Work with us.html"
  };
  return map[f] || null;
}
function Nav() {
  const [open, setOpen] = useState(false);
  const member = useMember();
  const active = navActiveHref();
  const navClass = (n) => {
    const here = n.href === active;
    return [n.accent ? "nav-studio" : "", here ? "is-here" : ""].filter(Boolean).join(" ") || undefined;
  };
  const navAria = (n) => (n.href === active ? "page" : undefined);
  return (
    <header className="nav">
      <div className="wrap nav-inner">
        <a href="Home.html" aria-label="London Free Guide home"><Logo /></a>
        <span className="nav-eye" role="img" aria-label="evil eye" title="warding off the tourist tax" style={{ fontSize: "1.15rem", marginLeft: "8px", lineHeight: 1 }}>🧿</span>
        <nav className="nav-links">
          {NAV.map((n) => <a key={n.label} href={n.href} className={navClass(n)} aria-current={navAria(n)}>{n.label}</a>)}
        </nav>
        <span className="nav-spacer"></span>
        <SavedNavButton />
        <ThemeToggle />
        <AccountControl />
        <button className="nav-burger" aria-label="Menu" onClick={() => setOpen(true)}>
          <i></i><i></i><i></i>
        </button>
      </div>
      <nav className="nav-scroll" aria-label="Sections">
        {NAV.map((n) => <a key={n.label} href={n.href} className={navClass(n)} aria-current={navAria(n)}>{n.label}</a>)}
      </nav>
      <div className={"drawer" + (open ? " open" : "")}>
        <div className="drawer-bg" onClick={() => setOpen(false)}></div>
        <div className="drawer-panel">
          <button className="drawer-close" aria-label="Close" onClick={() => setOpen(false)}>×</button>
          {NAV.map((n) => <a key={n.label} href={n.href} className={navClass(n)} aria-current={navAria(n)} onClick={() => setOpen(false)}>{n.label}</a>)}
          <button className="drawer-acc" onClick={() => { setOpen(false); window.dispatchEvent(new CustomEvent("lfg-account-open")); }}>{member ? "Your list" : "Join free"}</button>
        </div>
      </div>
    </header>
  );
}

/* a rotating "London rip-off index" — semi-static but changes each visit so it never goes stale */
const RIPOFF = [
  { e: "🍺", t: "£6.80 a pint" },
  { e: "☕", t: "£3.60 a flat white" },
  { e: "🚇", t: "£2.80 a single tube" },
  { e: "🍸", t: "£14 a cocktail" },
  { e: "🎭", t: "£85 a West End seat" },
  { e: "🥐", t: "£4.20 a croissant" },
  { e: "🎡", t: "£40 on the London Eye" },
  { e: "🅿️", t: "£8 an hour to park" }
];
const RIP = RIPOFF[Math.floor(Math.random() * RIPOFF.length)];

/* ---- week's weather strip — LIVE 3-day London forecast via window.LFGWeather
   (Open-Meteo, free, no key, cached 1h); the static WEATHER array below is only
   the offline fallback if the fetch fails ---- */
const WEATHER = [
  { d: "Today", ic: "⛅", t: 19 },
  { d: "Tue", ic: "🌦️", t: 18 },
  { d: "Wed", ic: "☁️", t: 17 },
  { d: "Thu", ic: "☀️", t: 21 },
  { d: "Fri", ic: "🌧️", t: 16 },
  { d: "Sat", ic: "⛅", t: 20 },
  { d: "Sun", ic: "☀️", t: 22 }
];
function Weather({ leftMode = "count", freeCount = 0 }) {
  const today = new Date().toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" });
  const [days, setDays] = useState(WEATHER.slice(0, 3));
  const [tube, setTube] = useState(null);
  useEffect(() => {
    if (!window.LFGWeather) return;
    let live = true;
    window.LFGWeather.london().then((d) => {
      if (!live || !d || !d.length) return;
      setDays(d.map((x, i) => ({
        d: i === 0 ? "Today" : new Date(x.date).toLocaleDateString("en-GB", { weekday: "short" }),
        ic: window.LFGWeather.icon(x.code),
        t: x.t
      })));
    }).catch(() => {});
    return () => { live = false; };
  }, []);
  useEffect(() => {
    if (!window.LFGTube) return;
    let live = true;
    window.LFGTube.status().then((s) => { if (live) setTube(s); }).catch(() => {});
    return () => { live = false; };
  }, []);
  function scrollToFeed(e) {
    e.preventDefault();
    const el = document.querySelector(".section");
    if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.pageYOffset - 64, behavior: "smooth" });
  }
  let left = null;
  if (leftMode === "count") {
    left = <a className="weather-anchor link" href="#explore" onClick={scrollToFeed}>{freeCount} free things on today <span aria-hidden="true">→</span></a>;
  } else if (leftMode === "date") {
    left = <span className="weather-anchor"><span className="wx-pin" aria-hidden="true">📍</span> {today}</span>;
  }
  const tubeNode = (tube && window.NowBar) ? window.NowBar.tube(tube) : null;
  return (
    <div className={"weatherbar wb-" + leftMode}>
      <div className="wrap">
        <div className="weather-inner">
          {left}
          <div className="weather-mid">
            <span className="wx-loc">London</span>
            <div className="week">
              {days.map((w) => (
                <div className="wday" key={w.d}>
                  <span className="wd">{w.d}</span>
                  <span className="wi" aria-hidden="true">{w.ic}</span>
                  <span className="wt">{w.t}°</span>
                </div>
              ))}
            </div>
          </div>
          {tubeNode && <span className="wx-tube">{tubeNode}</span>}
          <span className="pint-chip" title="What London charges. Everything on this site is free.">{RIP.e} {RIP.t}<span className="pint-aside"> · this lot's free</span></span>
        </div>
      </div>
    </div>
  );
}

/* ---- badges shown over a post image ---- */
function CardBadges({ item }) {
  return (
    <div className="badges">
      {item.kind === "guide" && <span className="badge guide">Guide</span>}
      {item.kind === "weekend" && <span className="badge weekend">This weekend</span>}
      {item.kind === "news" && <span className="badge news">Heads-up</span>}
      {item.secret && <span className="badge ghost">Hidden gem</span>}
      {item.inbloom && <span className="badge bloom">🌸 In bloom</span>}
    </div>
  );
}

/* ---- the post image itself (whole, or top-cropped for carousel slides) ---- */
function PostImage({ item }) {
  if (!item.img) {
    return (
      <div className="post-img noimg">
        <div className="noimg-inner">
          <span className="noimg-free">Free</span>
          <span className="noimg-title">{item.title}</span>
          <span className="noimg-mark">LONDON <span className="r">FREE</span> GUIDE</span>
        </div>
        <CardBadges item={item} />
        <SaveButton item={item} />
      </div>
    );
  }
  return (
    <div className={"post-img" + (item.cropTop ? " crop-top" : "") + (item.posterCrop ? " crop-poster" : "")}>
      <img src={item.img} alt={item.title} loading="lazy" />
      <CardBadges item={item} />
      <SaveButton item={item} />
      {item.reel && <span className="reel-mark" aria-label="Has a reel">▶</span>}
    </div>
  );
}

/* ---- a feed card: image+title link, with a separate Maps link in the footer ---- */
/* ---- date helpers (shared) ---- */
function lfgToday() { const d = new Date(); d.setHours(0, 0, 0, 0); return d; }
function lfgDate(s) { if (!s) return null; const p = String(s).split("-").map(Number); return new Date(p[0], p[1] - 1, p[2]); }
/* seasonal items (Chelsea/Belgravia in Bloom, tulips) carry a {from,to} MM-DD
   window — in season any year, out of season they read as "past" so the same
   hide-when-past plumbing tucks them away until they come round again */
function lfgInSeason(item) {
  if (!item || !item.season) return true;
  const t = lfgToday(), y = t.getFullYear();
  const mk = (s) => { const p = String(s).split("-").map(Number); return new Date(y, p[0] - 1, p[1]); };
  const a = mk(item.season.from), b = mk(item.season.to);
  return b < a ? (t >= a || t <= b) : (t >= a && t <= b);   // b<a wraps year-end
}
function lfgIsPast(item) {
  if (item && item.season) return !lfgInSeason(item);
  return !!(item && item.end && lfgDate(item.end) < lfgToday());
}
function lfgShuffle(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const t = a[i]; a[i] = a[j]; a[j] = t; } return a; }

function Card({ item }) {
  const maps = "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(item.title + " " + item.area + " London");
  const detailHref = item.noDetail ? (item.website || maps) : (item.kind === "guide" ? "Guide.html?id=" : "Spot.html?id=") + item.id;
  const ext = item.noDetail ? { target: "_blank", rel: "noopener noreferrer" } : {};
  const past = lfgIsPast(item);
  // some items (e.g. the Curse Cleansing pop-up) carry their own slide deck and
  // open an in-page story carousel instead of navigating to a detail page
  const story = item.slides && item.slides.length ? item.slides : null;
  const openStory = (e) => { e.preventDefault(); window.dispatchEvent(new CustomEvent("lfg-open-story", { detail: { slides: story, i: 0 } })); };
  return (
    <div className={"card" + (past ? " is-past" : "")} data-kind={item.kind}>
      {past ? <span className="card-ended">Ended</span> : null}
      <a href={story ? "#" : detailHref} className="card-link" {...(story ? { onClick: openStory } : ext)}>
        <PostImage item={item} />
        <div className="cap">
          <div className="cap-row">
            <span className={"cost " + item.cost}>{COST[item.cost]}</span>
            {item.date && <span className="cap-date">{item.date}</span>}
          </div>
          <div className="cap-title">{item.title}</div>
          <div className="cap-summary">{item.summary}</div>
          {item.hours && <div className="cap-hours"><span aria-hidden="true">🕐</span>{item.hours}</div>}
        </div>
      </a>
      <a className="maps-row" href={maps} target="_blank" rel="noopener noreferrer" aria-label={"Open " + item.title + " in Google Maps"}>
        <span className="cap-loc"><span className="pin" aria-hidden="true">📍</span><span className="cap-loc-txt">{item.area} · {item.tube}</span></span>
        <span className="maps-go">Google Maps <ExtIcon /></span>
      </a>
    </div>
  );
}

/* ---- featured rail (horizontal scroll-snap of this week's picks) ---- */
function Rail({ items }) {
  // only current picks — drop anything whose run has already ended
  const featured = items.filter((i) => i.featured && !lfgIsPast(i));
  if (!featured.length) return null;
  return (
    <section className="rail-sec">
      <div className="wrap">
        <span className="eyebrow">what's free in London right now</span>
        <div className="rail-head">
          <h1>This week in London</h1>
          <span className="rail-sub">updated Thursdays</span>
        </div>
      </div>
      <div className="wrap">
        <div className="rail" role="list">
          {featured.map((i) => {
            const story = i.slides && i.slides.length ? i.slides : null;
            const href = story ? "#" : (i.kind === "guide" ? "Guide.html?id=" : "Spot.html?id=") + i.id;
            return (
              <a key={i.id} href={href} className="rail-card" role="listitem"
                onClick={story ? (e) => { e.preventDefault(); window.dispatchEvent(new CustomEvent("lfg-open-story", { detail: { slides: story, i: 0 } })); } : undefined}>
                <PostImage item={i} />
              </a>
            );
          })}
        </div>
      </div>
    </section>
  );
}

/* ---- "get out of London" day-trips strip (home) ---- */
function DayTripsStrip({ items, photo }) {
  // always eight, reshuffled on each page load: photo'd trips first, then
  // fill the remaining slots with as-yet-unphotographed trips (black cards)
  const trips = useMemo(() => {
    const live = (items || []).filter((i) => i.daytrip && !lfgIsPast(i));
    const withPhoto = lfgShuffle(live.filter((i) => i.bg));
    const without = lfgShuffle(live.filter((i) => !i.bg));
    return withPhoto.concat(without).slice(0, 8);
  }, []);
  if (!trips.length) return null;
  return (
    <section className="dtstrip-sec">
      <div className="wrap dtstrip-head">
        <div>
          <span className="eyebrow">free days out</span>
          <h2 className="dtstrip-title">Get out of <span className="r">London</span></h2>
        </div>
        <a className="dtstrip-all" href="Day Trips.html">All day trips <span aria-hidden="true">→</span></a>
      </div>
      <div className="wrap">
        <div className={"dtstrip" + (photo ? " photo" : "")} role="list">
          {trips.map((t) => {
            const place = (t.area || t.title).split("·")[0].trim();
            const href = (t.kind === "guide" ? "Guide.html?id=" : "Spot.html?id=") + t.id;
            const f = t.fromLondon || {};
            const fareM = f.fare && f.fare.match(/£\d+/);
            const fare = fareM ? "from " + fareM[0] : "";
            const hasPhoto = photo && t.bg;
            return (
              <a className={"dtcard" + (hasPhoto ? " has-photo" : "")} href={href} key={t.id} role="listitem">
                {hasPhoto ? <span className="dtcard-bg" style={{ backgroundImage: "url(" + t.bg + ")" }} aria-hidden="true"></span> : null}
                <span className="dtcard-region">{t.region}</span>
                <span className="dtcard-place">{place}</span>
                <span className="dtcard-line">{t.summary}</span>
                <span className="dtcard-transit">{f.time}{fare ? " · " + fare : ""}</span>
                <span className="dtcard-cta">Free day out <span aria-hidden="true">→</span></span>
              </a>
            );
          })}
        </div>
      </div>
    </section>
  );
}

/* ---- "free right now" giveaways strip (home) — live, time-sensitive freebies ---- */
function FreeRightNow() {
  const content = window.LFG_CONTENT || [];
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const parse = (s) => { const p = String(s).split("-").map(Number); return new Date(p[0], p[1] - 1, p[2]); };
  // genuine freebies whose window has not ended yet — auto-expires once it passes
  const live = content
    .filter((i) => i.freebie && i.end && parse(i.end) >= today)
    .sort((a, b) => parse(a.start) - parse(b.start));
  if (!live.length) return null;

  const MS = 86400000;
  function status(i) {
    const s = parse(i.start), e = parse(i.end);
    if (today >= s && today <= e) return { label: "Live now", live: true };
    const days = Math.round((s - today) / MS);
    if (days <= 0) return { label: "Today" };
    if (days === 1) return { label: "Tomorrow" };
    return { label: s.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }) };
  }

  return (
    <section className="frn-sec">
      <div className="wrap frn-head">
        <div>
          <span className="eyebrow">grab it before it's gone</span>
          <h2 className="frn-title">Free <span className="r">right now</span></h2>
        </div>
        <a className="frn-all" href="Guide.html?id=june-popups">All the pop-ups <span aria-hidden="true">→</span></a>
      </div>
      <div className="wrap">
        <div className="frn" role="list">
          {live.map((i) => {
            const st = status(i);
            return (
              <a className="frn-card" href={"Spot.html?id=" + i.id} key={i.id} role="listitem">
                <span className={"frn-status" + (st.live ? " live" : "")}>
                  {st.live ? <span className="frn-dot" aria-hidden="true"></span> : null}
                  {st.label}
                </span>
                <span className="frn-name">{i.title}</span>
                <span className="frn-when">{i.hours || i.date}</span>
                <span className="frn-loc"><span className="pin" aria-hidden="true">📍</span> {i.area}</span>
                <span className="frn-cta">Grab it <span aria-hidden="true">→</span></span>
              </a>
            );
          })}
        </div>
      </div>
    </section>
  );
}

/* ---- "on now this month" strip (home) — pulls from the free calendar ---- */
function OnNow() {
  const cal = window.LFG_CALENDAR;
  if (!cal || !cal.months) return null;
  // auto-rolls by month; in the last week of a month it looks ahead to the next
  // one so the strip never sits on a fully-passed month
  const now = new Date();
  let idx = now.getMonth();
  const daysInMonth = new Date(now.getFullYear(), idx + 1, 0).getDate();
  const lookahead = now.getDate() > daysInMonth - 7;
  if (lookahead) idx = (idx + 1) % 12;
  const m = cal.months[idx];
  if (!m || !m.events || !m.events.length) return null;
  let year = now.getFullYear();
  if (lookahead && now.getMonth() === 11) year += 1;   // rolled Dec → Jan
  const href = "Calendar.html#cal-" + m.key;
  return (
    <section className="onnow-sec">
      <div className="wrap onnow-head">
        <div>
          <span className="eyebrow">{lookahead ? "coming up" : "free this month"}</span>
          <h2 className="onnow-title">{lookahead ? "Next up in " : "On now in "}<span className="r">{m.name}</span></h2>
        </div>
        <a className="onnow-all" href={href}>Full calendar <span aria-hidden="true">→</span></a>
      </div>
      <div className="wrap">
        <div className="onnow" role="list">
          {m.events.map((e) => (
            <a className="onnow-card" href={e.link || href} key={e.name} role="listitem">
              <span className="onnow-when">{e.when} {year}</span>
              <span className="onnow-name">{e.name}</span>
              <span className="onnow-area"><span className="pin" aria-hidden="true">📍</span> {e.area}</span>
            </a>
          ))}
        </div>
      </div>
    </section>
  );
}

/* ---- search + filter controls + masonry feed ---- */
/* adapt a directory spot into the shape <Card> expects (used by the grouped
   Explore feed; curated content items win over these where ids match) */
function dirSpotToCard(s) {
  return { id: s.id, kind: "spot", title: s.name, summary: s.desc, area: s.area, tube: s.tube, cost: "free", tags: [s.cat], noDetail: !s.pillar, website: s.website, pillar: s.pillar, dir: true, img: s.img };
}
function Feed({ items }) {
  const [q, setQ] = useState("");
  const [active, setActive] = useState(new Set());
  const [weekendOnly, setWeekendOnly] = useState(false);
  const [cheapOnly, setCheapOnly] = useState(false);
  const [showPast, setShowPast] = useState(false);
  const PAGE = 9;
  const [visible, setVisible] = useState(PAGE);

  function toggleTag(key) {
    const next = new Set(active);
    next.has(key) ? next.delete(key) : next.add(key);
    setActive(next);
  }

  function clearAll() {
    setQ(""); setActive(new Set()); setWeekendOnly(false); setCheapOnly(false); setShowPast(false);
  }
  const anyActive = q || active.size || weekendOnly || cheapOnly || showPast;

  // No query and no filters -> browse-by-category (mirrors Free A–Z): shuffled
  // category sections, 6 cards each, curated photo cards winning over the
  // text-forward directory entries. Search/filter switches back to the flat feed.
  const grouped = !q.trim() && !active.size && !weekendOnly && !cheapOnly && !showPast;
  const DIRCATS = (window.LFG_DIRECTORY || {}).categories || [];
  const DIRSPOTS = (window.LFG_DIRECTORY || {}).spots || [];
  const curatedById = useMemo(() => { const m = {}; (items || []).forEach((it) => { if (it && it.id) m[it.id] = it; }); return m; }, [items]);
  const catGroups = useMemo(() => lfgShuffle(DIRCATS).map((c) => ({ cat: c, spots: lfgShuffle(DIRSPOTS.filter((s) => s.cat === c.key)).slice(0, 6) })).filter((g) => g.spots.length), []);

  // The full searchable library — same content the Free A–Z reaches.
  // Curated feed first (rich cards), then the 15 pillars, then the evergreen
  // directory (298 places, text-forward). Deduped by id. A free-text search
  // spans ALL of this; with no query the page stays the curated feed.
  const fullPool = useMemo(() => {
    const byId = {};
    const add = (it) => { if (it && it.id && !byId[it.id]) byId[it.id] = it; };
    (items || []).forEach(add);
    (window.LFG_PILLARS || []).forEach(add);
    (((window.LFG_DIRECTORY || {}).spots) || []).forEach((s) => add({
      id: s.id, kind: "spot", title: s.name, summary: s.desc,
      area: s.area, tube: s.tube, cost: "free", tags: [s.cat],
      noDetail: !s.pillar, website: s.website, pillar: s.pillar, dir: true,
    }));
    return Object.values(byId);
  }, [items]);

  const shown = useMemo(() => {
    const needle = q.trim().toLowerCase();
    // A typed query searches the entire library (feed + pillars + A–Z directory).
    if (needle) {
      return fullPool.filter((i) => {
        // seasonal/recurring items (blooms) stay searchable year-round even out
        // of season; only genuinely-expired one-offs drop out of search results
        if (!showPast && lfgIsPast(i) && !i.season && !i.recurring) return false;
        const hay = (i.title + " " + (i.summary || "") + " " + (i.area || "") + " " + (i.tube || "") + " " + (i.tags || []).join(" ")).toLowerCase();
        return hay.includes(needle);
      });
    }
    // No query: the curated feed, with the category + cost + weekend filters.
    const base = items.filter((i) => {
      if (!showPast && lfgIsPast(i)) return false;
      if (active.size && !(i.tags || []).some((t) => active.has(t))) return false;
      if (weekendOnly && !i.weekend) return false;
      if (cheapOnly && i.cost === "ticketed") return false;
      return true;
    });
    // "Kids friendly" is a cross-cutting pill: most genuinely kid-friendly
    // places live in the A–Z directory (flagged `kids`), not the curated feed.
    // Surface them too so the pill returns a full feed, not just the two guides.
    if (active.has("kids") && !weekendOnly) {
      const have = new Set(base.map((i) => i.id));
      lfgShuffle(DIRSPOTS.filter((s) => s.kids && !have.has(s.id)))
        .forEach((s) => { base.push(curatedById[s.id] || dirSpotToCard(s)); have.add(s.id); });
    }
    return base;
  }, [items, fullPool, q, active, weekendOnly, cheapOnly, showPast]);

  useEffect(() => { setVisible(PAGE); }, [q, active, weekendOnly, cheapOnly, showPast]);

  return (
    <section className="section">
      <div className="wrap">
        <div className="explore-head">
          <h2>Explore</h2>
          <span className="explore-sub">{grouped ? (DIRSPOTS.length + " free things in London, by category") : (shown.length + " free & cheap things in London")}</span>
          {grouped && <a className="explore-browse-all" href="Free Spots.html">Browse all {DIRSPOTS.length} by category <span aria-hidden="true">→</span></a>}
        </div>

        <div className="search">
          <span className="mag" aria-hidden="true">⌕</span>
          <input
            type="search"
            value={q}
            onChange={(e) => setQ(e.target.value)}
            placeholder="search a spot, area or vibe. Try “rooftop” or “Margate”"
            aria-label="Search"
          />
        </div>

        <div className="filters" role="group" aria-label="Filters">
          {FILTERS.map((f) => (
            <button
              key={f.key}
              className={"pill" + (active.has(f.key) ? " on" : "")}
              aria-pressed={active.has(f.key)}
              onClick={() => toggleTag(f.key)}
            >{f.label}</button>
          ))}
          <span className="filter-div" aria-hidden="true"></span>
          <button className={"pill toggle" + (weekendOnly ? " on" : "")} aria-pressed={weekendOnly} onClick={() => setWeekendOnly(!weekendOnly)}>This weekend</button>
          <button className={"pill toggle" + (cheapOnly ? " on" : "")} aria-pressed={cheapOnly} onClick={() => setCheapOnly(!cheapOnly)}>Under £10</button>
          <button className={"pill toggle" + (showPast ? " on" : "")} aria-pressed={showPast} onClick={() => setShowPast(!showPast)}>Past events</button>
          {anyActive ? <button className="pill clear" onClick={clearAll}>Clear ✕</button> : null}
        </div>
      </div>

      <div className="wrap">
        {grouped ? (
          catGroups.map((g) => (
            <section className="feed-cat" id={"feedcat-" + g.cat.key} key={g.cat.key}>
              <div className="feed-cat-head">
                <h3 className="feed-cat-h"><a className="feed-cat-link" href={"Free Spots.html#cat-" + g.cat.key}>{g.cat.label}</a><span className="feed-cat-n">{g.cat.count} free</span></h3>
                <a className="feed-cat-all" href={"Free Spots.html#cat-" + g.cat.key}>See all {g.cat.count} <span aria-hidden="true">→</span></a>
              </div>
              <div className="masonry">
                {g.spots.map((s) => <Card key={s.id} item={curatedById[s.id] || dirSpotToCard(s)} />)}
              </div>
            </section>
          ))
        ) : shown.length ? (
          <React.Fragment>
            <div className="masonry">
              {shown.slice(0, visible).map((i) => <Card key={i.id} item={i} />)}
            </div>
            {shown.length > visible ? (
              <div className="loadmore-wrap">
                <button className="loadmore" onClick={() => setVisible(visible + PAGE)}>Load more</button>
                <span className="loadmore-count">showing {visible} of {shown.length}</span>
              </div>
            ) : null}
          </React.Fragment>
        ) : (
          <div className="feed-empty">No matches. Try clearing a filter or searching something broader.</div>
        )}
      </div>
    </section>
  );
}

/* ---- Newsletter + follow-everywhere band ---- */
/* Monochrome glyphs (simple-icons paths) — rendered in the brand white/red, never the platforms' own colours, so the strip stays on-brand. */
const SOC_PATHS = {
  Instagram: "M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z",
  TikTok: "M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z",
  Substack: "M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z",
  X: "M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932 6.59-6.933zm-1.29 19.49h2.039L6.486 3.24H4.298l13.313 17.403z",
  Threads: "M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.781 3.631 2.695 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.74-1.756-.5-.583-1.274-.881-2.3-.887h-.027c-.824 0-1.943.227-2.656 1.289L7.34 8.182c.952-1.418 2.5-2.198 4.368-2.198h.04c3.13.02 4.586 1.925 4.756 5.336.097.041.193.084.288.129 1.34.629 2.32 1.58 2.834 2.751.715 1.631.756 4.286-1.405 6.442-1.652 1.648-3.657 2.39-6.484 2.41h-.001z",
  Facebook: "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z",
};
function SocIcon({ name }) {
  return (
    <svg className="soc-ico" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" focusable="false">
      <path fill="currentColor" d={SOC_PATHS[name]} />
    </svg>
  );
}
const PLATFORMS = [
  { name: "Instagram", url: "https://www.instagram.com/londonfreeguide/" },
  { name: "TikTok", url: "https://www.tiktok.com/@londonfreeguide" },
  { name: "Substack", url: "https://substack.com/@londonfreeguide" },
  { name: "X", url: "https://x.com/londonfreeguide" },
  { name: "Threads", url: "https://www.threads.com/@londonfreeguide" },
  { name: "Facebook", url: "https://www.facebook.com/LondonFreeGuide" },
];
/* the newsletter signup — the primary email-capture surface. Real states:
   idle → sending → done (Buttondown confirmation on its way) or inline error.
   Wired to the single integration point in account.js (window.LFGNewsletter). */
function NewsletterForm() {
  const [email, setEmail] = useState("");
  const [state, setState] = useState("idle"); // idle | sending | done | error
  function submit(e) {
    e.preventDefault();
    const v = email.trim();
    if (!v || v.indexOf("@") < 1) { setState("error"); return; }
    setState("sending");
    Promise.resolve(window.LFGNewsletter ? window.LFGNewsletter.subscribe(v) : { ok: false, reason: "invalid" })
      .then(function (r) {
        if (r && (r.ok || r.reason === "not-configured")) setState("done");
        else setState("error");
      });
  }
  if (state === "done") {
    return (
      <div className="nl-done" role="status">
        <span className="nl-done-tick" aria-hidden="true">{"\u2713"}</span>
        <div className="nl-done-txt">
          <b>Almost there — check your inbox.</b>
          <span>We've sent a confirmation to {email}. Tap it and you're on the list.</span>
        </div>
      </div>
    );
  }
  return (
    <React.Fragment>
      <form className="nl-form" onSubmit={submit} noValidate>
        <input type="email" value={email} placeholder="your email" aria-label="Email address"
          aria-invalid={state === "error"}
          onChange={(e) => { setEmail(e.target.value); if (state === "error") setState("idle"); }} />
        <button className="btn" type="submit" disabled={state === "sending"}>
          {state === "sending" ? "Adding\u2026" : "Subscribe"}
        </button>
      </form>
      {state === "error"
        ? <div className="nl-note nl-err">That email doesn't look right — mind giving it another go?</div>
        : <div className="nl-note">No spam. One tap to leave. We never sell your email.</div>}
    </React.Fragment>
  );
}
function Band() {
  return (
    <section className="band">
      <div className="wrap band-inner">
        <div>
          <h2>Get it in your inbox</h2>
          <p>One email a week. The best free things to do, before everyone else finds them.</p>
          <NewsletterForm />
        </div>
        <div>
          <div className="socials-label">Follow everywhere, same content</div>
          <div className="socials">
            {PLATFORMS.map((p) => (
              <a key={p.name} href={p.url} className="soc" target="_blank" rel="noopener noreferrer" aria-label={"Follow London Free Guide on " + p.name}>
                <SocIcon name={p.name} />{p.name}
              </a>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

/* ---- Footer ---- */
function Footer() {
  return (
    <footer className="foot">
      <div className="wrap foot-inner">
        <div>
          <Logo />
          <div className="foot-copy">A London guide for people who hate overpaying.<br />© 2026 London Free Guide · @londonfreeguide</div>
        </div>
        <div className="foot-links">
          <a href="Home.html">Explore</a>
          <a href="First Time.html">First Time</a>
          <a href="Free Spots.html">Free A–Z</a>
          <a href="Calendar.html">Calendar</a>
          <a href="Guides.html">Guides</a>
          <a href="Day Trips.html">Day Trips</a>
          <a href="Saved.html">Saved</a>
          <a href="Support.html">Support</a>
          <a href="Work with us.html">Partner with us</a>
          <a href="About.html">About</a>
          <a href="Privacy.html">Privacy</a>
          <a href="Terms.html">Terms</a>
          <a href="Work with us.html#featured">Media kit</a>
        </div>
        {((window.LFG_DIRECTORY || {}).categories || []).length > 0 && (
          <div className="foot-cats">
            <span className="foot-cats-h">Browse free by category</span>
            <div className="foot-cats-links">
              {((window.LFG_DIRECTORY || {}).categories || []).map((c) => (
                <a key={c.key} href={"Free Spots.html#cat-" + c.key}>{c.label}</a>
              ))}
            </div>
          </div>
        )}
      </div>
    </footer>
  );
}

/* ---- cheeky cookie consent (self-mounts on every page; persisted) ---- */
function CookieBar() {
  const [show, setShow] = useState(false);
  useEffect(() => {
    try { if (!localStorage.getItem("lfg-cookies")) { setShow(true); document.body.classList.add("cookie-open"); } } catch (e) {}
  }, []);
  function choose(v) {
    try { localStorage.setItem("lfg-cookies", v); } catch (e) {}
    document.body.classList.remove("cookie-open");
    setShow(false);
  }
  if (!show) return null;
  return (
    <div className="cookiebar" role="dialog" aria-label="Cookie notice">
      <p className="cookie-txt">
        <span className="cookie-ico" aria-hidden="true">🍪</span>
        We use a couple of cookies. No ad-tracking nonsense, no flogging your data, just enough to remember your theme and the spots you save. Go on, it'd be rude not to.
      </p>
      <div className="cookie-btns">
        <button className="btn cookie-yes" onClick={() => choose("all")}>Go on then</button>
        <button className="cookie-min" onClick={() => choose("essential")}>Essentials only</button>
      </div>
    </div>
  );
}

/* ---- launch announcement strip (self-mounts at the very top of every page; persisted) ---- */
function LaunchBar() {
  const [show, setShow] = useState(false);
  useEffect(() => {
    try { if (localStorage.getItem("lfg-launch-note") !== "dismissed") setShow(true); } catch (e) { setShow(true); }
  }, []);
  function dismiss() {
    try { localStorage.setItem("lfg-launch-note", "dismissed"); } catch (e) {}
    setShow(false);
  }
  if (!show) return null;
  return (
    <div className="lfg-launchbar" role="region" aria-label="Announcement">
      <p className="lfg-launchbar-txt">We just launched and are still finding our feet. If something looks half baked, please bear with us</p>
      <button className="lfg-launchbar-x" onClick={dismiss} aria-label="Dismiss announcement">×</button>
    </div>
  );
}

/* ---- Global story viewer: a single lightbox that ANY page mounts, so a card
   carrying its own slide deck (a guide/pop-up/walking tour) opens the full
   swipeable carousel of all its steps no matter which page it sits on. Pages
   trigger it by dispatching window CustomEvent "lfg-open-story" with
   detail:{slides:[{src,alt}], i}. Self-mounts once at the bottom. ---- */
const LFG_STORY_CSS =
  ".lfg-sv{position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.93);display:flex;align-items:center;justify-content:center;padding:24px}" +
  ".lfg-sv-img{max-height:92vh;max-width:min(92vw,520px);border-radius:14px;box-shadow:0 24px 70px rgba(0,0,0,.5)}" +
  ".lfg-sv-x{position:absolute;top:16px;right:18px;width:44px;height:44px;border-radius:50%;border:1px solid rgba(255,255,255,.25);background:rgba(0,0,0,.3);color:#fff;font-size:1.5rem;cursor:pointer;display:flex;align-items:center;justify-content:center}" +
  ".lfg-sv-nav{position:absolute;top:50%;transform:translateY(-50%);width:48px;height:48px;border-radius:50%;border:1px solid rgba(255,255,255,.25);background:rgba(0,0,0,.3);color:#fff;font-size:1.8rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}" +
  ".lfg-sv-nav.prev{left:16px}.lfg-sv-nav.next{right:16px}" +
  ".lfg-sv-nav:disabled{opacity:.25;cursor:default}" +
  ".lfg-sv-count{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,.7);font-size:.78rem;font-weight:600;letter-spacing:.04em}" +
  "@media (max-width:600px){.lfg-sv-nav{width:42px;height:42px}}";
function StoryViewer() {
  const [deck, setDeck] = useState(null);
  const [i, setI] = useState(0);
  useEffect(() => {
    if (!document.getElementById("lfg-story-css")) {
      const s = document.createElement("style"); s.id = "lfg-story-css"; s.textContent = LFG_STORY_CSS; document.head.appendChild(s);
    }
    const onOpen = (e) => { const d = e.detail || {}; if (d.slides && d.slides.length) { setDeck(d.slides); setI(d.i || 0); } };
    window.addEventListener("lfg-open-story", onOpen);
    return () => window.removeEventListener("lfg-open-story", onOpen);
  }, []);
  useEffect(() => {
    if (!deck) return;
    const onKey = (e) => {
      if (e.key === "Escape") setDeck(null);
      else if (e.key === "ArrowRight") setI((x) => Math.min(deck.length - 1, x + 1));
      else if (e.key === "ArrowLeft") setI((x) => Math.max(0, x - 1));
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [deck]);
  if (!deck) return null;
  return (
    <div className="lfg-sv" role="dialog" aria-modal="true" aria-label="Story" onClick={() => setDeck(null)}>
      <button className="lfg-sv-x" aria-label="Close" onClick={() => setDeck(null)}>×</button>
      <button className="lfg-sv-nav prev" aria-label="Previous" disabled={i === 0} onClick={(e) => { e.stopPropagation(); setI((x) => Math.max(0, x - 1)); }}>‹</button>
      <img className="lfg-sv-img" src={deck[i].src} alt={deck[i].alt} onClick={(e) => e.stopPropagation()} />
      <button className="lfg-sv-nav next" aria-label="Next" disabled={i === deck.length - 1} onClick={(e) => { e.stopPropagation(); setI((x) => Math.min(deck.length - 1, x + 1)); }}>›</button>
      <div className="lfg-sv-count">{i + 1} / {deck.length}</div>
    </div>
  );
}

Object.assign(window, { Logo, Nav, Weather, Rail, Feed, Band, Footer, Card, CardBadges, PostImage, ThemeToggle, CookieBar, LaunchBar, StoryViewer, useMember, MagicLink, AccountControl, ExtIcon, OnNow, FreeRightNow });

/* self-mount the launch announcement strip once, at the very TOP of the page (above the sticky nav) */
(function () {
  if (document.getElementById("lfg-launch-root")) return;
  if (!document.getElementById("lfg-launchbar-css")) {
    var s = document.createElement("style");
    s.id = "lfg-launchbar-css";
    s.textContent =
      ".lfg-launchbar{display:flex;align-items:center;gap:12px;justify-content:center;" +
      "background:#1a1a1a;border-bottom:2px solid #E63B2E;padding:8px 44px 8px 16px;position:relative;" +
      "font-family:'Poppins',sans-serif;font-weight:500;font-size:13px;line-height:1.35;" +
      "color:rgba(255,255,255,.92);text-align:center;}" +
      ".lfg-launchbar-txt{margin:0;max-width:760px;}" +
      ".lfg-launchbar-x{position:absolute;right:10px;top:50%;transform:translateY(-50%);" +
      "background:none;border:0;cursor:pointer;padding:4px 8px;line-height:1;" +
      "font-size:18px;color:rgba(255,255,255,.55);transition:color .15s ease;}" +
      ".lfg-launchbar-x:hover{color:rgba(255,255,255,.95);}" +
      "@media (max-width:560px){.lfg-launchbar{font-size:12px;padding:6px 38px 6px 12px;gap:8px;}" +
      ".lfg-launchbar-x{font-size:16px;right:8px;}}";
    document.head.appendChild(s);
  }
  var el = document.createElement("div");
  el.id = "lfg-launch-root";
  document.body.insertBefore(el, document.body.firstChild);
  ReactDOM.createRoot(el).render(<LaunchBar />);
})();

/* self-mount the cookie bar once, independent of each page's app */
(function () {
  if (document.getElementById("lfg-cookie-root")) return;
  var el = document.createElement("div");
  el.id = "lfg-cookie-root";
  document.body.appendChild(el);
  ReactDOM.createRoot(el).render(<CookieBar />);
})();

/* self-mount the global story viewer once, so guide/pop-up cards carrying a
   slide deck open the full carousel on EVERY page, not just the home feed */
(function () {
  if (document.getElementById("lfg-story-root")) return;
  var el = document.createElement("div");
  el.id = "lfg-story-root";
  document.body.appendChild(el);
  ReactDOM.createRoot(el).render(<StoryViewer />);
})();
