// Main landing app — header, hero, category cards, popular tools, footer

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

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "primaryColor": "#2563eb",
  "highlightColor": "#ea580c",
  "cardStyle": "flat",
  "cornerRadius": 14,
  "density": "comfortable"
}/*EDITMODE-END*/;

// Supabase — replaces the old Express/Passport backend.
// The client is loaded as a UMD script in index.html (window.supabase).
// See supabase/ for SQL migration + Edge Function source.
const SB_URL = 'https://ghggcuapwvwiegjntsnj.supabase.co';
const SB_ANON_KEY = 'sb_publishable_5_ZxflWkt0bosRXfY0BwiA_VfFZgA_U';
const SB_FN_BASE = `${SB_URL}/functions/v1`;

// Cached client. Returns null if the UMD script hasn't loaded yet.
let _sbClient = null;
function getSupabase() {
  if (!SB_URL || !SB_ANON_KEY) return null;
  if (_sbClient) return _sbClient;
  const sb = window.supabase;
  if (!sb || typeof sb.createClient !== 'function') {
    console.warn('[minimagics] supabase-js not loaded yet — is the CDN reachable?');
    return null;
  }
  _sbClient = sb.createClient(SB_URL, SB_ANON_KEY, {
    auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: true },
  });
  return _sbClient;
}
// Expose for debugging from the console.
window.getSupabase = getSupabase;

// ---------- Telemetry: tool_open + tool_complete ----------
// Tools call window.mmTrackComplete(toolId, { durationMs, success }) when they
// finish successful work. Drop-off = opens without a matching complete in the
// admin analytics. Anonymous users still log (user_id is null) so home-page
// engagement is visible to admins.
const _openTimers = new Map(); // toolId → { startedAt }
// GC stale timers every 5 minutes (e.g. user opened tool but never completed).
setInterval(() => {
  const cutoff = performance.now() - 30 * 60 * 1000; // 30 min
  for (const [k, v] of _openTimers) if (v.startedAt < cutoff) _openTimers.delete(k);
}, 5 * 60 * 1000);

window.mmTrackOpen = function mmTrackOpen(toolId) {
  // Always start the timer locally (used to compute duration even if the
  // user hasn't consented yet — the duration just isn't sent).
  _openTimers.set(toolId, { startedAt: performance.now() });
  if (!window.MMConsent?.has('analytics')) return;
  const sb = getSupabase(); if (!sb) return;
  sb.from('tool_usage').insert({ tool_id: toolId, event_type: 'open' }).then(() => {});
};
window.mmTrackComplete = function mmTrackComplete(toolId, opts = {}) {
  const t = _openTimers.get(toolId);
  let durationMs = opts.durationMs;
  if (durationMs == null && t) durationMs = Math.round(performance.now() - t.startedAt);
  _openTimers.delete(toolId);
  if (!window.MMConsent?.has('analytics')) return;
  const sb = getSupabase(); if (!sb) return;
  sb.from('tool_usage').insert({
    tool_id: toolId,
    event_type: 'complete',
    duration_ms: durationMs ?? null,
    success: opts.success !== false,
  }).then(() => {});
};

// _pendingToolRef is a useRef inside App() — see below.

