// app.jsx — CHATONA Season X

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

const SUPABASE_URL = "https://whfrqyaohtexitvyjzrw.supabase.co";
const SUPABASE_KEY = "sb_publishable_3cKYXrmwYMJaYkr0fcS4jg_H_9ORUmb";
const db = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);

// ---------- Crew ----------
// Phone numbers are not stored client-side. Login is verified via the
// `match_phone` Postgres RPC, which compares a SHA-256 hash of the entered
// digits against rows in `crew_phones` (RLS deny-all, function is SECURITY DEFINER).
const CREW = [
  { id: "pat",     name: "Pat",     color: "#e8621a", img: "assets/avatars/pat.jpg" },
  { id: "luisa",   name: "Luisa",   color: "#f0a830", img: "assets/avatars/luisa.jpg" },
  { id: "feo",     name: "Feo",     color: "#d4506a", img: "assets/avatars/feo.jpg" },
  { id: "adam",    name: "Adam",    color: "#3a8fa0", img: "assets/avatars/adam.jpg" },
  { id: "howard",  name: "Howard",  color: "#4a8a3a", img: "assets/avatars/howard.jpg" },
  { id: "tori",    name: "Tori",    color: "#e8621a", img: "assets/avatars/tori.jpg" },
  { id: "eddie",   name: "Eddie",   color: "#f0a830", img: "assets/avatars/eddie.jpg" },
  { id: "amanda",  name: "Amanda",  color: "#d4506a", img: "assets/avatars/amanda.jpg" },
  { id: "danica",  name: "Danica",  color: "#3a8fa0", img: "assets/avatars/danica.jpg" },
  { id: "ian",     name: "Ian",     color: "#4a8a3a", img: "assets/avatars/ian.jpg" },
  { id: "joey",    name: "Joey",    color: "#e8621a", img: "assets/avatars/joey.jpg" },
  { id: "aaron",   name: "Aaron",   color: "#f0a830", img: "assets/avatars/aaron.jpg" },
  { id: "matt",    name: "Matt",    color: "#d4506a", img: "assets/avatars/matt.jpg" },
];

const findCrew = (id) => CREW.find(c => c.id === id);
const initials = (name) => name.split(/\s+/).map(s => s[0]).join("").slice(0, 2).toUpperCase();

// Normalize a phone string to digits only. Strips spaces, dashes, parens, +.
const normalizePhone = (raw) => (raw || "").replace(/\D/g, "");
const formatPhoneDisplay = (digits) => {
  const d = normalizePhone(digits);
  const last10 = d.slice(-10);
  if (last10.length < 4) return last10;
  if (last10.length < 7) return `(${last10.slice(0,3)}) ${last10.slice(3)}`;
  return `(${last10.slice(0,3)}) ${last10.slice(3,6)}-${last10.slice(6,10)}`;
};

// ---------- Episodes ----------
const EPISODES_INITIAL = [
  {
    id: "ep1",
    num: "01",
    title: "Opening Weekend",
    dates: "Jun 26 – 28, 2026",
    startDate: new Date("2026-06-26T15:00:00-04:00"),
    color: "orange",
    bgClass: "btn-orange",
    accent: "#e8621a",
    going: [],
  },
  {
    id: "ep2",
    num: "02",
    title: "Beerlympics",
    dates: "Aug 14 – 16, 2026",
    startDate: new Date("2026-08-14T15:00:00-04:00"),
    color: "teal",
    bgClass: "btn-teal",
    accent: "#3a8fa0",
    going: [],
  },
  {
    id: "ep3",
    num: "03",
    title: "The Finale",
    dates: "Sep 11 – 13, 2026",
    startDate: new Date("2026-09-11T15:00:00-04:00"),
    color: "coral",
    bgClass: "btn-coral",
    accent: "#d4506a",
    going: [],
  },
];

// ---------- Avatar ----------
const Avatar = ({ id, name, size = 48, fontSize, borderWidth = 2 }) => {
  const person = id ? findCrew(id) : { name, color: "#cccccc", img: null };
  if (!person) return null;
  if (person.img) {
    return (
      <span
        className="avatar avatar-img"
        style={{
          width: size, height: size,
          borderWidth,
          backgroundImage: `url("${person.img}")`,
        }}
        title={person.name}
        aria-label={person.name}
      />
    );
  }
  return (
    <span
      className="avatar"
      style={{
        width: size, height: size,
        background: person.color,
        fontSize: fontSize || Math.round(size * 0.36),
        borderWidth,
      }}
      title={person.name}
    >
      {initials(person.name)}
    </span>
  );
};

// ---------- Countdown ----------
const useCountdown = (target) => {
  const [now, setNow] = useState(() => new Date());
  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  const diff = Math.max(0, target.getTime() - now.getTime());
  const totalSec = Math.floor(diff / 1000);
  const days = Math.floor(totalSec / 86400);
  const hours = Math.floor((totalSec % 86400) / 3600);
  const minutes = Math.floor((totalSec % 3600) / 60);
  const seconds = totalSec % 60;
  return { days, hours, minutes, seconds, isLive: diff === 0 };
};

const pad = (n, w = 2) => String(n).padStart(w, "0");

