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

// Tweak presets
const ACCENTS = [
  { id: 'gold', value: '#c8a667', label: 'Gold' },
  { id: 'emerald', value: '#7bb897', label: 'Emerald' },
  { id: 'coral', value: '#e08969', label: 'Coral' },
  { id: 'violet', value: '#9d8ccc', label: 'Violet' },
  { id: 'cobalt', value: '#6d90d9', label: 'Cobalt' },
];

const FONT_PAIRS = {
  editorial: {
    display: "'Instrument Serif', 'Fraunces', Georgia, serif",
    sans: "'Geist', system-ui, sans-serif",
    mono: "'Geist Mono', ui-monospace, monospace",
  },
  grotesk: {
    display: "'Space Grotesk', system-ui, sans-serif",
    sans: "'Space Grotesk', system-ui, sans-serif",
    mono: "'JetBrains Mono', ui-monospace, monospace",
  },
  modernist: {
    display: "'Fraunces', 'Instrument Serif', serif",
    sans: "'Geist', system-ui, sans-serif",
    mono: "'JetBrains Mono', ui-monospace, monospace",
  },
};

const MECHANICS = [
  { id: 'carousel', label: 'Carousel' },
  { id: 'stack',    label: 'Stack' },
  { id: 'deck',     label: 'Deck' },
];

const DEFAULT_TWEAKS = {
  accent: '#c8a667',
  mechanic: 'carousel',
  density: 'spacious',
  fontPair: 'editorial',
  ambientBg: true,
};

