/* ============================================================
   App shell — routing, state, modals, Tweaks
   Yardley Country Club member portal.
   ============================================================ */
const Y = window.YCC;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "fairway",
  "fontPair": "elegant",
  "density": "regular",
  "bookingFlow": "sheet"
}/*EDITMODE-END*/;

const THEMES = {
  fairway: {
    label: "Fairway", dots: ["#1f3a2d", "#b4904e"],
    vars: {
      "--bg": "#eef0e6", "--surface": "#fbfaf4", "--surface-2": "#f3f2e8",
      "--ink": "#1b2620", "--ink-soft": "#5b6b60", "--ink-faint": "#8a978c",
      "--line": "#dcdccb", "--line-strong": "#c7c8b3",
      "--brand": "#1f3a2d", "--brand-deep": "#142519", "--brand-mid": "#2f5240", "--brand-soft": "#e4ebe2",
      "--accent": "#b4904e", "--accent-deep": "#8f6f37", "--on-brand": "#f4f1e6",
    },
  },
  heritage: {
    label: "Heritage", dots: ["#1c2c4a", "#c2a14e"],
    vars: {
      "--bg": "#ecedf2", "--surface": "#fbfbf8", "--surface-2": "#eef0f4",
      "--ink": "#1a2233", "--ink-soft": "#5a6378", "--ink-faint": "#8a92a3",
      "--line": "#dadee6", "--line-strong": "#c4c9d4",
      "--brand": "#1c2c4a", "--brand-deep": "#13203a", "--brand-mid": "#2c416b", "--brand-soft": "#e3e8f1",
      "--accent": "#c2a14e", "--accent-deep": "#9c7e34", "--on-brand": "#f2f0e6",
    },
  },
  earth: {
    label: "Earth", dots: ["#4a5436", "#bd7b4f"],
    vars: {
      "--bg": "#efeae0", "--surface": "#fcf9f1", "--surface-2": "#f3efe3",
      "--ink": "#2a2820", "--ink-soft": "#67614f", "--ink-faint": "#988f78",
      "--line": "#ddd6c5", "--line-strong": "#c9c0a9",
      "--brand": "#4a5436", "--brand-deep": "#333a23", "--brand-mid": "#5d6a45", "--brand-soft": "#e9e7d8",
      "--accent": "#bd7b4f", "--accent-deep": "#9a5e38", "--on-brand": "#f5f1e6",
    },
  },
};

const FONT_PAIRS = {
  elegant: { display: '"Libre Baskerville", Georgia, serif', body: '"Hanken Grotesk", system-ui, sans-serif' },
  editorial: { display: '"Cormorant Garamond", Georgia, serif', body: '"Hanken Grotesk", system-ui, sans-serif' },
  modern: { display: '"Archivo", system-ui, sans-serif', body: '"Hanken Grotesk", system-ui, sans-serif' },
};

const NAV = [
  ["dashboard", "Dashboard", "flag"],
  ["book", "Tee Times", "calendar"],
  ["bookings", "My Tee Times", "clock"],
  ["games", "Weekend Games", "trophy"],
  ["standings", "Season", "star"],
  ["profile", "Profile", "user"],
];
const MANAGER_NAV = [
  ["manage", "Tour Setup", "shield"],
  ["roster", "Members", "users"],
  ["standings", "Season", "trophy"],
  ["book", "Tee Sheet", "calendar"],
  ["games", "Weekend Games", "trophy"],
];

// map a name to a player object
function toPlayer(name, member) {
  if (name === member.name) return { name, initials: member.initials, hi: member.handicapIndex, ghin: member.ghin };
  const r = Y.ROSTER.find(x => x.name === name);
  if (r) return { ...r };
  const initials = name.split(" ").map(s => s[0]).join("").slice(0, 2).toUpperCase();
  return { name, initials, hi: "—", ghin: "—" };
}

function buildInitialTour(member) {
  const tour = Y.buildDivTour(member);
  return { tour, bookings: tour.bookings.map(b => ({ id: b.id, dateISO: b.dateISO, slotId: b.slotId })) };
}

function initialsFromName(n) { return (n || "?").trim().split(/\s+/).map(s => s[0]).join("").slice(0, 2).toUpperCase(); }
function mapMember(row, user) {
  const meta = (user && user.user_metadata) || {};
  if (row) {
    const name = `${row.first_name || ""} ${row.last_name || ""}`.trim() || meta.full_name || row.email;
    return {
      id: row.id, first: row.first_name || "", last: row.last_name || "", name, email: row.email,
      ghin: row.ghin || "", handicapIndex: (row.handicap_index ?? null), memberSince: row.member_since || null,
      homeClub: "Yardley Country Club", tier: row.tier || "Member", tee: row.tee || "White",
      phone: row.phone || "", initials: initialsFromName(name),
    };
  }
  const name = meta.full_name || meta.name || (user.email || "").split("@")[0];
  return {
    id: user.id, first: (name.split(" ")[0] || ""), last: name.split(" ").slice(1).join(" "),
    name, email: user.email, ghin: "", handicapIndex: null, memberSince: null,
    homeClub: "Yardley Country Club", tier: "Member", tee: "White", phone: "", initials: initialsFromName(name),
  };
}

