/* Slow Weekend — app shell: Home + Detail, filters, calming intro, tweaks. */

const TIME_OPTS = ["Today", "This Week", "This Weekend"];
const PROFILE_LABEL = { ages: "Ages", area: "Area", intent: "Lean", energy: "Pace", sensory: "Sensory" };
const LENS_CAPTION = {
  morning: "Picks that fit a morning.",
  afternoon: "Picks that fit an afternoon.",
  evening: "Picks that fit the 5 to 8pm stretch, the hardest hour to fill.",
};
// The time-of-day lens the current Detail was entered from (null = no lens). Set
// by openDetail from the open's ctx; read by the outbound "Go" trackers so the
// lens kill-metric (evening Go clicks) is tagged directly, not just inferred.
let _entryLens = null;

function useSaved() {
  const [saved, setSaved] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem("sw_saved") || "[]"); } catch { return []; }
  });
  React.useEffect(() => { localStorage.setItem("sw_saved", JSON.stringify(saved)); }, [saved]);
  const toggle = (id) => setSaved((s) => s.includes(id) ? s.filter((x) => x !== id) : [...s, id]);
  return [saved, toggle];
}

function useReflections() {
  const [refs, setRefs] = React.useState(() => SW.readReflections());
  React.useEffect(() => { SW.writeReflections(refs); }, [refs]);
  const set = (id, r) => setRefs((s) => ({ ...s, [id]: r }));
  return [refs, set];
}

function useFeedback() {
  const [fb, setFb] = React.useState(() => { try { return JSON.parse(localStorage.getItem("sw_feedback") || "{}"); } catch { return {}; } });
  React.useEffect(() => { try { localStorage.setItem("sw_feedback", JSON.stringify(fb)); } catch (e) {} }, [fb]);
  const set = (id, entry) => setFb((s) => ({ ...s, [id]: entry }));
  return [fb, set];
}

function useMemories() {
  const [mem, setMem] = React.useState(() => { try { return JSON.parse(localStorage.getItem("sw_memories") || "[]"); } catch { return []; } });
  React.useEffect(() => { try { localStorage.setItem("sw_memories", JSON.stringify(mem)); } catch (e) {} }, [mem]);
  const add = (id, date) => setMem((m) => [...m, { id: id, date: date || new Date().toISOString().slice(0, 10) }]);
  const removeAt = (i) => setMem((m) => m.filter((_, idx) => idx !== i));
  const setDate = (i, date) => setMem((m) => m.map((e, idx) => (idx === i ? { ...e, date: date } : e)));
  return [mem, add, removeAt, setDate];
}

// A directly-visitable link for a paired spot: its own url if we have one,
// otherwise a Google Maps search by name (+ the anchor's city for disambiguation).
function planSpotHref(spot, city) {
  if (spot.url) return spot.url;
  const q = /,/.test(spot.name) ? spot.name : (city ? `${spot.name}, ${city}` : spot.name);
  return "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(q);
}

function trackSpot(pairing, spot) {
  try { if (window.SWA) SWA.track("plan_spot_click", { pairing: pairing, name: spot, kind: "pairing" }); } catch (e) {}
}

// Booking link for ticketed / reservation outings. Prefers an affiliate URL
// when we have one (monetization that stays downstream of the editorial pick —
// see PRD §9), then a specific booking page, then the official site.
function bookingHref(pick) {
  const f = pick.facts || {};
  return f.affiliateUrl || f.bookingUrl || pick.sourceUrl || null;
}
// Only genuine ticketed admission or a required reservation is "bookable" —
// paid parking or pay-on-site (e.g. "$6 parking", "Pay per pound") is not.
function hasTickets(pick) { return /ticket|admission/i.test((pick.facts || {}).cost || ""); }
function isBookable(pick) {
  const f = pick.facts || {};
  return !!((hasTickets(pick) || f.reservation) && bookingHref(pick));
}
function bookingLabel(pick) {
  return hasTickets(pick) ? "Book tickets" : "Reserve a spot";
}
function isAffiliate(pick) { return !!(pick.facts && pick.facts.affiliateUrl); }
function trackBooking(pick) {
  try { if (window.SWA) SWA.track("outbound_click", { id: pick.id, title: pick.title, kind: "booking", affiliate: isAffiliate(pick), timeOfDay: _entryLens, partner: pick.partner || null }); } catch (e) {}
}

function makeICS(pick) {
  const body = [
    "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Slow Weekend//EN", "BEGIN:VEVENT",
    `SUMMARY:${pick.title}`, `LOCATION:${pick.city}`,
    `DESCRIPTION:${pick.blurb.replace(/,/g, "\\,")}`, "END:VEVENT", "END:VCALENDAR",
  ].join("\r\n");
  return "data:text/calendar;charset=utf-8," + encodeURIComponent(body);
}

/* ---------- Chrome ---------- */

function NavBar({ active, onHome, onCollections, onSaved, onMemories, onProfile, savedCount }) {
  const tabs = [["Home", "home", onHome], ["Explore", "collections", onCollections], ["Saved", "saved", onSaved], ["Memories", "memories", onMemories]];
  return (
    <header className="sw-nav" style={{
      position: "sticky", top: 0, zIndex: 20,
      display: "flex", alignItems: "center", justifyContent: "space-between",
      padding: "20px clamp(20px, 5vw, 64px)",
      background: "rgba(247,245,242,0.82)", backdropFilter: "blur(14px)",
      borderBottom: "1px solid rgba(47,47,47,0.06)",
    }}>
      <div onClick={onHome} style={{ cursor: "pointer", display: "flex", alignItems: "center", gap: 11, flexShrink: 0 }}>
        <span className="sw-brand-dot" style={{ width: 26, height: 26, borderRadius: "50%", background: "radial-gradient(circle at 35% 30%, #C2A878, #7E9078)", flexShrink: 0 }} />
        <span className="sw-brand-text" style={{ font: "600 19px 'Source Serif 4', serif", letterSpacing: "-0.01em", color: "#2F2F2F", whiteSpace: "nowrap" }}>Slow Weekend</span>
      </div>
      <nav className="sw-tabs" style={{ display: "flex", gap: 28, alignItems: "center" }}>
        {tabs.map(([label, key, fn]) => (
          <span key={label} className="sw-tab-link" onClick={fn || undefined} style={{
            fontSize: 14.5, cursor: fn ? "pointer" : "default", flexShrink: 0,
            color: active === key ? "#2F2F2F" : "rgba(47,47,47,0.46)",
            fontWeight: active === key ? 600 : 400,
            display: "inline-flex", alignItems: "center", gap: 6,
          }}>
            {label}
            {key === "saved" && savedCount > 0 && (
              <span style={{ fontSize: 11, fontWeight: 600, color: "#5d6b58", background: "rgba(126,144,120,0.18)", borderRadius: 999, padding: "1px 7px", lineHeight: 1.6 }}>{savedCount}</span>
            )}
          </span>
        ))}
        <button onClick={onProfile} title="Your pace" style={{
          border: active === "profile" ? "2px solid #7E9078" : "2px solid transparent",
          padding: 0, cursor: "pointer", borderRadius: "50%", background: "transparent", lineHeight: 0, marginLeft: 2,
        }}>
          <span style={{ display: "block", width: 30, height: 30, borderRadius: "50%", background: "radial-gradient(circle at 35% 30%, #C2A878, #7E9078)", boxShadow: "inset 0 0 0 2px #F7F5F2" }} />
        </button>
      </nav>
    </header>
  );
}

/* Bottom tab bar — shown only on phones (CSS `.sw-bottomnav`), the native app
   pattern: keeps all four tabs visible without crowding the top wordmark. */
function NavHomeIcon({ color }) {
  return (<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <path d="M3 10.6 12 3l9 7.6" /><path d="M5.2 9.4V20a1 1 0 0 0 1 1h11.6a1 1 0 0 0 1-1V9.4" /></svg>);
}
function NavCollectionsIcon({ color }) {
  return (<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <rect x="3.5" y="4" width="7" height="7" rx="1.6" /><rect x="13.5" y="4" width="7" height="7" rx="1.6" /><rect x="3.5" y="13" width="7" height="7" rx="1.6" /><rect x="13.5" y="13" width="7" height="7" rx="1.6" /></svg>);
}
function NavSavedIcon({ color }) {
  return (<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <path d="M12 20s-7-4.4-9.1-9C1.5 8.2 2.7 5.1 5.8 5.1c2.1 0 3.4 1.5 6.2 4 2.8-2.5 4.1-4 6.2-4 3.1 0 4.3 3.1 2.9 5.9C19 15.6 12 20 12 20Z" /></svg>);
}
function NavMemoriesIcon({ color }) {
  return (<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <circle cx="12" cy="12" r="9" /><path d="M12 7.2V12l3.4 2" /></svg>);
}

function MobileTabBar({ active, onHome, onCollections, onSaved, onMemories, savedCount }) {
  const tabs = [
    ["Home", "home", onHome, NavHomeIcon],
    ["Explore", "collections", onCollections, NavCollectionsIcon],
    ["Saved", "saved", onSaved, NavSavedIcon],
    ["Memories", "memories", onMemories, NavMemoriesIcon],
  ];
  return (
    <nav className="sw-bottomnav" style={{
      position: "fixed", left: 0, right: 0, bottom: 0, zIndex: 30,
      justifyContent: "space-around", alignItems: "stretch",
      background: "rgba(247,245,242,0.94)", backdropFilter: "blur(16px)",
      borderTop: "1px solid rgba(47,47,47,0.08)",
      padding: "7px 6px calc(7px + env(safe-area-inset-bottom))",
    }}>
      {tabs.map(([label, key, fn, Icon]) => {
        const on = active === key;
        const color = on ? "#2F2F2F" : "rgba(47,47,47,0.5)";
        return (
          <button key={key} onClick={fn} aria-label={label} aria-current={on ? "page" : undefined} style={{
            border: "none", background: "none", cursor: "pointer", font: "inherit",
            display: "flex", flexDirection: "column", alignItems: "center", gap: 3, flex: 1, padding: "4px 0", color,
          }}>
            <span style={{ position: "relative", lineHeight: 0 }}>
              <Icon color={color} />
              {key === "saved" && savedCount > 0 && (
                <span style={{ position: "absolute", top: -6, right: -10, fontSize: 10, fontWeight: 700, color: "#fff", background: "#7E9078", borderRadius: 999, padding: "0 5px", lineHeight: 1.7 }}>{savedCount}</span>
              )}
            </span>
            <span style={{ fontSize: 11, fontWeight: on ? 600 : 500 }}>{label}</span>
          </button>
        );
      })}
    </nav>
  );
}

function PinIcon({ color }) {
  return (<svg width="13" height="13" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
    <path d="M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11Z" stroke={color} strokeWidth="2" strokeLinejoin="round" />
    <circle cx="12" cy="10" r="2.4" stroke={color} strokeWidth="2" /></svg>);
}
function CalIcon({ color }) {
  return (<svg width="13" height="13" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
    <rect x="3.5" y="5" width="17" height="16" rx="2.5" stroke={color} strokeWidth="2" />
    <path d="M3.5 9.5h17M8 3v3.5M16 3v3.5" stroke={color} strokeWidth="2" strokeLinecap="round" /></svg>);
}
function LeafIcon({ color }) {
  return (<svg width="13" height="14" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
    <path d="M12 22v-8M12 14c0-5 3.5-8.5 8.5-8.5C20.5 11 17 14 12 14Z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
    <path d="M12 16C7 16 4 13 4 8c4.6 0 7.4 2.4 8 6.5" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>);
}
function ClockIcon({ color }) {
  return (<svg width="13" height="13" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
    <circle cx="12" cy="12" r="9" stroke={color} strokeWidth="2" />
    <path d="M12 7.5V12l3 1.8" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>);
}

