// Executables — Cyberpunk product shop
// Main app

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

// ---------- TWEAKABLE DEFAULTS ----------
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accentPrimary": "#00ffd1",
  "accentSecondary": "#ff2bd6",
  "bgBase": "#0a0a0f",
  "scanlines": true,
  "glitchHero": true,
  "cardDensity": "comfy",
  "gridCols": 3,
  "showSpotlight": true,
  "monoEverything": false,
  "noiseTexture": true,
  "cornerCuts": true
}/*EDITMODE-END*/;

// ---------- HELPERS ----------
const cls = (...a) => a.filter(Boolean).join(' ');
const fmt = (n) => '$' + Number(n).toFixed(2);

// Pricing helpers — month is the headline price everywhere outside the modal.
const priceFor = (p, tier = 'month') => (p.pricing ? p.pricing[tier] : p.price);
const headlinePrice = (p) => priceFor(p, 'month');

// Image helpers — real screenshots in product.images take priority over
// procedural filler SVGs. Card/spotlight/cart all use the first image;
// the modal gallery uses the full array.
const productCover = (p) =>
  (p && p.images && p.images.length > 0)
    ? p.images[0]
    : (window.fillerSrc ? window.fillerSrc(p) : '');
const productGallery = (p, n = 3) =>
  (p && p.images && p.images.length > 0)
    ? p.images
    : (window.fillerGallery ? window.fillerGallery(p, n) : []);

// Category label lookup — internal ids (cod, eft, apex, rust, pubg) match
// their display labels uppercase-for-lowercase, but routing through
// EXEC_CATEGORIES keeps any future rename in one place.
const categoryLabel = (id) => {
  const cats = window.EXEC_CATEGORIES || [];
  const c = cats.find((x) => x.id === id);
  return c ? c.label : (id || '').toUpperCase();
};

// Product state — single source of truth. window.ExecStates fetches the
// canonical map from /api/site/product-states once and re-emits when admin
// writes a new value. Components that display a status pill call
// useProductStates() so they re-render on change; one-off readers can use
// productState() directly. Default for any product without a row: 'updating'.
const STATE_LABEL = { online: 'ONLINE', offline: 'OFFLINE', updating: 'UPDATING' };
const productState = (p) =>
  (window.ExecStates ? window.ExecStates.get(p.id) : { state: 'updating', note: '' });
const useProductStates = () =>
  (window.ExecStates ? window.ExecStates.useStates() : { ready: false });

// Admin shortcuts (re-bound below so this file is order-independent).
const E = (props) => React.createElement(window.EditableText, props);
const Add = (props) => React.createElement(window.AdminAddButton, props);
const Ctl = (props) => React.createElement(window.AdminItemControls, props);

// ---------- ICONS (line, mono) ----------
const Icon = {
  Search: () => <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="11" cy="11" r="6"/><path d="m20 20-4.3-4.3"/></svg>,
  Cart: () => <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M3 5h2l2.5 11h11L21 8H6"/><circle cx="9" cy="20" r="1.2"/><circle cx="18" cy="20" r="1.2"/></svg>,
  Plus: () => <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.8"><path d="M12 5v14M5 12h14"/></svg>,
  Minus: () => <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.8"><path d="M5 12h14"/></svg>,
  X: () => <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M6 6l12 12M18 6 6 18"/></svg>,
  Chevron: () => <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M6 9l6 6 6-6"/></svg>,
  Check: () => <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 12l5 5L20 6"/></svg>,
  Arrow: () => <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M5 12h14M13 6l6 6-6 6"/></svg>,
  Lock: () => <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.6"><rect x="5" y="11" width="14" height="9" rx="1"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>,
  Hash: () => <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M5 9h14M5 15h14M10 4 8 20M16 4l-2 16"/></svg>,
  Github: () => <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.09-.65.35-1.08.63-1.33-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.33 4.68-4.56 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2Z"/></svg>,
  Telegram: () => <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M21.5 4.4 2.9 11.6c-1 .4-1 1 .1 1.3l4.7 1.5L19 6.8c.5-.3 1-.2.6.2L10.4 15l-.4 5.6c.4 0 .6-.2.8-.4l2.3-2.2 4.8 3.5c.9.5 1.5.2 1.7-.8l3.2-15c.3-1.3-.4-1.8-1.3-1.3Z"/></svg>,
  Discord: () => <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M19.6 5.3A17 17 0 0 0 15.3 4l-.2.4a14 14 0 0 1 3.9 1.8c-3.3-1.8-7.7-1.8-11 0A14 14 0 0 1 11.9 4l-.2-.4a17 17 0 0 0-4.3 1.3A18 18 0 0 0 4 16.5a17 17 0 0 0 4.5 2.3l.9-1.3a11 11 0 0 1-1.8-.9l.5-.3c3.5 1.6 7.3 1.6 10.7 0l.5.3-1.8.9.9 1.3a17 17 0 0 0 4.5-2.3 18 18 0 0 0-2.8-11.2ZM9.9 14.3c-.9 0-1.6-.8-1.6-1.8s.7-1.8 1.6-1.8 1.6.8 1.6 1.8-.7 1.8-1.6 1.8Zm4.2 0c-.9 0-1.6-.8-1.6-1.8s.7-1.8 1.6-1.8 1.6.8 1.6 1.8-.7 1.8-1.6 1.8Z"/></svg>,
};

// ---------- TERMINAL FRAME / CHROME ----------
function CornerCuts({ size = 10, color }) {
  return (
    <>
      <span className="cc tl" style={{ '--cc': size + 'px', '--cco': color }}></span>
      <span className="cc tr" style={{ '--cc': size + 'px', '--cco': color }}></span>
      <span className="cc bl" style={{ '--cc': size + 'px', '--cco': color }}></span>
      <span className="cc br" style={{ '--cc': size + 'px', '--cco': color }}></span>
    </>
  );
}

// ---------- HERO ----------
function Hero({ tweaks, onJump, hero, setHero, editing, products }) {
  const productStates = useProductStates();
  const [bootLines, setBootLines] = useState([]);
  const onlineCount = window.ExecStates
    ? window.ExecStates.counts(products || []).online
    : 0;
  const fullBoot = [
    '> auth_handshake .................. [OK]',
    '> mirror://exe.executables.sh ..... [SYNCED]',
    '> license_vault ................... [READY]',
    `> ${onlineCount} executable${onlineCount === 1 ? '' : 's'} online`,
  ];
  useEffect(() => {
    setBootLines([]);
    let i = 0;
    const t = setInterval(() => {
      setBootLines((p) => [...p, fullBoot[i]]);
      i++;
      if (i >= fullBoot.length) clearInterval(t);
    }, 280);
    return () => clearInterval(t);
  }, [onlineCount, productStates.ready]);

  return (
    <section className="hero" data-screen-label="hero">
      <div className="hero-characters" aria-hidden="true"></div>
      <div className="hero-veil" aria-hidden="true"></div>
      <div className="hero-inner">
        <div className="hero-meta">
          <span className="dot live"></span>
          <span>NODE//A7-EAST · LIVE</span>
          <span className="sep">/</span>
          <span>BUILD 26.05.14</span>
        </div>

        <h1 className={cls('hero-title', tweaks.glitchHero && 'glitch')} data-text={hero.title}>
          <E
            value={hero.title}
            onChange={(v) => setHero({ ...hero, title: v.toUpperCase() })}
            editing={editing}
            as="span"
            maxLength={14}
          />
        </h1>
        <p className="hero-sub">
          <E value={hero.sub_a} onChange={(v) => setHero({ ...hero, sub_a: v })} editing={editing} as="span"/>
          <br/>
          <E value={hero.sub_b} onChange={(v) => setHero({ ...hero, sub_b: v })} editing={editing} as="span"/>
        </p>

        <div className="hero-actions">
          <button className="btn primary" onClick={() => onJump('catalog')}>
            <span>BROWSE CATALOG</span><Icon.Arrow/>
          </button>
          <button className="btn ghost" onClick={() => onJump('faq')}>
            <span>READ FAQ</span>
          </button>
        </div>

        <div className="boot">
          {bootLines.map((l, i) => (
            <div key={i} className="boot-line">{l}</div>
          ))}
          <div className="boot-cursor">▌</div>
        </div>
      </div>

    </section>
  );
}

