/* global React */

// ---------------------------------------------------------------
// shared.jsx — Cambridge Ark Coaching
// Bilingual store + hook, header/footer chrome, primitives.
// ---------------------------------------------------------------

// ---------- small storage helpers -----------------------------
// Everything here is wrapped in try/catch: Safari private mode and
// "block all cookies" both throw rather than failing quietly.
function readCookie(name) {
  try {
    const hit = document.cookie.split("; ").find((c) => c.startsWith(name + "="));
    return hit ? decodeURIComponent(hit.slice(name.length + 1)) : null;
  } catch (e) { return null; }
}
function writeCookie(name, value, days = 365) {
  try {
    const secure = location.protocol === "https:" ? "; secure" : "";
    document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${days * 86400}; samesite=lax${secure}`;
  } catch (e) {}
}
function deleteCookie(name) {
  try { document.cookie = `${name}=; path=/; max-age=0; samesite=lax`; } catch (e) {}
}
function readLocal(key) {
  try { return localStorage.getItem(key); } catch (e) { return null; }
}
function writeLocal(key, value) {
  try { localStorage.setItem(key, value); } catch (e) {}
}
function deleteLocal(key) {
  try { localStorage.removeItem(key); } catch (e) {}
}

// ---------- cookie consent ------------------------------------
// PECR lets us store what's strictly necessary without asking — the
// login session, and the record of this choice itself. Everything
// else (language, timezone, saved contact details) is optional and
// stays in memory only until the visitor opts in, so a page load by
// someone who hasn't chosen writes nothing at all.
const CONSENT_COOKIE = "ca_consent";
const OPTIONAL_COOKIES = ["ca_lang", "ca_tz"];
const OPTIONAL_LOCAL = ["ca_lang", "ca_tz", "ca_contact"];

const ConsentStore = {
  // "all" = optional storage allowed, "necessary" = declined, null = undecided.
  value: (typeof document !== "undefined" && readCookie(CONSENT_COOKIE)) || null,
  listeners: new Set(),
  get decided() { return this.value === "all" || this.value === "necessary"; },
  get allowsOptional() { return this.value === "all"; },
  set(value) {
    this.value = value;
    writeCookie(CONSENT_COOKIE, value);
    if (value === "all") persistOptional(); else forgetOptional();
    this.listeners.forEach((fn) => fn(value));
  },
  subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
};

function forgetOptional() {
  OPTIONAL_COOKIES.forEach(deleteCookie);
  OPTIONAL_LOCAL.forEach(deleteLocal);
}
// Called the moment consent is given, so a choice the visitor already
// made this session (they toggled 中文, then accepted) sticks straight
// away rather than needing to be made again.
function persistOptional() {
  writeLocal("ca_lang", LangStore.lang);
  writeCookie("ca_lang", LangStore.lang);
  writeLocal("ca_tz", TzStore.mode);
  writeCookie("ca_tz", TzStore.mode);
}

// An earlier build wrote these before asking. If there's no consent
// record, clear them — they were never opted into.
if (typeof document !== "undefined" && !ConsentStore.decided) forgetOptional();

function useConsent() {
  const [value, setValue] = React.useState(ConsentStore.value);
  React.useEffect(() => ConsentStore.subscribe(setValue), []);
  return {
    value,
    decided: value === "all" || value === "necessary",
    allowsOptional: value === "all",
    set: (v) => ConsentStore.set(v),
  };
}

// ---------- bilingual store (EN / 中文) -----------------------
// When consent allows, the choice is mirrored into a cookie as well as
// localStorage so the server can set <html lang> and a translated
// <title> before the page reaches React. Without consent it lives in
// memory for the current page only.
const LangStore = {
  lang: (typeof document !== "undefined" && ConsentStore.allowsOptional
         && (readLocal("ca_lang") || readCookie("ca_lang"))) || "en",
  listeners: new Set(),
  set(lang) {
    this.lang = lang;
    if (ConsentStore.allowsOptional) {
      writeLocal("ca_lang", lang);
      writeCookie("ca_lang", lang);
    }
    this.listeners.forEach((fn) => fn(lang));
  },
  subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
};

function useLang() {
  const [lang, setLang] = React.useState(LangStore.lang);
  React.useEffect(() => LangStore.subscribe(setLang), []);
  const t = (en, zh) => (lang === "zh" ? (zh ?? en) : en);
  return { lang, setLang: (l) => LangStore.set(l), t };
}

// ---------- timezone preference -------------------------------
// Tao coaches across 8+ countries, but every time on the site was
// hardcoded to Europe/London with nothing on screen saying so — a parent
// in Shanghai was doing the arithmetic in their head. Times now render in
// the visitor's own zone by default, always labelled, with a toggle back
// to UK time. The choice rides in a cookie so it survives a new device
// session and is readable server-side later if booking emails follow.
const UK_ZONE = "Europe/London";

function browserZone() {
  try { return Intl.DateTimeFormat().resolvedOptions().timeZone || UK_ZONE; }
  catch (e) { return UK_ZONE; }
}

const TzStore = {
  // "local" = the visitor's own zone, "uk" = Europe/London. Only
  // persisted once the visitor has opted in; otherwise it defaults to
  // their own zone each page, which is the sensible default anyway.
  mode: (typeof document !== "undefined" && ConsentStore.allowsOptional
         && (readLocal("ca_tz") || readCookie("ca_tz"))) || "local",
  listeners: new Set(),
  set(mode) {
    this.mode = mode;
    if (ConsentStore.allowsOptional) {
      writeLocal("ca_tz", mode);
      writeCookie("ca_tz", mode);
    }
    this.listeners.forEach((fn) => fn(mode));
  },
  subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
};

function useTz() {
  const [mode, setMode] = React.useState(TzStore.mode);
  React.useEffect(() => TzStore.subscribe(setMode), []);

  const local = browserZone();
  // A visitor already on UK time needs no toggle and no clutter.
  const isUkVisitor = local === UK_ZONE;
  const zone = mode === "uk" || isUkVisitor ? UK_ZONE : local;

  const fmtTime = (iso) =>
    new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: zone });

  const fmtDate = (iso, opts) =>
    new Date(iso).toLocaleDateString("en-GB", { ...opts, timeZone: zone });

  // "BST", "CST" — short names keep the labels from swamping the times.
  let label = zone;
  try {
    const part = new Intl.DateTimeFormat("en-GB", { timeZone: zone, timeZoneName: "short" })
      .formatToParts(new Date()).find((p) => p.type === "timeZoneName");
    if (part) label = part.value;
  } catch (e) {}

  return { mode, setMode: (m) => TzStore.set(m), zone, label, isUkVisitor, localZone: local, fmtTime, fmtDate };
}

// ---------- editable site content (EN/ZH overrides) -------------
// Lets admin-edited copy (via the content editor) override the
// hardcoded defaults in JSX, without a code deploy. Falls back to
// the defaults whenever a field hasn't been edited.
const ContentStore = {
  data: {},
  loaded: false,
  listeners: new Set(),
  async fetch() {
    try {
      const r = await window.fetch("/api/content");
      this.data = await r.json();
    } catch (e) {}
    this.loaded = true;
    this.listeners.forEach((fn) => fn(this.data));
  },
  subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
};
ContentStore.fetch();

// Editable copy is stored as plain text, so there's no way to emphasise a
// phrase from the content editor — and any bold baked into a JSX default is
// silently discarded the moment someone edits that field. So **double
// asterisks** mark emphasis in editable text, here and in the editor itself.
// Returns the original string untouched when there's no markup, because ct()
// also feeds attributes (alt, placeholder) that can't take React nodes.
function renderMarkup(text) {
  if (typeof text !== "string" || text.indexOf("**") === -1) return text;
  const parts = text.split(/\*\*(.+?)\*\*/g);
  if (parts.length === 1) return text;
  return React.createElement(
    React.Fragment, null,
    ...parts.map((part, i) =>
      i % 2
        ? React.createElement("strong", { key: i, style: { fontWeight: 600 } }, part)
        : part)
  );
}

function useCT() {
  const { lang, setLang, t } = useLang();
  const [overrides, setOverrides] = React.useState(ContentStore.data);
  React.useEffect(() => ContentStore.subscribe(setOverrides), []);
  function ct(id, en, zh) {
    const o = overrides[id];
    const enVal = (o && typeof o.en === "string" && o.en) || en;
    const zhVal = (o && typeof o.zh === "string" && o.zh) || zh;
    return renderMarkup(lang === "zh" ? (zhVal ?? enVal) : enVal);
  }
  // For fields whose text is always shown in one fixed language regardless of
  // the site's EN/中文 toggle (e.g. the bilingual band's two static cards).
  function raw(id, fallback) {
    const o = overrides[id];
    return renderMarkup((o && typeof o.en === "string" && o.en) || fallback);
  }
  return { lang, setLang, t, ct, raw };
}

// ---------- responsive hook ------------------------------------
function useIsMobile(breakpoint = 768) {
  const [isMobile, setIsMobile] = React.useState(
    typeof window !== "undefined" && window.innerWidth <= breakpoint
  );
  React.useEffect(() => {
    const onResize = () => setIsMobile(window.innerWidth <= breakpoint);
    onResize();
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, [breakpoint]);
  return isMobile;
}

// ---------- icons (lucide-flavoured 1.5px inline) -------------
const I = {
  Arrow: ({ size = 16 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
  ),
  WhatsApp: ({ size = 18 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8z"/></svg>
  ),
  Globe: ({ size = 16 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
  ),
  Spark: ({ size = 18 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/></svg>
  ),
  Calendar: ({ size = 18 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 9h18M8 3v4M16 3v4"/></svg>
  ),
  Users: ({ size = 18 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9.5" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/></svg>
  ),
  Heart: ({ size = 18 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78z"/></svg>
  ),
  Compass: ({ size = 18 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
  ),
  Check: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
  ),
  Quote: ({ size = 28 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"><path d="M9.5 6c-3.6 1.4-6 5-6 9 0 2 1.5 3 3 3 1.6 0 3-1.4 3-3s-1.4-3-3-3c-.3 0-.6 0-.9.1.5-2.2 2.2-4 4.4-4.9L9.5 6zm9 0c-3.6 1.4-6 5-6 9 0 2 1.5 3 3 3 1.6 0 3-1.4 3-3s-1.4-3-3-3c-.3 0-.6 0-.9.1.5-2.2 2.2-4 4.4-4.9L18.5 6z"/></svg>
  ),
};

// ---------- primitives ----------------------------------------
function Eyebrow({ children, color = "var(--ink-navy)", style }) {
  return (
    <div style={{
      fontFamily: "var(--font-body)", fontWeight: 600,
      fontSize: 12, letterSpacing: ".22em",
      textTransform: "uppercase", color, ...style,
    }}>{children}</div>
  );
}

function Button({ kind = "coral", children, onClick, href, style, size = "md" }) {
  const sizes = {
    sm: { padding: "9px 18px", fontSize: 12 },
    md: { padding: "14px 26px", fontSize: 13.5 },
    lg: { padding: "18px 34px", fontSize: 15 },
  };
  const base = {
    fontFamily: "var(--font-body)", fontWeight: 600,
    letterSpacing: ".06em", borderRadius: 999,
    border: "none", cursor: "pointer",
    transition: "background .18s, color .18s, transform .12s, box-shadow .22s",
    display: "inline-flex", alignItems: "center", gap: 10,
    textDecoration: "none",
    ...sizes[size],
    ...style,
  };
  const variants = {
    primary: { background: "var(--ink-navy)", color: "var(--paper)", boxShadow: "0 2px 6px rgba(11,53,80,.16)" },
    coral:   { background: "var(--coral)",    color: "var(--ink)",   boxShadow: "var(--shadow-coral)" },
    ghost:   { background: "transparent",     color: "var(--ink-navy)", border: "1.5px solid var(--ink-navy)" },
    inkGhost:{ background: "transparent",     color: "var(--paper)",    border: "1.5px solid rgba(253, 250, 243,.55)" },
  };
  const Tag = href ? "a" : "button";
  return (
    <Tag href={href} style={{ ...base, ...variants[kind] }} onClick={onClick}>
      {children}
    </Tag>
  );
}

function PageFrame({ children, style, max = 1180 }) {
  const mobile = useIsMobile();
  return (
    <div style={{ maxWidth: max, margin: "0 auto", padding: mobile ? "0 20px" : "0 40px", ...style }}>
      {children}
    </div>
  );
}

// ---------- Auth store ----------------------------------------
const AuthStore = {
  user: null,
  loaded: false,
  listeners: new Set(),
  async fetch() {
    try {
      const r = await window.fetch("/api/auth/me");
      const d = await r.json();
      this.user = d.user || null;
    } catch {}
    this.loaded = true;
    this.listeners.forEach(fn => fn(this.user));
  },
  set(user) {
    this.user = user; this.loaded = true;
    this.listeners.forEach(fn => fn(user));
  },
  subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
};
AuthStore.fetch();

function useAuth() {
  const [user, setUser] = React.useState(AuthStore.user);
  React.useEffect(() => AuthStore.subscribe(setUser), []);
  return user;
}

// ---------- Header --------------------------------------------
function Header({ active = "home", base = "" }) {
  const { lang, setLang, t } = useLang();
  const authUser = useAuth();

  const link = (path) => base + path;
  const nav = [
    { id: "home",     en: "Home",       zh: "主页",      href: "/" },
    { id: "coaching", en: "Coaching",   zh: "一对一教练",  href: "/coaching" },
    { id: "groups",   en: "Groups",     zh: "小组项目",    href: "/groups" },
    { id: "about",    en: "About",      zh: "关于我们",    href: "/about" },
  ];

  const mobile = useIsMobile();
  const [menuOpen, setMenuOpen] = React.useState(false);
  const [hidden, setHidden] = React.useState(false); // mobile: hide on scroll-down, reveal on scroll-up

  React.useEffect(() => {
    if (!mobile) { setHidden(false); return; }
    let lastY = window.scrollY;
    const onScroll = () => {
      const y = window.scrollY;
      if (y < 64) setHidden(false);          // always visible near the top
      else if (y > lastY + 4) setHidden(true);   // scrolling down → hide
      else if (y < lastY - 4) setHidden(false);  // scrolling up → reveal
      lastY = y;
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [mobile]);

  async function handleLogout() {
    await window.fetch("/api/auth/logout", { method: "POST" });
    AuthStore.set(null);
    window.location.href = "/";
  }

  const navLink = (l, big) => {
    const isActive = active === l.id;
    return (
      <a key={l.id} href={l.href} onClick={() => setMenuOpen(false)} style={{
        fontFamily: "var(--font-body)", fontWeight: big ? 600 : 500,
        fontSize: big ? 15 : 12.5, letterSpacing: ".2em", textTransform: "uppercase",
        color: isActive ? "var(--ink-navy)" : "var(--ink)",
        textDecoration: "none", position: "relative",
        padding: big ? "12px 0" : "8px 0",
        borderBottom: big ? "1px solid var(--border-1)" : "none",
        display: big ? "block" : "inline-block",
      }}>
        {t(l.en, l.zh)}
        {isActive && !big && <span style={{ position: "absolute", left: 0, right: 0, bottom: -2, height: 2, background: "var(--coral)", borderRadius: 2 }}/>}
      </a>
    );
  };

  const authActions = (big) => (authUser ? (
    <>
      {authUser.role === "admin" && (
        <a href="/admin" onClick={() => setMenuOpen(false)} style={{ fontFamily: "var(--font-body)", fontWeight: big ? 600 : 500, fontSize: big ? 15 : 12.5, letterSpacing: ".2em", textTransform: "uppercase", color: "var(--ink)", textDecoration: "none", padding: big ? "12px 0" : 0, borderBottom: big ? "1px solid var(--border-1)" : "none", display: big ? "block" : "inline" }}>
          Admin
        </a>
      )}
      <a href="/dashboard" onClick={() => setMenuOpen(false)} style={{ fontFamily: "var(--font-body)", fontWeight: big ? 600 : 500, fontSize: big ? 15 : 12.5, letterSpacing: ".2em", textTransform: "uppercase", color: "var(--ink)", textDecoration: "none", padding: big ? "12px 0" : 0, borderBottom: big ? "1px solid var(--border-1)" : "none", display: big ? "block" : "inline" }}>
        {t("My bookings", "我的预约")}
      </a>
      <Button kind="ghost" size="sm" onClick={handleLogout} style={big ? { marginTop: 8 } : {}}>
        {t("Sign out", "退出")}
      </Button>
    </>
  ) : (
    <>
      <a href="/login" onClick={() => setMenuOpen(false)} style={{ fontFamily: "var(--font-body)", fontWeight: big ? 600 : 500, fontSize: big ? 15 : 12.5, letterSpacing: ".2em", textTransform: "uppercase", color: "var(--ink)", textDecoration: "none", padding: big ? "12px 0" : 0, borderBottom: big ? "1px solid var(--border-1)" : "none", display: big ? "block" : "inline" }}>
        {t("Sign in", "登录")}
      </a>
      <Button kind="coral" size="sm" href="/book" style={big ? { marginTop: 8 } : {}}>
        {t("Book a session", "预约课程")}&nbsp;<I.Arrow/>
      </Button>
    </>
  ));

  return (
    <header style={{
      position: "sticky", top: 0, zIndex: 20,
      background: "rgba(253, 250, 243, 0.88)",
      backdropFilter: "blur(10px)", WebkitBackdropFilter: "blur(10px)",
      borderBottom: "1px solid var(--border-1)",
      transform: (hidden && !menuOpen) ? "translateY(-100%)" : "translateY(0)",
      transition: "transform .25s ease",
    }}>
      <div style={{
        maxWidth: 1280, margin: "0 auto", padding: mobile ? "14px 20px" : "16px 40px",
        display: "flex", alignItems: "center", justifyContent: "space-between", gap: 24,
      }}>
        <a href="/" style={{ display: "flex", alignItems: "center", gap: 14, textDecoration: "none" }}>
          <img src={base + "assets/logo-boat-heart.png"} alt="" style={{ width: mobile ? 36 : 42, height: "auto" }}/>
          <div style={{ lineHeight: 1 }}>
            <div style={{ fontFamily: "var(--font-display)", fontWeight: 500, fontSize: mobile ? 17 : 19, color: "var(--ink)", letterSpacing: ".005em" }}>
              {t("Tao Yu", "Tao Yu")}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontWeight: 500, fontSize: mobile ? 9 : 10, letterSpacing: ".24em", textTransform: "uppercase", color: "var(--fg-3)", marginTop: 5 }}>
              {t("Cambridge Ark Coaching", "Cambridge Ark Coaching")}
            </div>
          </div>
        </a>

        {!mobile ? (
          <nav style={{ display: "flex", alignItems: "center", gap: 28 }}>
            {nav.map((l) => navLink(l, false))}
            <LangToggle lang={lang} setLang={setLang}/>
            {authActions(false)}
          </nav>
        ) : (
          <button onClick={() => setMenuOpen(o => !o)} aria-label="Menu" aria-expanded={menuOpen} style={{
            width: 44, height: 44, borderRadius: 12, border: "1.5px solid var(--border-1)",
            background: "var(--paper)", color: "var(--ink-navy)", cursor: "pointer",
            display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
          }}>
            {menuOpen
              ? <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
              : <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg>}
          </button>
        )}
      </div>

      {/* Mobile dropdown menu */}
      {mobile && menuOpen && (
        <div style={{
          borderTop: "1px solid var(--border-1)", background: "var(--paper)",
          padding: "8px 20px 20px", display: "flex", flexDirection: "column",
        }}>
          {nav.map((l) => navLink(l, true))}
          <div style={{ display: "flex", flexDirection: "column", gap: 10, marginTop: 16 }}>
            {authActions(true)}
            <div style={{ marginTop: 6 }}><LangToggle lang={lang} setLang={setLang}/></div>
          </div>
        </div>
      )}
    </header>
  );
}

function LangToggle({ lang, setLang }) {
  const btn = (val, label) => (
    <button onClick={() => setLang(val)} style={{
      fontFamily: "var(--font-body)", fontWeight: 600, fontSize: 11.5,
      letterSpacing: ".12em",
      padding: "6px 10px", borderRadius: 999, cursor: "pointer",
      border: "none",
      background: lang === val ? "var(--ink-navy)" : "transparent",
      color: lang === val ? "var(--paper)" : "var(--fg-2)",
      transition: "background .15s, color .15s",
    }}>{label}</button>
  );
  return (
    <div style={{
      display: "inline-flex", alignItems: "center", gap: 0,
      padding: 3, background: "var(--paper-2)",
      borderRadius: 999, border: "1px solid var(--border-1)",
    }}>
      {btn("en", "EN")}
      {btn("zh", "中文")}
    </div>
  );
}

// ---------- Footer --------------------------------------------
function Footer({ base = "" }) {
  const { t } = useLang();
  return (
    <footer style={{ background: "var(--paper-2)", borderTop: "1px solid var(--border-1)", padding: "56px 40px 44px" }}>
      <div style={{ maxWidth: 1180, margin: "0 auto" }}>
        <div style={{ display: "grid", gridTemplateColumns: "1.3fr 1fr", gap: 56, alignItems: "start" }}>
          <div>
            <div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 28 }}>
              <img src={base + "assets/logo-boat-heart.png"} alt="" style={{ width: 38 }}/>
              <div style={{ lineHeight: 1.1 }}>
                <div style={{ fontFamily: "var(--font-display)", fontSize: 22, color: "var(--ink)" }}>{t("Tao Yu", "Tao Yu")}</div>
                <div style={{ fontSize: 10.5, letterSpacing: ".22em", textTransform: "uppercase", color: "var(--fg-3)", marginTop: 5 }}>
                  {t("Cambridge Ark Coaching", "Cambridge Ark Coaching")}
                </div>
              </div>
            </div>
            <p style={{ fontFamily: "var(--font-body)", fontSize: 15, lineHeight: 1.65, color: "var(--fg-2)", margin: 0, maxWidth: "44ch" }}>
              {t(
                "A practice for neurodivergent adults and the families who walk beside them. ICF-accredited. Cambridge, UK.",
                "为神经多样性成人及其家庭提供国际教练联合会认证的教练服务。位于英国剑桥。"
              )}
            </p>
            <div style={{ marginTop: 22, display: "flex", alignItems: "center", gap: 24 }}>
              <img src={base + "assets/badge-icf.png"} alt="ICF" style={{ height: 34 }}/>
              <img src={base + "assets/badge-insight-coaching.png"} alt="Insight Coaching" style={{ height: 44 }}/>
            </div>
          </div>

          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 28 }}>
            <FooterCol title={t("Practice", "教练")} links={[
              [t("1:1 Coaching", "一对一教练"), "/coaching"],
              [t("Group Programmes", "小组项目"), "/groups"],
              [t("Family Consulting", "家庭咨询"), "/coaching#parents"],
            ]}/>
            <FooterCol title={t("Reach Tao", "联系 Tao")} links={[
              [t("WhatsApp", "WhatsApp"), "#contact"],
              [t("WeChat", "微信"), "#contact"],
              ["cambridgearkcoaching@gmail.com", "mailto:cambridgearkcoaching@gmail.com"],
              ["+44 (0) 7986 935783", "tel:+447986935783"],
            ]}/>
          </div>
        </div>

        <div style={{
          marginTop: 40, paddingTop: 22, borderTop: "1px dashed var(--border-1)",
          display: "flex", justifyContent: "space-between", alignItems: "center",
          fontFamily: "var(--font-body)", fontSize: 12, color: "var(--fg-3)",
        }}>
          <span>© 2026 {t("Tao Yu — All rights reserved.", "Tao Yu · 保留所有权利。")}</span>
          <span style={{ fontFamily: "var(--font-hand)", fontSize: 32, color: "var(--ink-navy)" }}>~ {t("Tao Yu", "Tao Yu")}</span>
        </div>
      </div>
    </footer>
  );
}
function FooterCol({ title, links }) {
  return (
    <div>
      <div style={{ fontSize: 11, letterSpacing: ".22em", textTransform: "uppercase", color: "var(--fg-3)", fontWeight: 600, marginBottom: 14 }}>{title}</div>
      <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 9 }}>
        {links.map(([label, href]) => (
          <li key={label}><a href={href} style={{ fontFamily: "var(--font-body)", fontSize: 14, color: "var(--ink)", textDecoration: "none" }}>{label}</a></li>
        ))}
      </ul>
    </div>
  );
}

// ---------- CentredSpinner -----------------------------------
function CentredSpinner() {
  return (
    <div style={{ minHeight: "60vh", display: "flex", alignItems: "center", justifyContent: "center" }}>
      <div style={{
        width: 36, height: 36, borderRadius: "50%",
        border: "3px solid var(--border-1)",
        borderTopColor: "var(--coral)",
        animation: "ca-spin .7s linear infinite",
      }}/>
      <style>{`@keyframes ca-spin { to { transform: rotate(360deg); } }`}</style>
    </div>
  );
}

// ---------- RevealEngine -------------------------------------
// Subtle scroll-reveal: sections fade up as they enter the viewport;
// containers tagged data-ca-stagger fan their direct children in
// with a small per-index delay. Respects prefers-reduced-motion.
function RevealEngine() {
  React.useEffect(() => {
    if (!document.getElementById("ca-reveal-css")) {
      const s = document.createElement("style");
      s.id = "ca-reveal-css";
      s.textContent = `
        [data-ca-reveal] {
          opacity: 0;
          transform: translateY(28px);
          transition:
            opacity .9s cubic-bezier(.22,.61,.36,1),
            transform .9s cubic-bezier(.22,.61,.36,1);
          transition-delay: var(--ca-reveal-delay, 0ms);
          will-change: opacity, transform;
        }
        [data-ca-reveal].is-visible {
          opacity: 1;
          transform: none;
        }
        @media (prefers-reduced-motion: reduce) {
          [data-ca-reveal] {
            opacity: 1 !important;
            transform: none !important;
            transition: none !important;
          }
        }
      `;
      document.head.appendChild(s);
    }

    // Section-level reveal — skip the very first (hero)
    document.querySelectorAll("main > section:not(:first-of-type)").forEach((sec) => {
      sec.setAttribute("data-ca-reveal", "");
    });

    // Children stagger inside containers marked with data-ca-stagger
    document.querySelectorAll("[data-ca-stagger]").forEach((parent) => {
      const step = Number(parent.getAttribute("data-ca-stagger-step")) || 90;
      const startDelay = Number(parent.getAttribute("data-ca-stagger-start")) || 0;
      Array.from(parent.children).forEach((child, i) => {
        child.setAttribute("data-ca-reveal", "");
        child.style.setProperty("--ca-reveal-delay", (startDelay + i * step) + "ms");
      });
    });

    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            entry.target.classList.add("is-visible");
            io.unobserve(entry.target);
          }
        });
      },
      { rootMargin: "0px 0px -8% 0px", threshold: 0.05 }
    );

    document.querySelectorAll("[data-ca-reveal]").forEach((el) => io.observe(el));

    return () => io.disconnect();
  }, []);
  return null;
}

// ---------- cookie consent banner -----------------------------
// Deliberately not a modal: it doesn't trap the page, and "Necessary
// only" sits next to "Accept" with equal weight rather than being
// buried behind a settings link.
function ConsentBanner() {
  const { t } = useLang();
  const consent = useConsent();
  const [detail, setDetail] = React.useState(false);
  if (consent.decided) return null;

  const btn = (primary) => ({
    fontFamily: "var(--font-body)", fontWeight: 600, fontSize: 13.5,
    padding: "10px 20px", borderRadius: 999, cursor: "pointer",
    border: primary ? "none" : "1.5px solid var(--border-1)",
    background: primary ? "var(--coral)" : "transparent",
    color: primary ? "var(--ink)" : "var(--fg-2)",
    whiteSpace: "nowrap",
  });

  return (
    <div role="region" aria-label={t("Cookie choices", "Cookie 选择")}
      style={{
        position: "fixed", left: 16, right: 16, bottom: 16, zIndex: 999,
        maxWidth: 720, margin: "0 auto", background: "var(--paper)",
        border: "1.5px solid var(--border-1)", borderRadius: 18,
        boxShadow: "var(--shadow-3)", padding: "20px 22px",
      }}>
      <p style={{ fontFamily: "var(--font-body)", fontSize: 14, lineHeight: 1.6, color: "var(--fg-2)", margin: "0 0 12px" }}>
        {t("This site stores only what it needs to work. We'd also like to remember your language, timezone and contact details to save you repeating yourself — only if you're happy with that.",
           "本网站仅存储运行所必需的信息。我们也希望记住你的语言、时区与联系方式，让你无需重复填写 — 但这完全取决于你是否同意。")}
      </p>

      <div style={{ display: "flex", gap: 10, flexWrap: "wrap", alignItems: "center" }}>
        <button type="button" style={btn(true)} onClick={() => consent.set("all")}>
          {t("Accept", "接受")}
        </button>
        <button type="button" style={btn(false)} onClick={() => consent.set("necessary")}>
          {t("Necessary only", "仅必要")}
        </button>
        <button type="button"
          onClick={() => setDetail((d) => !d)}
          aria-expanded={detail}
          style={{
            appearance: "none", background: "none", border: "none", padding: 0,
            fontFamily: "var(--font-body)", fontSize: 13, cursor: "pointer",
            color: "var(--coral-deep)", textDecoration: "underline", marginLeft: "auto",
          }}>
          {t("What's stored?", "存储了什么？")}
        </button>
      </div>

      {detail && (
        <dl style={{ fontFamily: "var(--font-body)", fontSize: 12.5, lineHeight: 1.6, color: "var(--fg-3)", margin: "16px 0 0", display: "grid", gridTemplateColumns: "auto 1fr", gap: "6px 14px" }}>
          <dt style={{ fontWeight: 600 }}>{t("Necessary", "必要")}</dt>
          <dd style={{ margin: 0 }}>
            {t("Your sign-in session, and a record of this choice. Always on.",
               "你的登录会话，以及这次选择的记录。始终启用。")}
          </dd>
          <dt style={{ fontWeight: 600 }}>{t("Optional", "可选")}</dt>
          <dd style={{ margin: 0 }}>
            {t("Language (English/中文), whether times show in your timezone or UK time, and your name, email and phone after you send an enquiry. Never the message itself. All kept in your browser only.",
               "语言（English/中文）、时间以你所在时区还是英国时间显示，以及你发送咨询后的姓名、邮箱与电话。绝不保存留言内容。全部仅存于你的浏览器中。")}
          </dd>
        </dl>
      )}
    </div>
  );
}

// Mounted from here rather than from a page component: the booking,
// dashboard and admin screens don't render Header/Footer, but they do
// load this file and they do use the stored preferences.
function mountConsentBanner() {
  if (typeof document === "undefined" || !window.ReactDOM) return;
  if (document.getElementById("ca-consent-root")) return;
  const host = document.createElement("div");
  host.id = "ca-consent-root";
  document.body.appendChild(host);
  ReactDOM.createRoot(host).render(<ConsentBanner/>);
}
if (typeof document !== "undefined") {
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", mountConsentBanner);
  } else {
    mountConsentBanner();
  }
}

Object.assign(window, {
  LangStore, useLang, ContentStore, useCT, useIsMobile, I, Eyebrow, Button, PageFrame,
  Header, LangToggle, Footer, FooterCol, RevealEngine,
  TzStore, useTz, readCookie, writeCookie, renderMarkup,
  ConsentStore, useConsent, ConsentBanner,
});