function Filters({ time, setTime, regions, setRegions, natureOnly, setNatureOnly, energyLevels, setEnergyLevels, timeOfDay = "all", setTimeOfDay, showLens = false }) {
  const [menu, setMenu] = React.useState(null); // 'loc' | 'when' | 'energy' | null
  const locRef = React.useRef(null);
  const whenRef = React.useRef(null);
  const energyRef = React.useRef(null);
  React.useEffect(() => {
    if (!menu) return;
    const ref = ({ when: whenRef, energy: energyRef, loc: locRef })[menu] || locRef;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setMenu(null); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [menu]);
  // Mobile control approach (founder 2026-06-22, refined per PRD §466): the window
  // AND region stay visible inline so a first-time visitor sees the product's
  // capability; the remaining refinements (When/Intensity/Nature) collapse into a
  // "Refine" bottom sheet so the picks aren't buried. Desktop keeps all inline.
  const [isMobile, setIsMobile] = React.useState(() => typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(max-width: 560px)").matches);
  React.useEffect(() => {
    if (!window.matchMedia) return;
    const mq = window.matchMedia("(max-width: 560px)");
    const h = (e) => setIsMobile(e.matches);
    mq.addEventListener ? mq.addEventListener("change", h) : mq.addListener(h);
    return () => { mq.removeEventListener ? mq.removeEventListener("change", h) : mq.removeListener(h); };
  }, []);
  const [refineOpen, setRefineOpen] = React.useState(false);
  // Count only what lives in the sheet (When/Intensity/Nature) — region has its own
  // inline control + active state, so it isn't part of the "Refine" badge.
  const refineCount = (timeOfDay !== "all" ? 1 : 0) + (energyLevels.length ? 1 : 0) + (natureOnly ? 1 : 0);
  const todLabel = (SW.TIME_OF_DAY.find((b) => b.key === timeOfDay) || SW.TIME_OF_DAY[0]).label;

  const allRegions = !regions.length;
  const locLabel = allRegions ? "All regions"
    : regions.length === 1 ? SW.REGION_LABEL[regions[0]]
    : `${regions.length} regions`;

  const toggleRegion = (key) => {
    if (key === "__all") { setRegions([]); return; }
    setRegions((s) => (s.includes(key) ? s.filter((x) => x !== key) : [...s, key]));
  };

  const anyIntensity = !energyLevels.length;
  const intensityLabel = anyIntensity ? "Any intensity"
    : energyLevels.length === 1 ? energyLevels[0]
    : `${energyLevels.length} levels`;
  const toggleEnergy = (key) => {
    if (key === "__any") { setEnergyLevels([]); return; }
    setEnergyLevels((s) => (s.includes(key) ? s.filter((x) => x !== key) : [...s, key]));
  };

  const pill = (active) => ({
    display: "inline-flex", alignItems: "center", gap: 8,
    border: active ? "1px solid #7E9078" : "1px solid rgba(47,47,47,0.16)",
    background: active ? "rgba(126,144,120,0.14)" : "#FFFFFF",
    color: active ? "#5d6b58" : "rgba(47,47,47,0.66)",
    borderRadius: 999, padding: "9px 15px", fontSize: 13.5, font: "inherit", cursor: "pointer", whiteSpace: "nowrap",
    boxShadow: "0 6px 18px -16px rgba(47,47,47,0.5)", transition: "all .25s ease",
  });

  // Chip used inside the mobile filter bottom sheet (no nested dropdowns — there's
  // room to lay each choice out flat).
  const sheetChip = (key, label, on, onClick, color, glyph) => (
    <button key={key} onClick={onClick} style={{
      display: "inline-flex", alignItems: "center", gap: 7,
      border: on ? `1px solid ${color || "#7E9078"}` : "1px solid rgba(47,47,47,0.16)",
      background: on ? (color || "#7E9078") + "22" : "#FFFFFF",
      color: on ? (color || "#5d6b58") : "rgba(47,47,47,0.66)",
      borderRadius: 999, padding: "9px 15px", fontSize: 14, font: "inherit", cursor: "pointer",
      fontWeight: on ? 600 : 400, transition: "all .2s ease",
    }}>{glyph}{label}</button>
  );
  const sheetSection = (label, children) => (
    <div style={{ marginBottom: 22 }}>
      <div style={{ fontSize: 11.5, letterSpacing: "0.14em", textTransform: "uppercase", color: "rgba(47,47,47,0.42)", fontWeight: 600, marginBottom: 11 }}>{label}</div>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>{children}</div>
    </div>
  );
  const clearAll = () => { setTimeOfDay("all"); setEnergyLevels([]); setNatureOnly(false); };

  // The region control — shown inline on BOTH desktop and mobile (a key capability
  // signal for first-time visitors, PRD §466). Its dropdown menu is absolute, so it
  // works inline anywhere (unlike the fixed bottom sheet).
  const regionControl = (
    <div ref={locRef} style={{ position: "relative" }}>
      <button onClick={() => setMenu(menu === "loc" ? null : "loc")} style={pill(!allRegions)}>
        <PinIcon color={!allRegions ? "#5d6b58" : "rgba(47,47,47,0.5)"} />
        {locLabel}
        <span style={{ fontSize: 9, opacity: 0.55, transform: menu === "loc" ? "rotate(180deg)" : "none", transition: "transform .2s" }}>▾</span>
      </button>
      {menu === "loc" && (
        <div style={{ position: "absolute", top: "calc(100% + 8px)", left: "50%", transform: "translateX(-50%)", zIndex: 30, background: "#FFFFFF", borderRadius: 16, padding: 8, minWidth: 214, boxShadow: "0 22px 50px -24px rgba(47,47,47,0.5)", border: "1px solid rgba(47,47,47,0.08)" }}>
          {[{ key: "__all", label: "All regions" }].concat(SW.REGIONS).map((r) => {
            const on = r.key === "__all" ? allRegions : regions.includes(r.key);
            return (
              <button key={r.key} onClick={() => toggleRegion(r.key)} style={{
                display: "flex", alignItems: "center", gap: 11, width: "100%", border: "none",
                background: on ? "rgba(126,144,120,0.1)" : "transparent", cursor: "pointer", font: "inherit",
                fontSize: 14, color: "#2F2F2F", padding: "9px 11px", borderRadius: 10, textAlign: "left",
              }}>
                <span style={{ width: 17, height: 17, borderRadius: 5, flexShrink: 0, border: on ? "none" : "1.5px solid rgba(47,47,47,0.25)", background: on ? "#7E9078" : "transparent", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 11 }}>{on ? "✓" : ""}</span>
                {r.label}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16, alignItems: "center" }}>
      {/* window segmented — the always-visible primary control (the weekly answer) */}
      <div style={{ display: "inline-flex", background: "#FFFFFF", borderRadius: 999, padding: 4, boxShadow: "0 8px 24px -18px rgba(47,47,47,0.4)", border: "1px solid rgba(47,47,47,0.06)" }}>
        {TIME_OPTS.map((t) => {
          const on = time === t;
          return (
            <button key={t} onClick={() => setTime(t)} style={{
              border: "none", cursor: "pointer", borderRadius: 999, padding: "9px 18px", fontSize: 13.5,
              font: "inherit", letterSpacing: "0.01em", whiteSpace: "nowrap",
              background: on ? "#2F2F2F" : "transparent", color: on ? "#F7F5F2" : "rgba(47,47,47,0.6)", transition: "all 0.4s ease",
            }}>{t}</button>
          );
        })}
      </div>

      {/* Mobile: window (above) + region inline + a "Refine" sheet for the rest, so
          first-time visitors still see the product's filtering capability (PRD §466). */}
      {isMobile && (
        <div style={{ display: "flex", flexWrap: "wrap", gap: 10, alignItems: "center", justifyContent: "center" }}>
          {regionControl}
          <button data-testid="refine-toggle" onClick={() => setRefineOpen((o) => !o)} style={pill(refineCount > 0)}>
            Refine{refineCount ? ` · ${refineCount}` : ""}
            <span style={{ fontSize: 9, opacity: 0.55, transform: refineOpen ? "rotate(180deg)" : "none", transition: "transform .2s" }}>▾</span>
          </button>
        </div>
      )}
      {!isMobile && (
      <div style={{ display: "flex", flexWrap: "wrap", gap: 10, alignItems: "center", justifyContent: "center" }}>
        {/* time-of-day lens (PRD §19, the 5–8pm Experiment) as a collapsed dropdown,
            matching the location control */}
        {showLens && (
          <div ref={whenRef} style={{ position: "relative" }}>
            <button onClick={() => setMenu(menu === "when" ? null : "when")} style={pill(timeOfDay !== "all")}>
              <ClockIcon color={timeOfDay !== "all" ? "#5d6b58" : "rgba(47,47,47,0.5)"} />
              {todLabel}
              <span style={{ fontSize: 9, opacity: 0.55, transform: menu === "when" ? "rotate(180deg)" : "none", transition: "transform .2s" }}>▾</span>
            </button>
            {menu === "when" && (
              <div style={{ position: "absolute", top: "calc(100% + 8px)", left: "50%", transform: "translateX(-50%)", zIndex: 30, background: "#FFFFFF", borderRadius: 16, padding: 8, minWidth: 184, boxShadow: "0 22px 50px -24px rgba(47,47,47,0.5)", border: "1px solid rgba(47,47,47,0.08)" }}>
                {SW.TIME_OF_DAY.map((b) => {
                  const on = timeOfDay === b.key;
                  return (
                    <button key={b.key} onClick={() => { setTimeOfDay(b.key); setMenu(null); }} style={{
                      display: "flex", alignItems: "center", gap: 11, width: "100%", border: "none",
                      background: on ? "rgba(126,144,120,0.1)" : "transparent", cursor: "pointer", font: "inherit",
                      fontSize: 14, color: "#2F2F2F", padding: "9px 11px", borderRadius: 10, textAlign: "left",
                    }}>
                      <span style={{ width: 17, height: 17, borderRadius: 999, flexShrink: 0, border: on ? "none" : "1.5px solid rgba(47,47,47,0.25)", background: on ? "#7E9078" : "transparent", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 11 }}>{on ? "✓" : ""}</span>
                      {b.label}
                    </button>
                  );
                })}
              </div>
            )}
          </div>
        )}
        {regionControl}

        {/* intensity — orthogonal to category (how much the day asks of you), as a
            multi-select dropdown next to region (collapsed to match the others) */}
        <div ref={energyRef} style={{ position: "relative" }}>
          <button onClick={() => setMenu(menu === "energy" ? null : "energy")} style={pill(!anyIntensity)}>
            <span style={{ display: "inline-flex", alignItems: "flex-end", gap: 2 }}>
              {[0, 1, 2].map((i) => (
                <span key={i} style={{ width: 3, height: [6, 8, 10][i], borderRadius: 1, background: !anyIntensity ? "#5d6b58" : "rgba(47,47,47,0.4)" }} />
              ))}
            </span>
            {intensityLabel}
            <span style={{ fontSize: 9, opacity: 0.55, transform: menu === "energy" ? "rotate(180deg)" : "none", transition: "transform .2s" }}>▾</span>
          </button>
          {menu === "energy" && (
            <div style={{ position: "absolute", top: "calc(100% + 8px)", left: "50%", transform: "translateX(-50%)", zIndex: 30, background: "#FFFFFF", borderRadius: 16, padding: 8, minWidth: 214, boxShadow: "0 22px 50px -24px rgba(47,47,47,0.5)", border: "1px solid rgba(47,47,47,0.08)" }}>
              {[{ key: "__any", label: "Any intensity", bars: 0 }].concat(SW.ENERGY_LEVELS).map((e) => {
                const on = e.key === "__any" ? anyIntensity : energyLevels.includes(e.key);
                const color = ENERGY_COLOR[e.key] || "#7E9078";
                return (
                  <button key={e.key} title={e.note} onClick={() => toggleEnergy(e.key)} style={{
                    display: "flex", alignItems: "center", gap: 11, width: "100%", border: "none",
                    background: on ? "rgba(126,144,120,0.1)" : "transparent", cursor: "pointer", font: "inherit",
                    fontSize: 14, color: "#2F2F2F", padding: "9px 11px", borderRadius: 10, textAlign: "left",
                  }}>
                    <span style={{ width: 17, height: 17, borderRadius: 5, flexShrink: 0, border: on ? "none" : "1.5px solid rgba(47,47,47,0.25)", background: on ? "#7E9078" : "transparent", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 11 }}>{on ? "✓" : ""}</span>
                    {e.key !== "__any" && (
                      <span style={{ display: "inline-flex", alignItems: "flex-end", gap: 2 }}>
                        {[0, 1, 2].map((i) => (
                          <span key={i} style={{ width: 3, height: [6, 8, 10][i], borderRadius: 1, background: i < e.bars ? color : "rgba(47,47,47,0.16)" }} />
                        ))}
                      </span>
                    )}
                    {e.label || e.key}
                  </button>
                );
              })}
            </div>
          )}
        </div>

        <button onClick={() => setNatureOnly(!natureOnly)} style={pill(natureOnly)}>
          <LeafIcon color={natureOnly ? "#5d6b58" : "rgba(47,47,47,0.5)"} />
          Nature only
          <span style={{ width: 18, height: 18, borderRadius: 999, flexShrink: 0, border: natureOnly ? "none" : "1.5px solid rgba(47,47,47,0.2)", background: natureOnly ? "#7E9078" : "transparent", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#fff", fontSize: 11 }}>{natureOnly ? "✓" : ""}</span>
        </button>
      </div>
      )}

      {/* Mobile: a real bottom sheet (founder request) — flat chip sections, no nested
          dropdowns, so the whole filter set is one calm panel instead of stacked rows.
          Portaled to <body> because an ancestor's transform (sw-rise) would otherwise
          trap position:fixed inside the control bar. */}
      {isMobile && refineOpen && ReactDOM.createPortal((
        <>
          <div onClick={() => setRefineOpen(false)} style={{ position: "fixed", inset: 0, background: "rgba(47,47,47,0.38)", zIndex: 49 }} />
          <div className="sw-stepin" data-testid="refine-sheet" style={{
            position: "fixed", left: 0, right: 0, bottom: 0, zIndex: 50, background: "#F7F5F2",
            borderRadius: "22px 22px 0 0", padding: "10px 22px calc(20px + env(safe-area-inset-bottom))",
            boxShadow: "0 -20px 50px -24px rgba(47,47,47,0.5)", maxHeight: "82vh", overflowY: "auto", textAlign: "left",
          }}>
            <div style={{ width: 38, height: 4, borderRadius: 999, background: "rgba(47,47,47,0.18)", margin: "0 auto 16px" }} />
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20 }}>
              <h3 style={{ font: "500 21px 'Source Serif 4', serif", color: "#2F2F2F", margin: 0 }}>Refine your picks</h3>
              {refineCount > 0 && <button onClick={clearAll} style={{ border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: 14, color: "rgba(47,47,47,0.5)", textDecoration: "underline", textUnderlineOffset: 3 }}>Clear all</button>}
            </div>

            {showLens && sheetSection("When", SW.TIME_OF_DAY.map((b) =>
              sheetChip(b.key, b.label, timeOfDay === b.key, () => onTimeOfDay(b.key))
            ))}
            {sheetSection("Intensity", [{ key: "__any", label: "Any" }].concat(SW.ENERGY_LEVELS).map((e) =>
              sheetChip(e.key, e.label || e.key, e.key === "__any" ? anyIntensity : energyLevels.includes(e.key), () => toggleEnergy(e.key), ENERGY_COLOR[e.key])
            ))}
            {sheetSection("Outdoors", [
              sheetChip("nature", "Nature only", natureOnly, () => setNatureOnly(!natureOnly)),
            ])}

            <button data-testid="refine-done" onClick={() => setRefineOpen(false)} style={{
              width: "100%", border: "none", cursor: "pointer", borderRadius: 999, padding: "14px 24px", marginTop: 6,
              font: "inherit", fontSize: 15.5, fontWeight: 500, background: "#2F2F2F", color: "#F7F5F2",
            }}>Show picks</button>
          </div>
        </>
      ), document.body)}
    </div>
  );
}

/* ---------- Weather (PRD §15 — light, microclimate by coords) ---------- */

const _wxCache = {};
function useWeather(coord, dateISO) {
  const key = coord ? coord.lat.toFixed(3) + "," + coord.lng.toFixed(3) + "|" + dateISO : null;
  const [wx, setWx] = React.useState(() => (key && _wxCache[key]) || null);
  React.useEffect(() => {
    if (!coord || !dateISO) return;
    if (_wxCache[key]) { setWx(_wxCache[key]); return; }
    let cancelled = false;
    SW.fetchWeather(coord, dateISO).then((w) => { if (!cancelled && w) { _wxCache[key] = w; setWx(w); } });
    return () => { cancelled = true; };
  }, [key]);
  return wx;
}

const WX_TONE = { warn: "#9A6238", info: "#5d6b58", good: "rgba(47,47,47,0.6)" };

// A labeled forecast chip for a card: states the window + city so it's clearly
// "the forecast for when you'd go," not today's weather. icon · window in city ·
// temp, condition · the packing nudge (only when it's a real warn/info).
function WeatherSignal({ coord, outing, wkey }) {
  const wx = useWeather(coord, SW.windowDateISO(wkey, new Date()));
  if (!wx) return null;
  const nudge = SW.weatherNudge(wx, outing);
  const cl = SW.weatherCodeLabel(wx.code);
  const city = outing && outing.city;
  const whenWord = wkey === "today" ? "Today" : wkey === "week" ? "This week" : "This weekend";
  return (
    <div title={`Forecast for ${city || "the outing"} ${whenWord.toLowerCase()}`} style={{
      display: "inline-flex", alignItems: "center", gap: 8, flexWrap: "wrap",
      background: "rgba(47,47,47,0.04)", border: "1px solid rgba(47,47,47,0.08)", borderRadius: 999, padding: "6px 13px", lineHeight: 1.4,
    }}>
      <span style={{ flexShrink: 0, fontSize: 14 }}>{cl.icon}</span>
      <span style={{ fontSize: 12.5, fontWeight: 600, color: "#2F2F2F" }}>{whenWord}{city ? ` in ${city}` : ""}</span>
      <span style={{ fontSize: 12.5, color: "rgba(47,47,47,0.55)" }}>{wx.tempMax}°, {cl.label.toLowerCase()}</span>
      {nudge.tone !== "good" && <span style={{ fontSize: 12.5, color: WX_TONE[nudge.tone] }}>· {nudge.line}</span>}
    </div>
  );
}

// Fuller weather block for the detail aside (defaults to the upcoming weekend).
function WeatherCard({ pick }) {
  const dateISO = SW.windowDateISO("weekend", new Date());
  const wx = useWeather(pick.coord, dateISO);
  if (!wx) return null;
  const nudge = SW.weatherNudge(wx, pick);
  const cl = SW.weatherCodeLabel(wx.code);
  const dayLabel = new Date(dateISO + "T12:00:00").toLocaleDateString(undefined, { weekday: "long" });
  const warn = nudge.tone === "warn";
  return (
    <div style={{ background: warn ? "rgba(166,136,79,0.09)" : "rgba(126,144,120,0.08)", border: `1px solid ${warn ? "rgba(166,136,79,0.26)" : "rgba(126,144,120,0.2)"}`, borderRadius: 20, padding: "22px 26px" }}>
      <h3 style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: warn ? "#9A6238" : "#5d6b58", fontWeight: 600, margin: "0 0 12px" }}>Weather · {dayLabel}</h3>
      <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 8 }}>
        <span style={{ fontSize: 26 }}>{cl.icon}</span>
        <span style={{ font: "600 30px 'Source Serif 4', serif", color: "#2F2F2F" }}>{wx.tempMax}°</span>
        <span style={{ fontSize: 15, color: "rgba(47,47,47,0.45)" }}>/ {wx.tempMin}° · {cl.label}</span>
      </div>
      <div style={{ fontSize: 13, color: "rgba(47,47,47,0.55)", marginBottom: 12 }}>
        Wind {wx.wind} mph · Rain {wx.precip == null ? "—" : wx.precip + "%"}{wx.aqi != null ? ` · AQI ${wx.aqi}` : ""}
      </div>
      {nudge.tone !== "good" && <p style={{ margin: 0, fontSize: 14.5, lineHeight: 1.5, color: warn ? "#9A6238" : "#5d6b58", fontWeight: 500 }}>{nudge.line}</p>}
      <p style={{ margin: "10px 0 0", fontSize: 12, color: "rgba(47,47,47,0.4)" }}>Forecast for {dayLabel}, it can still change.</p>
    </div>
  );
}

/* ---------- Home ---------- */

// Compact secondary card for the "More worth your time" discovery layer.
// Shows a per-event TRUST signal (qualityNote) instead of a repeated window
// line, and — when onMore is given — a "More like this" affordance.
function HighlightCard({ pick, onOpen, onMore, fixedWidth, active }) {
  const accent = CAT_ACCENT[pick.category];
  const [hover, setHover] = React.useState(false);
  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{ width: fixedWidth || "auto", flex: fixedWidth ? "0 0 auto" : "1 1 auto", border: `1px solid ${active ? accent + "88" : "rgba(47,47,47,0.07)"}`,
        borderRadius: 16, overflow: "hidden", background: "#FFFFFF", display: "flex", flexDirection: "column",
        boxShadow: hover || active ? "0 20px 44px -28px rgba(47,47,47,0.5)" : "0 10px 30px -28px rgba(47,47,47,0.5)",
        transform: hover ? "translateY(-3px)" : "none", transition: "transform .25s cubic-bezier(.3,.7,.3,1), box-shadow .25s, border-color .2s" }}>
      <button onClick={onOpen} style={{ textAlign: "left", cursor: "pointer", font: "inherit", padding: 0, border: "none", background: "none", display: "block" }}>
        <TextureBlock tones={pick.tones} style={{ height: 96 }}>
          <span style={{ position: "absolute", left: 13, top: 12, display: "inline-flex", alignItems: "center", gap: 6, background: "rgba(255,255,255,0.92)", borderRadius: 999, padding: "3px 10px" }}>
            <span style={{ width: 6, height: 6, borderRadius: "50%", background: accent }} />
            <span style={{ fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: accent, fontWeight: 600 }}>{pick.category}</span>
          </span>
          <span style={{ position: "absolute", right: 12, bottom: 11, display: "inline-flex", alignItems: "center", gap: 4, background: "rgba(47,47,47,0.5)", color: "#fff", borderRadius: 999, padding: "2px 9px", fontSize: 11.5, fontWeight: 600, backdropFilter: "blur(3px)" }}>
            {pick.calmScore}<span style={{ opacity: 0.6, fontWeight: 400 }}>/10</span>
          </span>
        </TextureBlock>
        <div style={{ padding: "13px 15px 6px", display: "flex", flexDirection: "column", gap: 6 }}>
          {pick.partner && <PartnerBadge partnerKey={pick.partner} />}
          <h4 style={{ font: "500 16.5px/1.25 'Source Serif 4', serif", color: "#2F2F2F", margin: 0, letterSpacing: "-0.01em" }}>{pick.title}</h4>
          <div style={{ display: "flex", alignItems: "center", gap: 9, flexWrap: "wrap" }}>
            <span style={{ fontSize: 12.5, color: "rgba(47,47,47,0.5)" }}>{pick.city} · {SW.whenLabel(pick)}</span>
            {pick.energyLevel && <EnergyBadge level={pick.energyLevel} />}
          </div>
          {pick.facts && <OutingFacts facts={pick.facts} />}
          <div style={{ display: "inline-flex", alignItems: "center", gap: 6, marginTop: 2, fontSize: 12.5, color: accent, fontWeight: 600 }}>
            <CheckSeal color={accent} />{SW.qualityNote(pick)}
          </div>
        </div>
      </button>
      {onMore && (
        <button onClick={() => onMore(pick)} style={{ margin: "2px 12px 12px", border: "none", cursor: "pointer", font: "inherit",
          background: active ? accent + "16" : "rgba(47,47,47,0.04)", color: active ? accent : "rgba(47,47,47,0.6)",
          borderRadius: 9, padding: "7px 10px", fontSize: 12, fontWeight: 600, textAlign: "left", transition: "background .15s, color .15s" }}>
          {active ? "Hide similar ✓" : "More like this →"}
        </button>
      )}
    </div>
  );
}