// ---------- Routing ----------
// Real path URLs for SEO + shareability:
//   /                    home
//   /tools/<slug>        single tool (modal or fullscreen)
//   /sports              sports landing
//   /dashboard, /admin   workspaces
//   /privacy /terms /contact
// Legacy hash routes (`#/dashboard`, `#/tool/X`, …) are still parsed so
// existing bookmarks keep working — the canonical URL is always the path.
function routeFromLocation() {
  // Path first.
  const path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
  const seg  = path.split('/').filter(Boolean);
  if (seg.length === 0) {
    // Could still be a hash-only legacy URL like /#/dashboard — try hash.
    const h = (window.location.hash || '').replace(/^#\/?/, '');
    if (h) return { name: legacyName(h.split('/')[0]), toolId: h.startsWith('tool/') ? h.slice(5) : null };
    return { name: 'home', toolId: null };
  }
  if (seg[0] === 'tools' && seg[1])     return { name: 'tool',     toolId: seg[1] };
  if (seg[0] === 'sports')              return { name: 'sports',   toolId: null };
  if (seg[0] === 'face-recognition')    return { name: 'face',     toolId: null };
  if (seg[0] === 'dashboard')           return { name: 'dashboard',toolId: null };
  if (seg[0] === 'admin')          return { name: 'admin',    toolId: null };
  if (seg[0] === 'privacy')        return { name: 'privacy',  toolId: null };
  if (seg[0] === 'terms')          return { name: 'terms',    toolId: null };
  if (seg[0] === 'contact')        return { name: 'contact',  toolId: null };
  return { name: 'home', toolId: null };
}
function legacyName(head) {
  if (head === 'dashboard') return 'dashboard';
  if (head === 'admin')     return 'admin';
  if (head === 'tool')      return 'tool';
  if (head === 'sports')    return 'sports';
  if (head === 'face-recognition') return 'face';
  if (head === 'privacy')   return 'privacy';
  if (head === 'terms')     return 'terms';
  if (head === 'contact')   return 'contact';
  return 'home';
}
// Path builder for any internal target. Accepts either a string ('home',
// 'sports', …) or `{ tool: '<slug>' }` for tool URLs. Returns absolute path.
function pathFor(target) {
  if (target == null || target === 'home' || target === '') return '/';
  if (typeof target === 'string') return '/' + target;
  if (target.tool) return '/tools/' + encodeURIComponent(target.tool);
  return '/';
}
// Push a path into the URL bar without a full page reload. The popstate
// event handler in App() picks it up and re-renders.
function navigate(target, opts = {}) {
  const path = typeof target === 'string' && target.startsWith('/') ? target : pathFor(target);
  if (path === window.location.pathname) {
    // Re-render anyway in case a tool re-opens.
    window.dispatchEvent(new PopStateEvent('popstate'));
    return;
  }
  if (opts.replace) history.replaceState(null, '', path);
  else              history.pushState(null, '', path);
  // popstate doesn't fire on pushState — emit one ourselves so listeners react.
  window.dispatchEvent(new PopStateEvent('popstate'));
}
window.mmNavigate = navigate;
window.mmPathFor  = pathFor;

// One-time legacy bookmark redirect: if someone arrives via `/#/foo`, rewrite
// the URL to `/foo` so canonical is set before render. Doesn't reload.
(function migrateLegacyHash() {
  const h = (window.location.hash || '').replace(/^#\/?/, '');
  if (!h) return;
  const head = h.split('/')[0];
  let target;
  if (h.startsWith('tool/')) target = '/tools/' + h.slice(5);
  else if (['dashboard','admin','sports','privacy','terms','contact'].includes(head)) target = '/' + head;
  if (target) {
    history.replaceState(null, '', target);
  }
})();

// ---------- Tool pricing helpers ----------
// Premium/pro tools require matching entitlement. Free tools are open to everyone.
function isToolLocked(toolId, pricing, entitlements) {
  if (!pricing) return true;  // fail-closed: lock until pricing loads
  const p = pricing[toolId];
  if (!p || !p.active || p.tier === 'free') return false;
  return !entitlements.includes(p.tier);
}
function toolTierOf(toolId, pricing) {
  if (!pricing) return 'free';
  return pricing[toolId]?.tier || 'free';
}
// True when this tool is gated behind sign-in. Premium tools always require
// auth (entitlement is per-user); free tools require auth only when the
// admin explicitly flagged it.
function toolRequiresAuth(toolId, pricing) {
  if (!pricing) return false;
  const p = pricing[toolId];
  if (!p) return false;
  if (p.tier === 'premium') return true;
  return p.requires_auth === true;
}

function App() {
  const [locale, t] = window.useI18n();
  const [tweaks, setTweak] = (window.useTweaks || (() => [TWEAK_DEFAULTS, () => {}]))(TWEAK_DEFAULTS);
  const [theme, setTheme] = useState(() => localStorage.getItem('mm-theme') || 'light');
  const [filter, setFilter] = useState('all');
  const [query, setQuery] = useState('');
  const [activeTool, setActiveTool] = useState(null);
  const [openMenu, setOpenMenu] = useState(null);
  const [favorites, setFavorites] = useState(() => { try { return new Set(JSON.parse(localStorage.getItem('mm-favs') || '[]')); } catch { return new Set(); } });
  const [recent, setRecent] = useState(() => { try { return JSON.parse(localStorage.getItem('mm-recent') || '["compress-image","qr-generator","password-gen"]'); } catch { return ['compress-image','qr-generator','password-gen']; } });
  const [user, setUser] = useState(null);
  const [role, setRole] = useState('user');                 // 'user' | 'admin'
  const [roleLoaded, setRoleLoaded] = useState(false);     // true once role RPC resolves
  const [entitlements, setEntitlements] = useState([]);     // ['premium'] subset
  const [pricing, setPricing] = useState(null);              // null until loaded; { toolId: { tier, price_cents, active } }
  const [routeInfo, setRouteInfo] = useState(() => routeFromLocation());
  const route = routeInfo.name;
  const [paywallTool, setPaywallTool] = useState(null);     // tool blocked by paywall
  const [signInOpen, setSignInOpen] = useState(false);
  const [featuredToolId, setFeaturedToolId] = useState('compress-image');
  // Admin-managed SEO overrides keyed by route/tool. Shape:
  //   { "home": { title, description, og_image, keywords, noindex }, ..., "tool:<id>": {...} }
  const [seoOverrides, setSeoOverrides] = useState({});
  const searchRef = useRef(null);
  // Pending click — when a user clicks an auth-required tool while signed-out
  // we open the sign-in modal and stash the tool here. After sign-in success
  // the App resumes the click.
  const _pendingToolRef = useRef(null);

  // ---------- Supabase auth + sync (optional) ----------
  // Pick up the current session on mount and subscribe to changes. When a user
  // signs in, merge local favorites/recent with whatever's on the server.
  useEffect(() => {
    const sb = getSupabase(); if (!sb) return;
    sb.auth.getSession().then(({ data }) => setUser(data.session?.user || null));
    const { data: { subscription } } = sb.auth.onAuthStateChange((_evt, session) => {
      setUser(session?.user || null);
    });
    return () => subscription?.unsubscribe();
  }, []);
  const _syncTimers = useRef({});
  const syncPush = (kind, data) => {
    if (!user) return;
    clearTimeout(_syncTimers.current[kind]);
    _syncTimers.current[kind] = setTimeout(async () => {
      const sb = getSupabase(); if (!sb) return;
      try {
        await sb.from('sync').upsert({ user_id: user.id, kind, data, updated_at: new Date().toISOString() });
      } catch {}
    }, 1000);
  };
  useEffect(() => {
    if (!user) return;
    const sb = getSupabase(); if (!sb) return;
    sb.from('sync').select('kind, data').in('kind', ['favorites', 'recent']).then(({ data }) => {
      if (!Array.isArray(data)) return;
      for (const row of data) {
        if (row.kind === 'favorites' && Array.isArray(row.data)) {
          const merged = new Set([...favorites, ...row.data]);
          setFavorites(merged);
          localStorage.setItem('mm-favs', JSON.stringify([...merged]));
        } else if (row.kind === 'recent' && Array.isArray(row.data)) {
          const merged = [...row.data, ...recent].filter((v, i, a) => a.indexOf(v) === i).slice(0, 4);
          setRecent(merged);
          localStorage.setItem('mm-recent', JSON.stringify(merged));
        }
      }
    });
    // Pull saved-calculator entries from the same `sync` table.
    window.MMSaves?.pull();
  }, [user?.id]);
  useEffect(() => { if (user) syncPush('favorites', [...favorites]); }, [favorites, user?.id]);
  useEffect(() => { if (user) syncPush('recent', recent); }, [recent, user?.id]);

  // ---------- Hash routing listener ----------
  useEffect(() => {
    const onNav = () => setRouteInfo(routeFromLocation());
    window.addEventListener('popstate', onNav);
    window.addEventListener('hashchange', onNav); // legacy
    return () => {
      window.removeEventListener('popstate', onNav);
      window.removeEventListener('hashchange', onNav);
    };
  }, []);

  // ---------- Load tool metadata + pricing (DB-driven) ----------
  // list_tools() is public; admins manage rows in the Admin Panel. We merge
  // DB metadata onto the baked-in handlers and keep pricing in sync from the
  // same response so the paywall has fresh tier/price data.
  useEffect(() => {
    const sb = getSupabase(); if (!sb) return;
    sb.rpc('list_tools').then(({ data, error }) => {
      if (error || !Array.isArray(data)) return;
      const byId = {};
      for (const r of data) byId[r.tool_id] = r;
      // Merge into window.TOOLS via the helper in tools-data.jsx so all
      // components using window.useTools() re-render.
      window.refreshTools?.();
      // Pricing map for paywall + auth-gate logic.
      const map = {};
      for (const r of data) map[r.tool_id] = {
        tier: r.tier, price_cents: r.price_cents, active: r.active,
        stripe_price_id: r.stripe_price_id,
        requires_auth: r.requires_auth === true,
      };
      setPricing(map);
    });
    // Load ALL settings in a single query and distribute values locally.
    sb.from('settings').select('key, value').then(({ data }) => {
      if (!Array.isArray(data)) return;
      const m = {};
      for (const r of data) m[r.key] = r.value;
      // Featured tool
      const ft = m.featured_tool;
      if (ft && typeof ft === 'string') setFeaturedToolId(ft);
      // SEO overrides
      if (m['seo.overrides'] && typeof m['seo.overrides'] === 'object') setSeoOverrides(m['seo.overrides']);
      // Photo-finder backend URL
      const pfUrl = m['photo_finder.backend_url'];
      const url = (typeof pfUrl === 'string') ? pfUrl : (typeof pfUrl === 'object' && pfUrl?.url) ? pfUrl.url : null;
      if (url) window.MM_PHOTO_FINDER_URL = url;
      // Photo-finder paywall toggle
      window.MM_PF_PAYWALL = m['photo_finder.paywall_enabled'] === true;
      // AdSense config
      window.MM_ADS_CONFIG = {
        enabled: m['ads.enabled'] === true,
        clientId: m['ads.client_id'] || '',
        slots: {
          leaderboard: m['ads.slot_leaderboard'] || '',
          ingrid: m['ads.slot_ingrid'] || '',
          modal: m['ads.slot_modal'] || '',
          footer: m['ads.slot_footer'] || '',
        },
      };
      try { window.dispatchEvent(new Event('mm-ads-ready')); } catch {}
    });
  }, []);
  useEffect(() => {
    if (!user) { setRole('user'); setRoleLoaded(false); setEntitlements([]); window.MM_IS_ADMIN = false; return; }
    const sb = getSupabase(); if (!sb) return;
    sb.from('user_roles').select('role').eq('user_id', user.id).maybeSingle()
      .then(({ data }) => {
        const r = data?.role || 'user';
        setRole(r);
        setRoleLoaded(true);
        // Expose for in-tool components that don't get role as a prop
        // (e.g. photo-finder.jsx's diagnostic mode).
        window.MM_IS_ADMIN = r === 'admin';
      });
    sb.from('entitlements').select('tier, expires_at').eq('user_id', user.id).then(({ data }) => {
      if (!Array.isArray(data)) return;
      const now = new Date();
      const tiers = data.filter((e) => !e.expires_at || new Date(e.expires_at) > now).map((e) => e.tier);
      setEntitlements(tiers);
      window.MM_USER_ENTITLEMENTS = tiers;
    });
  }, [user?.id]);

  // ---------- Guard + redirect for protected routes ----------
  useEffect(() => {
    if (route === 'dashboard' && !user) navigate('home');
    if (route === 'admin' && roleLoaded && role !== 'admin') navigate('home');
  }, [route, user?.id, role, roleLoaded]);

  // ---------- SEO meta sync per route ----------
  // Computes the default record per route, then merges any admin-managed
  // override on top. Empty-string overrides count as "use default" (so the
  // editor can clear a field without nuking it). Updates <title>, meta
  // description, canonical, OG, Twitter, hreflang, JSON-LD.
  useEffect(() => {
    const seo = window.mmSetSeo;
    if (!seo) return;
    const origin = location.origin;
    const merge = (key, base) => {
      const o = seoOverrides[key];
      if (!o || typeof o !== 'object') return base;
      const out = { ...base };
      if (o.title       && o.title.trim())       out.title       = o.title.trim();
      if (o.description && o.description.trim()) out.description = o.description.trim();
      if (o.keywords    && o.keywords.trim())    out.keywords    = o.keywords.trim();
      if (o.og_image    && o.og_image.trim())    out.ogImage     = o.og_image.trim();
      if (o.noindex === true)                    out.noindex     = true;
      return out;
    };

    if (route === 'tool' && routeInfo.toolId) {
      const tool = window.TOOLS.find((x) => x.id === routeInfo.toolId);
      if (tool) {
        const cat = window.CATEGORIES.find((c) => c.id === tool.cat);
        const name = window.tTool(tool, 'name') || tool.name;
        const desc = window.tTool(tool, 'desc') || tool.desc;
        const base = {
          title: name,
          description: `${desc} — runs entirely in your browser, no upload required.`,
          canonical: origin + '/tools/' + tool.id,
          type: 'website',
          ld: {
            '@context': 'https://schema.org',
            '@type': 'SoftwareApplication',
            name,
            description: desc,
            applicationCategory: cat?.name || 'Utility',
            operatingSystem: 'Web',
            url: origin + '/tools/' + tool.id,
            offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
            publisher: { '@type': 'Organization', name: 'MiniMagics', url: origin },
            inLanguage: 'en-US',
          },
        };
        seo(merge('tool:' + tool.id, base));
        return;
      }
    }
    if (route === 'sports') {
      seo(merge('sports', {
        title: 'Sports tools — pace, GPX, training',
        description: 'Free running and sports calculators: pace, race predictor, VO₂max, heart-rate zones, GPX viewer, activity share cards. All in-browser.',
        canonical: origin + '/sports',
      }));
      return;
    }
    if (route === 'face') {
      seo(merge('face-recognition', {
        title: 'Find yourself in the crowd — Free Face Recognition',
        description: 'Drop a selfie, paste a gallery URL, get every photo of you. Free face recognition for race finishers, weddings, conferences and corporate events. 100% private — runs entirely in your browser.',
        canonical: origin + '/face-recognition',
        keywords: 'face recognition, find me in photos, race photos, marathon photos, event photos, wedding photos, free face recognition, ai photo finder, find yourself in crowd',
        ld: {
          '@context': 'https://schema.org',
          '@type': 'SoftwareApplication',
          name: 'MiniMagics Face Recognition',
          alternateName: 'Find yourself in the crowd',
          description: 'AI face recognition that finds every photo of you in any gallery. Drop a selfie, paste a URL, get your photos. Runs in your browser — no upload, no signup.',
          applicationCategory: 'PhotographyApplication',
          operatingSystem: 'Web',
          url: origin + '/face-recognition',
          offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
          publisher: { '@type': 'Organization', name: 'MiniMagics', url: origin },
          inLanguage: 'en-US',
          aggregateRating: { '@type': 'AggregateRating', ratingValue: '4.9', ratingCount: '128' },
          featureList: [
            'Find yourself in race photo galleries',
            'Match faces across wedding albums',
            'Surface your conference appearances',
            'Bib-number OCR for marathon shots',
            '100% local processing — no upload',
          ],
        },
      }));
      return;
    }
    if (route === 'dashboard') return seo({ title: 'Dashboard', noindex: true, canonical: origin + '/dashboard' });
    if (route === 'admin')     return seo({ title: 'Admin',     noindex: true, canonical: origin + '/admin' });
    if (route === 'privacy')   return seo(merge('privacy', { title: 'Privacy Policy', description: 'How MiniMagics handles your data.',     canonical: origin + '/privacy' }));
    if (route === 'terms')     return seo(merge('terms',   { title: 'Terms of Service', description: 'MiniMagics terms of use.',             canonical: origin + '/terms' }));
    if (route === 'contact')   return seo(merge('contact', { title: 'Contact',          description: 'Get in touch with MiniMagics.',        canonical: origin + '/contact' }));

    // Home (default)
    seo(merge('home', {
      title: undefined,                  // → "MiniMagics — Free tools…"
      description: '70+ privacy-first tools for PDFs, images, GPX and running. Free, no upload, no signup. Everything runs in your browser.',
      canonical: origin + '/',
      ld: {
        '@context': 'https://schema.org',
        '@type': 'WebSite',
        name: 'MiniMagics',
        url: origin + '/',
        inLanguage: 'en-US',
        potentialAction: {
          '@type': 'SearchAction',
          target: origin + '/?q={search_term_string}',
          'query-input': 'required name=search_term_string',
        },
      },
    }));
  }, [route, routeInfo.toolId, seoOverrides]);

  // openTool is the user-facing intent: click came from a card, dropdown,
  // search, etc. We run the synchronous gates (paywall + auth) and, if they
  // pass, push the canonical /tools/<id> URL. The route effect below opens
  // the modal/fullscreen and fires telemetry; that way direct URL hits
  // (Google → /tools/X) and clicks share the same code path.
  const openTool = (tool) => {
    if (!tool) { navigate('home'); return; }
    // Face Recognition has its own marketing landing page — never open in a
    // modal. Anyone clicking the tool card lands on /face-recognition.
    if (tool.id === 'photo-finder') {
      navigate('face-recognition');
      return;
    }
    if (isToolLocked(tool.id, pricing, entitlements)) {
      setPaywallTool(tool);
      return;
    }
    if (!user && toolRequiresAuth(tool.id, pricing)) {
      setSignInOpen(true);
      _pendingToolRef.current = tool;
      return;
    }
    setRecent((prev) => {
      const next = [tool.id, ...prev.filter((id) => id !== tool.id)].slice(0, 4);
      localStorage.setItem('mm-recent', JSON.stringify(next));
      return next;
    });
    if (location.pathname === '/tools/' + tool.id) {
      // Already at the target URL (e.g. clicking the same tool again) — set
      // activeTool directly since popstate won't fire.
      if (!tool.fullscreen) setActiveTool(tool);
    } else {
      navigate({ tool: tool.id });
    }
  };

  // Route → open. Runs on direct URL hits, popstate (back/forward), and any
  // navigation that changed the path. Re-applies paywall/auth gates because
  // a logged-out user might have shared a /tools/<premium> URL.
  useEffect(() => {
    if (routeInfo.name !== 'tool' || !routeInfo.toolId) {
      // Route moved away from a tool — make sure the modal is closed.
      if (activeTool) setActiveTool(null);
      return;
    }
    // Wait for pricing to load so gate decisions aren't wrong.
    if (getSupabase() && pricing === null) return;
    const tool = window.TOOLS.find((t) => t.id === routeInfo.toolId);
    if (!tool) { navigate('home', { replace: true }); return; }
    if (isToolLocked(tool.id, pricing, entitlements)) {
      setPaywallTool(tool);
      navigate('home', { replace: true });
      return;
    }
    if (!user && toolRequiresAuth(tool.id, pricing)) {
      setSignInOpen(true);
      _pendingToolRef.current = tool;
      navigate('home', { replace: true });
      return;
    }
    // Track once per route change for this tool.
    if (!activeTool || activeTool.id !== tool.id) window.mmTrackOpen?.(tool.id);
    if (!tool.fullscreen) setActiveTool(tool);
    else if (activeTool) setActiveTool(null); // fullscreen path renders elsewhere
  }, [routeInfo.name, routeInfo.toolId, pricing, user?.id, entitlements.join(',')]);
  const toggleFav = (id) => {
    setFavorites(prev => {
      const n = new Set(prev);
      if (n.has(id)) n.delete(id); else n.add(id);
      localStorage.setItem('mm-favs', JSON.stringify([...n]));
      return n;
    });
  };

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem('mm-theme', theme);
  }, [theme]);

  useEffect(() => {
    const r = document.documentElement.style;
    r.setProperty('--id-brand-blue', tweaks.primaryColor);
    r.setProperty('--id-brand-blue-hover', tweaks.primaryColor);
    r.setProperty('--tt-highlight', tweaks.highlightColor);
    r.setProperty('--tt-card-radius', tweaks.cornerRadius + 'px');
    document.documentElement.dataset.density = tweaks.density === 'comfortable' ? '' : tweaks.density;
  }, [tweaks]);

  useEffect(() => {
    const h = (e) => { if (!e.target.closest('.nav-link')) setOpenMenu(null); };
    document.addEventListener('click', h);
    return () => document.removeEventListener('click', h);
  }, []);

  // '/' focuses search, Esc clears query
  useEffect(() => {
    const onKey = (e) => {
      const inField = ['INPUT','TEXTAREA'].includes(document.activeElement.tagName);
      if (e.key === '/' && !inField && !activeTool) { e.preventDefault(); searchRef.current?.focus(); searchRef.current?.select(); }
      if (e.key === 'Escape' && inField && document.activeElement === searchRef.current) { setQuery(''); searchRef.current.blur(); }
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [activeTool]);

  // Home page hides sports tools — they live under the dedicated /sports page.
  const homeTools = window.TOOLS.filter((t) => t.cat !== 'sports');

  const filtered = useMemo(() => {
    let list = homeTools;
    if (filter !== 'all') list = list.filter(x => x.cat === filter);
    if (query.trim()) {
      const q = query.toLowerCase();
      list = list.filter(x => {
        const name = (window.tTool(x, 'name') || '').toLowerCase();
        const desc = (window.tTool(x, 'desc') || '').toLowerCase();
        return name.includes(q) || desc.includes(q) ||
               x.name.toLowerCase().includes(q) || x.desc.toLowerCase().includes(q);
      });
    }
    return list;
  }, [filter, query, locale]);

  const catCounts = useMemo(() => {
    const m = {};
    window.CATEGORIES.forEach(c => { m[c.id] = window.TOOLS.filter(t => t.cat === c.id).length; });
    return m;
  }, [locale]);

  const favTools = useMemo(() => [...favorites].map(id => window.TOOLS.find(t => t.id === id)).filter(Boolean), [favorites]);

  // Render dashboard / admin overlays as full screens (they own the whole viewport).
  if (route === 'dashboard' && user && window.UserDashboard) {
    return <window.UserDashboard user={user} role={role} entitlements={entitlements}
             pricing={pricing} favorites={favorites} toggleFav={toggleFav} recent={recent}
             onOpenTool={openTool} onClose={() => navigate('home')} />;
  }
  if (route === 'admin' && !roleLoaded && user) {
    return <div style={{ textAlign: 'center', padding: '60px 20px' }}><window.LoadingCard label="Checking permissions…" /></div>;
  }
  if (route === 'admin' && roleLoaded && role === 'admin' && window.AdminPanel) {
    return <window.AdminPanel user={user} pricing={pricing} setPricing={setPricing}
             onClose={() => navigate('home')} />;
  }
  // Fullscreen tool route: /tools/<slug> where tool.fullscreen === true.
  // Modal-style tools fall through and render the home grid; the route
  // effect above sets activeTool so the modal opens on top.
  if (route === 'tool') {
    const id = routeInfo.toolId;
    const fsTool = id && window.TOOLS.find((x) => x.id === id);
    const Handler = fsTool && window.TOOL_HANDLERS?.[fsTool.id];
    if (fsTool && fsTool.fullscreen && Handler) {
      const cat = window.CATEGORIES.find((c) => c.id === fsTool.cat);
      return (
        <div className="fs-tool">
          <Handler tool={fsTool} cat={cat} fullscreen={true} onExit={() => navigate('home')} />
        </div>
      );
    }
    // Modal tools: fall through to home; the route effect already triggered
    // setActiveTool so the modal will render.
  }
  // Dedicated Sports landing page (#/sports). Wraps SportsPage in the same
  // TopNav + Footer chrome as the home page so category dropdowns and the
  // sign-in button are still reachable.
  if (route === 'sports' && window.SportsPage) {
    return (
      <>
        <TopNav theme={theme} setTheme={setTheme} query={query} setQuery={setQuery}
                openMenu={openMenu} setOpenMenu={setOpenMenu} onToolClick={openTool} searchRef={searchRef}
                user={user} setUser={setUser} role={role} onSignInClick={() => setSignInOpen(true)}
                currentRoute="sports" />
        <window.SportsPage
          theme={theme}
          query={query}
          favorites={favorites} toggleFav={toggleFav}
          pricing={pricing} entitlements={entitlements}
          onOpenTool={openTool} />
        <Footer />

        {activeTool && <ToolModal tool={activeTool} onClose={() => navigate("home")} onFavorite={toggleFav} isFavorite={favorites.has(activeTool.id)} />}
        {paywallTool && window.Paywall && (
          <window.Paywall tool={paywallTool} tier={toolTierOf(paywallTool.id, pricing)}
            priceCents={pricing?.[paywallTool.id]?.price_cents || 0}
            hasPriceId={!!pricing?.[paywallTool.id]?.stripe_price_id}
            signedIn={!!user}
            onSignIn={async () => { const sb = getSupabase(); if (sb) await sb.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: window.location.origin + window.location.pathname } }); }}
            onCheckout={async () => {
              const sb = getSupabase(); if (!sb) return;
              const { data: session } = await sb.auth.getSession();
              const jwt = session?.session?.access_token;
              if (!jwt) return alert(t('paywall.please_sign_in'));
              const res = await fetch(SB_FN_BASE + '/stripe-checkout', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
                body: JSON.stringify({ tier: toolTierOf(paywallTool.id, pricing) }),
              });
              const out = await res.json().catch(() => ({}));
              if (!res.ok) return alert(out.detail || out.error || t('paywall.checkout_failed'));
              if (out.url) window.location.href = out.url;
            }}
            onClose={() => setPaywallTool(null)} />
        )}
        {signInOpen && <SignInModal onClose={() => setSignInOpen(false)}
                                   pendingTool={_pendingToolRef.current}
                                   onSignedIn={(u) => {
                                     setUser(u);
                                     setSignInOpen(false);
                                     // Resume the pending tool click, if any.
                                     const pending = _pendingToolRef.current;
                                     _pendingToolRef.current = null;
                                     if (pending) setTimeout(() => openTool(pending), 100);
                                   }} />}
      </>
    );
  }
  // Dedicated Face Recognition landing page (#/face-recognition).
  if (route === 'face' && window.FaceRecognitionPage) {
    return (
      <>
        <TopNav theme={theme} setTheme={setTheme} query={query} setQuery={setQuery}
                openMenu={openMenu} setOpenMenu={setOpenMenu} onToolClick={openTool} searchRef={searchRef}
                user={user} setUser={setUser} role={role} onSignInClick={() => setSignInOpen(true)}
                currentRoute="face" />
        <window.FaceRecognitionPage theme={theme} />
        <Footer />
        {signInOpen && <SignInModal onClose={() => setSignInOpen(false)}
                                   pendingTool={_pendingToolRef.current}
                                   onSignedIn={(u) => {
                                     setUser(u); setSignInOpen(false);
                                     const pending = _pendingToolRef.current;
                                     _pendingToolRef.current = null;
                                     if (pending) setTimeout(() => openTool(pending), 100);
                                   }} />}
      </>
    );
  }

  // Static legal pages.
  if (route === 'privacy' && window.PrivacyPage) return <window.PrivacyPage />;
  if (route === 'terms'   && window.TermsPage)   return <window.TermsPage />;
  if (route === 'contact' && window.ContactPage) return <window.ContactPage />;

  return (
    <>
      <TopNav theme={theme} setTheme={setTheme} query={query} setQuery={setQuery}
              openMenu={openMenu} setOpenMenu={setOpenMenu} onToolClick={openTool} searchRef={searchRef}
              user={user} setUser={setUser} role={role} onSignInClick={() => setSignInOpen(true)} />

      <main className="container">
        <RecentBar recent={recent} onClick={openTool} />
        <Hero onSurprise={() => openTool(homeTools[Math.floor(Math.random() * homeTools.length)])} />
        <CategoryCards counts={catCounts} onPick={(id) => { setFilter(id); document.getElementById('popular').scrollIntoView({ behavior: 'smooth', block: 'start' }); }} />
        <FeaturedTool toolId={featuredToolId} onOpen={() => { const t = window.TOOLS.find(x => x.id === featuredToolId); if (t) openTool(t); }} />

        {/* Ad slot: leaderboard — between featured tool and tool grid */}
        {window.AdSlot && <window.AdSlot slot="leaderboard" style={{ margin: '20px 0' }} />}

        {favTools.length > 0 && (
          <section className="popular" style={{ paddingBottom: 0 }}>
            <div className="popular-head" style={{ marginBottom: 20 }}>
              <h2 style={{ fontSize: 28 }}>{t('favorites.heading')}</h2>
              <p>{t('favorites.sub')}</p>
            </div>
            <div className="tools-grid">
              {favTools.map(x => <ToolCard key={x.id} tool={x} style={tweaks.cardStyle} isFav={true} onFav={toggleFav} onClick={() => openTool(x)} tier={toolTierOf(x.id, pricing)} locked={isToolLocked(x.id, pricing, entitlements)}
                        authNeeded={!user && pricing?.[x.id]?.requires_auth === true && (pricing?.[x.id]?.tier || 'free') === 'free'} />)}
            </div>
          </section>
        )}

        <section id="popular" className="popular">
          <div className="popular-head">
            <h2>{t('popular.heading')}</h2>
            <p>{t('popular.sub')}</p>
          </div>

          <FilterRow filter={filter} setFilter={setFilter} counts={catCounts} />

          <div className="tools-grid">
            {filtered.length === 0 ? (
              <div className="empty-state">
                <div className="em-icon"><Icon name="search" size={48} strokeWidth={1.25} /></div>
                <div style={{ fontWeight: 700, marginBottom: 4, color: 'var(--id-text)' }}>{t('empty.title', { query })}</div>
                <div>{t('empty.hint')}</div>
              </div>
            ) : filtered.map((x, i) => (
              <React.Fragment key={x.id}>
                <ToolCard tool={x} style={tweaks.cardStyle} isFav={favorites.has(x.id)} onFav={toggleFav} onClick={() => openTool(x)}
                          tier={toolTierOf(x.id, pricing)} locked={isToolLocked(x.id, pricing, entitlements)}
                          authNeeded={!user && pricing?.[x.id]?.requires_auth === true && (pricing?.[x.id]?.tier || 'free') === 'free'} />
                {/* In-grid native ad after every 8th tool */}
                {window.AdSlot && (i + 1) % 8 === 0 && i < filtered.length - 1 && <window.AdSlot slot="ingrid" />}
              </React.Fragment>
            ))}
          </div>
        </section>
      </main>

      {/* Ad slot: footer banner */}
      {window.AdSlot && <div className="container" style={{ marginBottom: 20 }}><window.AdSlot slot="footer" /></div>}

      <Footer />

      {activeTool && <ToolModal tool={activeTool} onClose={() => navigate("home")} onFavorite={toggleFav} isFavorite={favorites.has(activeTool.id)} />}
      {paywallTool && window.Paywall && (
        <window.Paywall tool={paywallTool} tier={toolTierOf(paywallTool.id, pricing)}
          priceCents={pricing?.[paywallTool.id]?.price_cents || 0}
          hasPriceId={!!pricing?.[paywallTool.id]?.stripe_price_id}
          signedIn={!!user}
          onSignIn={async () => { const sb = getSupabase(); if (sb) await sb.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: window.location.origin + window.location.pathname } }); }}
          onCheckout={async () => {
            const sb = getSupabase(); if (!sb) return;
            const { data: session } = await sb.auth.getSession();
            const jwt = session?.session?.access_token;
            if (!jwt) return alert(t('paywall.please_sign_in'));
            const res = await fetch(SB_FN_BASE + '/stripe-checkout', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
              body: JSON.stringify({ tier: toolTierOf(paywallTool.id, pricing) }),
            });
            const out = await res.json().catch(() => ({}));
            if (!res.ok) return alert(out.detail || out.error || t('paywall.checkout_failed'));
            if (out.url) window.location.href = out.url;
          }}
          onClose={() => setPaywallTool(null)} />
      )}

      {signInOpen && <SignInModal onClose={() => setSignInOpen(false)}
                                   pendingTool={_pendingToolRef.current}
                                   onSignedIn={(u) => {
                                     setUser(u);
                                     setSignInOpen(false);
                                     // Resume the pending tool click, if any.
                                     const pending = _pendingToolRef.current;
                                     _pendingToolRef.current = null;
                                     if (pending) setTimeout(() => openTool(pending), 100);
                                   }} />}

      <TweaksShell tweaks={tweaks} setTweak={setTweak} />
    </>
  );
}

