// Dedicated Emoji Maker landing page. Routed via `/emoji-maker`.
// Hero + embedded full-featured emoji editor + platform guide + how-it-works + FAQ.
//
// The modal tool at tools/slack-emoji.jsx is left untouched — this page is a
// richer standalone experience with text overlay, saturation, wave/grow
// animations, background color, and batch mode.

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

  const ACCENT      = '#7c3aed';
  const ACCENT_SOFT = '#ede9fe';
  const ACCENT_DEEP = '#6d28d9';

  // ---------------------------------------------------------------------------
  // Main page shell
  // ---------------------------------------------------------------------------
  function EmojiMakerPage({ theme }) {
    useEffect(() => {
      if (theme) document.documentElement.dataset.theme = theme;
    }, [theme]);

    const scrollToTool = () => {
      document.getElementById('em-tool')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
    };
    const scrollToHow = () => {
      document.getElementById('em-how')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
    };

    return (
      <main className="container em-page">
        <PageStyles />

        {/* ----- HERO ----- */}
        <section className="em-hero">
          <div className="em-hero-deco" aria-hidden="true">
            <span className="ehd-1" /><span className="ehd-2" /><span className="ehd-3" />
            <span className="ehd-4" /><span className="ehd-5" /><span className="ehd-6" />
          </div>

          <div className="em-hero-tag">
            <window.Icon name="smile" size={14} strokeWidth={2.2} />
            <span>EMOJI MAKER &middot; FREE &middot; NO UPLOAD</span>
          </div>

          <h1 className="em-hero-title">
            Make custom emoji for&nbsp;<span className="hl">Slack &amp; Discord</span>.
          </h1>

          <p className="em-hero-sub">
            Create static <em>and</em> animated custom emoji — resize, crop, add text,
            apply effects, export as PNG or GIF. No sign-up, no uploads, everything
            runs in your browser.
          </p>

          <div className="em-hero-ctas">
            <button className="btn btn-primary em-btn-primary" onClick={scrollToTool}>
              <window.Icon name="smile" size={16} /> Start making
            </button>
            <button className="btn btn-secondary em-btn-secondary" onClick={scrollToHow}>
              How it works <window.Icon name="arrow" size={14} />
            </button>
          </div>

          <div className="em-hero-stats">
            <div className="ehs">
              <div className="ehs-v">4</div>
              <div className="ehs-l">platforms supported</div>
            </div>
            <div className="ehs">
              <div className="ehs-v">60+</div>
              <div className="ehs-l">animation effects</div>
            </div>
            <div className="ehs">
              <div className="ehs-v">100%</div>
              <div className="ehs-l">client-side processing</div>
            </div>
            <div className="ehs">
              <div className="ehs-v">0</div>
              <div className="ehs-l">files uploaded anywhere</div>
            </div>
          </div>
        </section>

        {/* ----- TEMPLATE GALLERY ----- */}
        <section className="em-templates">
          <h2 className="em-h2">Start with a template</h2>
          <p className="em-tool-sub">Click any emoji to customize it — or upload your own image below.</p>
          <TemplateGallery onSelect={(canvas) => {
            document.getElementById('em-tool')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
            // Dispatch a custom event so the editor picks up the template
            window.dispatchEvent(new CustomEvent('em-template-select', { detail: { canvas } }));
          }} />
        </section>

        {/* ----- EMBEDDED TOOL ----- */}
        <section id="em-tool" className="em-tool em-tool-top">
          <div className="em-tool-head">
            <h2 className="em-h2">Emoji editor</h2>
            <p className="em-tool-sub">Upload an image, tweak it, download the perfect emoji.</p>
          </div>
          <div className="em-tool-frame">
            <EmojiEditor />
          </div>
        </section>

        {/* ----- PLATFORM GUIDE ----- */}
        <section className="em-platforms">
          <h2 className="em-h2">Platform emoji specs</h2>
          <div className="em-plat-grid">
            <PlatformCard
              name="Slack"
              color="#4A154B"
              icon="message-circle"
              dims="128 x 128"
              maxSize="128 KB"
              formats="PNG, JPG, GIF"
              note="Animated GIF supported. Square aspect ratio required."
            />
            <PlatformCard
              name="Discord"
              color="#5865F2"
              icon="message-circle"
              dims="128 - 256px"
              maxSize="256 KB"
              formats="PNG, JPG, GIF"
              note="Nitro unlocks animated emoji. Non-Nitro: 50 static slots."
            />
            <PlatformCard
              name="Twitch"
              color="#9146FF"
              icon="tv"
              dims="28 / 56 / 112"
              maxSize="25 KB each"
              formats="PNG"
              note="Three sizes required. Transparent background recommended."
            />
            <PlatformCard
              name="MS Teams"
              color="#6264A7"
              icon="users"
              dims="100 x 100"
              maxSize="1 MB"
              formats="PNG, JPG, GIF"
              note="Custom emoji available for Teams with premium licenses."
            />
          </div>
        </section>

        {/* ----- HOW IT WORKS ----- */}
        <section id="em-how" className="em-how">
          <h2 className="em-h2">How it works</h2>
          <div className="em-how-grid">
            <div className="em-how-step">
              <span className="em-step-num">1</span>
              <div>
                <strong>Upload your image.</strong>
                <span>Drag and drop or click to upload any PNG, JPG, or WebP file.</span>
              </div>
            </div>
            <div className="em-how-step">
              <span className="em-step-num">2</span>
              <div>
                <strong>Customize everything.</strong>
                <span>Pick a platform preset, crop to circle, add text, adjust colors, choose an animation effect.</span>
              </div>
            </div>
            <div className="em-how-step">
              <span className="em-step-num">3</span>
              <div>
                <strong>Download &amp; upload.</strong>
                <span>Get a perfectly sized PNG or animated GIF ready to upload to your platform.</span>
              </div>
            </div>
          </div>
        </section>

        {/* ----- FAQ ----- */}
        <section className="em-faq">
          <h2 className="em-h2">Common questions</h2>
          <div className="em-faq-grid">
            <details className="em-faq-item" open>
              <summary>What size should Slack emoji be?</summary>
              <p>
                Slack requires 128 x 128 pixels, under 128 KB. The tool auto-resizes to these
                dimensions when you select the Slack preset.
              </p>
            </details>
            <details className="em-faq-item">
              <summary>Can I make animated emoji?</summary>
              <p>
                Yes! Choose any animation effect (shake, bounce, spin, pulse, party, wave, grow)
                and the tool exports an animated GIF. Adjust speed and quality to stay within
                your platform's file size limit.
              </p>
            </details>
            <details className="em-faq-item">
              <summary>Do you upload my images?</summary>
              <p>
                Never. Everything runs 100% in your browser using HTML Canvas and gif.js.
                Your images never leave your device.
              </p>
            </details>
            <details className="em-faq-item">
              <summary>Can I add text to the emoji?</summary>
              <p>
                Yes. The text overlay lets you add a label with customizable font size, color,
                and position (top, center, or bottom of the image).
              </p>
            </details>
            <details className="em-faq-item">
              <summary>What about transparent backgrounds?</summary>
              <p>
                PNG files with transparency are preserved. You can also set a solid background
                color if your platform requires one.
              </p>
            </details>
            <details className="em-faq-item">
              <summary>Can I process multiple emoji at once?</summary>
              <p>
                Yes! Switch to batch mode, drop multiple images, apply the same effects
                to all of them, and download everything as a ZIP file.
              </p>
            </details>
          </div>
        </section>
      </main>
    );
  }

  // ---------------------------------------------------------------------------
  // Platform guide card
  // ---------------------------------------------------------------------------
  function PlatformCard({ name, color, icon, dims, maxSize, formats, note }) {
    return (
      <div className="em-plat-card">
        <div className="em-plat-dot" style={{ background: color }} />
        <h3 className="em-plat-name">{name}</h3>
        <div className="em-plat-specs">
          <div><strong>Size:</strong> {dims}</div>
          <div><strong>Max:</strong> {maxSize}</div>
          <div><strong>Formats:</strong> {formats}</div>
        </div>
        <p className="em-plat-note">{note}</p>
      </div>
    );
  }

  // ---------------------------------------------------------------------------
  // Full-featured emoji editor (embedded in page, not modal)
  // ---------------------------------------------------------------------------
  function EmojiEditor() {
    // --- tabs ---
    const [tab, setTab] = useState('single'); // 'single' | 'batch'

    return (
      <div className="em-editor">
        <div className="em-tab-bar">
          <button className={'em-tab' + (tab === 'single' ? ' active' : '')}
                  onClick={() => setTab('single')}>
            <window.Icon name="image" size={14} /> Single
          </button>
          <button className={'em-tab' + (tab === 'batch' ? ' active' : '')}
                  onClick={() => setTab('batch')}>
            <window.Icon name="layers" size={14} /> Batch
          </button>
        </div>
        {tab === 'single' ? <SingleEditor /> : <BatchEditor />}
      </div>
    );
  }

  // ---------------------------------------------------------------------------
  // SINGLE EDITOR
  // ---------------------------------------------------------------------------
  // Shared effect logic — used by drawFrame, drawFrameForEffect, and Generate All.
  function applyEffectFilters(effectId, filters, frameIndex, totalFrames, cfg) {
    if (totalFrames <= 0) return;
    const t = frameIndex / totalFrames;
    const s = Math.sin(t * Math.PI * 2);
    switch (effectId) {
      case 'party':        filters.push('hue-rotate(' + Math.round(t * 360) + 'deg)'); break;
      case 'rainbow':      filters.push('hue-rotate(' + Math.round(t * 360) + 'deg)'); filters.push('saturate(150%)'); break;
      case 'sparkle':      filters.push('brightness(' + (100 + Math.sin(t * Math.PI * 4) * 60) + '%)'); break;
      case 'blink':        filters.push('opacity(' + (s > 0 ? 100 : 20) + '%)'); break;
      case 'burn':         filters.push('brightness(' + (120 + t * 80) + '%)'); filters.push('contrast(' + (100 + t * 60) + '%)'); filters.push('saturate(' + (100 + t * 100) + '%)'); break;
      case 'hologram':     filters.push('hue-rotate(' + (120 + s * 30) + 'deg)'); filters.push('brightness(' + (90 + Math.sin(t * Math.PI * 4) * 20) + '%)'); break;
      case 'scanning':     filters.push('brightness(' + (80 + s * 40) + '%)'); break;
      case 'strobe':       filters.push('brightness(' + (frameIndex % 2 === 0 ? 200 : 60) + '%)'); break;
      case 'infrared':     filters.push('hue-rotate(' + (300 + s * 30) + 'deg)'); filters.push('saturate(200%)'); filters.push('contrast(130%)'); break;
      case 'sepia-pulse':  filters.push('sepia(' + (50 + s * 50) + '%)'); filters.push('brightness(' + (100 + s * 15) + '%)'); break;
      case 'neon-glow':    filters.push('brightness(' + (110 + s * 30) + '%)'); filters.push('contrast(' + (110 + s * 20) + '%)'); filters.push('saturate(' + (140 + s * 40) + '%)'); break;
      case 'invert-flash': filters.push('invert(' + (s > 0 ? 0 : 100) + '%)'); break;
      case 'color-cycle':  filters.push('hue-rotate(' + Math.round(t * 720) + 'deg)'); break;
      case 'fade-in-out':  filters.push('opacity(' + (50 + Math.abs(s) * 50) + '%)'); break;
      case 'x-ray':        filters.push('invert(' + (50 + s * 30) + '%)'); filters.push('brightness(' + (120 + s * 20) + '%)'); filters.push('grayscale(80%)'); break;
      case 'vaporwave':    filters.push('hue-rotate(' + (200 + s * 40) + 'deg)'); filters.push('saturate(180%)'); filters.push('contrast(' + (90 + s * 15) + '%)'); break;
      case 'custom':
        if (cfg) {
          if (cfg.hueRotate) filters.push('hue-rotate(' + Math.round(t * 360 * (cfg.hueRotate / 100)) + 'deg)');
          if (cfg.brightness) filters.push('brightness(' + (100 + s * cfg.brightness) + '%)');
          if (cfg.blur) filters.push('blur(' + (Math.abs(s) * cfg.blur * 0.05) + 'px)');
        }
        break;
    }
  }

  function applyEffectTransform(ctx, effectId, frameIndex, totalFrames, size, cfg) {
    let dx = 0, dy = 0, rot = 0, sc = 1;
    if (totalFrames > 0) {
      const t = frameIndex / totalFrames;
      const s = Math.sin(t * Math.PI * 2);
      switch (effectId) {
        case 'shake': dx = s * size * 0.08; break;
        case 'bounce': dy = -Math.abs(s) * size * 0.15; break;
        case 'spin': rot = t * Math.PI * 2; break;
        case 'pulse': sc = 1 + 0.15 * s; break;
        case 'wave': dx = Math.sin(t * Math.PI * 4) * size * 0.06; dy = Math.cos(t * Math.PI * 2) * size * 0.03; break;
        case 'grow': sc = Math.min(1, t * 1.2); break;
        case 'wobble': rot = s * 0.25; break;
        case 'flip': ctx.scale(Math.cos(t * Math.PI * 2), 1); break;
        case 'slide': dx = (t < 0.5 ? t * 2 - 1 : 1 - (t - 0.5) * 2) * size * 0.3; break;
        case 'rubber': ctx.scale(1 + s * 0.2, 1 - s * 0.2); break;
        case 'glitch': dx = (Math.random() - 0.5) * size * 0.12; dy = (Math.random() - 0.5) * size * 0.06; break;
        case 'zoom': sc = 1 + Math.sin(t * Math.PI) * 0.4; break;
        case 'nod': dy = s * size * 0.06; rot = s * 0.08; break;
        case 'jelly': ctx.scale(1 + s * 0.12, 1 - s * 0.12); rot = s * 0.06; break;
        case 'hop': dy = -Math.abs(Math.sin(t * Math.PI)) * size * 0.25; sc = 1 + Math.sin(t * Math.PI) * 0.05; break;
        case 'orbit': dx = Math.cos(t * Math.PI * 2) * size * 0.15; dy = Math.sin(t * Math.PI * 2) * size * 0.15; break;
        case 'intensifies': dx = (Math.random() - 0.5) * size * 0.06; dy = (Math.random() - 0.5) * size * 0.06; sc = 1 + (Math.random() - 0.5) * 0.08; break;
        case 'zoom-close': sc = 1 + t * 0.8; break;
        case 'elastic': { const d = Math.exp(-t * 3) * Math.cos(t * Math.PI * 6); sc = 1 + d * 0.3; break; }
        case 'mega-bounce': dy = -Math.abs(Math.sin(t * Math.PI * 2)) * size * 0.35; sc = 1 - Math.abs(s) * 0.1; break;
        case 'explode': sc = 1 + t * 2; break;
        case 'tornado': rot = t * Math.PI * 4; sc = 1 - t * 0.7; break;
        case 'spiral-zoom': rot = t * Math.PI * 4; sc = 1 + Math.sin(t * Math.PI * 2) * 0.3; break;
        case 'twirl': rot = Math.sin(t * Math.PI * 2) * Math.PI; sc = 1 + Math.sin(t * Math.PI * 4) * 0.1; break;
        case 'pixelate': break;
        case 'rgb-split': dx = s * 3; break;
        case 'flag-wave': ctx.scale(1, 1 + s * 0.1); dx = Math.sin(t * Math.PI * 4) * size * 0.03; break;
        // New motion effects
        case 'pendulum': rot = Math.sin(t * Math.PI * 2) * 0.4; ctx.translate(0, -size * 0.3); break;
        case 'float': dy = Math.sin(t * Math.PI * 2) * size * 0.05; dx = Math.sin(t * Math.PI) * size * 0.02; break;
        case 'zigzag': dx = (t < 0.25 ? t * 4 : t < 0.5 ? 2 - t * 4 : t < 0.75 ? (t - 0.5) * -4 : -2 + (t - 0.75) * 4) * size * 0.12; dy = s * size * 0.04; break;
        case 'drop-bounce': { const p = t < 0.4 ? t / 0.4 : 1; dy = (t < 0.4 ? (p * p) : -Math.abs(Math.sin((t - 0.4) / 0.6 * Math.PI * 3)) * 0.6) * size * 0.3; break; }
        case 'earthquake': dx = (Math.random() - 0.5) * size * 0.08; dy = (Math.random() - 0.5) * size * 0.08; rot = (Math.random() - 0.5) * 0.06; break;
        case 'swing': rot = Math.sin(t * Math.PI * 2) * 0.35; ctx.translate(0, -size * 0.35); break;
        case 'figure-eight': dx = Math.sin(t * Math.PI * 2) * size * 0.12; dy = Math.sin(t * Math.PI * 4) * size * 0.06; break;
        case 'teleport': sc = t < 0.3 ? 1 - t / 0.3 : t < 0.5 ? 0 : t < 0.8 ? (t - 0.5) / 0.3 : 1; break;
        case 'panic': dx = (Math.random() - 0.5) * size * 0.15; dy = (Math.random() - 0.5) * size * 0.1; rot = (Math.random() - 0.5) * 0.15; break;
        // New transform effects
        case 'shrink': sc = 1 - t * 0.8; break;
        case 'flip-v': ctx.scale(1, Math.cos(t * Math.PI * 2)); break;
        case 'rotate-90': rot = (t < 0.5 ? t * 2 : 1) * Math.PI * 0.5; break;
        case 'rotate-180': rot = (t < 0.5 ? t * 2 : 1) * Math.PI; break;
        case 'squish': { const sq = s; ctx.scale(1 + sq * 0.25, 1 - sq * 0.25); break; }
        case 'stretch': { ctx.scale(1, 1 + s * 0.3); break; }
        case 'cube': { const c = Math.cos(t * Math.PI * 2); ctx.scale(Math.abs(c), 1); dx = c * size * 0.1; break; }
        case 'spring': { const d = Math.exp(-t * 4) * Math.sin(t * Math.PI * 8); dy = d * size * 0.2; sc = 1 + d * 0.1; break; }
        case 'custom':
          if (cfg) {
            dx = Math.sin(t * Math.PI * 2) * size * (cfg.moveX / 100) * 0.2;
            dy = Math.sin(t * Math.PI * 2) * size * (cfg.moveY / 100) * 0.2;
            rot = Math.sin(t * Math.PI * 2) * (cfg.rotate / 100) * Math.PI;
            sc = 1 + Math.sin(t * Math.PI * 2) * (cfg.scale / 100) * 0.5;
            if (cfg.skew) ctx.transform(1, 0, Math.sin(t * Math.PI * 2) * (cfg.skew / 100) * 0.3, 1, 0, 0);
          }
          break;
      }
    }
    ctx.translate(dx, dy);
    ctx.rotate(rot);
    ctx.scale(sc, sc);
  }

  function SingleEditor() {
    const [file, setFile] = useState(null);
    const [src, setSrc] = useState('');
    const imgRef = useRef(null);

    // Listen for template selections from the gallery
    useEffect(() => {
      const onTemplate = (e) => {
        const canvas = e.detail?.canvas;
        if (!canvas) return;
        canvas.toBlob((blob) => {
          if (!blob) return;
          const f = new File([blob], 'template.png', { type: 'image/png' });
          const url = URL.createObjectURL(blob);
          const img = new Image();
          img.onload = () => {
            imgRef.current = img;
            setFile(f);
            setSrc(url);
          };
          img.src = url;
        }, 'image/png');
      };
      window.addEventListener('em-template-select', onTemplate);
      return () => window.removeEventListener('em-template-select', onTemplate);
    }, []);

    // platform preset
    const PRESETS = useMemo(() => [
      { id: 'slack',   label: 'Slack',    size: 128, maxKB: 128 },
      { id: 'discord', label: 'Discord',  size: 256, maxKB: 256 },
      { id: 'twitch',  label: 'Twitch',   size: 112, maxKB: 25 },
      { id: 'custom',  label: 'Custom',   size: null, maxKB: null },
    ], []);
    const [presetId, setPresetId] = useState('slack');
    const [customSize, setCustomSize] = useState(128);

    // text overlay
    const [textOn, setTextOn] = useState(false);
    const [textVal, setTextVal] = useState('');
    const [textSize, setTextSize] = useState(24);
    const [textColor, setTextColor] = useState('#ffffff');
    const [textPos, setTextPos] = useState('bottom'); // 'top' | 'center' | 'bottom'
    const [textBold, setTextBold] = useState(true);

    // static effects
    const [roundCorners, setRoundCorners] = useState(false);
    const [circleCrop, setCircleCrop] = useState(false);
    const [flipH, setFlipH] = useState(false);
    const [flipV, setFlipV] = useState(false);
    const [borderOn, setBorderOn] = useState(false);
    const [borderColor, setBorderColor] = useState('#7c3aed');
    const [borderWidth, setBorderWidth] = useState(4);
    const [brightness, setBrightness] = useState(0);
    const [contrast, setContrast] = useState(0);
    const [saturation, setSaturation] = useState(0);
    const [bgColor, setBgColor] = useState('');
    const [bgOn, setBgOn] = useState(false);

    // animated effects — 35 total + custom
    // animated effects — 60+ total
    const ANIM_EFFECTS = useMemo(() => [
      { id: 'none',         label: 'None',         frames: 0,  icon: 'x',       cat: 'basic' },
      // ── Motion (20) ──
      { id: 'shake',        label: 'Shake',        frames: 6,  icon: 'swap',    cat: 'motion' },
      { id: 'bounce',       label: 'Bounce',       frames: 8,  icon: 'arrow',   cat: 'motion' },
      { id: 'spin',         label: 'Spin',         frames: 12, icon: 'rotate',  cat: 'motion' },
      { id: 'pulse',        label: 'Pulse',        frames: 8,  icon: 'heart',   cat: 'motion' },
      { id: 'wave',         label: 'Wave',         frames: 10, icon: 'swap',    cat: 'motion' },
      { id: 'wobble',       label: 'Wobble',       frames: 8,  icon: 'rotate',  cat: 'motion' },
      { id: 'nod',          label: 'Nod',          frames: 8,  icon: 'arrow',   cat: 'motion' },
      { id: 'slide',        label: 'Slide',        frames: 10, icon: 'arrow',   cat: 'motion' },
      { id: 'hop',          label: 'Hop',          frames: 10, icon: 'arrow',   cat: 'motion' },
      { id: 'orbit',        label: 'Orbit',        frames: 12, icon: 'rotate',  cat: 'motion' },
      { id: 'intensifies',  label: 'Intensifies',  frames: 8,  icon: 'bolt',    cat: 'motion' },
      { id: 'pendulum',     label: 'Pendulum',     frames: 10, icon: 'rotate',  cat: 'motion' },
      { id: 'float',        label: 'Float',        frames: 12, icon: 'arrow',   cat: 'motion' },
      { id: 'zigzag',       label: 'Zigzag',       frames: 10, icon: 'swap',    cat: 'motion' },
      { id: 'drop-bounce',  label: 'Drop Bounce',  frames: 12, icon: 'arrow',   cat: 'motion' },
      { id: 'earthquake',   label: 'Earthquake',   frames: 8,  icon: 'bolt',    cat: 'motion' },
      { id: 'swing',        label: 'Swing',        frames: 10, icon: 'rotate',  cat: 'motion' },
      { id: 'figure-eight', label: 'Figure 8',     frames: 14, icon: 'rotate',  cat: 'motion' },
      { id: 'teleport',     label: 'Teleport',     frames: 10, icon: 'bolt',    cat: 'motion' },
      { id: 'panic',        label: 'Panic',        frames: 6,  icon: 'bolt',    cat: 'motion' },
      // ── Transform (20) ──
      { id: 'grow',         label: 'Grow',         frames: 10, icon: 'upscale', cat: 'transform' },
      { id: 'flip',         label: 'Flip',         frames: 12, icon: 'swap',    cat: 'transform' },
      { id: 'rubber',       label: 'Rubber',       frames: 10, icon: 'resize',  cat: 'transform' },
      { id: 'jelly',        label: 'Jelly',        frames: 10, icon: 'droplet', cat: 'transform' },
      { id: 'zoom',         label: 'Zoom',         frames: 8,  icon: 'scan',    cat: 'transform' },
      { id: 'zoom-close',   label: 'Zoom Close',   frames: 10, icon: 'scan',    cat: 'transform' },
      { id: 'elastic',      label: 'Elastic',      frames: 12, icon: 'resize',  cat: 'transform' },
      { id: 'mega-bounce',  label: 'Mega Bounce',  frames: 10, icon: 'arrow',   cat: 'transform' },
      { id: 'explode',      label: 'Explode',      frames: 10, icon: 'bolt',    cat: 'transform' },
      { id: 'tornado',      label: 'Tornado',      frames: 12, icon: 'rotate',  cat: 'transform' },
      { id: 'spiral-zoom',  label: 'Spiral Zoom',  frames: 12, icon: 'rotate',  cat: 'transform' },
      { id: 'twirl',        label: 'Twirl',        frames: 12, icon: 'rotate',  cat: 'transform' },
      { id: 'shrink',       label: 'Shrink',       frames: 10, icon: 'resize',  cat: 'transform' },
      { id: 'flip-v',       label: 'Flip V',       frames: 12, icon: 'swap',    cat: 'transform' },
      { id: 'rotate-90',    label: 'Rotate 90°',   frames: 10, icon: 'rotate',  cat: 'transform' },
      { id: 'rotate-180',   label: 'Rotate 180°',  frames: 10, icon: 'rotate',  cat: 'transform' },
      { id: 'squish',       label: 'Squish',       frames: 8,  icon: 'resize',  cat: 'transform' },
      { id: 'stretch',      label: 'Stretch',      frames: 8,  icon: 'resize',  cat: 'transform' },
      { id: 'cube',         label: 'Cube',         frames: 12, icon: 'grid',    cat: 'transform' },
      { id: 'spring',       label: 'Spring',       frames: 12, icon: 'resize',  cat: 'transform' },
      // ── Visual (20) ──
      { id: 'party',        label: 'Party',        frames: 10, icon: 'palette',  cat: 'visual' },
      { id: 'glitch',       label: 'Glitch',       frames: 8,  icon: 'bolt',     cat: 'visual' },
      { id: 'rainbow',      label: 'Rainbow',      frames: 14, icon: 'palette',  cat: 'visual' },
      { id: 'sparkle',      label: 'Sparkle',      frames: 8,  icon: 'sparkle',  cat: 'visual' },
      { id: 'blink',        label: 'Blink',        frames: 6,  icon: 'scan',     cat: 'visual' },
      { id: 'burn',         label: 'Burn',         frames: 10, icon: 'bolt',     cat: 'visual' },
      { id: 'hologram',     label: 'Hologram',     frames: 10, icon: 'scan',     cat: 'visual' },
      { id: 'pixelate',     label: 'Pixelate',     frames: 8,  icon: 'grid',     cat: 'visual' },
      { id: 'rgb-split',    label: 'RGB Split',    frames: 8,  icon: 'palette',  cat: 'visual' },
      { id: 'scanning',     label: 'Scanning',     frames: 12, icon: 'scan',     cat: 'visual' },
      { id: 'flag-wave',    label: 'Flag Wave',    frames: 10, icon: 'swap',     cat: 'visual' },
      { id: 'strobe',       label: 'Strobe',       frames: 6,  icon: 'bolt',     cat: 'visual' },
      { id: 'infrared',     label: 'Infrared',     frames: 10, icon: 'palette',  cat: 'visual' },
      { id: 'sepia-pulse',  label: 'Sepia Pulse',  frames: 10, icon: 'palette',  cat: 'visual' },
      { id: 'neon-glow',    label: 'Neon Glow',    frames: 10, icon: 'sparkle',  cat: 'visual' },
      { id: 'invert-flash', label: 'Invert Flash', frames: 8,  icon: 'bolt',     cat: 'visual' },
      { id: 'color-cycle',  label: 'Color Cycle',  frames: 12, icon: 'palette',  cat: 'visual' },
      { id: 'fade-in-out',  label: 'Fade In/Out',  frames: 10, icon: 'scan',     cat: 'visual' },
      { id: 'x-ray',        label: 'X-Ray',        frames: 10, icon: 'scan',     cat: 'visual' },
      { id: 'vaporwave',    label: 'Vaporwave',    frames: 12, icon: 'palette',  cat: 'visual' },
      // Custom
      { id: 'custom',       label: 'Custom ✨',    frames: 12, icon: 'sparkle',  cat: 'custom' },
    ], []);

    const EFFECT_CATS = [
      { id: 'all', label: 'All' },
      { id: 'motion', label: 'Motion' },
      { id: 'transform', label: 'Transform' },
      { id: 'visual', label: 'Visual' },
      { id: 'custom', label: 'Custom' },
    ];
    const [effectCat, setEffectCat] = useState('all');
    const [animEffect, setAnimEffect] = useState('none');
    const [speed, setSpeed] = useState('normal');
    const [quality, setQuality] = useState(90);

    // Custom effect builder
    const [customCfg, setCustomCfg] = useState({
      moveX: 0, moveY: 0, rotate: 0, scale: 0, skew: 0,
      hueRotate: 0, brightness: 0, blur: 0,
    });
    const updateCustom = (key, val) => setCustomCfg(prev => ({ ...prev, [key]: val }));

    // output
    const [outUrl, setOutUrl] = useState('');
    const [outSize, setOutSize] = useState(0);
    const [generating, setGenerating] = useState(false);

    const isAnimated = animEffect !== 'none';
    const preset = PRESETS.find(p => p.id === presetId);
    const outDim = preset.size || customSize;
    const speedDelays = { slow: 120, normal: 70, fast: 35 };
    const frameDelay = speedDelays[speed] || 70;

    // file handling
    const handleFile = async (f) => {
      if (!f || !f.type.startsWith('image/')) return;
      const { img, url } = await window.loadImageFromFile(f);
      imgRef.current = img;
      setFile(f);
      setSrc(url);
      setOutUrl('');
      setOutSize(0);
    };

    // draw a single frame
    const drawFrame = useCallback((frameIndex, totalFrames) => {
      const img = imgRef.current;
      if (!img) return null;

      const size = outDim;
      const c = document.createElement('canvas');
      c.width = size;
      c.height = size;
      const ctx = c.getContext('2d');

      // background color
      if (bgOn && bgColor) {
        ctx.fillStyle = bgColor;
        ctx.fillRect(0, 0, size, size);
      }

      ctx.save();

      // clipping
      if (circleCrop) {
        ctx.beginPath();
        ctx.arc(size / 2, size / 2, size / 2 - (borderOn ? borderWidth : 0), 0, Math.PI * 2);
        ctx.closePath();
        ctx.clip();
      } else if (roundCorners) {
        const r = size * 0.18;
        const inset = borderOn ? borderWidth : 0;
        roundRect(ctx, inset, inset, size - inset * 2, size - inset * 2, r);
        ctx.clip();
      }

      // filters
      const filters = [];
      if (brightness !== 0) filters.push('brightness(' + (100 + brightness) + '%)');
      if (contrast !== 0) filters.push('contrast(' + (100 + contrast) + '%)');
      if (saturation !== 0) filters.push('saturate(' + (100 + saturation) + '%)');

      // Effect-specific filters
      applyEffectFilters(animEffect, filters, frameIndex, totalFrames, customCfg);
      if (filters.length) ctx.filter = filters.join(' ');

      ctx.save();
      ctx.translate(size / 2, size / 2);

      // flip
      const sx = flipH ? -1 : 1;
      const sy = flipV ? -1 : 1;
      ctx.scale(sx, sy);

      // animation transforms
      applyEffectTransform(ctx, animEffect, frameIndex, totalFrames, size, customCfg);

      ctx.translate(dx, dy);
      ctx.rotate(rot);
      ctx.scale(sc, sc);

      // draw image centered / cover
      const iw = img.width, ih = img.height;
      const ratio = Math.max(size / iw, size / ih);
      const dw = iw * ratio, dh = ih * ratio;
      ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh);

      ctx.restore();
      ctx.restore();

      // reset filter
      ctx.filter = 'none';

      // border
      if (borderOn && borderWidth > 0) {
        ctx.strokeStyle = borderColor;
        ctx.lineWidth = borderWidth;
        if (circleCrop) {
          ctx.beginPath();
          ctx.arc(size / 2, size / 2, size / 2 - borderWidth / 2, 0, Math.PI * 2);
          ctx.stroke();
        } else if (roundCorners) {
          const r = size * 0.18;
          roundRect(ctx, borderWidth / 2, borderWidth / 2, size - borderWidth, size - borderWidth, r);
          ctx.stroke();
        } else {
          ctx.strokeRect(borderWidth / 2, borderWidth / 2, size - borderWidth, size - borderWidth);
        }
      }

      // text overlay
      if (textOn && textVal.trim()) {
        const fontSize = Math.round(textSize * (size / 128));
        ctx.font = (textBold ? 'bold ' : '') + fontSize + 'px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
        ctx.fillStyle = textColor;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';

        // stroke outline for readability
        ctx.strokeStyle = 'rgba(0,0,0,0.6)';
        ctx.lineWidth = Math.max(1, fontSize * 0.12);
        ctx.lineJoin = 'round';

        let ty;
        if (textPos === 'top') ty = fontSize * 0.8 + (borderOn ? borderWidth : 0);
        else if (textPos === 'center') ty = size / 2;
        else ty = size - fontSize * 0.8 - (borderOn ? borderWidth : 0);

        ctx.strokeText(textVal, size / 2, ty);
        ctx.fillText(textVal, size / 2, ty);
      }

      return c;
    }, [outDim, circleCrop, roundCorners, flipH, flipV, borderOn, borderColor, borderWidth,
        brightness, contrast, saturation, animEffect, bgOn, bgColor,
        textOn, textVal, textSize, textColor, textPos, textBold]);

    // rounded-rect helper
    function roundRect(ctx, x, y, w, h, r) {
      ctx.beginPath();
      ctx.moveTo(x + r, y);
      ctx.lineTo(x + w - r, y);
      ctx.quadraticCurveTo(x + w, y, x + w, y + r);
      ctx.lineTo(x + w, y + h - r);
      ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
      ctx.lineTo(x + r, y + h);
      ctx.quadraticCurveTo(x, y + h, x, y + h - r);
      ctx.lineTo(x, y + r);
      ctx.quadraticCurveTo(x, y, x + r, y);
      ctx.closePath();
    }

    // static PNG preview (debounced)
    useEffect(() => {
      if (!imgRef.current || isAnimated) return;
      const id = setTimeout(() => {
        const c = drawFrame(0, 0);
        if (!c) return;
        c.toBlob((blob) => {
          if (!blob) return;
          if (outUrl) URL.revokeObjectURL(outUrl);
          setOutUrl(URL.createObjectURL(blob));
          setOutSize(blob.size);
        }, 'image/png');
      }, 80);
      return () => clearTimeout(id);
    }, [file, outDim, circleCrop, roundCorners, flipH, flipV, borderOn, borderColor,
        borderWidth, brightness, contrast, saturation, isAnimated, drawFrame, bgOn, bgColor,
        textOn, textVal, textSize, textColor, textPos, textBold]);

    // generate GIF
    const generateGif = async () => {
      if (!imgRef.current) return;
      setGenerating(true);
      try {
        await window.loadScript('https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.js');
        const workerBlob = await window.getGifWorkerBlob();
        const eff = ANIM_EFFECTS.find(e => e.id === animEffect);
        const numFrames = eff ? eff.frames : 6;

        const gif = new GIF({
          workers: 2,
          quality: Math.max(1, Math.round((110 - quality) / 5)),
          workerScript: workerBlob,
          width: outDim,
          height: outDim,
          transparent: null,
        });

        for (let i = 0; i < numFrames; i++) {
          const c = drawFrame(i, numFrames);
          if (c) gif.addFrame(c, { delay: frameDelay, copy: true });
        }

        const blob = await new Promise((resolve, reject) => {
          gif.on('finished', resolve);
          gif.on('error', reject);
          gif.render();
        });

        if (outUrl) URL.revokeObjectURL(outUrl);
        setOutUrl(URL.createObjectURL(blob));
        setOutSize(blob.size);
      } catch (e) {
        console.error('GIF generation failed:', e);
      } finally {
        setGenerating(false);
      }
    };

    // re-generate GIF whenever animated params change
    useEffect(() => {
      if (!imgRef.current || !isAnimated) return;
      generateGif();
    }, [animEffect, speed, outDim, circleCrop, roundCorners, flipH, flipV, borderOn,
        borderColor, borderWidth, brightness, contrast, saturation, quality, file,
        bgOn, bgColor, textOn, textVal, textSize, textColor, textPos, textBold]);

    // download
    const download = () => {
      if (!outUrl) return;
      const base = file ? file.name.replace(/\.[^.]+$/, '') : 'emoji';
      const ext = isAnimated ? 'gif' : 'png';
      fetch(outUrl).then(r => r.blob()).then(blob => {
        window.downloadBlob(blob, base + '-emoji.' + ext);
      });
    };

    // size warning
    const sizeWarning = useMemo(() => {
      if (!outSize || !preset.maxKB) return null;
      const maxBytes = preset.maxKB * 1024;
      if (outSize > maxBytes) {
        return 'File is ' + window.fmtBytes(outSize) + ' \u2014 exceeds ' + preset.label + ' limit of ' + preset.maxKB + ' KB. Try lowering quality or dimensions.';
      }
      return null;
    }, [outSize, preset]);

    // CSS animation for live preview
    const previewAnimStyle = useMemo(() => {
      if (!isAnimated) return {};
      const dur = speed === 'slow' ? '1.2s' : speed === 'fast' ? '0.35s' : '0.7s';
      const common = { animationDuration: dur, animationIterationCount: 'infinite', animationTimingFunction: 'ease-in-out' };
      // Map effect id to CSS keyframe name
      const cssMap = {
        shake:'em-shake', bounce:'em-bounce', spin:'em-spin', pulse:'em-pulse',
        party:'em-party', wave:'em-wave', grow:'em-grow', wobble:'em-wobble',
        flip:'em-flip', slide:'em-slide', rubber:'em-rubber', glitch:'em-glitch',
        zoom:'em-zoom', nod:'em-nod', jelly:'em-jelly', hop:'em-bounce',
        orbit:'em-orbit', intensifies:'em-glitch',
        'zoom-close':'em-zoom', elastic:'em-rubber', 'mega-bounce':'em-bounce',
        explode:'em-grow', tornado:'em-spin', 'spiral-zoom':'em-spin',
        twirl:'em-wobble', rainbow:'em-party', sparkle:'em-pulse',
        blink:'em-blink', burn:'em-pulse', hologram:'em-party',
        pixelate:'em-glitch', 'rgb-split':'em-shake', scanning:'em-pulse',
        'flag-wave':'em-wave', custom:'em-jelly',
        // New motion
        pendulum:'em-wobble', float:'em-wave', zigzag:'em-slide',
        'drop-bounce':'em-bounce', earthquake:'em-glitch', swing:'em-wobble',
        'figure-eight':'em-orbit', teleport:'em-blink', panic:'em-glitch',
        // New transform
        shrink:'em-grow', 'flip-v':'em-flip', 'rotate-90':'em-spin',
        'rotate-180':'em-spin', squish:'em-rubber', stretch:'em-rubber',
        cube:'em-flip', spring:'em-bounce',
        // New visual
        strobe:'em-blink', infrared:'em-party', 'sepia-pulse':'em-pulse',
        'neon-glow':'em-pulse', 'invert-flash':'em-blink', 'color-cycle':'em-party',
        'fade-in-out':'em-blink', 'x-ray':'em-party', vaporwave:'em-party',
      };
      const name = cssMap[animEffect];
      if (!name) return {};
      const overrides = {};
      if (animEffect === 'spin' || animEffect === 'tornado' || animEffect === 'spiral-zoom') overrides.animationTimingFunction = 'linear';
      if (animEffect === 'glitch' || animEffect === 'intensifies') overrides.animationDuration = '0.3s';
      return { ...common, animationName: name, ...overrides };
      }
    }, [isAnimated, animEffect, speed]);

    const reset = () => {
      if (outUrl) URL.revokeObjectURL(outUrl);
      if (src) URL.revokeObjectURL(src);
      setFile(null); setSrc(''); setOutUrl(''); setOutSize(0);
      setAllEffects([]); setAllBusy(false);
    };

    // --- Generate ALL effects at once ---
    const [allEffects, setAllEffects] = useState([]);
    const [allBusy, setAllBusy] = useState(false);

    const generateAll = async () => {
      if (!imgRef.current) return;
      setAllBusy(true);
      try {
        await window.loadScript('https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.js');
        const workerBlob = await window.getGifWorkerBlob();
        const effects = ANIM_EFFECTS.filter(e => e.id !== 'none');
        const results = [];

        // Static PNG first
        const staticCanvas = drawFrame(0, 0);
        if (staticCanvas) {
          const blob = await new Promise(r => staticCanvas.toBlob(r, 'image/png'));
          results.push({ label: 'Static', ext: 'png', url: URL.createObjectURL(blob), size: blob.size, blob });
        }

        // Each animation effect
        for (const eff of effects) {
          try {
            const gif = new GIF({
              workers: 2, quality: 10,
              workerScript: workerBlob,
              width: outDim, height: outDim, transparent: null,
            });
            for (let i = 0; i < eff.frames; i++) {
              // Temporarily set animEffect context for drawFrame
              const origEffect = animEffect;
              // We can't change state mid-loop, so we call drawFrame with explicit effect
              const c = drawFrameForEffect(eff.id, i, eff.frames);
              if (c) gif.addFrame(c, { delay: frameDelay, copy: true });
            }
            const blob = await new Promise((resolve, reject) => {
              gif.on('finished', resolve);
              gif.on('error', reject);
              gif.render();
            });
            results.push({ label: eff.label, ext: 'gif', url: URL.createObjectURL(blob), size: blob.size, blob });
          } catch {}
          // Update incrementally so user sees progress
          setAllEffects([...results]);
        }

        setAllEffects(results);
      } catch (e) { console.error('Generate all failed:', e); }
      finally { setAllBusy(false); }
    };

    // drawFrame variant that accepts explicit effect id (for Generate All)
    const drawFrameForEffect = useCallback((effectId, frameIndex, totalFrames) => {
      const img = imgRef.current;
      if (!img) return null;
      const size = outDim;
      const c = document.createElement('canvas');
      c.width = size; c.height = size;
      const ctx = c.getContext('2d');

      if (circleCrop) { ctx.save(); ctx.beginPath(); ctx.arc(size/2,size/2,size/2-(borderOn?borderWidth:0),0,Math.PI*2); ctx.clip(); }
      else if (roundCorners) { ctx.save(); const r=size*0.18,ins=borderOn?borderWidth:0; roundRect(ctx,ins,ins,size-ins*2,size-ins*2,r); ctx.clip(); }

      const filters = [];
      if (brightness!==0) filters.push('brightness('+(100+brightness)+'%)');
      if (contrast!==0) filters.push('contrast('+(100+contrast)+'%)');
      if (saturation!==0) filters.push('saturate('+(100+saturation)+'%)');
      applyEffectFilters(effectId, filters, frameIndex, totalFrames, null);
      if (filters.length) ctx.filter=filters.join(' ');

      ctx.save();
      ctx.translate(size/2,size/2);
      ctx.scale(flipH?-1:1, flipV?-1:1);
      applyEffectTransform(ctx, effectId, frameIndex, totalFrames, size, null);

      const iw=img.width, ih=img.height, ratio=Math.max(size/iw,size/ih);
      ctx.drawImage(img,-iw*ratio/2,-ih*ratio/2,iw*ratio,ih*ratio);
      ctx.restore();
      if (circleCrop||roundCorners) ctx.restore();
      ctx.filter='none';

      if (borderOn&&borderWidth>0) {
        ctx.strokeStyle=borderColor; ctx.lineWidth=borderWidth;
        if (circleCrop) { ctx.beginPath(); ctx.arc(size/2,size/2,size/2-borderWidth/2,0,Math.PI*2); ctx.stroke(); }
        else if (roundCorners) { roundRect(ctx,borderWidth/2,borderWidth/2,size-borderWidth,size-borderWidth,size*0.18); ctx.stroke(); }
        else ctx.strokeRect(borderWidth/2,borderWidth/2,size-borderWidth,size-borderWidth);
      }
      return c;
    }, [outDim, circleCrop, roundCorners, flipH, flipV, borderOn, borderColor, borderWidth, brightness, contrast, saturation]);

    const downloadAllZip = async () => {
      if (!allEffects.length) return;
      await window.loadScript('https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.js');
      const baseName = file ? file.name.replace(/\.[^.]+$/,'') : 'emoji';
      const entries = {};
      for (const e of allEffects) {
        const buf = await e.blob.arrayBuffer();
        entries[baseName + '-' + e.label.toLowerCase().replace(/\s+/g,'-') + '.' + e.ext] = new Uint8Array(buf);
      }
      const zipped = fflate.zipSync(entries, { level: 0 });
      window.downloadBlob(new Blob([zipped], { type: 'application/zip' }), baseName + '-all-effects.zip');
    };

    // --- dropzone ---
    if (!file) {
      return React.createElement(window.Dropzone, {
        onFile: handleFile,
        title: 'Drop an image to make an emoji',
        hint: 'PNG, JPG, WebP, GIF \u2014 any size',
        accept: 'image/*',
      });
    }

    // --- main UI ---
    return (
      <div className="em-single">
        {/* Two-column layout: preview left, controls right */}
        <div className="em-layout">
          {/* Left: preview */}
          <div className="em-preview-col">
            <div className="em-preview-wrap">
              <img
                src={isAnimated && outUrl ? outUrl : (outUrl || src)}
                alt="Emoji preview"
                style={isAnimated && !outUrl ? previewAnimStyle : {}}
              />
            </div>

            <div className="em-info-row">
              <span className="em-badge" style={{ background: ACCENT_SOFT, color: ACCENT }}>{outDim} x {outDim}</span>
              {outSize > 0 && (
                <span className="em-badge" style={sizeWarning
                  ? { background: 'var(--id-warning-soft)', color: 'var(--id-warning)' }
                  : { background: ACCENT_SOFT, color: ACCENT }}>
                  {window.fmtBytes(outSize)}
                </span>
              )}
              <span className="em-badge">
                {isAnimated ? 'GIF' : 'PNG'}
              </span>
              {generating && (
                <span className="em-badge" style={{ background: 'var(--id-info-soft)', color: 'var(--id-info)' }}>
                  <window.Icon name="bolt" size={10} /> Generating...
                </span>
              )}
            </div>

            {sizeWarning && (
              <div className="em-warn">
                <window.Icon name="bolt" size={14} />
                {sizeWarning}
              </div>
            )}

            <div className="em-actions">
              <button className="btn btn-secondary" onClick={reset}>
                <window.Icon name="upload" size={16} /> New image
              </button>
              <button className="btn btn-primary em-btn-dl" onClick={download} disabled={!outUrl || generating}>
                <window.Icon name="download" size={16} /> Download {isAnimated ? 'GIF' : 'PNG'}
              </button>
            </div>
          </div>

          {/* Right: controls */}
          <div className="em-controls-col">
            {/* Platform preset */}
            <div className="em-section">
              <div className="em-section-title">
                <window.Icon name="grid" size={13} /> Platform
              </div>
              <div className="em-presets">
                {PRESETS.map(p => (
                  <button key={p.id}
                    className={'em-preset' + (presetId === p.id ? ' active' : '')}
                    onClick={() => setPresetId(p.id)}>
                    {p.label}
                    {p.size && <span className="em-dim">{p.size}px</span>}
                  </button>
                ))}
              </div>
              {presetId === 'custom' && (
                <div className="em-slider-wrap">
                  <div className="em-slider-label"><span>Size</span><span>{customSize}px</span></div>
                  <input type="range" min="16" max="512" step="1" value={customSize}
                         onChange={e => setCustomSize(+e.target.value)} className="em-slider" />
                </div>
              )}
            </div>

            {/* Text overlay */}
            <div className="em-section">
              <div className="em-section-title">
                <window.Icon name="text" size={13} /> Text Overlay
                <button className={'em-toggle-sm' + (textOn ? ' on' : '')}
                        onClick={() => setTextOn(!textOn)}>
                  {textOn ? 'ON' : 'OFF'}
                </button>
              </div>
              {textOn && (
                <div className="em-text-controls">
                  <input type="text" className="em-input" placeholder="Emoji text..."
                         value={textVal} onChange={e => setTextVal(e.target.value)}
                         maxLength={20} />
                  <div className="em-text-row">
                    <div className="em-slider-wrap" style={{ flex: 1 }}>
                      <div className="em-slider-label"><span>Size</span><span>{textSize}px</span></div>
                      <input type="range" min="12" max="48" step="1" value={textSize}
                             onChange={e => setTextSize(+e.target.value)} className="em-slider" />
                    </div>
                    <input type="color" value={textColor} onChange={e => setTextColor(e.target.value)}
                           title="Text color" className="em-color-input" />
                  </div>
                  <div className="em-text-pos-row">
                    {['top', 'center', 'bottom'].map(p => (
                      <button key={p} className={'em-pos-btn' + (textPos === p ? ' active' : '')}
                              onClick={() => setTextPos(p)}>
                        {p.charAt(0).toUpperCase() + p.slice(1)}
                      </button>
                    ))}
                    <button className={'em-pos-btn' + (textBold ? ' active' : '')}
                            onClick={() => setTextBold(!textBold)}>
                      <strong>B</strong>
                    </button>
                  </div>
                </div>
              )}
            </div>

            {/* Shape & Adjustments */}
            <div className="em-section">
              <div className="em-section-title">
                <window.Icon name="square" size={13} /> Shape &amp; Adjustments
              </div>
              <div className="em-toggle-row">
                <button className={'em-toggle' + (roundCorners ? ' on' : '')} onClick={() => { setRoundCorners(!roundCorners); if (!roundCorners) setCircleCrop(false); }}>
                  <window.Icon name="square" size={12} /> Round
                </button>
                <button className={'em-toggle' + (circleCrop ? ' on' : '')} onClick={() => { setCircleCrop(!circleCrop); if (!circleCrop) setRoundCorners(false); }}>
                  <window.Icon name="target" size={12} /> Circle
                </button>
                <button className={'em-toggle' + (flipH ? ' on' : '')} onClick={() => setFlipH(!flipH)}>
                  Flip H
                </button>
                <button className={'em-toggle' + (flipV ? ' on' : '')} onClick={() => setFlipV(!flipV)}>
                  Flip V
                </button>
              </div>

              {/* Border */}
              <div className="em-toggle-row" style={{ marginTop: 8 }}>
                <button className={'em-toggle' + (borderOn ? ' on' : '')} onClick={() => setBorderOn(!borderOn)}>
                  <window.Icon name="square" size={12} /> Border
                </button>
                <button className={'em-toggle' + (bgOn ? ' on' : '')} onClick={() => setBgOn(!bgOn)}>
                  <window.Icon name="palette" size={12} /> Background
                </button>
              </div>

              {borderOn && (
                <div className="em-border-controls">
                  <input type="color" value={borderColor} onChange={e => setBorderColor(e.target.value)} title="Border color" className="em-color-input" />
                  <div style={{ flex: 1 }}>
                    <div className="em-slider-label"><span>Width</span><span>{borderWidth}px</span></div>
                    <input type="range" min="1" max="16" step="1" value={borderWidth}
                           onChange={e => setBorderWidth(+e.target.value)} className="em-slider" />
                  </div>
                </div>
              )}

              {bgOn && (
                <div className="em-border-controls">
                  <input type="color" value={bgColor || '#ffffff'} onChange={e => setBgColor(e.target.value)} title="Background color" className="em-color-input" />
                  <span style={{ fontSize: 12, color: 'var(--id-text-muted)' }}>Fill behind transparent areas</span>
                </div>
              )}

              {/* Sliders */}
              <div className="em-adjust-grid">
                <div className="em-slider-wrap">
                  <div className="em-slider-label"><span>Brightness</span><span>{brightness > 0 ? '+' : ''}{brightness}</span></div>
                  <input type="range" min="-50" max="50" step="1" value={brightness}
                         onChange={e => setBrightness(+e.target.value)} className="em-slider" />
                </div>
                <div className="em-slider-wrap">
                  <div className="em-slider-label"><span>Contrast</span><span>{contrast > 0 ? '+' : ''}{contrast}</span></div>
                  <input type="range" min="-50" max="50" step="1" value={contrast}
                         onChange={e => setContrast(+e.target.value)} className="em-slider" />
                </div>
                <div className="em-slider-wrap">
                  <div className="em-slider-label"><span>Saturation</span><span>{saturation > 0 ? '+' : ''}{saturation}</span></div>
                  <input type="range" min="-50" max="50" step="1" value={saturation}
                         onChange={e => setSaturation(+e.target.value)} className="em-slider" />
                </div>
              </div>
            </div>

            {/* Animated effects */}
            <div className="em-section">
              <div className="em-section-title">
                <window.Icon name="bolt" size={13} /> Animation — {ANIM_EFFECTS.length - 1} effects
              </div>
              {/* Category filter */}
              <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', marginBottom: 8 }}>
                {EFFECT_CATS.map(c => (
                  <button key={c.id}
                    className={'filter-pill' + (effectCat === c.id ? ' active' : '')}
                    style={effectCat === c.id ? { background: ACCENT, borderColor: ACCENT, fontSize: 10, padding: '2px 8px' } : { fontSize: 10, padding: '2px 8px' }}
                    onClick={() => setEffectCat(c.id)}>
                    {c.label}
                  </button>
                ))}
              </div>
              <div className="em-effects">
                {ANIM_EFFECTS
                  .filter(eff => effectCat === 'all' || eff.cat === effectCat || eff.id === 'none')
                  .map(eff => (
                  <div key={eff.id}
                    className={'em-fx-card' + (animEffect === eff.id ? ' active' : '')}
                    onClick={() => setAnimEffect(eff.id)}>
                    <div className="em-fx-icon">
                      <window.Icon name={eff.icon} size={14} />
                    </div>
                    {eff.label}
                  </div>
                ))}
              </div>

              {isAnimated && (
                <>
                  <div className="em-section-sub">Speed</div>
                  <div className="em-speed-row">
                    {['slow', 'normal', 'fast'].map(s => (
                      <button key={s} className={'em-speed-btn' + (speed === s ? ' active' : '')}
                              onClick={() => setSpeed(s)}>
                        {s.charAt(0).toUpperCase() + s.slice(1)}
                      </button>
                    ))}
                  </div>
                  <div className="em-slider-wrap" style={{ marginTop: 10 }}>
                    <div className="em-slider-label"><span>GIF Quality</span><span>{quality}%</span></div>
                    <input type="range" min="10" max="100" step="1" value={quality}
                           onChange={e => setQuality(+e.target.value)} className="em-slider" />
                  </div>
                </>
              )}

              {/* Custom Effect Builder */}
              {animEffect === 'custom' && (
                <div style={{
                  marginTop: 12, padding: 14, borderRadius: 12,
                  background: 'var(--id-surface-alt, #f5f5f5)', border: '1px solid var(--id-border)',
                }}>
                  <div style={{ fontSize: 12, fontWeight: 700, marginBottom: 10, color: ACCENT }}>
                    Custom Effect Builder
                  </div>
                  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px 14px' }}>
                    {[
                      { key: 'moveX',     label: 'Move X',     min: -100, max: 100 },
                      { key: 'moveY',     label: 'Move Y',     min: -100, max: 100 },
                      { key: 'rotate',    label: 'Rotate',     min: -100, max: 100 },
                      { key: 'scale',     label: 'Scale',      min: -100, max: 100 },
                      { key: 'skew',      label: 'Skew',       min: -100, max: 100 },
                      { key: 'hueRotate', label: 'Hue Rotate', min: 0,    max: 100 },
                      { key: 'brightness',label: 'Brightness',  min: 0,    max: 100 },
                      { key: 'blur',      label: 'Blur',        min: 0,    max: 100 },
                    ].map(ctrl => (
                      <div key={ctrl.key}>
                        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, fontWeight: 600, color: 'var(--id-text-muted)' }}>
                          <span>{ctrl.label}</span>
                          <span>{customCfg[ctrl.key]}</span>
                        </div>
                        <input type="range" className="em-slider" min={ctrl.min} max={ctrl.max} step="5"
                          value={customCfg[ctrl.key]}
                          onChange={e => updateCustom(ctrl.key, +e.target.value)} />
                      </div>
                    ))}
                  </div>
                  <button className="btn btn-secondary" style={{ marginTop: 8, fontSize: 11, padding: '4px 12px' }}
                    onClick={() => setCustomCfg({ moveX: 0, moveY: 0, rotate: 0, scale: 0, skew: 0, hueRotate: 0, brightness: 0, blur: 0 })}>
                    Reset All
                  </button>
                </div>
              )}
            </div>
          </div>
        </div>

        {/* ══ Generate All Effects ══ */}
        <div style={{ marginTop: 24, padding: '20px 0', borderTop: '1px solid var(--id-border)' }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, flexWrap: 'wrap', gap: 8 }}>
            <div>
              <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--id-text)' }}>All Effects Gallery</div>
              <div className="cmp-meta">Generate every animation at once — preview and download all as ZIP.</div>
            </div>
            <div style={{ display: 'flex', gap: 8 }}>
              <button className="btn btn-primary" onClick={generateAll} disabled={allBusy}
                style={{ background: ACCENT, borderColor: ACCENT }}>
                <window.Icon name="sparkle" size={14} /> {allBusy ? 'Generating…' : 'Generate All Effects'}
              </button>
              {allEffects.length > 0 && (
                <button className="btn btn-secondary" onClick={downloadAllZip}>
                  <window.Icon name="download" size={14} /> Download ZIP ({allEffects.length})
                </button>
              )}
            </div>
          </div>

          {allEffects.length > 0 && (
            <div style={{
              display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(110px, 1fr))', gap: 10,
              padding: 16, background: 'var(--id-surface-alt, #f5f5f5)', borderRadius: 12,
              border: '1px solid var(--id-border)',
            }}>
              {allEffects.map((e, i) => (
                <div key={i} style={{ textAlign: 'center' }}>
                  <div style={{
                    background: 'repeating-conic-gradient(var(--id-surface) 0 25%, transparent 0 50%) 50%/12px 12px',
                    borderRadius: 10, padding: 6, border: '1px solid var(--id-border)',
                    display: 'flex', alignItems: 'center', justifyContent: 'center', aspectRatio: '1',
                  }}>
                    <img src={e.url} alt={e.label} style={{ width: '100%', height: '100%', objectFit: 'contain', borderRadius: 6 }} />
                  </div>
                  <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--id-text)', marginTop: 4 }}>{e.label}</div>
                  <div style={{ fontSize: 10, color: 'var(--id-text-muted)' }}>{window.fmtBytes(e.size)} · {e.ext.toUpperCase()}</div>
                  <button onClick={() => window.downloadBlob(e.blob, 'emoji-' + e.label.toLowerCase() + '.' + e.ext)}
                    style={{
                      marginTop: 4, fontSize: 10, fontWeight: 600, padding: '3px 8px', borderRadius: 6,
                      border: '1px solid var(--id-border)', background: 'var(--id-surface)', cursor: 'pointer',
                      color: ACCENT,
                    }}>
                    <window.Icon name="download" size={10} /> Save
                  </button>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    );
  }

  // ---------------------------------------------------------------------------
  // BATCH EDITOR
  // ---------------------------------------------------------------------------
  function BatchEditor() {
    const [files, setFiles] = useState([]);
    const [results, setResults] = useState([]); // { name, blob, url }
    const [processing, setProcessing] = useState(false);
    const [zipping, setZipping] = useState(false);

    // shared settings
    const [presetId, setPresetId] = useState('slack');
    const PRESETS = useMemo(() => [
      { id: 'slack',   label: 'Slack',   size: 128, maxKB: 128 },
      { id: 'discord', label: 'Discord', size: 256, maxKB: 256 },
      { id: 'twitch',  label: 'Twitch',  size: 112, maxKB: 25 },
    ], []);
    const [circleCrop, setCircleCrop] = useState(false);
    const [roundCorners, setRoundCorners] = useState(false);

    const preset = PRESETS.find(p => p.id === presetId);
    const outDim = preset.size;

    const handleFiles = (fileList) => {
      const arr = Array.from(fileList).filter(f => f.type.startsWith('image/'));
      setFiles(arr);
      setResults([]);
    };

    const processAll = async () => {
      if (!files.length) return;
      setProcessing(true);
      setResults([]);
      const out = [];
      for (const f of files) {
        try {
          const { img } = await window.loadImageFromFile(f);
          const c = document.createElement('canvas');
          c.width = outDim;
          c.height = outDim;
          const ctx = c.getContext('2d');

          if (circleCrop) {
            ctx.beginPath();
            ctx.arc(outDim / 2, outDim / 2, outDim / 2, 0, Math.PI * 2);
            ctx.clip();
          } else if (roundCorners) {
            const r = outDim * 0.18;
            ctx.beginPath();
            ctx.moveTo(r, 0); ctx.lineTo(outDim - r, 0);
            ctx.quadraticCurveTo(outDim, 0, outDim, r);
            ctx.lineTo(outDim, outDim - r);
            ctx.quadraticCurveTo(outDim, outDim, outDim - r, outDim);
            ctx.lineTo(r, outDim);
            ctx.quadraticCurveTo(0, outDim, 0, outDim - r);
            ctx.lineTo(0, r);
            ctx.quadraticCurveTo(0, 0, r, 0);
            ctx.closePath();
            ctx.clip();
          }

          const iw = img.width, ih = img.height;
          const ratio = Math.max(outDim / iw, outDim / ih);
          const dw = iw * ratio, dh = ih * ratio;
          ctx.drawImage(img, (outDim - dw) / 2, (outDim - dh) / 2, dw, dh);

          const blob = await new Promise(res => c.toBlob(res, 'image/png'));
          const name = f.name.replace(/\.[^.]+$/, '') + '-emoji.png';
          out.push({ name, blob, url: URL.createObjectURL(blob) });
        } catch (e) {
          console.error('Batch item failed:', f.name, e);
        }
      }
      setResults(out);
      setProcessing(false);
    };

    const downloadZip = async () => {
      if (!results.length) return;
      setZipping(true);
      try {
        await window.loadScript('https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.js');
        const zipData = {};
        for (const r of results) {
          const buf = await r.blob.arrayBuffer();
          zipData[r.name] = new Uint8Array(buf);
        }
        const zipped = fflate.zipSync(zipData);
        const blob = new Blob([zipped], { type: 'application/zip' });
        window.downloadBlob(blob, 'emoji-batch.zip');
      } catch (e) {
        console.error('ZIP failed:', e);
      } finally {
        setZipping(false);
      }
    };

    if (!files.length) {
      return (
        <div className="em-batch-drop">
          <label className="em-batch-label">
            <input type="file" multiple accept="image/*" style={{ display: 'none' }}
                   onChange={e => handleFiles(e.target.files)} />
            <div className="em-batch-inner">
              <window.Icon name="upload" size={28} />
              <div style={{ fontWeight: 700, fontSize: 15 }}>Drop multiple images here</div>
              <div style={{ fontSize: 13, color: 'var(--id-text-muted)' }}>or click to browse</div>
            </div>
          </label>
        </div>
      );
    }

    return (
      <div className="em-batch">
        <div className="em-section">
          <div className="em-section-title">
            <window.Icon name="grid" size={13} /> Platform
          </div>
          <div className="em-presets">
            {PRESETS.map(p => (
              <button key={p.id}
                className={'em-preset' + (presetId === p.id ? ' active' : '')}
                onClick={() => setPresetId(p.id)}>
                {p.label}
                <span className="em-dim">{p.size}px</span>
              </button>
            ))}
          </div>
        </div>

        <div className="em-section">
          <div className="em-section-title">Shape</div>
          <div className="em-toggle-row">
            <button className={'em-toggle' + (roundCorners ? ' on' : '')}
                    onClick={() => { setRoundCorners(!roundCorners); if (!roundCorners) setCircleCrop(false); }}>
              Round
            </button>
            <button className={'em-toggle' + (circleCrop ? ' on' : '')}
                    onClick={() => { setCircleCrop(!circleCrop); if (!circleCrop) setRoundCorners(false); }}>
              Circle
            </button>
          </div>
        </div>

        <div className="em-batch-info">
          {files.length} image{files.length !== 1 ? 's' : ''} selected
          <button className="btn btn-secondary" style={{ marginLeft: 'auto', padding: '6px 14px', fontSize: 12 }}
                  onClick={() => { setFiles([]); setResults([]); }}>
            Clear
          </button>
        </div>

        {results.length === 0 && (
          <button className="btn btn-primary em-btn-dl" onClick={processAll} disabled={processing}
                  style={{ width: '100%', marginTop: 12 }}>
            {processing ? 'Processing...' : 'Process all'}
          </button>
        )}

        {results.length > 0 && (
          <>
            <div className="em-batch-results">
              {results.map((r, i) => (
                <div key={i} className="em-batch-thumb">
                  <img src={r.url} alt={r.name} />
                  <span className="em-batch-name">{r.name}</span>
                </div>
              ))}
            </div>
            <button className="btn btn-primary em-btn-dl" onClick={downloadZip} disabled={zipping}
                    style={{ width: '100%', marginTop: 12 }}>
              <window.Icon name="download" size={16} />
              {zipping ? 'Zipping...' : 'Download ZIP'}
            </button>
          </>
        )}
      </div>
    );
  }

  // ---------------------------------------------------------------------------
  // Inline styles — prefixed em- to avoid collisions
  // ---------------------------------------------------------------------------
  function PageStyles() {
    return (
      <style>{`
        /* --- Animations ---- */
        @keyframes em-shake  { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-6px)} 75%{transform:translateX(6px)} }
        @keyframes em-bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-14px)} }
        @keyframes em-spin   { 0%{transform:rotate(0)} 100%{transform:rotate(360deg)} }
        @keyframes em-pulse  { 0%,100%{transform:scale(1)} 50%{transform:scale(1.18)} }
        @keyframes em-party  { 0%{filter:hue-rotate(0)} 100%{filter:hue-rotate(360deg)} }
        @keyframes em-wave   { 0%,100%{transform:translate(0,0)} 25%{transform:translate(5px,-3px)} 50%{transform:translate(-5px,3px)} 75%{transform:translate(5px,3px)} }
        @keyframes em-grow   { 0%{transform:scale(0)} 100%{transform:scale(1)} }
        @keyframes em-wobble { 0%,100%{transform:rotate(0)} 25%{transform:rotate(-12deg)} 75%{transform:rotate(12deg)} }
        @keyframes em-flip   { 0%{transform:scaleX(1)} 50%{transform:scaleX(-1)} 100%{transform:scaleX(1)} }
        @keyframes em-slide  { 0%,100%{transform:translateX(0)} 50%{transform:translateX(20px)} }
        @keyframes em-rubber { 0%,100%{transform:scale(1,1)} 30%{transform:scale(1.2,0.8)} 60%{transform:scale(0.8,1.2)} }
        @keyframes em-glitch { 0%{transform:translate(0,0)} 20%{transform:translate(-3px,2px)} 40%{transform:translate(3px,-2px)} 60%{transform:translate(-2px,-3px)} 80%{transform:translate(2px,3px)} 100%{transform:translate(0,0)} }
        @keyframes em-zoom   { 0%,100%{transform:scale(1)} 50%{transform:scale(1.35)} }
        @keyframes em-nod    { 0%,100%{transform:translateY(0) rotate(0)} 50%{transform:translateY(5px) rotate(5deg)} }
        @keyframes em-jelly  { 0%,100%{transform:scale(1,1) rotate(0)} 25%{transform:scale(1.1,0.9) rotate(3deg)} 50%{transform:scale(0.9,1.1) rotate(-3deg)} 75%{transform:scale(1.05,0.95) rotate(1deg)} }
        @keyframes em-orbit  { 0%{transform:translate(12px,0)} 25%{transform:translate(0,-12px)} 50%{transform:translate(-12px,0)} 75%{transform:translate(0,12px)} 100%{transform:translate(12px,0)} }
        @keyframes em-blink  { 0%,40%,100%{opacity:1} 50%,90%{opacity:0.15} }

        /* --- Page --- */
        .em-page { padding-bottom: 56px; }

        /* --- HERO --- */
        .em-hero {
          position: relative;
          margin: 24px 0 0;
          padding: 44px 32px 36px;
          border-radius: 22px;
          text-align: center;
          background:
            radial-gradient(800px 320px at 50% 0%, color-mix(in oklab, ${ACCENT} 22%, transparent), transparent 70%),
            linear-gradient(180deg,
              color-mix(in oklab, ${ACCENT} 5%, var(--id-surface)) 0%,
              var(--id-surface) 100%);
          border: 1px solid color-mix(in oklab, ${ACCENT} 20%, var(--id-border));
          overflow: hidden;
        }
        .em-hero-deco { position: absolute; inset: 0; pointer-events: none; }
        .em-hero-deco span { position: absolute; display: block; }
        .em-hero-deco .ehd-1 { top: 12%; left: 8%;  width: 16px; height: 16px; background: ${ACCENT}; border-radius: 999px; opacity: .5; }
        .em-hero-deco .ehd-2 { top: 22%; right: 10%; width: 20px; height: 20px; background: #f59e0b; border-radius: 4px; transform: rotate(18deg); opacity: .55; }
        .em-hero-deco .ehd-3 { top: 62%; left: 12%;  width: 14px; height: 14px; background: #2563eb; border-radius: 999px; opacity: .5; }
        .em-hero-deco .ehd-4 { top: 68%; right: 16%; width: 18px; height: 18px; background: #ec4899; border-radius: 4px; transform: rotate(40deg); opacity: .55; }
        .em-hero-deco .ehd-5 { top: 8%;  right: 28%; width: 12px; height: 12px; background: ${ACCENT}; border-radius: 999px; opacity: .6; }
        .em-hero-deco .ehd-6 { top: 78%; left: 30%;  width: 14px; height: 14px; background: #10b981; border-radius: 4px; transform: rotate(12deg); opacity: .45; }

        .em-hero-tag {
          display: inline-flex; align-items: center; gap: 8px;
          padding: 5px 12px; border-radius: 999px;
          background: ${ACCENT}; color: white;
          font-size: 10px; font-weight: 800; letter-spacing: 0.12em;
          margin-bottom: 16px;
          box-shadow: 0 6px 18px -6px color-mix(in oklab, ${ACCENT} 65%, transparent);
        }
        .em-hero-title {
          font-size: clamp(34px, 5.2vw, 60px); font-weight: 800;
          line-height: 1.04; letter-spacing: -0.03em;
          margin: 0 0 14px; max-width: 820px; margin-inline: auto;
          text-wrap: balance;
        }
        .em-hero-title .hl {
          display: inline-block; padding: 0 14px;
          background: ${ACCENT}; color: white;
          border-radius: 8px;
          transform: rotate(-1.2deg);
          margin: 0 4px;
        }
        .em-hero-sub {
          font-size: 15px; color: var(--id-text-muted);
          max-width: 580px; margin: 0 auto 22px; line-height: 1.55;
        }
        .em-hero-sub em { font-style: italic; color: ${ACCENT}; font-weight: 700; }
        .em-hero-ctas {
          display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;
          margin-bottom: 22px;
        }
        .em-btn-primary {
          background: ${ACCENT}; border-color: ${ACCENT};
          padding: 12px 22px; font-size: 15px; font-weight: 700;
          box-shadow: 0 14px 28px -12px color-mix(in oklab, ${ACCENT} 70%, transparent);
        }
        .em-btn-primary:hover { background: ${ACCENT_DEEP}; border-color: ${ACCENT_DEEP}; }
        .em-btn-secondary { padding: 12px 22px; font-size: 15px; font-weight: 600; }

        .em-hero-stats {
          display: inline-flex; gap: 22px;
          padding: 12px 22px; border-radius: 14px;
          background: var(--id-surface); border: 1px solid var(--id-border);
          box-shadow: var(--id-shadow-sm);
          flex-wrap: wrap; justify-content: center;
        }
        .ehs { text-align: center; }
        .ehs-v { font-size: 22px; font-weight: 800; color: ${ACCENT}; letter-spacing: -0.02em; line-height: 1; }
        .ehs-l { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--id-text-muted); margin-top: 3px; font-weight: 700; }

        /* --- TEMPLATE GALLERY --- */
        .em-templates { margin-top: 36px; text-align: center; }

        /* --- TOOL SECTION --- */
        .em-tool { margin-top: 56px; scroll-margin-top: 24px; }
        .em-tool.em-tool-top { margin-top: 32px; }
        .em-tool-head { text-align: center; margin-bottom: 18px; }
        .em-tool-sub { font-size: 14px; color: var(--id-text-muted); margin: 0; }
        .em-tool-frame {
          background: var(--id-surface);
          border: 1px solid var(--id-border-strong);
          border-radius: 16px;
          overflow: hidden;
          box-shadow: 0 18px 44px -22px color-mix(in oklab, ${ACCENT} 40%, transparent);
          padding: 24px;
        }

        /* Section heading */
        .em-h2 {
          font-size: clamp(22px, 2.4vw, 30px); font-weight: 800;
          letter-spacing: -0.02em; line-height: 1.2;
          text-align: center; margin: 0 0 22px;
          text-wrap: balance;
        }

        /* --- EDITOR --- */
        .em-editor {}
        .em-tab-bar {
          display: flex; gap: 4px; margin-bottom: 20px;
          padding: 4px; border-radius: 12px;
          background: var(--id-surface-alt);
          border: 1px solid var(--id-border);
        }
        .em-tab {
          flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
          padding: 10px 16px; border-radius: 9px;
          font-weight: 700; font-size: 13px; cursor: pointer;
          background: transparent; border: 1.5px solid transparent;
          color: var(--id-text-muted);
          transition: all 180ms var(--id-ease);
        }
        .em-tab:hover { color: var(--id-text); background: var(--id-surface); }
        .em-tab.active {
          background: var(--id-surface); color: ${ACCENT};
          border-color: var(--id-border);
          box-shadow: var(--id-shadow-xs);
        }

        /* Two-column layout */
        .em-layout {
          display: grid; grid-template-columns: 300px 1fr; gap: 24px;
          align-items: start;
        }
        @media (max-width: 800px) {
          .em-layout { grid-template-columns: 1fr; }
        }

        /* Preview column */
        .em-preview-col {
          position: sticky; top: 24px;
        }
        @media (max-width: 800px) {
          .em-preview-col { position: static; }
        }

        .em-preview-wrap {
          display: flex; align-items: center; justify-content: center;
          padding: 24px;
          background: repeating-conic-gradient(var(--id-surface-alt) 0 25%, var(--id-surface) 0 50%) 50%/16px 16px;
          border: 1px solid var(--id-border); border-radius: 14px;
          min-height: 256px;
        }
        .em-preview-wrap img {
          max-width: 256px; max-height: 256px;
          image-rendering: pixelated;
        }

        .em-info-row {
          display: flex; align-items: center; gap: 6px; margin-top: 10px;
          flex-wrap: wrap;
        }
        .em-badge {
          display: inline-flex; align-items: center; gap: 4px;
          height: 24px; padding: 0 10px; border-radius: 999px;
          font-weight: 700; font-size: 11px;
          background: var(--id-surface-alt); color: var(--id-text-muted);
        }

        .em-warn {
          display: flex; align-items: center; gap: 8px;
          padding: 10px 14px; margin-top: 10px; border-radius: 10px;
          background: var(--id-warning-soft); color: var(--id-warning);
          font-size: 12px; font-weight: 600;
        }

        .em-actions {
          display: flex; gap: 8px; margin-top: 14px;
        }
        .em-actions .btn { flex: 1; justify-content: center; }
        .em-btn-dl {
          background: ${ACCENT} !important; border-color: ${ACCENT} !important;
        }
        .em-btn-dl:hover { background: ${ACCENT_DEEP} !important; border-color: ${ACCENT_DEEP} !important; }
        .em-btn-dl:disabled { opacity: 0.5; cursor: not-allowed; }

        /* Controls column */
        .em-controls-col {
          display: flex; flex-direction: column; gap: 2px;
        }

        .em-section {
          padding: 14px 0;
          border-bottom: 1px solid var(--id-divider);
        }
        .em-section:last-child { border-bottom: none; }
        .em-section-title {
          display: flex; align-items: center; gap: 6px;
          font-size: 11px; font-weight: 800; text-transform: uppercase;
          letter-spacing: 0.07em; color: var(--id-text-muted);
          margin-bottom: 10px;
        }
        .em-section-sub {
          font-size: 11px; font-weight: 700; text-transform: uppercase;
          letter-spacing: 0.06em; color: var(--id-text-muted); margin-top: 12px; margin-bottom: 6px;
        }

        /* Presets */
        .em-presets { display: flex; gap: 6px; flex-wrap: wrap; }
        .em-preset {
          display: inline-flex; align-items: center; gap: 5px;
          height: 34px; padding: 0 14px; border-radius: 9px;
          font-weight: 600; font-size: 12px; cursor: pointer;
          background: var(--id-surface-alt); border: 1.5px solid var(--id-border);
          color: var(--id-text); transition: all 160ms var(--id-ease);
        }
        .em-preset:hover { border-color: var(--id-border-strong); }
        .em-preset.active {
          background: ${ACCENT}; color: white; border-color: ${ACCENT};
          box-shadow: 0 4px 12px -4px color-mix(in oklab, ${ACCENT} 50%, transparent);
        }
        .em-dim { font-size: 10px; opacity: 0.75; font-weight: 500; }

        /* Toggle buttons */
        .em-toggle-row { display: flex; flex-wrap: wrap; gap: 6px; }
        .em-toggle {
          display: inline-flex; align-items: center; gap: 5px;
          height: 32px; padding: 0 12px; border-radius: 8px;
          font-weight: 600; font-size: 12px; cursor: pointer;
          background: var(--id-surface-alt); border: 1.5px solid var(--id-border);
          color: var(--id-text-muted); transition: all 160ms var(--id-ease);
        }
        .em-toggle:hover { border-color: var(--id-border-strong); color: var(--id-text); }
        .em-toggle.on {
          background: color-mix(in oklab, ${ACCENT} 12%, var(--id-surface));
          border-color: ${ACCENT}; color: ${ACCENT};
        }

        .em-toggle-sm {
          margin-left: auto;
          display: inline-flex; align-items: center;
          height: 22px; padding: 0 8px; border-radius: 6px;
          font-weight: 800; font-size: 9px; letter-spacing: 0.08em;
          cursor: pointer;
          background: var(--id-surface-alt); border: 1.5px solid var(--id-border);
          color: var(--id-text-muted); transition: all 160ms var(--id-ease);
        }
        .em-toggle-sm.on {
          background: color-mix(in oklab, ${ACCENT} 14%, var(--id-surface));
          border-color: ${ACCENT}; color: ${ACCENT};
        }

        /* Text controls */
        .em-text-controls { display: flex; flex-direction: column; gap: 8px; }
        .em-input {
          width: 100%; height: 36px; padding: 0 12px;
          border: 1.5px solid var(--id-border); border-radius: 9px;
          background: var(--id-surface-alt); color: var(--id-text);
          font-size: 13px; font-weight: 500;
          outline: none; transition: border-color 160ms;
        }
        .em-input:focus { border-color: ${ACCENT}; }
        .em-text-row { display: flex; gap: 8px; align-items: flex-end; }
        .em-text-pos-row { display: flex; gap: 4px; }
        .em-pos-btn {
          height: 30px; padding: 0 12px; border-radius: 7px;
          font-weight: 600; font-size: 11px; cursor: pointer;
          background: var(--id-surface-alt); border: 1.5px solid var(--id-border);
          color: var(--id-text-muted); transition: all 160ms;
        }
        .em-pos-btn:hover { border-color: var(--id-border-strong); color: var(--id-text); }
        .em-pos-btn.active {
          background: color-mix(in oklab, ${ACCENT} 12%, var(--id-surface));
          border-color: ${ACCENT}; color: ${ACCENT};
        }

        /* Color input */
        .em-color-input {
          width: 36px; height: 36px; min-width: 36px;
          border: 1.5px solid var(--id-border-strong);
          border-radius: 9px; cursor: pointer; padding: 3px;
          background: none;
        }

        /* Border controls */
        .em-border-controls {
          display: flex; align-items: center; gap: 10px; margin-top: 8px;
          padding: 8px 12px; border-radius: 10px;
          background: var(--id-surface-alt); border: 1px solid var(--id-border);
        }

        /* Adjust grid */
        .em-adjust-grid {
          display: grid; grid-template-columns: 1fr 1fr; gap: 8px 14px; margin-top: 10px;
        }
        .em-adjust-grid > :last-child:nth-child(odd) {
          grid-column: 1 / -1;
        }

        /* Slider */
        .em-slider-wrap { margin-top: 4px; }
        .em-slider-label {
          display: flex; justify-content: space-between;
          font-size: 11px; font-weight: 600; color: var(--id-text-muted);
          margin-bottom: 4px;
        }
        .em-slider {
          width: 100%; height: 6px; -webkit-appearance: none; appearance: none;
          background: var(--id-border); border-radius: 3px; outline: none;
        }
        .em-slider::-webkit-slider-thumb {
          -webkit-appearance: none; appearance: none;
          width: 16px; height: 16px; border-radius: 50%;
          background: ${ACCENT}; cursor: pointer;
          border: 2px solid var(--id-surface);
          box-shadow: 0 1px 4px rgba(0,0,0,0.18);
        }
        .em-slider::-moz-range-thumb {
          width: 16px; height: 16px; border-radius: 50%;
          background: ${ACCENT}; cursor: pointer;
          border: 2px solid var(--id-surface);
          box-shadow: 0 1px 4px rgba(0,0,0,0.18);
        }

        /* Effects grid */
        .em-effects {
          display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 6px;
        }
        .em-fx-card {
          display: flex; flex-direction: column; align-items: center; justify-content: center;
          gap: 4px; padding: 10px 6px; border-radius: 10px; cursor: pointer;
          background: var(--id-surface-alt); border: 1.5px solid var(--id-border);
          color: var(--id-text); font-weight: 600; font-size: 11px;
          transition: all 160ms var(--id-ease); text-align: center;
        }
        .em-fx-card:hover { border-color: var(--id-border-strong); }
        .em-fx-card.active {
          background: color-mix(in oklab, ${ACCENT} 10%, var(--id-surface));
          border-color: ${ACCENT}; color: ${ACCENT};
          box-shadow: 0 2px 8px -2px color-mix(in oklab, ${ACCENT} 30%, transparent);
        }
        .em-fx-icon {
          width: 28px; height: 28px; border-radius: 7px;
          display: grid; place-items: center;
          background: var(--id-surface-sunken);
        }
        .em-fx-card.active .em-fx-icon { background: color-mix(in oklab, ${ACCENT} 15%, transparent); }

        /* Speed buttons */
        .em-speed-row { display: flex; gap: 4px; }
        .em-speed-btn {
          height: 28px; padding: 0 12px; border-radius: 7px;
          font-weight: 600; font-size: 11px; cursor: pointer;
          background: var(--id-surface-alt); border: 1.5px solid var(--id-border);
          color: var(--id-text-muted); transition: all 160ms var(--id-ease);
        }
        .em-speed-btn:hover { border-color: var(--id-border-strong); color: var(--id-text); }
        .em-speed-btn.active { background: ${ACCENT}; color: white; border-color: ${ACCENT}; }

        /* --- PLATFORM GUIDE --- */
        .em-platforms { margin-top: 48px; }
        .em-plat-grid {
          display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
        }
        @media (max-width: 900px) { .em-plat-grid { grid-template-columns: repeat(2, 1fr); } }
        @media (max-width: 520px) { .em-plat-grid { grid-template-columns: 1fr; } }
        .em-plat-card {
          display: flex; flex-direction: column; gap: 8px;
          padding: 18px 16px;
          background: var(--id-surface); border: 1px solid var(--id-border);
          border-radius: 12px;
          transition: border-color 180ms, transform 180ms;
        }
        .em-plat-card:hover { transform: translateY(-2px); border-color: var(--id-border-strong); }
        .em-plat-dot { width: 8px; height: 8px; border-radius: 999px; }
        .em-plat-name { font-size: 16px; font-weight: 800; color: var(--id-text); margin: 0; }
        .em-plat-specs { font-size: 12px; color: var(--id-text-muted); line-height: 1.7; }
        .em-plat-specs strong { color: var(--id-text); font-weight: 700; }
        .em-plat-note { font-size: 12px; color: var(--id-text-muted); margin: 0; line-height: 1.45; }

        /* --- HOW IT WORKS --- */
        .em-how { margin-top: 48px; }
        .em-how-grid {
          display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;
        }
        @media (max-width: 760px) { .em-how-grid { grid-template-columns: 1fr; } }
        .em-how-step {
          display: flex; gap: 14px; align-items: start;
          padding: 20px;
          background: var(--id-surface); border: 1px solid var(--id-border);
          border-radius: 14px;
        }
        .em-how-step div { display: flex; flex-direction: column; gap: 4px; }
        .em-how-step strong { font-size: 14px; font-weight: 800; color: var(--id-text); }
        .em-how-step span { font-size: 13px; color: var(--id-text-muted); line-height: 1.5; }
        .em-step-num {
          width: 28px; height: 28px; min-width: 28px; border-radius: 8px;
          background: color-mix(in oklab, ${ACCENT} 12%, var(--id-surface));
          color: ${ACCENT};
          display: grid; place-items: center;
          font-weight: 800; font-size: 13px;
        }

        /* --- FAQ --- */
        .em-faq { margin-top: 48px; }
        .em-faq-grid {
          display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
        }
        @media (max-width: 760px) { .em-faq-grid { grid-template-columns: 1fr; } }
        .em-faq-item {
          padding: 12px 16px;
          background: var(--id-surface); border: 1px solid var(--id-border);
          border-radius: 10px;
          transition: border-color 180ms;
        }
        .em-faq-item[open] { border-color: var(--id-border-strong); }
        .em-faq-item summary {
          cursor: pointer;
          font-size: 14px; font-weight: 700;
          color: var(--id-text);
          list-style: none;
          display: flex; align-items: center; justify-content: space-between;
          padding-right: 6px;
        }
        .em-faq-item summary::-webkit-details-marker { display: none; }
        .em-faq-item summary::after {
          content: '+';
          font-size: 20px; font-weight: 300;
          color: ${ACCENT};
          transition: transform 180ms;
        }
        .em-faq-item[open] summary::after { content: '\\2212'; }
        .em-faq-item p {
          margin: 8px 0 0; font-size: 13px; color: var(--id-text-muted);
          line-height: 1.55;
        }

        /* --- BATCH --- */
        .em-batch-drop {
          border: 2px dashed var(--id-border);
          border-radius: 14px;
          transition: border-color 180ms;
        }
        .em-batch-drop:hover { border-color: ${ACCENT}; }
        .em-batch-label {
          display: flex; cursor: pointer;
        }
        .em-batch-inner {
          flex: 1;
          display: flex; flex-direction: column; align-items: center; justify-content: center;
          gap: 6px; padding: 40px 20px;
          color: var(--id-text-muted);
        }
        .em-batch { }
        .em-batch-info {
          display: flex; align-items: center; gap: 8px;
          padding: 10px 14px; margin-top: 12px;
          background: var(--id-surface-alt); border: 1px solid var(--id-border);
          border-radius: 10px;
          font-size: 13px; font-weight: 600; color: var(--id-text);
        }
        .em-batch-results {
          display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 8px;
          margin-top: 12px;
        }
        .em-batch-thumb {
          display: flex; flex-direction: column; align-items: center; gap: 4px;
          padding: 8px;
          background: var(--id-surface-alt); border: 1px solid var(--id-border);
          border-radius: 10px;
        }
        .em-batch-thumb img {
          width: 56px; height: 56px; object-fit: contain;
          image-rendering: pixelated;
        }
        .em-batch-name {
          font-size: 9px; font-weight: 600; color: var(--id-text-muted);
          max-width: 72px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        }

        /* --- Mobile (<=640) --- */
        @media (max-width: 640px) {
          .em-page { padding-bottom: 40px; }
          .em-hero {
            margin-top: 16px; padding: 32px 18px 26px; border-radius: 16px;
          }
          .em-hero-title { font-size: 32px; line-height: 1.05; margin-bottom: 12px; }
          .em-hero-title .hl { padding: 0 8px; }
          .em-hero-sub { font-size: 14px; margin-bottom: 18px; }
          .em-hero-tag { font-size: 9px; padding: 4px 10px; letter-spacing: 0.1em; margin-bottom: 12px; }
          .em-hero-ctas { flex-direction: column; gap: 8px; margin-bottom: 18px; }
          .em-hero-ctas .btn { width: 100%; justify-content: center; }
          .em-hero-stats {
            width: 100%; gap: 0; padding: 12px; flex-wrap: nowrap; justify-content: space-around;
          }
          .ehs { flex: 1; min-width: 0; }
          .ehs-v { font-size: 18px; }
          .ehs-l { font-size: 9px; letter-spacing: 0.06em; }

          .em-tool { margin-top: 32px; }
          .em-tool.em-tool-top { margin-top: 24px; }
          .em-tool-frame { padding: 16px; border-radius: 12px; }

          .em-platforms, .em-how, .em-faq { margin-top: 36px; }
          .em-h2 { font-size: 22px; margin-bottom: 16px; }

          .em-preview-wrap { min-height: 200px; padding: 16px; }
          .em-preview-wrap img { max-width: 200px; max-height: 200px; }

          .em-how-step { padding: 16px; }
          .em-how-step strong { font-size: 13px; }
          .em-how-step span { font-size: 12px; }

          .em-plat-card { padding: 14px; }
          .em-faq-item { padding: 10px 14px; }
          .em-faq-item summary { font-size: 13px; }
          .em-faq-item p { font-size: 12px; }
        }

        /* Very small (<=380) */
        @media (max-width: 380px) {
          .em-hero-stats { flex-direction: column; gap: 8px; }
          .ehs { width: 100%; }
        }
      `}</style>
    );
  }

  // ---------------------------------------------------------------------------
  // Template Gallery — canvas-drawn emoji templates users can pick & customize
  // ---------------------------------------------------------------------------
  const TEMPLATE_CATS = [
    { id: 'all', label: 'All' },
    { id: 'faces', label: 'Faces' },
    { id: 'blobs', label: 'Blobs' },
    { id: 'text', label: 'Text' },
    { id: 'shapes', label: 'Shapes' },
    { id: 'memes', label: 'Memes' },
  ];

  const TEMPLATES = [
    // Faces
    { id: 'smile',      cat: 'faces', label: '😊', emoji: '😊' },
    { id: 'laugh',      cat: 'faces', label: '😂', emoji: '😂' },
    { id: 'heart-eyes', cat: 'faces', label: '😍', emoji: '😍' },
    { id: 'cool',       cat: 'faces', label: '😎', emoji: '😎' },
    { id: 'think',      cat: 'faces', label: '🤔', emoji: '🤔' },
    { id: 'cry',        cat: 'faces', label: '😢', emoji: '😢' },
    { id: 'angry',      cat: 'faces', label: '😡', emoji: '😡' },
    { id: 'shock',      cat: 'faces', label: '😱', emoji: '😱' },
    { id: 'wink',       cat: 'faces', label: '😜', emoji: '😜' },
    { id: 'skull',      cat: 'faces', label: '💀', emoji: '💀' },
    { id: 'clown',      cat: 'faces', label: '🤡', emoji: '🤡' },
    { id: 'fire-face',  cat: 'faces', label: '🔥', emoji: '🔥' },
    // Blobs
    { id: 'blob-happy', cat: 'blobs', label: '🟢 Happy',   color: '#4ade80', mouth: 'smile' },
    { id: 'blob-sad',   cat: 'blobs', label: '🔵 Sad',     color: '#60a5fa', mouth: 'sad' },
    { id: 'blob-angry', cat: 'blobs', label: '🔴 Angry',   color: '#f87171', mouth: 'angry' },
    { id: 'blob-love',  cat: 'blobs', label: '💜 Love',    color: '#c084fc', mouth: 'love' },
    { id: 'blob-cool',  cat: 'blobs', label: '🟡 Cool',    color: '#fbbf24', mouth: 'cool' },
    { id: 'blob-shock', cat: 'blobs', label: '🟠 Shock',   color: '#fb923c', mouth: 'shock' },
    // Text
    { id: 'text-ok',    cat: 'text', label: 'OK',    text: 'OK',    bg: '#4ade80' },
    { id: 'text-no',    cat: 'text', label: 'NO',    text: 'NO',    bg: '#f87171' },
    { id: 'text-yes',   cat: 'text', label: 'YES',   text: 'YES',   bg: '#60a5fa' },
    { id: 'text-lol',   cat: 'text', label: 'LOL',   text: 'LOL',   bg: '#fbbf24' },
    { id: 'text-gg',    cat: 'text', label: 'GG',    text: 'GG',    bg: '#c084fc' },
    { id: 'text-brb',   cat: 'text', label: 'BRB',   text: 'BRB',   bg: '#f472b6' },
    { id: 'text-lgtm',  cat: 'text', label: 'LGTM',  text: 'LGTM',  bg: '#34d399' },
    { id: 'text-ship',  cat: 'text', label: '🚀',    text: '🚀',    bg: '#818cf8' },
    // Shapes
    { id: 'star',       cat: 'shapes', label: '⭐', emoji: '⭐' },
    { id: 'heart-red',  cat: 'shapes', label: '❤️', emoji: '❤️' },
    { id: 'sparkle',    cat: 'shapes', label: '✨', emoji: '✨' },
    { id: 'check-mark', cat: 'shapes', label: '✅', emoji: '✅' },
    { id: 'cross-mark', cat: 'shapes', label: '❌', emoji: '❌' },
    { id: 'hundred',    cat: 'shapes', label: '💯', emoji: '💯' },
    // Memes
    { id: 'thumbs-up',  cat: 'memes', label: '👍', emoji: '👍' },
    { id: 'thumbs-dn',  cat: 'memes', label: '👎', emoji: '👎' },
    { id: 'eyes',       cat: 'memes', label: '👀', emoji: '👀' },
    { id: 'brain',      cat: 'memes', label: '🧠', emoji: '🧠' },
    { id: 'rocket',     cat: 'memes', label: '🚀', emoji: '🚀' },
    { id: 'trophy',     cat: 'memes', label: '🏆', emoji: '🏆' },
    { id: 'money',      cat: 'memes', label: '💰', emoji: '💰' },
    { id: 'party',      cat: 'memes', label: '🎉', emoji: '🎉' },
  ];

  function renderTemplate(t) {
    const size = 128;
    const c = document.createElement('canvas');
    c.width = size; c.height = size;
    const ctx = c.getContext('2d');

    if (t.emoji) {
      // Emoji-based template — draw emoji as large text
      ctx.fillStyle = '#f0f0f0';
      ctx.beginPath();
      ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
      ctx.fill();
      ctx.font = (size * 0.7) + 'px "Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(t.emoji, size / 2, size / 2 + 2);
    } else if (t.text) {
      // Text template — colored circle + bold text
      ctx.fillStyle = t.bg || '#333';
      ctx.beginPath();
      ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
      ctx.fill();
      const fs = t.text.length > 3 ? size * 0.32 : size * 0.42;
      ctx.font = '900 ' + fs + 'px system-ui, -apple-system, sans-serif';
      ctx.fillStyle = '#ffffff';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(t.text, size / 2, size / 2 + 2);
    } else if (t.color) {
      // Blob template — colored blob shape + face
      ctx.fillStyle = t.color;
      ctx.beginPath();
      ctx.arc(size / 2, size / 2, size * 0.44, 0, Math.PI * 2);
      ctx.fill();
      // Eyes
      ctx.fillStyle = '#fff';
      ctx.beginPath(); ctx.arc(size * 0.37, size * 0.4, 8, 0, Math.PI * 2); ctx.fill();
      ctx.beginPath(); ctx.arc(size * 0.63, size * 0.4, 8, 0, Math.PI * 2); ctx.fill();
      ctx.fillStyle = '#333';
      ctx.beginPath(); ctx.arc(size * 0.38, size * 0.41, 4, 0, Math.PI * 2); ctx.fill();
      ctx.beginPath(); ctx.arc(size * 0.64, size * 0.41, 4, 0, Math.PI * 2); ctx.fill();
      // Mouth
      ctx.strokeStyle = '#333'; ctx.lineWidth = 3; ctx.lineCap = 'round';
      ctx.beginPath();
      if (t.mouth === 'smile') { ctx.arc(size / 2, size * 0.52, 14, 0.1 * Math.PI, 0.9 * Math.PI); }
      else if (t.mouth === 'sad') { ctx.arc(size / 2, size * 0.65, 12, 1.15 * Math.PI, 1.85 * Math.PI); }
      else if (t.mouth === 'angry') { ctx.moveTo(size * 0.38, size * 0.62); ctx.lineTo(size * 0.62, size * 0.62); }
      else if (t.mouth === 'love') { ctx.font = '28px sans-serif'; ctx.fillStyle = '#e11d48'; ctx.textAlign = 'center'; ctx.fillText('♥', size / 2, size * 0.65); }
      else if (t.mouth === 'cool') { ctx.moveTo(size * 0.38, size * 0.6); ctx.quadraticCurveTo(size / 2, size * 0.68, size * 0.62, size * 0.6); }
      else if (t.mouth === 'shock') { ctx.arc(size / 2, size * 0.6, 8, 0, Math.PI * 2); }
      ctx.stroke();
    }

    return c;
  }

  function TemplateGallery({ onSelect }) {
    const [cat, setCat] = useState('all');

    const filtered = cat === 'all' ? TEMPLATES : TEMPLATES.filter(t => t.cat === cat);

    return (
      <div>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 14, justifyContent: 'center' }}>
          {TEMPLATE_CATS.map(c => (
            <button key={c.id}
              className={'filter-pill' + (cat === c.id ? ' active' : '')}
              style={cat === c.id ? { background: ACCENT, borderColor: ACCENT } : {}}
              onClick={() => setCat(c.id)}>
              {c.label}
            </button>
          ))}
        </div>
        <div style={{
          display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(72px, 1fr))', gap: 10,
          maxHeight: 340, overflowY: 'auto', padding: '4px 2px',
        }}>
          {filtered.map(t => (
            <TemplateCard key={t.id} tmpl={t} onSelect={onSelect} />
          ))}
        </div>
      </div>
    );
  }

  function TemplateCard({ tmpl, onSelect }) {
    const canvasRef = useRef(null);
    const rendered = useRef(null);

    useEffect(() => {
      const c = renderTemplate(tmpl);
      rendered.current = c;
      const ctx = canvasRef.current?.getContext('2d');
      if (ctx) {
        canvasRef.current.width = 64;
        canvasRef.current.height = 64;
        ctx.drawImage(c, 0, 0, 64, 64);
      }
    }, [tmpl]);

    return (
      <button onClick={() => { if (rendered.current) onSelect(rendered.current); }}
        title={tmpl.label}
        style={{
          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
          padding: 8, borderRadius: 10, border: '1.5px solid var(--id-border)',
          background: 'var(--id-surface)', cursor: 'pointer',
          transition: 'all 150ms ease',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.borderColor = ACCENT; e.currentTarget.style.transform = 'scale(1.08)'; }}
        onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--id-border)'; e.currentTarget.style.transform = 'scale(1)'; }}>
        <canvas ref={canvasRef} width={64} height={64} style={{ width: 48, height: 48, borderRadius: 6 }} />
        <span style={{ fontSize: 10, fontWeight: 600, color: 'var(--id-text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 64 }}>
          {tmpl.label}
        </span>
      </button>
    );
  }

  window.EmojiMakerPage = EmojiMakerPage;
})();