const Countdown = ({ target }) => {
  const { days, hours, minutes, seconds } = useCountdown(target);
  const cells = [
    { num: pad(days, 3), lbl: "Days",    cls: "orange" },
    { num: pad(hours),    lbl: "Hours",   cls: "amber" },
    { num: pad(minutes),  lbl: "Minutes", cls: "coral" },
    { num: pad(seconds),  lbl: "Seconds", cls: "teal" },
  ];
  return (
    <div className="countdown">
      {cells.map((c) => (
        <div key={c.lbl} className={`cell ${c.cls}`}>
          <div className="corner" />
          <div className="num">{c.num}</div>
          <div className="lbl">{c.lbl}</div>
        </div>
      ))}
    </div>
  );
};

// ---------- Login ----------
const LoginScreen = ({ onLogin }) => {
  const [picked, setPicked] = useState(null); // crew member object, or null
  const [phone, setPhone] = useState("");
  const [error, setError] = useState(null);
  const [shake, setShake] = useState(false);
  const inputRef = useRef(null);

  useEffect(() => {
    if (picked && inputRef.current) inputRef.current.focus();
  }, [picked]);

  const back = () => {
    setPicked(null);
    setPhone("");
    setError(null);
  };

  const onChange = (e) => {
    const digits = normalizePhone(e.target.value).slice(0, 11);
    setPhone(digits);
    if (error) setError(null);
  };

  const [checking, setChecking] = useState(false);

  const submit = async (e) => {
    e.preventDefault();
    if (!picked || checking) return;
    const digits = normalizePhone(phone);
    if (digits.length < 10) {
      setError("Enter all 10 digits.");
      setShake(true); setTimeout(() => setShake(false), 400);
      return;
    }
    setChecking(true);
    const { data: matchedId, error: rpcErr } = await db.rpc("match_phone", { input: digits });
    setChecking(false);
    if (rpcErr) {
      setError("Couldn't verify — try again.");
      setShake(true); setTimeout(() => setShake(false), 400);
      return;
    }
    if (matchedId === picked.id) {
      onLogin(picked.id);
    } else {
      setError(`That number doesn't match ${picked.name}'s.`);
      setShake(true); setTimeout(() => setShake(false), 400);
    }
  };

  return (
    <div className="login-page">
      <div className="login-header">
        <div className="row">
          <div>Private · Crew only</div>
          <div>Est. 2017 · Beaver Lake, ON</div>
          <div>Season X · 2026</div>
        </div>
      </div>

      <div className="poster-wrap" style={{ marginTop: 28 }}>
        <img className="poster" src="assets/chatona-poster.jpg" alt="Chatona Season X — Kearney, Ontario 2026" />
      </div>

      <div className="who-block">
        {!picked && (
          <React.Fragment>
            <h3>Who's there?</h3>
            <div className="sub">Tap your face to enter the cottage</div>

            <div className="avatar-grid">
              {CREW.map((c) => (
                <button
                  key={c.id}
                  className="pick"
                  onClick={() => setPicked(c)}
                >
                  <Avatar id={c.id} size={96} borderWidth={3} />
                  <span className="name">{c.name}</span>
                </button>
              ))}
            </div>
          </React.Fragment>
        )}

        {picked && (
          <div className="phone-confirm">
            <button className="back-link" onClick={back}>← Not me</button>

            <div className="picked-card">
              <Avatar id={picked.id} size={120} borderWidth={4} />
              <h3 style={{ marginTop: 18 }}>Hey {picked.name}.</h3>
              <div className="sub">Prove it — punch in your number</div>
            </div>

            <form
              className={`phone-form ${shake ? "shake" : ""}`}
              onSubmit={submit}
              noValidate
            >
              <div className="phone-input-row">
                <span className="phone-cc">+1</span>
                <input
                  ref={inputRef}
                  className="phone-input"
                  type="tel"
                  inputMode="numeric"
                  autoComplete="tel"
                  placeholder="(416) 555 — 1234"
                  value={formatPhoneDisplay(phone)}
                  onChange={onChange}
                  maxLength={16}
                  aria-label="Phone number"
                />
              </div>
              <button type="submit" className="btn btn-orange phone-submit" disabled={checking}>
                {checking ? "Checking…" : "Enter the cottage →"}
              </button>
              <div className={`phone-error ${error ? "show" : ""}`}>
                {error || "\u00A0"}
              </div>
            </form>
          </div>
        )}
      </div>
    </div>
  );
};

// ---------- Weather ----------
// Open-Meteo: free, no API key. Kearney, ON ≈ 45.55°N, 79.18°W.
const KEARNEY = { lat: 45.55, lon: -79.18 };

// Kearney/Muskoka climate normals (°C). Monthly average daily max/min.
// Source: Environment Canada Huntsville/Muskoka region normals.
const AIR_NORMALS = [
  { max: -4,  min: -15 }, // Jan
  { max: -3,  min: -14 }, // Feb
  { max: 3,   min: -9  }, // Mar
  { max: 11,  min: -1  }, // Apr
  { max: 18,  min: 5   }, // May
  { max: 23,  min: 10  }, // Jun
  { max: 26,  min: 13  }, // Jul
  { max: 25,  min: 12  }, // Aug
  { max: 20,  min: 8   }, // Sep
  { max: 12,  min: 3   }, // Oct
  { max: 4,   min: -3  }, // Nov
  { max: -2,  min: -11 }, // Dec
];