/* ---------- Top nav ---------- */
function TopNav({ theme, setTheme, query, setQuery, openMenu, setOpenMenu, onToolClick, searchRef, user, setUser, role, onSignInClick, currentRoute = 'home' }) {
  const [, t] = window.useI18n();
  const signOut = async () => {
    const sb = getSupabase(); if (!sb) return;
    await sb.auth.signOut();
    setUser(null);
  };
  return (
    <nav className="nav-top">
      <div className="container nav-inner">
        <a href="/" className="brand-logo" onClick={(e) => { e.preventDefault(); if (location.pathname === '/') window.scrollTo({ top: 0, behavior: 'smooth' }); else navigate('home'); }}>
          <div className="mark"><Icon name="sparkle" size={18} strokeWidth={2} /></div>
          <div className="name">MiniMagics<small>{t('nav.brand_sub')}</small></div>
        </a>

        <div className="nav-links">
          {window.CATEGORIES.filter(c => c.id !== 'sports').map(c => (
            <button key={c.id}
              className={`nav-link ${openMenu === c.id ? 'open' : ''}`}
              onClick={(e) => { e.stopPropagation(); setOpenMenu(openMenu === c.id ? null : c.id); }}>
              {window.tCat(c, 'short')} <Icon name="chevron" size={14} />
              <div className="nav-dropdown">
                {window.TOOLS.filter(x => x.cat === c.id).slice(0, 8).map(x => (
                  <div key={x.id} className="nav-dropdown-item" onClick={() => { onToolClick(x); setOpenMenu(null); }}>
                    <div className="dot" style={{ background: c.soft, color: c.tint }}>
                      <Icon name={x.icon} size={14} />
                    </div>
                    <span>{window.tTool(x, 'name')}</span>
                  </div>
                ))}
              </div>
            </button>
          ))}
          {/* Sports — opens its own dedicated landing page rather than a dropdown. */}
          <button className={`nav-link nav-link-sports ${currentRoute === 'sports' ? 'is-active' : ''}`}
                  onClick={(e) => { e.stopPropagation(); setOpenMenu(null); window.mmNavigate('sports'); }}>
            <Icon name="run" size={14} strokeWidth={2} style={{ opacity: 1, color: '#16a34a' }} />
            <span>Sports</span>
          </button>
        </div>

        <div className="nav-right">
          <label className="nav-search">
            <Icon name="search" size={16} />
            <input ref={searchRef} placeholder={t('nav.search_placeholder', { count: window.TOOLS.length })} value={query} onChange={(e) => setQuery(e.target.value)}
                   aria-label={t('nav.search_placeholder', { count: window.TOOLS.length })}
                   aria-keyshortcuts="/" />
            <kbd style={{ marginLeft: 4 }} aria-hidden="true">/</kbd>
          </label>
          <button className="icon-btn" onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} aria-label={t('nav.toggle_theme')}>
            <Icon name={theme === 'light' ? 'moon' : 'sun'} size={16} />
          </button>
          <button className="icon-btn" aria-label={t('nav.share')} onClick={() => {
            const url = location.href;
            const title = document.title;
            if (navigator.share) {
              navigator.share({ title, url }).catch(() => {});
            } else {
              navigator.clipboard.writeText(url).then(() => {
                const el = document.createElement('div');
                el.textContent = 'Link copied!';
                Object.assign(el.style, { position:'fixed',top:'20px',left:'50%',transform:'translateX(-50%)',background:'var(--id-surface-raised)',color:'var(--id-text)',padding:'8px 16px',borderRadius:'8px',fontSize:'13px',fontWeight:'600',boxShadow:'0 2px 8px rgba(0,0,0,.15)',zIndex:'9999' });
                document.body.appendChild(el);
                setTimeout(() => el.remove(), 2000);
              });
            }
          }}><Icon name="share" size={16} /></button>
          {user
            ? <UserMenu user={user} role={role} onSignOut={signOut} />
            : <button className="btn-signin" onClick={onSignInClick}>{t('nav.sign_in')}</button>}
        </div>
      </div>
    </nav>
  );
}