// Small seal icon — a quiet trust mark used beside the quality note.
function CheckSeal({ color }) {
  return (
    <svg width="13" height="13" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
      <path d="M8 1l1.8 1.2 2.1-.3.9 1.9 1.9.9-.3 2.1L15.5 8l-1.2 1.8.3 2.1-1.9.9-.9 1.9-2.1-.3L8 15.5l-1.8-1.2-2.1.3-.9-1.9-1.9-.9.3-2.1L.5 8l1.2-1.8-.3-2.1 1.9-.9.9-1.9 2.1.3L8 1z" fill={color} opacity="0.18" />
      <path d="M5.5 8.2l1.7 1.6 3.3-3.4" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

// Editorial horizontal row (the "list" variation). Score sits on the image and
// "More like this" is a full-width footer, so the row stays readable on a phone.
function HighlightRow({ pick, onOpen, onMore, active }) {
  const accent = CAT_ACCENT[pick.category];
  const [hover, setHover] = React.useState(false);
  return (
    <div data-testid="more-row" onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{ display: "flex", flexDirection: "column", background: "#FFFFFF", borderRadius: 16, overflow: "hidden",
        border: `1px solid ${active ? accent + "88" : "rgba(47,47,47,0.07)"}`, boxShadow: hover || active ? "0 18px 40px -30px rgba(47,47,47,0.5)" : "none", transition: "box-shadow .25s, border-color .2s" }}>
      <button onClick={onOpen} style={{ display: "flex", alignItems: "stretch", gap: 0, textAlign: "left", cursor: "pointer", font: "inherit", border: "none", background: "none", padding: 0, minWidth: 0 }}>
        <TextureBlock tones={pick.tones} style={{ width: 112, flexShrink: 0, minHeight: 112 }}>
          <span style={{ position: "absolute", right: 9, bottom: 9, display: "inline-flex", alignItems: "center", gap: 3, background: "rgba(47,47,47,0.5)", color: "#fff", borderRadius: 999, padding: "2px 8px", fontSize: 11, fontWeight: 600, backdropFilter: "blur(3px)" }}>
            {pick.calmScore}<span style={{ opacity: 0.6, fontWeight: 400 }}>/10</span>
          </span>
        </TextureBlock>
        <div style={{ padding: "14px 16px", display: "flex", flexDirection: "column", gap: 5, minWidth: 0, flex: 1 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
            <span style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: accent, fontWeight: 600 }}>
              <span style={{ width: 6, height: 6, borderRadius: "50%", background: accent }} />{pick.category}
            </span>
            {pick.partner && <PartnerBadge partnerKey={pick.partner} />}
          </div>
          <h4 style={{ font: "500 18px/1.2 'Source Serif 4', serif", color: "#2F2F2F", margin: 0, letterSpacing: "-0.01em" }}>{pick.title}</h4>
          <div style={{ display: "flex", alignItems: "center", gap: 9, flexWrap: "wrap" }}>
            <span style={{ fontSize: 12.5, color: "rgba(47,47,47,0.5)" }}>{pick.city} · {SW.whenLabel(pick)}</span>
            {pick.energyLevel && <EnergyBadge level={pick.energyLevel} />}
          </div>
          {pick.facts && <OutingFacts facts={pick.facts} />}
          <div style={{ display: "inline-flex", alignItems: "center", gap: 6, marginTop: 2, fontSize: 12.5, color: accent, fontWeight: 600 }}>
            <CheckSeal color={accent} />{SW.qualityNote(pick)}
          </div>
        </div>
      </button>
      {onMore && (
        <button onClick={() => onMore(pick)} style={{ border: "none", borderTop: "1px solid rgba(47,47,47,0.06)", cursor: "pointer", font: "inherit", width: "100%", textAlign: "left",
          background: active ? accent + "10" : "transparent", color: active ? accent : "rgba(47,47,47,0.55)", fontSize: 12.5, fontWeight: 600, padding: "11px 16px", transition: "background .15s, color .15s" }}>
          {active ? "Hide similar ✓" : "More like this →"}
        </button>
      )}
    </div>
  );
}

// A revealed strip of similar outings — the inline "browse deeper" payoff.
function SimilarReveal({ seed, pool, wkey, onOpen }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (ref.current) ref.current.scrollIntoView({ behavior: "smooth", block: "nearest" });
  }, [seed.id]);
  const accent = CAT_ACCENT[seed.category];
  const sims = SW.similarTo(seed.id, pool, 5, new Date(), wkey);
  if (!sims.length) return (
    <div ref={ref} style={{ padding: "16px 18px", fontSize: 13.5, color: "rgba(47,47,47,0.5)" }}>Nothing closely similar is open in this window, try another.</div>
  );
  return (
    <div ref={ref} style={{ background: accent + "0d", border: `1px solid ${accent}33`, borderRadius: 16, padding: "16px 18px", marginTop: 12 }}>
      <div style={{ fontSize: 12.5, color: accent, fontWeight: 600, marginBottom: 12, display: "inline-flex", alignItems: "center", gap: 7 }}>
        <span style={{ width: 6, height: 6, borderRadius: "50%", background: accent }} />Because you liked <span style={{ color: "#2F2F2F" }}>{seed.title}</span>
      </div>
      <div className="sw-lane" style={{ display: "flex", gap: 12, overflowX: "auto", paddingBottom: 4 }}>
        {sims.map((o, i) => <HighlightCard key={o.id} pick={o} fixedWidth={208} onOpen={() => onOpen(o.id, { section: "more_like_this", seed: seed.id, position: i, window: wkey })} />)}
      </div>
    </div>
  );
}

// A compact Fit-score ↔ Time sort switch for the discovery list.
function SortToggle({ mode, onChange }) {
  const opt = (key, label) => {
    const on = mode === key;
    return (
      <button key={key} onClick={() => onChange(key)} style={{
        border: "none", cursor: "pointer", font: "inherit", fontSize: 12.5, padding: "5px 13px", borderRadius: 999,
        background: on ? "#2F2F2F" : "transparent", color: on ? "#F7F5F2" : "rgba(47,47,47,0.55)", transition: "all .3s ease",
      }}>{label}</button>
    );
  };
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 2, background: "#FFFFFF", borderRadius: 999, padding: 3, border: "1px solid rgba(47,47,47,0.1)" }}>
      <span style={{ fontSize: 11, color: "rgba(47,47,47,0.4)", padding: "0 6px 0 9px", letterSpacing: "0.08em", textTransform: "uppercase" }}>Sort</span>
      {opt("fit", "Fit score")}
      {opt("time", "Time")}
    </span>
  );
}

// A labelled section within the discovery list — a day (Week/Weekend) or a
// time-of-day band (Today), or the pinned "open any day" evergreen anchors.
function DiscoveryGroup({ label, sub, children }) {
  return (
    <div style={{ marginTop: 30 }}>
      <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 14 }}>
        <span style={{ width: 7, height: 7, borderRadius: "50%", background: "#7E9078" }} />
        <h4 style={{ font: "500 18px 'Source Serif 4', serif", color: "#2F2F2F", margin: 0, letterSpacing: "-0.01em" }}>{label}</h4>
        {sub && <span style={{ fontSize: 13, color: "rgba(47,47,47,0.45)" }}>· {sub}</span>}
      </div>
      {children}
    </div>
  );
}

// Curated discovery layer — shows 4 picks up front, expands on request.
// Items are flattened from the window-aware groups (evergreen first, then events)
// and sorted by Fit score or time. "More like this" is inline per row.
function MoreWorthYourTime({ extras, pool, wkey, meta, onOpen, timeOfDay }) {
  const INITIAL = 4, PAGE = 10;
  const [seed, setSeed] = React.useState(null);
  const [sortMode, setSortMode] = React.useState(wkey === "today" ? "time" : "fit");
  const [shown, setShown] = React.useState(INITIAL);
  React.useEffect(() => { setSortMode(wkey === "today" ? "time" : "fit"); setSeed(null); setShown(INITIAL); }, [wkey]);

  const onMore = (pick) => {
    setSeed((cur) => (cur && cur.id === pick.id ? null : pick));
    try { if (window.SWA) SWA.track("more_like_this", { id: pick.id, title: pick.title, category: pick.category, window: wkey }); } catch (e) {}
  };

  const { evergreen, groups } = SW.windowGroups(extras, wkey, new Date(), sortMode);
  // A recurring event can land in several day-groups; flatten then de-dup by id
  // so it shows as a single row (its `when` already names the days).
  const seen = new Set();
  const allItems = [...evergreen, ...groups.flatMap((g) => g.items)]
    .filter((o) => (seen.has(o.id) ? false : seen.add(o.id)));
  if (!allItems.length) return null;

  const visible = allItems.slice(0, shown);
  const remaining = allItems.length - shown;
  const windowPhrase = wkey === "today" ? "today" : wkey === "week" ? "this week" : "this weekend";

  return (
    <div style={{ marginTop: 60 }}>
      <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 16, flexWrap: "wrap", marginBottom: 22 }}>
        <div>
          <h3 style={{ font: "500 clamp(22px,3vw,27px) ‘Source Serif 4’, serif", color: "#2F2F2F", margin: "0 0 6px", letterSpacing: "-0.01em" }}>
            Also worth your time
          </h3>
          <p style={{ margin: 0, fontSize: 14, color: "rgba(47,47,47,0.5)", lineHeight: 1.4 }}>
            The next-best matches for your pace {windowPhrase}
          </p>
        </div>
        <SortToggle mode={sortMode} onChange={setSortMode} />
      </div>

      <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
        {visible.map((o, i) => (
          <React.Fragment key={o.id}>
            <HighlightRow pick={o} active={seed && seed.id === o.id}
              onOpen={() => onOpen(o.id, { section: "discovery", position: i, window: wkey, timeOfDay })}
              onMore={onMore} />
            {seed && seed.id === o.id && <SimilarReveal seed={seed} pool={pool} wkey={wkey} onOpen={onOpen} />}
          </React.Fragment>
        ))}
      </div>

      {remaining > 0 && (
        <button data-testid="more-toggle" onClick={() => { setShown((n) => n + PAGE); try { if (window.SWA) SWA.track("more_worth_paginate", { window: wkey, shown: shown + PAGE }); } catch (e) {} }} style={{
          marginTop: 16, border: "1px solid rgba(47,47,47,0.13)", background: "transparent",
          cursor: "pointer", font: "inherit", fontSize: 14, color: "rgba(47,47,47,0.58)",
          borderRadius: 999, padding: "12px 24px", display: "block", width: "100%",
          transition: "border-color .2s, color .2s",
        }}>
          Show {Math.min(PAGE, remaining)} more →
        </button>
      )}
      {remaining <= 0 && allItems.length > INITIAL && (
        <p style={{ marginTop: 16, textAlign: "center", fontSize: 13.5, color: "rgba(47,47,47,0.45)" }}>
          That’s everything that fits your pace {windowPhrase}. More to browse in Explore.
        </p>
      )}
    </div>
  );
}

