// CalGrillz — shared shell (header, footer, ticker, common UI). Loaded on every page.

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

// Laravel-friendly URL resolver. The Blade wrapper injects window.CG_URLS
// with route-generated paths; we fall back to the original *.html names so
// the template still previews standalone (e.g. opening index.html directly).
const CG_URL = (key, fallback) => (window.CG_URLS && window.CG_URLS[key]) || fallback;
// Asset resolver — same fallback pattern. Lets the Blade host serve assets
// from /template2/assets/... while the standalone preview uses ./assets/...
const CG_ASSET = (name) => {
  if (!name) return name;
  // Uploaded photos are stored as absolute URLs (https://…/storage/…) or
  // root-anchored paths (/storage/…), and data: URIs are self-contained — use
  // any of those as-is. Prepending the template asset base to a full URL is what
  // produced the doubled "website address twice" link. Only bare filenames
  // (the template's own bundled assets) get the base.
  if (/^(https?:)?\/\//i.test(name) || name.charAt(0) === '/' || name.slice(0, 5) === 'data:') return name;
  return (window.CG_ASSET_BASE || 'assets') + '/' + name;
};

// ── Shared CMS helpers (available to EVERY page via the shell) ──
// cgC(key, fallback): read an owner-edited value from window.CG_CONTENT.
// cgImg(value): use an absolute CMS URL (/storage/… or http…) as-is; only
//   a bare bundled-asset filename gets the /template2/assets prefix.
// cgHtml(value): render a sanitized CMS html string (server allowlists tags).
function cgC(key, fallback = '') {
  const v = (window.CG_CONTENT || {})[key];
  return (v === undefined || v === null || v === '') ? fallback : v;
}
const cgImg = (v) => (!v ? '' : (/^(https?:|\/)/.test(v) ? v : CG_ASSET(v)));
function cgHtml(value) {
  return <span dangerouslySetInnerHTML={{ __html: value }} />;
}

// ── Social links ─────────────────────────────────────────────────────────
// Keep the public profiles in one place so the header, footer, and Contact
// page always show the same real destinations and recognizable brand marks.
function CGSocialIcon({ name, size = 16 }) {
  const iconProps = {
    width: size,
    height: size,
    viewBox: '0 0 24 24',
    'aria-hidden': 'true',
    focusable: 'false',
  };

  switch (name) {
    case 'instagram':
      return <svg {...iconProps} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r="1" fill="currentColor" stroke="none"/></svg>;
    case 'tiktok':
      return <svg {...iconProps} fill="currentColor"><path d="M14.5 3v10.15a3.65 3.65 0 1 1-3-3.59V6.5a6.65 6.65 0 1 0 6 6.61V8.9A7 7 0 0 0 21 10V7a4 4 0 0 1-4-4h-2.5Z"/></svg>;
    case 'facebook':
      return <svg {...iconProps} fill="currentColor"><path d="M14.2 21v-8h2.95l.45-3.5H14.2V7.27c0-1.01.28-1.7 1.73-1.7h1.84V2.44A24.3 24.3 0 0 0 15.1 2c-2.65 0-4.47 1.62-4.47 4.6v2.9H7.6V13h3.03v8h3.57Z"/></svg>;
    case 'whatsapp':
      return <svg {...iconProps} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20.5 3.5A11 11 0 0 0 3.1 16.8L2 22l5.35-1.65A11 11 0 1 0 20.5 3.5Z"/><path d="M8.5 7.8c.22-.5.45-.52.76-.52h.4c.13 0 .32.05.42.3l.74 1.8c.1.25.06.42-.05.6l-.36.5c-.12.14-.24.3-.1.55.15.25.66 1.08 1.42 1.75.98.88 1.8 1.15 2.06 1.28.25.13.4.1.55-.06l.7-.82c.18-.2.36-.17.6-.08l1.73.82c.27.13.45.2.51.31.06.12.06.68-.16 1.33-.22.65-1.25 1.23-1.73 1.3-.44.07-1 .1-1.62-.1-.38-.12-.88-.29-1.5-.57-2.64-1.2-4.36-4-4.5-4.18-.13-.18-1.08-1.43-1.08-2.72 0-1.29.67-1.92.91-2.18Z"/></svg>;
    default:
      return null;
  }
}

function CGSocialLinks({ className = '', size = 16 }) {
  const whatsappNumber = String(cgC('contact.whatsapp', '2347085701097')).replace(/\D/g, '');
  const links = [
    { id: 'instagram', label: 'Instagram', href: 'https://instagram.com/calgrillz' },
    { id: 'tiktok', label: 'TikTok', href: 'https://tiktok.com/@calgrillz' },
    { id: 'facebook', label: 'Facebook', href: 'https://facebook.com/calgrillz' },
    ...(whatsappNumber ? [{ id: 'whatsapp', label: 'WhatsApp', href: 'https://wa.me/' + whatsappNumber }] : []),
  ];

  return (
    <div className={className} aria-label="Follow CalGrillz">
      {links.map((link) => (
        <a key={link.id} href={link.href} target="_blank" rel="noopener noreferrer"
           aria-label={`Follow CalGrillz on ${link.label}`} title={link.label}>
          <CGSocialIcon name={link.id} size={size}/>
        </a>
      ))}
    </div>
  );
}

// Live cart count for the header badge — re-renders whenever the CGCart
// store changes (add/remove/qty change, including from other tabs).
function CGHeaderCartCount() {
  const cart = useCart();
  return <>{cart.count}</>;
}

// ─── Shared sign-out helper ──────────────────────────────────────────────
// One implementation used by every Sign-out button (header user-menu,
// mobile drawer, the profile-tab fallback). POSTs to /logout, clears the
// local cart + voucher stores, then redirects home. Designed to never
// throw — even if the network drops, the user still gets logged out
// client-side and bounced to a safe page.
async function cgSignOut(opts) {
  const o = opts || {};
  if (o.confirm !== false) {
    if (typeof window !== 'undefined' && !window.confirm('Sign out of your CalGrillz account?')) return false;
  }
  const url  = (window.CG_URLS && window.CG_URLS.logout) || '/logout';
  const csrf = (window.CG_URLS && window.CG_URLS.csrf)   || '';
  try {
    await fetch(url, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'X-CSRF-TOKEN': csrf,
        'X-Requested-With': 'XMLHttpRequest',
      },
      credentials: 'same-origin',
    });
  } catch (e) { /* offline / blocked — fall through, navigate anyway */ }
  try { if (window.CGCart)     window.CGCart.clear(); }     catch (e) {}
  try { if (window.CGVouchers) window.CGVouchers.clear(); } catch (e) {}
  window.location.href = (window.CG_URLS && window.CG_URLS.home) || '/';
  return true;
}

// ─── Header user-menu dropdown ───────────────────────────────────────────
// Replaces the bare user-icon link with a click-to-open menu so Sign out
// lives in the obvious place every web user reaches for. Anonymous users
// still see the icon as a Sign-in shortcut (no dropdown).
function CGHeaderUserMenu() {
  const isLoggedIn = !!(window.CG_DATA && window.CG_DATA.isLoggedIn);
  const customerName = (window.CG_DATA && window.CG_DATA.customer && window.CG_DATA.customer.name) || null;
  const [open, setOpen] = useState(false);
  const wrapRef = useRef(null);

  // Close on outside click / Escape key — standard popover hygiene.
  useEffect(() => {
    if (!open) return;
    function onDocClick(e) { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }
    function onKey(e) { if (e.key === 'Escape') setOpen(false); }
    document.addEventListener('mousedown', onDocClick);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDocClick);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  // Anonymous: render the plain Sign-in link, no dropdown.
  if (!isLoggedIn) {
    return (
      <a href={CG_URL('login', 'login.html')} className="cgw-icon-btn"
         aria-label="Sign in" title="Sign in"
         style={{ textDecoration: 'none' }}>
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>
      </a>
    );
  }

  // Logged in: icon button toggles a dropdown.
  return (
    <div ref={wrapRef} style={{ position: 'relative' }}>
      <button
        type="button"
        onClick={() => setOpen(v => !v)}
        className="cgw-icon-btn"
        aria-label="Account menu"
        aria-expanded={open}
        aria-haspopup="menu"
        title="Account"
        style={{ background: 'transparent', border: 'none', cursor: 'pointer' }}
      >
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>
      </button>
      {open && (
        <div role="menu" className="cgw-user-menu" style={{
          position: 'absolute', top: 'calc(100% + 8px)', right: 0,
          minWidth: 220,
          background: 'var(--charcoal-800)',
          border: '1px solid var(--charcoal-700)',
          borderRadius: 12,
          boxShadow: '0 24px 48px -12px rgba(0,0,0,0.65)',
          padding: 6, zIndex: 100,
        }}>
          {customerName && (
            <div style={{
              padding: '10px 12px 8px',
              borderBottom: '1px solid var(--charcoal-700)',
              marginBottom: 6,
            }}>
              <div style={{ fontSize: 11, color: 'var(--muted)', letterSpacing: '0.1em', textTransform: 'uppercase', fontWeight: 700 }}>Signed in as</div>
              <div style={{ fontSize: 14, color: 'var(--cream)', fontWeight: 600, marginTop: 2 }}>{customerName}</div>
            </div>
          )}
          <a href={CG_URL('rewards', 'me.html')} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>
            My account
          </a>
          <a href="/checkin" role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="m9 16 2 2 4-4"/></svg>
            Daily check-in
          </a>
          <a href={CG_URL('spin', 'spin.html')} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <span style={{ width: 14, textAlign: 'center' }}>🎰</span>
            Spin the wheel
          </a>
          <a href="/predict" role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <span style={{ width: 14, textAlign: 'center' }}>⚽</span>
            Predict &amp; Chop
          </a>
          <a href="/mystery-box" role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <span style={{ width: 14, textAlign: 'center' }}>🎁</span>
            Mystery Box
          </a>
          <a href="/raffle" role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <span style={{ width: 14, textAlign: 'center' }}>🎟</span>
            Weekly Raffle
          </a>
          <div style={{ height: 1, background: 'var(--charcoal-700)', margin: '6px 0' }}/>
          {/* Services — book the venue for an event, or a Cocokalina room. */}
          <a href={CG_URL('events', 'events.html')} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <span style={{ width: 14, textAlign: 'center' }}>🎉</span>
            Book an event
          </a>
          {!(window.CG_DATA && window.CG_DATA.cocokalinaOn === false) && (
            <a href={CG_URL('bookRoom', '/book-room')} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
              <span style={{ width: 14, textAlign: 'center' }}>🛏️</span>
              Book a room
            </a>
          )}
          <div style={{ height: 1, background: 'var(--charcoal-700)', margin: '6px 0' }}/>
          <a href={CG_URL('rewards', 'me.html') + '#orders'} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
            My orders
          </a>
          <a href={CG_URL('rewards', 'me.html') + '#vouchers'} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 1 1 0-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 1 0 0-5C13 2 12 7 12 7z"/></svg>
            My vouchers
          </a>
          <a href={CG_URL('voucher', 'voucher.html')} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="2" x2="12" y2="22"/><polyline points="5 9 12 2 19 9"/></svg>
            Buy a gift voucher
          </a>
          <a href={CG_URL('refer', 'refer.html')} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
            Refer a friend
          </a>
          <div style={{ height: 1, background: 'var(--charcoal-700)', margin: '6px 0' }}/>
          <button
            type="button"
            role="menuitem"
            onClick={() => { setOpen(false); cgSignOut(); }}
            className="cgw-user-menu__item cgw-user-menu__item--danger"
          >
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
            Sign out
          </button>
        </div>
      )}
    </div>
  );
}

// ─── Web Push subscribe button (top of the bell drawer) ────────────────
// Only renders if:
//   1. VAPID key is configured server-side (window.CG_URLS.vapidPublicKey)
//   2. Browser supports Notification + PushManager + service worker
//   3. Customer hasn't already subscribed on this device (Notification.permission)
// One-tap "Enable notifications" → asks OS for permission → subscribes to
// PushManager → POSTs the subscription to the server. Errors are silent —
// the in-app bell still works even if push is refused.
function CGPushEnable() {
  const CG = window.CG_URLS || {};
  const vapid = CG.vapidPublicKey || '';
  const subscribeUrl = CG.pushSubscribe || null;
  const csrf = CG.csrf || '';

  const supported = typeof window !== 'undefined'
    && 'Notification' in window
    && 'serviceWorker' in navigator
    && 'PushManager' in window
    && vapid !== ''
    && subscribeUrl;

  const [perm, setPerm] = React.useState(() => (supported ? Notification.permission : 'denied'));
  const [busy, setBusy] = React.useState(false);

  React.useEffect(() => {
    if (!supported) return;
    // If already subscribed on this device, don't nag.
    navigator.serviceWorker.ready.then(reg =>
      reg.pushManager.getSubscription().then(sub => {
        if (sub) setPerm('subscribed');
      })
    ).catch(() => {});
  }, [supported]);

  if (!supported) return null;
  if (perm === 'granted' || perm === 'subscribed') return null;
  if (perm === 'denied') return (
    <div style={{ padding: '10px 14px', fontSize: 12, color: 'var(--muted)', borderBottom: '1px solid var(--charcoal-700)', background: 'rgba(216,60,44,.05)' }}>
      Push notifications are blocked in your browser settings. Turn them on for calgrillz.com to get updates when the app is closed.
    </div>
  );

  const enable = async () => {
    if (busy) return;
    setBusy(true);
    try {
      const p = await Notification.requestPermission();
      if (p !== 'granted') { setPerm(p); return; }
      const reg = await navigator.serviceWorker.ready;
      const sub = await reg.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(vapid),
      });
      const raw = sub.toJSON();
      const res = await fetch(subscribeUrl, {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf, Accept: 'application/json' },
        body: JSON.stringify(raw),
      });
      if (res.ok) setPerm('subscribed');
    } catch (e) { /* silent — bell still works */ }
    finally { setBusy(false); }
  };

  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10,
      padding: '12px 14px',
      background: 'linear-gradient(135deg, rgba(251,191,36,.10), rgba(216,60,44,.10))',
      borderBottom: '1px solid var(--charcoal-700)',
    }}>
      <span style={{ fontSize: 18 }}>🔔</span>
      <div style={{ flex: 1, minWidth: 0, fontSize: 12.5, color: 'var(--cream-dim)', lineHeight: 1.4 }}>
        Get notifications on your phone the moment there's an update.
      </div>
      <button type="button" onClick={enable} disabled={busy} style={{
        padding: '7px 12px', borderRadius: 999,
        background: 'linear-gradient(135deg, #c8102e, #f97316)',
        color: '#fff', border: 'none', cursor: busy ? 'wait' : 'pointer',
        fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
      }}>{busy ? 'Enabling…' : 'Enable'}</button>
    </div>
  );
}