function loadTweakDefaults() {
  try {
    const el = document.getElementById('tweak-defaults');
    if (!el) return DEFAULT_TWEAKS;
    const raw = el.textContent.replace(/\/\*EDITMODE-[^*]+\*\//g, '');
    return { ...DEFAULT_TWEAKS, ...JSON.parse(raw) };
  } catch {
    return DEFAULT_TWEAKS;
  }
}

function App() {
  const defaults = loadTweakDefaults();

  // Live location + reverse-geocoded place name. Hooks live in
  // backgrounds.jsx so the ambient backgrounds and the hero card
  // share the same cached fix and the same Nominatim lookup.
  const userLoc = (typeof window.useGeolocation === 'function') ? window.useGeolocation() : null;
  const userPlace = (typeof window.usePlaceName === 'function') ? window.usePlaceName(userLoc || { lat: 0, lon: 0, label: null }) : null;

  // Always open on the first card (the hero). Each visit is a fresh
  // intro — restoring the last-viewed card from localStorage was
  // jarring for return visitors landing mid-carousel without context.
  const [idx, setIdx] = useState(0);
  const [flipped, setFlipped] = useState(new Set());
  const [theme, setTheme] = useState(() => localStorage.getItem('jv_theme') || 'light');
  const [accent, setAccent] = useState(defaults.accent);
  const [mechanic, setMechanic] = useState(defaults.mechanic);
  const [density, setDensity] = useState(defaults.density);
  const [fontPair, setFontPair] = useState(defaults.fontPair);
  const [ambientBg, setAmbientBg] = useState(defaults.ambientBg !== false);
  const [tweaksOpen, setTweaksOpen] = useState(false);
  const [editMode, setEditMode] = useState(false);
  const [autoplay, setAutoplay] = useState(false);
  const [drag, setDrag] = useState({ active: false, dx: 0, startX: 0 });
  const [hovered, setHovered] = useState(false);
  const [viewportW, setViewportW] = useState(() => typeof window !== 'undefined' ? window.innerWidth : 1024);
  const [toast, setToast] = useState(null);
  const toastTimerRef = useRef(0);
  const showToast = useCallback((msg) => {
    setToast(msg);
    if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
    toastTimerRef.current = setTimeout(() => setToast(null), 2400);
  }, []);

  // Email copy helper exposed on window so renderers (in cards.jsx)
  // can call it directly from button onClicks. Belt-and-braces with the
  // anchor interceptor below.
  useEffect(() => {
    const copy = (email) => {
      const finish = (msg) => showToast(msg);
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(email).then(
          () => finish('Email copied — ' + email),
          () => finish(email)
        );
      } else {
        try {
          const ta = document.createElement('textarea');
          ta.value = email;
          ta.style.position = 'fixed';
          ta.style.left = '-9999px';
          document.body.appendChild(ta);
          ta.select();
          document.execCommand('copy');
          document.body.removeChild(ta);
          finish('Email copied — ' + email);
        } catch {
          finish(email);
        }
      }
    };
    window.__copyEmail = copy;
    return () => { try { delete window.__copyEmail; } catch {} };
  }, [showToast]);

  // Defense-in-depth: catch any anchor that still slips through with a
  // non-http(s) scheme (mailto:, tel:, sms:, custom protocols) and copy
  // its target to the clipboard instead of letting the OS try to open
  // it. iOS without the right handler pops "Can't open this page".
  useEffect(() => {
    const onAnchorClick = (e) => {
      const a = e.target && e.target.closest && e.target.closest('a[href]');
      if (!a) return;
      const href = (a.getAttribute('href') || '').trim();
      // http/https/anchor links go through normally
      if (!href || href.startsWith('http') || href.startsWith('/') || href.startsWith('#')) return;
      // Special-case mailto so we surface the email; everything else
      // just gets the raw href copied.
      const isMailto = /^mailto:/i.test(href);
      const value = isMailto ? href.replace(/^mailto:/i, '').split('?')[0] : href;
      e.preventDefault();
      e.stopPropagation();
      if (window.__copyEmail) window.__copyEmail(value);
      else showToast(value);
    };
    document.addEventListener('click', onAnchorClick, true);
    return () => document.removeEventListener('click', onAnchorClick, true);
  }, [showToast]);

  // Field Notes: load gallery manifest + lightbox state
  const [fieldNotes, setFieldNotes] = useState([]);
  const [lightbox, setLightbox] = useState({ open: false, images: [], index: 0 });
  useEffect(() => {
    fetch('images/gallery/manifest.json')
      .then(r => r.ok ? r.json() : null)
      .then(async data => {
        if (!data || !Array.isArray(data.images)) return;
        // For each entry, prefer the local file; if it 404s or fails to load,
        // fall back to the placeholder URL. This pre-resolves URLs so plain
        // CSS background-image works without error handling in every cell.
        const probe = (url) => new Promise(resolve => {
          if (!url) return resolve(false);
          const img = new Image();
          img.onload = () => resolve(true);
          img.onerror = () => resolve(false);
          img.src = url;
        });
        const resolved = await Promise.all(data.images.map(async img => {
          const localUrl = 'images/gallery/' + encodeURI(img.src);
          const localOk = await probe(localUrl);
          return {
            ...img,
            url: localOk ? localUrl : (img._placeholder || localUrl),
            fallback: img._placeholder || null,
          };
        }));
        setFieldNotes(resolved);
      })
      .catch(() => {});
  }, []);
  const openLightbox = useCallback((images, startIndex) => {
    setLightbox({ open: true, images, index: startIndex });
  }, []);
  const closeLightbox = useCallback(() => setLightbox(lb => ({ ...lb, open: false })), []);
  useEffect(() => {
    let raf = 0;
    const onResize = () => {
      if (raf) return;
      raf = requestAnimationFrame(() => {
        raf = 0;
        setViewportW(window.innerWidth);
      });
    };
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventListener('resize', onResize);
      if (raf) cancelAnimationFrame(raf);
    };
  }, []);

  const total = CARDS.length;

  // Persist (theme only — see useState(0) above for why card index isn't persisted)
  useEffect(() => { localStorage.setItem('jv_theme', theme); document.documentElement.dataset.theme = theme; document.body.dataset.theme = theme; }, [theme]);

  // Apply tweaks to CSS vars
  useEffect(() => {
    document.documentElement.style.setProperty('--accent', accent);
    const pair = FONT_PAIRS[fontPair] || FONT_PAIRS.editorial;
    document.documentElement.style.setProperty('--font-display', pair.display);
    document.documentElement.style.setProperty('--font-sans', pair.sans);
    document.documentElement.style.setProperty('--font-mono', pair.mono);
    document.documentElement.style.setProperty('--density', density === 'compact' ? '0.85' : '1');
  }, [accent, fontPair, density]);

  // Nav
  const go = useCallback((delta) => {
    setFlipped(new Set());
    setIdx((i) => (i + delta + total) % total);
  }, [total]);

  const goTo = useCallback((n) => {
    setFlipped(new Set());
    setIdx(n);
  }, []);

  const toggleFlip = useCallback((i) => {
    setFlipped((prev) => {
      const n = new Set(prev);
      if (n.has(i)) n.delete(i); else n.add(i);
      return n;
    });
  }, []);

  // Keyboard
  useEffect(() => {
    const onKey = (e) => {
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
      if (e.key === 'ArrowLeft') { go(-1); setAutoplay(false); }
      else if (e.key === 'ArrowRight') { go(1); setAutoplay(false); }
      else if (e.key === ' ') { e.preventDefault(); setAutoplay(a => !a); }
      else if (e.key === 'Enter') { toggleFlip(idx); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [go, toggleFlip, idx]);

  // Scroll wheel navigation
  useEffect(() => {
    let cooldown = false;
    let accum = 0;
    const onWheel = (e) => {
      // If the wheel is over a scrollable back region that actually needs scrolling, let it scroll
      const scrollHost = e.target && e.target.closest && e.target.closest('.card-scroll');
      if (scrollHost && scrollHost.scrollHeight > scrollHost.clientHeight + 2) {
        return; // let native scroll happen, don't advance carousel
      }
      // prefer horizontal if present (trackpads), otherwise use vertical
      const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
      if (Math.abs(delta) < 4) return;
      e.preventDefault();
      if (cooldown) return;
      accum += delta;
      if (Math.abs(accum) > 30) {
        go(accum > 0 ? 1 : -1);
        setAutoplay(false);
        accum = 0;
        cooldown = true;
        setTimeout(() => { cooldown = false; }, 320);
      }
    };
    window.addEventListener('wheel', onWheel, { passive: false });
    return () => window.removeEventListener('wheel', onWheel);
  }, [go]);

  // Auto-advance
  useEffect(() => {
    if (!autoplay || hovered || flipped.has(idx) || editMode) return;
    const t = setTimeout(() => go(1), 6500);
    return () => clearTimeout(t);
  }, [autoplay, hovered, idx, flipped, editMode, go]);

  // Edit-mode protocol
  useEffect(() => {
    const onMsg = (e) => {
      if (!e.data || !e.data.type) return;
      if (e.data.type === '__activate_edit_mode') { setEditMode(true); setTweaksOpen(true); }
      if (e.data.type === '__deactivate_edit_mode') { setEditMode(false); setTweaksOpen(false); }
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const persistTweak = (edits) => {
    window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*');
  };

  // Drag — keep the live x in a ref + flush to state at most once per
  // animation frame. Avoids re-rendering every card on every pointermove
  // (a dozen+ React reconciliations per second is noticeable on phones).
  const dragRef = useRef(null);
  const dragRafRef = useRef(0);
  const dragLatestRef = useRef(0);
  // Tracks whether the most recent pointer interaction had real movement.
  // iOS / mobile browsers fire a synthetic click after pointer-up that can
  // land on whatever element is under the finger at release — sometimes
  // a mailto: row or external link, which then triggers
  // "Can't open this page" if the URL scheme has no handler. We swallow
  // any click that fires within ~250ms of a drag with movement.
  const dragMovedRef = useRef(false);
  const dragEndedAtRef = useRef(0);
  useEffect(() => {
    const onClickCapture = (e) => {
      if (dragMovedRef.current && (Date.now() - dragEndedAtRef.current) < 250) {
        e.preventDefault();
        e.stopPropagation();
        dragMovedRef.current = false;
      }
    };
    document.addEventListener('click', onClickCapture, true);
    return () => document.removeEventListener('click', onClickCapture, true);
  }, []);
  const onPointerDown = (e) => {
    if (flipped.has(idx)) return;
    if (e.target.closest && e.target.closest('.flip-hint')) return;
    if (e.target.closest && e.target.closest('a')) return;
    e.currentTarget.setPointerCapture?.(e.pointerId);
    dragMovedRef.current = false;
    setDrag({ active: true, dx: 0, startX: e.clientX });
    setAutoplay(false);
  };
  const onPointerMove = (e) => {
    if (!drag.active) return;
    dragLatestRef.current = e.clientX;
    if (Math.abs(dragLatestRef.current - drag.startX) > 8) {
      dragMovedRef.current = true;
    }
    if (dragRafRef.current) return;
    dragRafRef.current = requestAnimationFrame(() => {
      dragRafRef.current = 0;
      setDrag((d) => d.active ? { ...d, dx: dragLatestRef.current - d.startX } : d);
    });
  };
  const onPointerUp = (e) => {
    if (e.currentTarget?.releasePointerCapture && e.pointerId != null) {
      try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
    }
    if (dragRafRef.current) {
      cancelAnimationFrame(dragRafRef.current);
      dragRafRef.current = 0;
    }
    if (!drag.active) return;
    // Pull final dx from the ref in case the rAF didn't get to flush
    const finalDx = dragLatestRef.current ? dragLatestRef.current - drag.startX : drag.dx;
    if (Math.abs(finalDx) > 8) dragMovedRef.current = true;
    dragEndedAtRef.current = Date.now();
    const threshold = 80;
    if (finalDx > threshold) go(-1);
    else if (finalDx < -threshold) go(1);
    setDrag({ active: false, dx: 0, startX: 0 });
  };

  // Card positions for carousel
  const cardPos = (i, isFlipped = false) => {
    let rel = i - idx;
    // wrap around
    if (rel > total / 2) rel -= total;
    if (rel < -total / 2) rel += total;

    const flipRot = isFlipped ? 180 : 0;
    // All visibly rendered cards stay clickable so a tap on an adjacent
    // card brings it to centre. Far-out cards (beyond the visible range)
    // get pointer-events: none to keep stray hits cheap.
    const isVisible = Math.abs(rel) <= 2;
    const pe = isVisible ? 'auto' : 'none';

    if (mechanic === 'carousel') {
      const isNarrow = viewportW < 640;
      const offset = rel * (isNarrow ? 300 : 460);
      const rotY = rel * (isNarrow ? -10 : -18) + flipRot;
      const scale = rel === 0 ? 1 : 0.82;
      const z = rel === 0 ? 10 : Math.max(0, 5 - Math.abs(rel));
      const opacity = Math.abs(rel) > 2 ? 0 : (rel === 0 ? 1 : 0.5);
      const tx = rel === 0 ? drag.dx : offset + drag.dx * 0.3;
      return {
        transform: `translate3d(${tx}px, 0, 0) rotateY(${rotY}deg) scale(${scale})`,
        opacity, zIndex: z,
        pointerEvents: pe,
      };
    }
    if (mechanic === 'stack') {
      const offset = rel * 40;
      const scale = rel === 0 ? 1 : Math.max(0.7, 1 - Math.abs(rel) * 0.08);
      const z = 10 - Math.abs(rel);
      const opacity = Math.abs(rel) > 3 ? 0 : 1 - Math.abs(rel) * 0.22;
      const tx = rel === 0 ? drag.dx : offset * 6;
      return {
        transform: `translate3d(${tx}px, ${Math.abs(rel) * -12}px, 0) scale(${scale}) rotateY(${flipRot}deg)`,
        opacity, zIndex: z,
        pointerEvents: pe,
      };
    }
    // deck — like a fanned poker hand
    const rot = rel * 6;
    const tx = rel * 30 + (rel === 0 ? drag.dx : 0);
    const ty = Math.abs(rel) * 14;
    const z = 10 - Math.abs(rel);
    const opacity = Math.abs(rel) > 4 ? 0 : 1 - Math.abs(rel) * 0.15;
    return {
      transform: `translate3d(${tx}px, ${ty}px, 0) rotate(${rot}deg) rotateY(${flipRot}deg)`,
      opacity, zIndex: z,
      pointerEvents: pe,
    };
  };

  const progress = ((idx + 1) / total) * 100;

  return (
    <>
      {/* Ambient location-aware background */}
      <AmbientBackground theme={theme} enabled={ambientBg} />

      {/* Topbar */}
      <div className="topbar">
        <div className="mark">
          <div className="mark-dot"></div>
          <span>James Varga</span>
        </div>
        <div className="topbar-right">
          <span style={{opacity: 0.7}}>Interactive card · v1.4</span>
          <button className="theme-toggle" onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
            {theme === 'dark' ? 'Light' : 'Dark'}
          </button>
        </div>
      </div>

      {/* Stage */}
      <div
        className="stage"
        data-mech={mechanic}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
      >
        {CARDS.map((card, i) => {
          const isActive = i === idx;
          const isFlipped = flipped.has(i);
          const style = cardPos(i, isFlipped);
          // Adjacent (with wrap-around)
          let rel = i - idx;
          if (rel > total / 2) rel -= total;
          if (rel < -total / 2) rel += total;
          const isPrev = rel === -1;
          const isNext = rel === 1;
          // Inject loaded field-notes into the gallery card
          const enrichedCard = (card.kind === 'gallery' && fieldNotes.length)
            ? { ...card, fieldNotes }
            : card;
          return (
            <div
              key={card.id}
              className={
                "card-shell" +
                (isFlipped ? " flipped" : "") +
                (drag.active && isActive ? " dragging" : "") +
                (isPrev ? " adj-prev" : "") +
                (isNext ? " adj-next" : "")
              }
              inert={!isActive ? "" : undefined}
              aria-hidden={!isActive}
              style={{
                position: 'absolute',
                ...style,
                transition: drag.active && isActive ? 'none' : 'transform 0.42s cubic-bezier(.22,1,.36,1), opacity 0.32s ease',
                willChange: drag.active && isActive ? 'transform' : undefined,
              }}
              onPointerDown={isActive ? onPointerDown : undefined}
              onPointerMove={isActive ? onPointerMove : undefined}
              onPointerUp={isActive ? onPointerUp : undefined}
              onPointerCancel={isActive ? onPointerUp : undefined}
              onClick={(e) => {
                if (!isActive) { goTo(i); return; }
                if (Math.abs(drag.dx) > 8) return;
                if (e.target.closest && e.target.closest('.flip-hint')) return;
                if (e.target.closest && e.target.closest('.gallery-cell')) return;
                if (e.target.closest && e.target.closest('.contact-row')) return;
                if (card.kind === 'hero') return;
                if (card.back) toggleFlip(i);
              }}
            >
              <Card card={enrichedCard} num={i+1} total={total} flipped={isFlipped} onFlip={() => toggleFlip(i)} isHero={card.kind === 'hero'} onOpenLightbox={openLightbox} userLoc={userLoc} userPlace={userPlace} />
            </div>
          );
        })}
      </div>

      {/* Controls */}
      <div className="controls">
        <div className="dots" role="tablist" aria-label="Card navigation">
          {(() => {
            const SECTION_BY_PREFIX = { '#': 'me', '@': 'companies', '/': 'projects' };
            const SECTION_NAMES     = { me: 'Me', companies: 'Companies', projects: 'Projects' };
            return CARDS.flatMap((c, i) => {
              const section     = SECTION_BY_PREFIX[c.prefix];
              const sectionName = section ? SECTION_NAMES[section] : null;
              const prevSection = i > 0 ? SECTION_BY_PREFIX[CARDS[i-1].prefix] : undefined;
              const needsDivider = i > 0 && section !== prevSection;
              const els = [];
              if (needsDivider) {
                els.push(
                  <div
                    key={`div-${i}`}
                    className="section-divider"
                    data-section={section || undefined}
                    title={sectionName || undefined}
                    aria-label={sectionName || undefined}
                  >
                    <span className="section-divider-glyph">{c.prefix}</span>
                    <span className="section-divider-line" />
                  </div>
                );
              }
              els.push(
                <button
                  key={c.id}
                  className={"dot" + (i === idx ? " active" : "")}
                  data-section={section || undefined}
                  onClick={() => { goTo(i); setAutoplay(false); }}
                  aria-label={sectionName ? `${sectionName} — card ${i+1}` : `Go to card ${i+1}`}
                  title={sectionName || undefined}
                  aria-current={i === idx ? 'true' : undefined}
                />
              );
              return els;
            });
          })()}
        </div>
        <div className="nav">
          <button className="nav-btn" onClick={() => { go(-1); setAutoplay(false); }} aria-label="Previous">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M15 18l-6-6 6-6"/></svg>
          </button>
          <button
            className="nav-btn"
            onClick={() => setAutoplay(a => !a)}
            aria-label={autoplay ? 'Pause' : 'Play'}
            style={{position: 'relative'}}
          >
            {autoplay ? (
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>
            ) : (
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M6 4l14 8-14 8V4z"/></svg>
            )}
          </button>
          <div className="nav-count"><b>{String(idx+1).padStart(2,'0')}</b> / {String(total).padStart(2,'0')}</div>
          <button className="nav-btn" onClick={() => { go(1); setAutoplay(false); }} aria-label="Next">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M9 18l6-6-6-6"/></svg>
          </button>
        </div>
      </div>

      {/* Tweaks panel (only when edit mode) */}
      {editMode && tweaksOpen && (
        <div className="tweaks-panel">
          <div className="tweaks-title">
            <span><b>Tweaks</b></span>
            <button className="tweaks-close" onClick={() => setTweaksOpen(false)}>×</button>
          </div>

          <div className="tweak-group">
            <div className="tweak-label">Accent</div>
            <div className="swatches">
              {ACCENTS.map(a => (
                <div
                  key={a.id}
                  className={"swatch" + (accent === a.value ? ' active' : '')}
                  style={{background: a.value}}
                  onClick={() => { setAccent(a.value); persistTweak({ accent: a.value }); }}
                  title={a.label}
                />
              ))}
            </div>
          </div>

          <div className="tweak-group">
            <div className="tweak-label">Mechanic</div>
            <div className="seg">
              {MECHANICS.map(m => (
                <button
                  key={m.id}
                  className={mechanic === m.id ? 'active' : ''}
                  onClick={() => { setMechanic(m.id); persistTweak({ mechanic: m.id }); }}
                >{m.label}</button>
              ))}
            </div>
          </div>

          <div className="tweak-group">
            <div className="tweak-label">Density</div>
            <div className="seg">
              {['compact', 'spacious'].map(d => (
                <button
                  key={d}
                  className={density === d ? 'active' : ''}
                  onClick={() => { setDensity(d); persistTweak({ density: d }); }}
                >{d}</button>
              ))}
            </div>
          </div>

          <div className="tweak-group">
            <div className="tweak-label">Type pairing</div>
            <div className="seg">
              {['editorial', 'grotesk', 'modernist'].map(p => (
                <button
                  key={p}
                  className={fontPair === p ? 'active' : ''}
                  onClick={() => { setFontPair(p); persistTweak({ fontPair: p }); }}
                >{p}</button>
              ))}
            </div>
          </div>

          <div className="tweak-group">
            <div className="tweak-label">Ambient background</div>
            <div className="seg">
              {[['on', true], ['off', false]].map(([lbl, val]) => (
                <button
                  key={lbl}
                  className={ambientBg === val ? 'active' : ''}
                  onClick={() => { setAmbientBg(val); persistTweak({ ambientBg: val }); }}
                >{lbl}</button>
              ))}
            </div>
          </div>
        </div>
      )}
      {editMode && !tweaksOpen && (
        <button className="tweaks-toggle" onClick={() => setTweaksOpen(true)} title="Open Tweaks">
          <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="3"/><path d="M12 1v6M12 17v6M4.2 4.2l4.3 4.3M15.5 15.5l4.3 4.3M1 12h6M17 12h6M4.2 19.8l4.3-4.3M15.5 8.5l4.3-4.3"/></svg>
        </button>
      )}

      {toast && (
        <div className="toast" role="status" aria-live="polite">{toast}</div>
      )}

      {lightbox.open && (
        <Lightbox
          images={lightbox.images}
          index={lightbox.index}
          onIndex={(i) => setLightbox(lb => ({ ...lb, index: i }))}
          onClose={closeLightbox}
        />
      )}
    </>
  );
}

window.loadCards().then(() => {
  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
}).catch(err => {
  console.error('Failed to load cards:', err);
  document.getElementById('root').textContent = 'Failed to load cards. Check the console.';
});