/* ---------- Signed-in user menu (avatar dropdown) ---------- */
function UserMenu({ user, role, onSignOut }) {
  const [, t] = window.useI18n();
  const [open, setOpen] = useState(false);
  const rootRef = useRef(null);

  const meta = user.user_metadata || {};
  const avatar = meta.avatar_url || meta.picture;
  // Username (set during email signup) takes precedence over Google's full_name
  // and the email fallback. Surfaces in the menu trigger and the panel header.
  const display = meta.username || meta.display_name || meta.full_name || meta.name || user.email || t('nav.account_fallback');
  const firstName = display.split(' ')[0];

  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (!rootRef.current?.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  return (
    <div className="user-menu" ref={rootRef}>
      <button className="btn-signin user-menu-trigger" onClick={() => setOpen((v) => !v)}
              aria-expanded={open} aria-haspopup="true" title={user.email || display}>
        {avatar && <img src={avatar} alt={display} className="user-menu-avatar" />}
        {firstName}
        <Icon name="chevron" size={14} style={{ marginLeft: 4, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
      </button>

      {open && (
        <div className="user-menu-panel" role="menu">
          <div className="user-menu-header">
            {avatar
              ? <img src={avatar} alt={display} className="user-menu-avatar-lg" />
              : <div className="user-menu-avatar-lg user-menu-avatar-fallback">{firstName[0]?.toUpperCase() || '?'}</div>}
            <div style={{ minWidth: 0 }}>
              <div className="user-menu-name">{display}</div>
              {/* Show email below the username only if we don't already have a custom username
                  (e.g. Google sign-in users keep email visible; email/password users see only username). */}
              {user.email && !meta.username && <div className="user-menu-email">{user.email}</div>}
            </div>
          </div>

          <div className="user-menu-section">
            <button className="user-menu-item" onClick={() => { setOpen(false); navigate('dashboard'); }}>
              <Icon name="grid" size={14} />
              <span>{t('nav.my_dashboard')}</span>
            </button>
            {role === 'admin' && (
              <button className="user-menu-item" onClick={() => { setOpen(false); navigate('admin'); }}>
                <Icon name="bolt" size={14} />
                <span>{t('nav.admin_panel')}</span>
                <span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--id-brand-blue)', fontWeight: 700, letterSpacing: '0.08em' }}>{t('nav.admin_badge')}</span>
              </button>
            )}
          </div>

          {/* Developer/API key section disabled — re-enable by restoring this block. */}

          <div className="user-menu-section">
            <button className="user-menu-item user-menu-item-danger" onClick={() => { setOpen(false); onSignOut(); }}>
              <Icon name="x" size={14} />
              <span>{t('nav.sign_out')}</span>
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

/* ---------- Recent bar ---------- */
function RecentBar({ recent, onClick }) {
  const [, t] = window.useI18n();
  const items = recent.map(id => window.TOOLS.find(x => x.id === id)).filter(Boolean);
  if (items.length === 0) return null;
  return (
    <div className="recent-bar">
      <span className="label">{t('recent.label')}</span>
      {items.map(x => (
        <button key={x.id} className="recent-chip" onClick={() => onClick(x)}>
          <Icon name={x.icon} size={12} /> {window.tTool(x, 'name')}
        </button>
      ))}
      <span className="kbd-hint muted">{t('recent.kbd_hint_before')} <kbd>/</kbd> {t('recent.kbd_hint_after')}</span>
    </div>
  );
}

/* ---------- Hero ---------- */
function Hero({ onSurprise }) {
  const [, t] = window.useI18n();
  return (
    <section className="hero">
      <div className="hero-deco">
        <span className="d1" /><span className="d2" /><span className="d3" />
        <span className="d4" /><span className="d5" /><span className="d6" /><span className="d7" />
      </div>
      <h1 className="hero-title">
        {t('hero.title_plain_start')} <span className="hl">{t('hero.title_highlight')}</span> {t('hero.title_plain_end')}
      </h1>
      <p className="hero-sub">{t('hero.sub')}</p>
      <div style={{ textAlign: 'center', marginTop: 8 }}>
        <button className="btn btn-secondary" onClick={onSurprise}>
          <Icon name="sparkle" size={16} /> {t('hero.surprise')}
        </button>
      </div>
    </section>
  );
}

/* ---------- Featured Tool (Tool of the Month) ---------- */
function FeaturedTool({ toolId, onOpen }) {
  const [, t] = window.useI18n();
  const tool = window.TOOLS.find((x) => x.id === toolId);
  if (!tool) return null;
  // The locale strings still describe photo-finder copy. When the admin
  // points the featured slot at a different tool, fall back to that tool's
  // own name + description so the card never looks misleading.
  const heroTitle = toolId === 'photo-finder' ? t('featured.title') : (window.tTool(tool, 'name') || tool.name);
  const heroDesc  = toolId === 'photo-finder' ? t('featured.desc')  : (window.tTool(tool, 'desc')  || tool.desc);
  return (
    <section className="featured-tool" onClick={onOpen} role="button" tabIndex={0}
             onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(); } }}>
      <div className="ft-left">
        <div className="ft-label">
          <Icon name="sparkle" size={12} strokeWidth={2.2} />
          <span>{t('featured.label')}</span>
        </div>
        <h2 className="ft-title">{heroTitle}</h2>
        <p className="ft-desc">{heroDesc}</p>
        <div className="ft-ctas">
          <button className="btn btn-primary ft-btn" onClick={(e) => { e.stopPropagation(); onOpen(); }}>
            <Icon name={tool.icon} size={16} /> Try {heroTitle}
          </button>
          <span className="ft-meta">
            <Icon name="lock" size={13} />
            <span>{t('featured.meta')}</span>
          </span>
        </div>
      </div>
      <div className="ft-visual" aria-hidden="true">
        <div className="ft-ring ft-ring-1" />
        <div className="ft-ring ft-ring-2" />
        <div className="ft-ring ft-ring-3" />
        <div className="ft-core">
          <Icon name={tool.icon} size={56} strokeWidth={1.5} />
        </div>
        <span className="ft-chip ft-chip-top">{t('featured.chip_ai')}</span>
        <span className="ft-chip ft-chip-bot">{t('featured.chip_private')}</span>
      </div>
    </section>
  );
}

/* ---------- Category cards ---------- */
function CategoryCards({ counts, onPick }) {
  const [, t] = window.useI18n();
  return (
    <section className="cat-row">
      {window.CATEGORIES.filter(c => c.id !== 'sports').map(c => (
        <button key={c.id} className="cat-card" style={{ '--cat-bg': c.tint }} onClick={() => onPick(c.id)}>
          <div className="cat-head">
            <div className="cat-icon">
              <Icon name={c.id === 'pdf' ? 'doc' : c.id === 'image' ? 'image' : c.id === 'converter' ? 'swap' : 'archive'} size={22} />
            </div>
            <span className="cat-count">{t('card.cat_suffix', { count: counts[c.id] })}</span>
          </div>
          <div>
            <div className="cat-title">{window.tCat(c, 'name')}</div>
            <div className="cat-sub">{window.tCat(c, 'desc')}</div>
            <div className="cat-featured">
              <span className="lab">{t('card.featured_label')}</span>
              <span className="tool">{window.tCat(c, 'featured')}</span>
            </div>
          </div>
          <div className="cat-arrow"><Icon name="arrow" size={14} strokeWidth={2} /></div>
        </button>
      ))}
    </section>
  );
}

/* ---------- Filter row ---------- */
function FilterRow({ filter, setFilter, counts }) {
  const [, t] = window.useI18n();
  const all = window.TOOLS.filter(x => x.cat !== 'sports').length;
  return (
    <div className="filter-row">
      <button className={`filter-pill ${filter === 'all' ? 'active' : ''}`} onClick={() => setFilter('all')}>
        <span className="fpc-icon"><Icon name="grid" size={14} /></span>
        {t('popular.all_tools')} <span style={{ opacity: 0.6, fontWeight: 500 }}>· {all}</span>
      </button>
      {window.CATEGORIES.filter(c => c.id !== 'sports').map(c => (
        <button key={c.id} className={`filter-pill ${filter === c.id ? 'active' : ''}`} onClick={() => setFilter(c.id)}
          style={filter === c.id ? { background: c.tint, boxShadow: `0 4px 12px -4px ${c.tint}80` } : {}}>
          <span className="fpc-icon" style={{ color: filter === c.id ? 'white' : c.tint }}>
            <Icon name={c.id === 'pdf' ? 'doc' : c.id === 'image' ? 'image' : c.id === 'converter' ? 'swap' : 'archive'} size={14} />
          </span>
          {window.tCat(c, 'name')} <span style={{ opacity: 0.6, fontWeight: 500 }}>· {counts[c.id]}</span>
        </button>
      ))}
    </div>
  );
}

/* ---------- Tool card ---------- */
function ToolCard({ tool, style, isFav, onFav, onClick, tier = 'free', locked = false, authNeeded = false }) {
  const [, t] = window.useI18n();
  const cat = window.CATEGORIES.find(c => c.id === tool.cat);
  const cardStyle = { '--tc-tint': cat.tint, '--tc-soft': cat.soft };
  const classes = ['tool-card'];
  if (tool.featured) classes.push('has-badge');
  if (style === 'gradient') classes.push('tc-gradient');
  else if (style === 'outline') classes.push('tc-outline');
  if (locked) classes.push('tc-locked');
  if (authNeeded) classes.push('tc-auth');
  return (
    <div className={classes.join(' ')} data-id={tool.id} data-tier={tier} style={cardStyle} onClick={onClick} role="button" tabIndex={0}
         onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } }}>
      {tool.featured && <span className="tc-badge"><Icon name="sparkle" size={10} strokeWidth={2} /> {t('card.featured')}</span>}
      {tier !== 'free' && (
        <span className={`tc-tier tc-tier-${tier}`}>
          {locked ? <Icon name="lock" size={10} strokeWidth={2.5} /> : <Icon name="bolt" size={10} strokeWidth={2.5} />}
          {tier.toUpperCase()}
        </span>
      )}
      {tier === 'free' && authNeeded && (
        <span className="tc-auth-badge" title="Sign in required">
          <Icon name="lock" size={10} strokeWidth={2.5} /> SIGN IN
        </span>
      )}
      <button className={`tc-fav ${isFav ? 'active' : ''}`} onClick={(e) => { e.stopPropagation(); onFav(tool.id); }} aria-label={t('card.favorite')}>
        <Icon name="sparkle" size={14} strokeWidth={isFav ? 2.2 : 1.75} />
      </button>
      <div className="tc-head">
        <div className="tc-icon"><Icon name={tool.icon} size={20} /></div>
        <div>
          <div className="tc-name">{window.tTool(tool, 'name')}</div>
          <div className="tc-cat">{window.tCat(cat, 'name')}</div>
        </div>
      </div>
      <div className="tc-desc">{window.tTool(tool, 'desc')}</div>
      {tool.working && <span className="try-pill"><Icon name="check" size={10} strokeWidth={2.5} /> {t('card.live_demo')}</span>}
    </div>
  );
}