// ---------- SPOTLIGHT (cycles through all products) ----------
const SPOTLIGHT_INTERVAL = 4200; // ms per product

function shuffle(arr) {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function Spotlight({ products, onOpen, onAdd, isInCart }) {
  useProductStates();
  const orderRef = useRef(null);
  if (!orderRef.current) orderRef.current = shuffle(products.map((p) => p.id)).slice(0, 5);
  const order = orderRef.current;
  const touchRef = useRef({ x: 0, y: 0, swiping: false });
  const tiers = window.EXEC_TIERS || [];

  const [idx, setIdx] = useState(0);
  const [tier, setTier] = useState('month');
  const [paused, setPaused] = useState(false);
  const [progress, setProgress] = useState(0); // 0..1

  // advance + progress ticker
  useEffect(() => {
    if (paused) return;
    setProgress(0);
    const started = performance.now();
    const tick = () => {
      const elapsed = performance.now() - started;
      const p = Math.min(1, elapsed / SPOTLIGHT_INTERVAL);
      setProgress(p);
      if (p < 1) raf = requestAnimationFrame(tick);
      else setIdx((i) => (i + 1) % order.length);
    };
    let raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [idx, paused, order.length]);

  const product = products.find((p) => p.id === order[idx]);
  if (!product) return null;
  const inCart = isInCart(product.id, tier);
  const filler = productCover(product);

  const next = () => setIdx((i) => (i + 1) % order.length);
  const prev = () => setIdx((i) => (i - 1 + order.length) % order.length);

  return (
    <section
      className="spotlight"
      data-screen-label="spotlight"
      onMouseEnter={() => setPaused(true)}
      onMouseLeave={() => setPaused(false)}
      onTouchStart={(e) => {
        setPaused(true);
        const t = e.touches[0];
        touchRef.current = { x: t.clientX, y: t.clientY, swiping: false };
      }}
      onTouchMove={(e) => {
        const t = e.touches[0];
        const dx = t.clientX - touchRef.current.x;
        const dy = t.clientY - touchRef.current.y;
        if (Math.abs(dx) > 12 && Math.abs(dx) > Math.abs(dy)) touchRef.current.swiping = true;
      }}
      onTouchEnd={(e) => {
        const swiping = touchRef.current.swiping;
        const dx = e.changedTouches[0].clientX - touchRef.current.x;
        touchRef.current.swiping = false;
        setPaused(false);
        if (!swiping || order.length < 2 || Math.abs(dx) < 45) return;
        if (dx < 0) next(); else prev();
      }}
      onTouchCancel={() => {
        touchRef.current.swiping = false;
        setPaused(false);
      }}
    >
      <CornerCuts size={12}/>
      <div className="sl-label">
        <span className="dot pulse"></span>
        <span>FEATURED</span>
        <span className="sl-pos">{String(idx + 1).padStart(2, '0')}<span>/{String(order.length).padStart(2, '0')}</span></span>
      </div>
      <div className="sl-progress"><span style={{ width: (progress * 100) + '%' }}></span></div>

      <div className="sl-grid" key={product.id}>
        <div className="sl-left">
          <div className="sl-kicker"><span>{product.tag}</span><span>{categoryLabel(product.category)}</span></div>
          <div className="sl-name">{product.name}</div>
          <div className="sl-blurb">{product.blurb}</div>
          <div className="sl-actions">
            <div className="sl-prices" aria-label="Prices">
              {tiers.map((t) => (
                <button
                  type="button"
                  className={cls('sl-price', tier === t.id && 'active')}
                  key={t.id}
                  onClick={() => setTier(t.id)}
                  aria-pressed={tier === t.id}
                >
                  <span className="sl-price-label">{t.label}</span>
                  <span className="sl-amt">{fmt(priceFor(product, t.id))}</span>
                  <span className="sl-cur">{t.suffix}</span>
                </button>
              ))}
            </div>
            <button
              className="btn primary"
              onClick={() => onAdd(product, tier)}
              disabled={inCart}
            >
              <Icon.Cart/><span>{inCart ? 'IN CART' : 'ADD TO CART'}</span>
            </button>
            <button className="btn ghost sl-details-btn" onClick={() => onOpen(product.id)}>
              <span>DETAILS</span>
            </button>
          </div>
        </div>
        <div className="sl-right">
          <image-slot
            id={`spotlight-${product.id}`}
            shape="rect"
            src={filler}
            placeholder={`Drop ${product.name} hero image`}
            style={{ width: '100%', height: '100%' }}
          ></image-slot>
        </div>
      </div>

      {/* controls */}
      <div className="sl-nav">
        <button className="sl-arrow" onClick={prev} aria-label="Previous">←</button>
        <div className="sl-dots">
          {order.map((id, i) => (
            <button
              key={id}
              className={cls('sl-dot', i === idx && 'active')}
              onClick={() => setIdx(i)}
              aria-label={`Show ${id}`}
            />
          ))}
        </div>
        <button className="sl-arrow" onClick={next} aria-label="Next">→</button>
      </div>
    </section>
  );
}

// ---------- CATEGORY TABS + SEARCH ----------
function Filters({ category, setCategory, query, setQuery, count, categories }) {
  const cats = categories || window.EXEC_CATEGORIES;
  return (
    <div className="filters" data-screen-label="filters">
      <div className="filter-tabs">
        {cats.map((c) => (
          <button
            key={c.id}
            className={cls('tab', category === c.id && 'active')}
            onClick={() => setCategory(c.id)}
          >
            <span className="tab-label">{c.label}</span>
            <span className="tab-count">{c.count}</span>
          </button>
        ))}
      </div>
      <div className="filter-right">
        <div className="search">
          <Icon.Search/>
          <input
            placeholder="grep ./catalog"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
          />
          {query && <button className="search-clear" onClick={() => setQuery('')}><Icon.X/></button>}
        </div>
        <div className="result-count">{count} RESULTS</div>
      </div>
    </div>
  );
}

// ---------- PRODUCT CARD ----------
function ProductCard({ product, onOpen, onAdd, inCart, editing, onChange, onDelete }) {
  useProductStates();
  const stop = (e) => { if (editing) { e.preventDefault(); e.stopPropagation(); } };
  return (
    <article className="card" data-screen-label={`product-${product.id}`}>
      <CornerCuts size={8}/>
      {editing && <Ctl onDelete={onDelete}/>}
      <button className="card-main" onClick={(e) => editing ? stop(e) : onOpen()}>
        <div className="card-media">
          <image-slot
            id={`card-${product.id}`}
            shape="rect"
            src={productCover(product)}
            placeholder={product.tag}
            style={{ width: '100%', height: '100%' }}
          ></image-slot>
          <div className="card-tag">
            <E value={product.tag} onChange={(v) => onChange({ ...product, tag: v })} editing={editing} as="span"/>
          </div>
          <div className={cls('card-status', productState(product).state)}>{STATE_LABEL[productState(product).state]}</div>
        </div>
        <div className="card-body">
          <div className="card-name">
            <E value={product.name} onChange={(v) => onChange({ ...product, name: v.toUpperCase() })} editing={editing} as="span"/>
          </div>
          <div className="card-blurb">
            <E value={product.blurb} onChange={(v) => onChange({ ...product, blurb: v })} editing={editing} as="span" multiline/>
          </div>
          <div className="card-foot">
            <div className="card-price">
              {editing ? (
                <>$<E
                  value={String(headlinePrice(product))}
                  onChange={(v) => {
                    const n = Math.max(0, parseInt(v.replace(/[^\d]/g,''), 10) || 0);
                    onChange({ ...product, pricing: { ...product.pricing, month: n }});
                  }}
                  editing={editing}
                  as="span"
                /></>
              ) : (
                fmt(headlinePrice(product))
              )}
              <small>/mo</small>
            </div>
            <div className="card-cat">/{categoryLabel(product.category)}</div>
          </div>
        </div>
      </button>
      <div className="card-hover-cta">
        <span>{editing ? 'CLICK TEXT TO EDIT' : 'VIEW DETAILS'}</span><Icon.Arrow/>
      </div>
    </article>
  );
}

// ---------- LIGHTBOX (full-size image viewer over the product modal) ----------
const LbCloseIcon = () => (
  <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor"
       strokeWidth="1.6" strokeLinecap="round">
    <path d="M6 6l12 12M18 6 6 18"/>
  </svg>
);
const LbArrow = ({ dir }) => (
  <svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor"
       strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
    {dir === 'left'
      ? <path d="M15 5l-7 7 7 7"/>
      : <path d="M9 5l7 7-7 7"/>}
  </svg>
);
function Lightbox({ images, index, onClose, onPrev, onNext }) {
  // Capture keys before ProductModal's listener sees them, so Esc closes
  // the lightbox first (not the modal) and arrow keys page within it.
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') { e.stopPropagation(); onClose(); }
      if (e.key === 'ArrowLeft')  { e.stopPropagation(); onPrev(); }
      if (e.key === 'ArrowRight') { e.stopPropagation(); onNext(); }
    };
    document.addEventListener('keydown', onKey, true);
    return () => document.removeEventListener('keydown', onKey, true);
  }, [onClose, onPrev, onNext]);

  // Every interactive click must stop propagation — the lightbox lives
  // inside the ProductModal's .pm-scrim, which has its own click-to-close
  // listener that would otherwise close the underlying modal too.
  const stopAnd = (fn) => (e) => { e.stopPropagation(); fn(); };

  return (
    <div className="lb-scrim" onClick={stopAnd(onClose)} role="dialog" aria-modal="true">
      <div className="lb-stage">
        <img
          src={images[index]}
          alt={`Image ${index + 1} of ${images.length}`}
          onClick={(e) => e.stopPropagation()}
        />
      </div>
      <button className="lb-close" onClick={stopAnd(onClose)} aria-label="Close image viewer">
        <LbCloseIcon/>
      </button>
      {images.length > 1 && (
        <>
          <button className="lb-arrow left" onClick={stopAnd(onPrev)} aria-label="Previous image">
            <LbArrow dir="left"/>
          </button>
          <button className="lb-arrow right" onClick={stopAnd(onNext)} aria-label="Next image">
            <LbArrow dir="right"/>
          </button>
          <div className="lb-counter">
            <span>{String(index + 1).padStart(2, '0')}</span>
            <span className="lb-counter-sep">/</span>
            <span>{String(images.length).padStart(2, '0')}</span>
          </div>
        </>
      )}
    </div>
  );
}