const interpMonthly = (date, getValue) => {
  const d = new Date(date);
  const month = d.getMonth();
  const day = d.getDate();
  const daysInMonth = new Date(d.getFullYear(), month + 1, 0).getDate();
  const frac = (day - 1) / daysInMonth;
  const cur = getValue(month);
  const next = getValue((month + 1) % 12);
  const prev = getValue((month + 11) % 12);
  return frac < 0.5
    ? prev + (cur - prev) * (frac + 0.5)
    : cur + (next - cur) * (frac - 0.5);
};

const estimateAirTemp = (date = new Date()) => ({
  tmax: Math.round(interpMonthly(date, m => AIR_NORMALS[m].max)),
  tmin: Math.round(interpMonthly(date, m => AIR_NORMALS[m].min)),
});

// Beaver Lake (Kearney, ON) surface temp estimate by month (°C).
// Based on typical Muskoka/Parry Sound region lake surface temps — small
// shield lakes follow this curve closely year to year.
const LAKE_TEMP_BY_MONTH = [1, 1, 2, 6, 13, 18, 22, 22, 18, 12, 6, 2];

const estimateLakeTemp = (date = new Date()) =>
  Math.round(interpMonthly(date, m => LAKE_TEMP_BY_MONTH[m]));

// WMO weather code → { emoji, label }
const wmoMap = (code) => {
  if (code === 0) return { emoji: "☀️", label: "Clear" };
  if (code === 1) return { emoji: "🌤", label: "Mostly clear" };
  if (code === 2) return { emoji: "⛅", label: "Partly cloudy" };
  if (code === 3) return { emoji: "☁️", label: "Overcast" };
  if (code === 45 || code === 48) return { emoji: "🌫", label: "Foggy" };
  if (code >= 51 && code <= 57) return { emoji: "🌦", label: "Drizzle" };
  if (code >= 61 && code <= 67) return { emoji: "🌧", label: "Rain" };
  if (code >= 71 && code <= 77) return { emoji: "🌨", label: "Snow" };
  if (code >= 80 && code <= 82) return { emoji: "🌧", label: "Showers" };
  if (code >= 85 && code <= 86) return { emoji: "🌨", label: "Snow showers" };
  if (code >= 95) return { emoji: "⛈", label: "Thunderstorm" };
  return { emoji: "🌡", label: "—" };
};

const useWeather = () => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(false);
  useEffect(() => {
    const url = `https://api.open-meteo.com/v1/forecast?latitude=${KEARNEY.lat}&longitude=${KEARNEY.lon}&current=temperature_2m,weather_code,apparent_temperature&daily=temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max&timezone=America/Toronto&forecast_days=16`;
    fetch(url)
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(setData)
      .catch(() => setError(true));
  }, []);
  return { data, error };
};

// Build a lookup of YYYY-MM-DD → daily forecast
const dailyByDate = (data) => {
  if (!data?.daily?.time) return {};
  const out = {};
  data.daily.time.forEach((d, i) => {
    out[d] = {
      tmax: Math.round(data.daily.temperature_2m_max[i]),
      tmin: Math.round(data.daily.temperature_2m_min[i]),
      code: data.daily.weather_code[i],
      pop: data.daily.precipitation_probability_max[i],
    };
  });
  return out;
};

const isoLocalDate = (d) => {
  // Format Date as YYYY-MM-DD in America/Toronto local terms
  const fmt = new Intl.DateTimeFormat("en-CA", {
    timeZone: "America/Toronto", year: "numeric", month: "2-digit", day: "2-digit",
  });
  return fmt.format(d);
};