function App() {
  // Each weekend gets its own saved state, so every Sat/Sun auto-populates from
  // the season defaults until a manager edits it. (Matches the existing config
  // value for the current weekend, so no data is lost.)
  if (window.YCC_CONFIG && window.YCC.SAT) window.YCC_CONFIG.weekendId = "wknd-" + window.YCC.isoDay(window.YCC.SAT);
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [authReady, setAuthReady] = useState(false);
  const [session, setSession] = useState(null);
  const [view, setView] = useState("dashboard");
  const [role, setRole] = useState("member"); // member | manager
  const [member, setMember] = useState(null);
  const init = useMemo(() => buildInitialTour(Y.MEMBER), []);
  const [tour, setTour] = useState(init.tour);
  const [bookings, setBookings] = useState(init.bookings);
  const [gameConfig, setGameConfig] = useState({ gameType: "skins", skinValue: 5, carryover: true, scoring: "gross", groupTee: "Blue", splitDays: false, gameSat: "skins", gameSun: "skins", lowMode: "day", sideGames: (window.YCC_GAMES && window.YCC_GAMES.defaultSideGames ? window.YCC_GAMES.defaultSideGames() : { sat: { ctp: true }, sun: { ctp: true }, longDriveHole: 7 }) });
  const [scores, setScores] = useState({});
  const [sideResults, setSideResults] = useState({});
  const [modal, setModal] = useState(null);
  const [toastMsg, setToastMsg] = useState("");
  const [mobileNav, setMobileNav] = useState(false);
  const [userMenu, setUserMenu] = useState(false);
  const [accountMembers, setAccountMembers] = useState([]);
  const [seasonDoc, setSeasonDoc] = useState({ weeks: {} });
  const loadedRef = useRef(false);
  const saveTimer = useRef(null);
  const remoteApplyRef = useRef(false);
  const lastEditRef = useRef(0);

  const dayKeys = [
    { iso: tour.satISO, label: Y.shortDate(Y.SAT) },
    { iso: tour.sunISO, label: Y.shortDate(Y.SUN) },
  ];

  // apply theme + fonts + density
  useEffect(() => {
    const root = document.documentElement;
    const theme = THEMES[t.theme] || THEMES.fairway;
    Object.entries(theme.vars).forEach(([k, v]) => root.style.setProperty(k, v));
    const fp = FONT_PAIRS[t.fontPair] || FONT_PAIRS.elegant;
    root.style.setProperty("--font-display", fp.display);
    root.style.setProperty("--font-body", fp.body);
    document.body.setAttribute("data-density", t.density || "regular");
  }, [t.theme, t.fontPair, t.density]);

  // On load, restore the saved tee sheet once we know who's signed in.
  // (Gated on `member` so the booking pointers resolve to the right person,
  //  and so a background restore never clobbers state before auth resolves.)
  useEffect(() => {
    const db = window.YCCdb;
    if (!member || loadedRef.current) return;
    if (!(db && db.isConfigured && db.isConfigured())) { loadedRef.current = true; return; }
    db.pull().then(obj => {
      if (obj && (obj._appState || (obj.tee_times && obj.tee_times.length))) applyImport(obj, { silent: true });
    }).catch(() => {}).finally(() => { loadedRef.current = true; });
  }, [member]);

  // Auto-save the live tee sheet on every change, so signing out and back in
  // restores exactly what was saved (including a day cleared to zero tee times).
  useEffect(() => {
    const db = window.YCCdb;
    if (!loadedRef.current || !member) return;
    if (remoteApplyRef.current) { remoteApplyRef.current = false; return; } // don't echo a remote update back
    if (!(db && db.isConfigured && db.isConfigured()) || !db.saveState) return;
    lastEditRef.current = Date.now();
    clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => {
      try { db.saveState(buildExportData()._appState).catch(() => {}); } catch (e) {}
    }, 900);
    return () => clearTimeout(saveTimer.current);
  }, [tour.slots, tour.published, tour.managers, gameConfig, scores, sideResults, member]);

  // Live updates — apply another device's changes (e.g. scores entered hole-by-hole),
  // unless we're mid-edit locally (so we don't stomp the active typist).
  useEffect(() => {
    const db = window.YCCdb;
    if (!member || !db || !db.subscribeState || !db.isConfigured || !db.isConfigured()) return;
    let unsub = () => {};
    db.subscribeState((state) => {
      if (!state) return;
      if (Date.now() - lastEditRef.current < 3000) return;
      remoteApplyRef.current = true;
      applyImport({ _appState: state }, { silent: true });
    }).then((fn) => { unsub = fn || (() => {}); });
    return () => unsub();
  }, [member]);

  // Account roster — powers the manager's "Add a member" dropdown.
  useEffect(() => {
    const db = window.YCCdb;
    if (!member || !(db && db.getMembers)) return;
    db.getMembers().then(list => setAccountMembers(list || [])).catch(() => {});
  }, [member]);

  // Season win standings doc (shared across weekends).
  const SEASON_ID = "season-2026";
  useEffect(() => {
    const db = window.YCCdb;
    if (!member || !(db && db.getDoc && db.isConfigured && db.isConfigured())) return;
    db.getDoc(SEASON_ID).then(doc => { if (doc && doc.weeks) setSeasonDoc(doc); }).catch(() => {});
  }, [member]);

  function postWeekendResults() {
    const wins = window.YCC_SCORING.weekendWins({ tour, scores, gameConfig });
    const stats = window.YCC_SCORING.weekendStats ? window.YCC_SCORING.weekendStats({ tour, scores, sideResults, gameConfig }) : {};
    const next = { ...seasonDoc, weeks: { ...(seasonDoc.weeks || {}), [tour.satISO]: wins.winsByName }, stats: { ...(seasonDoc.stats || {}), [tour.satISO]: stats }, updatedAt: new Date().toISOString() };
    setSeasonDoc(next);
    const db = window.YCCdb;
    if (db && db.putDoc && db.isConfigured && db.isConfigured()) {
      db.putDoc(SEASON_ID, next).then((r) => toast(r && r.error ? "Saved locally, cloud sync failed" : "Weekend results posted to the season"));
    } else { toast("Weekend results posted"); }
  }
  function clearWeekendResults() {
    const weeks = { ...(seasonDoc.weeks || {}) }; delete weeks[tour.satISO];
    const stats = { ...(seasonDoc.stats || {}) }; delete stats[tour.satISO];
    const next = { ...seasonDoc, weeks, stats };
    setSeasonDoc(next);
    const db = window.YCCdb;
    if (db && db.putDoc && db.isConfigured && db.isConfigured()) db.putDoc(SEASON_ID, next);
    toast("This weekend's results cleared from the season");
  }

  // Authentication gate — require a real signed-in account.
  useEffect(() => {
    const db = window.YCCdb;
    if (!db || !db.isConfigured || !db.isConfigured()) { setAuthReady(true); return; }
    const watchdog = setTimeout(() => setAuthReady(true), 6000); // never get stuck on "Loading…"
    try {
      db.auth.onAuthChange((s) => {
        setSession(s || null);
        if (s && s.user) {
          // Render immediately from the account; never block on a DB call.
          const fallback = mapMember(null, s.user);
          setMember(fallback);
          setView(fallback.first && fallback.last && fallback.ghin ? "dashboard" : "profile");
          clearTimeout(watchdog); setAuthReady(true);
          // Then refine from the members table if/when it answers (non-blocking).
          db.getMemberByEmail(s.user.email).then((row) => {
            if (row) { const m = mapMember(row, s.user); setMember(m); setView(m.first && m.last && m.ghin ? "dashboard" : "profile"); }
          }).catch(() => {});
        } else {
          setMember(null);
          clearTimeout(watchdog); setAuthReady(true);
        }
      });
    } catch (e) { clearTimeout(watchdog); setAuthReady(true); }
    return () => clearTimeout(watchdog);
  }, []);

  // Load saved managers from Supabase so their assignments + access persist across reloads.
  useEffect(() => {
    const db = window.YCCdb;
    if (!db || !db.isConfigured || !db.isConfigured() || !db.getManagersList) return;
    db.getManagersList().then((rows) => {
      if (!rows || !rows.length) return;
      setTour(prev => {
        const managers = prev.managers.map(m => {
          const row = rows.find(r => (r.role || "").toLowerCase() === (m.role || "").toLowerCase());
          if (!row) return m;
          const name = row.name || m.name;
          return { ...m, name, initials: row.initials || initialsFromName(name) || m.initials,
            email: row.email || m.email, phone: row.phone || m.phone,
            available: row.available !== false, placeholder: !name, id: row.id || m.id };
        });
        return { ...prev, managers };
      });
    }).catch(() => {});
  }, [session]);

  const memberEmail = ((member && member.email) || "").trim().toLowerCase();
  const isAdmin = Y.isAdminEmail(memberEmail);
  const isManagerUser = isAdmin || (!!memberEmail && tour.managers.some(m => m.email && m.email.trim().toLowerCase() === memberEmail));
  const entry = Y.entryWindow(Y.SAT);
  const phase = entry.phase; // open | managerOnly | locked
  const confirmedNames = useMemo(() => {
    const set = new Set();
    (accountMembers || []).forEach(m => {
      if (Y.confirmedForWeek(m.handicap_confirmed_at, Y.SAT)) {
        const n = `${m.first_name || ""} ${m.last_name || ""}`.trim() || m.email;
        set.add(n);
      }
    });
    return set;
  }, [accountMembers]);
  const MANAGER_VIEWS = ["manage", "roster"];
  useEffect(() => {
    if (!isManagerUser) {
      if (role === "manager") setRole("member");
      if (MANAGER_VIEWS.includes(view)) setView("dashboard");
    }
  }, [isManagerUser, role, view]);

  async function signOut() {
    const db = window.YCCdb;
    if (db && db.isConfigured && db.isConfigured()) { try { await db.auth.signOut(); } catch (e) {} }
    setSession(null); setMember(null); setRole("member");
  }

  function toast(msg) { setToastMsg(msg); }
  function go(v) { setView(v); setMobileNav(false); window.scrollTo({ top: 0 }); }
  function daysUntil(iso) {
    const d = new Date(iso + "T00:00:00");
    const diff = Math.round((d - Y.TODAY) / 86400000);
    if (diff === 0) return "Today";
    if (diff === 1) return "Tomorrow";
    if (diff < 0) return "Past";
    return `In ${diff} days`;
  }
  function switchRole(r) {
    setRole(r);
    setMobileNav(false);
    go(r === "manager" ? "manage" : "dashboard");
  }

  // --- live derivations from the tour ---
  function slotOf(b) { return (tour.slots[b.dateISO] || []).find(s => s.id === b.slotId); }
  const liveBookings = bookings.filter(b => slotOf(b));
  const onDutyIdx = tour.managers.findIndex(m => m.available && !m.placeholder && m.name);
  const activeManager = tour.managers[onDutyIdx >= 0 ? onDutyIdx : (tour.managers.findIndex(m => m.name) >= 0 ? tour.managers.findIndex(m => m.name) : 0)];
  // The primary manager receives member change-request emails once sign-ups close.
  const primaryManager = tour.managers.find(m => (m.role || "").toLowerCase() === "primary" && m.name && m.email)
    || tour.managers.find(m => m.name && m.email) || activeManager;
  const primaryManagerEmail = (primaryManager && primaryManager.email) || (Y.PRIMARY_MANAGER && Y.PRIMARY_MANAGER.email) || "";

  // --- member: claim / release / move an individual spot ---
  function requestClaim(dayISO, slot) {
    if (phase !== "open") { toast(phase === "managerOnly" ? "Sign-ups closed — ask your manager to add you." : "Groups are final for this weekend."); return; }
    setModal({ type: "claim", dayISO, slotId: slot.id });
  }

  function confirmClaim(dayISO, slotId, transport = "walk") {
    if (phase !== "open") { setModal(null); return; }
    const slot = (tour.slots[dayISO] || []).find(s => s.id === slotId);
    if (!slot || slot.players.length >= 4) { setModal(null); return; }
    // One tee time per day per member — can't be in two groups the same day.
    const alreadyThatDay = (tour.slots[dayISO] || []).some(s => s.players.some(p => p.name === member.name));
    if (alreadyThatDay) { setModal(null); toast("You already have a tee time that day — one per day."); return; }
    setTour(prev => {
      const day = prev.slots[dayISO];
      const newDay = day.map(s => s.id === slotId
        ? { ...s, players: [...s.players, { name: member.name, initials: member.initials, hi: member.handicapIndex, ghin: member.ghin, tee: member.tee || "White", transport }] }
        : s);
      return { ...prev, slots: { ...prev.slots, [dayISO]: newDay } };
    });
    setBookings(prev => [...prev.filter(b => b.slotId !== slotId), { id: "bk-" + Date.now(), dateISO: dayISO, slotId }]);
    setModal(null);
    toast(`You're in — ${slot.time}, ${Y.shortDate(new Date(dayISO + "T00:00:00"))}`);
  }

  // Set a player's walk/ride preference (member sets own; manager sets anyone's).
  function setTransport(dayISO, slotId, name, mode) {
    setTour(prev => {
      const day = prev.slots[dayISO];
      if (!day) return prev;
      const newDay = day.map(s => s.id === slotId
        ? { ...s, players: s.players.map(p => p.name === name ? { ...p, transport: mode } : p) } : s);
      return { ...prev, slots: { ...prev.slots, [dayISO]: newDay } };
    });
  }

  // Shared, synced group score entry (game day). Keyed by day|slot → name → 18 holes.
  function setScore(dayISO, slotId, name, hIdx, val) {
    const key = dayISO + "|" + slotId;
    setScores(prev => {
      const grp = { ...(prev[key] || {}) };
      const arr = grp[name] ? [...grp[name]] : new Array(18).fill(null);
      arr[hIdx] = val;
      grp[name] = arr;
      return { ...prev, [key]: grp };
    });
  }

  // Shared, synced add-on game winners (Closest-to-the-Pin per par 3, Longest Drive).
  // Keyed by day|slot → { ctp: { [holeIdx]: name }, longDrive: name }.
  function setSideResult(dayISO, slotId, kind, holeIdx, name) {
    const key = dayISO + "|" + slotId;
    setSideResults(prev => {
      const grp = { ...(prev[key] || {}) };
      if (kind === "ctp") {
        const ctp = { ...(grp.ctp || {}) };
        if (name == null) delete ctp[holeIdx]; else ctp[holeIdx] = name;
        grp.ctp = ctp;
      } else if (kind === "longDrive") {
        if (name == null) delete grp.longDrive; else grp.longDrive = name;
      }
      return { ...prev, [key]: grp };
    });
  }

  function releaseSpot(booking) {
    if (phase !== "open") { setModal(null); toast("Sign-ups closed — ask your manager to release your spot."); return; }
    const dayISO = booking.dateISO;
    setTour(prev => {
      const day = prev.slots[dayISO];
      if (!day) return prev;
      const newDay = day.map(s => s.id === booking.slotId
        ? { ...s, players: s.players.filter(p => p.name !== member.name) } : s);
      return { ...prev, slots: { ...prev.slots, [dayISO]: newDay } };
    });
    setBookings(prev => prev.filter(b => b.id !== booking.id));
    setModal(null);
    toast("Spot released");
  }

  function moveSpot(booking, newSlot) {
    if (phase !== "open") { setModal(null); toast("Sign-ups closed — ask your manager to move you."); return; }
    const dayISO = booking.dateISO;
    setTour(prev => {
      const day = prev.slots[dayISO];
      const fromSlot = day.find(s => s.id === booking.slotId);
      const me = (fromSlot && fromSlot.players.find(p => p.name === member.name))
        || { name: member.name, initials: member.initials, hi: member.handicapIndex, ghin: member.ghin, tee: member.tee || "White", transport: "walk" };
      const newDay = day.map(s => {
        if (s.id === booking.slotId) return { ...s, players: s.players.filter(p => p.name !== member.name) };
        if (s.id === newSlot.id) return { ...s, players: [...s.players, { ...me }] };
        return s;
      });
      return { ...prev, slots: { ...prev.slots, [dayISO]: newDay } };
    });
    setBookings(prev => prev.map(b => b.id === booking.id ? { ...b, slotId: newSlot.id } : b));
    setModal(null);
    toast(`Moved to ${newSlot.time}`);
  }

  // --- manager: move ANY player between groups & add off-app members ---
  function managerMovePlayer(dayISO, fromSlotId, toSlotId, name) {
    setTour(prev => {
      const day = prev.slots[dayISO];
      if (!day) return prev;
      const fromSlot = day.find(s => s.id === fromSlotId);
      const target = day.find(s => s.id === toSlotId);
      const player = fromSlot && fromSlot.players.find(p => p.name === name);
      if (!player || !target || target.players.length >= 4) return prev;
      const newDay = day.map(s => {
        if (s.id === fromSlotId) return { ...s, players: s.players.filter(p => p.name !== name) };
        if (s.id === toSlotId) return { ...s, players: [...s.players, { ...player }] };
        return s;
      });
      return { ...prev, slots: { ...prev.slots, [dayISO]: newDay } };
    });
    // keep the member's own booking pointer in sync if the manager moved them
    if (member && name === member.name) {
      setBookings(prev => prev.map(b => (b.dateISO === dayISO && b.slotId === fromSlotId) ? { ...b, slotId: toSlotId } : b));
    }
    const tLabel = (tour.slots[dayISO] || []).find(s => s.id === toSlotId);
    setModal(null);
    toast(`${name.split(" ")[0]} moved to ${tLabel ? tLabel.time : "new group"}`);
  }

  // Game-day drag-and-drop: swap two players between groups (A↔B). If the drop
  // target seat is empty, this is just a move (handled by managerMovePlayer).
  function managerSwapPlayers(dayISO, slotAId, nameA, slotBId, nameB) {
    if (slotAId === slotBId && nameA === nameB) return;
    setTour(prev => {
      const day = prev.slots[dayISO];
      if (!day) return prev;
      const slotA = day.find(s => s.id === slotAId), slotB = day.find(s => s.id === slotBId);
      const pA = slotA && slotA.players.find(p => p.name === nameA);
      const pB = slotB && slotB.players.find(p => p.name === nameB);
      if (!pA || !pB) return prev;
      const newDay = day.map(s => {
        if (s.id === slotAId) return { ...s, players: s.players.map(p => p.name === nameA ? { ...pB } : p) };
        if (s.id === slotBId) return { ...s, players: s.players.map(p => p.name === nameB ? { ...pA } : p) };
        return s;
      });
      return { ...prev, slots: { ...prev.slots, [dayISO]: newDay } };
    });
    // keep the signed-in member's booking pointer correct if they were swapped
    if (member && (nameA === member.name || nameB === member.name)) {
      const myFrom = nameA === member.name ? slotAId : slotBId;
      const myTo = nameA === member.name ? slotBId : slotAId;
      setBookings(prev => prev.map(b => (b.dateISO === dayISO && b.slotId === myFrom) ? { ...b, slotId: myTo } : b));
    }
    toast(`Swapped ${nameA.split(" ")[0]} ↔ ${nameB.split(" ")[0]}`);
  }

  // Add a member (from the account list) or an off-app guest to a tee time.
  function managerAddPlayer(dayISO, slotId, info) {
    const name = (info.name || "").trim();
    if (!name) { setModal(null); return; }
    const dup = (tour.slots[dayISO] || []).some(s => s.players.some(p => p.name.toLowerCase() === name.toLowerCase()));
    if (dup) { setModal(null); toast(`${name.split(" ")[0]} already has a tee time that day.`); return; }
    const initials = name.split(/\s+/).map(s => s[0]).join("").slice(0, 2).toUpperCase();
    setTour(prev => {
      const day = prev.slots[dayISO];
      if (!day) return prev;
      const newDay = day.map(s => {
        if (s.id !== slotId) return s;
        if (s.players.length >= 4 || s.players.some(p => p.name.toLowerCase() === name.toLowerCase())) return s;
        return { ...s, players: [...s.players, {
          name, initials,
          hi: (info.hi === "" || info.hi == null ? "—" : Number(info.hi)),
          ghin: info.ghin || "—",
          tee: info.tee || "White",
          transport: info.transport || "walk",
          guest: !!info.guest,
          memberId: info.memberId || undefined,
        }] };
      });
      return { ...prev, slots: { ...prev.slots, [dayISO]: newDay } };
    });
    setModal(null);
    toast(`${name.split(" ")[0]} added to the group`);
  }

  // --- manager: edit the weekly entry ---
  function addTime(dayISO, timeLabel) {
    setTour(prev => {
      const day = prev.slots[dayISO] || [];
      if (day.some(s => s.time === timeLabel)) return prev;
      const slot = { id: `${dayISO}-n${Date.now()}`, time: timeLabel, players: [] };
      return { ...prev, slots: { ...prev.slots, [dayISO]: [...day, slot] } };
    });
    toast(`Added ${timeLabel}`);
  }
  function editTime(dayISO, slotId, newTime) {
    setTour(prev => {
      const day = prev.slots[dayISO];
      return { ...prev, slots: { ...prev.slots, [dayISO]: day.map(s => s.id === slotId ? { ...s, time: newTime } : s) } };
    });
    toast(`Updated to ${newTime}`);
  }
  function removeTime(dayISO, slotId) {
    setTour(prev => {
      const day = prev.slots[dayISO];
      return { ...prev, slots: { ...prev.slots, [dayISO]: day.filter(s => s.id !== slotId) } };
    });
    setBookings(prev => prev.filter(b => b.slotId !== slotId));
    setModal(null);
    toast("Tee time removed");
  }
  function togglePublish() {
    setTour(prev => ({ ...prev, published: !prev.published }));
  }
  // manager can edit ANY signup; members only act on their own (handled by claim/release/move).
  function removeSignup(dayISO, slotId, name) {
    setTour(prev => {
      const day = prev.slots[dayISO];
      return { ...prev, slots: { ...prev.slots, [dayISO]: day.map(s => s.id === slotId ? { ...s, players: s.players.filter(p => p.name !== name) } : s) } };
    });
    if (name === member.name) setBookings(prev => prev.filter(b => !(b.dateISO === dayISO && b.slotId === slotId)));
    toast(name === member.name ? "Spot released" : `${name.split(" ")[0]} removed from group`);
  }
  function toggleManagerAvail(idx) {
    setTour(prev => ({ ...prev, managers: prev.managers.map((m, i) => i === idx ? { ...m, available: !m.available } : m) }));
  }
  // Admin can fill in / edit a manager (e.g. add the primary manager).
  function setManagerInfo(idx, patch) {
    let saved = null;
    setTour(prev => {
      const managers = prev.managers.map((m, i) => {
        if (i !== idx) return m;
        const next = { ...m, ...patch };
        if (patch.name !== undefined) { next.initials = initialsFromName(patch.name); next.placeholder = !patch.name; }
        saved = next;
        return next;
      });
      return { ...prev, managers };
    });
    // Persist to Supabase so the assignment (and their manager access) survives reloads.
    const db = window.YCCdb;
    if (saved && db && db.isConfigured && db.isConfigured() && db.upsertManagerRow) {
      db.upsertManagerRow({
        id: saved.id || ("mgr-" + saved.role.toLowerCase()),
        name: saved.name || "", initials: saved.initials || "",
        role: (saved.role || "Primary").toLowerCase(), title: saved.title || "",
        email: (saved.email || "").toLowerCase(), phone: saved.phone || "", available: saved.available !== false,
      }).then((res) => {
        if (res && res.error) toast("Manager saved locally, cloud sync failed: " + (res.error.message || res.error));
        else toast("Manager saved");
      });
    } else { toast("Manager updated"); }
  }

  // --- JSON export / import + Supabase sync ---
  function initialsOf(name) { return name.split(" ").map(s => s[0]).join("").slice(0, 2).toUpperCase(); }
  function to24(label) { const t = Y.parseTime(label); return `${String(Math.floor(t / 60)).padStart(2, "0")}:${String(t % 60).padStart(2, "0")}`; }
  function to12(hhmm) { const [h, m] = hhmm.split(":").map(Number); return Y.timeLabel(h, m); }

  function buildExportData() {
    const idByName = { [Y.MEMBER.name]: "m1" };
    Y.ROSTER.forEach((r, i) => { idByName[r.name] = "m" + (i + 2); });
    const members = [
      { id: "m1", first_name: Y.MEMBER.first, last_name: Y.MEMBER.last, email: Y.MEMBER.email, ghin: Y.MEMBER.ghin, handicap_index: Y.MEMBER.handicapIndex },
      ...Y.ROSTER.map((r, i) => ({ id: "m" + (i + 2), first_name: r.name.split(" ")[0], last_name: r.name.split(" ").slice(1).join(" "), ghin: r.ghin, handicap_index: r.hi })),
    ];
    const tee_times = [], signups = []; let su = 1;
    [tour.satISO, tour.sunISO].forEach(dayISO => {
      Y.sortByTime(tour.slots[dayISO] || []).forEach(s => {
        tee_times.push({ id: s.id, play_date: dayISO, tee_time: to24(s.time), max_players: 4 });
        s.players.forEach((p, idx) => signups.push({ id: "su" + (su++), tee_time_id: s.id, member_id: p.memberId || idByName[p.name] || null, member_name: p.name, ghin: p.ghin, handicap_index: p.hi, transport: p.transport || "walk", guest: !!p.guest, position: idx + 1 }));
      });
    });
    return {
      _about: "Yardley Country Club — DIV Tour export. Re-importable here; tables match the Supabase schema in supabase/divtour-schema.sql.",
      exportedAt: new Date().toISOString(),
      club: { name: "Yardley Country Club", program: "DIV Tour", course_rating: Y.COURSE.rating, slope: Y.COURSE.slope, par: Y.COURSE.par, tee: Y.COURSE.tee },
      weekend: { label: tour.weekendLabel, sat_date: tour.satISO, sun_date: tour.sunISO, published: tour.published },
      managers: tour.managers,
      members,
      course_holes: Y.HOLES.map(h => ({ hole: h.n, par: h.par, yards: h.yards, stroke_index: h.si })),
      tee_times, signups,
      game: gameConfig,
      _appState: { slots: tour.slots, published: tour.published, managers: tour.managers, gameConfig, scores, sideResults },
    };
  }

  function exportTourJSON() {
    const text = JSON.stringify(buildExportData(), null, 2);
    const blob = new Blob([text], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `divtour-${tour.satISO}.json`;
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
    toast("Tour exported as JSON");
  }

  function applyImport(obj, opts = {}) {
    const silent = !!opts.silent;
    let newSlots;
    if (obj._appState && obj._appState.slots) {
      newSlots = obj._appState.slots;
      setTour(prev => ({ ...prev, slots: newSlots, published: !!obj._appState.published, managers: obj._appState.managers || prev.managers }));
      if (obj._appState.gameConfig) setGameConfig(obj._appState.gameConfig);
      if (obj._appState.scores) setScores(obj._appState.scores);
      if (obj._appState.sideResults) setSideResults(obj._appState.sideResults);
    } else if (obj.tee_times) {
      const memById = {};
      (obj.members || []).forEach(m => {
        const name = m.name || `${m.first_name || ""} ${m.last_name || ""}`.trim();
        memById[m.id] = { name, initials: initialsOf(name), hi: m.handicap_index, ghin: m.ghin };
      });
      newSlots = { [tour.satISO]: [], [tour.sunISO]: [] };
      obj.tee_times.forEach(tt => {
        const dayISO = tt.play_date;
        if (!newSlots[dayISO]) newSlots[dayISO] = [];
        const players = (obj.signups || [])
          .filter(s => s.tee_time_id === tt.id)
          .sort((a, b) => (a.position || 0) - (b.position || 0))
          .map(s => {
            const base = memById[s.member_id] || { name: s.member_name, initials: initialsOf(s.member_name || "?"), hi: s.handicap_index, ghin: s.ghin };
            return { ...base, transport: s.transport || base.transport || "walk", guest: s.guest != null ? !!s.guest : !!base.guest };
          });
        newSlots[dayISO].push({ id: tt.id, time: to12(tt.tee_time), players });
      });
      setTour(prev => ({ ...prev, slots: newSlots, published: obj.weekend ? !!obj.weekend.published : true }));
      if (obj.game) setGameConfig(obj.game);
    } else {
      if (!silent) toast("Unrecognized file format");
      return;
    }
    // recompute the member's claimed spots from the imported slots
    const meName = member && member.name;
    const mine = [];
    Object.entries(newSlots).forEach(([dayISO, slots]) => {
      (slots || []).forEach(s => { if (meName && s.players.some(p => p.name === meName)) mine.push({ id: "bk-" + dayISO + "-" + s.id, dateISO: dayISO, slotId: s.id }); });
    });
    setBookings(mine);
    if (!silent) toast("Tour data imported");
  }

  function importTourJSON(file) {
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try { applyImport(JSON.parse(reader.result)); }
      catch (e) { toast("Couldn't read that JSON file"); }
    };
    reader.readAsText(file);
  }

  const supabaseReady = !!(window.YCCdb && window.YCCdb.isConfigured && window.YCCdb.isConfigured());
  function pushSupabase() {
    if (!supabaseReady) { toast("Add Supabase keys in config.js first"); return; }
    toast("Pushing to Supabase…");
    window.YCCdb.push(buildExportData()).then(() => toast("Synced to Supabase")).catch(() => toast("Supabase push failed"));
  }
  function pullSupabase() {
    if (!supabaseReady) { toast("Add Supabase keys in config.js first"); return; }
    toast("Pulling from Supabase…");
    window.YCCdb.pull().then(obj => obj && applyImport(obj)).catch(() => toast("Supabase pull failed"));
  }

  function saveMember(next) {
    setMember(next);
    const db = window.YCCdb;
    if (db && db.isConfigured && db.isConfigured() && next && next.email) {
      return db.upsertMember({
        id: next.id || next.email,
        email: next.email,
        first_name: next.first || "",
        last_name: next.last || "",
        ghin: next.ghin || null,
        handicap_index: (next.handicapIndex === "" ? null : next.handicapIndex),
        phone: next.phone || null,
        tee: next.tee || null,
        tier: next.tier || "Member",
      }).then((res) => {
        if (res && res.error) { const msg = res.error.message || String(res.error); toast("Cloud save failed: " + msg); return { ok: false, error: msg }; }
        toast("Profile saved"); return { ok: true };
      }).catch((e) => { const msg = (e && e.message) || "network error"; toast("Cloud save error: " + msg); return { ok: false, error: msg }; });
    }
    return Promise.resolve({ ok: true, local: true });
  }

  const ctx = {
    member, setMember: saveMember, tour, setTour, bookings: liveBookings, slotOf, dayKeys, go, daysUntil, toast, role, t,
    activeManager, gameConfig, setGameConfig,
    scores, setScore,
    sideResults, setSideResult,
    primaryManager, primaryManagerEmail,
    requestManagerEmail: () => setModal({ type: "emailManager" }),
    phase, entry, confirmedNames,
    requestClaim,
    setTransport,
    requestEdit: (b) => setModal({ type: "move", booking: b }),
    requestCancel: (b) => setModal({ type: "release", booking: b }),
    // manager
    addTime, editTime, toggleManagerAvail, removeSignup, setManagerInfo, isAdmin, isManagerUser,
    managerMovePlayer, managerAddPlayer, managerSwapPlayers,
    accountMembers, seasonDoc, postWeekendResults, clearWeekendResults,
    requestManagerMove: (dayISO, slot, player) => setModal({ type: "managerMove", dayISO, slotId: slot.id, player }),
    requestAddPlayer: (dayISO, slot) => setModal({ type: "addPlayer", dayISO, slotId: slot.id }),
    requestRemoveTime: (dayISO, slot) => setModal({ type: "removeTime", dayISO, slot }),
    togglePublish,
    exportTourJSON, importTourJSON, pushSupabase, pullSupabase, supabaseReady,
  };

  if (!authReady) {
    return (
      <div style={{ minHeight: "100vh", display: "grid", placeItems: "center", background: "var(--brand-deep)", color: "var(--on-brand)" }}>
        <div style={{ textAlign: "center" }}>
          <div className="crest" style={{ width: 54, height: 54, fontSize: 20, margin: "0 auto", borderColor: "var(--accent)", color: "var(--accent)" }}>YCC</div>
          <div className="serif" style={{ fontSize: 20, marginTop: 14 }}>Yardley Country Club</div>
          <div className="faint" style={{ fontSize: 12.5, marginTop: 4, color: "rgba(244,241,230,.6)" }}>Loading…</div>
        </div>
      </div>
    );
  }

  if (!session || !member) {
    return (
      <>
        <LoginScreen supabaseReady={!!(window.YCCdb && window.YCCdb.isConfigured && window.YCCdb.isConfigured())} />
        <TweaksUI t={t} setTweak={setTweak} />
      </>
    );
  }

  // resolve modal live from current tour state
  let modalNode = null;
  if (modal?.type === "claim") {
    const slot = (tour.slots[modal.dayISO] || []).find(s => s.id === modal.slotId);
    if (slot) {
      const dLabel = Y.fmtDate(new Date(modal.dayISO + "T00:00:00"));
      modalNode = (
        <ClaimModal
          slot={slot} dayLabel={dLabel} member={member}
          onConfirm={(transport) => confirmClaim(modal.dayISO, modal.slotId, transport)}
          onClose={() => setModal(null)}
        />
      );
    } else { setModal(null); }
  } else if (modal?.type === "managerMove") {
    const day = tour.slots[modal.dayISO] || [];
    const fromSlot = day.find(s => s.id === modal.slotId);
    const openSlots = day.filter(s => s.id !== modal.slotId && s.players.length < 4);
    if (fromSlot) {
      modalNode = <ManagerMoveModal player={modal.player} fromTime={fromSlot.time}
        dateLabel={Y.fmtDate(new Date(modal.dayISO + "T00:00:00"))} openSlots={openSlots}
        onPick={(s) => managerMovePlayer(modal.dayISO, modal.slotId, s.id, modal.player.name)}
        onClose={() => setModal(null)} />;
    } else { setModal(null); }
  } else if (modal?.type === "addPlayer") {
    const slot = (tour.slots[modal.dayISO] || []).find(s => s.id === modal.slotId);
    if (slot) {
      modalNode = <AddPlayerModal slot={slot} dayLabel={Y.fmtDate(new Date(modal.dayISO + "T00:00:00"))}
        members={accountMembers} weekSlots={tour.slots}
        onAdd={(info) => managerAddPlayer(modal.dayISO, modal.slotId, info)}
        onClose={() => setModal(null)} />;
    } else { setModal(null); }
  } else if (modal?.type === "move") {
    const b = modal.booking;
    const slot = slotOf(b);
    const openSlots = (tour.slots[b.dateISO] || []).filter(s => s.id !== b.slotId && s.players.length < 4);
    modalNode = <MoveSpotModal booking={b} fromTime={slot ? slot.time : ""} dateLabel={Y.fmtDate(new Date(b.dateISO + "T00:00:00"))} openSlots={openSlots} onPick={(s) => moveSpot(b, s)} onClose={() => setModal(null)} />;
  } else if (modal?.type === "release") {
    const b = modal.booking;
    const slot = slotOf(b);
    modalNode = <ConfirmModal title="Release this spot?" body={`Your spot in the ${slot ? slot.time : ""} group on ${Y.fmtDate(new Date(b.dateISO + "T00:00:00"))} will open back up for another member. This can't be undone.`} confirmLabel="Release spot" danger onConfirm={() => releaseSpot(b)} onClose={() => setModal(null)} />;
  } else if (modal?.type === "removeTime") {
    const s = modal.slot;
    const claimed = s.players.length;
    modalNode = <ConfirmModal title={`Remove the ${s.time} tee time?`} body={claimed ? `${claimed} member${claimed > 1 ? "s have" : " has"} already claimed a spot in this group. Removing it will release their booking.` : "This time will be removed from the published weekly entry."} confirmLabel="Remove tee time" danger onConfirm={() => removeTime(modal.dayISO, s.id)} onClose={() => setModal(null)} />;
  } else if (modal?.type === "ghinStale") {
    modalNode = <ConfirmModal title="Confirm your handicap to book" body="Before signing up for a tee time each week, please confirm your Handicap Index on your profile so weekend games use your current number. Update it (manually or from GHIN) and tap Save — then you're set for the week." confirmLabel="Go to profile" onConfirm={() => { setModal(null); go("profile"); }} onClose={() => setModal(null)} />;
  } else if (modal?.type === "emailManager") {
    modalNode = <RequestChangeModal member={member} primary={primaryManager} primaryEmail={primaryManagerEmail}
      bookings={liveBookings} slotOf={slotOf} tour={tour} phase={phase} toast={toast} onClose={() => setModal(null)} />;
  }

  return (
    <div className="app">
      <header className="topbar">
        <div className="topbar-inner">
          <button className="menu-toggle" onClick={() => setMobileNav(v => !v)} aria-label="Menu">
            <Icon name={mobileNav ? "x" : "menu"} size={20} />
          </button>
          <div className="brandmark">
            <div className="crest">YCC</div>
            <div className="brand-text">
              <div className="b1" style={{ whiteSpace: "nowrap" }}>Yardley Country Club</div>
              <div className="b2">DIV Tour</div>
            </div>
          </div>
          <nav className="nav">
            {(role === "manager" ? MANAGER_NAV : NAV).map(([id, label]) => (
              <button key={id} className={view === id ? "active" : ""} onClick={() => go(id)}>{label}</button>
            ))}
          </nav>
          <div className="topbar-right">
            {isManagerUser && (
              <div className="role-switch" role="group" aria-label="View as">
                <button className={role === "member" ? "on" : ""} onClick={() => switchRole("member")}>Member</button>
                <button className={role === "manager" ? "on" : ""} onClick={() => switchRole("manager")}>Manager</button>
              </div>
            )}
            <div className="user-menu-wrap" style={{ position: "relative" }}>
              <button className="avatar-btn" onClick={() => setUserMenu(v => !v)} aria-haspopup="true" aria-expanded={userMenu}>
                <div className="who"><div className="n">{role === "manager" ? activeManager.name : member.name}</div><div className="h">{role === "manager" ? `${activeManager.role} · on duty` : `HI ${member.handicapIndex ?? "—"} · GHIN ${member.ghin || "—"}`}</div></div>
                <div className="avatar">{role === "manager" ? activeManager.initials : member.initials}</div>
                <span className="caret" style={{ display: "flex", color: "rgba(244,241,230,.6)" }}><Icon name="chevD" size={15} /></span>
              </button>
              {userMenu && (
                <>
                  <div onClick={() => setUserMenu(false)} style={{ position: "fixed", inset: 0, zIndex: 59 }}></div>
                  <div className="user-menu" style={{ position: "absolute", top: "calc(100% + 8px)", right: 0, zIndex: 60, minWidth: 210, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: 12, boxShadow: "var(--shadow-lg)", overflow: "hidden", animation: "fadeIn .14s ease both" }}>
                    <div style={{ padding: "12px 14px", borderBottom: "1px solid var(--line)" }}>
                      <div style={{ fontSize: 13.5, fontWeight: 700, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{member.name}</div>
                      <div className="faint" style={{ fontSize: 11.5, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{member.email}</div>
                    </div>
                    <button className="user-menu-item" onClick={() => { setUserMenu(false); go(role === "manager" ? "manage" : "profile"); }}>
                      <Icon name="user" size={15} /> {role === "manager" ? "Tour Setup" : "My profile"}
                    </button>
                    <button className="user-menu-item danger" onClick={() => { setUserMenu(false); signOut(); }}>
                      <Icon name="logout" size={15} /> Sign out
                    </button>
                  </div>
                </>
              )}
            </div>
          </div>
        </div>
        {mobileNav && (
          <div className="mobile-nav">
            {isManagerUser && (
              <div className="role-switch role-switch-mobile" role="group">
                <button className={role === "member" ? "on" : ""} onClick={() => switchRole("member")}>Member view</button>
                <button className={role === "manager" ? "on" : ""} onClick={() => switchRole("manager")}>Manager view</button>
              </div>
            )}
            {(role === "manager" ? MANAGER_NAV : NAV).map(([id, label, icon]) => (
              <button key={id} className={view === id ? "active" : ""} onClick={() => go(id)}>
                <span style={{ display: "inline-flex", alignItems: "center", gap: 12 }}><Icon name={icon} size={18} /> {label}</span>
              </button>
            ))}
            <button onClick={signOut} style={{ color: "var(--accent)" }}>
              <span style={{ display: "inline-flex", alignItems: "center", gap: 12 }}><Icon name="logout" size={18} /> Sign out</span>
            </button>
          </div>
        )}
      </header>

      <main className="main">
        {view === "dashboard" && <Dashboard ctx={ctx} />}
        {view === "book" && <BookingScreen ctx={ctx} />}
        {view === "bookings" && <MyBookings ctx={ctx} />}
        {view === "games" && <GamesScreen ctx={ctx} />}
        {view === "profile" && <Profile ctx={ctx} />}
        {view === "manage" && isManagerUser && <ManagerScreen ctx={ctx} />}
        {view === "roster" && isManagerUser && <RosterScreen ctx={ctx} />}
        {view === "standings" && <SeasonScreen ctx={ctx} />}
      </main>

      {modalNode}
      <Toast msg={toastMsg} onDone={() => setToastMsg("")} />
      {role === "manager" && <SyncDock supabaseReady={supabaseReady} onExport={exportTourJSON} onImport={importTourJSON} onPush={pushSupabase} onPull={pullSupabase} />}
      <TweaksUI t={t} setTweak={setTweak} />
    </div>
  );
}

/* -------------------- SYNC DOCK (managers only) -------------------- */
function SyncDock({ supabaseReady, onExport, onImport, onPush, onPull }) {
  const [open, setOpen] = useState(false);
  const fileRef = useRef(null);
  const ok = !!supabaseReady;
  const statusColor = ok ? "var(--ok)" : "var(--danger)";
  const statusText = ok ? "Connected to Supabase" : "Working offline";

  return (
    <div
      onMouseEnter={() => setOpen(true)}
      onMouseLeave={() => setOpen(false)}
      style={{ position: "fixed", right: 18, bottom: 18, zIndex: 70, fontFamily: "var(--font-body)" }}
    >
      <input ref={fileRef} type="file" accept="application/json,.json" style={{ display: "none" }}
        onChange={(e) => { onImport(e.target.files[0]); e.target.value = ""; }} />

      {!open ? (
        <button onClick={() => setOpen(true)} title={statusText} style={{
          display: "flex", alignItems: "center", gap: 8, padding: "9px 14px", borderRadius: 999,
          background: "var(--surface)", border: "1px solid var(--line-strong)", boxShadow: "var(--shadow-md)",
          fontSize: 12.5, fontWeight: 700, color: "var(--ink)", cursor: "pointer",
        }}>
          <span style={{ width: 9, height: 9, borderRadius: "50%", background: statusColor, boxShadow: `0 0 0 3px color-mix(in srgb, ${statusColor} 22%, transparent)` }}></span>
          {ok ? "Synced" : "Offline"}
        </button>
      ) : (
        <div style={{ width: 248, background: "var(--surface)", border: "1px solid var(--line-strong)", borderRadius: 14, boxShadow: "var(--shadow-lg)", overflow: "hidden", animation: "fadeIn .15s ease both" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 9, padding: "12px 14px", borderBottom: "1px solid var(--line)" }}>
            <span style={{ width: 9, height: 9, borderRadius: "50%", background: statusColor, boxShadow: `0 0 0 3px color-mix(in srgb, ${statusColor} 22%, transparent)`, flex: "0 0 auto" }}></span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 12.5, fontWeight: 700, whiteSpace: "nowrap" }}>{statusText}</div>
              <div className="faint" style={{ fontSize: 10.5, letterSpacing: ".04em", textTransform: "uppercase" }}>Tour data · JSON</div>
            </div>
          </div>
          <div style={{ padding: 12, display: "flex", flexDirection: "column", gap: 8 }}>
            <div style={{ display: "flex", gap: 8 }}>
              <button className="btn btn-primary btn-sm" style={{ flex: 1 }} onClick={onExport}><Icon name="logout" size={14} style={{ transform: "rotate(-90deg)" }} /> Export</button>
              <button className="btn btn-ghost btn-sm" style={{ flex: 1 }} onClick={() => fileRef.current && fileRef.current.click()}><Icon name="logout" size={14} style={{ transform: "rotate(90deg)" }} /> Import</button>
            </div>
            <div style={{ display: "flex", gap: 8 }}>
              <button className="btn btn-quiet btn-sm" style={{ flex: 1, opacity: ok ? 1 : .5 }} onClick={onPush}><Icon name="shield" size={13} /> Push</button>
              <button className="btn btn-quiet btn-sm" style={{ flex: 1, opacity: ok ? 1 : .5 }} onClick={onPull}><Icon name="shield" size={13} /> Pull</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* -------------------- TWEAKS PANEL -------------------- */
function TweaksUI({ t, setTweak }) {
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Theme" />
      <TweakRow label="Club colors">
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          {Object.entries(THEMES).map(([key, th]) => {
            const on = t.theme === key;
            return (
              <button key={key} onClick={() => setTweak("theme", key)} title={th.label} style={{
                display: "flex", alignItems: "center", gap: 6, padding: "6px 8px", borderRadius: 9,
                border: "1.5px solid " + (on ? th.dots[1] : "rgba(0,0,0,.12)"),
                background: on ? "rgba(0,0,0,.03)" : "transparent", cursor: "pointer",
              }}>
                <span style={{ display: "flex" }}>
                  <span style={{ width: 16, height: 16, borderRadius: "50%", background: th.dots[0] }}></span>
                  <span style={{ width: 16, height: 16, borderRadius: "50%", background: th.dots[1], marginLeft: -6, border: "1.5px solid #fff" }}></span>
                </span>
                <span style={{ fontSize: 11.5, fontWeight: 600 }}>{th.label}</span>
              </button>
            );
          })}
        </div>
      </TweakRow>

      <TweakSection label="Typography" />
      <TweakRadio label="Type pairing" value={t.fontPair}
        options={[{ value: "elegant", label: "Heritage" }, { value: "editorial", label: "Refined" }, { value: "modern", label: "Modern" }]}
        onChange={(v) => setTweak("fontPair", v)} />

      <TweakSection label="Layout" />
      <TweakRadio label="Density" value={t.density}
        options={["compact", "regular", "comfy"]}
        onChange={(v) => setTweak("density", v)} />
      <TweakRadio label="Booking flow" value={t.bookingFlow}
        options={[{ value: "sheet", label: "Tee sheet" }, { value: "grid", label: "Slot grid" }]}
        onChange={(v) => setTweak("bookingFlow", v)} />
    </TweaksPanel>
  );
}

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