// VAPID public key comes back as URL-safe base64; PushManager wants a Uint8Array.
function urlBase64ToUint8Array(base64) {
  const padding = '='.repeat((4 - base64.length % 4) % 4);
  const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
  const raw = atob(b64);
  const arr = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; ++i) arr[i] = raw.charCodeAt(i);
  return arr;
}

// ─── Notification bell ───────────────────────────────────────────────────
// Header icon that shows an unread badge + opens a right-side drawer with
// the latest 20 notifications. Fetches from GET /notifications/inbox,
// marks-read on tap (posts to /notifications/{id}/read), and offers a
// "Mark all read" button. Silently self-hides for anonymous visitors and
// on backends that lie about the inbox route (defensive).
function CGHeaderBell() {
  const [open, setOpen] = React.useState(false);
  const [items, setItems] = React.useState([]);
  const [unread, setUnread] = React.useState(0);
  const [loading, setLoading] = React.useState(false);
  const wrapRef = React.useRef(null);

  const isLoggedIn = !!(window.CG_DATA && window.CG_DATA.isLoggedIn);
  const inboxUrl = (window.CG_URLS && window.CG_URLS.notifInbox) || null;
  const readAllUrl = (window.CG_URLS && window.CG_URLS.notifReadAll) || null;
  const readBase = (window.CG_URLS && window.CG_URLS.notifReadBase) || '/notifications';
  const csrf = (window.CG_URLS && window.CG_URLS.csrf) || '';

  const fetchInbox = React.useCallback(() => {
    if (!isLoggedIn || !inboxUrl) return;
    setLoading(true);
    fetch(inboxUrl, { credentials: 'same-origin', headers: { Accept: 'application/json' } })
      .then(r => r.ok ? r.json() : { ok: false })
      .then(d => {
        if (d && d.ok) { setItems(d.items || []); setUnread(d.unread || 0); }
      })
      .catch(() => {})
      .finally(() => setLoading(false));
  }, [isLoggedIn, inboxUrl]);

  // Prime the unread badge on first render + refresh every 60s when the
  // drawer is closed. Chatty enough to feel live, cheap enough not to
  // matter (single tiny JSON payload).
  React.useEffect(() => {
    if (!isLoggedIn) return;
    fetchInbox();
    const t = setInterval(() => { if (! open) fetchInbox(); }, 60_000);
    return () => clearInterval(t);
  }, [fetchInbox, isLoggedIn, open]);

  // Refresh whenever the drawer opens.
  React.useEffect(() => { if (open) fetchInbox(); }, [open, fetchInbox]);

  // Dismiss the drawer on outside-click / Escape.
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    const onEsc = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onEsc);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onEsc); };
  }, [open]);

  if (!isLoggedIn) return null;

  const markOne = (n) => {
    if (n.read) return;
    fetch(readBase + '/' + n.id + '/read', {
      method: 'POST',
      credentials: 'same-origin',
      headers: { 'X-CSRF-TOKEN': csrf, Accept: 'application/json' },
    }).then(r => r.ok ? r.json() : null).then(d => {
      if (d && d.ok) {
        setUnread(d.unread || 0);
        setItems(items.map(x => x.id === n.id ? { ...x, read: true } : x));
      }
    }).catch(() => {});
  };

  const markAll = () => {
    if (!readAllUrl) return;
    fetch(readAllUrl, {
      method: 'POST',
      credentials: 'same-origin',
      headers: { 'X-CSRF-TOKEN': csrf, Accept: 'application/json' },
    }).then(r => r.ok ? r.json() : null).then(d => {
      if (d && d.ok) { setUnread(0); setItems(items.map(x => ({ ...x, read: true }))); }
    }).catch(() => {});
  };

  return (
    <div ref={wrapRef} style={{ position: 'relative' }}>
      <button
        type="button"
        onClick={() => setOpen(v => !v)}
        className="cgw-icon-btn"
        aria-label={unread > 0 ? unread + ' unread notifications' : 'Notifications'}
        aria-expanded={open}
        title="Notifications"
        style={{ background: 'transparent', border: 'none', cursor: 'pointer', position: 'relative' }}
      >
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
        {unread > 0 && (
          <span aria-hidden="true" style={{
            position: 'absolute',
            top: -4, right: -4,
            background: 'var(--ember-500)',
            color: '#fff',
            fontSize: 10,
            fontWeight: 700,
            minWidth: 18, height: 18,
            borderRadius: 9,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            padding: '0 4px',
            border: '2px solid #0a0a0a',
          }}>{unread > 9 ? '9+' : unread}</span>
        )}
      </button>

      {open && (
        // Position: fixed to the viewport top-right corner so a bell button
        // that sits mid-header on mobile doesn't push the drawer off-screen.
        // Width caps at 360px, and the right/top offsets shrink on narrow
        // viewports for a full-width app-drawer feel.
        <div role="menu" style={{
          position: 'fixed',
          top: 68, right: 12,
          width: 'min(360px, calc(100vw - 24px))',
          maxHeight: 'calc(100vh - 88px)',
          background: 'var(--charcoal-900)',
          border: '1px solid var(--charcoal-700)',
          borderRadius: 14,
          boxShadow: '0 24px 70px rgba(0,0,0,.65)',
          overflow: 'hidden',
          zIndex: 1000,
          display: 'flex', flexDirection: 'column',
        }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 14px', borderBottom: '1px solid var(--charcoal-700)' }}>
            <strong style={{ fontSize: 14, color: 'var(--cream)' }}>Notifications</strong>
            {unread > 0 && (
              <button type="button" onClick={markAll} style={{ background: 'transparent', border: 'none', color: '#fbbf24', cursor: 'pointer', fontSize: 12, fontWeight: 700 }}>
                Mark all read
              </button>
            )}
          </div>
          <CGPushEnable/>
          <div style={{ flex: 1, overflowY: 'auto' }}>
            {loading && items.length === 0 && (
              <div style={{ padding: '20px 14px', fontSize: 13, color: 'var(--muted)', textAlign: 'center' }}>Loading…</div>
            )}
            {!loading && items.length === 0 && (
              <div style={{ padding: '30px 14px', fontSize: 13, color: 'var(--muted)', textAlign: 'center' }}>
                No notifications yet. Order something, pay for a tournament, check in for points — updates land here.
              </div>
            )}
            {items.map(n => (
              <a key={n.id}
                 href={n.url || '#'}
                 onClick={() => markOne(n)}
                 style={{
                   display: 'block', padding: '12px 14px',
                   textDecoration: 'none', color: 'inherit',
                   borderBottom: '1px solid rgba(58,51,42,.5)',
                   background: n.read ? 'transparent' : 'rgba(249,115,22,.06)',
                   position: 'relative',
                 }}>
                <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
                  <span style={{ fontSize: 18, lineHeight: 1.2 }}>{n.icon}</span>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 13.5, fontWeight: n.read ? 500 : 700, color: 'var(--cream)', lineHeight: 1.35 }}>{n.title}</div>
                    {n.body && <div style={{ fontSize: 12.5, color: 'var(--cream-dim)', marginTop: 2, lineHeight: 1.4 }}>{n.body}</div>}
                    <div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 4 }}>{n.when}</div>
                  </div>
                  {!n.read && (
                    <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--ember-500)', marginTop: 6, flexShrink: 0 }}/>
                  )}
                </div>
              </a>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// ─── Desktop nav dropdown group (Games / Services) ───────────────────────
// Renders a nav label with a caret that opens a small popover of sub-links.
// Opens on hover (desktop) and on click/tap; closes on outside-click, Escape,
// or picking an item. Modeled on CGHeaderUserMenu. The parent shows as active
// when the current page id is one of the group's `match` ids.
function CGWNavGroup({ group, activeId }) {
  const [open, setOpen] = useState(false);
  const wrapRef = useRef(null);
  const isActive = (group.match || []).includes(activeId);
  // Click-to-toggle on every device — the menu stays collapsed until the
  // trigger is clicked (no hover-open, which pops menus as the pointer drifts).

  useEffect(() => {
    if (!open) return;
    function onDocClick(e) { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }
    function onKey(e) { if (e.key === 'Escape') setOpen(false); }
    document.addEventListener('mousedown', onDocClick);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDocClick);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  return (
    <div ref={wrapRef}
      style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>
      <a href="#" role="button" aria-haspopup="menu" aria-expanded={open}
        className={isActive ? 'active' : ''}
        onClick={(e) => { e.preventDefault(); setOpen(v => !v); }}
        style={{ display: 'inline-flex', alignItems: 'center', gap: 5, cursor: 'pointer' }}>
        {group.label}
        <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"
          style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }}><path d="M6 9l6 6 6-6"/></svg>
      </a>
      {open && (
        // Outer wrapper carries the visual gap as padding so there's no dead
        // zone between the trigger and the panel (hover stays alive across it).
        <div style={{ position: 'absolute', top: '100%', left: 0, paddingTop: 10, zIndex: 100 }}>
          <div role="menu" className="cgw-user-menu" style={{
            minWidth: 210,
            background: 'var(--charcoal-800)',
            border: '1px solid var(--charcoal-700)',
            borderRadius: 12,
            boxShadow: '0 24px 48px -12px rgba(0,0,0,0.65)',
            padding: 6,
          }}>
            {group.children.map(c => (
              <a key={c.id} href={c.href} role="menuitem" className="cgw-user-menu__item" onClick={() => setOpen(false)}>
                {c.icon && <span style={{ width: 16, textAlign: 'center' }}>{c.icon}</span>}
                {c.label}
              </a>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// Mobile nav group (Games / Services): collapsed by default; tap the label to
// expand it (accordion), so the drawer isn't a long wall of every sub-link.
function CGWMobileNavGroup({ group, activeId, onNavigate }) {
  const [open, setOpen] = useState(false);
  const hasActive = (group.match || []).includes(activeId);
  return (
    <div style={{ borderTop: '1px solid var(--charcoal-800)', marginTop: 4 }}>
      <button type="button" onClick={() => setOpen(v => !v)} aria-expanded={open}
        style={{
          background: 'transparent', border: 'none', cursor: 'pointer', width: '100%', textAlign: 'left',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 0',
          color: hasActive ? 'var(--cream)' : 'var(--muted)', fontSize: 11, letterSpacing: '0.14em',
          textTransform: 'uppercase', fontWeight: 700,
        }}>
        {group.label}
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"
          style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }}><path d="M6 9l6 6 6-6"/></svg>
      </button>
      {open && group.children.map(c => (
        <a key={c.id} href={c.href} className={activeId === c.id ? 'active' : ''} onClick={onNavigate}>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
            {c.icon && <span style={{ width: 18, textAlign: 'center' }}>{c.icon}</span>}
            {c.label}
          </span>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M9 6l6 6-6 6"/></svg>
        </a>
      ))}
    </div>
  );
}

// ─── Header ──────────────────────────────────────────────────────────────
function CGWHeader({ active, minimal = false }) {
  const [menuOpen, setMenuOpen] = useState(false);
  const isLoggedIn = !!(window.CG_DATA && window.CG_DATA.isLoggedIn);
  const contactAddress = cgC('home.find.address', 'No 1 Cocokalina Close, Agbor 330221, Delta State');
  const contactPhone = cgC('home.find.phone', '0708 570 1097');
  const contactPhoneHref = String(contactPhone).replace(/[^\d+]/g, '');
  // Pages like /me, /spin, /refer all pass active="rewards" but the nav
  // shows "My Account" instead of "Rewards" when signed-in. Map the legacy
  // id so the highlight still lands on the right tab.
  const activeId = (isLoggedIn && active === 'rewards') ? 'account' : active;
  // Rewards / My Account share the same URL (/me/{token}), so we show only
  // one entry: "Rewards" when logged out (acts as login gateway with a
  // peek at what's behind the door) and "My Account" when logged in (the
  // dashboard already serves as the rewards hub).
  // When signed in, the menu switches to the links a customer actually needs —
  // their account, orders, vouchers — instead of the marketing pages. (The
  // #orders / #vouchers deep-links open the matching tab on the dashboard.)
  const meUrl = CG_URL('rewards', 'me.html');
  // Games + Services are grouped into dropdowns (desktop) / labelled sections
  // (mobile) instead of a long flat row. Shared by both nav variants.
  const gamesGroup = {
    id: 'games', label: 'Games', group: true,
    match: ['spin', 'predict', 'raffle', 'mystery'],
    children: [
      { id: 'spin',    href: CG_URL('spin', 'spin.html'), label: 'Spin the wheel', icon: '🎰' },
      { id: 'predict', href: '/predict',                  label: 'Predict & Chop', icon: '⚽' },
      { id: 'raffle',  href: '/raffle',                   label: 'Weekly Raffle',  icon: '🎟️' },
      { id: 'mystery', href: '/mystery-box',              label: 'Mystery Box',    icon: '🎁' },
    ],
  };
  // "Book a Room" (Cocokalina) hides everywhere when the owner disables the
  // module (window.CG_DATA.cocokalinaOn === false). Undefined → shown.
  const cocokalinaOn = !(window.CG_DATA && window.CG_DATA.cocokalinaOn === false);
  const servicesGroup = {
    id: 'services', label: 'Services', group: true,
    match: ['services', 'events'],
    children: [
      { id: 'events',   href: CG_URL('events', 'events.html'),   label: 'Book an Event', icon: '🎉' },
      ...(cocokalinaOn ? [{ id: 'services', href: CG_URL('bookRoom', '/book-room'), label: 'Book a Room', icon: '🛏️' }] : []),
    ],
  };
  const nav = isLoggedIn
    ? [
        { id: 'menu',     href: CG_URL('menu', 'menu.html'),     label: 'Menu' },
        { id: 'account',  href: meUrl,                           label: 'My Account', accent: true },
        { id: 'checkin',  href: '/checkin',                      label: 'Check in' },
        gamesGroup,
        servicesGroup,
        { id: 'orders',   href: meUrl + '#orders',               label: 'My Orders' },
        { id: 'refer',    href: CG_URL('refer', 'refer.html'),   label: 'Refer' },
      ]
    : [
        { id: 'home',     href: CG_URL('home',    'index.html'),   label: 'Home' },
        { id: 'menu',     href: CG_URL('menu',    'menu.html'),    label: 'Menu' },
        gamesGroup,
        servicesGroup,
        { id: 'rewards',  href: meUrl,                             label: 'Rewards' },
        { id: 'voucher',  href: CG_URL('voucher', 'voucher.html'), label: 'Gift voucher' },
        { id: 'about',    href: CG_URL('about',   'about.html'),   label: 'About' },
        { id: 'contact',  href: CG_URL('contact', 'contact.html'), label: 'Contact' },
      ];
  // Close the mobile drawer if the viewport gets stretched to desktop.
  useEffect(() => {
    function onResize() { if (window.innerWidth > 980) setMenuOpen(false); }
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return (
    <header className="cgw-header">
      {!minimal && (
        <div className="cgw-header__topbar">
          <div className="cgw__wrap">
            <div className="cgw-header__topbar-inner">
              <div className="cgw-header__contact">
                <a href={CG_URL('contact', 'contact.html')} className="cgw-header__address" aria-label={`Visit CalGrillz at ${contactAddress}`}>
                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M20 10c0 5-8 12-8 12S4 15 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/></svg>
                  <span>{contactAddress}</span>
                </a>
                <a href={'tel:' + contactPhoneHref} className="cgw-header__phone" aria-label={`Call CalGrillz on ${contactPhone}`}>
                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M22 16.9v3a2 2 0 0 1-2.18 2 19.8 19.8 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.12 4.2 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.12.9.33 1.77.62 2.6a2 2 0 0 1-.45 2.11L8.02 9.7a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.83.29 1.7.5 2.6.62A2 2 0 0 1 22 16.9Z"/></svg>
                  <span>{contactPhone}</span>
                </a>
              </div>
              <CGSocialLinks className="cgw-header__socials" size={14}/>
            </div>
          </div>
        </div>
      )}
      <div className="cgw__wrap">
        <div className="cgw-header__row">
          <a href={CG_URL('home', 'index.html')} style={{ display: 'flex', alignItems: 'center', textDecoration: 'none' }}>
            <img src={cgImg(cgC('brand.logo', CG_ASSET('calgrillz-logo.png')))} alt="CalGrillz" width="65" height="48" fetchpriority="high" decoding="async"
                 style={{ height: 48, width: 'auto', filter: 'drop-shadow(0 2px 8px rgba(0,0,0,0.5))' }}/>
          </a>
          {!minimal && (
            <nav className="cgw-nav">
              {nav.map(n => n.group ? (
                <CGWNavGroup key={n.id} group={n} activeId={activeId}/>
              ) : (
                <a key={n.id} href={n.href}
                  className={(activeId === n.id ? 'active' : '') + (n.accent ? ' cgw-nav__account' : '')}>
                  {n.accent && (
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" style={{ marginRight: 6, verticalAlign: -2 }}>
                      <circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>
                    </svg>
                  )}
                  {n.label}
                </a>
              ))}
            </nav>
          )}
          {minimal ? (
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--muted)', fontSize: 13 }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
              Secure checkout
            </div>
          ) : (
            <div className="cgw-header__right">
              {/* Notification bell — visible to logged-in customers only.
                  Click opens a drawer with the latest inbox items; the red
                  dot shows unread count. Backed by CustomerNotificationService
                  on the PHP side. See CGHeaderBell in cgw-shell.jsx. */}
              <CGHeaderBell/>
              {/* User icon — anonymous customers see a Sign-in shortcut;
                  signed-in customers get a click-to-open dropdown with
                  My Account / Gift vouchers / Refer / Sign out.
                  The Sign out is right where users expect it. */}
              <CGHeaderUserMenu/>
              <a href={CG_URL('cart', 'cart.html')} className="cgw-icon-btn" aria-label="Cart" style={{ position: 'relative', textDecoration: 'none' }}>
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/></svg>
                <span style={{
                  position: 'absolute',
                  top: -4, right: -4,
                  background: 'var(--ember-500)',
                  color: '#fff',
                  fontSize: 10,
                  fontWeight: 700,
                  minWidth: 18,
                  height: 18,
                  borderRadius: 9,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  padding: '0 4px',
                  border: '2px solid #0a0a0a',
                }}><CGHeaderCartCount/></span>
              </a>
              <a href={CG_URL('menu', 'menu.html')} className="cgw-btn cgw-btn--primary cgw-header__order" style={{ padding: '14px 22px', fontSize: 13, textDecoration: 'none' }}>
                Order now
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
              </a>
              {/* Mobile-only Order CTA — sits between the cart icon and the
                  hamburger on phones (the desktop button above is hidden by
                  CSS at ≤980px). Compact pill that still reads as primary. */}
              <a href={CG_URL('menu', 'menu.html')} className="cgw-header__order-mobile" aria-label="Order now" style={{ textDecoration: 'none' }}>
                Order now
              </a>
              {/* Hamburger — visible only ≤980px via CSS */}
              <button
                type="button"
                className="cgw-hamburger"
                aria-label={menuOpen ? 'Close menu' : 'Open menu'}
                aria-expanded={menuOpen}
                onClick={() => setMenuOpen(v => !v)}
              >
                {menuOpen ? (
                  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
                ) : (
                  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
                )}
              </button>
            </div>
          )}
        </div>

        {/* Mobile slide-down drawer — kept inside the header so it stacks naturally */}
        {!minimal && (
          <div className={'cgw-mobile-menu' + (menuOpen ? ' open' : '')} role="dialog" aria-modal="true" aria-hidden={!menuOpen}>
            <nav>
              {nav.map(n => n.group ? (
                <CGWMobileNavGroup key={n.id} group={n} activeId={activeId} onNavigate={() => setMenuOpen(false)}/>
              ) : (
                <a key={n.id} href={n.href}
                  className={(activeId === n.id ? 'active' : '') + (n.accent ? ' cgw-mobile-menu__account' : '')}
                  onClick={() => setMenuOpen(false)}>
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
                    {n.accent && (
                      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
                        <circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>
                      </svg>
                    )}
                    {n.label}
                  </span>
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M9 6l6 6-6 6"/></svg>
                </a>
              ))}
              {/* Sign-in shortcut for logged-OUT users only. Logged-in users
                  already have "My Account" in the nav list above, so we don't
                  duplicate it here as "My dashboard". */}
              {!isLoggedIn && (
                <a
                  href={CG_URL('login', 'login.html')}
                  onClick={() => setMenuOpen(false)}
                  style={{ borderTop: '1px solid var(--charcoal-700)', marginTop: 6, paddingTop: 14, color: 'var(--ember-200)' }}
                >
                  <span>Sign in</span>
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M9 6l6 6-6 6"/></svg>
                </a>
              )}
              {/* Sign out — only when signed in. Lives at the bottom of the
                  drawer so it's discoverable without being a footgun. */}
              {isLoggedIn && (
                <button
                  type="button"
                  onClick={() => { setMenuOpen(false); cgSignOut(); }}
                  style={{
                    background: 'transparent', border: 'none', cursor: 'pointer',
                    width: '100%', textAlign: 'left',
                    display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    padding: '14px 0', color: 'var(--danger)',
                    fontSize: 14, fontWeight: 600,
                  }}
                >
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
                    Sign out
                  </span>
                </button>
              )}
            </nav>
          </div>
        )}
      </div>
    </header>
  );
}

// ─── Footer ──────────────────────────────────────────────────────────────
function CGWFooter() {
  return (
    <footer className="cgw-footer">
      <div style={{
        position: 'absolute',
        left: '50%',
        bottom: -180,
        transform: 'translateX(-50%)',
        width: 800, height: 360,
        borderRadius: '50%',
        background: 'radial-gradient(circle, rgba(249,115,22,0.18) 0%, rgba(249,115,22,0) 60%)',
        pointerEvents: 'none',
      }}/>
      <div className="cgw__wrap" style={{ position: 'relative' }}>
        <div className="cgw-footer__inner">
          <div className="cgw-footer__brand">
            <img src={cgImg(cgC('brand.logo', CG_ASSET('calgrillz-logo.png')))} alt="CalGrillz" width="87" height="64" loading="lazy" decoding="async" style={{ height: 64, width: 'auto' }}/>
            <p>Nigerian charcoal grill in Agbor, Delta. Family-style food. Smoky, bold, premium-street. No shortcuts.</p>
            <CGSocialLinks className="cgw-footer__socials" size={16}/>
          </div>
          <div>
            <h4>Order</h4>
            <ul>
              <li><a href={CG_URL('menu', 'menu.html')}>Menu</a></li>
              <li>Pickup</li>
              <li>Delivery</li>
              <li>Group orders</li>
              <li><a href={CG_URL('voucher', 'voucher.html')}>Gift vouchers</a></li>
            </ul>
          </div>
          <div>
            <h4>Earn</h4>
            <ul>
              <li><a href={CG_URL('rewards', 'me.html')}>Rewards</a></li>
              <li>Tiers</li>
              <li>Streaks</li>
              <li><a href={CG_URL('spin', 'spin.html')}>Spin the wheel</a></li>
              <li><a href={CG_URL('refer', '/refer')}>Refer a friend</a></li>
            </ul>
          </div>
          <div>
            <h4>About</h4>
            <ul>
              <li><a href={CG_URL('about', 'about.html')}>Our story</a></li>
              <li><a href={CG_URL('about', 'about.html') + '#pit'}>The pit</a></li>
              <li><a href={CG_URL('events', 'events.html')}>Events</a></li>
              <li>Press</li>
              <li><a href={CG_URL('contact', 'contact.html')}>Contact</a></li>
            </ul>
          </div>
          <div>
            <h4>Visit</h4>
            <ul>
              <li>{cgC('home.find.address', 'No 1 Cocokalina Close, Agbor 330221, Delta State')}</li>
              <li>{cgC('home.find.phone', '0708 570 1097')}</li>
              <li>{cgC('home.find.hours_short', 'Every day · 10am – 9pm')}</li>
              <li><a href={CG_URL('contact', 'contact.html') + '#book'}>Book us for events</a></li>
            </ul>
          </div>
        </div>
        <div className="cgw-footer__bottom">
          <span>© CalGrillz 2026 · Made By <a href="https://dev.unearth.ng" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit', textDecoration: 'underline' }}>Unearth Technology</a></span>
          <span style={{ display: 'flex', gap: 22 }}>
            <a href="/privacy" style={{ color: 'inherit', textDecoration: 'none' }}>Privacy</a>
            <span>Terms</span>
            <span>Allergens</span>
          </span>
        </div>
      </div>
    </footer>
  );
}

// ─── Ticker ─────────────────────────────────────────────────────────
function CGWTicker() {
  const items = [
    'Slow-grilled over real charcoal',
    'Point-and-kill catfish daily',
    'Earn points on every order',
    'Free delivery over ₦15,000',
    'Open every day · 10am – 9pm',
  ];
  const Flame = () => (
    <svg width="20" height="20" viewBox="0 0 24 24" fill="#fff">
      <path d="M12 2c1 3 3 4 3 7a3 3 0 0 1-3 3 3 3 0 0 1-3-3c0-1 1-2 1-3-2 1-4 4-4 7a6 6 0 0 0 12 0c0-5-4-8-6-11z"/>
    </svg>
  );
  return (
    <div className="cgw-ticker">
      <div className="cgw-ticker__row">
        {[...items, ...items].map((it, i) => <span key={i}><Flame/>{it}</span>)}
      </div>
    </div>
  );
}

// ─── Page hero (small banner under header) ──────────────────────────
function CGWPageHero({ eyebrow, title, sub, breadcrumbs, image }) {
  return (
    <section style={{
      position: 'relative',
      padding: '70px 0 50px',
      overflow: 'hidden',
      background: image ? undefined : 'linear-gradient(180deg, var(--charcoal-850), var(--charcoal-900))',
      borderBottom: '1px solid var(--charcoal-700)',
    }}>
      {image && (
        <>
          <div style={{
            position: 'absolute', inset: 0,
            backgroundImage: `url(${cgImg(image)})`,
            backgroundSize: 'cover',
            backgroundPosition: 'center',
            filter: 'brightness(0.35) saturate(1.1)',
          }}/>
          <div style={{
            position: 'absolute', inset: 0,
            background: 'linear-gradient(180deg, rgba(10,10,10,0.4) 0%, rgba(10,10,10,0.85) 100%)',
          }}/>
        </>
      )}
      <div className="cgw__wrap" style={{ position: 'relative', zIndex: 2 }}>
        {breadcrumbs && (
          <div style={{ marginBottom: 18, fontSize: 12, color: 'var(--muted)', letterSpacing: '0.08em', textTransform: 'uppercase' }}>
            {breadcrumbs.map((b, i) => (
              <React.Fragment key={i}>
                {i > 0 && <span style={{ margin: '0 8px' }}>/</span>}
                {b.href ? <a href={b.href} style={{ color: 'var(--cream-dim)', textDecoration: 'none' }}>{b.label}</a> : <span style={{ color: 'var(--ember-200)' }}>{b.label}</span>}
              </React.Fragment>
            ))}
          </div>
        )}
        {eyebrow && <div className="cgw-eyebrow ember">{eyebrow}</div>}
        <h1 className="cgw-h2" style={{ marginTop: 12 }}>{title}</h1>
        {sub && <p style={{ fontSize: 17, color: 'var(--cream-dim)', maxWidth: 600, marginTop: 18, lineHeight: 1.45 }}>{sub}</p>}
      </div>
    </section>
  );
}

// ─── Page shell wrapper ─────────────────────────────────────────────
// ─── Mobile bottom tab bar ───────────────────────────────────────────
// The app-like footer menu: Menu · Cart · HOME (raised, centre) · Orders ·
// Settings. Phones only (hidden from 768px up), hidden on minimal pages
// (login/checkout) so it never covers a pay button. Orders and Settings
// deep-link into the account page's own tabs; logged-out taps land on login.
function CGWMobileTabBar({ active }) {
  const isLoggedIn = !!(window.CG_DATA && window.CG_DATA.isLoggedIn);
  const meUrl = CG_URL('rewards', 'me.html');
  const loginUrl = CG_URL('login', 'login.html');
  const { count } = (typeof useCart === 'function' ? useCart() : { count: 0 });

  // Highlight follows the page. The account page distinguishes its tabs by
  // hash, so listen for changes while the customer moves between them.
  const [hash, setHash] = React.useState((window.location.hash || '').replace('#', ''));
  React.useEffect(() => {
    const h = () => setHash((window.location.hash || '').replace('#', ''));
    window.addEventListener('hashchange', h);
    return () => window.removeEventListener('hashchange', h);
  }, []);

  const path = window.location.pathname;
  const onMe = /^\/me(\/|$)/.test(path);
  const current =
    active === 'menu' || /^\/(menu|item)(\/|$)/.test(path) ? 'menu'
    : /^\/cart(\/|$)/.test(path) ? 'cart'
    : /^\/order(\/|$)/.test(path) || (onMe && hash === 'orders') ? 'orders'
    : onMe ? 'settings'
    : (active === 'home' || path === '/') ? 'home'
    : '';

  const S = { icon: { display: 'block' } };
  const ic = (d, filled) => (
    <svg width="22" height="22" viewBox="0 0 24 24" fill={filled ? 'currentColor' : 'none'}
         stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={S.icon}>
      {d}
    </svg>
  );

  const item = (id, href, label, icon, badge) => (
    <a key={id} href={href} className={'cgw-tabbar__item' + (current === id ? ' is-active' : '')}>
      <span style={{ position: 'relative' }}>
        {icon}
        {badge > 0 && <span className="cgw-tabbar__badge">{badge > 9 ? '9+' : badge}</span>}
      </span>
      <span className="cgw-tabbar__label">{label}</span>
    </a>
  );

  return (
    <nav className="cgw-tabbar" aria-label="App navigation">
      <style>{`
        .cgw-tabbar { display: none; }
        @media (max-width: 767px) {
          .cgw { padding-bottom: calc(66px + env(safe-area-inset-bottom, 0px)); }
          .cgw-tabbar {
            position: fixed; bottom: 0; left: 0; right: 0; z-index: 80;
            display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
            align-items: end;
            background: rgba(15, 13, 10, 0.97);
            -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
            border-top: 1px solid var(--charcoal-700, #2a251d);
            padding: 6px 4px calc(8px + env(safe-area-inset-bottom, 0px));
          }
          .cgw-tabbar__item {
            display: flex; flex-direction: column; align-items: center; gap: 3px;
            padding: 4px 0 2px; text-decoration: none;
            color: var(--muted, #8d8574); -webkit-tap-highlight-color: transparent;
          }
          .cgw-tabbar__item.is-active { color: var(--ember-300, #fdba74); }
          .cgw-tabbar__label { font-size: 10px; font-weight: 700; letter-spacing: 0.04em; }
          .cgw-tabbar__badge {
            position: absolute; top: -6px; right: -10px; min-width: 16px; height: 16px;
            padding: 0 4px; border-radius: 999px; background: var(--flame, #f97316);
            color: #1a0f00; font-size: 10px; font-weight: 800;
            display: inline-flex; align-items: center; justify-content: center;
          }
          .cgw-tabbar__home {
            display: flex; flex-direction: column; align-items: center; gap: 3px;
            text-decoration: none; color: var(--muted, #8d8574);
            transform: translateY(-14px); -webkit-tap-highlight-color: transparent;
          }
          .cgw-tabbar__homebtn {
            width: 52px; height: 52px; border-radius: 50%;
            display: flex; align-items: center; justify-content: center;
            background: linear-gradient(135deg, #fb923c, #ea580c);
            color: #1a0f00; box-shadow: 0 6px 18px rgba(249, 115, 22, 0.45);
            border: 3px solid rgba(15, 13, 10, 0.97);
          }
          .cgw-tabbar__home.is-active .cgw-tabbar__homebtn { box-shadow: 0 6px 22px rgba(249, 115, 22, 0.7); }
          .cgw-tabbar__home .cgw-tabbar__label { margin-top: -1px; }
          .cgw-tabbar__home.is-active { color: var(--ember-300, #fdba74); }
        }
      `}</style>
      {item('menu', CG_URL('menu', 'menu.html'), 'Menu',
        ic(<><path d="M4 5h16M4 12h16M4 19h10"/><circle cx="19" cy="19" r="1" fill="currentColor" stroke="none"/></>))}
      {item('cart', CG_URL('cart', 'cart.html'), 'Cart',
        ic(<><circle cx="9" cy="20" r="1.4"/><circle cx="17" cy="20" r="1.4"/><path d="M3 4h2l2.2 11.2a1.6 1.6 0 0 0 1.6 1.3h7.6a1.6 1.6 0 0 0 1.6-1.3L20 8H6"/></>), count)}
      <a href={CG_URL('home', 'index.html')} className={'cgw-tabbar__home' + (current === 'home' ? ' is-active' : '')}>
        <span className="cgw-tabbar__homebtn">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
            <path d="M12 3.2 3.5 10.4a1 1 0 0 0-.35.76V20a1 1 0 0 0 1 1H9.5v-5.4a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V21h5.35a1 1 0 0 0 1-1v-8.84a1 1 0 0 0-.35-.76L12 3.2Z"/>
          </svg>
        </span>
        <span className="cgw-tabbar__label">Home</span>
      </a>
      {item('orders', isLoggedIn ? meUrl + '#orders' : loginUrl, 'Orders',
        ic(<><path d="M7 3h10a1 1 0 0 1 1 1v17l-2.5-1.6L13 21l-2.5-1.6L8 21l-2-1.3V4a1 1 0 0 1 1-1Z"/><path d="M9.5 8h5M9.5 12h5"/></>))}
      {item('settings', isLoggedIn ? meUrl + '#profile' : loginUrl, 'Settings',
        ic(<><circle cx="12" cy="12" r="3"/><path d="M19.4 13.5a7.6 7.6 0 0 0 0-3l1.8-1.4-2-3.4-2.1.9a7.6 7.6 0 0 0-2.6-1.5L14.2 3h-4l-.3 2.1a7.6 7.6 0 0 0-2.6 1.5l-2.1-.9-2 3.4 1.8 1.4a7.6 7.6 0 0 0 0 3l-1.8 1.4 2 3.4 2.1-.9a7.6 7.6 0 0 0 2.6 1.5l.3 2.1h4l.3-2.1a7.6 7.6 0 0 0 2.6-1.5l2.1.9 2-3.4-1.8-1.4Z"/></>))}
    </nav>
  );
}

function CGWPage({ children, active, minimal, hideFooter, hideTicker }) {
  return (
    <div className={'cgw' + (minimal ? ' cgw--minimal' : '')}>
      <CGWHeader active={active} minimal={minimal}/>
      {children}
      {!hideFooter && <CGWFooter/>}
      {!minimal && <CGWMobileTabBar active={active}/>}
      {/* PWA install prompt — slides up from the bottom of the viewport
          on Android/Chrome when the browser fires beforeinstallprompt,
          or on iOS Safari with a "tap Share → Add to Home Screen" hint
          (iOS never fires the event so we can't call prompt() there).
          Hides itself once the app is running standalone. */}
      <CGInstallPrompt/>
    </div>
  );
}

// ─── PWA "Install app" prompt ────────────────────────────────────────
// Two flows in one component:
//   1. Android / Chromium — window fires beforeinstallprompt; we capture
//      the event, show a custom pill, and on tap call event.prompt().
//   2. iOS Safari — the event NEVER fires (Apple decision). We detect
//      iPhone/iPad + non-standalone and show a short "tap Share, then
//      Add to Home Screen" hint instead. No programmatic install exists.
// Dismissible; stays hidden for 24h after dismissal (localStorage day key).
// Silently hides once display-mode: standalone (app is already installed).
function CGInstallPrompt() {
  const [deferred, setDeferred] = React.useState(null);
  const [iosHint, setIosHint] = React.useState(false);
  const [installed, setInstalled] = React.useState(false);
  const [dismissed, setDismissed] = React.useState(false);

  React.useEffect(() => {
    const KEY = 'cg-install-dismiss-day';
    const today = new Date().toISOString().slice(0, 10);
    try {
      if (localStorage.getItem(KEY) === today) setDismissed(true);
    } catch { /* private mode */ }

    // Already installed / running as PWA → never show.
    const isStandalone = window.matchMedia('(display-mode: standalone)').matches
      || window.navigator.standalone === true;
    if (isStandalone) { setInstalled(true); return; }

    // iOS Safari: no beforeinstallprompt, detect + hint.
    const ua = navigator.userAgent;
    const isIOS = /iPhone|iPad|iPod/.test(ua) && !window.MSStream;
    const isIOSSafari = isIOS && /Safari/.test(ua) && !/CriOS|FxiOS|EdgiOS/.test(ua);
    if (isIOSSafari) setIosHint(true);

    const onPrompt = (e) => {
      e.preventDefault();
      setDeferred(e);
    };
    const onInstalled = () => { setInstalled(true); setDeferred(null); };
    window.addEventListener('beforeinstallprompt', onPrompt);
    window.addEventListener('appinstalled', onInstalled);
    return () => {
      window.removeEventListener('beforeinstallprompt', onPrompt);
      window.removeEventListener('appinstalled', onInstalled);
    };
  }, []);

  const dismiss = () => {
    try { localStorage.setItem('cg-install-dismiss-day', new Date().toISOString().slice(0, 10)); } catch {}
    setDismissed(true);
  };

  const install = async () => {
    if (!deferred) return;
    deferred.prompt();
    try { await deferred.userChoice; } catch {}
    setDeferred(null);
  };

  if (installed || dismissed) return null;
  if (!deferred && !iosHint) return null;

  return (
    <div style={{
      position: 'fixed',
      left: 12, right: 12,
      // On phones, sit above the mobile tab bar so the two never overlap.
      bottom: window.innerWidth < 768 ? 'calc(78px + env(safe-area-inset-bottom, 0px))' : 12,
      zIndex: 999,
      maxWidth: 520, margin: '0 auto',
      background: 'linear-gradient(160deg, rgba(24,20,17,.98), rgba(14,11,9,.98))',
      border: '1px solid rgba(240,163,90,.35)',
      borderRadius: 16,
      padding: '14px 16px',
      color: 'var(--cream)',
      boxShadow: '0 24px 60px -20px rgba(0,0,0,.75)',
      display: 'flex', alignItems: 'center', gap: 12,
    }}>
      <img src="/pwa/icon-192.png" alt="" width="42" height="42"
           style={{ width: 42, height: 42, borderRadius: 10, flexShrink: 0 }}/>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 15, color: 'var(--cream)', lineHeight: 1.2 }}>
          Install CalGrillz
        </div>
        <div style={{ fontSize: 12.5, color: 'var(--cream-dim)', marginTop: 2, lineHeight: 1.4 }}>
          {iosHint
            ? <>Tap <strong>Share</strong> then <strong>Add to Home Screen</strong>.</>
            : 'One tap to add it to your home screen.'}
        </div>
      </div>
      {deferred && (
        <button type="button" onClick={install} style={{
          padding: '8px 14px', borderRadius: 999,
          background: 'linear-gradient(135deg, #c8102e, #f97316)',
          color: '#fff', border: 'none', cursor: 'pointer',
          fontSize: 12.5, fontWeight: 700, whiteSpace: 'nowrap',
        }}>Install</button>
      )}
      <button type="button" onClick={dismiss} aria-label="Dismiss" style={{
        background: 'transparent', border: 'none', color: 'var(--muted)', cursor: 'pointer',
        fontSize: 22, lineHeight: 1, padding: 4,
      }}>×</button>
    </div>
  );
}

// ─── Money formatter ────────────────────────────────────────────────
function nairaW(n) {
  return '₦' + Math.round(n).toLocaleString('en-NG');
}

// ─── Menu data (shared across pages) ────────────────────────────────
// EMPTY on purpose. The real menu is always injected by the backend
// (window.CG_DATA.menu = cg_public_menu()) on every live page, so this fallback
// is never used there. It used to hold a dozen fake sample dishes with
// hardcoded stock photos (whole-tilapia/grill-closeup.jpg, suya-platter, chapman
// jug, …) which could surface as fake items/pictures — removed.
const CGW_MENU_FALLBACK = [];

// Prefer the REAL menu injected by the backend (window.CG_DATA.menu) so prices
// and items match what checkout actually charges. Fall back to the sample menu
// above only for the standalone static preview (no backend data present).
const CGW_MENU = (typeof window !== 'undefined' && window.CG_DATA && Array.isArray(window.CG_DATA.menu) && window.CG_DATA.menu.length)
  ? window.CG_DATA.menu
  : CGW_MENU_FALLBACK;

const CGW_CATS = [
  { id: 'all',      label: 'All',          count: CGW_MENU.length },
  { id: 'fish',     label: 'Fish',         count: CGW_MENU.filter(m => m.cat === 'fish').length },
  { id: 'chicken',  label: 'Chicken & suya', count: CGW_MENU.filter(m => m.cat === 'chicken').length },
  { id: 'shawarma', label: 'Shawarma',     count: CGW_MENU.filter(m => m.cat === 'shawarma').length },
  { id: 'parfait',  label: 'Parfait',      count: CGW_MENU.filter(m => m.cat === 'parfait').length },
  { id: 'sides',    label: 'Sides',        count: CGW_MENU.filter(m => m.cat === 'sides').length },
  { id: 'drinks',   label: 'Drinks',       count: CGW_MENU.filter(m => m.cat === 'drinks').length },
].filter(c => c.id === 'all' || c.count > 0);

// ─── Photo or placeholder helper ────────────────────────────────────
function CGWDishMedia({ item, height, tagPrefix = '' }) {
  if (item.img) {
    return (
      <div style={{
        height,
        backgroundImage: `url(${CG_ASSET(item.img)})`,
        backgroundSize: 'cover',
        backgroundPosition: 'center',
        position: 'relative',
      }}/>
    );
  }
  // placeholder — guard against a missing slug/name so a data gap never crashes the card
  const code = (tagPrefix + String(item.slug || item.name || 'item').toUpperCase()).slice(0, 8);
  return (
    <div className="cgw-placeholder" style={{ position: 'relative', height, padding: 22 }}>
      <div className="cgw-placeholder__corner">{code}</div>
      <div className="cgw-placeholder__caption">
        <b>SHOOT NEEDED</b>
        {item.placeholder || 'Photo TBD'}
      </div>
    </div>
  );
}

// ─── Shared chip ────────────────────────────────────────────────────
function CGWTag({ kind }) {
  if (kind === 'fire') return <span className="cg-chip ember">🔥 GOING FAST</span>;
  if (kind === 'signature') return <span className="cg-chip ember">⭐ SIGNATURE</span>;
  if (kind === 'vip') return <span className="cg-chip" style={{ background: 'rgba(0,0,0,0.55)', color: '#fbbf24', borderColor: 'rgba(251,191,36,0.4)' }}>👑 VIP</span>;
  if (kind === 'spicy') return <span className="cg-chip" style={{ background: 'rgba(0,0,0,0.55)', color: '#f87171', borderColor: 'rgba(239,68,68,0.4)' }}>🌶 SPICY</span>;
  return null;
}

// ════════════════════════════════════════════════════════════════════════
// VOUCHER CARD COMPONENTS — premium gift-card design system, shared across
// the dashboard, voucher page, and purchase-success modal. Lifted from
// voucher/vouchers.html so this is the single source of truth.
// ════════════════════════════════════════════════════════════════════════

// Real scannable QR code, generated by qrcode-generator (loaded in layout.blade.php
// as window.qrcode). The payload is the voucher redemption URL so any phone
// camera can scan it and land on the right page.
//
// The `size` prop is retained for API compatibility with the previous FauxQR
// (it used to control module count) but now only acts as a hint when the QR
// library isn't loaded yet — we fall back to a simple deterministic pattern
// so the card never renders empty.
function CGFauxQR({ code = 'CG-VOUCHER', size = 21 }) {
  const data = React.useMemo(() => {
    // Always encode an absolute URL so a phone camera opens it directly.
    // Falls back to the bare code if window isn't ready (SSR-safe).
    if (typeof window === 'undefined') return code;
    const base = (window.CG_URLS && window.CG_URLS.voucher) || (window.location.origin + '/voucher');
    const sep = base.includes('?') ? '&' : '?';
    return `${base}${sep}code=${encodeURIComponent(code)}`;
  }, [code]);

  // Build the module grid via qrcode-generator. Type 0 = auto-pick the smallest
  // QR version that fits the data; 'M' = ~15% error-correction (still scans
  // even though we carve a small quiet zone for the centre logo).
  const matrix = React.useMemo(() => {
    if (typeof window === 'undefined' || !window.qrcode) return null;
    try {
      const qr = window.qrcode(0, 'M');
      qr.addData(data);
      qr.make();
      const count = qr.getModuleCount();
      const grid = new Array(count * count);
      for (let r = 0; r < count; r++) {
        for (let c = 0; c < count; c++) {
          grid[r * count + c] = qr.isDark(r, c);
        }
      }
      return { count, grid };
    } catch (e) {
      return null;
    }
  }, [data]);

  // Fallback deterministic pattern — only used while qrcode-generator loads.
  const fallback = React.useMemo(() => {
    if (matrix) return null;
    const n = size * size;
    const grid = new Array(n).fill(false);
    let h = 2166136261;
    for (let i = 0; i < code.length; i++) {
      h ^= code.charCodeAt(i);
      h = Math.imul(h, 16777619);
    }
    for (let i = 0; i < n; i++) {
      h ^= h << 13; h ^= h >>> 17; h ^= h << 5;
      grid[i] = ((h >>> 0) & 1) === 1;
    }
    const stamp = (cx, cy) => {
      for (let y = 0; y < 7; y++) for (let x = 0; x < 7; x++) {
        const onEdge = x === 0 || y === 0 || x === 6 || y === 6;
        const inner = x >= 2 && x <= 4 && y >= 2 && y <= 4;
        const ring = (x === 1 || y === 1 || x === 5 || y === 5);
        const on = onEdge || inner;
        const off = ring && !onEdge && !inner;
        grid[(cy + y) * size + (cx + x)] = on && !off;
      }
    };
    stamp(0, 0); stamp(size - 7, 0); stamp(0, size - 7);
    return { count: size, grid };
  }, [matrix, code, size]);

  const m = matrix || fallback;
  const count = m.count;
  const cell = 100 / count;
  return (
    <svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" aria-label={`QR code for ${code}`}>
      <rect width="100" height="100" fill="#ffffff"/>
      {m.grid.map((on, i) => on ? (
        <rect key={i}
          x={(i % count) * cell}
          y={Math.floor(i / count) * cell}
          width={cell + 0.4}
          height={cell + 0.4}
          fill="#0a0a0a"/>
      ) : null)}
    </svg>
  );
}

// Strip a trailing "off" / "OFF" (with optional " any order") from a value
// string so it doesn't render twice — the card already shows a dedicated
// "OFF / any order" badge column next to the big number.
function stripOff(s) {
  if (!s) return s;
  return String(s).replace(/\s*off(\s+any\s+order)?\s*$/i, '').trim();
}

// Full-size gift-card display. Props mirror the API:
//   state:     'available' | 'redeemed' | 'revoked'
//   amount:    pre-formatted display string (e.g. '₦5,000' or '15% off')
//              — a trailing "off" is stripped so it doesn't double up with
//              the card's built-in OFF badge.
//   code:      monospace voucher code
//   from:      optional gifter name
//   issuedDate / expires:  pre-formatted date strings
function CGVoucherCard({ state = 'available', pending = false, amount, code, from, expires, issuedDate, refEl, copyable = true }) {
  const cleanAmount = stripOff(amount);
  const [copied, setCopied] = React.useState(false);
  function copy() {
    if (!copyable || state !== 'available') return;
    if (navigator.clipboard) navigator.clipboard.writeText(code);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  }

  return (
    <div ref={refEl} className={`vc vc--${state}` + (copyable ? ' vc--interactive' : '')} onClick={copy}>
      {state === 'available' && !pending && <div className="vc__seal">Active</div>}
      {pending && <div className="vc__seal" style={{ background: 'linear-gradient(135deg,#fbbf24,#d8632c)', color: '#231a00' }}>Pending</div>}

      <svg className="vc__corner-flame" width="180" height="180" viewBox="0 0 24 24">
        <defs>
          <linearGradient id={`flame-${state}-${code}`} x1="0" x2="0" y1="1" y2="0">
            <stop offset="0" stopColor="#c8102e"/>
            <stop offset="0.5" stopColor="#d8632c"/>
            <stop offset="1" stopColor="#d4a02e"/>
          </linearGradient>
        </defs>
        <path d="M12 2c1 3 3 4 3 7a3 3 0 0 1-3 3 3 3 0 0 1-3-3c0-1 1-2 1-3-2 1-4 4-4 7a6 6 0 0 0 12 0c0-5-4-8-6-11z" fill={`url(#flame-${state}-${code})`}/>
      </svg>

      {state === 'available' && <div className="vc__shimmer"/>}
      {state === 'available' && (
        <div className="vc__embers">
          {Array.from({ length: 6 }).map((_, i) => (
            <span key={i} className="cg-ember" style={{
              left: `${20 + i * 13}%`,
              bottom: `${10 + (i * 7) % 30}%`,
              width: 3 + (i % 3), height: 3 + (i % 3),
              animation: `ember-rise ${3 + (i % 3)}s ease-out ${i * 0.5}s infinite`,
            }}/>
          ))}
        </div>
      )}

      <div className="vc__notch vc__notch--l"/>
      <div className="vc__notch vc__notch--r"/>
      <div className="vc__inset"/>

      {state === 'redeemed' && <div className="vc__stamp vc__stamp--redeemed">Redeemed</div>}
      {state === 'revoked' && <div className="vc__stamp vc__stamp--revoked">Revoked</div>}

      <div className="vc__body">
        <div className="vc__top">
          <div className="vc__brand">
            <img src={CG_ASSET('calgrillz-logo.png')} alt=""/>
          </div>
          <span className={`vc__status vc__status--${state === 'available' ? 'avail' : state}`} style={pending ? { color: '#fbbf24' } : undefined}>
            <span className="dot" style={pending ? { background: '#fbbf24' } : undefined}/>
            {pending ? 'Pending payment' : state === 'available' ? 'Active' : state === 'redeemed' ? 'Redeemed' : 'Revoked'}
          </span>
        </div>
        <div className="vc__main">
          <div className="vc__type">Gift voucher{from ? <span style={{ opacity: 0.7, fontWeight: 500 }}> · from {from}</span> : null}</div>
          <div className="vc__mid">
            <span className="vc__amount">{cleanAmount}</span>
            <span className="vc__amount-meta">
              <span className="vc__amount-off">OFF</span>
              <span className="vc__amount-label">any order</span>
            </span>
          </div>
          <div className="vc__code">
            <span className="vc__code-val">{code}</span>
            <button type="button" className="vc__code-action" onClick={(e) => { e.stopPropagation(); copy(); }}>
              {state === 'available' ? (copied ? '✓ Copied' : 'Tap to copy') : state === 'redeemed' ? 'Used' : 'Cancelled'}
            </button>
          </div>
          <div className="vc__meta">
            {issuedDate && <span>Issued <strong>{issuedDate}</strong></span>}
            {expires && <span>{state === 'available' ? <>Expires <strong>{expires}</strong></> : state === 'redeemed' ? <>Redeemed <strong>{expires}</strong></> : <>Revoked <strong>{expires}</strong></>}</span>}
          </div>
        </div>
        <div className="vc__qr">
          <div className="vc__qr-frame">
            <CGFauxQR code={code}/>
            <div className="vc__qr-mark"><span>
              <svg width="12" height="12" viewBox="0 0 24 24">
                <defs><linearGradient id={`qrf-${code}`} x1="0" x2="0" y1="1" y2="0">
                  <stop offset="0" stopColor="#c8102e"/><stop offset="0.5" stopColor="#d8632c"/><stop offset="1" stopColor="#d4a02e"/>
                </linearGradient></defs>
                <path d="M12 2c1 3 3 4 3 7a3 3 0 0 1-3 3 3 3 0 0 1-3-3c0-1 1-2 1-3-2 1-4 4-4 7a6 6 0 0 0 12 0c0-5-4-8-6-11z" fill={`url(#qrf-${code})`}/>
              </svg>
            </span></div>
          </div>
          <div className="vc__qr-label">Scan to redeem</div>
        </div>
      </div>
    </div>
  );
}

// Compact row variant for wallet lists / dashboards.
function CGVoucherMini({ state = 'avail', amount, code, sub }) {
  return (
    <div className={`vc-mini s-${state}`}>
      <span className="vc-mini__amt">{stripOff(amount)}</span>
      <div className="vc-mini__info">
        <div className="vc-mini__code">{code}</div>
        {sub && <div className="vc-mini__sub">{sub}</div>}
      </div>
      <div className="vc-mini__qr"><CGFauxQR code={code} size={15}/></div>
      <span className="vc-mini__status">{state === 'avail' ? 'Active' : state === 'redeemed' ? 'Used' : 'Revoked'}</span>
    </div>
  );
}

// Download toolbar — captures a referenced VoucherCard DOM node and emits
// PNG, JPEG, or PDF. Relies on html2canvas + jsPDF (loaded from CDN by the
// Blade layout). Falls back gracefully if either library isn't available.
function CGVoucherDownloads({ targetRef, fileBase = 'voucher' }) {
  const [busy, setBusy] = React.useState(null);   // 'png' | 'jpeg' | 'pdf' | null

  // ─── Export-DOM builder ──────────────────────────────────────────────────
  // Trying to make html2canvas render the live `.vc` element kept hitting
  // walls — the gradient-border `::before` mask-composite, the `border-image`
  // ring, and `backdrop-filter`-blurred pills are all unrenderable, and CSS
  // overrides in onclone proved unreliable. So instead of fighting cloned
  // stylesheets, we extract the data from the live card (amount, code,
  // expiry, etc.) and build a brand-new offscreen DOM that uses ONLY inline
  // styles and html2canvas-safe primitives. html2canvas captures that, then
  // we throw it away. The live page is untouched.
  function buildExportCard() {
    const live = targetRef.current;
    if (!live) throw new Error('no live card to capture');

    // Pull the data straight from the live DOM so we don't need new props.
    const state = live.classList.contains('vc--available') ? 'available'
                : live.classList.contains('vc--redeemed')  ? 'redeemed'
                : live.classList.contains('vc--revoked')   ? 'revoked'
                : 'available';
    const txt = (sel) => { const el = live.querySelector(sel); return el ? el.textContent.trim() : ''; };
    const amount    = txt('.vc__amount');
    const code      = txt('.vc__code-val');
    const typeRaw   = txt('.vc__type');
    const metaSpans = live.querySelectorAll('.vc__meta span');
    const issuedTxt = metaSpans[0] ? metaSpans[0].textContent.trim() : '';
    const expiresTxt = (metaSpans.length > 1 ? metaSpans[metaSpans.length - 1] : metaSpans[0])
                       ? (metaSpans[metaSpans.length - 1] || metaSpans[0]).textContent.trim()
                       : '';
    const liveQrSvg = live.querySelector('.vc__qr svg');
    const qrSvgHtml = liveQrSvg ? liveQrSvg.outerHTML : '';
    const logoSrc   = (live.querySelector('.vc__brand img') || {}).src
                   || ((window.CG_ASSET_BASE || 'assets') + '/calgrillz-logo.png');

    // Colour palette per state — concrete hex / rgba only.
    const palette = state === 'redeemed' ? {
      bg: 'linear-gradient(135deg, #1a1612 0%, #13100d 100%)',
      border: '1px solid #322a23',
      amount: '#bcaa90', off: '#8c7f70', type: '#8c7f70',
      sealText: 'Used', sealBg: '#8c7f70', sealFg: '#13100d',
      statusBg: '#2a2620', statusFg: '#8c7f70', statusBorder: 'rgba(140,127,112,0.3)',
      codeBg: 'rgba(0,0,0,0.3)', codeBorder: '1px dashed rgba(140,127,112,0.3)',
      codeFg: '#8c7f70', meta: '#bcaa90',
    } : state === 'revoked' ? {
      bg: 'linear-gradient(135deg, #1a0e0c 0%, #0f0808 100%)',
      border: '1.5px solid rgba(216,99,44,0.45)',
      amount: '#d8632c', off: '#d8632c', type: '#d8632c',
      sealText: 'Cancelled', sealBg: '#d8632c', sealFg: '#13100d',
      statusBg: '#3a201a', statusFg: '#d8632c', statusBorder: 'rgba(216,99,44,0.45)',
      codeBg: 'rgba(0,0,0,0.45)', codeBorder: '1px dashed rgba(216,99,44,0.4)',
      codeFg: '#d8632c', meta: '#d8632c',
    } : {
      bg:
        'radial-gradient(ellipse at top right, rgba(216,99,44,0.35) 0%, transparent 55%),'+
        'radial-gradient(ellipse at bottom left, rgba(200,16,46,0.25) 0%, transparent 55%),'+
        'linear-gradient(135deg, #2a1410 0%, #1a0a08 50%, #0a0a0a 100%)',
      border: '1.5px solid #d8632c',
      amount: '#f5ecdc', off: '#d8632c', type: '#d4a02e',
      sealText: 'Active', sealBg: '#22c55e', sealFg: '#0a0a0a',
      statusBg: '#1d3329', statusFg: '#6fc18b', statusBorder: 'rgba(95,163,119,0.4)',
      codeBg: 'rgba(0,0,0,0.4)', codeBorder: '1px dashed rgba(212,160,46,0.3)',
      codeFg: '#f5ecdc', meta: '#d4a02e',
    };

    // 5:3 aspect, generous size for crisp downscale.
    const W = 720, H = Math.round(W * 3 / 5);
    const card = document.createElement('div');
    card.style.cssText = [
      `width:${W}px`, `height:${H}px`,
      'position:fixed', 'left:-10000px', 'top:0', 'z-index:-1',
      `background:${palette.bg}`,
      `border:${palette.border}`,
      'border-radius:24px',
      `box-shadow:0 24px 70px -22px rgba(200,16,46,0.55)`,
      "font-family:'Inter Tight', system-ui, sans-serif",
      `color:${palette.amount}`,
      'box-sizing:border-box',
      'padding:32px 36px',
      'display:flex', 'flex-direction:column', 'justify-content:space-between',
      'overflow:hidden',
    ].join(';');

    // Active / Used / Cancelled corner banner.
    const seal = document.createElement('div');
    seal.textContent = palette.sealText.toUpperCase();
    seal.style.cssText = [
      'position:absolute', 'top:18px', 'left:-44px',
      'transform:rotate(-45deg)', 'transform-origin:center',
      `background:${palette.sealBg}`, `color:${palette.sealFg}`,
      'font-size:11px', 'font-weight:800', 'letter-spacing:0.18em',
      'padding:6px 60px', 'text-align:center',
      'box-shadow:0 2px 6px rgba(0,0,0,0.4)',
    ].join(';');

    // Status pill (top-right).
    const status = document.createElement('div');
    status.style.cssText = [
      'position:absolute', 'top:24px', 'right:32px',
      `background:${palette.statusBg}`, `color:${palette.statusFg}`,
      `border:1px solid ${palette.statusBorder}`,
      'padding:7px 14px', 'border-radius:999px',
      'font-size:11px', 'font-weight:700', 'letter-spacing:0.14em',
      'display:flex', 'align-items:center', 'gap:8px',
    ].join(';');
    const dot = document.createElement('span');
    dot.style.cssText = `width:7px;height:7px;border-radius:50%;background:${palette.statusFg};display:inline-block`;
    status.appendChild(dot);
    status.appendChild(document.createTextNode(palette.sealText.toUpperCase()));

    // Header row: logo + (status sits absolutely above this).
    const header = document.createElement('div');
    header.style.cssText = 'display:flex;align-items:center;gap:12px;margin-top:8px';
    const logo = document.createElement('img');
    logo.src = logoSrc;
    logo.crossOrigin = 'anonymous';
    logo.style.cssText = 'height:36px;width:auto;display:block';
    header.appendChild(logo);

    // Main row: amount + OFF / any order + QR (right side).
    const main = document.createElement('div');
    main.style.cssText = 'display:flex;align-items:flex-end;gap:24px;flex:1';

    const left = document.createElement('div');
    left.style.cssText = 'flex:1;display:flex;flex-direction:column;gap:14px';

    // "Gift voucher" eyebrow.
    const eyebrow = document.createElement('div');
    eyebrow.textContent = typeRaw || 'Gift voucher';
    eyebrow.style.cssText = `font-size:11px;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:${palette.type};line-height:1`;

    // Amount + OFF column.
    const amtRow = document.createElement('div');
    amtRow.style.cssText = 'display:flex;align-items:flex-end;gap:18px';
    const amt = document.createElement('span');
    amt.textContent = stripOff(amount);
    amt.style.cssText = `font-family:'DM Serif Display', Georgia, serif;font-size:72px;line-height:0.9;letter-spacing:-0.015em;color:${palette.amount}`;
    const meta = document.createElement('div');
    meta.style.cssText = 'display:flex;flex-direction:column;gap:4px;padding-bottom:10px';
    const off = document.createElement('span');
    off.textContent = 'OFF';
    off.style.cssText = `font-size:22px;font-weight:800;letter-spacing:0.06em;color:${palette.off};line-height:1`;
    const sub = document.createElement('span');
    sub.textContent = 'ANY ORDER';
    sub.style.cssText = `font-size:10px;font-weight:600;letter-spacing:0.22em;color:#d4c4ad;line-height:1`;
    meta.appendChild(off); meta.appendChild(sub);
    amtRow.appendChild(amt); amtRow.appendChild(meta);

    // Code pill.
    const codeRow = document.createElement('div');
    codeRow.style.cssText = [
      'display:flex','align-items:center','justify-content:space-between',
      `background:${palette.codeBg}`, `border:${palette.codeBorder}`,
      'border-radius:12px','padding:12px 18px','margin-top:4px',
    ].join(';');
    const codeVal = document.createElement('span');
    codeVal.textContent = code;
    codeVal.style.cssText = `font-family:'JetBrains Mono', ui-monospace, monospace;font-size:16px;letter-spacing:0.1em;font-weight:500;color:${palette.codeFg}`;
    const codeTag = document.createElement('span');
    codeTag.textContent = state === 'available' ? 'Tap to copy' : state === 'redeemed' ? 'Used' : 'Cancelled';
    codeTag.style.cssText = `font-size:11px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:${palette.off}`;
    codeRow.appendChild(codeVal); codeRow.appendChild(codeTag);

    // Issued / expires meta (bottom strip).
    const dates = document.createElement('div');
    dates.style.cssText = `display:flex;gap:18px;font-size:11px;color:${palette.statusFg};margin-top:2px;letter-spacing:0.04em`;
    if (issuedTxt) {
      const a = document.createElement('span'); a.textContent = issuedTxt;
      a.style.color = palette.meta; dates.appendChild(a);
    }
    if (expiresTxt && expiresTxt !== issuedTxt) {
      const b = document.createElement('span'); b.textContent = expiresTxt;
      b.style.color = palette.meta; dates.appendChild(b);
    }

    left.appendChild(eyebrow);
    left.appendChild(amtRow);
    left.appendChild(codeRow);
    left.appendChild(dates);

    // Right: QR + label.
    const right = document.createElement('div');
    right.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:10px';
    const qrFrame = document.createElement('div');
    qrFrame.style.cssText = [
      'width:128px','height:128px',
      'background:#ffffff',
      'border-radius:14px',
      'padding:8px','box-sizing:border-box',
      'border:2px solid #d4a02e',
      'display:flex','align-items:center','justify-content:center',
    ].join(';');
    if (qrSvgHtml) {
      const wrap = document.createElement('div');
      wrap.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center';
      wrap.innerHTML = qrSvgHtml;
      const innerSvg = wrap.querySelector('svg');
      if (innerSvg) {
        innerSvg.setAttribute('width', '100%');
        innerSvg.setAttribute('height', '100%');
        innerSvg.style.display = 'block';
      }
      qrFrame.appendChild(wrap);
    }
    const qrLabel = document.createElement('div');
    qrLabel.textContent = 'SCAN TO REDEEM';
    qrLabel.style.cssText = `font-size:10px;font-weight:700;letter-spacing:0.18em;color:${palette.statusFg}`;
    right.appendChild(qrFrame);
    right.appendChild(qrLabel);

    main.appendChild(left); main.appendChild(right);
    card.appendChild(seal);
    card.appendChild(status);
    card.appendChild(header);
    card.appendChild(main);

    document.body.appendChild(card);
    return card;
  }

  // Build the export DOM, capture it with html2canvas, then remove it.
  async function render() {
    if (!window.html2canvas) throw new Error('html2canvas not loaded');
    if (!targetRef?.current) throw new Error('no card to capture');
    // Wait for web fonts so the amount glyph isn't substituted in the snapshot.
    if (document.fonts && document.fonts.ready) {
      try { await document.fonts.ready; } catch (e) { /* not fatal */ }
    }
    const card = buildExportCard();
    // Give the browser a paint tick so the image loads + layout settles.
    await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
    try {
      return await window.html2canvas(card, {
        backgroundColor: null,
        scale: 2,
        useCORS: true,
        logging: false,
        allowTaint: false,
      });
    } finally {
      // Always clean up, even if html2canvas threw.
      if (card.parentNode) card.parentNode.removeChild(card);
    }
  }

  async function downloadImage(format) {
    setBusy(format);
    try {
      const canvas = await render();
      const mime = format === 'jpeg' ? 'image/jpeg' : 'image/png';
      const quality = format === 'jpeg' ? 0.92 : undefined;
      const link = document.createElement('a');
      link.download = `${fileBase}.${format}`;
      link.href = canvas.toDataURL(mime, quality);
      link.click();
    } catch (e) {
      alert('Could not render the card: ' + e.message);
    } finally {
      setBusy(null);
    }
  }

  async function downloadPdf() {
    setBusy('pdf');
    try {
      const canvas = await render();
      const img = canvas.toDataURL('image/png');
      // jsPDF lives at window.jspdf.jsPDF when loaded from the UMD bundle
      const JsPDF = (window.jspdf && window.jspdf.jsPDF) || window.jsPDF;
      if (!JsPDF) throw new Error('jsPDF not loaded');
      // 5:3 aspect ratio → use a landscape page sized to the card
      const wMm = 148, hMm = wMm * 3 / 5;
      const pdf = new JsPDF({ orientation: 'landscape', unit: 'mm', format: [wMm, hMm] });
      pdf.addImage(img, 'PNG', 0, 0, wMm, hMm);
      pdf.save(`${fileBase}.pdf`);
    } catch (e) {
      alert('Could not generate PDF: ' + e.message);
    } finally {
      setBusy(null);
    }
  }

  return (
    <div className="vc-actions">
      <button type="button" disabled={busy === 'png'} onClick={() => downloadImage('png')}>
        {busy === 'png' ? 'Rendering…' : '📥 PNG'}
      </button>
      <button type="button" disabled={busy === 'jpeg'} onClick={() => downloadImage('jpeg')}>
        {busy === 'jpeg' ? 'Rendering…' : '📥 JPEG'}
      </button>
      <button type="button" className="primary" disabled={busy === 'pdf'} onClick={downloadPdf}>
        {busy === 'pdf' ? 'Building…' : '📄 PDF'}
      </button>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════
// CART STORE — single source of truth, backed by localStorage so the cart
// state survives page navigation between menu / item / cart / checkout.
// Pages subscribe via the `cgCart:changed` window event.
// ════════════════════════════════════════════════════════════════════════
const CGCart = (() => {
  const KEY = 'cg_cart_v1';

  function read() {
    try {
      const raw = localStorage.getItem(KEY);
      const parsed = raw ? JSON.parse(raw) : [];
      return Array.isArray(parsed) ? parsed : [];
    } catch (e) {
      return [];
    }
  }
  function write(items) {
    try { localStorage.setItem(KEY, JSON.stringify(items)); } catch (e) { /* private mode etc */ }
    window.dispatchEvent(new CustomEvent('cgCart:changed', { detail: items }));
  }

  function get() { return read(); }
  function count() { return read().reduce((s, x) => s + (x.qty || 0), 0); }
  function subtotal() { return read().reduce((s, x) => s + (x.price || 0) * (x.qty || 0), 0); }

  function add(item) {
    if (!item || !item.slug) return;
    // Pixel/analytics — every add on the whole site funnels through here.
    try {
      const qty = item.qty || 1;
      window.CG_TRACK && window.CG_TRACK.event('AddToCart', {
        content_ids: [item.slug], content_name: item.name || item.slug,
        content_type: 'product', value: (item.price || 0) * qty, currency: 'NGN', num_items: qty,
      });
    } catch (e) {}
    const items = read();
    const variantKey = item.variant || '';
    const existing = items.find(x => x.slug === item.slug && (x.variant || '') === variantKey);
    if (existing) {
      existing.qty = (existing.qty || 1) + (item.qty || 1);
    } else {
      items.push({
        id: Date.now() + Math.random(),
        slug: item.slug,
        name: item.name || item.slug,
        variant: item.variant || '',
        sides: item.sides || [],
        qty: item.qty || 1,
        price: item.price || 0,
        img: item.img || null,
      });
    }
    write(items);
  }

  function setQty(id, qty) {
    const items = read();
    const x = items.find(it => it.id === id);
    if (!x) return;
    x.qty = Math.max(1, qty);
    write(items);
  }
  function inc(id) { const x = read().find(it => it.id === id); if (x) setQty(id, (x.qty || 1) + 1); }
  function dec(id) { const x = read().find(it => it.id === id); if (x) setQty(id, Math.max(1, (x.qty || 1) - 1)); }
  function remove(id) { write(read().filter(it => it.id !== id)); }
  function clear() { write([]); }

  // Cross-tab sync: storage events fire when another tab edits the cart.
  window.addEventListener('storage', (e) => {
    if (e.key === KEY) window.dispatchEvent(new CustomEvent('cgCart:changed', { detail: read() }));
  });

  return { get, count, subtotal, add, setQty, inc, dec, remove, clear };
})();

// React hook — subscribes to the cart and re-renders on change.
function useCart() {
  const [items, setItems] = React.useState(() => CGCart.get());
  React.useEffect(() => {
    const handler = (e) => setItems(e.detail || CGCart.get());
    window.addEventListener('cgCart:changed', handler);
    return () => window.removeEventListener('cgCart:changed', handler);
  }, []);
  return {
    items,
    count: items.reduce((s, x) => s + (x.qty || 0), 0),
    subtotal: items.reduce((s, x) => s + (x.price || 0) * (x.qty || 0), 0),
    add: CGCart.add,
    setQty: CGCart.setQty,
    inc: CGCart.inc,
    dec: CGCart.dec,
    remove: CGCart.remove,
    clear: CGCart.clear,
  };
}

// ════════════════════════════════════════════════════════════════════════
// WISHLIST STORE — items a customer saves for later. Same localStorage +
// event pattern as the cart, so it persists on the device and every page
// (menu cards, saved strip, header count) stays in sync. Stores just enough
// to render + add-to-cart later: slug, name, priceFrom, img.
// ════════════════════════════════════════════════════════════════════════
const CGWish = (() => {
  const KEY = 'cg_wish_v1';
  function read() {
    try { const raw = localStorage.getItem(KEY); const p = raw ? JSON.parse(raw) : []; return Array.isArray(p) ? p : []; }
    catch (e) { return []; }
  }
  function write(items) {
    try { localStorage.setItem(KEY, JSON.stringify(items)); } catch (e) { /* private mode */ }
    window.dispatchEvent(new CustomEvent('cgWish:changed', { detail: items }));
  }
  function get() { return read(); }
  function has(slug) { return read().some(x => x.slug === slug); }
  function toggle(item) {
    if (!item || !item.slug) return;
    const items = read();
    const i = items.findIndex(x => x.slug === item.slug);
    if (i >= 0) items.splice(i, 1);
    else items.push({ slug: item.slug, name: item.name || item.slug, priceFrom: item.priceFrom || 0, img: item.img || null });
    write(items);
  }
  function remove(slug) { write(read().filter(x => x.slug !== slug)); }
  function clear() { write([]); }
  window.addEventListener('storage', (e) => {
    if (e.key === KEY) window.dispatchEvent(new CustomEvent('cgWish:changed', { detail: read() }));
  });
  return { get, has, toggle, remove, clear };
})();

// React hook — subscribes to the wishlist and re-renders on change.
function useWish() {
  const [items, setItems] = React.useState(() => CGWish.get());
  React.useEffect(() => {
    const handler = (e) => setItems(e.detail || CGWish.get());
    window.addEventListener('cgWish:changed', handler);
    return () => window.removeEventListener('cgWish:changed', handler);
  }, []);
  return {
    items,
    count: items.length,
    has: (slug) => items.some(x => x.slug === slug),
    toggle: CGWish.toggle,
    remove: CGWish.remove,
    clear: CGWish.clear,
  };
}

// ════════════════════════════════════════════════════════════════════════
// VOUCHER STORE — applied vouchers survive page navigation between
// /cart and /checkout so the discount preview stays in sync.
//
// computeDiscount() mirrors app/Models/Voucher.php (effectiveMinOrderKobo +
// discountFor) so the client-side preview matches what OrderService applies
// at order commit. Money-typed vouchers require subtotal ≥ a fraction of the
// face value (kept as a quiet implementation detail — never surfaced in copy).
// ════════════════════════════════════════════════════════════════════════
const CG_VOUCHER_FLOOR = 0.85;

function cgVoucherDiscount(voucher, remainingSubtotal) {
  if (!voucher || remainingSubtotal <= 0) return 0;
  const percent = Number(voucher.percent) || 0;
  if (voucher.type === 'percentage' && percent > 0) {
    return Math.floor(remainingSubtotal * (percent / 100));
  }
  const value = Number(voucher.value) || 0;
  if (value <= 0) return 0;
  const minOrder = Math.ceil(value * CG_VOUCHER_FLOOR);
  if (remainingSubtotal < minOrder) return 0;
  return Math.min(value, remainingSubtotal);
}

// Run a list of applied vouchers against a subtotal and return both the
// total discount and a per-voucher breakdown ({ applied: kobo }) so the UI
// can subtly grey out an idle voucher without spelling out why.
function cgComputeVouchers(applied, subtotal) {
  let remaining = subtotal;
  const lines = [];
  for (const v of (applied || [])) {
    const d = cgVoucherDiscount(v, remaining);
    lines.push({ ...v, applied: d });
    remaining = Math.max(0, remaining - d);
  }
  return { discount: subtotal - remaining, lines };
}

// Live server-side voucher validation — called the moment a code is typed on
// the cart OR checkout. Runs the real ownership/expiry/status/min-order rules
// server-side (mirrors what OrderService re-runs at commit), so an invalid,
// expired, used, or someone-else's code is rejected UP FRONT instead of being
// accepted blindly (value 0) and only failing at place-order. On success it
// returns the canonical voucher normalized to the store shape (money value in
// naira, percent 0–100), identical to how CG_DATA.ownedVouchers is normalized.
async function cgValidateVoucher(code, subtotalNaira, appliedCodes) {
  const url = window.CG_URLS && window.CG_URLS.validateVoucher;
  if (!url) return { ok: false, error: 'Voucher check is unavailable right now.' };
  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json', 'Accept': 'application/json',
        'X-CSRF-TOKEN': (window.CG_URLS && window.CG_URLS.csrf) || '',
        'X-Requested-With': 'XMLHttpRequest',
      },
      credentials: 'same-origin',
      body: JSON.stringify({
        code: String(code || '').trim().toUpperCase(),
        subtotal_kobo: Math.max(0, Math.round((Number(subtotalNaira) || 0) * 100)),
        applied_codes: (appliedCodes || []).map(c => String(c).toUpperCase()),
      }),
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok || !data.ok) {
      return { ok: false, error: (data && data.error) || 'Could not check that voucher.', needsLogin: !!(data && data.needsLogin) };
    }
    const v = data.voucher || {};
    const isPct = v.type === 'percentage';
    return {
      ok: true,
      voucher: {
        code: v.code,
        type: v.type || 'fixed',
        value: isPct ? 0 : Math.round((Number(v.value) || 0) / 100),   // kobo → naira
        percent: isPct ? Number(v.value) || 0 : 0,
        label: v.label || v.code,
        discountNaira: Number(v.discount_naira) || 0,
      },
    };
  } catch (e) {
    return { ok: false, error: 'Could not reach the server to check that voucher. Try again.' };
  }
}

const CGVouchers = (() => {
  const KEY = 'cg_vouchers_v1';
  function read() {
    try {
      const raw = localStorage.getItem(KEY);
      const parsed = raw ? JSON.parse(raw) : [];
      return Array.isArray(parsed) ? parsed : [];
    } catch (e) { return []; }
  }
  function write(list) {
    try { localStorage.setItem(KEY, JSON.stringify(list)); } catch (e) { /* private mode */ }
    window.dispatchEvent(new CustomEvent('cgVouchers:changed', { detail: list }));
  }
  function get() { return read(); }
  function apply(voucher) {
    if (!voucher || !voucher.code) return;
    const list = read();
    if (list.find(v => v.code === voucher.code)) return;
    write([...list, {
      code: voucher.code,
      value: Number(voucher.value) || 0,
      type: voucher.type || 'fixed',
      percent: Number(voucher.percent) || 0,
      label: voucher.label || voucher.code,
    }]);
  }
  function remove(code) { write(read().filter(v => v.code !== code)); }
  function clear() { write([]); }
  window.addEventListener('storage', (e) => {
    if (e.key === KEY) window.dispatchEvent(new CustomEvent('cgVouchers:changed', { detail: read() }));
  });
  return { get, apply, remove, clear };
})();

function useVouchers() {
  const [list, setList] = React.useState(() => CGVouchers.get());
  React.useEffect(() => {
    const handler = (e) => setList(e.detail || CGVouchers.get());
    window.addEventListener('cgVouchers:changed', handler);
    return () => window.removeEventListener('cgVouchers:changed', handler);
  }, []);
  return {
    list,
    apply: CGVouchers.apply,
    remove: CGVouchers.remove,
    clear: CGVouchers.clear,
  };
}

// ════════════════════════════════════════════════════════════════════════
// ADDRESS BOOK — saved delivery addresses, shared by checkout + profile.
// localStorage-backed (key: cg_addresses_v1) so additions survive refresh
// and stay in sync between tabs.
//
// Starts EMPTY — a new customer has no saved addresses and is prompted to
// add their real one at checkout. (We used to seed two demo addresses here,
// "Plot 14, Ekei Crescent" / "Old Lagos Rd, Block C", which showed up as
// fake "saved" addresses for every real user — removed.)
//
// Shape: { id, label, sub, isDefault? }
// ════════════════════════════════════════════════════════════════════════
const CG_DEFAULT_ADDRESSES = [];

const CGAddresses = (() => {
  // v2: bumped from v1 to flush the old demo seed addresses
  // ("Plot 14, Ekei Crescent" / "Old Lagos Rd, Block C") that returning
  // visitors still had cached in localStorage.
  const KEY = 'cg_addresses_v2';
  function read() {
    try {
      const raw = localStorage.getItem(KEY);
      const parsed = raw ? JSON.parse(raw) : null;
      if (Array.isArray(parsed) && parsed.length) return parsed;
    } catch (e) { /* fall through */ }
    return CG_DEFAULT_ADDRESSES.slice();
  }
  function write(list) {
    try { localStorage.setItem(KEY, JSON.stringify(list)); } catch (e) { /* private mode */ }
    window.dispatchEvent(new CustomEvent('cgAddresses:changed', { detail: list }));
  }
  function get() { return read(); }
  function add({ label, sub }) {
    const cleanLabel = String(label || '').trim();
    if (!cleanLabel) return null;
    const id = 'a' + Date.now() + Math.floor(Math.random() * 100);
    const next = [...read(), { id, label: cleanLabel, sub: String(sub || '').trim() || 'New address' }];
    write(next);
    return id;
  }
  function update(id, patch) {
    const list = read();
    const idx = list.findIndex(a => a.id === id);
    if (idx < 0) return;
    list[idx] = { ...list[idx], ...patch };
    write(list);
  }
  function remove(id) {
    const a = read().find(x => x.id === id);
    if (a && a.isSeed) return;          // seeds are immutable
    write(read().filter(x => x.id !== id));
  }
  function clear() { write(CG_DEFAULT_ADDRESSES.slice()); }
  window.addEventListener('storage', (e) => {
    if (e.key === KEY) window.dispatchEvent(new CustomEvent('cgAddresses:changed', { detail: read() }));
  });
  return { get, add, update, remove, clear };
})();

function useAddresses() {
  const [list, setList] = React.useState(() => CGAddresses.get());
  React.useEffect(() => {
    const handler = (e) => setList(e.detail || CGAddresses.get());
    window.addEventListener('cgAddresses:changed', handler);
    return () => window.removeEventListener('cgAddresses:changed', handler);
  }, []);
  return {
    list,
    add: CGAddresses.add,
    update: CGAddresses.update,
    remove: CGAddresses.remove,
    clear: CGAddresses.clear,
  };
}

// Export
Object.assign(window, {
  CGWHeader, CGWFooter, CGWTicker, CGWPageHero, CGWPage, CGWMobileTabBar,
  CGSocialIcon, CGSocialLinks,
  CGW_MENU, CGW_CATS, CGWDishMedia, CGWTag, nairaW,
  CG_URL, CG_ASSET,
  CGFauxQR, CGVoucherCard, CGVoucherMini, CGVoucherDownloads,
  CGCart, useCart,
  CGVouchers, useVouchers, cgVoucherDiscount, cgComputeVouchers,
  CGAddresses, useAddresses,
  cgSignOut,
});