// ---------- PRODUCT MODAL (frosted glass detail panel) ----------
function ProductModal({ product, onClose, onAdd, isInCart, onViewCart, editing, onChange }) {
  useProductStates();
  const [tier, setTier] = useState('month');
  const [imgIdx, setImgIdx] = useState(0);
  const [lightboxIdx, setLightboxIdx] = useState(null); // null when closed
  const [featuresExpanded, setFeaturesExpanded] = useState(() => window.innerWidth > 720);
  const touchRef = useRef({ x: 0, y: 0, swiping: false });

  // Reset gallery + tier + lightbox whenever the product changes.
  useEffect(() => {
    setTier('month');
    setImgIdx(0);
    setLightboxIdx(null);
    setFeaturesExpanded(window.innerWidth > 720);
  }, [product?.id]);

  // Trap escape + lock body scroll while open. When the lightbox is open
  // it owns the keyboard via a capture-phase listener, so this handler
  // never runs in that state — keeping prev/next + Esc from double-firing.
  useEffect(() => {
    if (!product) return;
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowLeft') setImgIdx((i) => (i - 1 + gallery.length) % gallery.length);
      if (e.key === 'ArrowRight') setImgIdx((i) => (i + 1) % gallery.length);
    };
    document.addEventListener('keydown', onKey);
    const scrollY = window.scrollY;
    const prev = {
      overflow: document.body.style.overflow,
      position: document.body.style.position,
      top: document.body.style.top,
      width: document.body.style.width,
    };
    document.body.style.overflow = 'hidden';
    document.body.style.position = 'fixed';
    document.body.style.top = `-${scrollY}px`;
    document.body.style.width = '100%';
    return () => {
      document.removeEventListener('keydown', onKey);
      document.body.style.overflow = prev.overflow;
      document.body.style.position = prev.position;
      document.body.style.top = prev.top;
      document.body.style.width = prev.width;
      window.scrollTo(0, scrollY);
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [product, onClose]);

  if (!product) return null;
  const gallery = productGallery(product, 3);
  const tiers = window.EXEC_TIERS || [];
  const inCart = isInCart ? isInCart(product.id, tier) : false;
  const canEdit = editing && onChange;

  const updateFeature = (i, v) => {
    const next = product.features.slice();
    next[i] = v;
    onChange({ ...product, features: next });
  };
  const addFeature = () => onChange({ ...product, features: [...product.features, 'New feature'] });
  const removeFeature = (i) => {
    const next = product.features.slice();
    next.splice(i, 1);
    onChange({ ...product, features: next });
  };

  return (
    <div className="pm-scrim" onClick={onClose} role="dialog" aria-modal="true">
      <div className="pm-grain"></div>
      <div className="pm" onClick={(e) => e.stopPropagation()} data-screen-label={`modal-${product.id}`}>
        <CornerCuts size={14}/>

        <button className="pm-close" onClick={onClose} aria-label="Close">
          <Icon.X/>
        </button>

        {/* header strip */}
        <div className="pm-strip">
          <span className="dot pulse"></span>
          <span className="pm-strip-tag">{product.tag}</span>
          <span className="pm-strip-sep">//</span>
          <span className="pm-strip-cat">{categoryLabel(product.category)}</span>
          <span className={cls('pm-strip-status', productState(product).state)}>{STATE_LABEL[productState(product).state]}</span>
          <span className="pm-strip-v">{product.version}</span>
        </div>

        <div className="pm-grid">
          {/* LEFT — gallery */}
          <div className="pm-gallery">
            <div
              className="pm-stage"
              onClick={(e) => {
                // Don't pop the lightbox when clicking the arrows / meta overlay,
                // while admin edit-mode is active (image-slot owns drops then),
                // or right after a swipe gesture just changed the image.
                if (canEdit) return;
                if (e.target.closest('.pm-stage-arrow, .pm-stage-meta')) return;
                if (touchRef.current.swiping) { touchRef.current.swiping = false; return; }
                setLightboxIdx(imgIdx);
              }}
              onTouchStart={(e) => {
                const t = e.touches[0];
                touchRef.current = { x: t.clientX, y: t.clientY, swiping: false };
              }}
              onTouchMove={(e) => {
                const t = e.touches[0];
                const dx = t.clientX - touchRef.current.x;
                const dy = t.clientY - touchRef.current.y;
                if (Math.abs(dx) > 10 && Math.abs(dx) > Math.abs(dy)) touchRef.current.swiping = true;
              }}
              onTouchEnd={(e) => {
                if (!touchRef.current.swiping || gallery.length < 2) return;
                const t = e.changedTouches[0];
                const dx = t.clientX - touchRef.current.x;
                if (Math.abs(dx) < 40) { touchRef.current.swiping = false; return; }
                if (dx < 0) setImgIdx((i) => (i + 1) % gallery.length);
                else setImgIdx((i) => (i - 1 + gallery.length) % gallery.length);
              }}
              style={{ cursor: canEdit ? 'default' : 'zoom-in' }}
            >
              <image-slot
                key={`${product.id}-${imgIdx}`}
                id={`modal-${product.id}-${imgIdx}`}
                shape="rect"
                src={gallery[imgIdx]}
                placeholder={`Drop ${product.name} image ${imgIdx + 1}`}
                style={{ width: '100%', height: '100%' }}
              ></image-slot>
              <div className="pm-stage-meta">
                <span>{String(imgIdx + 1).padStart(2, '0')}<span>/{String(gallery.length).padStart(2, '0')}</span></span>
              </div>
              {gallery.length > 1 && (
                <>
                  <button
                    className="pm-stage-arrow left"
                    onClick={() => setImgIdx((i) => (i - 1 + gallery.length) % gallery.length)}
                    aria-label="Previous image"
                  >←</button>
                  <button
                    className="pm-stage-arrow right"
                    onClick={() => setImgIdx((i) => (i + 1) % gallery.length)}
                    aria-label="Next image"
                  >→</button>
                </>
              )}
            </div>
            {gallery.length > 1 && (
              <div className="pm-thumbs">
                {gallery.map((src, i) => (
                  <button
                    key={i}
                    className={cls('pm-thumb', i === imgIdx && 'active')}
                    onClick={() => setImgIdx(i)}
                    aria-label={`View image ${i + 1}`}
                  >
                    <img src={src} alt=""/>
                    <span className="pm-thumb-n">0{i + 1}</span>
                  </button>
                ))}
              </div>
            )}
          </div>

          {/* RIGHT — details */}
          <div className="pm-detail">
            <div className="pm-headline">
              <h2 className="pm-name">
                <E
                  value={product.name}
                  onChange={(v) => canEdit && onChange({ ...product, name: v.toUpperCase() })}
                  editing={canEdit}
                  as="span"
                />
              </h2>
              <div className="pm-blurb">
                <E
                  value={product.blurb}
                  onChange={(v) => canEdit && onChange({ ...product, blurb: v })}
                  editing={canEdit}
                  as="span"
                  multiline
                />
              </div>
            </div>

            <section className={cls('pm-section', 'pm-section--features', featuresExpanded && 'is-expanded')}>
              <div className="pm-h pm-features-head">
                <span className="pm-features-label">FEATURES</span>
                <button
                  type="button"
                  className="pm-features-toggle"
                  aria-expanded={featuresExpanded}
                  aria-controls={`features-${product.id}`}
                  onClick={() => setFeaturesExpanded((expanded) => !expanded)}
                >
                  <span>{featuresExpanded ? 'RETRACT' : 'EXPAND'}</span>
                  <span className="pm-features-chevron" aria-hidden="true">⌄</span>
                </button>
              </div>
              <ul className="pm-feat" id={`features-${product.id}`}>
                {product.features.map((f, i) => (
                  <li key={i} className="pm-feat-item" style={{ position: 'relative' }}>
                    <Icon.Check/>
                    <E
                      value={f}
                      onChange={(v) => updateFeature(i, v)}
                      editing={canEdit}
                      as="span"
                    />
                    {canEdit && (
                      <button
                        type="button"
                        className="adm-ctl-b danger"
                        style={{ position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' }}
                        onClick={() => removeFeature(i)}
                        title="Remove feature"
                      >
                        <Icon.X/>
                      </button>
                    )}
                  </li>
                ))}
                {canEdit && (
                  <li style={{ listStyle: 'none', marginTop: 8 }}>
                    <Add onClick={addFeature} label="ADD FEATURE"/>
                  </li>
                )}
              </ul>
            </section>

            <section className="pm-section pm-section--tiers">
              <div className="pm-h">LICENSE · SELECT TIER</div>
              <div className="pm-tiers">
                {tiers.map((t) => {
                  const v = priceFor(product, t.id);
                  const active = tier === t.id;
                  return (
                    <button
                      key={t.id}
                      className={cls('pm-tier', active && 'active', t.recommended && 'rec')}
                      onClick={() => setTier(t.id)}
                    >
                      {t.recommended && <span className="pm-tier-rec">BEST</span>}
                      <span className="pm-tier-l">{t.label}</span>
                      <span className="pm-tier-p">
                        {canEdit ? (
                          <>$<E
                            value={String(v)}
                            onChange={(vv) => {
                              const n = Math.max(0, parseInt(vv.replace(/[^\d]/g,''), 10) || 0);
                              onChange({ ...product, pricing: { ...product.pricing, [t.id]: n }});
                            }}
                            editing={canEdit}
                            as="span"
                          /></>
                        ) : fmt(v)}
                        <small>{t.suffix}</small>
                      </span>
                      <span className="pm-tier-sub">{t.sub}</span>
                    </button>
                  );
                })}
              </div>
            </section>

            <div className="pm-buy">
              <div className="pm-price">
                <span className="pm-amt">{fmt(priceFor(product, tier))}</span>
                <span className="pm-cur">USD {tiers.find((t) => t.id === tier)?.suffix}</span>
              </div>
              <div className="pm-actions">
                {onAdd && (
                  <button
                    className="btn primary"
                    onClick={() => onAdd(product, tier)}
                    disabled={inCart}
                  >
                    <Icon.Cart/><span>{inCart ? 'IN CART' : 'ADD TO CART'}</span>
                  </button>
                )}
                {inCart && onViewCart && (
                  <button className="btn ghost pm-view-cart" onClick={onViewCart}>
                    <Icon.Cart/><span>VIEW CART</span>
                  </button>
                )}
              </div>
            </div>
          </div>
        </div>
      </div>

      {lightboxIdx !== null && gallery.length > 0 && (
        <Lightbox
          images={gallery}
          index={lightboxIdx}
          onClose={() => setLightboxIdx(null)}
          onPrev={() => setLightboxIdx((i) => (i - 1 + gallery.length) % gallery.length)}
          onNext={() => setLightboxIdx((i) => (i + 1) % gallery.length)}
        />
      )}
    </div>
  );
}

// Expose for status.jsx
window.ProductModal = ProductModal;

// ---------- AUTH (Discord login / account pill) ----------
// When logged out → button that kicks the OAuth flow.
// When logged in → link straight to the dedicated /account.html page (the
// in-modal mini-view was replaced with a full page so users can navigate
// order history properly).
function AuthButton({ user }) {
  if (!user) {
    return (
      <button
        className="nav-auth nav-auth-login"
        onClick={() => window.ExecAuth && window.ExecAuth.login()}
        title="Log in with Discord"
      >
        <Icon.Lock/><span>LOG IN</span>
      </button>
    );
  }
  const name = user.global_name || user.username || 'USER';
  return (
    <a className="nav-auth nav-auth-user" href="/account.html" title="Account">
      {user.avatar_url
        ? <img className="nav-auth-avatar" src={user.avatar_url} alt=""/>
        : <span className="nav-auth-avatar nav-auth-avatar-fallback">{(name[0] || '?').toUpperCase()}</span>}
      <span className="nav-auth-name">{name}</span>
    </a>
  );
}


// ---------- CART DRAWER ----------
function Cart({ open, onClose, items, products, onChange, onCheckout, user, pending }) {
  const lines = items.map((it) => ({
    ...it,
    product: products.find((p) => p.id === it.id),
  })).filter((l) => l.product);
  const subtotal = lines.reduce((s, l) => s + priceFor(l.product, l.tier) * l.qty, 0);
  const tax = subtotal * 0.08;
  const total = subtotal + tax;

  return (
    <>
      <div className={cls('cart-scrim', open && 'open')} onClick={onClose}></div>
      <aside className={cls('cart', open && 'open')} aria-hidden={!open}>
        <CornerCuts size={10}/>
        <header className="cart-head">
          <div className="cart-title">
            <span className="dot pulse"></span>
            <span>CART // {lines.length} ITEM{lines.length === 1 ? '' : 'S'}</span>
          </div>
          <button className="icon-btn" onClick={onClose}><Icon.X/></button>
        </header>

        <div className="cart-body">
          {lines.length === 0 ? (
            <div className="cart-empty">
              <div className="ce-art">[ ∅ ]</div>
              <div>NO EXECUTABLES QUEUED</div>
              <div className="ce-sub">browse catalog to add</div>
            </div>
          ) : lines.map((l) => {
            const tierMeta = (window.EXEC_TIERS || []).find((t) => t.id === l.tier) || { suffix: '' };
            const unit = priceFor(l.product, l.tier);
            return (
            <div key={l.id + ':' + l.tier} className="cart-line">
              <div className="cl-media">
                <image-slot id={`cart-${l.id}`} shape="rect" src={productCover(l.product)} placeholder={l.product.tag} style={{ width: '100%', height: '100%' }}></image-slot>
              </div>
              <div className="cl-body">
                <div className="cl-name">{l.product.name}</div>
                <div className="cl-tag">{l.product.tag} · {l.product.version} · <span className="cl-tier">{(l.tier || 'month').toUpperCase()}</span></div>
                <div className="cl-controls">
                  <div className="qty">
                    <button onClick={() => onChange(l.id, l.tier, l.qty - 1)}><Icon.Minus/></button>
                    <span>{l.qty}</span>
                    <button onClick={() => onChange(l.id, l.tier, l.qty + 1)}><Icon.Plus/></button>
                  </div>
                  <button className="cl-remove" onClick={() => onChange(l.id, l.tier, 0)}>REMOVE</button>
                </div>
              </div>
              <div className="cl-price">
                {fmt(unit * l.qty)}
                <small>{tierMeta.suffix}</small>
              </div>
            </div>
          );})}
        </div>

        {lines.length > 0 && (
          <footer className="cart-foot">
            <div className="cart-row"><span>SUBTOTAL</span><span>{fmt(subtotal)}</span></div>
            <div className="cart-row"><span>TAX (8%)</span><span>{fmt(tax)}</span></div>
            <div className="cart-row total"><span>TOTAL</span><span>{fmt(total)}</span></div>
            <button className="btn primary big" onClick={onCheckout} disabled={pending}>
              <Icon.Lock/>
              <span>{
                pending ? 'OPENING STRIPE…'
                  : user ? 'SECURE CHECKOUT'
                  : 'LOG IN WITH DISCORD TO CHECKOUT'
              }</span>
            </button>
            <div className="cart-pay">
              <span>STRIPE</span><span>·</span><span>CARD</span><span>·</span><span>APPLE PAY</span><span>·</span><span>GOOGLE PAY</span>
            </div>
          </footer>
        )}
      </aside>
    </>
  );
}

// ---------- PAYMENT RETURN BANNER ----------
// Shown when the user returns from Stripe with ?paid=ok or ?paid=cancel.
// Auto-dismisses after 8s on success so it doesn't linger.
function PaymentBanner({ kind, onClose }) {
  useEffect(() => {
    if (kind !== 'ok') return;
    const t = setTimeout(onClose, 8000);
    return () => clearTimeout(t);
  }, [kind, onClose]);
  if (kind === 'ok') {
    return (
      <div className="pay-banner pay-banner-ok" onClick={onClose}>
        <span className="dot pulse"></span>
        <span>PAYMENT RECEIVED — open your account to see order status &amp; keys</span>
        <button className="pay-banner-x" onClick={onClose}><Icon.X/></button>
      </div>
    );
  }
  return (
    <div className="pay-banner pay-banner-cancel" onClick={onClose}>
      <span className="dot"></span>
      <span>CHECKOUT CANCELLED — no charge was made</span>
      <button className="pay-banner-x" onClick={onClose}><Icon.X/></button>
    </div>
  );
}

// ---------- FAQ ----------
function FAQ({ faq, setFaq, editing }) {
  const [open, setOpen] = useState(0);
  const update = (i, patch) => setFaq(faq.map((f, idx) => idx === i ? { ...f, ...patch } : f));
  const remove = (i) => setFaq(faq.filter((_, idx) => idx !== i));
  const add = () => {
    setFaq([...faq, { q: 'New question?', a: 'New answer — click to edit.' }]);
    setOpen(faq.length);
  };
  return (
    <section className="faq" id="faq" data-screen-label="faq">
      <div className="sec-head">
        <div className="sec-tag">// 04</div>
        <h2 className="sec-title">FREQUENTLY ASKED</h2>
      </div>
      <div className="faq-list">
        {faq.map((f, i) => (
          <div key={i} className={cls('faq-item', open === i && 'open')}>
            {editing && <Ctl onDelete={() => remove(i)}/>}
            <button className="faq-q" onClick={(e) => {
              // when editing, clicking the editable text shouldn't toggle
              if (editing && e.target.closest('.editable')) return;
              setOpen(open === i ? -1 : i);
            }}>
              <span className="faq-n">0{i + 1}</span>
              <span className="faq-qt">
                <E value={f.q} onChange={(v) => update(i, { q: v })} editing={editing} as="span"/>
              </span>
              <span className="faq-chev"><Icon.Chevron/></span>
            </button>
            {(open === i || editing) && (
              <div className="faq-a">
                <E value={f.a} onChange={(v) => update(i, { a: v })} editing={editing} as="span" multiline/>
              </div>
            )}
          </div>
        ))}
        {editing && (
          <div style={{ marginTop: 16, display: 'flex', justifyContent: 'center' }}>
            <Add onClick={add} label="+ ADD QUESTION"/>
          </div>
        )}
      </div>
    </section>
  );
}

// ---------- FOOTER ----------
function Footer({ footer, setFooter, editing }) {
  const EditableUrl = window.EditableUrl;
  const socialIcon = {
    github: <Icon.Github/>,
    telegram: <Icon.Telegram/>,
    discord: <img src="images/discord.png" alt="discord" width="108" height="58" style={{ maxWidth: 'none' }}/>,
  };
  const updateColumn = (ci, patch) => {
    setFooter({ ...footer, columns: footer.columns.map((c, i) => i === ci ? { ...c, ...patch } : c) });
  };
  const updateLink = (ci, li, patch) => {
    const cols = footer.columns.map((c, i) => {
      if (i !== ci) return c;
      return { ...c, links: c.links.map((l, j) => j === li ? { ...l, ...patch } : l) };
    });
    setFooter({ ...footer, columns: cols });
  };
  const addLink = (ci) => {
    const cols = footer.columns.map((c, i) => i === ci ? { ...c, links: [...c.links, { label: 'New link', url: '#' }] } : c);
    setFooter({ ...footer, columns: cols });
  };
  const removeLink = (ci, li) => {
    const cols = footer.columns.map((c, i) => i === ci ? { ...c, links: c.links.filter((_, j) => j !== li) } : c);
    setFooter({ ...footer, columns: cols });
  };
  const updateSocial = (si, patch) => {
    setFooter({ ...footer, socials: footer.socials.map((s, i) => i === si ? { ...s, ...patch } : s) });
  };

  return (
    <footer className="foot" data-screen-label="footer">
      <div className="foot-grid">
        <div>
          <div className="foot-logo">[EXE]<span>CUTABLES</span></div>
          <p className="foot-tag">
            <E value={footer.tagline} onChange={(v) => setFooter({ ...footer, tagline: v })} editing={editing} as="span"/>
          </p>
          <div className="foot-socials">
            {footer.socials.map((s, i) => (
              editing ? (
                <EditableUrl
                  key={i}
                  label={s.label}
                  url={s.url}
                  onChange={({ label, url }) => updateSocial(i, { label, url })}
                  editing={editing}
                />
              ) : (
                <a key={i} href={s.url || '#'} aria-label={s.label}>
                  {socialIcon[s.kind] || <Icon.Github/>}
                </a>
              )
            ))}
            <a
              href="https://www.elitepvpers.com/"
              target="_blank"
              rel="noopener noreferrer"
              aria-label="elitepvpers"
              className="foot-social-img"
            >
              <img src="images/epvp.png" alt="elitepvpers" width="120" height="64" style={{ maxWidth: 'none' }}/>
            </a>
          </div>
        </div>
        {footer.columns.map((col, ci) => (
          <div key={ci} className="foot-col">
            <div className="foot-h">
              <E value={col.title} onChange={(v) => updateColumn(ci, { title: v.toUpperCase() })} editing={editing} as="span"/>
            </div>
            <ul>
              {col.links.map((l, li) => (
                <li key={li} style={{ position: 'relative' }}>
                  {editing ? (
                    <EditableUrl
                      label={l.label}
                      url={l.url}
                      onChange={(patch) => updateLink(ci, li, patch)}
                      editing={editing}
                    />
                  ) : (
                    <a href={l.url || '#'}>{l.label}</a>
                  )}
                  {editing && (
                    <button
                      type="button"
                      className="adm-ctl-b danger"
                      style={{ position: 'absolute', right: -2, top: '50%', transform: 'translateY(-50%)' }}
                      onClick={() => removeLink(ci, li)}
                      title="Remove link"
                    >
                      <Icon.X/>
                    </button>
                  )}
                </li>
              ))}
              {editing && (
                <li style={{ listStyle: 'none', marginTop: 6 }}>
                  <Add onClick={() => addLink(ci)} label="+ LINK"/>
                </li>
              )}
            </ul>
          </div>
        ))}
      </div>
      <div className="foot-bottom">
        <span>
          <E value={footer.copyright} onChange={(v) => setFooter({ ...footer, copyright: v })} editing={editing} as="span"/>
        </span>
        <span>
          <E value={footer.node} onChange={(v) => setFooter({ ...footer, node: v })} editing={editing} as="span"/>
        </span>
        <span>
          <E value={footer.build} onChange={(v) => setFooter({ ...footer, build: v })} editing={editing} as="span"/>
        </span>
      </div>
    </footer>
  );
}
window.Footer = Footer;

// ---------- TWEAKS ----------
function TweaksUI({ tweaks, setTweak }) {
  const TweaksPanel = window.TweaksPanel;
  const TweakSection = window.TweakSection;
  const TweakColor = window.TweakColor;
  const TweakToggle = window.TweakToggle;
  const TweakRadio = window.TweakRadio;
  const TweakSelect = window.TweakSelect;

  return (
    <TweaksPanel title="Tweaks · EXECUTABLES">
      <TweakSection label="Accent">
        <TweakColor
          label="Primary"
          value={tweaks.accentPrimary}
          options={['#00ffd1', '#39ff14', '#ff5b1f', '#7c5cff', '#00d4ff', '#ffea00']}
          onChange={(v) => setTweak('accentPrimary', v)}
        />
        <TweakColor
          label="Secondary"
          value={tweaks.accentSecondary}
          options={['#ff2bd6', '#ff0066', '#00ffd1', '#7c5cff', '#ffea00', '#ff5b1f']}
          onChange={(v) => setTweak('accentSecondary', v)}
        />
        <TweakColor
          label="Background"
          value={tweaks.bgBase}
          options={['#0a0a0f', '#000000', '#0b0e14', '#0d0612', '#0a1014']}
          onChange={(v) => setTweak('bgBase', v)}
        />
      </TweakSection>
      <TweakSection label="Layout">
        <TweakRadio
          label="Density"
          value={tweaks.cardDensity}
          options={[{ value: 'compact', label: 'COMPACT' }, { value: 'comfy', label: 'COMFY' }]}
          onChange={(v) => setTweak('cardDensity', v)}
        />
        <TweakSelect
          label="Grid columns"
          value={tweaks.gridCols}
          options={[{ value: 2, label: '2 columns' }, { value: 3, label: '3 columns' }, { value: 4, label: '4 columns' }]}
          onChange={(v) => setTweak('gridCols', Number(v))}
        />
        <TweakToggle label="Corner cuts" value={tweaks.cornerCuts} onChange={(v) => setTweak('cornerCuts', v)} />
        <TweakToggle label="Show spotlight" value={tweaks.showSpotlight} onChange={(v) => setTweak('showSpotlight', v)} />
      </TweakSection>
      <TweakSection label="Effects">
        <TweakToggle label="Scanline overlay" value={tweaks.scanlines} onChange={(v) => setTweak('scanlines', v)} />
        <TweakToggle label="Hero glitch" value={tweaks.glitchHero} onChange={(v) => setTweak('glitchHero', v)} />
        <TweakToggle label="Noise texture" value={tweaks.noiseTexture} onChange={(v) => setTweak('noiseTexture', v)} />
        <TweakToggle label="Mono everywhere" value={tweaks.monoEverything} onChange={(v) => setTweak('monoEverything', v)} />
      </TweakSection>
    </TweaksPanel>
  );
}

// ---------- ROOT APP ----------
// Subscribes to window.ExecAuth and re-renders when the logged-in user changes.
// Returns { user, isAdmin } — both null/false until /api/me resolves. Safe if
// auth.js failed to load (returns the resting null/false forever).
function useAuthState() {
  const [state, setState] = useState(() => ({
    user:    window.ExecAuth ? window.ExecAuth.getUser()  : null,
    isAdmin: window.ExecAuth ? window.ExecAuth.isAdmin() : false,
    isMod:   window.ExecAuth ? window.ExecAuth.isMod()   : false,
  }));
  useEffect(() => {
    if (!window.ExecAuth) return;
    return window.ExecAuth.subscribe((user, meta) => setState({
      user,
      isAdmin: !!(meta && meta.isAdmin),
      isMod:   !!(meta && (meta.isMod || meta.isAdmin)),
    }));
  }, []);
  return state;
}

function App() {
  const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
  const [cmsState, setKey] = window.useCMS();
  const [editing, setEditing] = window.useEditMode();
  const isAdmin = window.CMS.isAdmin();
  const { user: authUser } = useAuthState();

  const [category, setCategory] = useState('all');
  const [query, setQuery] = useState('');
  const [openProductId, setOpenProductId] = useState(null);
  const [cart, setCart] = useState([]);
  const [cartOpen, setCartOpen] = useState(false);
  const [navOpen, setNavOpen] = useState(false);
  const [scrollMenuVisible, setScrollMenuVisible] = useState(false);
  const [scrollMenuOpen, setScrollMenuOpen] = useState(false);
  const [checkoutPending, setCheckoutPending] = useState(false);
  const [paymentBanner, setPaymentBanner] = useState(null);  // 'ok' | 'cancel' | null

  // Apply CSS variables from tweaks
  useEffect(() => {
    const r = document.documentElement;
    r.style.setProperty('--accent', tweaks.accentPrimary);
    r.style.setProperty('--accent-2', tweaks.accentSecondary);
    r.style.setProperty('--bg', tweaks.bgBase);
    document.body.classList.toggle('no-scanlines', !tweaks.scanlines);
    document.body.classList.toggle('no-noise', !tweaks.noiseTexture);
    document.body.classList.toggle('no-cuts', !tweaks.cornerCuts);
    document.body.classList.toggle('mono-all', tweaks.monoEverything);
    r.style.setProperty('--grid-cols', tweaks.gridCols);
    r.style.setProperty('--density', tweaks.cardDensity === 'compact' ? '0.85' : '1');
  }, [tweaks]);

  // On desktop, replace the full navigation with a compact menu after the
  // visitor has scrolled beyond it. Mobile keeps its regular nav behavior.
  useEffect(() => {
    const syncScrollMenu = () => {
      const visible = window.innerWidth > 720 && window.scrollY > 90;
      setScrollMenuVisible(visible);
      if (!visible) setScrollMenuOpen(false);
    };
    syncScrollMenu();
    window.addEventListener('scroll', syncScrollMenu, { passive: true });
    window.addEventListener('resize', syncScrollMenu);
    return () => {
      window.removeEventListener('scroll', syncScrollMenu);
      window.removeEventListener('resize', syncScrollMenu);
    };
  }, []);

  const products = cmsState.products;
  const categories = useMemo(() => window.CMS.computeCategories(), [products]);

  const filtered = useMemo(() => {
    let arr = products;
    if (category !== 'all') arr = arr.filter((p) => p.category === category);
    if (query.trim()) {
      const q = query.trim().toLowerCase();
      arr = arr.filter((p) =>
        p.name.toLowerCase().includes(q) ||
        p.blurb.toLowerCase().includes(q) ||
        p.tag.toLowerCase().includes(q)
      );
    }
    return arr;
  }, [products, category, query]);

  const cartCount = cart.reduce((s, i) => s + i.qty, 0);
  const cartTotal = cart.reduce((s, i) => {
    const p = products.find((pp) => pp.id === i.id);
    return p ? s + priceFor(p, i.tier) * i.qty : s;
  }, 0);
  const cartTotalTaxed = cartTotal * 1.08;
  const inCart = (id, tier) =>
    tier ? cart.some((i) => i.id === id && i.tier === tier)
         : cart.some((i) => i.id === id);

  const addToCart = (p, tier = 'month') => {
    if (editing) return; // no shopping while editing
    setCart((c) => {
      const ex = c.find((i) => i.id === p.id && i.tier === tier);
      if (ex) return c.map((i) => i.id === p.id && i.tier === tier ? { ...i, qty: i.qty + 1 } : i);
      return [...c, { id: p.id, tier, qty: 1 }];
    });
    setCartOpen(true);
  };
  const changeQty = (id, tier, qty) => {
    setCart((c) => qty <= 0
      ? c.filter((i) => !(i.id === id && i.tier === tier))
      : c.map((i) => i.id === id && i.tier === tier ? { ...i, qty } : i));
  };

  const jumpTo = (k) => {
    const el = document.querySelector(k === 'catalog' ? '#catalog' : '#faq');
    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  };

  // POSTs the cart to the Worker, which creates a Stripe Checkout Session and
  // returns its hosted URL. We then hand the browser off — Stripe's page is
  // where the user actually pays.
  const beginCheckout = async () => {
    if (!authUser) { window.ExecAuth && window.ExecAuth.login(); return; }
    if (cart.length === 0 || checkoutPending) return;
    setCheckoutPending(true);
    try {
      const res = await fetch('/api/checkout', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          items: cart.map((c) => ({ product_id: c.id, tier: c.tier, qty: c.qty })),
        }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok || !data.url) {
        alert('Checkout failed: ' + (data.error || res.status));
        setCheckoutPending(false);
        return;
      }
      // Clear the cart locally before redirecting — Stripe is the source of
      // truth from here on; the order is already stubbed in our DB.
      setCart([]);
      window.location.href = data.url;
    } catch (e) {
      alert('Checkout failed: ' + (e && e.message || 'network error'));
      setCheckoutPending(false);
    }
  };

  // On page load, surface success/cancel banners from the Stripe redirect,
  // then strip the query params so refreshes don't re-show them.
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const paid = params.get('paid');
    if (paid === 'ok' || paid === 'cancel') {
      setPaymentBanner(paid);
      params.delete('paid');
      params.delete('sid');
      const qs = params.toString();
      window.history.replaceState({}, '', window.location.pathname + (qs ? '?' + qs : ''));
    }
  }, []);

  // ---- CMS mutations ----
  const updateProduct = (next) => {
    setKey('products', products.map((p) => p.id === next.id ? next : p));
  };
  const deleteProduct = (id) => {
    if (!confirm('Delete this product?')) return;
    setKey('products', products.filter((p) => p.id !== id));
  };
  const addProduct = () => {
    const p = window.CMS.blankProduct();
    setKey('products', [...products, p]);
    setOpenProductId(p.id);
  };
  const setFaq    = (faq)    => setKey('faq', faq);
  const setFooter = (footer) => setKey('footer', footer);
  const setHero   = (hero)   => setKey('hero', hero);

  return (
    <div className="root">
      {/* ADMIN BAR */}
      {isAdmin && window.AdminBar && (
        <window.AdminBar editing={editing} setEditing={setEditing}/>
      )}

      {/* TOP NAV */}
      <header className="nav">
        <div className="nav-logo">
          <span className="nl-b">[EXE]</span><span className="nl-r">cutables</span>
          <span className="nl-v">v26.5</span>
        </div>
        <nav id="primary-navigation" className={cls('nav-links', navOpen && 'is-open')}>
          <a href="#catalog" onClick={() => setNavOpen(false)}>CATALOG</a>
          <a href="#faq" onClick={() => setNavOpen(false)}>FAQ</a>
          <a href="status.html" className="nav-status" onClick={() => setNavOpen(false)}><span className="dot live"></span>STATUS</a>
          <a href="https://discord.gg/TdKjc7qYRT" target="_blank" rel="noopener noreferrer" onClick={() => setNavOpen(false)}>DISCORD</a>
          {/* Admin login entry disabled — CMS edits live in the visitor's own localStorage and never reach the server,
              so the lock icon was misleading visitors into thinking they could change the public site. login.html and
              js/components/admin-controls.jsx are intentionally kept on disk for when a real backend is wired up; to
              re-enable, uncomment the line below.
          {!isAdmin && <a href="login.html" className="nav-status" title="Admin"><Icon.Lock/></a>}
          */}
        </nav>
        <div className="nav-right">
          <button
            className="nav-mobile-toggle"
            type="button"
            aria-label={navOpen ? 'Close navigation menu' : 'Open navigation menu'}
            aria-expanded={navOpen}
            aria-controls="primary-navigation"
            onClick={() => setNavOpen(!navOpen)}
          >
            {navOpen
              ? <Icon.X/>
              : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg>}
          </button>
          <AuthButton user={authUser}/>
          <button className="nav-cart" onClick={() => setCartOpen(true)}>
            <Icon.Cart/>
            <span>CART</span>
            {cartCount > 0 && <span className="nav-cart-badge">{cartCount}</span>}
          </button>
        </div>
      </header>

      {scrollMenuVisible && (
        <div className={cls('scroll-menu', scrollMenuOpen && 'is-open')}>
          <button
            type="button"
            className="scroll-menu-toggle"
            aria-label={scrollMenuOpen ? 'Close quick menu' : 'Open quick menu'}
            aria-expanded={scrollMenuOpen}
            aria-controls="scroll-quick-menu"
            onClick={() => setScrollMenuOpen((open) => !open)}
          >
            {scrollMenuOpen ? <Icon.X/> : <Icon.Chevron/>}
            <span>MENU</span>
          </button>
          <nav className="scroll-menu-links" id="scroll-quick-menu" aria-label="Quick navigation">
            <a href="#catalog" onClick={() => setScrollMenuOpen(false)}>CATALOG</a>
            <a href="#faq" onClick={() => setScrollMenuOpen(false)}>FAQ</a>
            <a href="status.html" onClick={() => setScrollMenuOpen(false)}>STATUS</a>
            <a href="https://discord.gg/TdKjc7qYRT" target="_blank" rel="noopener noreferrer">DISCORD</a>
            <AuthButton user={authUser}/>
            <button
              type="button"
              className="scroll-menu-cart"
              onClick={() => {
                setCartOpen(true);
                setScrollMenuOpen(false);
              }}
            >
              <Icon.Cart/><span>CART</span>
              {cartCount > 0 && <span className="scroll-menu-cart-count">{cartCount}</span>}
            </button>
          </nav>
        </div>
      )}

      <main>
        <Hero tweaks={tweaks} onJump={jumpTo} hero={cmsState.hero} setHero={setHero} editing={editing} products={products}/>

        {tweaks.showSpotlight && !editing && products.length > 0 && (
          <Spotlight
            products={products}
            onOpen={(id) => setOpenProductId(id)}
            onAdd={addToCart}
            isInCart={inCart}
          />
        )}

        <section className="catalog" id="catalog" data-screen-label="catalog">
          <div className="sec-head">
            <div className="sec-tag">// 02</div>
            <h2 className="sec-title">CATALOG <span className="muted">/ {filtered.length}</span></h2>
            <div className="sec-side">SORT BY: <button>NEW</button> · <button>PRICE</button> · <button>NAME</button></div>
          </div>

          <Filters
            category={category}
            setCategory={setCategory}
            query={query}
            setQuery={setQuery}
            count={filtered.length}
            categories={categories}
          />

          <div className="grid">
            {(() => {
              // For single-category views, prepend a decorative banner tile so
              // the products visually shift right by one. ALL has no banner
              // image and falls through to the normal product grid.
              const activeCat = category !== 'all' ? categories.find((c) => c.id === category) : null;
              return activeCat && activeCat.image ? (
                <div key={`banner-${activeCat.id}`} className="grid-item">
                  <div className="category-banner" aria-label={`${activeCat.label} category`}>
                    {/* .category-banner-frame is an inline-block wrapper that shrink-wraps
                        to the image's rendered size so the hover beam traces the image
                        edges (not the slot, which letterboxes when ratios don't match). */}
                    <span className="category-banner-frame">
                      <img src={activeCat.image} alt={`${activeCat.label} category`}/>
                      {/* Second img is a difference-blended ghost layer driven by CSS
                          animations (drift + slice tears) to produce the glitch effect. */}
                      <img
                        className="cb-ghost"
                        src={activeCat.image}
                        alt=""
                        aria-hidden="true"
                      />
                    </span>
                  </div>
                </div>
              ) : null;
            })()}
            {filtered.map((p) => (
              <div key={p.id} id={'card-' + p.id} className="grid-item">
                <ProductCard
                  product={p}
                  onOpen={() => setOpenProductId(p.id)}
                  onAdd={addToCart}
                  inCart={inCart(p.id)}
                  editing={editing}
                  onChange={updateProduct}
                  onDelete={() => deleteProduct(p.id)}
                />
              </div>
            ))}
            {editing && (
              <div className="grid-item">
                <Add onClick={addProduct} label="ADD PRODUCT" className="tile"/>
              </div>
            )}
            {filtered.length === 0 && !editing && (
              <div className="empty">
                <div className="empty-art">[ ∅ ]</div>
                <div>NO MATCHES</div>
                <div className="empty-sub">try a different filter or query</div>
              </div>
            )}
          </div>
        </section>

        <FAQ faq={cmsState.faq} setFaq={setFaq} editing={editing}/>
      </main>

      <Footer footer={cmsState.footer} setFooter={setFooter} editing={editing}/>

      <Cart
        open={cartOpen}
        onClose={() => setCartOpen(false)}
        items={cart}
        products={products}
        onChange={changeQty}
        user={authUser}
        pending={checkoutPending}
        onCheckout={() => beginCheckout()}
      />
      {paymentBanner && (
        <PaymentBanner kind={paymentBanner} onClose={() => setPaymentBanner(null)}/>
      )}

      <ProductModal
        product={products.find((p) => p.id === openProductId) || null}
        onClose={() => setOpenProductId(null)}
        onAdd={addToCart}
        isInCart={inCart}
        onViewCart={() => setCartOpen(true)}
        editing={editing}
        onChange={updateProduct}
      />

      {!editing && <TweaksUI tweaks={tweaks} setTweak={setTweak}/>}
    </div>
  );
}

// Only mount when there's an #app element (index.html).
// status.html loads this file to get ProductModal/Footer/Icon helpers,
// but mounts its own React root.
const __mountEl = document.getElementById('app');
if (__mountEl) {
  ReactDOM.createRoot(__mountEl).render(<App/>);
}

// Expose helpers status.jsx + account.jsx + admin.jsx need
window.Icon = Icon;
window.cls = cls;
window.fmt = fmt;
window.priceFor = priceFor;
window.headlinePrice = headlinePrice;
window.CornerCuts = CornerCuts;
window.categoryLabel = categoryLabel;
window.useAuthState = useAuthState;
window.AuthButton = AuthButton;