/* ---------- Footer ---------- */
function Footer() {
  const [locale, t] = window.useI18n();
  const catLinks = window.CATEGORIES.filter(c => c.id !== 'sports').map(c => ({
    id: c.id,
    title: window.tCat(c, 'name'),
    items: window.TOOLS.filter(x => x.cat === c.id).slice(0, 5).map(x => ({ id: x.id, label: window.tTool(x, 'name') })),
  }));
  return (
    <footer className="footer">
      <div className="container">
        <div className="footer-grid">
          <div>
            <a href="#" className="brand-logo">
              <div className="mark"><Icon name="sparkle" size={18} strokeWidth={2} /></div>
              <div className="name">MiniMagics<small>{t('nav.brand_sub')}</small></div>
            </a>
            <p className="footer-blurb">{t('footer.blurb')}</p>
          </div>
          {catLinks.map((c) => (
            <div key={c.id}>
              <h4>{c.title}</h4>
              <ul>{c.items.map(i => <li key={i.id}><a href="#">{i.label}</a></li>)}</ul>
            </div>
          ))}
        </div>
        <div className="footer-bottom">
          <span>{t('footer.copyright')}</span>
          <span className="footer-links">
            <a href="#/privacy" onClick={(e) => { e.preventDefault(); window.mmNavigate('privacy'); }}>Privacy</a>
            <span className="footer-sep">·</span>
            <a href="#/terms" onClick={(e) => { e.preventDefault(); window.mmNavigate('terms'); }}>Terms</a>
            <span className="footer-sep">·</span>
            <a href="#/contact" onClick={(e) => { e.preventDefault(); window.mmNavigate('contact'); }}>Contact</a>
            <span className="footer-sep">·</span>
            <a href="#" onClick={(e) => { e.preventDefault(); window.MMConsent?.open(); }}>Cookie preferences</a>
          </span>
        </div>
      </div>
    </footer>
  );
}