const WeatherChip = ({ weather, episodes }) => {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    if (!open) return;
    const onDown = (e) => {
      if (ref.current && !ref.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [open]);

  if (weather.error) return null;
  if (!weather.data) {
    return <div className="weather-chip loading" aria-label="Loading weather">…</div>;
  }

  const cur = weather.data.current;
  const curInfo = wmoMap(cur.weather_code);
  const tempNow = Math.round(cur.temperature_2m);
  const feels = Math.round(cur.apparent_temperature);
  const byDate = dailyByDate(weather.data);

  return (
    <div className="weather-chip-wrap" ref={ref}>
      <button
        className="weather-chip"
        onClick={() => setOpen(o => !o)}
        aria-label={`Weather in Kearney: ${tempNow} degrees, ${curInfo.label}`}
      >
        <span className="w-emoji">{curInfo.emoji}</span>
        <span className="w-temp">{tempNow}°</span>
        <span className="w-label">{curInfo.label}</span>
      </button>
      {open && (
        <div className="weather-pop">
          <div className="weather-pop-head">
            <div className="w-pop-loc">Kearney, ON</div>
            <div className="w-pop-now">
              <span className="w-pop-emoji">{curInfo.emoji}</span>
              <span className="w-pop-temp">{tempNow}°C</span>
            </div>
            <div className="w-pop-meta">
              {curInfo.label} · feels {feels}°
            </div>
            <div className="w-pop-lake">
              <span className="w-pop-lake-emoji">🦦</span>
              <span>Lake ~{estimateLakeTemp()}°C</span>
            </div>
          </div>
          <div className="weather-pop-divider" />
          <div className="w-pop-ep-label">Episode forecasts</div>
          <div className="w-pop-eps">
            {episodes.map(ep => {
              const dateKey = isoLocalDate(ep.startDate);
              const f = byDate[dateKey];
              return (
                <div key={ep.id} className="w-pop-ep">
                  <div className="w-pop-ep-name">
                    <span className="w-pop-ep-num">EP·{ep.num}</span>
                    <span>{ep.title}</span>
                  </div>
                  <div className="w-pop-ep-fc">
                    {f ? (
                      <div className="w-pop-ep-air">
                        <span className="w-pop-ep-emoji">{wmoMap(f.code).emoji}</span>
                        <span className="w-pop-ep-temp">{f.tmax}°</span>
                        {f.pop != null && <span className="w-pop-ep-pop">{f.pop}% 💧</span>}
                      </div>
                    ) : (() => {
                      const est = estimateAirTemp(ep.startDate);
                      return (
                        <div className="w-pop-ep-air typical">
                          <span className="w-pop-ep-typical">Typical</span>
                          <span className="w-pop-ep-temp">~{est.tmax}°</span>
                        </div>
                      );
                    })()}
                    <span className="w-pop-ep-lake">
                      <span className="w-pop-ep-lake-label">🦦 Lake</span>
                      <span className="w-pop-ep-lake-temp">~{estimateLakeTemp(ep.startDate)}°</span>
                    </span>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
};

// ---------- TopNav ----------
const TopNav = ({ userId, onLogout, onNav, onBrand, active, weather, episodes }) => {
  return (
    <div className="topnav">
      <div className="topnav-inner">
        <div className="brand" onClick={onBrand} style={{ cursor: "pointer" }}>
          Chatona<span className="tick">S·X</span>
        </div>
        <nav className="nav-links">
          {[
            ["hero", "Cottage"],
            ["episodes", "Episodes"],
            ["photos", "Photos"],
            ["merch", "Merch"],
          ].map(([k, l]) => (
            <a
              key={k}
              href={`#${k}`}
              className={`nav-link ${active === k ? "active" : ""}`}
              onClick={() => onNav(k)}
            >{l}</a>
          ))}
        </nav>
        <WeatherChip weather={weather} episodes={episodes} />
        <button className="user-chip" onClick={onLogout}>
          <Avatar id={userId} size={32} borderWidth={2} />
          <span>{findCrew(userId)?.name}</span>
          <span className="logout">EXIT</span>
        </button>
      </div>
    </div>
  );
};

// ---------- Hero ----------
const Hero = () => {
  const ep1 = EPISODES_INITIAL[0].startDate;
  return (
    <section id="hero" className="hero">
      <div className="container">
        <div className="hero-band">
          <div>★ Chatona · Year Ten</div>
          <div>Episode 01 incoming ★</div>
        </div>

        <div className="hero-wordmark">
          <div className="poster-stars">★ ★ ★ &nbsp; A CHATONA PRODUCTION &nbsp; ★ ★ ★</div>
          <h1 className="word" style={{ marginTop: 12 }}>Season X</h1>
          <div className="meta-row">
            <span>Beaver Lake</span>
            <span className="pip" />
            <span>Kearney, ON</span>
            <span className="pip" />
            <span>MMXXVI</span>
          </div>
        </div>

        <div className="hero-inline-row" style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          fontFamily: "JetBrains Mono, monospace",
          fontSize: 12,
          letterSpacing: "0.25em",
          textTransform: "uppercase",
          marginTop: 32,
        }}>
          <div>Hot Tub Starts Heating → Jun 26, 2026</div>
          <div>No Refunds</div>
        </div>
        <Countdown target={ep1} />
        <div className="countdown-caption">
          <div>★ Episode 01 — Opening Weekend ★</div>
          <div>BBQ ready. Bunkee swept. Beer cold.</div>
        </div>
      </div>

      <div style={{ borderTop: "4px solid var(--ink)" }}>
        <div className="hero-horizon" />
      </div>
    </section>
  );
};

// ---------- Ticket / RSVP ----------
const Ticket = ({ inIt, onClick, label }) => (
  <button className={`ticket ${inIt ? "in" : ""}`} onClick={onClick}>
    <span className="stub">ADMIT<br/>ONE</span>
    <span className="main">
      {inIt ? "✓ You're in" : "I'm in →"}
    </span>
  </button>
);

// ---------- Episodes ----------
const Episodes = ({ episodes, userId, onToggle }) => {
  return (
    <section id="episodes" className="section">
      <div className="container">
        <div className="section-head">
          <h2>Episodes</h2>
          <div className="meta">Three weekends · Season X · 2026</div>
        </div>

        <div className="ep-grid">
          {episodes.map((ep, i) => {
            const going = ep.going;
            const inIt = going.includes(userId);
            return (
              <div key={ep.id} className="ep-card" style={{ "--accent": ep.accent }}>
                <span className="num-tag">EP·{ep.num}</span>
                <h3 className="ep-title">{ep.title}</h3>
                <div className="ep-dates">{ep.dates}</div>

                <div className="ep-featuring">
                  <div className="ep-meta-row">
                    <div className="ep-featuring-label">Featuring</div>
                    <div className="who-count">{going.length} going</div>
                  </div>
                  <div className="who-stack">
                    {going.map(id => <Avatar key={id} id={id} size={32} borderWidth={2} fontSize={11} />)}
                  </div>
                </div>

                <Ticket
                  inIt={inIt}
                  onClick={() => onToggle(ep.id)}
                />
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
};

// ---------- Photos ----------
const ROTS = [-1.2, 0.8, -0.6, 1.4, -1.8, 0.4, 1.0, -1.0, 0.6, 1.6, -0.4, 0.2];
const FILLS = ["fill-teal","fill-orange","fill-green","fill-amber","fill-coral","fill-cream"];

const relativeTime = (iso) => {
  const sec = Math.floor((Date.now() - new Date(iso)) / 1000);
  if (sec < 60) return "just now";
  if (sec < 3600) return `${Math.floor(sec/60)}m ago`;
  if (sec < 86400) return `${Math.floor(sec/3600)}h ago`;
  return `${Math.floor(sec/86400)}d ago`;
};

const getPhotoUrl = (path) =>
  `https://whfrqyaohtexitvyjzrw.supabase.co/storage/v1/object/public/photos/${path}`;

const Lightbox = ({ photos, initialIndex, onClose }) => {
  const [idx, setIdx] = useState(initialIndex);
  const touchX = useRef(null);

  const prev = (e) => { e && e.stopPropagation(); setIdx(i => (i - 1 + photos.length) % photos.length); };
  const next = (e) => { e && e.stopPropagation(); setIdx(i => (i + 1) % photos.length); };

  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      if (e.key === "ArrowLeft") prev();
      if (e.key === "ArrowRight") next();
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, []);

  const photo = photos[idx];
  const crew = findCrew(photo.user_id);

  return (
    <div className="lightbox-overlay" onClick={onClose}>
      <div
        className="lightbox-card"
        onClick={e => e.stopPropagation()}
        onTouchStart={e => { touchX.current = e.touches[0].clientX; }}
        onTouchEnd={e => {
          if (touchX.current === null) return;
          const dx = e.changedTouches[0].clientX - touchX.current;
          if (dx > 50) prev();
          else if (dx < -50) next();
          touchX.current = null;
        }}
      >
        <button className="lightbox-close" onClick={onClose}>✕</button>
        {photos.length > 1 && <button className="lightbox-nav lightbox-prev" onClick={prev}>‹</button>}
        <img src={getPhotoUrl(photo.storage_path)} alt="" className="lightbox-img" />
        {photos.length > 1 && <button className="lightbox-nav lightbox-next" onClick={next}>›</button>}
        <div className="lightbox-meta">
          <span>@{crew?.name?.toLowerCase() || photo.user_id}</span>
          <span>{photos.length > 1 ? `${idx + 1} / ${photos.length}` : relativeTime(photo.created_at)}</span>
        </div>
      </div>
    </div>
  );
};

// Shared photo data: newest-first list + upload. Used by both the homepage
// preview and the full photos page so they stay in sync.
const usePhotos = (userId) => {
  const [photos, setPhotos] = useState([]);
  const [uploading, setUploading] = useState(false);

  const reload = () => {
    db.from("photos").select("*").order("created_at", { ascending: false })
      .then(({ data }) => setPhotos(data || []));
  };

  useEffect(() => { reload(); }, []);

  const upload = async (file, onToast) => {
    if (!file) return;
    setUploading(true);
    onToast && onToast("Uploading…");

    const ext = file.name.split(".").pop().toLowerCase();
    const path = `${userId}/${Date.now()}.${ext}`;

    const { error: upErr } = await db.storage.from("photos").upload(path, file, { upsert: false });
    if (upErr) { onToast && onToast("Upload failed — try again"); setUploading(false); return; }

    const { error: dbErr } = await db.from("photos").insert({ user_id: userId, storage_path: path });
    if (dbErr) { onToast && onToast("Couldn't save photo"); setUploading(false); return; }

    onToast && onToast("Photo pinned!");
    setUploading(false);
    reload();
  };

  return { photos, uploading, upload };
};

const PhotoTile = ({ p, i, onClick }) => {
  const crew = findCrew(p.user_id);
  return (
    <div
      className={`photo-tile ${FILLS[i % FILLS.length]} ${i % 3 === 0 ? "tape" : ""}`}
      style={{ "--rot": `${ROTS[i % ROTS.length]}deg`, cursor: "pointer" }}
      onClick={onClick}
    >
      <img
        src={getPhotoUrl(p.storage_path)}
        alt={`Photo by ${crew?.name || p.user_id}`}
        style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }}
      />
      <div className="caption">
        <span>{relativeTime(p.created_at)}</span>
        <span>@{crew?.name?.toLowerCase() || p.user_id}</span>
      </div>
    </div>
  );
};

// "Pin a photo" tile — the upload entry point, shown in both the homepage
// preview grid and the full page grid.
const UploadTile = ({ uploading, onUpload, onToast }) => {
  const inputRef = useRef(null);
  const handleFileChange = (e) => {
    const file = e.target.files?.[0];
    e.target.value = "";
    onUpload(file, onToast);
  };
  return (
    <React.Fragment>
      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        style={{ display: "none" }}
        onChange={handleFileChange}
      />
      <button
        className="photo-tile empty"
        onClick={() => !uploading && inputRef.current?.click()}
        style={{ flexDirection: "column", opacity: uploading ? 0.5 : 1 }}
      >
        <span className="plus">{uploading ? "…" : "+"}</span>
        <span className="lbl">{uploading ? "Uploading" : "Pin a photo"}</span>
      </button>
    </React.Fragment>
  );
};

// Homepage preview — newest 8 photos + the pin tile, then a link to the full wall.
const HOME_PHOTO_LIMIT = 8;
const PhotosPreview = ({ photos, uploading, onUpload, onLightbox, onViewAll, onToast }) => {
  const latest = photos.slice(0, HOME_PHOTO_LIMIT);
  return (
    <section id="photos" className="section">
      <div className="container">
        <div className="section-head">
          <h2>Crew Photos</h2>
          <div className="meta">The latest from the wall</div>
        </div>

        <div className="photo-grid">
          {latest.map((p, i) => (
            <PhotoTile key={p.id} p={p} i={i} onClick={() => onLightbox({ photos: latest, index: i })} />
          ))}
          <UploadTile uploading={uploading} onUpload={onUpload} onToast={onToast} />
        </div>

        <div className="photos-more">
          <button className="btn btn-ghost" onClick={onViewAll}>View all photos →</button>
        </div>
      </div>
    </section>
  );
};

// Full page — every photo + the upload tile, same grid format as the preview.
const PhotosPage = ({ photos, uploading, onUpload, onLightbox, onBack, onToast }) => {
  return (
    <section className="merch-page">
      <div className="container">
        <div className="mp-header">
          <button className="mp-back" onClick={onBack}>← Back to cottage</button>
          <div className="pre">— The Whole Wall —</div>
          <h1>All Photos</h1>
          <p className="mp-blurb">Every shot the crew has pinned. Tap any photo to view it big, or add your own.</p>
        </div>

        <div className="photo-grid" style={{ marginTop: 32 }}>
          {photos.map((p, i) => (
            <PhotoTile key={p.id} p={p} i={i} onClick={() => onLightbox({ photos, index: i })} />
          ))}
          <UploadTile uploading={uploading} onUpload={onUpload} onToast={onToast} />
        </div>
      </div>
    </section>
  );
};

// ---------- Products (Printify) ----------
const useProducts = () => {
  const [state, setState] = useState({
    products: [],
    featured: [],
    storeUrl: "https://chatona.printify.me/",
    loading: true,
    error: null,
  });
  useEffect(() => {
    let cancelled = false;
    fetch("/api/products")
      .then(r => r.json())
      .then(data => {
        if (cancelled) return;
        if (data && data.error) {
          setState(s => ({ ...s, loading: false, error: data.error }));
        } else {
          setState({
            products: (data && data.products) || [],
            featured: (data && data.featured) || [],
            storeUrl: (data && data.storeUrl) || "https://chatona.printify.me/",
            loading: false,
            error: null,
          });
        }
      })
      .catch(e => {
        if (cancelled) return;
        setState(s => ({ ...s, loading: false, error: String(e) }));
      });
    return () => { cancelled = true; };
  }, []);
  return state;
};

const formatPrice = (cents) => {
  if (cents == null) return "—";
  const dollars = cents / 100;
  return `$${dollars.toFixed(dollars >= 100 ? 0 : 2)}`;
};

const ProductCard = ({ p, storeUrl, onSelect }) => {
  const href = p.url || storeUrl;
  if (onSelect) {
    return (
      <button
        type="button"
        className="product-card"
        onClick={() => onSelect(p)}
      >
        <div className="product-img">
          {p.image
            ? <img src={p.image} alt={p.title} loading="lazy" />
            : <div className="product-placeholder">★</div>}
        </div>
        <div className="product-meta">
          <div className="product-title">{p.title}</div>
          <div className="product-price">{formatPrice(p.price)}</div>
        </div>
      </button>
    );
  }
  return (
    <a
      className="product-card"
      href={href}
      target="_blank"
      rel="noopener noreferrer"
    >
      <div className="product-img">
        {p.image
          ? <img src={p.image} alt={p.title} loading="lazy" />
          : <div className="product-placeholder">★</div>}
      </div>
      <div className="product-meta">
        <div className="product-title">{p.title}</div>
        <div className="product-price">{formatPrice(p.price)}</div>
      </div>
    </a>
  );
};

// ---------- Product detail modal ----------
const ProductModal = ({ product, onClose }) => {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = prevOverflow;
    };
  }, [onClose]);

  if (!product) return null;
  return (
    <div className="pm-overlay" onClick={onClose}>
      <div className="pm-card" onClick={(e) => e.stopPropagation()}>
        <button className="pm-close" onClick={onClose} aria-label="Close">×</button>
        <div className="pm-img">
          {product.image
            ? <img src={product.image} alt={product.title} />
            : <div className="product-placeholder">★</div>}
        </div>
        <div className="pm-body">
          <div className="pm-title">{product.title}</div>
          <div className="pm-price">{formatPrice(product.price)}</div>
          {product.description && (
            <div
              className="pm-desc"
              dangerouslySetInnerHTML={{ __html: product.description }}
            />
          )}
          <a
            className="btn btn-orange pm-buy"
            href={product.url}
            target="_blank"
            rel="noopener noreferrer"
          >
            Buy on Printify →
          </a>
        </div>
      </div>
    </div>
  );
};

// ---------- Merch (homepage preview) ----------
const Merch = ({ products, featured, storeUrl, loading, error, onViewAll }) => {
  const preview = (featured && featured.length)
    ? featured.map(id => products.find(p => p.id === id)).filter(Boolean)
    : products.slice(0, 4);
  return (
    <section id="merch" className="merch">
      <div className="container">
        <div className="merch-inner">
          <div>
            <div className="pre">— Limited Run · Crew Only —</div>
            <h2>Season X<br/>Merch Is Here</h2>
          </div>
          <div className="merch-cta">
            <button className="btn btn-ghost" onClick={onViewAll}>View All Merch →</button>
            <div style={{
              fontFamily: "JetBrains Mono, monospace",
              fontSize: 11,
              letterSpacing: "0.25em",
              textTransform: "uppercase",
            }}>
              Ships before<br/>Episode 01
            </div>
          </div>
        </div>

        {loading && <div className="merch-status">Loading the merch table…</div>}
        {error && <div className="merch-status">Merch is offline — try again soon.</div>}
        {!loading && !error && preview.length === 0 && (
          <div className="merch-status">No items live yet — check back soon.</div>
        )}
        {!loading && !error && preview.length > 0 && (
          <div className="product-grid product-grid-4">
            {preview.map(p => <ProductCard key={p.id} p={p} storeUrl={storeUrl} />)}
          </div>
        )}
      </div>
    </section>
  );
};

// ---------- Merch full page ----------
const MerchPage = ({ products, storeUrl, loading, error, onBack, onSelect }) => {
  return (
    <section className="merch-page">
      <div className="container">
        <div className="mp-header">
          <button className="mp-back" onClick={onBack}>← Back to cottage</button>
          <div className="pre">— The Full Lineup —</div>
          <h1>All Merch</h1>
          <p className="mp-blurb">
            Browse the goods. Click any item to head to the Printify shop and check out.
          </p>
          <a className="btn btn-orange" href={storeUrl} target="_blank" rel="noopener noreferrer">
            Open Printify Shop →
          </a>
        </div>

        {loading && <div className="merch-status">Loading the merch table…</div>}
        {error && <div className="merch-status">Merch is offline — try again soon.</div>}
        {!loading && !error && products.length === 0 && (
          <div className="merch-status">No items live yet — check back soon.</div>
        )}
        {!loading && !error && products.length > 0 && (
          <div className="product-grid">
            {products.map(p => <ProductCard key={p.id} p={p} storeUrl={storeUrl} onSelect={onSelect} />)}
          </div>
        )}
      </div>
    </section>
  );
};

// ---------- Footer ----------
const Footer = ({ userId }) => (
  <footer className="footer">
    <div className="container">
      <div className="footer-inner">
        <div>★ Chatona · Season X · MMXXVI</div>
        <div className="stamp">
          <span className="dot" /> Logged in as {findCrew(userId)?.name}
        </div>
        <div>Beaver Lake · Kearney · ON · 45°34'25"N 79°12'52"W</div>
      </div>
    </div>
  </footer>
);

// ---------- App shell ----------
const App = () => {
  // persist user across reloads
  const [userId, setUserId] = useState(() => {
    try { return localStorage.getItem("chatona_user") || null; } catch { return null; }
  });
  const [episodes, setEpisodes] = useState(EPISODES_INITIAL);
  const [toast, setToast] = useState(null);
  const [active, setActive] = useState("hero");
  const [view, setView] = useState("home"); // "home" | "merch" | "photos"
  const [lightbox, setLightbox] = useState(null);
  const [selectedProduct, setSelectedProduct] = useState(null);
  const weather = useWeather();
  const merchData = useProducts();
  const photosData = usePhotos(userId);

  // Load RSVPs from Supabase on mount
  useEffect(() => {
    db.from("rsvps").select("user_id, episode_id").eq("going", true)
      .then(({ data, error }) => {
        if (error) { console.error("RSVP load error:", error); return; }
        setEpisodes(prev => prev.map(ep => ({
          ...ep,
          going: (data || []).filter(r => r.episode_id === ep.id).map(r => r.user_id),
        })));
      });
  }, []);

  useEffect(() => {
    try {
      if (userId) localStorage.setItem("chatona_user", userId);
      else localStorage.removeItem("chatona_user");
    } catch {}
  }, [userId]);

  // Always scroll to top on login / logout transition so users land on the hero,
  // not wherever they had scrolled to on the previous screen.
  useEffect(() => {
    window.scrollTo({ top: 0, left: 0, behavior: "auto" });
  }, [userId]);

  // active nav-link tracking by scroll (home view only)
  useEffect(() => {
    if (!userId || view !== "home") return;
    const ids = ["hero", "episodes", "photos", "merch"];
    const handler = () => {
      const y = window.scrollY + 200;
      let cur = ids[0];
      for (const id of ids) {
        const el = document.getElementById(id);
        if (el && el.offsetTop <= y) cur = id;
      }
      setActive(cur);
    };
    window.addEventListener("scroll", handler, { passive: true });
    handler();
    return () => window.removeEventListener("scroll", handler);
  }, [userId, view]);

  const showToast = (msg) => {
    setToast(msg);
    clearTimeout(showToast._t);
    showToast._t = setTimeout(() => setToast(null), 2000);
  };

  const handleToggleRSVP = async (epId) => {
    const ep = episodes.find(e => e.id === epId);
    const has = ep.going.includes(userId);
    const newGoing = !has;

    // Optimistic update
    setEpisodes(prev => prev.map(e => {
      if (e.id !== epId) return e;
      const next = has ? e.going.filter(x => x !== userId) : [...e.going, userId];
      return { ...e, going: next };
    }));
    showToast(has ? `Out for ${ep.title}` : `You're in for ${ep.title}!`);

    // Persist to Supabase
    const { error } = await db.from("rsvps").upsert(
      { user_id: userId, episode_id: epId, going: newGoing, updated_at: new Date().toISOString() },
      { onConflict: "user_id,episode_id" }
    );
    if (error) {
      console.error("RSVP save error:", error);
      // Revert on failure
      setEpisodes(prev => prev.map(e => {
        if (e.id !== epId) return e;
        const reverted = newGoing ? e.going.filter(x => x !== userId) : [...e.going, userId];
        return { ...e, going: reverted };
      }));
      showToast("Couldn't save — try again");
    }
  };

  const goToMerchPage = () => {
    setView("merch");
    setActive("merch");
    window.scrollTo({ top: 0, left: 0, behavior: "auto" });
  };

  const goToPhotosPage = () => {
    setView("photos");
    setActive("photos");
    window.scrollTo({ top: 0, left: 0, behavior: "auto" });
  };

  const goHome = (anchorId) => {
    const wasOnMerch = view !== "home";
    setView("home");
    if (anchorId) setActive(anchorId);
    const scrollToAnchor = () => {
      if (!anchorId) {
        window.scrollTo({ top: 0, left: 0, behavior: "auto" });
        return;
      }
      const el = document.getElementById(anchorId);
      if (el) window.scrollTo({ top: el.offsetTop - 90, behavior: wasOnMerch ? "auto" : "smooth" });
    };
    if (wasOnMerch) {
      // Wait for home view to render before scrolling
      setTimeout(scrollToAnchor, 40);
    } else {
      scrollToAnchor();
    }
  };

  const handleNav = (id) => {
    if (id === "merch") {
      goToMerchPage();
    } else if (id === "photos") {
      goToPhotosPage();
    } else {
      goHome(id);
    }
  };

  if (!userId) {
    return <LoginScreen onLogin={setUserId} />;
  }

  return (
    <div className="app-shell">
      <TopNav
        userId={userId}
        onLogout={() => setUserId(null)}
        onNav={handleNav}
        onBrand={() => goHome("hero")}
        active={view === "home" ? active : view}
        weather={weather}
        episodes={episodes}
      />
      {view === "home" && (
        <>
          <Hero />
          <Episodes episodes={episodes} userId={userId} onToggle={handleToggleRSVP} />
          <PhotosPreview
            photos={photosData.photos}
            uploading={photosData.uploading}
            onUpload={photosData.upload}
            onLightbox={setLightbox}
            onViewAll={goToPhotosPage}
            onToast={showToast}
          />
          <Merch
            products={merchData.products}
            featured={merchData.featured}
            storeUrl={merchData.storeUrl}
            loading={merchData.loading}
            error={merchData.error}
            onViewAll={goToMerchPage}
          />
        </>
      )}
      {view === "merch" && (
        <MerchPage
          products={merchData.products}
          storeUrl={merchData.storeUrl}
          loading={merchData.loading}
          error={merchData.error}
          onBack={() => goHome("hero")}
          onSelect={setSelectedProduct}
        />
      )}
      {view === "photos" && (
        <PhotosPage
          photos={photosData.photos}
          uploading={photosData.uploading}
          onUpload={photosData.upload}
          onLightbox={setLightbox}
          onBack={() => goHome("hero")}
          onToast={showToast}
        />
      )}
      {lightbox && (
        <Lightbox
          photos={lightbox.photos}
          initialIndex={lightbox.index}
          onClose={() => setLightbox(null)}
        />
      )}
      <Footer userId={userId} />
      {selectedProduct && (
        <ProductModal product={selectedProduct} onClose={() => setSelectedProduct(null)} />
      )}
      {toast && <div className="toast">{toast}</div>}
    </div>
  );
};

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