/* London Free Guide — home page app. Renders from the single content source. */
const { useEffect } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#E63B2E",
  "columns": "regular",
  "daytripStyle": "photo",
  "roundedCards": true,
  "savePattern": "heart",
  "weatherLeft": "count"
}/*EDITMODE-END*/;

const COL_COUNT = { tight: 2, regular: 2, roomy: 1 };        // mobile
const COL_COUNT_WIDE = { tight: 4, regular: 3, roomy: 2 };   // desktop

/* ---- "This weekend" carousel: the IG story slides, shown full + swipeable,
   with a tap-to-enlarge lightbox (the slides are self-contained, all info baked
   in, so we present them whole rather than re-deriving cards) ---- */
const WEEKEND_SLIDES = [
  { src: "img/weekend-jun21/w01.jpg", alt: "9 free and cheap things to do in London this weekend, 19 to 21 June 2026" },
  { src: "img/weekend-jun21/w02.jpg", alt: "West End Live, free, Sat 20 and Sun 21 June, Trafalgar Square WC2N" },
  { src: "img/weekend-jun21/w03.jpg", alt: "Okinawa Day, free, Sat 20 June 11am to 5pm, The Blue Market Bermondsey SE16" },
  { src: "img/weekend-jun21/w04.jpg", alt: "Gunnersbury Dog Show, free to watch, Sun 21 June 11am to 4pm, Gunnersbury Park W3" },
  { src: "img/weekend-jun21/w05.jpg", alt: "Midsummer Fairy Tale solstice late, 28 pounds, Sat 20 June 6.30pm, British Library NW1" },
  { src: "img/weekend-jun21/w06.jpg", alt: "Teatime Concert, free with tokens on arrival, Sun 21 June 4pm, Royal Opera House WC2E" },
  { src: "img/weekend-jun21/w07.jpg", alt: "Ideas Festival, free, book ahead, Fri 19 and Sat 20 June, Carlton House Terrace SW1" },
  { src: "img/weekend-jun21/w08.jpg", alt: "Flavours and Makers market, free, Sat 20 and Sun 21 June, Strand Aldwych WC2B" },
  { src: "img/weekend-jun21/w09.jpg", alt: "Wayward Plants launch, free entry, Sat 20 June, Cultivate Colindale NW9" },
  { src: "img/weekend-jun21/w10.jpg", alt: "LFA Lego Challenge, free family drop-in, Sat 20 June 12 to 2pm, The London Centre EC2V" },
];
const WEEKEND_EVENTS = [
  { name:"West End Live", iso:"2026-06-20", when:"Sat 20 + Sun 21 Jun", place:"Trafalgar Square, WC2N", tube:"Charing Cross", blurb:"Fifty plus free performances from London's biggest West End musicals on a pop-up stage. Arrive early or watch the relay screen in Victoria Embankment Gardens." },
  { name:"Okinawa Day", iso:"2026-06-20", when:"Sat 20 Jun · 11am–5pm", place:"The Blue Market, Bermondsey SE16", tube:"Bermondsey", blurb:"Ryukyu music, Eisa dance, sanshin folk songs, karate demos and Okinawan street food. There is a free dugong puppet workshop too." },
  { name:"Gunnersbury Dog Show", iso:"2026-06-21", when:"Sun 21 Jun · 11am–4pm", place:"Gunnersbury Park, W3", tube:"Acton Town", blurb:"A gloriously un-serious Father's Day dog show for the park's 100th. Top Dog Dad, Waggiest Tail and Rescue Story categories. £5 to enter your own pup." },
  { name:"Midsummer Fairy Tale", iso:"2026-06-20", when:"Sat 20 Jun · 6.30pm", place:"British Library, NW1", tube:"King's Cross St Pancras", free:false, blurb:"An after-hours solstice party with Mermaid Chunky electronica, a soundsystem maypole, a panpipe orchestra, a fairytale hatwalk and late access to the Fairy Tales show." },
  { name:"Teatime Concert", iso:"2026-06-21", when:"Sun 21 Jun · 4pm", place:"Royal Opera House, WC2E", tube:"Covent Garden", blurb:"A free chamber concert by Royal Opera House orchestra players in the glass Paul Hamlyn Hall. Mozart, Holst and Stravinsky. Grab an entry token on arrival." },
  { name:"Ideas Festival", iso:"2026-06-19", when:"Fri 19 + Sat 20 Jun", place:"Carlton House Terrace, SW1", tube:"Piccadilly Circus", blurb:"Two days of free talks, debates and hands-on exhibits on history, politics, culture and tech, in a grand townhouse on The Mall. Advance booking needed." },
  { name:"Flavours & Makers", iso:"2026-06-20", when:"Sat 20 + Sun 21 Jun", place:"Strand Aldwych, WC2B", tube:"Temple", blurb:"A global food and makers' market on the newly pedestrianised Strand, right next to West End Live." },
  { name:"Wayward Plants launch", iso:"2026-06-20", when:"Sat 20 Jun", place:"Cultivate Colindale, NW9", tube:"Colindale", blurb:"Adopt rescued RHS Chelsea Flower Show plants at this new circular-economy garden hub. Free to visit." },
  { name:"LFA Lego Challenge", iso:"2026-06-20", when:"Sat 20 Jun · 12–2pm", place:"The London Centre, EC2V", tube:"St Paul's", blurb:"Architects team up with kids aged 6 to 12 to build sustainable homes in Lego. Drop in, free." },
];