// A gentle, dismissible "did you go?" nudge shown on a *return* visit for an
// outing the family saved but hasn't logged — converts saves into the
// "I actually went" signal (memory_add). Asks one at a time; never nags (asked
// ids are remembered).
function WentPrompt({ saved, memories, onLog }) {
  const [asked, setAsked] = React.useState(() => { try { return JSON.parse(localStorage.getItem("sw_went_asked") || "[]"); } catch (e) { return []; } });
  const data = window.SLOW_WEEKEND_DATA;
  let returning = false; try { returning = (Number(localStorage.getItem("swa_visits")) || 0) > 1; } catch (e) {}
  const visited = new Set((memories || []).map((m) => m.id));
  const askedSet = new Set(asked);
  const candidateId = (saved || []).find((id) => !visited.has(id) && !askedSet.has(id));
  const o = candidateId ? data.outings.find((x) => x.id === candidateId) : null;
  if (!returning || !o) return null;
  const accent = CAT_ACCENT[o.category];
  const answer = (yes) => {
    const next = [...asked, candidateId];
    setAsked(next); try { localStorage.setItem("sw_went_asked", JSON.stringify(next)); } catch (e) {}
    try { if (window.SWA) SWA.track("went_prompt", { id: candidateId, title: o.title, answered: yes ? "yes" : "not_yet" }); } catch (e) {}
    if (yes && onLog) onLog(candidateId);
  };
  return (
    <div className="sw-rise" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, flexWrap: "wrap", background: "rgba(126,144,120,0.09)", border: "1px solid rgba(126,144,120,0.22)", borderRadius: 16, padding: "16px 22px", marginBottom: 30 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12, minWidth: 0 }}>
        <span style={{ width: 9, height: 9, borderRadius: "50%", background: accent, flexShrink: 0 }} />
        <span style={{ fontSize: 15, color: "#2F2F2F", lineHeight: 1.4 }}>Welcome back. You saved <strong style={{ fontWeight: 600 }}>{o.title}</strong>, did you get to go?</span>
      </div>
      <div style={{ display: "flex", gap: 9, flexShrink: 0 }}>
        <button onClick={() => answer(true)} style={{ border: "none", cursor: "pointer", borderRadius: 999, padding: "9px 18px", font: "inherit", fontSize: 13.5, fontWeight: 600, background: "#2F2F2F", color: "#F7F5F2", whiteSpace: "nowrap" }}>Yes, add to memories</button>
        <button onClick={() => answer(false)} style={{ border: "1px solid rgba(47,47,47,0.16)", cursor: "pointer", borderRadius: 999, padding: "9px 16px", font: "inherit", fontSize: 13.5, color: "rgba(47,47,47,0.6)", background: "transparent", whiteSpace: "nowrap" }}>Not yet</button>
      </div>
    </div>
  );
}

// Regional forecast anchor for context-freshness ordering — a mid-Peninsula
// centroid. The gross rainy/hot/clear signal is regional; per-card microclimate
// still drives each card's own WeatherSignal.
const SW_REGION_COORD = { lat: 37.56, lng: -122.32 };

function Home({ tweaks, prefs = {}, onOpen, onProfile, saved = [], memories = [], onLog, onToggleSave, tuneHook, homeState, initialPicks }) {
  const data = window.SLOW_WEEKEND_DATA;
  const tuned = !!(prefs.intent || prefs.sensory || prefs.energy || (prefs.ages && prefs.ages.length));
  const summary = SW.prefSummary(prefs);
  const profile = SW.profileChips(prefs);
  const payoff = tweaks.personalization;

  // Filter state lives in App (it must survive navigation away and back; Home
  // unmounts on every detail/tab visit, so local state would reset — the
  // filter-persistence bug). First-load values come from the calendar + clock
  // (SW.contextualDefaults), set up in App.
  const { time, setTime, timeOfDay, setTimeOfDay, natureOnly, setNatureOnly, energyLevels, setEnergyLevels, regions, setRegions } = homeState;

  // Snapshot share (PRD §8): a ?picks= link lands the recipient on the sharer's
  // exact three. They stay pinned until the visitor touches a control, at which
  // point we release the pin and they're in a normal, personalized session.
  const [pinned, setPinned] = React.useState(initialPicks && initialPicks.length ? initialPicks : null);
  const releasePin = () => { if (pinned) setPinned(null); };
  const pin = (setter) => (v) => { releasePin(); setter(v); };

  // Today / This Week / This Weekend are nested in time but distinct in intent.
  const WINDOW_KEY = { "Today": "today", "This Week": "week", "This Weekend": "weekend" };
  const wkey = WINDOW_KEY[time] || "weekend";

  // Time-of-day lens (PRD §19, Experiment): the tweak doubles as the kill-switch.
  const lensOn = tweaks.timeLens !== "hidden";
  React.useEffect(() => { if (!lensOn && timeOfDay !== "all") setTimeOfDay("all"); }, [lensOn, timeOfDay]);
  const onTimeOfDay = (band) => {
    setTimeOfDay(band);
    try { if (window.SWA && band !== "all") SWA.track("time_lens", { band, window: wkey }); } catch (e) {}
  };
  // Saves under an active lens carry the band, so the kill-metric (evening saves)
  // is measurable. The central save_toggle still fires via onToggleSave.
  const onSaveLens = (id) => {
    try { if (window.SWA && timeOfDay !== "all") SWA.track("time_lens_save", { id, band: timeOfDay, window: wkey }); } catch (e) {}
    onToggleSave(id);
  };

  // The pool narrows for Nature-only (outdoor), the Energy Level tag, a
  // "Free only" budget preference, and the chosen sub-regions (location filters
  // the three by region, a bounded control that can't sprawl as inventory grows).
  const pool = React.useMemo(
    () => SW.applyPoolFilters(data.outings, { natureOnly, energyLevels, budget: prefs.budget, regions }),
    [natureOnly, energyLevels.join(","), prefs.budget, JSON.stringify(regions)]
  );
  // The lens narrows the pool to outings whose hours fit the chosen band, so the
  // weekly three become the answer for that time of day (the 5–8pm moment).
  const lensPool = React.useMemo(
    () => (timeOfDay === "all" ? pool : pool.filter((o) => SW.fitsTimeOfDay(o, timeOfDay))),
    [pool, timeOfDay]
  );

  // Context-driven freshness (PRD §19, Experiment; the tweak is the kill-switch):
  // one regional forecast for this window's date + the family's recent visits feed
  // SOFT nudges into the ranking, so the same pool reorders as the world and their
  // history change. Weather is fetched once for the region (not per card here).
  const freshnessOn = tweaks.freshness !== "hidden";
  const [freshWeather, setFreshWeather] = React.useState(null);
  React.useEffect(() => {
    if (!freshnessOn) { setFreshWeather(null); return; }
    let alive = true;
    const dateISO = SW.windowDateISO(wkey, new Date());
    SW.fetchWeather(SW_REGION_COORD, dateISO).then((w) => { if (alive) setFreshWeather(w); });
    return () => { alive = false; };
  }, [freshnessOn, wkey]);
  const recentIds = React.useMemo(
    () => (freshnessOn ? SW.recentlyVisitedIds(memories, new Date(), 21) : []),
    [freshnessOn, JSON.stringify(memories)]
  );
  // Taste learned from the family's own saves (always on; the user's own data, a
  // soft nudge that holds §4). Bundled into the same context the engine reads.
  const taste = React.useMemo(() => SW.savedTaste(saved, data.outings), [saved.join(",")]);
  const freshCtx = React.useMemo(
    () => ({ weather: freshnessOn ? freshWeather : null, recent: freshnessOn ? recentIds : null, taste }),
    [freshnessOn, freshWeather, recentIds.join(","), taste]
  );

  const windowed = React.useMemo(
    () => SW.windowedPicks(lensPool, { ...prefs, regions }, new Date(), freshCtx),
    [lensPool, prefs.intent, prefs.sensory, prefs.energy, JSON.stringify(prefs.ages), JSON.stringify(regions), freshCtx]
  );
  // Pinned snapshot picks (from a ?picks= share) take precedence over the
  // computed three; render the exact outings the sharer saw, in their order.
  const pinnedPicks = React.useMemo(
    () => (pinned ? pinned.map((id) => data.outings.find((o) => o.id === id)).filter(Boolean) : null),
    [pinned]
  );
  const heroPicks = (pinnedPicks && pinnedPicks.length) ? pinnedPicks : (windowed[wkey] || []);
  const heroIds = heroPicks.map((p) => p.id);
  const extras = React.useMemo(
    () => SW.windowExtras(lensPool, { ...prefs, regions }, heroIds, wkey, new Date(), 14, freshCtx),
    [lensPool, prefs.intent, prefs.sensory, prefs.energy, JSON.stringify(prefs.ages), JSON.stringify(regions), wkey, heroIds.join(","), freshCtx]
  );
  // The honest "why it shifted" line, only when weather genuinely moved the three.
  const freshNote = (freshnessOn && !pinned)
    ? SW.freshnessNote(freshWeather, wkey === "today" ? "today" : wkey === "week" ? "this week" : "this weekend")
    : null;
  React.useEffect(() => {
    try { if (freshNote && window.SWA) SWA.track("freshness_context", { window: wkey }); } catch (e) {}
  }, [freshNote, wkey]);
  const [heroSeed, setHeroSeed] = React.useState(null);
  React.useEffect(() => { setHeroSeed(null); }, [wkey, timeOfDay, JSON.stringify(regions), natureOnly, energyLevels.join(",")]);
  const onHeroMore = (p) => {
    setHeroSeed((cur) => (cur && cur.id === p.id ? null : p));
    try { if (window.SWA) SWA.track("more_like_this", { id: p.id, title: p.title, category: p.category, window: wkey, section: "top3" }); } catch (e) {}
  };
  const meta = SW.windowMeta(wkey);
  // Honest framing (§9): the heading earns "your three" only as the family tunes +
  // saves + logs. A pinned share isn't the viewer's tuned three, so it stays neutral.
  const density = SW.signalDensity(prefs, saved, memories);
  const heroHeading = pinned ? meta.title : SW.windowHeading(wkey, density);
  const locLabel = !regions.length ? "Bay Area" : regions.map((k) => SW.REGION_LABEL[k] || k).join(", ");
  // The honest "chosen from N" depth line (PRD §9; Tweak-staged Experiment). N is
  // the true count eligible this window from the current pool — only worth saying
  // when it's meaningfully more than the three, and not for a pinned share.
  const eligibleN = React.useMemo(() => SW.windowEligibleCount(lensPool, wkey, new Date()), [lensPool, wkey]);
  const whenWord = wkey === "today" ? "today" : wkey === "week" ? "this week" : "this weekend";
  const showTrustLine = tweaks.trustLine !== "hidden" && !pinned && heroPicks.length >= 3 && eligibleN > 4;

  return (
    <div data-testid="home" data-window={wkey} data-timeofday={timeOfDay} data-nature={String(natureOnly)} data-regions={regions.join(",") || "all"}>
      {/* Masthead — what it is, and the one control that drives everything */}
      <section style={{ textAlign: "center", padding: "clamp(26px,4vw,52px) clamp(20px,6vw,64px) 0", maxWidth: 720, margin: "0 auto" }}>
        <div className="sw-rise" style={{ animationDelay: "0.05s" }}>
          <span style={{ fontSize: 13, letterSpacing: "0.22em", textTransform: "uppercase", color: "rgba(47,47,47,0.42)" }}>Plans that feel good</span>
        </div>
        <h1 className="sw-rise" style={{ animationDelay: "0.14s", font: "500 clamp(32px,5vw,54px)/1.06 'Source Serif 4', serif", letterSpacing: "-0.02em", color: "#2F2F2F", margin: "12px 0 0" }}>
          Family plans, filtered.
        </h1>
        <p className="sw-rise" style={{ animationDelay: "0.24s", fontSize: "clamp(15px,1.8vw,18px)", lineHeight: 1.5, color: "rgba(47,47,47,0.62)", maxWidth: 520, margin: "12px auto 0" }}>
          Curated Bay Area family outings worth your energy, three fresh picks each week. No endless scrolling, no second-guessing.
        </p>
      </section>

      {/* The control bar — tightened vertically so the top of the first pick peeks
          above the fold on tall screens (instant proof of value). position+zIndex
          lift the whole section so an open filter dropdown paints above the picks
          below (the sw-rise transform makes this section its own stacking context). */}
      <section className="sw-rise" style={{ position: "relative", zIndex: 40, animationDelay: "0.34s", padding: "clamp(22px,3vw,32px) clamp(20px,6vw,64px) clamp(24px,3.2vw,36px)" }}>
        <Filters time={time} setTime={pin(setTime)} regions={regions} setRegions={pin(setRegions)} natureOnly={natureOnly} setNatureOnly={pin(setNatureOnly)} energyLevels={energyLevels} setEnergyLevels={pin(setEnergyLevels)} timeOfDay={timeOfDay} setTimeOfDay={pin(onTimeOfDay)} showLens={lensOn} />
      </section>

      {/* Picks */}
      <section style={{ padding: "0 clamp(20px,6vw,64px) 110px", maxWidth: 1080, margin: "0 auto" }}>
        {onLog && <WentPrompt saved={saved} memories={memories} onLog={onLog} />}
        {tuneHook && (
          <div className="sw-fade" style={{ display: "flex", alignItems: "center", gap: 14, justifyContent: "space-between", flexWrap: "wrap", background: "rgba(126,144,120,0.09)", border: "1px solid rgba(126,144,120,0.22)", borderRadius: 16, padding: "13px 18px", marginBottom: 26 }}>
            <span style={{ fontSize: 14, color: "#4f5c4a", lineHeight: 1.5 }}>Like what you see? Tune these to your family, three quick taps.</span>
            <span style={{ display: "inline-flex", gap: 8, flexShrink: 0 }}>
              <button onClick={tuneHook.onTune} style={{ border: "none", cursor: "pointer", borderRadius: 999, padding: "8px 16px", font: "inherit", fontSize: 13.5, fontWeight: 600, background: "#2F2F2F", color: "#F7F5F2" }}>Tune my picks</button>
              <button onClick={tuneHook.onDismiss} style={{ border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: 13.5, color: "rgba(47,47,47,0.45)", padding: "8px 6px" }}>Not now</button>
            </span>
          </div>
        )}
        <div style={{ display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap", marginBottom: 26 }}>
          <h2 data-testid="hero-heading" style={{ font: "500 clamp(24px,3.2vw,29px)/1.15 'Source Serif 4', serif", color: "#2F2F2F", margin: 0, letterSpacing: "-0.01em" }}>{heroHeading}</h2>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 13.5, color: "rgba(47,47,47,0.5)" }}>
            <span className="sw-breath" style={{ width: 7, height: 7, borderRadius: "50%", background: "#7E9078" }} />
            {meta.sub}
          </span>
          {heroPicks.length > 0 && <span style={{ marginLeft: "auto", alignSelf: "center" }}><ShareButton route={{ type: "picks", ids: heroIds, window: wkey }} label="Share this week" /></span>}
        </div>

        {timeOfDay !== "all" && (
          <p style={{ margin: "-8px 0 26px", fontSize: 13.5, color: "#6f5f93", display: "inline-flex", alignItems: "center", gap: 8 }}>
            <span style={{ width: 7, height: 7, borderRadius: "50%", background: "#8576A0", flexShrink: 0 }} />
            {LENS_CAPTION[timeOfDay]}
          </p>
        )}

        {freshNote && (
          <p style={{ margin: "-8px 0 26px", fontSize: 13.5, color: "#5d6b58", display: "inline-flex", alignItems: "center", gap: 8 }}>
            <span style={{ width: 7, height: 7, borderRadius: "50%", background: "#7E9078", flexShrink: 0 }} />
            {freshNote}
          </p>
        )}

        {showTrustLine && (
          <p style={{ margin: "-8px 0 26px", fontSize: 13, color: "rgba(47,47,47,0.5)" }}>
            Three, chosen from <strong style={{ color: "#2F2F2F", fontWeight: 600 }}>{eligibleN}</strong> that cleared the Slow Weekend Filter {whenWord}.
          </p>
        )}

        {tweaks.howWeChoose !== "hidden" && !pinned && heroPicks.length >= 3 && (
          <HowWeChoose tuned={tuned} whenWord={whenWord} hasSaves={saved.length > 0} hasMemories={memories.length > 0} />
        )}

        {tuned && payoff === "banner" ? (
          <div style={{ background: "rgba(126,144,120,0.09)", border: "1px solid rgba(126,144,120,0.2)", borderRadius: 16, padding: "18px 24px", marginBottom: 34 }}>
            <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 18, flexWrap: "wrap" }}>
              <span style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: "#5d6b58", fontWeight: 600 }}>Chosen for your pace</span>
              <button onClick={onProfile} style={{ border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: 13.5, fontWeight: 600, color: "#5d6b58", whiteSpace: "nowrap" }}>Tune your pace →</button>
            </div>
            {profile.length > 0 && (
              <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 13 }}>
                {profile.map((c) => (
                  <span key={c.k} style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 13, color: "#4f5c4a", background: "rgba(126,144,120,0.16)", borderRadius: 999, padding: "5px 13px" }}>
                    <span style={{ fontSize: 10.5, letterSpacing: "0.06em", textTransform: "uppercase", color: "rgba(93,107,88,0.7)", fontWeight: 600 }}>{PROFILE_LABEL[c.k]}</span>
                    {c.label}
                  </span>
                ))}
              </div>
            )}
          </div>
        ) : (
          <p style={{ margin: "0 0 36px", fontSize: 15, color: "rgba(47,47,47,0.52)", maxWidth: 580, lineHeight: 1.6 }}>
            {tuned
              ? <>Re-ranked for {summary.length ? <>a <strong style={{ color: "#2F2F2F", fontWeight: 600 }}>{summary.join(" · ")}</strong> family</> : "your family"}, then matched to the moment. Each still cleared the Slow Weekend Filter, choose without second-guessing.</>
              : <>{meta.intro} Each one cleared the Slow Weekend Filter, so you can choose without second-guessing.</>}
          </p>
        )}

        {heroPicks.length === 0 && (
          <div className="sw-fade" style={{ textAlign: "center", padding: "38px 22px", border: "1px dashed rgba(133,118,160,0.4)", borderRadius: 16, color: "rgba(47,47,47,0.6)", fontSize: 15, lineHeight: 1.6 }}>
            Nothing fits {timeOfDay} {time.toLowerCase()} just yet. Try another time of day, or a wider window.
          </div>
        )}
        <div data-testid="hero-picks" style={{ display: "grid", gap: 26, gridTemplateColumns: tweaks.cardStyle === "panel" ? "1fr" : "repeat(auto-fit, minmax(300px, 1fr))" }}>
          {heroPicks.map((p, i) => {
            const seeded = heroSeed && heroSeed.id === p.id;
            return (
              <div key={p.id} className="sw-rise" style={{ animationDelay: `${0.55 + i * 0.12}s`, display: "flex", flexDirection: "column", height: "100%" }}>
                {payoff === "inline" && (
                  <div style={{ display: "inline-flex", alignItems: "center", gap: 7, marginBottom: 11, fontSize: 12.5, color: "#5d6b58", fontWeight: 600 }}>
                    <CheckSeal color="#7E9078" />
                    {SW.qualityNote(p)}
                  </div>
                )}
                <RecommendationCard pick={p} variant={tweaks.cardStyle} scoreVariant={tweaks.scoreStyle} onOpen={(id) => onOpen(id, { section: "top3", position: i, window: wkey, timeOfDay })} isSaved={saved.includes(p.id)} onToggleSave={onSaveLens} />
                <div style={{ marginTop: 11, display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 7, minHeight: 44 }}>
                  <WeatherSignal coord={p.coord} outing={p} wkey={wkey} />
                  <button onClick={() => onHeroMore(p)} style={{
                    border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: 13, fontWeight: 600,
                    color: seeded ? CAT_ACCENT[p.category] : "rgba(47,47,47,0.55)", padding: "4px 2px", whiteSpace: "nowrap",
                  }}>{seeded ? "Hide similar ✓" : "More like this →"}</button>
                </div>
              </div>
            );
          })}
        </div>

        {heroSeed && <SimilarReveal seed={heroSeed} pool={pool} wkey={wkey} onOpen={onOpen} />}

        {extras.length > 0 && (
          <MoreWorthYourTime extras={extras} pool={pool} wkey={wkey} meta={meta} onOpen={onOpen} timeOfDay={timeOfDay} />
        )}

        <MakeItAPlan pool={pool} onOpen={onOpen} />

        <NewsletterSignup variant="card" />
      </section>
    </div>
  );
}