/* ---------- Google "G" mark — official 4-color SVG ---------- */
function GoogleLogo({ size = 18 }) {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
      <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
      <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
      <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
      <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
    </svg>
  );
}

/* ---------- Sign-in modal — single page: Google + email/password ---------- */
function SignInModal({ onClose, onSignedIn, pendingTool = null }) {
  const [mode, setMode] = useState('signin');      // 'signin' | 'signup' | 'reset'
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [username, setUsername] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const [info, setInfo] = useState('');

  useEffect(() => {
    const onKey = (e) => e.key === 'Escape' && onClose();
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);

  const submit = async (e) => {
    e.preventDefault();
    const sb = getSupabase(); if (!sb) { setErr('Auth backend unreachable.'); return; }
    setBusy(true); setErr(''); setInfo('');
    try {
      if (mode === 'signin') {
        const { data, error } = await sb.auth.signInWithPassword({ email, password });
        if (error) throw error;
        onSignedIn?.(data.user);
      } else if (mode === 'signup') {
        const trimmed = username.trim();
        if (trimmed.length < 2) { setErr('Username must be at least 2 characters.'); setBusy(false); return; }
        const { data, error } = await sb.auth.signUp({
          email, password,
          options: {
            emailRedirectTo: window.location.origin + window.location.pathname,
            // Stored in user_metadata so we can show it across the app instead
            // of the email address.
            data: { username: trimmed, display_name: trimmed },
          },
        });
        // Two paths to detect "this email is already registered":
        //  (a) confirm-email is OFF: Supabase returns an error like
        //      "User already registered".
        //  (b) confirm-email is ON: Supabase returns success with a user
        //      object whose `identities` array is empty — this is the
        //      documented anti-enumeration behaviour. Either way, surface
        //      a single clear message.
        if (error) {
          if (/already registered|already exists/i.test(error.message || '')) {
            throw new Error('An account with this email already exists. Try signing in or use "Forgot password".');
          }
          throw error;
        }
        if (data?.user && Array.isArray(data.user.identities) && data.user.identities.length === 0) {
          throw new Error('An account with this email already exists. Try signing in or use "Forgot password".');
        }
        // Best-effort write to profiles too so admin views and the dashboard
        // can use the canonical display name.
        if (data.user) {
          sb.from('profiles').upsert({ user_id: data.user.id, display_name: trimmed }).then(() => {});
        }
        if (data.user && data.session) onSignedIn?.(data.user);
        else setInfo("Check your email to confirm your address. You can close this window.");
      } else if (mode === 'reset') {
        const { error } = await sb.auth.resetPasswordForEmail(email, {
          redirectTo: window.location.origin + window.location.pathname,
        });
        if (error) throw error;
        setInfo('Password reset link sent. Check your inbox.');
      }
    } catch (e2) {
      setErr(e2.message || String(e2));
    } finally {
      setBusy(false);
    }
  };

  const google = async () => {
    const sb = getSupabase(); if (!sb) { setErr('Auth backend unreachable.'); return; }
    setBusy(true); setErr('');
    try {
      const { error } = await sb.auth.signInWithOAuth({
        provider: 'google',
        options: { redirectTo: window.location.origin + window.location.pathname },
      });
      if (error) throw error;
    } catch (e2) {
      setErr(e2.message || String(e2));
      setBusy(false);
    }
  };

  const title = pendingTool
    ? `Sign in to use ${pendingTool.name}`
    : mode === 'signup' ? 'Create your account'
    : mode === 'reset' ? 'Reset password'
    : 'Sign in to MiniMagics';
  const ctaLabel = mode === 'signin' ? 'Sign in with email' : mode === 'signup' ? 'Create account' : 'Send reset link';

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 420 }}>
        <div className="modal-head">
          <div className="mh-icon" style={{ background: 'var(--id-brand-blue-soft)', color: 'var(--id-brand-blue)' }}>
            <Icon name="sparkle" size={22} />
          </div>
          <div style={{ flex: 1 }}>
            <div className="mh-name">{title}</div>
            <div className="mh-desc">Sync favorites across devices, manage subscription.</div>
          </div>
          <button className="mh-close" onClick={onClose} aria-label="Close"><Icon name="x" size={18} /></button>
        </div>
        <div className="modal-body signin-body">
          {/* Google — only useful for signin/signup, not reset */}
          {mode !== 'reset' && (
            <>
              <button type="button" className="signin-google" onClick={google} disabled={busy}>
                <GoogleLogo size={18} />
                <span>Continue with Google</span>
              </button>
              <div className="signin-divider" role="separator">
                <span>or {mode === 'signup' ? 'sign up' : 'sign in'} with email</span>
              </div>
            </>
          )}

          <form onSubmit={submit} className="signin-form">
            {mode === 'signup' && (
              <label className="mini-field">
                <span className="mini-label">Username</span>
                <input type="text" required minLength={2} maxLength={32}
                       className="mini-input" autoComplete="nickname"
                       placeholder="How should we call you?"
                       value={username} onChange={(e) => setUsername(e.target.value)} />
              </label>
            )}
            <label className="mini-field">
              <span className="mini-label">Email</span>
              <input type="email" required className="mini-input" autoComplete="email"
                     placeholder="you@example.com"
                     value={email} onChange={(e) => setEmail(e.target.value)} />
            </label>
            {mode !== 'reset' && (
              <label className="mini-field">
                <span className="mini-label">Password</span>
                <input type="password" required minLength={8} className="mini-input"
                       autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}
                       placeholder={mode === 'signup' ? 'At least 8 characters' : '••••••••'}
                       value={password} onChange={(e) => setPassword(e.target.value)} />
              </label>
            )}
            <button className="btn btn-primary signin-cta" type="submit" disabled={busy}>
              {busy ? 'Working…' : ctaLabel}
            </button>
          </form>

          <div className="signin-links">
            {mode === 'signin' && (
              <>
                <button type="button" className="dash-linkbtn" onClick={() => { setMode('signup'); setErr(''); setInfo(''); }}>Create account</button>
                <span style={{ opacity: 0.4 }}>·</span>
                <button type="button" className="dash-linkbtn" onClick={() => { setMode('reset'); setErr(''); setInfo(''); }}>Forgot password?</button>
              </>
            )}
            {mode === 'signup' && (
              <button type="button" className="dash-linkbtn" onClick={() => { setMode('signin'); setErr(''); setInfo(''); }}>Already have an account? Sign in</button>
            )}
            {mode === 'reset' && (
              <button type="button" className="dash-linkbtn" onClick={() => { setMode('signin'); setErr(''); setInfo(''); }}>← Back to sign in</button>
            )}
          </div>

          {err && <div style={{ marginTop: 12 }}><window.ToolError error={err} /></div>}
          {info && <div className="signin-info">{info}</div>}
          <div className="cmp-meta signin-legal">
            By continuing you accept our <a href="#/terms" onClick={(e) => { e.preventDefault(); onClose(); window.mmNavigate('terms'); }}>Terms</a> and <a href="#/privacy" onClick={(e) => { e.preventDefault(); onClose(); window.mmNavigate('privacy'); }}>Privacy Policy</a>.
          </div>
        </div>
      </div>
    </div>
  );
}