const WS_CSS = `
.ws-sec{padding:26px 0 30px;background:linear-gradient(180deg,#181818 0%,#131313 100%);border-bottom:1px solid var(--lfg-white-08)}
[data-theme="light"] .ws-sec{background:linear-gradient(180deg,#efe8dc 0%,#f4f0ea 100%)}
.ws-head{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:15px}
.ws-title{font-family:var(--lfg-font-head);font-weight:700;text-transform:uppercase;font-size:clamp(1.6rem,4vw,2.4rem);line-height:.95}
.ws-all{font-weight:700;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:var(--lfg-accent);white-space:nowrap}
.ws-all:hover{color:var(--lfg-white)}
.ws-rail{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding:2px 0 8px}
.ws-rail::-webkit-scrollbar{display:none}
.ws-card{flex:0 0 auto;scroll-snap-align:start;width:clamp(228px,62vw,290px);border:0;background:transparent;padding:0;cursor:pointer;text-align:left;display:flex;flex-direction:column;gap:9px;transition:transform .15s}
.ws-card:hover{transform:translateY(-3px)}
.ws-card-img{display:block;width:100%;aspect-ratio:9/16;border-radius:14px;overflow:hidden;background:var(--lfg-white-08);box-shadow:0 10px 30px rgba(0,0,0,.28)}
.ws-card-img img{width:100%;height:100%;object-fit:cover;display:block}
.ws-card-cap{display:flex;flex-direction:column;gap:2px;padding:0 2px}
.ws-cap-n{font-family:var(--lfg-font-head);font-weight:700;text-transform:uppercase;font-size:.84rem;line-height:1.05;color:var(--lfg-white);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.ws-cap-when{font-size:.68rem;font-weight:600;color:var(--lfg-white-50)}
.ws-lb{position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.92);display:flex;align-items:center;justify-content:center;padding:24px}
.ws-lb-img{max-height:92vh;max-width:min(92vw,520px);border-radius:14px;box-shadow:0 24px 70px rgba(0,0,0,.5)}
.ws-lb-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}
.ws-lb-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}
.ws-lb-nav.prev{left:16px}.ws-lb-nav.next{right:16px}
.ws-lb-nav:disabled{opacity:.25;cursor:default}
.ws-lb-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){.ws-lb-nav{width:42px;height:42px}}
.ws-railwrap{position:relative}
.ws-railwrap .ws-more{position:absolute;top:50%;right:0;transform:translateY(-50%);color:var(--lfg-accent);font-size:2rem;line-height:1;pointer-events:none;opacity:.7;transition:opacity .35s;text-shadow:0 2px 12px rgba(0,0,0,.55);animation:ws-nudge 1.5s ease-in-out infinite}
.ws-railwrap .ws-more.hide{opacity:0}
@keyframes ws-nudge{0%,100%{transform:translateY(-50%) translateX(0)}50%{transform:translateY(-50%) translateX(5px)}}
@media (prefers-reduced-motion:reduce){.ws-more{animation:none}}
`;
function WeekendCarousel() {
  // the lightbox can show any "deck" of slides (the weekend post, or the
  // Curse Cleansing story) — `deck` is which array, `lb` the index into it
  const railRef = React.useRef(null);
  const [atEnd, setAtEnd] = React.useState(false);
  const onRailScroll = (e) => { const el = e.currentTarget; setAtEnd(el.scrollLeft + el.clientWidth >= el.scrollWidth - 8); };
  React.useEffect(() => {
    const el = railRef.current; if (!el) return;
    const check = () => setAtEnd(el.scrollWidth <= el.clientWidth + 8 || el.scrollLeft + el.clientWidth >= el.scrollWidth - 8);
    check();
    window.addEventListener("resize", check);
    return () => window.removeEventListener("resize", check);
  }, []);
  // open a story deck through the global StoryViewer (mounted by components.jsx)
  const open = (slides, i) => window.dispatchEvent(new CustomEvent("lfg-open-story", { detail: { slides, i: i || 0 } }));
  useEffect(() => {
    if (document.getElementById("ws-css")) return;
    const s = document.createElement("style"); s.id = "ws-css"; s.textContent = WS_CSS;
    document.head.appendChild(s);
  }, []);
  useEffect(() => {
    if (document.getElementById("ws-jsonld")) return;
    const data = {
      "@context": "https://schema.org", "@type": "ItemList",
      name: "9 things to do in London this weekend, 19 to 21 June 2026",
      itemListElement: WEEKEND_EVENTS.map((e, i) => ({
        "@type": "ListItem", position: i + 1,
        item: {
          "@type": "Event", name: e.name, startDate: e.iso,
          eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
          isAccessibleForFree: e.free !== false, description: e.blurb,
          location: { "@type": "Place", name: e.place, address: { "@type": "PostalAddress", addressLocality: "London", addressCountry: "GB" } }
        }
      }))
    };
    const el = document.createElement("script"); el.type = "application/ld+json"; el.id = "ws-jsonld";
    el.textContent = JSON.stringify(data); document.head.appendChild(el);
  }, []);
  useEffect(() => {
    if (document.getElementById("fb-jsonld")) return;
    const evs = [
      { name: "Free sarnies and fibre shots at Plenish", startDate: "2026-06-10", desc: "First 200 in the door get a free sandwich and a Plenish fibre shot with ginger, lime and mint.", place: "My Favourite Sandwich, 141 Commercial St", pc: "E1" },
      { name: "Free Curse Cleansing", startDate: "2026-06-12", desc: "A healer cleanses any cursed object you bring, plus free entry to the Ramses exhibition worth £32.", place: "NEON, Battersea Power Station", pc: "SW11" },
      { name: "Free Pizza Planet pop-up by Papa Johns", startDate: "2026-06-13", desc: "Toy Story themed Pizza Planet pop-up with free pizza. Walk-ins only, 18 plus, turn up early.", place: "White Rabbit Studios, 471-473 Dereham Pl", pc: "EC2A 3HJ" },
      { name: "Free Sriracha and free entry", startDate: "2026-06-12", desc: "30 Flying Goose sriracha flavours to taste, free entry, first 50 each day get a free bottle.", place: "59 Greek Street, Soho", pc: "W1D" }
    ];
    const data = {
      "@context": "https://schema.org", "@type": "ItemList",
      name: "4 free pop-ups in London this week, 10 to 20 June 2026",
      itemListElement: evs.map((e, i) => ({
        "@type": "ListItem", position: i + 1,
        item: {
          "@type": "Event", name: e.name, startDate: e.startDate,
          eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
          isAccessibleForFree: true, description: e.desc,
          location: { "@type": "Place", name: e.place, address: { "@type": "PostalAddress", postalCode: e.pc, addressLocality: "London", addressCountry: "GB" } }
        }
      }))
    };
    const el = document.createElement("script"); el.type = "application/ld+json"; el.id = "fb-jsonld";
    el.textContent = JSON.stringify(data); document.head.appendChild(el);
  }, []);
  useEffect(() => {
    if (document.getElementById("cn-jsonld")) return;
    const evs = [
      { name: "KOKO Electronic — free tickets", startDate: "2026-06-13", desc: "Free tickets to three big electronic nights at KOKO Camden. Comment KOKO and we send the link. No ticket, no entry.", place: "KOKO, 1a Camden High St", pc: "NW1 7JE" },
      { name: "Paraiso Disco — free terrace", startDate: "2026-07-11", desc: "Free terrace entry all day, disco til late at Night Tales in Hackney. Comment PARAISO for the link.", place: "Night Tales, 14 Bohemia Pl", pc: "E8 1DU" }
    ];
    const data = {
      "@context": "https://schema.org", "@type": "ItemList",
      name: "Free club nights in London this summer 2026",
      itemListElement: evs.map((e, i) => ({
        "@type": "ListItem", position: i + 1,
        item: {
          "@type": "Event", name: e.name, startDate: e.startDate,
          eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
          isAccessibleForFree: true, description: e.desc,
          location: { "@type": "Place", name: e.place, address: { "@type": "PostalAddress", postalCode: e.pc, addressLocality: "London", addressCountry: "GB" } }
        }
      }))
    };
    const el = document.createElement("script"); el.type = "application/ld+json"; el.id = "cn-jsonld";
    el.textContent = JSON.stringify(data); document.head.appendChild(el);
  }, []);
  useEffect(() => {
    if (document.getElementById("hw-jsonld")) return;
    const stops = [
      { name: "The Spray-Can Girl", desc: "A photoreal portrait of a woman holding a spray can, two minutes from Hackney Wick station.", place: "90A Wallis Road", pc: "E9 5LN" },
      { name: "The Elder", desc: "A towering portrait mural on the side of Curie House at 90 Mainyard.", place: "Curie House, Wallis Road", pc: "E9 5LN" },
      { name: "Hybrid Desire", desc: "A riotously colourful figure on the same wall as The Elder.", place: "Curie House, Wallis Road", pc: "E9 5LN" },
      { name: "The Old Man", desc: "A black and white portrait of a smiling old man, back past the station.", place: "96 White Post Lane", pc: "E9 5EN" },
      { name: "The Furby Door", desc: "A pink and teal Furby face painted across an old doorway.", place: "43 White Post Lane", pc: "E9 5EN" },
      { name: "Dale Grimshaw, Vintage", desc: "A large Dale Grimshaw portrait above the Schwartz Wharf vintage sign.", place: "51 White Post Lane", pc: "E9 5EN" },
      { name: "The Frog", desc: "A cheeky green frog character down a side alley near Schwartz Wharf.", place: "2 Schwartz Wharf", pc: "E9 5GU" },
      { name: "Big Teeth", desc: "A grinning big-teeth character across the canal in the Olympic Park.", place: "Kings Yard Energy Centre", pc: "E15 2HG" },
      { name: "The Yellow King", desc: "A bold yellow face on a concrete column, on the same spot as Big Teeth.", place: "Kings Yard Energy Centre", pc: "E15 2HG" }
    ];
    const data = {
      "@context": "https://schema.org", "@type": "ItemList",
      name: "Hackney Wick street art walking tour, free self-guided walk",
      itemListElement: stops.map((s, i) => ({
        "@type": "ListItem", position: i + 1,
        item: {
          "@type": "TouristAttraction", name: s.name, description: s.desc,
          isAccessibleForFree: true, publicAccess: true,
          address: { "@type": "PostalAddress", streetAddress: s.place, postalCode: s.pc, addressLocality: "London", addressCountry: "GB" }
        }
      }))
    };
    const el = document.createElement("script"); el.type = "application/ld+json"; el.id = "hw-jsonld";
    el.textContent = JSON.stringify(data); document.head.appendChild(el);
  }, []);
  return (
    <section className="ws-sec">
      <div className="wrap ws-head">
        <div>
          <span className="eyebrow">free this weekend · 19–21 Jun</span>
          <h2 className="ws-title">This weekend <span className="r">in London</span></h2>
        </div>
      </div>
      <div className="wrap ws-railwrap">
        <div className="ws-rail" role="list" ref={railRef} onScroll={onRailScroll}>
          {WEEKEND_SLIDES.map((s, i) => {
            const ev = i === 0 ? { name: "9 things this weekend", when: "19–21 Jun" } : WEEKEND_EVENTS[i - 1];
            return (
              <button className="ws-card" key={i} role="listitem" onClick={() => open(WEEKEND_SLIDES, i)} aria-label={"Enlarge: " + s.alt}>
                <span className="ws-card-img"><img src={s.src} alt={s.alt} loading="lazy" /></span>
                {ev && <span className="ws-card-cap"><span className="ws-cap-n">{ev.name}</span><span className="ws-cap-when">{ev.when}</span></span>}
              </button>
            );
          })}
        </div>
        <span className={"ws-more" + (atEnd ? " hide" : "")} aria-hidden="true" style={{ opacity: atEnd ? 0 : 0.7 }}>›</span>
      </div>
    </section>
  );
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const content = window.LFG_CONTENT;
  const freeCount = content.filter((i) => i.cost !== "ticketed").length;

  useEffect(() => {
    const r = document.documentElement.style;
    r.setProperty("--lfg-accent", t.accent);
    r.setProperty("--card-radius", t.roundedCards ? "12px" : "0px");
    r.setProperty("--mcols", COL_COUNT[t.columns] || 2);
    r.setProperty("--mcols-wide", COL_COUNT_WIDE[t.columns] || 3);
  }, [t]);

  // save direction: reflect on the root + persist + broadcast so the
  // self-mounted saved layer and every card/nav entry re-render live
  useEffect(() => {
    document.documentElement.setAttribute("data-save-pattern", t.savePattern);
    try { localStorage.setItem("lfg-save-pattern", t.savePattern); } catch (e) {}
    window.dispatchEvent(new CustomEvent("lfg-pattern-change"));
  }, [t.savePattern]);

  return (
    <React.Fragment>
      <Nav />
      <Weather leftMode={t.weatherLeft} freeCount={freeCount} />
      <main>
        <WeekendCarousel />
        <Rail items={content} />
        <FreeRightNow />
        <OnNow />
        <DayTripsStrip items={content} photo={t.daytripStyle === "photo"} />
        <Feed items={content} />
        <Band />
      </main>
      <Footer />
      <GuideAgent />

      <TweaksPanel>
        <TweakSection label="Brand accent" />
        <TweakColor label="Accent colour" value={t.accent}
          options={["#E63B2E", "#FF6A1A", "#1F8A5B", "#2A6FDB"]}
          onChange={(v) => setTweak("accent", v)} />

        <TweakSection label="Feed" />
        <TweakRadio label="Weather bar — left side" value={t.weatherLeft}
          options={[{ value: "count", label: "Count" }, { value: "tidy", label: "Label" }, { value: "date", label: "Date" }]}
          onChange={(v) => setTweak("weatherLeft", v)} />
        <TweakRadio label="Columns" value={t.columns}
          options={["tight", "regular", "roomy"]}
          onChange={(v) => setTweak("columns", v)} />
        <TweakRadio label="Get out of London cards" value={t.daytripStyle}
          options={[{ value: "photo", label: "Photo" }, { value: "bold", label: "Black" }]}
          onChange={(v) => setTweak("daytripStyle", v)} />
        <TweakToggle label="Rounded corners" value={t.roundedCards}
          onChange={(v) => setTweak("roundedCards", v)} />

        <TweakSection label="Save spots — direction" />
        <TweakRadio label="Pattern" value={t.savePattern}
          options={["heart", "tray", "map"]}
          onChange={(v) => setTweak("savePattern", v)} />
      </TweaksPanel>
    </React.Fragment>
  );
}

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