/* ---------- Feedback (PRD §16 — solicited signal) ---------- */

// Per-pick 👍/👎. Stores the vote locally and fires `feedback` events for the
// kill/keep loop. It informs our editorial calls — it never auto-ranks (§4).
function FeedbackVote({ pick, value, onVote }) {
  const vote = value && value.vote;
  const [reason, setReason] = React.useState((value && value.reason) || "");
  const [showReason, setShowReason] = React.useState(false);
  const cast = (v) => {
    onVote(pick.id, { vote: v, reason: v === "down" ? (value && value.reason) || "" : "" });
    try { if (window.SWA) SWA.track("feedback", { id: pick.id, title: pick.title, category: pick.category, vote: v, section: "detail" }); } catch (e) {}
    setShowReason(v === "down");
  };
  const sendReason = () => {
    const r = reason.trim();
    onVote(pick.id, { vote: "down", reason: r });
    try { if (window.SWA && r) SWA.track("feedback_reason", { id: pick.id, title: pick.title, reason: r }); } catch (e) {}
    setShowReason(false);
  };
  const btn = (v, label, emoji) => {
    const on = vote === v;
    const c = v === "up" ? "#5d6b58" : "#9A6238";
    return (
      <button onClick={() => cast(v)} style={{ flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8, cursor: "pointer", font: "inherit", fontSize: 14, fontWeight: 600,
        border: `1px solid ${on ? c : "rgba(47,47,47,0.16)"}`, background: on ? c + "14" : "#FFFFFF", color: on ? c : "rgba(47,47,47,0.62)", borderRadius: 12, padding: "11px 12px", transition: "all .2s" }}>
        <span style={{ fontSize: 16 }}>{emoji}</span>{label}
      </button>
    );
  };
  return (
    <div style={{ background: "#FFFFFF", borderRadius: 20, padding: "22px 26px", boxShadow: "0 18px 44px -30px rgba(47,47,47,0.4)", border: "1px solid rgba(47,47,47,0.06)" }}>
      <h3 style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: "rgba(47,47,47,0.5)", fontWeight: 600, margin: "0 0 6px" }}>Was this a good pick?</h3>
      <p style={{ margin: "0 0 14px", fontSize: 13, lineHeight: 1.5, color: "rgba(47,47,47,0.55)" }}>Your vote helps us tune what we recommend, it never auto-ranks the list.</p>
      <div style={{ display: "flex", gap: 10 }}>
        {btn("up", "Good fit", "👍")}
        {btn("down", "Not for us", "👎")}
      </div>
      {(showReason || vote === "down") && (
        <div style={{ marginTop: 12 }}>
          <input value={reason} onChange={(e) => setReason(e.target.value)} placeholder="What was off? (optional)"
            style={{ width: "100%", border: "1px solid rgba(47,47,47,0.16)", borderRadius: 12, padding: "10px 14px", fontSize: 13.5, background: "#FFFFFF", color: "#2F2F2F", outline: "none", boxSizing: "border-box" }} />
          <button onClick={sendReason} style={{ marginTop: 8, border: "none", cursor: "pointer", font: "inherit", fontSize: 13, fontWeight: 600, color: "#9A6238", background: "none", padding: "2px 0" }}>Send →</button>
        </div>
      )}
      {vote && !showReason && (
        <p style={{ margin: "12px 2px 0", fontSize: 12.5, color: "#5d6b58", fontWeight: 500 }}>Thanks, noted.</p>
      )}
    </div>
  );
}

/* ---------- Detail ---------- */

function Pill({ children }) {
  return <span style={{ fontSize: 14, color: "rgba(47,47,47,0.6)", display: "inline-flex", alignItems: "center", gap: 8, whiteSpace: "nowrap" }}>{children}</span>;
}

/* The toddler-hikes layer, made first-class: the living draw, when it's best,
   and what to bring, with a link out to the full trail map. */
function TrailDraws({ pick, accent }) {
  const t = pick.trail;
  if (!t) return null;
  const hasPairing = (window.SLOW_WEEKEND_DATA.pairings || []).some((p) => p.anchor === pick.id);
  const season = window.SW && SW.trailSeasonStatus ? SW.trailSeasonStatus(pick) : { inBest: false };
  const labelCss = { fontSize: 11.5, letterSpacing: "0.13em", textTransform: "uppercase", color: accent, fontWeight: 700, flexShrink: 0, width: 64, paddingTop: 3 };
  return (
    <div style={{ background: "#FFFFFF", border: "1px solid rgba(47,47,47,0.07)", borderLeft: `3px solid ${accent}`, borderRadius: 16, padding: "24px 26px", marginBottom: 36, boxShadow: "0 14px 38px -32px rgba(47,47,47,0.5)" }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap", margin: "0 0 16px" }}>
        <h3 style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: accent, fontWeight: 600, margin: 0 }}>Why a little one will love it</h3>
        {season.inBest && <SeasonNowChip />}
      </div>
      <ul style={{ listStyle: "none", padding: 0, margin: "0 0 20px", display: "flex", flexDirection: "column", gap: 12 }}>
        {(t.draws || []).map((d, i) => (
          <li key={i} style={{ display: "flex", gap: 13, alignItems: "flex-start", font: "400 17px/1.4 'Source Serif 4', serif", color: "#2F2F2F" }}>
            <span style={{ marginTop: 9, width: 7, height: 7, borderRadius: "50%", background: accent, flexShrink: 0 }} />
            {d}
          </li>
        ))}
      </ul>
      <div style={{ display: "flex", flexDirection: "column", gap: 14, paddingTop: 18, borderTop: "1px solid rgba(47,47,47,0.08)" }}>
        {t.bestSeason && (
          <div style={{ display: "flex", gap: 14, alignItems: "flex-start" }}>
            <span style={labelCss}>Best in</span>
            <p style={{ margin: 0, fontSize: 14.5, lineHeight: 1.55, color: "rgba(47,47,47,0.72)" }}>
              <strong style={{ color: "#2F2F2F", fontWeight: 600 }}>{t.bestSeason.label}.</strong> {t.bestSeason.why}
            </p>
          </div>
        )}
        {t.gear && t.gear.length > 0 && (
          <div style={{ display: "flex", gap: 14, alignItems: "flex-start" }}>
            <span style={labelCss}>Bring</span>
            <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }}>
              {t.gear.map((g, i) => (
                <li key={i} style={{ fontSize: 14.5, lineHeight: 1.5, color: "rgba(47,47,47,0.72)" }}>{g}</li>
              ))}
            </ul>
          </div>
        )}
        {t.pair && !hasPairing && (
          <div style={{ display: "flex", gap: 14, alignItems: "flex-start" }}>
            <span style={labelCss}>After</span>
            <p style={{ margin: 0, fontSize: 14.5, lineHeight: 1.55, color: "rgba(47,47,47,0.72)" }}>{t.pair}</p>
          </div>
        )}
      </div>
      {t.trailUrl && (
        <a href={t.trailUrl} target="_blank" rel="noopener noreferrer"
          onClick={() => { try { if (window.SWA) SWA.track("outbound_click", { id: pick.id, title: pick.title, kind: "alltrails", timeOfDay: _entryLens, partner: pick.partner || null }); } catch (e) {} }}
          style={{ display: "inline-flex", alignItems: "center", gap: 8, marginTop: 20, textDecoration: "none", fontSize: 13.5, fontWeight: 600, color: accent }}>
          See the full route &amp; map ↗
        </a>
      )}
    </div>
  );
}