/* ---------- Tweaks ---------- */
function TweaksShell({ tweaks, setTweak }) {
  const Panel = window.TweaksPanel;
  const Section = window.TweakSection;
  if (!Panel) return null;
  return (
    <Panel title="Tweaks">
      <Section label="Colors">
        <window.TweakColor label="Primary" value={tweaks.primaryColor} onChange={(v) => setTweak('primaryColor', v)} />
        <window.TweakColor label="Highlight" value={tweaks.highlightColor} onChange={(v) => setTweak('highlightColor', v)} />
      </Section>
      <Section label="Cards">
        <window.TweakRadio label="Style" value={tweaks.cardStyle} onChange={(v) => setTweak('cardStyle', v)}
          options={[{ value: 'flat', label: 'Flat' }, { value: 'gradient', label: 'Gradient' }, { value: 'outline', label: 'Outline' }]} />
        <window.TweakSlider label="Corner radius" value={tweaks.cornerRadius} min={0} max={24} step={1} onChange={(v) => setTweak('cornerRadius', v)} />
      </Section>
      <Section label="Density">
        <window.TweakRadio label="Density" value={tweaks.density} onChange={(v) => setTweak('density', v)}
          options={[{ value: 'compact', label: 'Compact' }, { value: 'comfortable', label: 'Comfy' }, { value: 'spacious', label: 'Spacious' }]} />
      </Section>
    </Panel>
  );
}