function Detail({ id, onBack, backLabel = "← Back to this week", saved, toggleSave, feedback, onVote, memories, onLog, onUnlog, prefs = {} }) {
  const pick = window.SLOW_WEEKEND_DATA.outings.find((p) => p.id === id);
  const accent = CAT_ACCENT[pick.category];
  const isSaved = saved.includes(id);
  // Type/category-aware emphasis (existing data only): the kind leads, events
  // lead their meta with the schedule, and context chips name what matters.
  const ctx = SW.detailContext(pick);
  const meta = ctx.isEvent
    ? [SW.whenLabel(pick), pick.city, pick.ageRange]
    : [pick.city, SW.whenLabel(pick), pick.ageRange];

  return (
    <div className="sw-fade" style={{ maxWidth: 980, margin: "0 auto", padding: "28px clamp(20px,6vw,64px) 110px" }}>
      <button onClick={onBack} style={{ border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: 14.5, color: "rgba(47,47,47,0.6)", display: "inline-flex", alignItems: "center", gap: 8, padding: "6px 0", marginBottom: 22, whiteSpace: "nowrap" }}>
        {backLabel}
      </button>

      <TextureBlock tones={pick.tones} style={{ height: 260, borderRadius: 22 }}>
        <div style={{ position: "absolute", top: 18, right: 18, display: "inline-flex", alignItems: "center", gap: 10 }}>
          <ShareButton route={{ type: "outing", id }} variant="icon" label={pick.title} />
          <HeartButton saved={isSaved} accent={CAT_ACCENT[pick.category]} onToggle={() => toggleSave(id)} />
        </div>
        <div style={{ position: "absolute", left: 30, bottom: 26, display: "inline-flex", alignItems: "center", gap: 9, whiteSpace: "nowrap" }}>
          <span style={{ width: 8, height: 8, borderRadius: "50%", background: "#fff" }} />
          <span style={{ fontSize: 12.5, letterSpacing: "0.16em", textTransform: "uppercase", color: "#fff", fontWeight: 600, textShadow: "0 1px 10px rgba(47,47,47,0.35)" }}>{pick.category}</span>
          <span style={{ fontSize: 12.5, color: "rgba(255,255,255,0.78)", textShadow: "0 1px 10px rgba(47,47,47,0.35)" }}>· {pick.texture}</span>
        </div>
      </TextureBlock>

      <div className="sw-detail-grid" style={{ marginTop: 40 }}>
        {/* Main */}
        <div>
          <div style={{ display: "inline-flex", alignItems: "center", gap: 9, marginBottom: 16 }}>
            <span style={{ width: 8, height: 8, borderRadius: "50%", background: accent }} />
            <span style={{ fontSize: 12.5, letterSpacing: "0.16em", textTransform: "uppercase", color: accent, fontWeight: 600 }}>{ctx.typeLabel ? `${ctx.typeLabel} · ${pick.category}` : pick.category}</span>
          </div>
          <h1 style={{ font: "500 clamp(30px,4.4vw,42px)/1.12 'Source Serif 4', serif", letterSpacing: "-0.02em", color: "#2F2F2F", margin: "0 0 18px" }}>{pick.title}</h1>
          <div style={{ display: "flex", flexWrap: "wrap", gap: "8px 18px", alignItems: "center", marginBottom: 30 }}>
            {meta.map((m, i) => (
              <React.Fragment key={i}>
                {i > 0 && <span style={{ width: 3, height: 3, borderRadius: "50%", background: "rgba(47,47,47,0.25)" }} />}
                <Pill>{m}</Pill>
              </React.Fragment>
            ))}
            {pick.energyLevel && <span style={{ width: 3, height: 3, borderRadius: "50%", background: "rgba(47,47,47,0.25)" }} />}
            {pick.energyLevel && <EnergyBadge level={pick.energyLevel} lg />}
          </div>

          {ctx.chips.length > 0 && (
            <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 30 }}>
              {ctx.chips.map((c) => (
                <span key={c} style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 12.5, fontWeight: 600, color: "#5d6b58", background: "rgba(126,144,120,0.13)", border: "1px solid rgba(126,144,120,0.22)", borderRadius: 999, padding: "5px 13px" }}>
                  <span style={{ width: 6, height: 6, borderRadius: "50%", background: "#7E9078" }} />
                  {c}
                </span>
              ))}
            </div>
          )}

          {pick.partner && <div style={{ marginBottom: 30 }}><PartnerBadge partnerKey={pick.partner} variant="full" /></div>}

          <p style={{ font: "400 20px/1.5 'Source Serif 4', serif", color: "#2F2F2F", margin: "0 0 32px" }}>{pick.blurb}</p>

          <TrailDraws pick={pick} accent={accent} />

          {pick.facts && (
            <div style={{ background: "#FFFFFF", border: "1px solid rgba(47,47,47,0.07)", borderRadius: 16, padding: "22px 26px", marginBottom: 36, boxShadow: "0 14px 38px -32px rgba(47,47,47,0.5)" }}>
              <h3 style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: "rgba(47,47,47,0.5)", fontWeight: 600, margin: "0 0 16px" }}>Good to know</h3>
              <OutingFacts facts={pick.facts} variant="full" bookingHref={isBookable(pick) ? bookingHref(pick) : null} onBook={() => trackBooking(pick)} />
            </div>
          )}

          <div style={{ background: "rgba(126,144,120,0.08)", border: "1px solid rgba(126,144,120,0.18)", borderRadius: 16, padding: "26px 28px", marginBottom: 40 }}>
            <h3 style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: accent, fontWeight: 600, margin: "0 0 12px" }}>Why this made the cut</h3>
            <p style={{ margin: 0, fontSize: 16, lineHeight: 1.62, color: "rgba(47,47,47,0.78)" }}>{pick.why}</p>
            {SW.ageReason(prefs, pick) && (
              <p style={{ margin: "12px 0 0", display: "inline-flex", alignItems: "center", gap: 8, fontSize: 14, fontWeight: 600, color: accent }}>
                <CheckSeal color={accent} />{SW.ageReason(prefs, pick)}
              </p>
            )}
          </div>

          <h3 style={{ font: "500 21px 'Source Serif 4', serif", color: "#2F2F2F", margin: "0 0 18px" }}>Logistics, handled</h3>
          <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 13 }}>
            {pick.logistics.map((l, i) => (
              <li key={i} style={{ display: "flex", gap: 13, alignItems: "flex-start", fontSize: 15.5, lineHeight: 1.55, color: "rgba(47,47,47,0.72)" }}>
                <span style={{ marginTop: 8, width: 6, height: 6, borderRadius: "50%", background: accent, flexShrink: 0 }} />
                {l}
              </li>
            ))}
          </ul>

          {(() => {
            const pair = (window.SLOW_WEEKEND_DATA.pairings || []).find((p) => p.anchor === pick.id);
            if (!pair) return null;
            const prox = SW.proximityLabel(pick.coord, pair.with.coord);
            return (
              <div style={{ marginTop: 36, background: "rgba(126,144,120,0.07)", border: "1px solid rgba(126,144,120,0.2)", borderRadius: 16, padding: "22px 26px" }}>
                <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 12, flexWrap: "wrap", marginBottom: 10 }}>
                  <h3 style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: accent, fontWeight: 600, margin: 0 }}>Make it a plan</h3>
                  {prox && <span style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 12.5, color: "rgba(47,47,47,0.6)", fontWeight: 600 }}><RouteDots accent={accent} />{prox}</span>}
                </div>
                <p style={{ margin: "0 0 12px", font: "500 18px 'Source Serif 4', serif", color: "#2F2F2F" }}>{pair.title}</p>
                <p style={{ margin: 0, fontSize: 14.5, lineHeight: 1.55, color: "rgba(47,47,47,0.7)" }}>
                  Pair it with <a href={planSpotHref(pair.with, pick.city)} target="_blank" rel="noopener noreferrer"
                    onClick={() => trackSpot(pair.id, pair.with.name)}
                    style={{ color: "#2F2F2F", fontWeight: 600, textDecoration: "underline", textUnderlineOffset: 2, textDecorationColor: "rgba(47,47,47,0.3)" }}>{pair.with.name} ↗</a>, {pair.with.note}
                </p>
              </div>
            );
          })()}
        </div>

        {/* Aside: the filter + actions */}
        <aside style={{ position: "sticky", top: 92, display: "flex", flexDirection: "column", gap: 22 }}>
          <div style={{ background: "#FFFFFF", borderRadius: 20, padding: "28px 26px", boxShadow: "0 18px 44px -30px rgba(47,47,47,0.4)", border: "1px solid rgba(47,47,47,0.06)" }}>
            <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 4 }}>
              <h3 style={{ font: "500 18px 'Source Serif 4', serif", color: "#2F2F2F", margin: 0 }}>The Slow Weekend Filter</h3>
            </div>
            <div style={{ display: "flex", alignItems: "baseline", gap: 7, marginBottom: 22 }}>
              <span style={{ font: "600 34px 'Source Serif 4', serif", color: "#2F2F2F" }}>{pick.calmScore}</span>
              <span style={{ fontSize: 15, color: "rgba(47,47,47,0.45)" }}>/ 10 · passed</span>
            </div>
            <FilterBreakdown pick={pick} accent={accent} />
            <p style={{ marginTop: 22, paddingTop: 18, borderTop: "1px solid rgba(47,47,47,0.08)", fontSize: 12.5, lineHeight: 1.55, color: "rgba(47,47,47,0.45)" }}>
              Two dots is calmest. We only recommend outings scoring 8 or higher.
            </p>
          </div>

          <WeatherCard pick={pick} />

          <div style={{ background: "#FFFFFF", borderRadius: 20, padding: "24px 26px", boxShadow: "0 18px 44px -30px rgba(47,47,47,0.4)", border: "1px solid rgba(47,47,47,0.06)" }}>
            <h3 style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: accent, fontWeight: 600, margin: "0 0 12px" }}>Plan your visit</h3>
            <p style={{ margin: "0 0 18px", fontSize: 14.5, lineHeight: 1.5, color: "rgba(47,47,47,0.7)", display: "flex", gap: 10, alignItems: "flex-start" }}>
              <span style={{ marginTop: 2, flexShrink: 0 }}><PinIcon color={accent} /></span>
              {pick.address}
            </p>
            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              {isBookable(pick) && (
                <a href={bookingHref(pick)} target="_blank" rel="noopener noreferrer" onClick={() => trackBooking(pick)}
                  style={{
                  display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8, textDecoration: "none",
                  borderRadius: 999, padding: "13px 18px", fontSize: 14.5, fontWeight: 600,
                  background: accent, color: "#FFFFFF", boxShadow: `0 14px 30px -18px ${accent}`,
                }}>{bookingLabel(pick)} →</a>
              )}
              {isBookable(pick) && isAffiliate(pick) && (
                <span style={{ fontSize: 11.5, color: "rgba(47,47,47,0.42)", textAlign: "center", lineHeight: 1.4 }}>
                  Affiliate link, we may earn a little if you book. It never affects which outings we pick.
                </span>
              )}
              <a href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(pick.address)}`} target="_blank" rel="noopener noreferrer"
                onClick={() => { try { if (window.SWA) SWA.track("outbound_click", { id: pick.id, title: pick.title, kind: "directions", timeOfDay: _entryLens, partner: pick.partner || null }); } catch (e) {} }}
                style={{
                display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8, textDecoration: "none",
                borderRadius: 999, padding: "12px 18px", fontSize: 14.5, fontWeight: 500,
                background: "rgba(126,144,120,0.14)", color: "#5d6b58", border: "1px solid rgba(126,144,120,0.28)",
              }}>Get directions →</a>
              {pick.sourceUrl && (
                <a href={pick.sourceUrl} target="_blank" rel="noopener noreferrer"
                  onClick={() => { try { if (window.SWA) SWA.track("outbound_click", { id: pick.id, title: pick.title, kind: "source", timeOfDay: _entryLens, partner: pick.partner || null }); } catch (e) {} }}
                  style={{
                  display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8, textDecoration: "none",
                  borderRadius: 999, padding: "12px 18px", fontSize: 14.5, fontWeight: 500,
                  background: "transparent", color: "#2F2F2F", border: "1px solid rgba(47,47,47,0.16)",
                }}>Visit official page ↗</a>
              )}
            </div>
          </div>

          <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
            <a href={makeICS(pick)} download={`${pick.id}.ics`} style={{
              textAlign: "center", textDecoration: "none", borderRadius: 999, padding: "14px 20px", fontSize: 15, fontWeight: 500,
              border: "1px solid rgba(47,47,47,0.18)", color: "#2F2F2F", background: "transparent",
            }}>Add to calendar</a>
            {onLog && (() => {
              const today = new Date().toISOString().slice(0, 10);
              const visitedToday = (memories || []).some((m) => m.id === id && m.date === today);
              return (
                <button onClick={() => (visitedToday ? (onUnlog && onUnlog(id)) : onLog(id))} style={{
                  border: "1px solid rgba(126,144,120,0.3)", cursor: "pointer", borderRadius: 999, padding: "14px 20px", font: "inherit", fontSize: 15, fontWeight: 500,
                  background: visitedToday ? "rgba(126,144,120,0.12)" : "transparent", color: "#5d6b58",
                }}>{visitedToday ? "✓ Logged today · tap to undo" : "We went →"}</button>
              );
            })()}
          </div>

          {onVote && <FeedbackVote pick={pick} value={feedback && feedback[id]} onVote={onVote} />}
        </aside>
      </div>
    </div>
  );
}

/* ---------- Calming intro ---------- */

function Intro({ onDone }) {
  React.useEffect(() => {
    const t = setTimeout(onDone, 2300);
    return () => clearTimeout(t);
  }, []);
  return (
    <div className="sw-intro" onClick={onDone} style={{
      position: "fixed", inset: 0, zIndex: 100, background: "#F7F5F2",
      display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", cursor: "pointer",
    }}>
      <span className="sw-breath" style={{ width: 56, height: 56, borderRadius: "50%", background: "radial-gradient(circle at 35% 30%, #C2A878, #7E9078)", marginBottom: 30 }} />
      <p className="sw-introtext" style={{ font: "500 clamp(26px,4vw,38px) 'Source Serif 4', serif", color: "#2F2F2F", margin: 0, letterSpacing: "-0.01em" }}>Let us make it easier</p>
      <p className="sw-introtext" style={{ fontSize: 14, letterSpacing: "0.2em", textTransform: "uppercase", color: "rgba(47,47,47,0.4)", marginTop: 16, animationDelay: "0.4s" }}>Fewer plans. Better weekends.</p>
    </div>
  );
}

/* ---------- Newsletter capture (distribution seed, PRD §8) ---------- */

function NewsletterSignup({ variant = "card", onProfile }) {
  const [email, setEmail] = React.useState("");
  const [done, setDone] = React.useState(() => { try { return !!localStorage.getItem("sw_newsletter"); } catch (e) { return false; } });
  const [err, setErr] = React.useState(false);
  const submit = (e) => {
    e.preventDefault();
    const v = email.trim();
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v)) { setErr(true); return; }
    try { localStorage.setItem("sw_newsletter", v); } catch (er) {}
    try { if (window.SWA) SWA.track("subscribe", { source: variant }); } catch (er) {}
    // Best-effort POST to the serverless endpoint (PRD §17); the UX never blocks
    // on it, so signup still "works" in static preview where the API isn't live.
    try { fetch("/api/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: v, source: variant }) }).catch(() => {}); } catch (er) {}
    setDone(true);
  };
  const compact = variant === "footer";

  if (done) {
    return (
      <div style={{ display: "inline-flex", alignItems: "center", gap: 9, fontSize: compact ? 13.5 : 15, color: "#5d6b58", fontWeight: 500 }}>
        <CheckSeal color="#7E9078" />
        You’re on the list, the next drop heads out this week.
      </div>
    );
  }

  const input = (
    <form onSubmit={submit} style={{ display: "flex", gap: 9, flexWrap: "wrap", justifyContent: compact ? "center" : "flex-start" }}>
      <input value={email} onChange={(e) => { setEmail(e.target.value); setErr(false); }} type="email" placeholder="you@email.com" aria-label="Email address"
        style={{ flex: compact ? "0 1 240px" : "1 1 240px", minWidth: 0, border: `1px solid ${err ? "#b4654a" : "rgba(47,47,47,0.18)"}`, borderRadius: 999, padding: "12px 18px", fontSize: 14.5, background: "#FFFFFF", color: "#2F2F2F", outline: "none" }} />
      <button type="submit" style={{ border: "none", cursor: "pointer", borderRadius: 999, padding: "12px 22px", fontSize: 14.5, font: "inherit", fontWeight: 500, background: "#2F2F2F", color: "#F7F5F2", whiteSpace: "nowrap" }}>
        {compact ? "Subscribe" : "Get the Thursday picks"}
      </button>
    </form>
  );

  if (compact) {
    return (
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10 }}>
        <span style={{ fontSize: 13.5, color: "rgba(47,47,47,0.6)" }}>Three picks and a collection, in your inbox each week.</span>
        {input}
      </div>
    );
  }

  return (
    <div style={{ background: "rgba(126,144,120,0.08)", border: "1px solid rgba(126,144,120,0.22)", borderRadius: 20, padding: "30px 34px", marginTop: 60 }}>
      <span style={{ fontSize: 12.5, letterSpacing: "0.14em", textTransform: "uppercase", color: "#5d6b58", fontWeight: 600 }}>The weekly drop</span>
      <h3 style={{ font: "500 clamp(22px,3vw,27px) 'Source Serif 4', serif", color: "#2F2F2F", margin: "12px 0 8px", letterSpacing: "-0.01em" }}>One email. Three picks. Every week.</h3>
      <p style={{ margin: "0 0 18px", fontSize: 15, lineHeight: 1.6, color: "rgba(47,47,47,0.62)", maxWidth: 520 }}>
        The same taste as this page, without having to open it, three outings and a collection, nothing else. No spam, unsubscribe anytime.
      </p>
      {input}
      {err && <p style={{ margin: "10px 2px 0", fontSize: 13, color: "#b4654a" }}>That doesn’t look like an email, mind checking?</p>}
    </div>
  );
}

/* ---------- Micro-outing pairings (PRD §6/§13) ---------- */

// A→B route glyph: anchor dot, dashed connector, second-beat dot. Reads as
// "from here to there," so the computed proximity beside it needs no "apart".
function RouteDots({ accent, end = "#2F2F2F", line = "rgba(47,47,47,0.35)" }) {
  return (
    <svg width="22" height="8" viewBox="0 0 22 8" fill="none" style={{ flexShrink: 0 }}>
      <circle cx="3" cy="4" r="2.5" fill={accent} />
      <line x1="6.5" y1="4" x2="15.5" y2="4" stroke={line} strokeWidth="1.4" strokeDasharray="1.5 2" strokeLinecap="round" />
      <circle cx="19" cy="4" r="2.5" fill={end} />
    </svg>
  );
}

function PairingCard({ pairing, onOpen, fixedWidth }) {
  const anchor = window.SLOW_WEEKEND_DATA.outings.find((o) => o.id === pairing.anchor);
  if (!anchor) return null;
  const accent = CAT_ACCENT[anchor.category];
  const [hover, setHover] = React.useState(false);
  const prox = SW.proximityLabel(anchor.coord, pairing.with.coord);
  const isWalk = /walk/.test(prox || "");
  const proxHeadline = prox ? (isWalk ? prox + " between" : prox + " apart") : null;
  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onClick={() => onOpen(anchor.id, { section: "pairing", pairing: pairing.id })}
      style={{ cursor: "pointer", background: "#FFFFFF", borderRadius: 16, overflow: "hidden", border: "1px solid rgba(47,47,47,0.07)",
        width: fixedWidth || "auto", flex: fixedWidth ? "0 0 auto" : undefined,
        boxShadow: hover ? "0 20px 44px -28px rgba(47,47,47,0.42)" : "0 10px 30px -28px rgba(47,47,47,0.5)", transform: hover ? "translateY(-3px)" : "none", transition: "transform .25s, box-shadow .25s" }}>
      <TextureBlock tones={anchor.tones} style={{ height: 92 }}>
        <span style={{ position: "absolute", top: 10, right: 10 }}>
          <ShareButton route={{ type: "plan", id: pairing.id }} variant="icon" label={pairing.title} />
        </span>
        {proxHeadline && (
          <span style={{ position: "absolute", left: 16, bottom: 13, display: "inline-flex", alignItems: "center", gap: 8 }}>
            <RouteDots accent="rgba(255,255,255,0.95)" end="rgba(255,255,255,0.95)" line="rgba(255,255,255,0.6)" />
            <span style={{ fontSize: 13, letterSpacing: "0.12em", textTransform: "uppercase", color: "rgba(255,255,255,0.97)", fontWeight: 700, textShadow: "0 1px 8px rgba(47,47,47,0.4)" }}>{proxHeadline}</span>
          </span>
        )}
      </TextureBlock>
      <div style={{ padding: "15px 17px 17px" }}>
        <h4 style={{ font: "500 17px/1.25 'Source Serif 4', serif", color: "#2F2F2F", margin: "0 0 9px", letterSpacing: "-0.01em" }}>{pairing.title}</h4>
        <p style={{ margin: "0 0 12px", fontSize: 13.5, lineHeight: 1.5, color: "rgba(47,47,47,0.6)" }}>{pairing.blurb}</p>
        <div style={{ display: "flex", alignItems: "center", gap: 9 }}>
          <span style={{ fontSize: 12.5, color: accent, fontWeight: 600, whiteSpace: "nowrap" }}>{anchor.title.length > 22 ? anchor.city : anchor.title}</span>
          <span style={{ fontSize: 13, color: "rgba(47,47,47,0.35)" }}>+</span>
          <a href={planSpotHref(pairing.with, anchor.city)} target="_blank" rel="noopener noreferrer"
            onClick={(e) => { e.stopPropagation(); trackSpot(pairing.id, pairing.with.name); }}
            style={{ fontSize: 12.5, color: "#2F2F2F", fontWeight: 600, textDecoration: "underline", textUnderlineOffset: 2, textDecorationColor: "rgba(47,47,47,0.3)" }}>
            {pairing.with.name} ↗
          </a>
        </div>
      </div>
    </div>
  );
}

// An honest, on-demand explainer of the engine's depth (PRD §19 "communicate
// depth"; §14 legible-not-creepy): names the live factors so the three never read
// as random or popular. Reflects what's actually active for this family.
function HowWeChoose({ tuned, whenWord, hasSaves, hasMemories }) {
  const [open, setOpen] = React.useState(false);
  const toggle = () => {
    setOpen((v) => !v);
    try { if (window.SWA && !open) SWA.track("how_we_choose_open", {}); } catch (e) {}
  };
  const factors = [
    tuned ? "your pace, from your profile" : "your pace, once you tune it in Profile",
    `${whenWord}'s weather, read from each spot's own microclimate`,
  ];
  if (hasSaves) factors.push("what you tend to save");
  if (hasMemories) factors.push("where you've been lately, so it keeps varying");
  return (
    <div style={{ margin: "2px 0 26px" }}>
      <button onClick={toggle} style={{
        border: "none", background: "none", cursor: "pointer", font: "inherit", padding: "2px 0",
        fontSize: 13, fontWeight: 600, color: "rgba(47,47,47,0.55)", display: "inline-flex", alignItems: "center", gap: 7,
      }}>How we choose these three <span style={{ fontSize: 9, opacity: 0.7 }}>{open ? "▲" : "▾"}</span></button>
      {open && (
        <div className="sw-fade" style={{ marginTop: 12, maxWidth: 560, background: "rgba(126,144,120,0.07)", border: "1px solid rgba(126,144,120,0.18)", borderRadius: 14, padding: "16px 18px" }}>
          <p style={{ margin: "0 0 10px", fontSize: 14, lineHeight: 1.55, color: "rgba(47,47,47,0.7)" }}>
            Not random, and not a popularity list. We start from every outing that cleared the Slow Weekend Filter, then tune to:
          </p>
          <ul style={{ margin: "0 0 10px", paddingLeft: 18, display: "flex", flexDirection: "column", gap: 5 }}>
            {factors.map((f, i) => <li key={i} style={{ fontSize: 13.5, lineHeight: 1.5, color: "rgba(47,47,47,0.62)" }}>{f}</li>)}
          </ul>
          <p style={{ margin: 0, fontSize: 13.5, lineHeight: 1.5, color: "rgba(47,47,47,0.5)" }}>Then we show the three that fit best, never more.</p>
        </div>
      )}
    </div>
  );
}

function MakeItAPlan({ pool, onOpen }) {
  const all = window.SLOW_WEEKEND_DATA.pairings || [];
  const list = SW.planPicks(all, pool.map((o) => o.id));
  if (list.length < 1) return null;
  return (
    <div style={{ marginTop: 60 }}>
      <h3 style={{ font: "500 clamp(22px,3vw,27px) 'Source Serif 4', serif", color: "#2F2F2F", margin: "0 0 6px", letterSpacing: "-0.01em" }}>Make it a plan</h3>
      <p style={{ margin: "0 0 22px", fontSize: 14.5, color: "rgba(47,47,47,0.5)", maxWidth: 560, lineHeight: 1.55 }}>
        Not events, plans. An outing paired with a second beat, so an hour becomes a morning.
      </p>
      <div className="sw-lane" style={{ display: "flex", gap: 16, overflowX: "auto", paddingBottom: 6, margin: "0 -4px", padding: "0 4px 6px" }}>
        {list.map((p) => <PairingCard key={p.id} pairing={p} onOpen={onOpen} fixedWidth={272} />)}
      </div>
    </div>
  );
}

/* ---------- Premium guide (PRD §6 v1) ---------- */

function GuideView({ id, onBack }) {
  const guide = (window.SLOW_WEEKEND_DATA.guides || []).find((g) => g.id === id);
  if (!guide) return null;
  const [joined, setJoined] = React.useState(() => { try { return !!localStorage.getItem("sw_guide_" + id); } catch (e) { return false; } });
  const [email, setEmail] = React.useState("");
  const [err, setErr] = React.useState(false);
  const join = (e) => {
    e.preventDefault();
    const v = email.trim();
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v)) { setErr(true); return; }
    try { localStorage.setItem("sw_guide_" + id, v); } catch (er) {}
    try { if (window.SWA) SWA.track("guide_interest", { id: id, title: guide.title }); } catch (er) {}
    setJoined(true);
  };
  return (
    <div className="sw-fade" style={{ maxWidth: 760, margin: "0 auto", padding: "28px clamp(20px,6vw,64px) 110px" }}>
      <button onClick={onBack} style={{ border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: 14.5, color: "rgba(47,47,47,0.6)", display: "inline-flex", alignItems: "center", gap: 8, padding: "6px 0", marginBottom: 26 }}>
        ← Back to collections
      </button>
      <TextureBlock tones={guide.tones} style={{ height: 180, borderRadius: 20, marginBottom: 30 }} />
      <span style={{ fontSize: 12.5, letterSpacing: "0.16em", textTransform: "uppercase", color: "#9A6238", fontWeight: 600 }}>Premium guide · in the works</span>
      <h1 style={{ font: "500 clamp(28px,4vw,40px)/1.12 'Source Serif 4', serif", letterSpacing: "-0.02em", color: "#2F2F2F", margin: "14px 0 6px" }}>{guide.title}</h1>
      <p style={{ font: "400 19px/1.4 'Source Serif 4', serif", color: "rgba(47,47,47,0.55)", margin: "0 0 22px" }}>{guide.subtitle}</p>
      <p style={{ fontSize: 16.5, lineHeight: 1.6, color: "#2F2F2F", margin: "0 0 30px" }}>{guide.blurb}</p>

      <h3 style={{ font: "500 19px 'Source Serif 4', serif", color: "#2F2F2F", margin: "0 0 16px" }}>What’s inside</h3>
      <ul style={{ listStyle: "none", padding: 0, margin: "0 0 38px", display: "flex", flexDirection: "column", gap: 12 }}>
        {guide.chapters.map((c, i) => (
          <li key={i} style={{ display: "flex", gap: 13, alignItems: "flex-start", fontSize: 15.5, lineHeight: 1.5, color: "rgba(47,47,47,0.75)" }}>
            <span style={{ flexShrink: 0, fontSize: 13, color: "#9A6238", fontWeight: 600, marginTop: 1 }}>{String(i + 1).padStart(2, "0")}</span>
            {c}
          </li>
        ))}
      </ul>

      <div style={{ background: "rgba(166,136,79,0.08)", border: "1px solid rgba(166,136,79,0.24)", borderRadius: 18, padding: "28px 30px" }}>
        {joined ? (
          <div style={{ display: "inline-flex", alignItems: "center", gap: 10, fontSize: 16, color: "#9A6238", fontWeight: 500 }}>
            <CheckSeal color="#B0764F" />
            You’re on the list, we’ll email you the moment it’s ready.
          </div>
        ) : (
          <React.Fragment>
            <h3 style={{ font: "500 22px 'Source Serif 4', serif", color: "#2F2F2F", margin: "0 0 8px" }}>Be first to read it</h3>
            <p style={{ margin: "0 0 18px", fontSize: 15, lineHeight: 1.55, color: "rgba(47,47,47,0.65)" }}>
              It’s being researched and written now. Leave your email and we’ll let you know when it’s ready, <strong style={{ color: "#2F2F2F" }}>{guide.price}</strong>, one-time, no subscription. No charge today.
            </p>
            <form onSubmit={join} style={{ display: "flex", gap: 9, flexWrap: "wrap" }}>
              <input value={email} onChange={(e) => { setEmail(e.target.value); setErr(false); }} type="email" placeholder="you@email.com" aria-label="Email address"
                style={{ flex: "1 1 240px", minWidth: 0, border: `1px solid ${err ? "#b4654a" : "rgba(47,47,47,0.18)"}`, borderRadius: 999, padding: "12px 18px", fontSize: 14.5, background: "#FFFFFF", color: "#2F2F2F", outline: "none" }} />
              <button type="submit" style={{ border: "none", cursor: "pointer", borderRadius: 999, padding: "12px 24px", fontSize: 14.5, font: "inherit", fontWeight: 500, background: "#B0764F", color: "#FFFFFF", whiteSpace: "nowrap" }}>Join the list</button>
            </form>
            {err && <p style={{ margin: "10px 2px 0", fontSize: 13, color: "#b4654a" }}>That doesn’t look like an email, mind checking?</p>}
          </React.Fragment>
        )}
      </div>
    </div>
  );
}

/* ---------- About (mission + principles, made visible) ---------- */

function AboutScreen({ onBack, onBrowse }) {
  const beliefs = [
    ["Fewer, better", "Three picks, never a feed. We make the call so your weekend isn't one more thing to manage."],
    ["Calm is the point", "Rest is part of the plan, not a reward for finishing. A quiet morning still counts. Maybe it counts most."],
    ["Honest, always", "Real places, real judgment. We keep hours and prices directional or link the venue, so we never publish a number that quietly goes stale."],
    ["Never for sale", "A spot is in your three because it's genuinely good, never because someone paid. Our taste isn't for rent."],
    ["Your data is yours", "We won't hold your family's photos or your children's data until we can protect it like a bank. Words first, kept on your device."],
    ["Every element earns its place", "If something on the screen doesn't help you decide, it's clutter, and we cut it."],
  ];
  const h2 = { font: "500 clamp(22px,3vw,28px) 'Source Serif 4', serif", letterSpacing: "-0.01em", color: "#2F2F2F", margin: "48px 0 18px" };
  const p = { fontSize: 16.5, lineHeight: 1.65, color: "rgba(47,47,47,0.72)", margin: "0 0 16px" };
  return (
    <div className="sw-fade" style={{ maxWidth: 720, margin: "0 auto", padding: "30px clamp(20px,6vw,64px) 120px" }}>
      <button onClick={onBack} style={{ border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: 14.5, color: "rgba(47,47,47,0.6)", display: "inline-flex", alignItems: "center", gap: 8, padding: "6px 0", marginBottom: 22 }}>
        ← Back
      </button>
      <TextureBlock tones={["#7E9C6B", "#AFC39A", "#5F7A50"]} style={{ height: 150, borderRadius: 20, marginBottom: 30 }} />
      <span style={{ fontSize: 13, letterSpacing: "0.22em", textTransform: "uppercase", color: "rgba(47,47,47,0.42)" }}>Why we built this</span>
      <h1 style={{ font: "500 clamp(28px,4.4vw,42px)/1.12 'Source Serif 4', serif", letterSpacing: "-0.02em", color: "#2F2F2F", margin: "14px 0 18px" }}>
        Slow Weekend is the filter we wished we had.
      </h1>
      <p style={p}>
        Weekends with little kids are short, and they get eaten by decision fatigue, endless lists, and plans that leave everyone more tired than when they started. So we built the opposite: a calm place that hands you three outings worth your energy, then gets out of the way.
      </p>
      <p style={{ ...p, font: "400 19px/1.5 'Source Serif 4', serif", color: "rgba(47,47,47,0.6)", fontStyle: "italic", margin: "22px 0 0" }}>
        We'd rather you went once and loved it than five times and didn't.
      </p>

      <h2 style={h2}>What we believe</h2>
      <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
        {beliefs.map(([title, body]) => (
          <div key={title} style={{ background: "#FFFFFF", border: "1px solid rgba(47,47,47,0.08)", borderRadius: 16, padding: "20px 22px" }}>
            <h3 style={{ font: "500 18px 'Source Serif 4', serif", color: "#2F2F2F", margin: "0 0 7px" }}>{title}</h3>
            <p style={{ margin: 0, fontSize: 14.5, lineHeight: 1.55, color: "rgba(47,47,47,0.62)" }}>{body}</p>
          </div>
        ))}
      </div>

      <h2 style={h2}>How we choose your three</h2>
      <p style={p}>
        Every outing first clears the Slow Weekend Filter, a real bar for how calm, low-friction, and forgiving it is with kids. Then, from the ones that pass, we pick the three that fit your family this week: your pace, the weather for the days you're actually looking at, the season (some places are only magic at the right time of year), what you've saved, and where you've been lately. Never random. Never a popularity contest.
      </p>

      <h2 style={h2}>Two promises</h2>
      <p style={p}>
        <strong style={{ color: "#2F2F2F", fontWeight: 600 }}>We're honest, and we're not for sale.</strong> We feature places because they're genuinely good for your family, never because anyone paid us. When we don't know something for sure, we say so or link you to the source.
      </p>
      <p style={p}>
        <strong style={{ color: "#2F2F2F", fontWeight: 600 }}>We treat your family's data like a bank would, or we don't hold it.</strong> No children's photos, no quiet data collection. The personal parts of Slow Weekend live on your own device until we can protect them to that standard.
      </p>

      <p style={{ ...p, marginTop: 36 }}>Built by parents, for parents. Thank you for spending a slow weekend with us.</p>
      <button onClick={onBrowse} style={{ marginTop: 14, border: "none", cursor: "pointer", borderRadius: 999, padding: "14px 28px", fontSize: 15, font: "inherit", fontWeight: 500, background: "#2F2F2F", color: "#F7F5F2" }}>
        See this week's three →
      </button>
    </div>
  );
}

/* ---------- App ---------- */

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  // A fresh line from the pool on each page load (stable across in-session nav).
  const [quote] = React.useState(() => {
    const q = (window.SLOW_WEEKEND_DATA.quotes || []);
    return q.length ? q[Math.floor(Math.random() * q.length)] : "A good weekend is one you don’t need to recover from.";
  });
  // A shared link carries its target as a query param (?outing= / ?plan= /
  // ?collection= / ?week=); hydrate the nav stack from it on first load so the
  // recipient lands on the exact view (PRD §8). Unknown ids degrade to home.
  const [stack, setStack] = React.useState(() => {
    try {
      // Insights is the founder-facing metrics read-back, not for ordinary
      // visitors — no public link (it confused beta users). Reach it directly via
      // an obscure ?insights=1 URL (bookmark it).
      if (new URLSearchParams(location.search).has("insights")) return [{ type: "home" }, { type: "insights" }];
      const s = SW.parseShareParams(location.search, window.SLOW_WEEKEND_DATA);
      if (s) return s;
    } catch (e) {}
    return [{ type: "home" }];
  });
  React.useEffect(() => {
    try {
      const s = SW.parseShareParams(location.search, window.SLOW_WEEKEND_DATA);
      const last = s && s[s.length - 1];
      if (!last || !window.SWA) return;
      const map = last.type === "detail" ? { type: "outing", id: last.id }
        : last.type === "collection" ? { type: "collection", id: last.id }
        : last.picks ? { type: "picks", id: last.window || "weekend" }
        : last.window ? { type: "week", id: last.window } : null;
      if (map) SWA.track("share_open", map);
    } catch (e) {}
  }, []);
  const [saved, toggleSave] = useSaved();
  const [reflections, setReflection] = useReflections();
  const [feedback, setFeedbackEntry] = useFeedback();
  const [memories, addMemory, removeMemoryAt, setMemoryDate] = useMemories();

  // Instrumentation: real behavioral data only. Sample data is opt-in for a
  // demo (?demo=1) so the live dashboard reflects actual visitors, not fixtures.
  React.useEffect(() => {
    try {
      var demo = false;
      try { demo = new URLSearchParams(location.search).has("demo"); } catch (e) {}
      if (demo && window.SWA) SWA.seedDemo();
      if (demo && memories.length === 0) {
        [["edgewood", "2026-06-13"], ["hiddenvilla", "2026-05-25"], ["hiddenvilla", "2026-05-04"]].forEach(function (m) { addMemory(m[0], m[1]); });
      }
    } catch (e) {}
  }, []);
  const outingById = (id) => window.SLOW_WEEKEND_DATA.outings.find((o) => o.id === id);
  const logMemory = (id) => {
    try { const o = outingById(id); if (window.SWA && o) SWA.track("memory_add", { id: id, title: o.title, category: o.category }); } catch (e) {}
    addMemory(id);
  };
  // Undo an accidental "we went" — removes today's entry for this outing.
  const unlogMemory = (id) => {
    const today = new Date().toISOString().slice(0, 10);
    const i = memories.findIndex((m) => m.id === id && m.date === today);
    if (i >= 0) removeMemoryAt(i);
  };
  const openDetail = (id, ctx) => {
    try {
      const o = outingById(id);
      if (window.SWA && o) SWA.track("recommendation_click", Object.assign({ id: id, title: o.title, category: o.category, texture: o.texture, ageRange: o.ageRange, energyLevel: o.energyLevel, partner: o.partner || null }, ctx || {}));
    } catch (e) {}
    // Remember the lens this detail was entered from, so its outbound "Go" carries
    // it (the kill-metric). "all"/absent means no lens entry.
    const band = ctx && ctx.timeOfDay;
    _entryLens = band && band !== "all" ? band : null;
    setOpens((n) => n + 1);
    push({ type: "detail", id });
  };
  const trackedToggleSave = (id) => {
    try {
      const o = outingById(id); const willSave = !saved.includes(id);
      if (window.SWA && o) SWA.track("save_toggle", { id: id, title: o.title, category: o.category, saved: willSave, standing: SW.isStanding(o), partner: o.partner || null });
    } catch (e) {}
    toggleSave(id);
  };
  // Frictionless entry (PRD §18): a cold visitor (e.g. a curiosity click from a
  // shared link) lands straight in the picks after the calming intro — no upfront
  // questionnaire. We only *offer* tuning once they've tasted it (opened a pick
  // and come back), via a quiet, dismissable hook. Onboarding is never auto-shown;
  // it opens only when explicitly launched (the hook, or "replay" from Profile).
  const [onboardedInit] = React.useState(() => localStorage.getItem("sw_onboarded") === "1");
  const [intro, setIntro] = React.useState(t.intro);
  const [onboarded, setOnboarded] = React.useState(onboardedInit);
  const [onboardingOpen, setOnboardingOpen] = React.useState(false);
  const [opens, setOpens] = React.useState(0);
  const [tuneDismissed, setTuneDismissed] = React.useState(() => localStorage.getItem("sw_tune_dismissed") === "1");
  const [prefs, setPrefs] = React.useState(() => readPrefs());

  // Home filter state lives here so it survives navigation (Home unmounts on every
  // detail/tab visit; local state reset = the filter-persistence bug). First-load
  // values come from the calendar + clock (SW.contextualDefaults), unless a shared
  // ?week=/?picks= link pins the window.
  const WINDOW_LABEL = { today: "Today", week: "This Week", weekend: "This Weekend" };
  const [homeFilters, setHomeFilters] = React.useState(() => {
    const ctx = SW.contextualDefaults(new Date());
    let shareWindow = null;
    try {
      const s0 = SW.parseShareParams(location.search, window.SLOW_WEEKEND_DATA);
      const hr = s0 && s0.find((r) => r.type === "home" && r.window);
      if (hr) shareWindow = hr.window;
    } catch (e) {}
    const p = readPrefs();
    return {
      time: WINDOW_LABEL[shareWindow || ctx.window] || "This Weekend",
      timeOfDay: ctx.timeOfDay,
      natureOnly: false,
      energyLevels: [],
      regions: (p.regions && p.regions.length) ? p.regions : [],
    };
  });
  const setHF = (k) => (v) => setHomeFilters((f) => ({ ...f, [k]: typeof v === "function" ? v(f[k]) : v }));
  // Profile owns regions ("Where you wander"); sync the Home location control when
  // it changes (the Home control is a transient session override otherwise).
  React.useEffect(() => {
    if (prefs.regions && prefs.regions.length) setHomeFilters((f) => ({ ...f, regions: prefs.regions }));
  }, [JSON.stringify(prefs.regions)]);
  const homeState = {
    time: homeFilters.time, setTime: setHF("time"),
    timeOfDay: homeFilters.timeOfDay, setTimeOfDay: setHF("timeOfDay"),
    natureOnly: homeFilters.natureOnly, setNatureOnly: setHF("natureOnly"),
    energyLevels: homeFilters.energyLevels, setEnergyLevels: setHF("energyLevels"),
    regions: homeFilters.regions, setRegions: setHF("regions"),
  };

  // Navigation is a stack synced to the browser History API, so the device/
  // browser Back button (and gestures) walk the app's screens. `nav` sets the
  // stack and pushes a history entry; the popstate listener restores the stack
  // the browser hands back.
  const nav = (next) => { setStack(next); try { window.history.pushState({ swStack: next }, ""); } catch (e) {} window.scrollTo({ top: 0 }); };
  React.useEffect(() => {
    try { window.history.replaceState({ swStack: stack }, ""); } catch (e) {}
    const onPop = (e) => { setStack((e.state && e.state.swStack) ? e.state.swStack : [{ type: "home" }]); window.scrollTo({ top: 0 }); };
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  const replayOnboarding = () => { setIntro(false); setOnboardingOpen(true); nav([{ type: "home" }]); };
  const finishOnboarding = (p) => { setPrefs(p); setOnboarded(true); setOnboardingOpen(false); setTuneDismissed(true); window.scrollTo({ top: 0 }); };
  const savePrefs = (p) => { SW.writePrefs(p); setPrefs(p); };
  const dismissTune = () => { setTuneDismissed(true); try { localStorage.setItem("sw_tune_dismissed", "1"); } catch (e) {} };
  // The deferred hook: offered after the aha (one opened pick), never to someone
  // who's already tuned or dismissed it.
  const showTuneHook = !onboarded && !tuneDismissed && opens >= 1;

  const cur = stack[stack.length - 1];
  const prev = stack[stack.length - 2];
  const push = (route) => nav([...stack, route]);
  const back = () => { if (stack.length > 1) window.history.back(); };
  const goHome = () => nav([{ type: "home" }]);
  const goCollections = () => nav([{ type: "home" }, { type: "collections" }]);
  const goSaved = () => nav([{ type: "home" }, { type: "saved" }]);
  const goMemories = () => nav([{ type: "home" }, { type: "memories" }]);
  const goProfile = () => nav([{ type: "home" }, { type: "profile" }]);

  const baseOf = (ty) => (ty === "collections" || ty === "collection" || ty === "guide") ? "collections"
    : ty === "saved" ? "saved" : ty === "memories" ? "memories" : ty === "profile" ? "profile" : "home";
  const active = (cur.type === "detail" && prev) ? baseOf(prev.type) : baseOf(cur.type);

  React.useEffect(() => { try { if (window.SWA) SWA.track("screen_view", { screen: cur.type, id: cur.id }); } catch (e) {} }, [cur.type, cur.id]);
  const goAbout = () => nav([{ type: "home" }, { type: "about" }]);

  let screen;
  if (cur.type === "home") {
    screen = <Home tweaks={t} prefs={prefs} onOpen={openDetail} onProfile={goProfile} saved={saved} memories={memories} onLog={logMemory} onToggleSave={trackedToggleSave} homeState={homeState} initialPicks={cur.picks} tuneHook={showTuneHook ? { onTune: () => setOnboardingOpen(true), onDismiss: dismissTune } : null} />;
  } else if (cur.type === "collections") {
    screen = <CollectionsIndex onOpenCollection={(id) => push({ type: "collection", id })} onOpenOuting={(id, ctx) => openDetail(id, ctx || { section: "plans" })} onOpenGuide={(id) => { try { if (window.SWA) SWA.track("guide_view", { id }); } catch (e) {} push({ type: "guide", id }); }} />;
  } else if (cur.type === "collection") {
    screen = <CollectionView id={cur.id} onBack={back} onOpenOuting={(id) => openDetail(id, { section: "collection", window: cur.id })} />;
  } else if (cur.type === "guide") {
    screen = <GuideView id={cur.id} onBack={back} />;
  } else if (cur.type === "memories") {
    screen = <MemoriesScreen memories={memories} reflections={reflections} onOpen={(id) => openDetail(id, { section: "memories" })} onReflect={setReflection} onRemove={removeMemoryAt} onSetDate={setMemoryDate} onBrowse={goHome} />;
  } else if (cur.type === "saved") {
    screen = <SavedScreen saved={saved} onOpen={(id) => openDetail(id, { section: "saved" })}
      onRemove={trackedToggleSave} onLog={logMemory} memories={memories} onBrowse={goHome} />;
  } else if (cur.type === "profile") {
    screen = <FamilyProfile prefs={prefs} onSave={savePrefs} onReplay={replayOnboarding} />;
  } else if (cur.type === "insights") {
    screen = <InsightsView onBack={goHome} />;
  } else if (cur.type === "about") {
    screen = <AboutScreen onBack={back} onBrowse={goHome} />;
  } else {
    const fromCollection = prev && prev.type === "collection";
    screen = <Detail id={cur.id} onBack={back} backLabel={fromCollection ? "← Back to collection" : "← Back to this week"} saved={saved} toggleSave={trackedToggleSave} feedback={feedback} onVote={setFeedbackEntry} memories={memories} onLog={logMemory} onUnlog={unlogMemory} prefs={prefs} />;
  }

  const _sg = SW.savedGroups(saved, memories, window.SLOW_WEEKEND_DATA.outings);
  const savedCount = _sg.week.length + _sg.shortlist.length;
  return (
    <div style={{ minHeight: "100vh" }}>
      {intro && t.intro && <Intro onDone={() => setIntro(false)} />}
      {onboardingOpen && <Onboarding onDone={finishOnboarding} minimal={t.onboardStyle === "minimal"} depth={t.onboardDepth} />}
      <NavBar active={active} onHome={goHome} onCollections={goCollections} onSaved={goSaved} onMemories={goMemories} onProfile={goProfile} savedCount={savedCount} />
      {screen}

      <footer style={{ textAlign: "center", padding: "0 24px 64px", color: "rgba(47,47,47,0.4)", fontSize: 13.5 }}>
        {cur.type !== "home" && (
          <div style={{ maxWidth: 460, margin: "0 auto 30px" }}>
            <NewsletterSignup variant="footer" />
          </div>
        )}
        <p style={{ font: "400 18px 'Source Serif 4', serif", color: "rgba(47,47,47,0.55)", marginBottom: 6 }}>{quote}</p>
        <span>The Slow Weekend Family</span>
        <span style={{ margin: "0 8px", opacity: 0.4 }}>·</span>
        <button onClick={goAbout} style={{ border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: 13.5, color: "rgba(47,47,47,0.4)", textDecoration: "underline", textUnderlineOffset: 3 }}>About</button>
      </footer>

      <MobileTabBar active={active} onHome={goHome} onCollections={goCollections} onSaved={goSaved} onMemories={goMemories} savedCount={savedCount} />

      <TweaksPanel>
        <TweakSection label="Recommendation card" />
        <TweakRadio label="Card design" value={t.cardStyle} options={["editorial", "quiet", "panel"]}
          onChange={(v) => setTweak("cardStyle", v)} />
        <TweakSection label="Slow Weekend Filter" />
        <TweakRadio label="Score on cards" value={t.scoreStyle} options={["single", "dots", "ring"]}
          onChange={(v) => setTweak("scoreStyle", v)} />
        <TweakSection label="Motion" />
        <TweakToggle label="Calming intro on load" value={t.intro} onChange={(v) => setTweak("intro", v)} />
        <TweakSection label="Onboarding" />
        <TweakRadio label="Question copy" value={t.onboardStyle} options={["guided", "minimal"]}
          onChange={(v) => setTweak("onboardStyle", v)} />
        <TweakRadio label="Depth" value={t.onboardDepth} options={["minimal", "standard", "full"]}
          onChange={(v) => setTweak("onboardDepth", v)} />
        <TweakButton label="Replay onboarding" onClick={replayOnboarding} />
        <TweakSection label="Also worth your time" />
        <TweakSection label="Personalized picks" />
        <TweakRadio label="Payoff on home" value={t.personalization} options={["banner", "inline", "off"]}
          onChange={(v) => setTweak("personalization", v)} />
        <TweakSection label="Time-of-day lens" />
        <TweakRadio label="5–8pm lens" value={t.timeLens} options={["shown", "hidden"]}
          onChange={(v) => setTweak("timeLens", v)} />
        <TweakRadio label="Context freshness" value={t.freshness} options={["shown", "hidden"]}
          onChange={(v) => setTweak("freshness", v)} />
        <TweakRadio label="Chosen-from-N line" value={t.trustLine} options={["shown", "hidden"]}
          onChange={(v) => setTweak("trustLine", v)} />
        <TweakRadio label="How we choose" value={t.howWeChoose} options={["shown", "hidden"]}
          onChange={(v) => setTweak("howWeChoose", v)} />
      </TweaksPanel>
    </div>
  );
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "cardStyle": "editorial",
  "scoreStyle": "single",
  "intro": true,
  "onboardStyle": "guided",
  "onboardDepth": "standard",
  "personalization": "banner",
  "moreLayout": "grid",
  "timeLens": "shown",
  "freshness": "shown",
  "trustLine": "shown",
  "howWeChoose": "shown"
}/*EDITMODE-END*/;

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