// ---------- Error Boundary ----------
// Wraps the entire App so a single tool throwing doesn't blank the page.
// Reports to window.mmReportError (error-tracking.js) and shows a friendly
// fallback with a reload button.
class MMErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }
  static getDerivedStateFromError(error) {
    return { error };
  }
  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo });
    try {
      window.mmReportError?.(error, {
        type: 'react.errorBoundary',
        componentStack: (errorInfo?.componentStack || '').slice(0, 2000),
        scope: this.props.scope || 'app',
      });
    } catch {}
  }
  render() {
    if (!this.state.error) return this.props.children;
    if (this.props.fallback) return this.props.fallback(this.state.error, () => this.setState({ error: null, errorInfo: null }));
    return (
      <div className="mm-error-fallback">
        <div className="mm-error-card">
          <div className="mm-error-icon"><Icon name="x" size={28} strokeWidth={2.2} /></div>
          <h2>Something went wrong</h2>
          <p>The page hit an unexpected error. We've logged it and our team will see it shortly.</p>
          <pre className="mm-error-msg">{String(this.state.error?.message || this.state.error || 'Unknown error').slice(0, 600)}</pre>
          <div className="mm-error-actions">
            <button className="btn btn-primary" onClick={() => location.reload()}>
              <Icon name="rotate" size={14} /> Reload page
            </button>
            <button className="btn btn-secondary" onClick={() => this.setState({ error: null, errorInfo: null })}>
              Try again
            </button>
            <a className="btn btn-secondary" href="#/" onClick={() => { location.hash = ''; this.setState({ error: null, errorInfo: null }); }}>
              Go home
            </a>
          </div>
        </div>
      </div>
    );
  }
}

window.MMErrorBoundary = MMErrorBoundary;

ReactDOM.createRoot(document.getElementById('root')).render(
  <MMErrorBoundary scope="app">
    <App />
    {/* Cookie consent banner — auto-shows on first visit, re-openable via footer link */}
    {window.MMConsentBanner && <window.MMConsentBanner />}
  </MMErrorBoundary>
);
