/* MostlyPDF web app — React (via Babel), ported faithfully from the Claude Design
   handoff (MostlyPDF.dc.html) onto the Forge/Mostly Tiny _ds + _kit. Four screens:
   Landing · Runner · Pricing · Compare, coral accent. The runner is wired to the
   real pdfProcess endpoint (window.MOSTLYPDF.endpoint) + the generated tool registry
   (window.PDF_TOOLS), replacing the design's mock data. Style strings are kept
   verbatim from the design via S(). */
(function () {
  'use strict';
  const { useState, useEffect, useRef, useMemo } = React;
  const CFG = window.MOSTLYPDF || {};
  const e = React.createElement;

  // MostlySign is its own product (ADR 0048, full move) — this sender app ships only under
  // the MostlySign brand. (It began as a copy of the MostlyPDF SPA; the marketing surfaces
  // are the static web/index.html + /trust + /verify, and the /sign signer page.)
  const BRAND = { name: 'MostlySign', word: 'Mostly', mark: 'Sign', site: 'https://mostlysign.com' };

  // i18n runtime (the @mostly-tiny/i18n window.ForgeI18n global, installed by the
  // forge-i18n-core.js + _i18n_bundle.js script tags before this babel module evals —
  // and re-installed per-locale during the SSR prerender). The identity fallback keeps
  // the page rendering if the bundle is absent (e.g. raw dev server). SSR-safe: the
  // prerender re-evaluates THIS module once per locale with ForgeI18n forced to that
  // locale's server translator, so module-scope tr('…') calls localize correctly.
  const I18N = (typeof window !== 'undefined' && window.ForgeI18n) || {
    t: (k) => k, has: () => false, locale: 'en', dir: 'ltr',
    fmt: { number: (n) => String(n ?? ''), currency: (n) => String(n ?? ''), percent: (n) => String(n ?? ''),
           date: (d) => String(d ?? ''), time: (d) => String(d ?? ''), relative: (d) => String(d ?? '') },
  };
  const tr = I18N.t, fmt = I18N.fmt;

  // Public sign-up/sign-in gate. FALSE = closed beta (team-only): the header hides the
  // Sign in / Get started buttons and shows an "In development" pill. Flip to true at public
  // launch (alongside auth.config publicSignup:true + opening the IdP). Mirrors the backend
  // gate — magic-link send already refuses non-team emails while publicSignup is false.
  const PUBLIC_APP_OPEN = false;

  // Lightweight fuzzy matcher for the tool search: a contiguous substring wins (ranked by
  // how early it appears); otherwise an in-order subsequence match. Returns a score
  // (higher = better) or -1 for no match.
  function fuzzyScore(query, text) {
    const q = (query || '').toLowerCase().trim();
    const t = (text || '').toLowerCase();
    if (!q) return 0;
    const idx = t.indexOf(q);
    if (idx >= 0) return 1000 - idx;
    let qi = 0;
    for (let i = 0; i < t.length && qi < q.length; i++) if (t[i] === q[qi]) qi++;
    return qi === q.length ? 1 : -1;
  }

  // Parse a design inline-style string ("a:b;c:d") into a React style object,
  // preserving --custom-properties verbatim.
  function S(str) {
    const o = {};
    (str || '').split(';').forEach((decl) => {
      const i = decl.indexOf(':');
      if (i < 0) return;
      let k = decl.slice(0, i).trim();
      const v = decl.slice(i + 1).trim();
      if (!k) return;
      if (!k.startsWith('--')) k = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
      o[k] = v;
    });
    return o;
  }

  // ── icon paths (verbatim from the design) ──
  const P = {
    layers: 'M12 2 2 7l10 5 10-5-10-5z M2 17l10 5 10-5 M2 12l10 5 10-5',
    scissors: 'M6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M20 4 8.12 15.88 M14.47 14.48 20 20 M8.12 8.12 12 12',
    fileMinus: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z M14 2v7h7 M9 15h6',
    copy: 'M9 9h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z M5 15H4a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1',
    grid: 'M3 3h7v7H3z M14 3h7v7h-7z M14 14h7v7h-7z M3 14h7v7H3z',
    rotate: 'M21 2v6h-6 M21 13a9 9 0 1 1-3-7.7L21 8',
    shrink: 'M15 3h6v6 M9 21H3v-6 M21 3l-7 7 M3 21l7-7',
    image: 'M3 3h18v18H3z M8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z M21 15l-5-5L5 21',
    image2: 'M3 3h18v18H3z M9 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z M21 14l-4-4L7 20',
    fileText: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z M14 2v7h7 M16 13H8 M16 17H8 M10 9H8',
    fileType: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z M14 2v7h7 M9 13v5 M7 13h4',
    scan: 'M3 7V5a2 2 0 0 1 2-2h2 M17 3h2a2 2 0 0 1 2 2v2 M21 17v2a2 2 0 0 1-2 2h-2 M7 21H5a2 2 0 0 1-2-2v-2 M7 8h8 M7 12h10 M7 16h6',
    hash: 'M4 9h16 M4 15h16 M10 3 8 21 M16 3l-2 18',
    droplet: 'M12 2.7 6.3 8.4a8 8 0 1 0 11.4 0z M9.5 14a2.5 2.5 0 0 0 2.5 2.5',
    lock: 'M5 11h14v10H5z M8 11V7a4 4 0 0 1 8 0v4',
    unlock: 'M5 11h14v10H5z M8 11V7a4 4 0 0 1 7.9-1',
    pen: 'M2 22s4-1 7-4l9.5-9.5a2.1 2.1 0 0 0-3-3L6 15c-3 3-4 7-4 7z M15 5l4 4',
    zap: 'M13 2 3 14h7l-1 8 10-12h-7z',
    code: 'M16 18l6-6-6-6 M8 6l-6 6 6 6',
    link: 'M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7 M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7',
    shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z M9 12l2 2 4-4',
    send: 'M22 2 11 13 M22 2 15 22l-4-9-9-4z',
  };
  function ic(name, size, sw) {
    const d = P[name] || '';
    return e('svg', { width: size || 20, height: size || 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: sw || 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
      d.split(' M').map((p, i) => e('path', { key: i, d: (i ? 'M' : '') + p })));
  }

  // Render-engine tool UI (F147): the HTML→PDF / URL→PDF tools use a text/URL form (not a file
  // drop) and receive a hosted PDF URL back. Built in mp/render-runner.jsx and eval'd via the
  // prerender `shared` list; if that module is absent (bare dev), fall back to null so the app
  // still renders the rest of the grid.
  const RenderRunner = (typeof window !== 'undefined' && window.MostlyPDFRenderRunner)
    ? window.MostlyPDFRenderRunner({ React, useState, useEffect, S, ic, endpoint: CFG.endpoint })
    : null;
  const RENDER_SLUGS = { 'html-to-pdf': true, 'url-to-pdf': true };
  // E-sign kit (F184/F185): signature capture + click-to-place + the request-signature
  // screens, from mp/sign-kit.jsx (prerender `shared`). The authed screens get this app's
  // firebase/auth helpers (function declarations — hoisted, resolved at call time).
  const SignKit = (typeof window !== 'undefined' && window.MostlyPDFSignKit)
    ? window.MostlyPDFSignKit({ React })
    : null;
  // Sender screens (F185/F190) live in mp/esign-screens.jsx (multi-signer + templates).
  const SignScreens = (SignKit && typeof window !== 'undefined' && window.MostlyPDFEsignScreens)
    ? window.MostlyPDFEsignScreens({ React, SignKit, initFirebase, authUser, idpLoginUrl: CFG.idpLoginUrl })
    : null;
  const Logo = (sz) => (
    <svg width={sz} height={sz} viewBox="0 0 100 100" aria-hidden="true">
      <rect x="3" y="3" width="94" height="94" rx="27" fill="var(--accent)" />
      <path d="M34 22H56L70 36V74A4 4 0 0 1 66 78H34A4 4 0 0 1 30 74V26A4 4 0 0 1 34 22Z" fill="none" stroke="#fff" strokeWidth="6.5" strokeLinejoin="round" strokeLinecap="round" />
      <path d="M56 22V36H70" fill="none" stroke="#fff" strokeWidth="6.5" strokeLinejoin="round" strokeLinecap="round" />
      <circle cx="79" cy="79" r="5" fill="#fff" />
    </svg>
  );

  // ── tool catalog: the design's display copy, reconciled with the live registry.
  // engine 'pure' tools are wired to pdfProcess; 'container' tools render as Soon. ──
  // Tool display copy is keyed by slug (mp.tool.<slug>.title/answer); category names/notes by
  // mp.cat.<key>.name/note. Slugs/icons/flags are NOT translated (they're code/registry-driven).
  const CATS = [
    { key: 'organize', name: tr('mp.cat.organize.name'), note: tr('mp.cat.organize.note'), tools: [
      { slug: 'merge-pdf', icon: 'layers', title: tr('mp.tool.merge-pdf.title'), answer: tr('mp.tool.merge-pdf.answer') },
      { slug: 'split-pdf', icon: 'scissors', title: tr('mp.tool.split-pdf.title'), answer: tr('mp.tool.split-pdf.answer') },
      { slug: 'remove-pages', icon: 'fileMinus', title: tr('mp.tool.remove-pages.title'), answer: tr('mp.tool.remove-pages.answer') },
      { slug: 'extract-pages', icon: 'copy', title: tr('mp.tool.extract-pages.title'), answer: tr('mp.tool.extract-pages.answer') },
      { slug: 'organize-pdf', icon: 'grid', title: tr('mp.tool.organize-pdf.title'), answer: tr('mp.tool.organize-pdf.answer') },
      { slug: 'rotate-pdf', icon: 'rotate', title: tr('mp.tool.rotate-pdf.title'), answer: tr('mp.tool.rotate-pdf.answer') },
    ] },
    { key: 'optimize', name: tr('mp.cat.optimize.name'), note: tr('mp.cat.optimize.note'), tools: [
      { slug: 'compress-pdf', icon: 'shrink', title: tr('mp.tool.compress-pdf.title'), answer: tr('mp.tool.compress-pdf.answer') },
    ] },
    { key: 'convert', name: tr('mp.cat.convert.name'), note: tr('mp.cat.convert.note'), tools: [
      { slug: 'jpg-to-pdf', icon: 'image', title: tr('mp.tool.jpg-to-pdf.title'), answer: tr('mp.tool.jpg-to-pdf.answer') },
      { slug: 'pdf-to-word', icon: 'fileText', title: tr('mp.tool.pdf-to-word.title'), answer: tr('mp.tool.pdf-to-word.answer'), pro: true },
      { slug: 'pdf-to-jpg', icon: 'image2', title: tr('mp.tool.pdf-to-jpg.title'), answer: tr('mp.tool.pdf-to-jpg.answer') },
      { slug: 'word-to-pdf', icon: 'fileType', title: tr('mp.tool.word-to-pdf.title'), answer: tr('mp.tool.word-to-pdf.answer') },
      { slug: 'ocr-pdf', icon: 'scan', title: tr('mp.tool.ocr-pdf.title'), answer: tr('mp.tool.ocr-pdf.answer'), pro: true },
      // Render-engine tools (F147, MostlyRender). Copy is plain strings (like F145's DocWizard)
      // so the feature ships without adding a translation key per locale — render is the wedge.
      { slug: 'html-to-pdf', icon: 'code', title: 'HTML to PDF', answer: 'Paste HTML and render it to a clean, paginated A4 PDF — also on the API.', render: true },
      { slug: 'url-to-pdf', icon: 'link', title: 'Webpage to PDF', answer: 'Enter a public URL and capture the whole page as a paginated A4 PDF.', render: true },
    ] },
    { key: 'edit', name: tr('mp.cat.edit.name'), note: tr('mp.cat.edit.note'), tools: [
      { slug: 'page-numbers', icon: 'hash', title: tr('mp.tool.page-numbers.title'), answer: tr('mp.tool.page-numbers.answer') },
      { slug: 'watermark-pdf', icon: 'droplet', title: tr('mp.tool.watermark-pdf.title'), answer: tr('mp.tool.watermark-pdf.answer') },
    ] },
    { key: 'security', name: tr('mp.cat.security.name'), note: tr('mp.cat.security.note'), tools: [
      { slug: 'protect-pdf', icon: 'lock', title: tr('mp.tool.protect-pdf.title'), answer: tr('mp.tool.protect-pdf.answer') },
      { slug: 'unlock-pdf', icon: 'unlock', title: tr('mp.tool.unlock-pdf.title'), answer: tr('mp.tool.unlock-pdf.answer') },
      { slug: 'sign-pdf', icon: 'pen', title: tr('mp.tool.sign-pdf.title'), answer: tr('mp.tool.sign-pdf.answer'), pro: true },
      // E-sign tools (F184/F185). Copy is plain strings (render-tools precedent) so the
      // feature ships without a per-locale key pass; keyed localization is a follow-up.
      { slug: 'sign-pdf-certified', icon: 'shield', title: 'Certify PDF (digital signature)', answer: 'Apply a verifiable PKI digital signature (PAdES) using your own .p12/.pfx certificate.', pro: true },
      { slug: 'request-signature', icon: 'send', title: 'Request Signature', answer: 'Email someone a secure signing link — you both get the signed PDF back with an audit certificate, digitally sealed. One envelope a month free; unlimited on Pro.', esign: true, pro: true },
    ] },
  ];
  // Per-tool runner copy. Prose (runLabel/workingLabel/successTitle/errorMsg/doMore.label/verb/howto/
  // genericNote/pagesLabel) is keyed mp.runner.<slug>.*; opt/accepts/min/placeholder stay as code/format.
  const META = {
    'merge-pdf': { inputs: 'multi', reorder: true, opt: 'merge', accepts: 'PDF', min: 2, runLabel: tr('mp.runner.merge-pdf.runLabel'), workingLabel: tr('mp.runner.merge-pdf.workingLabel'), successTitle: tr('mp.runner.merge-pdf.successTitle'), errorMsg: tr('mp.runner.merge-pdf.errorMsg'), doMore: { slug: 'compress-pdf', label: tr('mp.runner.merge-pdf.doMore') }, verb: tr('mp.runner.merge-pdf.verb'), howto: tr('mp.runner.merge-pdf.howto') },
    'split-pdf': { inputs: 'single', opt: 'split', accepts: 'PDF', runLabel: tr('mp.runner.split-pdf.runLabel'), workingLabel: tr('mp.runner.split-pdf.workingLabel'), successTitle: tr('mp.runner.split-pdf.successTitle'), errorMsg: tr('mp.runner.split-pdf.errorMsg'), doMore: { slug: 'merge-pdf', label: tr('mp.runner.split-pdf.doMore') }, verb: tr('mp.runner.split-pdf.verb'), howto: tr('mp.runner.split-pdf.howto') },
    'remove-pages': { inputs: 'single', opt: 'pages', pagesLabel: tr('mp.runner.remove-pages.pagesLabel'), placeholder: '2, 5-7', accepts: 'PDF', runLabel: tr('mp.runner.remove-pages.runLabel'), workingLabel: tr('mp.runner.remove-pages.workingLabel'), successTitle: tr('mp.runner.remove-pages.successTitle'), errorMsg: tr('mp.runner.remove-pages.errorMsg'), doMore: { slug: 'organize-pdf', label: tr('mp.runner.remove-pages.doMore') }, verb: tr('mp.runner.remove-pages.verb'), howto: tr('mp.runner.remove-pages.howto') },
    'extract-pages': { inputs: 'single', opt: 'pages', pagesLabel: tr('mp.runner.extract-pages.pagesLabel'), placeholder: '1, 3-4', accepts: 'PDF', runLabel: tr('mp.runner.extract-pages.runLabel'), workingLabel: tr('mp.runner.extract-pages.workingLabel'), successTitle: tr('mp.runner.extract-pages.successTitle'), errorMsg: tr('mp.runner.extract-pages.errorMsg'), doMore: { slug: 'merge-pdf', label: tr('mp.runner.extract-pages.doMore') }, verb: tr('mp.runner.extract-pages.verb'), howto: tr('mp.runner.extract-pages.howto') },
    'organize-pdf': { inputs: 'single', opt: 'order', accepts: 'PDF', runLabel: tr('mp.runner.organize-pdf.runLabel'), workingLabel: tr('mp.runner.organize-pdf.workingLabel'), successTitle: tr('mp.runner.organize-pdf.successTitle'), errorMsg: tr('mp.runner.organize-pdf.errorMsg'), doMore: { slug: 'page-numbers', label: tr('mp.runner.organize-pdf.doMore') }, verb: tr('mp.runner.organize-pdf.verb'), howto: tr('mp.runner.organize-pdf.howto') },
    'rotate-pdf': { inputs: 'single', opt: 'rotate', accepts: 'PDF', runLabel: tr('mp.runner.rotate-pdf.runLabel'), workingLabel: tr('mp.runner.rotate-pdf.workingLabel'), successTitle: tr('mp.runner.rotate-pdf.successTitle'), errorMsg: tr('mp.runner.rotate-pdf.errorMsg'), doMore: { slug: 'page-numbers', label: tr('mp.runner.rotate-pdf.doMore') }, verb: tr('mp.runner.rotate-pdf.verb'), howto: tr('mp.runner.rotate-pdf.howto') },
    'watermark-pdf': { inputs: 'single', opt: 'watermark', accepts: 'PDF', runLabel: tr('mp.runner.watermark-pdf.runLabel'), workingLabel: tr('mp.runner.watermark-pdf.workingLabel'), successTitle: tr('mp.runner.watermark-pdf.successTitle'), errorMsg: tr('mp.runner.watermark-pdf.errorMsg'), doMore: { slug: 'page-numbers', label: tr('mp.runner.watermark-pdf.doMore') }, verb: tr('mp.runner.watermark-pdf.verb'), howto: tr('mp.runner.watermark-pdf.howto') },
    'page-numbers': { inputs: 'single', opt: 'pagenum', accepts: 'PDF', runLabel: tr('mp.runner.page-numbers.runLabel'), workingLabel: tr('mp.runner.page-numbers.workingLabel'), successTitle: tr('mp.runner.page-numbers.successTitle'), errorMsg: tr('mp.runner.page-numbers.errorMsg'), doMore: { slug: 'watermark-pdf', label: tr('mp.runner.page-numbers.doMore') }, verb: tr('mp.runner.page-numbers.verb'), howto: tr('mp.runner.page-numbers.howto') },
    'jpg-to-pdf': { inputs: 'multi', opt: 'images', accepts: 'JPG, PNG', min: 1, runLabel: tr('mp.runner.jpg-to-pdf.runLabel'), workingLabel: tr('mp.runner.jpg-to-pdf.workingLabel'), successTitle: tr('mp.runner.jpg-to-pdf.successTitle'), errorMsg: tr('mp.runner.jpg-to-pdf.errorMsg'), doMore: { slug: 'merge-pdf', label: tr('mp.runner.jpg-to-pdf.doMore') }, verb: tr('mp.runner.jpg-to-pdf.verb'), howto: tr('mp.runner.jpg-to-pdf.howto'), genericNote: tr('mp.runner.jpg-to-pdf.genericNote') },
    // E-sign runners (F184). Plain-string copy, same shipping precedent as the render tools.
    'sign-pdf': { inputs: 'single', opt: 'sign', accepts: 'PDF', runLabel: 'Sign PDF', workingLabel: 'Signing your PDF…', successTitle: 'Your PDF is signed', errorMsg: 'Could not sign this PDF. Check the file and try again.', doMore: { slug: 'protect-pdf', label: 'Protect the signed PDF' }, verb: 'sign a pdf', howto: 'draw, type or upload your signature, click where it should sit on the page' },
    'sign-pdf-certified': { inputs: 'single', opt: 'signcert', accepts: 'PDF', runLabel: 'Certify PDF', workingLabel: 'Applying the digital signature…', successTitle: 'Your PDF is certified', errorMsg: 'Could not apply the digital signature. Check the certificate and passphrase.', doMore: { slug: 'sign-pdf', label: 'Add a visual signature' }, verb: 'digitally sign a pdf', howto: 'upload your .p12/.pfx certificate, enter its passphrase, and certify' },
  };
  const toolMap = () => { const m = {}; CATS.forEach((c) => c.tools.forEach((t) => { m[t.slug] = Object.assign({ category: c.name, categoryKey: c.key }, t); })); return m; };
  const TM = toolMap();
  const RUNNER_TABS = ['merge-pdf', 'split-pdf', 'rotate-pdf', 'watermark-pdf'];
  const POS = ['top-left', 'top-center', 'top-right', 'center-left', 'center', 'center-right', 'bottom-left', 'bottom-center', 'bottom-right'];
  const placeMap = { 'top-left': 'start start', 'top-center': 'start center', 'top-right': 'start end', 'center-left': 'center start', center: 'center center', 'center-right': 'center end', 'bottom-left': 'end start', 'bottom-center': 'end center', 'bottom-right': 'end end' };

  const fileToB64 = (f) => new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(String(r.result).split(',')[1]); r.onerror = rej; r.readAsDataURL(f); });
  const fmtSize = (n) => (n < 1024 ? n + ' B' : n < 1048576 ? (n / 1024).toFixed(0) + ' KB' : (n / 1048576).toFixed(1) + ' MB');

  // Derive a PDF's page aspect ratio ("w / h", CSS-ready) straight from its bytes — the first
  // /MediaBox gives the page box; a /Rotate of 90/270 swaps the axes. `bin` is the latin1 byte
  // string from atob(). Returns null (→ A4 fallback) if no MediaBox is found. Cheap: no renderer.
  function pdfAspect(bin) {
    try {
      const m = /\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\]/.exec(bin || '');
      if (!m) return null;
      let w = Math.abs(parseFloat(m[3]) - parseFloat(m[1]));
      let h = Math.abs(parseFloat(m[4]) - parseFloat(m[2]));
      if (!(w > 0 && h > 0)) return null;
      const r = /\/Rotate\s+(-?\d+)/.exec(bin);
      if (r) { const deg = ((parseInt(r[1], 10) % 360) + 360) % 360; if (deg === 90 || deg === 270) { const t = w; w = h; h = t; } }
      return w + ' / ' + h;
    } catch (e) { return null; }
  }

  // Tool-chaining handoff: when the user picks "do more" on a result, we stash the result
  // file(s) here and navigate to the target tool. Because <Runner key={slug}> REMOUNTS on a
  // route change, this module-scoped box survives the hop; the target Runner's mount effect
  // consumes it (seeds `files`) so the result becomes the next tool's input — no re-download.
  let pendingChain = null; // { slug, files: [File] } | null

  // Cross-REDIRECT handoff: "Sign in with Mostly Tiny" is a full-page redirect, so pendingChain
  // (module state) is wiped. To return the user to the SAME tool with their file still loaded —
  // no re-upload — we stash { slug, files } in IndexedDB before the redirect (File objects are
  // structured-cloneable) and the target Runner restores it on return. Best-effort: any IDB
  // failure just falls back to re-upload.
  const PENDING_KEY = 'pending-run';
  function idbKV(run) {
    return new Promise((resolve) => {
      try {
        if (typeof indexedDB === 'undefined') return resolve(undefined);
        const req = indexedDB.open('mpdf', 1);
        req.onupgradeneeded = () => { try { req.result.createObjectStore('kv'); } catch (e) {} };
        req.onerror = () => resolve(undefined);
        req.onsuccess = () => { try { run(req.result, resolve); } catch (e) { resolve(undefined); } };
      } catch (e) { resolve(undefined); }
    });
  }
  function idbPut(key, val) { return idbKV((d, done) => { const tx = d.transaction('kv', 'readwrite'); tx.objectStore('kv').put(val, key); tx.oncomplete = () => done(true); tx.onerror = () => done(false); }); }
  function idbGet(key) { return idbKV((d, done) => { const r = d.transaction('kv', 'readonly').objectStore('kv').get(key); r.onsuccess = () => done(r.result); r.onerror = () => done(undefined); }); }
  function idbDel(key) { return idbKV((d, done) => { const tx = d.transaction('kv', 'readwrite'); tx.objectStore('kv').delete(key); tx.oncomplete = () => done(true); tx.onerror = () => done(false); }); }

  // ── shared chrome ──
  // Dark-theme toggle. Bright stays the default: dark is opt-in, stored as mp.theme='dark'
  // (absent = light), applied by the boot script in the shell <head> before first paint.
  // Both icons are always rendered — the shell CSS ([data-theme] rules) shows one — so the
  // SSR-prerendered markup is theme-independent and hydrates without a mismatch (#418).
  function themeFlip() {
    const el = document.documentElement;
    const toDark = el.getAttribute('data-theme') !== 'dark';
    try { toDark ? localStorage.setItem('mp.theme', 'dark') : localStorage.removeItem('mp.theme'); } catch (e) { /* private mode: theme still flips for this page */ }
    if (toDark) el.setAttribute('data-theme', 'dark'); else el.removeAttribute('data-theme');
  }
  function ThemeToggle() {
    return (
      <button type="button" className="forge-btn forge-btn--ghost" onClick={themeFlip} aria-label="Toggle dark theme" title="Toggle dark theme" style={S('width:34px;padding:0;color:var(--fg-muted);')}>
        <svg className="mp-theme-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z" /></svg>
        <svg className="mp-theme-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" /></svg>
      </button>
    );
  }

  function Nav({ go, onSearch }) {
    // Per-product org context switcher (F203). Once the user is signed in, read their org
    // memberships + active selection (getOrgContext) and let the shared kit mount a Personal/org
    // <select> into orgRef. mountOrgSwitcher renders NOTHING for a Personal-only user, so this is
    // invisible unless they belong to ≥1 org; on change it persists via setActiveOrg then reloads
    // so entitlement re-resolves. All firebase work stays inside the effect (SSR-safe).
    const orgRef = useRef(null);
    useEffect(() => {
      let cancelled = false;
      const fb = initFirebase();
      if (!fb || !fb.fns || !orgRef.current || !window.MostlyAuth || !window.MostlyAuth.mountOrgSwitcher) return;
      authUser().then((u) => {
        if (cancelled || !u || !orgRef.current) return;
        fb.fns.httpsCallable('getOrgContext')({})
          .then((res) => {
            const d = (res && res.data) || {};
            if (cancelled || !orgRef.current) return;
            window.MostlyAuth.mountOrgSwitcher({
              el: orgRef.current,
              fns: fb.fns,
              memberships: d.memberships || {},
              activeOrg: d.active_org || 'personal',
              ariaLabel: 'Active organization',
              className: 'mp-org-switcher',
            });
          })
          .catch(() => { /* signed-out / offline → leave the slot empty */ });
      });
      return () => { cancelled = true; };
    }, []);
    return (
      <div style={S('position:sticky;top:0;z-index:40;background:color-mix(in srgb,var(--bg) 82%,transparent);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1px solid var(--hairline);')}>
        <div style={S('max-width:1180px;margin:0 auto;padding:0 28px;height:64px;display:flex;align-items:center;gap:26px;')}>
          <a href="/" style={S('display:inline-flex;align-items:center;gap:10px;cursor:pointer;text-decoration:none;color:inherit;')}>
            {Logo(30)}
            <span style={S('font-family:var(--font-display);font-weight:600;font-size:20px;letter-spacing:-0.04em;')}>{BRAND.word}<span style={S('color:var(--accent);')}>{BRAND.mark}</span></span>
          </a>
          <nav style={S('display:flex;gap:22px;margin-left:6px;')}>
            <span className="pdf-link" onClick={() => go('request')} style={S('font-size:13.5px;font-weight:500;color:var(--fg-muted);cursor:pointer;')}>Send</span>
            <span className="pdf-link" onClick={() => go('bulk')} style={S('font-size:13.5px;font-weight:500;color:var(--fg-muted);cursor:pointer;')}>Bulk</span>
            <span className="pdf-link" onClick={() => go('team')} style={S('font-size:13.5px;font-weight:500;color:var(--fg-muted);cursor:pointer;')}>Team</span>
            <span className="pdf-link" onClick={() => go('pricing')} style={S('font-size:13.5px;font-weight:500;color:var(--fg-muted);cursor:pointer;')}>Pricing</span>
            <a className="pdf-link" href="/trust" style={S('font-size:13.5px;font-weight:500;color:var(--fg-muted);text-decoration:none;')}>Trust</a>
            <a className="pdf-link" href="/verify" style={S('font-size:13.5px;font-weight:500;color:var(--fg-muted);text-decoration:none;')}>Verify</a>
          </nav>
          <div style={S('margin-left:auto;display:flex;align-items:center;gap:10px;')}>
            {/* Per-product org context switcher (F203) — the kit fills this slot only for a
                user who belongs to ≥1 org; it stays empty (and invisible) otherwise. */}
            <span ref={orgRef} className="mp-org-switcher-slot" style={S('display:inline-flex;align-items:center;')} />
            {/* Shared ⌘K command palette trigger (@mostly-tiny/cmdk). The language switcher
                lives in the shared Footer, so the header carries search instead. */}
            {window.ForgeCmdK && <window.ForgeCmdK.Trigger onClick={onSearch} />}
            <ThemeToggle />
            {PUBLIC_APP_OPEN ? (
              <>
                <button className="forge-btn forge-btn--ghost">{tr('mp.nav.signIn')}</button>
                <button className="forge-btn forge-btn--primary">{tr('mp.nav.getStarted')}</button>
              </>
            ) : (
              <span title={tr('mp.nav.betaTitle')} style={S('display:inline-flex;align-items:center;gap:7px;padding:7px 13px;border:1px solid var(--border);border-radius:999px;font-size:12.5px;font-weight:600;color:var(--fg-muted);')}>
                <span style={S('width:7px;height:7px;border-radius:50%;background:var(--accent);')} />
                {tr('mp.nav.inDevelopment')}
              </span>
            )}
          </div>
        </div>
      </div>
    );
  }

  // The ONE shared portfolio footer template (window.ForgeKit.Footer) — same component as every
  // other Mostly Tiny product, so the footer can't drift between products. Legal links go to the
  // canonical /legal/mostlypdf/<kind> path; tool links use the SPA's #/<slug> runner routes.
  function Footer() {
    const LEGAL = 'https://mostlyprivacy.com/legal/mostlysign/';
    const brand = (
      <a href="/" style={S('display:inline-flex;align-items:center;gap:9px;text-decoration:none;')}>
        {Logo(26)}
        <span style={S('font-family:var(--font-display);font-weight:600;font-size:17px;letter-spacing:-0.04em;color:var(--fg);')}>{BRAND.word}<span style={S('color:var(--accent);')}>{BRAND.mark}</span></span>
      </a>
    );
    const columns = [
      { heading: 'Product', links: [
        { label: 'Send a document', href: '#/request-signature' },
        { label: 'Pricing', href: '#/pricing' },
        { label: 'API keys', href: '#/keys' }, // subtle, team-only entry while in private beta
        { label: 'API docs', href: '/docs' },
      ] },
      { heading: 'Trust', links: [
        { label: 'How the seal works', href: '/trust' },
        { label: 'Verify a document', href: '/verify' },
      ] },
      { heading: 'Company', links: [
        { label: 'About', href: 'https://mostlytiny.io' },
        { label: 'Support', href: 'https://mostlytiny.io/#contact' },
      ] },
      { heading: 'Legal', links: [
        { label: 'Terms', href: LEGAL + 'terms' },
        { label: 'Privacy', href: LEGAL + 'privacy' },
        { label: 'Cookies', href: LEGAL + 'cookie' },
      ] },
    ];
    return (
      <window.ForgeKit.Footer
        brand={brand}
        tagline="Legally binding e-signatures — PAdES-sealed and verifiable anywhere, not just here. Free to start, no per-envelope caps."
        columns={columns}
        suiteSelf={BRAND.site}
        version={BRAND.name + ' · v1.0'}
      />
    );
  }

  const check = (stroke, w) => <svg width={w || 15} height={w || 15} viewBox="0 0 24 24" fill="none" stroke={stroke} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>;
  const eye = (w) => <svg width={w || 16} height={w || 16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" /><circle cx="12" cy="12" r="3" /></svg>;

  const isImageInput = (f) => /^image\//.test((f && f.type) || '') || /\.(png|jpe?g|webp|gif|bmp|avif)$/i.test((f && f.name) || '');

  // Live thumbnail for an image input (jpg-to-pdf etc.): an object URL created from the File,
  // revoked on unmount so we never leak. PDFs keep the styled extension badge instead.
  function ImgThumb({ file }) {
    const [url, setUrl] = useState('');
    useEffect(() => {
      const u = URL.createObjectURL(file);
      setUrl(u);
      return () => { try { URL.revokeObjectURL(u); } catch (e) { /* ignore */ } };
    }, [file]);
    return <img src={url} alt={file.name} style={S('width:36px;height:44px;object-fit:cover;border-radius:6px;border:1px solid var(--border);background:#fff;flex:none;')} />;
  }

  // Full-file preview lightbox. PDFs render in the browser's native <iframe> viewer (zero deps,
  // no pdf.js/worker); images render inline. Used for BOTH added inputs and processed results —
  // the caller owns the object URL (revoke handled in closePreview), so this component is pure.
  function PreviewModal({ item, onClose }) {
    useEffect(() => {
      const onKey = (ev) => { if (ev.key === 'Escape') onClose(); };
      window.addEventListener('keydown', onKey);
      const prev = document.body.style.overflow;
      document.body.style.overflow = 'hidden';
      return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = prev; };
    }, [onClose]);
    if (!item) return null;
    const isImg = item.kind === 'image';
    return (
      <div onClick={onClose} style={S('position:fixed;inset:0;z-index:80;display:flex;flex-direction:column;padding:22px;background:color-mix(in srgb,var(--brand-midnight) 60%,transparent);backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);animation:pdf-rise .2s var(--ease-out);')}>
        <div onClick={(ev) => ev.stopPropagation()} style={S('width:100%;max-width:960px;height:100%;margin:0 auto;display:flex;flex-direction:column;background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius-xl);box-shadow:0 24px 80px rgba(10,10,26,0.34);overflow:hidden;')}>
          <div style={S('display:flex;align-items:center;gap:12px;padding:13px 15px;border-bottom:1px solid var(--hairline);flex:none;')}>
            <span style={S('display:inline-grid;place-items:center;width:34px;height:34px;border-radius:8px;background:var(--accent-bg-soft);color:var(--accent);flex:none;')}>{ic('fileText', 17)}</span>
            <div style={S('flex:1;min-width:0;')}>
              <div style={S('font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;')}>{item.name}</div>
              {item.size && <div style={S('font-size:11.5px;color:var(--fg-subtle);')}>{item.size}</div>}
            </div>
            {item.download && (
              <a href={item.url} download={item.name} className="forge-btn forge-btn--primary forge-btn--sm">
                <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="M7 10l5 5 5-5" /><path d="M12 15V3" /></svg>
                <span>{tr('mp.runner.download')}</span>
              </a>
            )}
            <button onClick={onClose} className="forge-iconbtn" aria-label="Close preview" title="Close">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="M6 6l12 12" /></svg>
            </button>
          </div>
          <div style={S('flex:1;min-height:0;background:var(--bg-chip);')}>
            {isImg
              ? <div style={S('width:100%;height:100%;display:grid;place-items:center;overflow:auto;padding:20px;')}><img src={item.url} alt={item.name} style={S('max-width:100%;max-height:100%;border-radius:8px;box-shadow:var(--shadow-soft);')} /></div>
              : <iframe title={item.name} src={item.url + '#toolbar=1&navpanes=0&view=FitH'} style={S('width:100%;height:100%;border:none;background:#fff;')} />}
          </div>
        </div>
      </div>
    );
  }

  // Pro-tool sign-in gate. Pro tools can't run on the anonymous web endpoint — instead of a red
  // error we pop this calm modal: "Sign in required" + a Sign in button that starts the UNIFIED
  // "Sign in with Mostly Tiny" (MostlyID SSO) flow, plus a secondary link to the Pro plans.
  // Dismissible (× / backdrop / Esc), so the file stays loaded.
  function AuthGate({ toolTitle, onSignIn, go, onClose, mode }) {
    // 'upgrade' = a signed-in FREE user hit a Pro tool (server 403) → prompt an upgrade, not sign-in.
    const upgrade = mode === 'upgrade';
    useEffect(() => {
      const onKey = (ev) => { if (ev.key === 'Escape') onClose(); };
      window.addEventListener('keydown', onKey);
      const prev = document.body.style.overflow;
      document.body.style.overflow = 'hidden';
      return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = prev; };
    }, [onClose]);
    return (
      <div onClick={onClose} style={S('position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:22px;background:color-mix(in srgb,var(--brand-midnight) 55%,transparent);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);animation:pdf-rise .2s var(--ease-out);')}>
        <div onClick={(ev) => ev.stopPropagation()} style={S('position:relative;width:100%;max-width:420px;background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius-xl);box-shadow:0 24px 80px rgba(10,10,26,0.34);padding:28px 26px;text-align:center;')}>
          <button onClick={onClose} className="forge-iconbtn forge-iconbtn--sm" aria-label="Close" title="Close" style={S('position:absolute;top:12px;right:12px;')}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="M6 6l12 12" /></svg>
          </button>
          <span style={S('display:inline-grid;place-items:center;width:56px;height:56px;border-radius:50%;background:var(--accent-bg-soft);color:var(--accent);margin-bottom:14px;')}>
            <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="10" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
          </span>
          <div style={S('font-family:var(--font-display);font-size:20px;font-weight:600;letter-spacing:-0.02em;')}>{upgrade ? 'Upgrade to Pro' : 'Sign in required'}</div>
          <p style={S('font-size:13.5px;line-height:1.55;color:var(--fg-muted);margin:8px auto 20px;max-width:30em;')}><strong style={S('color:var(--fg);font-weight:600;')}>{toolTitle}</strong> is a Pro tool. {upgrade ? 'Upgrade your Mostly Tiny plan to use it — no API key needed.' : 'Sign in with your Mostly Tiny account to use it.'}</p>
          {upgrade
            ? <button onClick={() => { onClose(); go('pricing'); }} className="forge-btn forge-btn--primary forge-btn--lg forge-btn--block"><span>Upgrade to Pro</span></button>
            : <button onClick={() => { onClose(); onSignIn(); }} className="forge-btn forge-btn--primary forge-btn--lg forge-btn--block" style={S('display:flex;align-items:center;justify-content:center;gap:9px;')}>{Logo(18)}<span>Sign in with Mostly Tiny</span></button>}
          {!upgrade && <button onClick={() => { onClose(); go('pricing'); }} style={S('margin-top:12px;font-size:13px;font-weight:600;color:var(--fg-muted);background:none;border:none;cursor:pointer;')}>View Pro plans</button>}
        </div>
      </div>
    );
  }

  // ── LANDING ──
  function Landing({ go, open }) {
    const trust = [tr('mp.landing.trust.free'), tr('mp.landing.trust.noSignup'), tr('mp.landing.trust.noWatermark'), tr('mp.landing.trust.noLimit'), tr('mp.landing.trust.noAds')];
    const why = [
      { icon: 'zap', title: tr('mp.landing.why.free.title'), body: tr('mp.landing.why.free.body') },
      { icon: 'fileText', title: tr('mp.landing.why.convert.title'), body: tr('mp.landing.why.convert.body') },
      { icon: 'code', title: tr('mp.landing.why.api.title'), body: tr('mp.landing.why.api.body') },
    ];
    // Fuzzy tool search: as the visitor types, match across title/answer/slug and show a
    // ranked result grid in place of the category sections (empty query = browse by category).
    const [q, setQ] = useState('');
    const query = q.trim();
    const allTools = CATS.flatMap((c) => c.tools);
    const results = query
      ? allTools
          .map((tool) => ({
            tool,
            s: Math.max(
              fuzzyScore(query, tool.title),
              fuzzyScore(query, tool.answer),
              fuzzyScore(query, tool.slug.replace(/-/g, ' ')),
            ),
          }))
          .filter((x) => x.s > 0)
          .sort((a, b) => b.s - a.s)
          .map((x) => x.tool)
      : null;
    const ToolCard = (tool) => (
      <div key={tool.slug} onClick={() => open(tool.slug)} className="forge-card forge-card--interactive" style={S('padding:18px;cursor:pointer;display:flex;flex-direction:column;')}>
        <div style={S('display:flex;align-items:flex-start;justify-content:space-between;gap:10px;')}>
          <span style={S('display:inline-grid;place-items:center;width:42px;height:42px;border-radius:12px;background:var(--accent-bg-soft);color:var(--accent);')}>{ic(tool.icon, 22)}</span>
          <div style={S('display:flex;gap:6px;')}>
            {tool.pro && <span className="forge-badge forge-badge--accent">{tr('mp.badge.pro')}</span>}
            {tool.soon && <span className="forge-badge">{tr('mp.badge.soon')}</span>}
          </div>
        </div>
        <div style={S('margin-top:14px;font-size:15px;font-weight:600;letter-spacing:-0.012em;')}>{tool.title}</div>
        <div style={S('margin-top:5px;font-size:13px;line-height:1.5;color:var(--fg-muted);text-wrap:pretty;')}>{tool.answer}</div>
      </div>
    );
    return (
      <div>
        <section style={S('max-width:1180px;margin:0 auto;padding:60px 28px 8px;text-align:center;')}>
          <span style={S('display:inline-block;font-size:11px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent);')}>{tr('mp.landing.eyebrow')}</span>
          <h1 style={S('font-family:var(--font-display);font-weight:600;font-size:56px;line-height:1.03;letter-spacing:-0.045em;margin:14px auto 0;max-width:16em;text-wrap:balance;')}>{tr('mp.landing.title')}</h1>
          <p style={S('font-size:17px;line-height:1.55;color:var(--fg-muted);max-width:34em;margin:18px auto 0;text-wrap:pretty;')}>{tr('mp.landing.subtitle')}</p>
          <div style={S('display:flex;justify-content:center;flex-wrap:wrap;gap:10px 22px;margin-top:24px;')}>
            {trust.map((t) => <span key={t} style={S('display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:500;color:var(--fg-muted);')}>{check('var(--good)')}{t}</span>)}
          </div>
          <div style={S('max-width:520px;margin:30px auto 0;position:relative;')}>
            <span style={S('position:absolute;left:16px;top:50%;transform:translateY(-50%);color:var(--fg-subtle);display:grid;place-items:center;')}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8" /><path d="M21 21l-4.3-4.3" /></svg>
            </span>
            <input value={q} onChange={(ev) => setQ(ev.target.value)} placeholder={tr('mp.landing.searchPlaceholder')} style={S('width:100%;height:52px;padding:0 18px 0 46px;font-family:var(--font-sans);font-size:15px;background:var(--bg-elev);color:var(--fg);border:1px solid var(--border);border-radius:999px;box-shadow:var(--shadow-soft);outline:none;')} />
          </div>
        </section>

        <section style={S('max-width:1180px;margin:0 auto;padding:18px 28px 8px;')}>
          {results ? (
            results.length ? (
              <div style={S('margin-top:24px;')}>
                <div style={S('font-size:13px;color:var(--fg-subtle);margin-bottom:16px;')}>{tr('mp.landing.resultsCount', { count: results.length, query })}</div>
                <div style={S('display:grid;grid-template-columns:repeat(auto-fill,minmax(252px,1fr));gap:14px;')}>
                  {results.map((tool) => ToolCard(tool))}
                </div>
              </div>
            ) : (
              <div style={S('text-align:center;padding:56px 0;color:var(--fg-muted);')}>
                <div style={S('font-size:15px;font-weight:600;color:var(--fg);')}>{tr('mp.landing.noResults', { query })}</div>
                <p style={S('font-size:13.5px;margin:8px 0 0;')}>{tr('mp.landing.noResultsHint')}</p>
              </div>
            )
          ) : (
            CATS.map((cat) => (
              <div key={cat.key} style={S('margin-top:40px;')}>
                <div style={S('display:flex;align-items:baseline;gap:12px;margin-bottom:16px;')}>
                  <h2 style={S('font-family:var(--font-display);font-size:20px;font-weight:600;letter-spacing:-0.022em;margin:0;')}>{cat.name}</h2>
                  <span style={S('font-size:13px;color:var(--fg-subtle);')}>{cat.note}</span>
                </div>
                <div style={S('display:grid;grid-template-columns:repeat(auto-fill,minmax(252px,1fr));gap:14px;')}>
                  {cat.tools.map((tool) => ToolCard(tool))}
                </div>
              </div>
            ))
          )}
        </section>

        <section style={S('max-width:1180px;margin:0 auto;padding:64px 28px 8px;')}>
          <div style={S('text-align:center;margin-bottom:36px;')}>
            <span style={S('display:inline-block;font-size:11px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent);')}>{tr('mp.landing.whyEyebrow')}</span>
            <h2 style={S('font-family:var(--font-display);font-size:34px;font-weight:600;letter-spacing:-0.03em;margin:10px 0 0;text-wrap:balance;')}>{tr('mp.landing.whyTitle')}</h2>
          </div>
          <div style={S('display:grid;grid-template-columns:repeat(3,1fr);gap:16px;')}>
            {why.map((w) => (
              <div key={w.title} className="forge-card" style={S('padding:24px;')}>
                <span style={S('display:inline-grid;place-items:center;width:44px;height:44px;border-radius:12px;background:var(--accent-bg-soft);color:var(--accent);margin-bottom:14px;')}>{ic(w.icon, 22)}</span>
                <div style={S('font-family:var(--font-display);font-size:17px;font-weight:600;letter-spacing:-0.018em;')}>{w.title}</div>
                <p style={S('font-size:13.5px;line-height:1.56;color:var(--fg-muted);margin:8px 0 0;text-wrap:pretty;')}>{w.body}</p>
              </div>
            ))}
          </div>
        </section>

        <section style={S('max-width:1180px;margin:0 auto;padding:48px 28px 8px;')}>
          <div style={S('display:flex;align-items:center;justify-content:space-between;gap:28px;flex-wrap:wrap;padding:30px 36px;border-radius:var(--radius-xl);background:var(--brand-midnight);color:#fff;')}>
            <div>
              <div style={S('font-family:var(--font-display);font-size:24px;font-weight:600;letter-spacing:-0.025em;')}>{tr('mp.landing.ctaTitle')}</div>
              <p style={S('font-size:14px;color:rgba(255,255,255,0.62);margin:8px 0 0;max-width:40em;')}>{tr('mp.landing.ctaBody')}</p>
            </div>
            <button onClick={() => go('pricing')} className="forge-btn forge-btn--primary forge-btn--lg">{tr('mp.landing.ctaButton')}</button>
          </div>
        </section>
      </div>
    );
  }

  // ── RUNNER (wired to pdfProcess) ──
  function Runner({ slug, go, open }) {
    const tool = TM[slug] || {};
    const _baseMeta = META[slug] || { inputs: 'single', opt: 'generic', accepts: 'PDF', runLabel: tr('mp.runner.default.runLabel', { tool: tool.title || '' }), workingLabel: tr('mp.runner.default.workingLabel'), successTitle: tr('mp.runner.default.successTitle'), errorMsg: tr('mp.runner.default.errorMsg'), doMore: { slug: 'merge-pdf', label: tr('mp.runner.default.doMore') }, verb: (tool.title || '').toLowerCase(), howto: tr('mp.runner.default.howto'), genericNote: tr('mp.runner.default.genericNote') };
    // Container tools have no hand-written META entry — layer their input shape (option
    // type + accepted upload types) onto the generic default so they're actually usable.
    const OFFICE_ACCEPT = '.doc,.docx,.xls,.xlsx,.ppt,.pptx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation';
    const CONTAINER_META = {
      'compress-pdf': { opt: 'quality', accepts: 'PDF' },
      'pdf-to-jpg': { opt: 'generic', accepts: 'PDF' },
      'word-to-pdf': { opt: 'generic', accepts: 'DOC, DOCX, XLS, PPT', accept: OFFICE_ACCEPT },
      'protect-pdf': { opt: 'password', accepts: 'PDF' },
      'unlock-pdf': { opt: 'password', accepts: 'PDF' },
    };
    const meta = Object.assign({}, _baseMeta, CONTAINER_META[slug] || {});
    const multi = meta.inputs === 'multi';
    const minFiles = meta.min || 1;

    const [files, setFiles] = useState([]);
    const [phase, setPhase] = useState('idle'); // idle | files | working | success | error | paywall
    const [slow, setSlow] = useState(false); // still working after a few seconds → reassure (cold start)
    const [dragOn, setDragOn] = useState(false);
    const [errMsg, setErrMsg] = useState(meta.errorMsg);
    const [result, setResult] = useState(null);
    const [faqOpen, setFaqOpen] = useState(0);
    const [preview, setPreview] = useState(null); // { url, name, size, kind:'pdf'|'image', download?, revoke? }
    const [resultSel, setResultSel] = useState(0); // which output is shown in the big inline preview
    const [chainQuery, setChainQuery] = useState(''); // "do more with this file" tool search
    const [authGate, setAuthGate] = useState(false); // pro-tool "sign in required" popup
    const [resume, setResume] = useState(false); // restored a file after SSO → auto-continue once signed in
    const inputRef = useRef(null);
    // options
    const [rotation, setRotation] = useState(90);
    const [wmText, setWmText] = useState('CONFIDENTIAL');
    const [wmOpacity, setWmOpacity] = useState(0.18);
    const [wmPos, setWmPos] = useState('center');
    const [pnPos, setPnPos] = useState('bottom-center');
    const [pnStart, setPnStart] = useState(1);
    const [splitMode, setSplitMode] = useState('ranges');
    const [splitRanges, setSplitRanges] = useState('1-3, 5, 8-12');
    const [splitEvery, setSplitEvery] = useState(2);
    const [pagesVal, setPagesVal] = useState('');
    const [compressQuality, setCompressQuality] = useState('ebook');
    const [pw, setPw] = useState('');
    const [orderVal, setOrderVal] = useState('');
    // e-sign options (F184)
    const [sigVal, setSigVal] = useState(null); // SignatureInput value
    const [sigPlace, setSigPlace] = useState([]); // one placement box (normalized coords)
    const [sigSizes, setSigSizes] = useState({}); // {page: {w,h}} in PDF points
    const [p12, setP12] = useState(null); // { name, base64 }
    const [p12Pass, setP12Pass] = useState('');
    const [certReason, setCertReason] = useState('');
    const [certName, setCertName] = useState('');

    useEffect(() => {
      // Chained from another tool? Seed this tool's input with the handed-off result file(s).
      if (pendingChain && pendingChain.slug === slug && pendingChain.files && pendingChain.files.length) {
        setFiles(pendingChain.files);
        setPhase('files');
        setResult(null); setResultSel(0); setChainQuery(''); setErrMsg(meta.errorMsg); setAuthGate(false); closePreview();
        pendingChain = null; // consume once
        return;
      }
      setFiles([]); setPhase('idle'); setResult(null); setResultSel(0); setChainQuery(''); setErrMsg(meta.errorMsg); setAuthGate(false); closePreview();
      // Returning from "Sign in with Mostly Tiny"? Restore the file we stashed before the redirect
      // so the user continues this tool without re-uploading, then auto-run once the SSO exchange
      // has signed them in (a Pro subscriber runs; a signed-in free user gets the upgrade prompt).
      let cancelled = false; let unsub = null;
      (async () => {
        const p = await idbGet(PENDING_KEY);
        if (cancelled || !p || p.slug !== slug || !p.files || !p.files.length) return;
        await idbDel(PENDING_KEY);
        if (cancelled) return;
        setFiles(p.files); setPhase('files');
        const fb = initFirebase();
        if (fb && fb.auth) { try { unsub = fb.auth.onAuthStateChanged((u) => { if (u && !cancelled) setResume(true); }); } catch (e) {} }
      })();
      return () => { cancelled = true; try { if (unsub) unsub(); } catch (e) {} };
    }, [slug]);
    // Auto-continue a restored run once we're both signed in (resume) and the file is loaded.
    useEffect(() => { if (resume && files.length && phase === 'files') { setResume(false); run(); } }, [resume, files, phase]);
    // After ~3.5s still processing, show a reassurance line — the conversion engine can
    // cold-start on the first run (Cloud Run scale-to-zero), so a longer wait is normal,
    // not stuck. Fast (pure) ops finish well before this fires and never show it.
    useEffect(() => {
      if (phase !== 'working') { setSlow(false); return; }
      const t = setTimeout(() => setSlow(true), 3500);
      return () => clearTimeout(t);
    }, [phase]);

    function addFiles(list) {
      const arr = Array.prototype.slice.call(list || []);
      if (!arr.length) return;
      setFiles((prev) => (multi ? prev.concat(arr) : arr.slice(0, 1)));
      setPhase('files');
    }
    function removeAt(i) { setFiles((prev) => { const n = prev.filter((_, j) => j !== i); if (!n.length) setPhase('idle'); return n; }); }

    // Preview an added input file — object URL is created here and revoked in closePreview.
    function previewFile(f) {
      setPreview({ url: URL.createObjectURL(f), name: f.name, size: fmtSize(f.size), kind: isImageInput(f) ? 'image' : 'pdf', revoke: true });
    }
    // Preview a processed result — reuses the download's existing object URL (don't revoke it).
    function previewResult(dl) {
      const isImg = /\.(png|jpe?g|webp|gif|bmp|avif)$/i.test(dl.name || '');
      setPreview({ url: dl.url, name: dl.name, size: dl.size, kind: isImg ? 'image' : 'pdf', download: true, revoke: false });
    }
    function closePreview() { setPreview((p) => { if (p && p.revoke) { try { URL.revokeObjectURL(p.url); } catch (e) { /* ignore */ } } return null; }); }

    function collectOptions() {
      switch (meta.opt) {
        case 'split': return splitMode === 'ranges' ? { ranges: splitRanges.split(',').map((s) => s.trim()).filter(Boolean) } : splitMode === 'every' ? { everyN: Number(splitEvery) } : {};
        case 'rotate': return { degrees: rotation };
        case 'watermark': return { text: wmText, opacity: Number(wmOpacity) };
        case 'pagenum': return { position: pnPos, start: Number(pnStart) };
        case 'pages': return { pages: pagesVal };
        case 'order': return { order: orderVal.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n)) };
        case 'images': return {};
        case 'quality': return { quality: compressQuality };
        case 'password': return { password: pw };
        case 'sign': {
          // Convert the placed box (fractions from top-left) into the engine's point
          // coords (origin bottom-left) using the real page size from the preview;
          // A4 fallback if the preview couldn't load.
          const p = sigPlace[0];
          const ps = (p && sigSizes[p.page]) || { w: 595.28, h: 841.89 };
          const base = { page: p ? p.page : 1, x: p ? p.x * ps.w : 56, y: p ? (1 - p.y - p.h) * ps.h : 64 };
          if (sigVal && sigVal.imageBase64) {
            return { ...base, type: 'image', image: { base64: sigVal.imageBase64, type: sigVal.imageType }, width: p ? p.w * ps.w : 180 };
          }
          return { ...base, type: 'text', text: (sigVal && sigVal.text) || '', fontSize: p ? Math.max(8, Math.round(p.h * ps.h * 0.6)) : 18 };
        }
        case 'signcert': return { p12: p12 && p12.base64, passphrase: p12Pass, reason: certReason || undefined, name: certName || undefined };
        default: return {};
      }
    }

    async function run() {
      // Pro tools: a signed-in Pro user runs them straight from the web (server checks the plan).
      // Only when NOT signed in do we pop the calm "sign in with Mostly Tiny" gate up front.
      if (tool.pro) {
        const u = await authUser();
        if (!u) { setAuthGate('signin'); return; }
      }
      if (files.length < minFiles) { setErrMsg(meta.errorMsg); setPhase('error'); return; }
      if (meta.opt === 'password' && !pw.trim()) { setErrMsg(tr('mp.opt.passwordRequired')); setPhase('error'); return; }
      // Never submit an unsignable request (empty options would 400 server-side — P7).
      if (meta.opt === 'sign' && !(sigVal && (sigVal.imageBase64 || (sigVal.text || '').trim()))) { setErrMsg('Add your signature first — draw, type or upload it.'); setPhase('error'); return; }
      if (meta.opt === 'signcert' && !(p12 && p12.base64)) { setErrMsg('Upload your .p12/.pfx certificate first.'); setPhase('error'); return; }
      setPhase('working');
      try {
        const b64 = await Promise.all(files.map(fileToB64));
        const res = await fetch(CFG.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(await appCheckHeader()), ...(await authHeader()) }, body: JSON.stringify({ tool: slug, options: collectOptions(), files: b64 }) });
        const j = await res.json().catch(() => ({}));
        // A forbidden response (paid/entitlement) → same sign-in popup, not a red error.
        // 403 after passing the sign-in check → a signed-in FREE user on a Pro tool: prompt upgrade.
        if (res.status === 403) { setPhase(files.length ? 'files' : 'idle'); setAuthGate(tool.pro ? 'upgrade' : 'signin'); return; }
        if (!res.ok) throw new Error(j.error || 'Processing failed');
        const dls = j.outputs.map((b, i) => { const bin = atob(b); const u = new Uint8Array(bin.length); for (let k = 0; k < bin.length; k++) u[k] = bin.charCodeAt(k); const name = (j.filenames && j.filenames[i]) || (slug + '.pdf'); const isPdf = /pdf/i.test(j.contentType || '') || /\.pdf$/i.test(name); const url = URL.createObjectURL(new Blob([u], { type: j.contentType || 'application/pdf' })); return { name, size: fmtSize(u.length), url, ar: isPdf ? pdfAspect(bin) : null }; });
        setResult({ downloads: dls });
        setResultSel(0);
        setPhase('success');
        // Funnel top (free→paid contract): which tool delivered value, pre-signup/anonymous.
        try { if (window.MPAnalytics) window.MPAnalytics.track(window.MPAnalytics.EVENTS.TOOL_USED, { tool: slug, type: meta.opt }); } catch (e) {}
      } catch (err) {
        setErrMsg(err.message + (/Failed to fetch|NetworkError/i.test(err.message) ? tr('mp.runner.networkHint') : ''));
        setPhase('error');
      }
    }

    // Start the UNIFIED "Sign in with Mostly Tiny" flow (MostlyID SSO, identitySSO:true) — the same
    // path as the #/keys dashboard. Redirects to the IdP and returns to #/keys, where the mount
    // effect exchanges the identity token for a session. Falls back to routing to #/keys (which
    // hosts the same button) if the auth kit isn't present.
    async function startProSignIn() {
      // Stash the current file + tool so we return HERE with progress intact (no re-upload), then
      // redirect back to THIS tool route (…/#/<slug>), not the keys dashboard.
      try { if (files && files.length) await idbPut(PENDING_KEY, { slug, files }); } catch (e) {}
      try {
        const cur = (typeof window !== 'undefined' ? window.location.href : '');
        window.MostlyAuth.redirectToIdentitySignIn({
          idpLoginUrl: CFG.idpLoginUrl || 'https://id.mostlytiny.io/login',
          product: 'mostlysign',
          // Hashless — the IdP appends `#token=…`; a hash route in the continue collides with it.
          continueUrl: cur.split('#')[0],
        });
      } catch (e) { go('keys'); }
    }

    const accentChip = 'display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 13px;border-radius:999px;font-size:12.5px;font-weight:600;border:1px solid transparent;cursor:pointer;';
    const onChip = accentChip + 'background:var(--accent-bg-soft);color:var(--accent);';
    const offChip = accentChip + 'background:var(--bg-chip);color:var(--fg-muted);';
    const dropStyle = 'display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:48px 24px;border-radius:var(--radius-xl);border:2px dashed ' + (dragOn ? 'var(--accent)' : 'var(--border-strong)') + ';background:' + (dragOn ? 'var(--accent-bg-soft)' : 'var(--bg-elev)') + ';cursor:pointer;transition:border-color .15s,background .15s,transform .15s;transform:' + (dragOn ? 'scale(1.01)' : 'none') + ';';
    const cellBase = 'height:26px;border-radius:6px;border:1px solid var(--border);background:var(--bg);cursor:pointer;';
    const cellOn = 'height:26px;border-radius:6px;border:1px solid var(--accent);background:var(--accent-bg-soft);cursor:pointer;';

    // The "is it free?" FAQ must match the tool's tier — a paid tool answering "completely
    // free" directly contradicts its own Pro paywall (QA finding). Paid tools get the Pro answer.
    const faqList = [
      tool.pro
        ? { q: tr('mp.runner.faq.pro.q', { tool: tool.title }), a: tr('mp.runner.faq.pro.a', { tool: tool.title }) }
        : { q: tr('mp.runner.faq.free.q', { tool: tool.title }), a: tr('mp.runner.faq.free.a', { tool: tool.title }) },
      { q: tr('mp.runner.faq.how.q', { verb: meta.verb }), a: tr('mp.runner.faq.how.a', { howto: meta.howto, runLabel: meta.runLabel }) },
      { q: tr('mp.runner.faq.safe.q'), a: tr('mp.runner.faq.safe.a') },
    ];

    const twoCol = phase === 'idle' || phase === 'files' || phase === 'error' || phase === 'paywall';
    const showDrop = phase === 'idle' || phase === 'error' || phase === 'paywall';

    return (
      <div style={S('max-width:1100px;margin:0 auto;padding:26px 28px 8px;')}>
        <div onClick={() => go('landing')} style={S('display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--fg-muted);cursor:pointer;margin-bottom:18px;')}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5" /><path d="M12 19l-7-7 7-7" /></svg>
          {tr('mp.nav.allTools')} <span style={S('color:var(--fg-subtle);')}>/</span> <span style={S('color:var(--fg);font-weight:500;')}>{tool.category}</span>
        </div>

        <div style={S('display:flex;align-items:flex-start;gap:16px;')}>
          <span style={S('display:inline-grid;place-items:center;width:54px;height:54px;border-radius:15px;background:var(--accent-bg-soft);color:var(--accent);flex:none;')}>{ic(tool.icon, 26)}</span>
          <div>
            <div style={S('display:flex;align-items:center;gap:10px;')}>
              <h1 style={S('font-family:var(--font-display);font-size:30px;font-weight:600;letter-spacing:-0.03em;margin:0;')}>{tool.title}</h1>
              {tool.pro && <span className="forge-badge forge-badge--accent">{tr('mp.badge.pro')}</span>}
              {tool.soon && <span className="forge-badge">{tr('mp.badge.soon')}</span>}
            </div>
            <p style={S('font-size:15px;line-height:1.5;color:var(--fg-muted);margin:7px 0 0;max-width:42em;text-wrap:pretty;')}>{tool.answer}</p>
          </div>
        </div>

        <div style={S('display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:22px;padding-bottom:18px;border-bottom:1px solid var(--hairline);')}>
          <span style={S('font-size:12px;font-weight:600;color:var(--fg-subtle);text-transform:uppercase;letter-spacing:0.05em;margin-right:2px;')}>{tr('mp.runner.toolLabel')}</span>
          {RUNNER_TABS.map((rt) => <button key={rt} onClick={() => open(rt)} style={S(rt === slug ? onChip : offChip)}>{TM[rt].title}</button>)}
        </div>

        {tool.soon && (
          <div style={S('display:flex;align-items:center;gap:12px;margin-top:22px;padding:16px 20px;border-radius:var(--radius);background:var(--warn-soft);border:1px solid color-mix(in srgb,var(--warn) 28%,transparent);')}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--warn)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>
            <div style={S('font-size:13.5px;color:var(--fg);')}><strong style={S('font-weight:600;')}>{tr('mp.runner.soonBannerLead')}</strong> {tr('mp.runner.soonBannerBody')}</div>
          </div>
        )}

        {phase === 'working' && (
          <div className="forge-card" style={S('margin-top:26px;padding:44px;text-align:center;max-width:560px;margin-left:auto;margin-right:auto;')}>
            <div style={S('font-family:var(--font-display);font-size:19px;font-weight:600;letter-spacing:-0.02em;')}>{meta.workingLabel}</div>
            {/* Indeterminate bar: a segment sliding across, forever — never fills-and-freezes,
                so a slow cold-start conversion reads as "working", not "stuck". */}
            <div style={S('position:relative;height:8px;border-radius:999px;background:var(--bg-chip);overflow:hidden;margin:22px 0 12px;')}>
              <div style={S('position:absolute;top:0;width:40%;height:100%;border-radius:999px;background:var(--accent-grad);animation:pdf-bar-indet 1.15s var(--ease-out) infinite;')}></div>
            </div>
            <div style={S('font-size:12.5px;color:var(--fg-subtle);')}>{slow ? tr('mp.runner.processingSlow') : tr('mp.runner.processing')}</div>
          </div>
        )}

        {phase === 'success' && result && (() => {
          const outs = result.downloads;
          const sel = outs[Math.min(resultSel, outs.length - 1)] || outs[0];
          const selIsImg = /\.(png|jpe?g|webp|gif|bmp|avif)$/i.test(sel.name || '');
          const dlIcon = <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="M7 10l5 5 5-5" /><path d="M12 15V3" /></svg>;
          // Preview box is always the full container width; its height follows the document's own
          // page ratio so the whole page is visible (PDFs → parsed /MediaBox, A4 fallback; images
          // size naturally). previewBox = the container style for the selected output.
          const previewBox = 'position:relative;width:100%;border-radius:var(--radius-lg);overflow:hidden;border:1px solid var(--border);background:var(--bg-chip);'
            + (selIsImg ? '' : 'aspect-ratio:' + (sel.ar || '595 / 842') + ';');

          // "Do more with this file" — chain the result straight into another tool. Candidates
          // are tools that can INGEST this output (PDF output → PDF-input tools; image output →
          // JPG to PDF). Suggested = a curated few; the search box reaches every other tool.
          const allTools = Object.keys(TM).map((k) => TM[k]);
          const PDF_INPUT_BLOCK = { 'jpg-to-pdf': 1, 'word-to-pdf': 1, 'html-to-pdf': 1, 'url-to-pdf': 1 };
          const chainTools = allTools.filter((t) => t.slug !== slug && !t.soon
            && (selIsImg ? t.slug === 'jpg-to-pdf' : !PDF_INPUT_BLOCK[t.slug]));
          const CHAIN_PRIORITY = ['compress-pdf', 'merge-pdf', 'split-pdf', 'rotate-pdf', 'watermark-pdf', 'page-numbers', 'organize-pdf', 'protect-pdf', 'extract-pages', 'remove-pages'];
          const suggested = [meta.doMore && meta.doMore.slug].concat(CHAIN_PRIORITY)
            .filter((s, i, a) => s && a.indexOf(s) === i)
            .map((s) => TM[s]).filter((t) => t && chainTools.indexOf(t) >= 0).slice(0, 5);
          const baseList = suggested.length ? suggested : chainTools.slice(0, 6);
          const cq = chainQuery.trim();
          const chainList = cq
            ? chainTools.map((t) => ({ t, s: Math.max(fuzzyScore(cq, t.title), fuzzyScore(cq, t.slug.replace(/-/g, ' ')), fuzzyScore(cq, t.answer || '')) }))
                .filter((x) => x.s > 0).sort((a, b) => b.s - a.s).map((x) => x.t)
            : baseList;
          // Pipe the result blob(s) back into a File and hand off. A multi-input target (merge)
          // with several outputs (split) receives them ALL; otherwise just the selected output.
          const useInTool = async (targetSlug) => {
            try {
              const targetMulti = (META[targetSlug] || {}).inputs === 'multi';
              const picks = (targetMulti && outs.length > 1) ? outs : [sel];
              const files = await Promise.all(picks.map(async (d) => {
                const blob = await fetch(d.url).then((r) => r.blob());
                return new File([blob], d.name, { type: blob.type || 'application/pdf' });
              }));
              pendingChain = { slug: targetSlug, files };
            } catch (e) { pendingChain = null; }
            open(targetSlug);
          };
          return (
          <div className="forge-card" style={S('margin-top:26px;padding:22px;max-width:1040px;margin-left:auto;margin-right:auto;animation:pdf-rise .3s var(--ease-out);')}>
            <div style={S('display:flex;align-items:center;gap:12px;margin-bottom:16px;')}>
              <span style={S('display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:var(--good-soft);color:var(--good);flex:none;animation:pdf-pop .32s var(--ease-spring);')}>{check('currentColor', 22)}</span>
              <div style={S('flex:1;min-width:0;')}>
                <div style={S('font-family:var(--font-display);font-size:19px;font-weight:600;letter-spacing:-0.02em;')}>{meta.successTitle}</div>
                <div style={S('font-size:12.5px;color:var(--fg-muted);margin-top:2px;')}>{tr('mp.runner.filesReady', { count: outs.length })}</div>
              </div>
              <button onClick={() => { setFiles([]); setResult(null); setPhase('idle'); }} className="forge-btn forge-btn--secondary forge-btn--sm">{tr('mp.runner.startOver')}</button>
            </div>

            {/* Large interactive preview of the (selected) output, rendered right in the container.
                PDFs use the browser's native <iframe> viewer (scroll/zoom); images render inline. */}
            <div style={S(previewBox)}>
              {selIsImg
                ? <img src={sel.url} alt={sel.name} style={S('display:block;width:100%;height:auto;')} />
                : <iframe title={sel.name} src={sel.url + '#toolbar=1&navpanes=0&view=FitH'} style={S('width:100%;height:100%;border:none;background:#fff;')} />}
              <button onClick={() => previewResult(sel)} className="forge-iconbtn" title="Open fullscreen" aria-label="Open fullscreen" style={S('position:absolute;top:10px;right:10px;background:var(--bg-elev);border:1px solid var(--border);box-shadow:var(--shadow-xs);')}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M9 21H3v-6" /><path d="M21 3l-7 7" /><path d="M3 21l7-7" /></svg>
              </button>
            </div>

            {outs.length > 1 ? (
              /* Multiple outputs (e.g. split): a selectable strip — click to preview, download each. */
              <div style={S('display:flex;gap:8px;overflow-x:auto;margin-top:14px;padding-bottom:2px;')}>
                {outs.map((dl, i) => (
                  <div key={i} onClick={() => setResultSel(i)} title="Click to preview" style={S('display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:var(--radius);border:1px solid ' + (i === resultSel ? 'var(--accent)' : 'var(--border)') + ';background:' + (i === resultSel ? 'var(--accent-bg-soft)' : 'var(--bg)') + ';cursor:pointer;flex:none;')}>
                    <div style={S('min-width:0;')}>
                      <div style={S('font-size:12.5px;font-weight:600;white-space:nowrap;')}>{dl.name}</div>
                      <div style={S('font-size:11px;color:var(--fg-subtle);')}>{dl.size}</div>
                    </div>
                    <a href={dl.url} download={dl.name} onClick={(ev) => ev.stopPropagation()} className="forge-iconbtn forge-iconbtn--sm" title={tr('mp.runner.download')} aria-label={tr('mp.runner.download')}>{dlIcon}</a>
                  </div>
                ))}
              </div>
            ) : (
              <div style={S('display:flex;align-items:center;gap:12px;margin-top:14px;padding:12px 14px;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg);')}>
                <span style={S('display:inline-grid;place-items:center;width:34px;height:34px;border-radius:8px;background:var(--accent-bg-soft);color:var(--accent);flex:none;')}>
                  <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" /><path d="M14 2v7h7" /></svg>
                </span>
                <div style={S('flex:1;min-width:0;')}>
                  <div style={S('font-size:13.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;')}>{sel.name}</div>
                  <div style={S('font-size:11.5px;color:var(--fg-subtle);')}>{sel.size}</div>
                </div>
                <a href={sel.url} download={sel.name} className="forge-btn forge-btn--primary forge-btn--sm">{dlIcon}<span>{tr('mp.runner.download')}</span></a>
              </div>
            )}

            <div style={S('margin-top:18px;padding-top:16px;border-top:1px solid var(--hairline);')}>
              <div style={S('display:flex;align-items:center;justify-content:space-between;gap:12px 16px;flex-wrap:wrap;margin-bottom:12px;')}>
                <div>
                  <div style={S('font-size:13.5px;font-weight:600;letter-spacing:-0.01em;')}>Do more with this file</div>
                  <div style={S('font-size:12px;color:var(--fg-subtle);margin-top:2px;')}>Keep going — the result becomes the next tool's input, no re-upload.</div>
                </div>
                <div style={S('position:relative;width:240px;max-width:100%;')}>
                  <span style={S('position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--fg-subtle);display:grid;place-items:center;pointer-events:none;')}>
                    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8" /><path d="M21 21l-4.3-4.3" /></svg>
                  </span>
                  <input value={chainQuery} onChange={(ev) => setChainQuery(ev.target.value)} placeholder="Search all tools…" className="forge-input" style={S('height:38px;padding-left:34px;font-size:13px;')} />
                </div>
              </div>
              {chainList.length ? (
                <div style={S('display:flex;flex-wrap:wrap;gap:8px;')}>
                  {chainList.map((t) => (
                    <button key={t.slug} onClick={() => useInTool(t.slug)} title={'Send this file to ' + t.title} className="pdf-chainchip"
                      style={S('display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 13px;border-radius:10px;border:1px solid var(--border);background:var(--bg);color:var(--fg);font-family:var(--font-sans);font-size:13px;font-weight:600;cursor:pointer;transition:border-color .12s,background .12s;')}>
                      <span style={S('display:inline-grid;place-items:center;color:var(--accent);')}>{ic(t.icon, 16)}</span>
                      {t.title}
                      {t.pro && <span className="forge-badge forge-badge--accent">{tr('mp.badge.pro')}</span>}
                    </button>
                  ))}
                </div>
              ) : (
                <div style={S('font-size:13px;color:var(--fg-muted);padding:4px 0;')}>No tools match “{cq}”.</div>
              )}
            </div>
          </div>
          );
        })()}

        {twoCol && (
          <div style={S('display:grid;grid-template-columns:1.45fr 1fr;gap:20px;margin-top:24px;align-items:start;')}>
            <div>
              {showDrop ? (
                <div>
                  <div onClick={() => inputRef.current && inputRef.current.click()}
                    onDragOver={(ev) => { ev.preventDefault(); setDragOn(true); }}
                    onDragLeave={() => setDragOn(false)}
                    onDrop={(ev) => { ev.preventDefault(); setDragOn(false); addFiles(ev.dataTransfer.files); }}
                    style={S(dropStyle)}>
                    <span style={S('display:grid;place-items:center;width:60px;height:60px;border-radius:16px;background:var(--accent-bg-soft);color:var(--accent);margin-bottom:16px;')}>
                      <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="M17 8l-5-5-5 5" /><path d="M12 3v12" /></svg>
                    </span>
                    <div style={S('font-family:var(--font-display);font-size:18px;font-weight:600;letter-spacing:-0.018em;')}>{dragOn ? tr('mp.runner.dropActive') : tr('mp.runner.dropIdle')}</div>
                    <div style={S('font-size:13.5px;color:var(--fg-muted);margin-top:5px;')}>{tr('mp.runner.orBrowsePre')}<span style={S('color:var(--accent);font-weight:600;')}>{tr('mp.runner.browseFiles')}</span></div>
                    <div style={S('font-size:12px;color:var(--fg-subtle);margin-top:14px;')}>{tr('mp.runner.acceptsLimit', { accepts: meta.accepts })}</div>
                  </div>
                  {phase === 'error' && (
                    <div style={S('display:flex;align-items:center;gap:10px;margin-top:12px;padding:12px 15px;border-radius:var(--radius);background:var(--bad-soft);border:1px solid color-mix(in srgb,var(--bad) 24%,transparent);')}>
                      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--bad)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8v5" /><path d="M12 16h.01" /></svg>
                      <div style={S('font-size:13px;color:var(--bad);font-weight:500;')}>{errMsg}</div>
                    </div>
                  )}
                  {phase === 'paywall' && (
                    <div style={S('margin-top:12px;padding:16px 18px;border-radius:var(--radius);background:var(--accent-bg-soft);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);')}>
                      <div style={S('display:flex;align-items:center;gap:9px;')}>
                        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="10" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
                        <div style={S('font-size:14px;font-weight:600;color:var(--fg);')}>{tr('mp.runner.paywall.lead', { tool: tool.title || '' })}</div>
                      </div>
                      <p style={S('font-size:13px;line-height:1.55;color:var(--fg-muted);margin:8px 0 12px;')}>{tr('mp.runner.paywall.body')}</p>
                      <button onClick={() => go('pricing')} className="forge-btn forge-btn--primary"><span>{tr('mp.runner.paywall.cta')}</span></button>
                    </div>
                  )}
                </div>
              ) : (
                <div>
                  <div className="forge-card" style={S('padding:8px;')}>
                    <div style={S('display:flex;align-items:center;justify-content:space-between;padding:10px 12px 8px;')}>
                      <span style={S('font-size:12px;font-weight:600;color:var(--fg-subtle);text-transform:uppercase;letter-spacing:0.05em;white-space:nowrap;')}>{tr('mp.runner.fileCount', { count: files.length })}</span>
                    </div>
                    <div style={S('display:flex;flex-direction:column;gap:6px;')}>
                      {files.map((f, i) => (
                        <div key={i} className="pdf-filerow" style={S('display:flex;align-items:center;gap:11px;padding:9px 10px;border-radius:var(--radius);background:var(--bg);')}>
                          <div onClick={() => previewFile(f)} title="Click to preview" style={S('display:flex;align-items:center;gap:11px;flex:1;min-width:0;cursor:pointer;')}>
                            {isImageInput(f)
                              ? <ImgThumb file={f} />
                              : <span style={S('display:inline-grid;place-items:center;width:36px;height:44px;border-radius:6px;background:#fff;border:1px solid var(--border);font-size:8px;font-weight:700;letter-spacing:0.02em;color:var(--accent);flex:none;')}>{(f.name.split('.').pop() || 'PDF').toUpperCase().slice(0, 4)}</span>}
                            <div style={S('flex:1;min-width:0;')}>
                              <div style={S('font-size:13.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;')}>{f.name}</div>
                              <div style={S('font-size:11.5px;color:var(--fg-subtle);')}>{fmtSize(f.size)}</div>
                            </div>
                          </div>
                          <button onClick={() => previewFile(f)} className="forge-iconbtn forge-iconbtn--sm" aria-label="Preview file" title="Preview">{eye(16)}</button>
                          <button onClick={() => removeAt(i)} className="forge-iconbtn forge-iconbtn--sm" aria-label={tr('mp.runner.remove')}>
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="M6 6l12 12" /></svg>
                          </button>
                        </div>
                      ))}
                    </div>
                    {multi && (
                      <button onClick={() => inputRef.current && inputRef.current.click()} style={S('display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:6px;padding:11px;border-radius:var(--radius);border:1px dashed var(--border-strong);background:transparent;color:var(--fg-muted);font-size:13px;font-weight:500;cursor:pointer;')}>
                        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14" /><path d="M5 12h14" /></svg>
                        {tr('mp.runner.addMore')}
                      </button>
                    )}
                  </div>
                  <div style={S('display:flex;align-items:center;gap:8px;margin-top:12px;font-size:12px;color:var(--fg-subtle);')}>
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg>
                    {tr('mp.runner.encryptionNote')}
                  </div>
                </div>
              )}
            </div>

            <div className="forge-card" style={S('padding:20px;position:sticky;top:84px;')}>
              <div style={S('font-family:var(--font-display);font-size:16px;font-weight:600;letter-spacing:-0.016em;margin-bottom:16px;')}>{tr('mp.opt.options')}</div>

              {meta.opt === 'merge' && <p style={S('font-size:13px;line-height:1.55;color:var(--fg-muted);margin:0 0 4px;')}>{tr('mp.opt.mergeNote')}</p>}

              {meta.opt === 'split' && (
                <div>
                  <div style={S('display:flex;flex-direction:column;gap:6px;margin-bottom:14px;')}>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.splitMode')}</span>
                    <div className="forge-select-wrap">
                      <select value={splitMode} onChange={(ev) => setSplitMode(ev.target.value)} className="forge-select">
                        <option value="ranges">{tr('mp.opt.splitRangesOpt')}</option>
                        <option value="each">{tr('mp.opt.splitEachOpt')}</option>
                        <option value="every">{tr('mp.opt.splitEveryOpt')}</option>
                      </select>
                      <span className="forge-select-wrap__chev"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 9l6 6 6-6" /></svg></span>
                    </div>
                  </div>
                  {splitMode === 'ranges' && (
                    <div style={S('display:flex;flex-direction:column;gap:6px;')}>
                      <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.pageRanges')}</span>
                      <input value={splitRanges} onChange={(ev) => setSplitRanges(ev.target.value)} className="forge-input" placeholder="1-3, 5, 8-12" />
                      <span style={S('font-size:11.5px;color:var(--fg-subtle);')}>{tr('mp.opt.separateRanges')}</span>
                    </div>
                  )}
                  {splitMode === 'every' && (
                    <div style={S('display:flex;flex-direction:column;gap:6px;')}>
                      <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.pagesPerFile')}</span>
                      <input value={splitEvery} onChange={(ev) => setSplitEvery(ev.target.value)} type="number" className="forge-input" />
                    </div>
                  )}
                </div>
              )}

              {meta.opt === 'pages' && (
                <div style={S('display:flex;flex-direction:column;gap:6px;')}>
                  <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{meta.pagesLabel}</span>
                  <input value={pagesVal} onChange={(ev) => setPagesVal(ev.target.value)} className="forge-input" placeholder={meta.placeholder} />
                  <span style={S('font-size:11.5px;color:var(--fg-subtle);')}>{tr('mp.opt.pagesHint', { example: meta.placeholder })}</span>
                </div>
              )}

              {meta.opt === 'order' && (
                <div style={S('display:flex;flex-direction:column;gap:6px;')}>
                  <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.newOrder')}</span>
                  <input value={orderVal} onChange={(ev) => setOrderVal(ev.target.value)} className="forge-input" placeholder="3, 1, 2, 4" />
                  <span style={S('font-size:11.5px;color:var(--fg-subtle);')}>{tr('mp.opt.newOrderHint')}</span>
                </div>
              )}

              {meta.opt === 'rotate' && (
                <div>
                  <div style={S('display:flex;flex-direction:column;gap:8px;margin-bottom:16px;')}>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.rotation')}</span>
                    <div style={S('display:flex;gap:8px;')}>
                      {[90, 180, 270].map((a) => <button key={a} onClick={() => setRotation(a)} style={S((a === rotation ? onChip : offChip) + 'min-width:54px;justify-content:center;')}>{a}°</button>)}
                    </div>
                  </div>
                  <div style={S('display:flex;flex-direction:column;align-items:center;gap:8px;padding:18px;border-radius:var(--radius);background:var(--bg);border:1px solid var(--hairline);')}>
                    <div style={S('width:66px;height:84px;background:#fff;border:1px solid var(--border);border-radius:4px;box-shadow:var(--shadow-xs);transition:transform .25s var(--ease-out);transform:rotate(' + rotation + 'deg);')}>
                      <div style={S('font-size:8px;color:var(--fg-subtle);line-height:1.6;padding:8px;')}>
                        <div style={S('width:60%;height:3px;background:var(--fg-subtle);opacity:.4;border-radius:2px;margin-bottom:4px;')}></div>
                        <div style={S('width:90%;height:3px;background:var(--fg-subtle);opacity:.25;border-radius:2px;margin-bottom:3px;')}></div>
                        <div style={S('width:80%;height:3px;background:var(--fg-subtle);opacity:.25;border-radius:2px;')}></div>
                      </div>
                    </div>
                    <span style={S('font-size:11.5px;color:var(--fg-subtle);')}>{tr('mp.opt.rotatePreview', { deg: rotation })}</span>
                  </div>
                </div>
              )}

              {meta.opt === 'watermark' && (
                <div>
                  <div style={S('display:flex;flex-direction:column;gap:6px;margin-bottom:14px;')}>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.watermarkText')}</span>
                    <input value={wmText} onChange={(ev) => setWmText(ev.target.value)} className="forge-input" />
                  </div>
                  <div style={S('display:flex;flex-direction:column;gap:6px;margin-bottom:14px;')}>
                    <div style={S('display:flex;justify-content:space-between;')}><span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.opacity')}</span><span style={S('font-size:12px;color:var(--fg-muted);font-variant-numeric:tabular-nums;')}>{Math.round(wmOpacity * 100)}%</span></div>
                    <input type="range" min="0" max="100" value={Math.round(wmOpacity * 100)} onChange={(ev) => setWmOpacity(ev.target.value / 100)} style={S('width:100%;accent-color:var(--accent);')} />
                  </div>
                  <div style={S('position:relative;display:grid;place-items:' + placeMap[wmPos] + ';padding:14px;border-radius:var(--radius);background:#fff;border:1px solid var(--hairline);min-height:104px;overflow:hidden;')}>
                    <span style={S('font-family:var(--font-display);font-weight:700;font-size:20px;letter-spacing:0.04em;color:var(--accent);transform:rotate(-24deg);opacity:' + wmOpacity + ';white-space:nowrap;')}>{wmText}</span>
                  </div>
                </div>
              )}

              {meta.opt === 'pagenum' && (
                <div>
                  <div style={S('display:flex;flex-direction:column;gap:6px;margin-bottom:14px;')}>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.position')}</span>
                    <div style={S('display:grid;grid-template-columns:repeat(3,1fr);gap:5px;width:108px;')}>
                      {POS.map((k) => <button key={k} onClick={() => setPnPos(k)} style={S(k === pnPos ? cellOn : cellBase)} aria-label={k}></button>)}
                    </div>
                  </div>
                  <div style={S('display:flex;flex-direction:column;gap:6px;')}>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.startAt')}</span>
                    <input value={pnStart} onChange={(ev) => setPnStart(ev.target.value)} type="number" className="forge-input" />
                  </div>
                </div>
              )}

              {meta.opt === 'quality' && (
                <div style={S('display:flex;flex-direction:column;gap:8px;margin-bottom:4px;')}>
                  <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.quality')}</span>
                  <div style={S('display:flex;gap:8px;flex-wrap:wrap;')}>
                    {['screen', 'ebook', 'printer', 'prepress'].map((q) => <button key={q} onClick={() => setCompressQuality(q)} style={S((q === compressQuality ? onChip : offChip) + 'justify-content:center;')}>{tr('mp.opt.quality.' + q)}</button>)}
                  </div>
                </div>
              )}
              {meta.opt === 'password' && (
                <div style={S('display:flex;flex-direction:column;gap:8px;margin-bottom:4px;')}>
                  <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.opt.password')}</span>
                  <input type="password" value={pw} onChange={(ev) => setPw(ev.target.value)} placeholder={tr('mp.opt.passwordPlaceholder')} style={S('width:100%;height:44px;padding:0 14px;font-family:var(--font-sans);font-size:14px;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:var(--radius);outline:none;')} />
                </div>
              )}
              {meta.opt === 'sign' && (
                <div>
                  {SignKit ? (
                    <div>
                      <SignKit.SignatureInput value={sigVal} onChange={setSigVal} />
                      {files.length > 0 && (
                        <div style={S('margin-top:14px;')}>
                          <span style={S('font-size:12px;font-weight:500;color:var(--fg);display:block;margin-bottom:6px;')}>Where should it go? Drag the box into place.</span>
                          <SignKit.PagePlacer file={files[0]} placements={sigPlace} onPlacements={setSigPlace} single={true} onPageSizes={setSigSizes} />
                        </div>
                      )}
                      {!files.length && <p style={S('font-size:12px;color:var(--fg-subtle);margin:10px 0 0;')}>Add your PDF to position the signature on the page (defaults to the lower-left of the last page).</p>}
                    </div>
                  ) : <p style={S('font-size:13px;color:var(--fg-muted);margin:0;')}>Signature tools are loading…</p>}
                </div>
              )}
              {meta.opt === 'signcert' && (
                <div style={S('display:flex;flex-direction:column;gap:12px;')}>
                  <div>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);display:block;margin-bottom:6px;')}>Your PKCS#12 certificate (.p12 / .pfx)</span>
                    <button onClick={() => document.getElementById('mp-p12-file').click()} className="forge-btn forge-btn--secondary" style={S('width:100%;')}>{p12 ? p12.name : 'Choose certificate file'}</button>
                    <input id="mp-p12-file" type="file" accept=".p12,.pfx,application/x-pkcs12" style={S('display:none;')} onChange={async (ev) => { const f = ev.target.files && ev.target.files[0]; if (f) setP12({ name: f.name, base64: await fileToB64(f) }); }} />
                    <span style={S('font-size:11.5px;color:var(--fg-subtle);display:block;margin-top:5px;')}>Used once to sign, never stored.</span>
                  </div>
                  <div>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);display:block;margin-bottom:5px;')}>Certificate passphrase</span>
                    <input type="password" value={p12Pass} onChange={(ev) => setP12Pass(ev.target.value)} className="forge-input" style={S('width:100%;')} />
                  </div>
                  <div>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);display:block;margin-bottom:5px;')}>Reason (optional)</span>
                    <input value={certReason} onChange={(ev) => setCertReason(ev.target.value)} placeholder="I approve this document" className="forge-input" style={S('width:100%;')} />
                  </div>
                  <div>
                    <span style={S('font-size:12px;font-weight:500;color:var(--fg);display:block;margin-bottom:5px;')}>Signer name (optional)</span>
                    <input value={certName} onChange={(ev) => setCertName(ev.target.value)} className="forge-input" style={S('width:100%;')} />
                  </div>
                </div>
              )}
              {(meta.opt === 'images' || meta.opt === 'generic') && <p style={S('font-size:13px;line-height:1.55;color:var(--fg-muted);margin:0;')}>{meta.genericNote || tr('mp.runner.default.genericNote')}</p>}

              <div style={S('margin-top:18px;padding-top:16px;border-top:1px solid var(--hairline);')}>
                <button onClick={run} disabled={tool.soon || files.length < minFiles} className="forge-btn forge-btn--primary forge-btn--lg forge-btn--block"><span>{meta.runLabel}</span></button>
              </div>
            </div>
          </div>
        )}

        <input ref={inputRef} type="file" accept={meta.accept || (slug === 'jpg-to-pdf' ? 'image/jpeg,image/png' : 'application/pdf')} multiple={multi} onChange={(ev) => addFiles(ev.target.files)} />

        <section style={S('max-width:760px;margin:56px auto 0;')}>
          <div style={S('text-align:center;margin-bottom:24px;')}>
            <span style={S('display:inline-block;font-size:11px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent);')}>{tr('mp.runner.goodToKnow')}</span>
            <h2 style={S('font-family:var(--font-display);font-size:26px;font-weight:600;letter-spacing:-0.028em;margin:8px 0 0;')}>{tr('mp.runner.questionsHeading', { tool: tool.title })}</h2>
          </div>
          <div style={S('display:flex;flex-direction:column;gap:10px;')}>
            {faqList.map((f, i) => (
              <div key={i} className="forge-card" style={S('padding:0;overflow:hidden;')}>
                <button onClick={() => setFaqOpen(faqOpen === i ? -1 : i)} style={S('display:flex;align-items:center;justify-content:space-between;gap:14px;width:100%;padding:16px 20px;background:none;border:none;cursor:pointer;text-align:left;')}>
                  <span style={S('font-size:14.5px;font-weight:600;letter-spacing:-0.01em;')}>{f.q}</span>
                  <span style={S('display:grid;place-items:center;color:var(--fg-subtle);transition:transform .18s;flex:none;transform:rotate(' + (faqOpen === i ? '180deg' : '0deg') + ');')}><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 9l6 6 6-6" /></svg></span>
                </button>
                {faqOpen === i && <p style={S('font-size:13.5px;line-height:1.6;color:var(--fg-muted);margin:0;padding:0 20px 18px;text-wrap:pretty;')}>{f.a}</p>}
              </div>
            ))}
          </div>
        </section>

        {preview && <PreviewModal item={preview} onClose={closePreview} />}
        {authGate && <AuthGate mode={authGate} toolTitle={tool.title || 'This tool'} onSignIn={startProSignIn} go={go} onClose={() => setAuthGate(false)} />}
      </div>
    );
  }

  // ── PRICING ──
  function Pricing() {
    const [annual, setAnnual] = useState(true);
    const [busy, setBusy] = useState('');   // plan key mid-checkout
    const [err, setErr] = useState('');
    // Wire each plan's CTA: Pro/Business → Stripe checkout (upgrade), Free → the send flow.
    function onCta(key) {
      setErr('');
      if (key === 'pro' || key === 'business') {
        setBusy(key);
        startCheckout(key, annual).catch((e) => { setErr(String((e && e.message) || e)); setBusy(''); });
      } else { window.location.hash = '#/request-signature'; }
    }
    const seg = 'border:none;background:transparent;padding:8px 18px;border-radius:999px;font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--fg-muted);cursor:pointer;';
    const segSel = 'background:var(--bg-elev);padding:8px 18px;border:none;border-radius:999px;font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--fg);box-shadow:var(--shadow-xs);cursor:pointer;';
    const planCard = 'position:relative;background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius-card);box-shadow:var(--shadow-soft);padding:26px;display:flex;flex-direction:column;';
    const planCardFeat = 'position:relative;background:var(--bg-elev);border:1px solid color-mix(in srgb,var(--accent) 40%,transparent);border-radius:var(--radius-card);box-shadow:0 1px 3px rgba(10,10,26,.04),0 18px 44px color-mix(in srgb,var(--accent) 16%,transparent);padding:26px;display:flex;flex-direction:column;';
    const ctaPrimary = 'margin-top:18px;height:42px;border:1px solid transparent;border-radius:999px;background:var(--accent);color:var(--accent-fg);font-family:var(--font-sans);font-size:14px;font-weight:600;cursor:pointer;box-shadow:0 1px 4px color-mix(in srgb,var(--accent) 28%,transparent);';
    const ctaSecondary = 'margin-top:18px;height:42px;border:1px solid var(--border-strong);border-radius:999px;background:var(--bg-elev);color:var(--fg);font-family:var(--font-sans);font-size:14px;font-weight:500;cursor:pointer;';
    // Plans mirror billing.config.js + the static Home (web/index.html) exactly. Plain strings
    // (like the marketing pages) — no per-envelope caps, no PDF-tool language. Annual billing
    // shows the effective monthly price ($10 Pro / $24 Business, billed yearly).
    const plans = [
      { key: 'free', name: 'Free', amt: '$0', per: 'forever', desc: 'For getting a signature, fast.', cta: 'Start free', feat: false,
        feats: ['3 documents / month', '1 signer', 'Full audit trail on every document', 'PAdES seal + certificate of completion', 'Verify in any PDF reader'] },
      { key: 'pro', name: 'Pro', amt: annual ? '$10' : '$12', per: annual ? '/mo · billed yearly' : '/mo · billed monthly', desc: 'For teams sending every day.', cta: 'Choose Pro', feat: true,
        feats: ['Unlimited documents — no per-envelope caps', '5 reusable templates', 'Automatic reminders & expiry', 'Your logo on the signer page', '3 seats'] },
      { key: 'business', name: 'Business', amt: annual ? '$24' : '$29', per: annual ? '/mo · billed yearly' : '/mo · billed monthly', desc: 'For scale & compliance.', cta: 'Choose Business', feat: false,
        feats: ['Everything in Pro', 'SSO (Sign in with Mostly Tiny)', 'Unlimited templates', 'Signing API + bulk send', '10 seats'] },
    ];
    return (
      <div style={S('max-width:1180px;margin:0 auto;padding:48px 28px 8px;')}>
        <div style={S('text-align:center;')}>
          <span style={S('display:inline-block;font-size:11px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent);')}>Pricing</span>
          <h1 style={S('font-family:var(--font-display);font-size:42px;font-weight:600;letter-spacing:-0.038em;margin:12px 0 0;text-wrap:balance;')}>Honest pricing. No envelope caps.</h1>
          <p style={S('font-size:16px;line-height:1.55;color:var(--fg-muted);max-width:34em;margin:14px auto 0;')}>Free to start — no card, no envelope games. Every completed document is PAdES-sealed and verifiable anywhere, not just here.</p>
          <div style={S('display:inline-flex;align-items:center;gap:4px;padding:4px;border-radius:999px;background:var(--bg-chip);margin-top:24px;')}>
            <button onClick={() => setAnnual(false)} style={S(annual ? seg : segSel)}>Monthly</button>
            <button onClick={() => setAnnual(true)} style={S(annual ? segSel : seg)}>Yearly <span style={S('color:var(--good);font-weight:600;')}>−17%</span></button>
          </div>
        </div>
        <div style={S('display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));justify-content:center;gap:16px;margin-top:40px;align-items:start;')}>
          {plans.map((p) => (
            <div key={p.name} style={S(p.feat ? planCardFeat : planCard)}>
              {p.feat && <span style={S('position:absolute;top:-11px;left:28px;padding:4px 11px;border-radius:999px;background:var(--accent);color:var(--accent-fg);font-size:10.5px;font-weight:700;letter-spacing:0.04em;text-transform:uppercase;')}>Most popular</span>}
              <div style={S('font-family:var(--font-display);font-size:17px;font-weight:600;')}>{p.name}</div>
              <div style={S('display:flex;align-items:baseline;gap:5px;margin-top:10px;')}>
                <span style={S('font-family:var(--font-display);font-size:40px;font-weight:600;letter-spacing:-0.04em;line-height:1;')}>{p.amt}</span>
                <span style={S('font-size:13px;color:var(--fg-subtle);')}>{p.per}</span>
              </div>
              <div style={S('font-size:12.5px;color:var(--fg-muted);line-height:1.5;margin-top:8px;min-height:36px;')}>{p.desc}</div>
              <button onClick={() => onCta(p.key)} disabled={busy === p.key} style={S((p.feat ? ctaPrimary : ctaSecondary) + (busy === p.key ? 'opacity:.7;cursor:default;' : ''))}><span>{busy === p.key ? 'Redirecting…' : p.cta}</span></button>
              <div style={S('height:1px;background:var(--hairline);margin:18px 0;')}></div>
              <ul style={S('list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:11px;')}>
                {p.feats.map((ft) => <li key={ft} style={S('display:flex;align-items:flex-start;gap:9px;font-size:13px;line-height:1.4;color:var(--fg);')}><span style={S('flex:none;margin-top:1px;')}>{check('var(--accent)')}</span>{ft}</li>)}
              </ul>
            </div>
          ))}
        </div>
        {err && <div style={S('text-align:center;margin-top:14px;font-size:13px;color:var(--bad);font-weight:500;')}>{err}</div>}
        <div style={S('text-align:center;margin-top:26px;font-size:12.5px;color:var(--fg-subtle);')}>Every plan includes the full audit trail, the PAdES seal, and the certificate of completion. No hidden envelope caps.</div>
      </div>
    );
  }

  // ── API-KEYS DASHBOARD (#/keys) ──
  // Team-only while PUBLIC_APP_OPEN is false: not advertised in the public Nav (a subtle footer-
  // style link from the dashboard itself is enough); the team navigates directly to #/keys.
  //
  // SSR-SAFE: this whole component is only rendered for the #/keys route, but even then it never
  // touches window/firebase at render time — all firebase/auth work happens inside useEffect, so
  // the i18n prerender (no DOM) renders the static "loading" shell without throwing.

  // Lazy-initialise the compat Firebase app + auth + functions, reading window.MOSTLYPDF.firebase.
  // Returns { auth, fns } or null if firebase/config isn't available. Idempotent (reuses the app).
  let _emuWired = false;
  function initFirebase() {
    if (typeof window === 'undefined') return null;
    const fb = window.firebase;
    const conf = (window.MOSTLYPDF || {}).firebase;
    if (!fb || !fb.initializeApp || !conf) return null;
    try {
      const app = fb.apps && fb.apps.length ? fb.app() : fb.initializeApp(conf);
      const auth = app.auth();
      const fns = app.functions('europe-west2');
      // Local dev (config.js sets `emulators` only on localhost): point auth + callables
      // at the emulator suite, and sign in the seeded Pro user (scripts/dev-seed.js) so
      // paid tools are exercisable without magic-link email. Loud in the console, once.
      if (CFG.emulators && !_emuWired) {
        _emuWired = true;
        try {
          auth.useEmulator('http://127.0.0.1:9099', { disableWarnings: true });
          fns.useEmulator('127.0.0.1', 5001);
          const dev = CFG.devUser;
          if (dev && !auth.currentUser) {
            auth.signInWithEmailAndPassword(dev.email, dev.password)
              .then(() => console.info('[dev] signed in as ' + dev.email + ' (auth emulator, Pro plan)'))
              .catch((e2) => console.warn('[dev] auto sign-in failed — run scripts/dev-seed.js first:', e2.message));
          }
        } catch (e1) { console.warn('[dev] emulator wiring failed:', e1.message); }
      }
      return { auth, fns };
    } catch (err) {
      return null;
    }
  }

  // App Check (F153): if a reCAPTCHA v3 site key is configured, attest each pdfProcess
  // call so the server can reject scripted abuse. Returns the header to merge into the
  // fetch, or {} when App Check isn't configured / unavailable — so the anonymous tools
  // keep working before App Check is set up, and a token hiccup never blocks a request.
  let _appCheckActivated = false;
  async function appCheckHeader() {
    try {
      if (typeof window === 'undefined') return {};
      const key = CFG.appCheckKey;
      const fb = window.firebase;
      const conf = CFG.firebase;
      if (!key || !conf || !fb || !fb.appCheck || !fb.initializeApp) return {};
      const app = fb.apps && fb.apps.length ? fb.app() : fb.initializeApp(conf);
      if (!_appCheckActivated) {
        app.appCheck().activate(new fb.appCheck.ReCaptchaV3Provider(key), true);
        _appCheckActivated = true;
      }
      const r = await app.appCheck().getToken();
      return r && r.token ? { 'X-Firebase-AppCheck': r.token } : {};
    } catch (e) {
      return {};
    }
  }

  // Signed-in entitlement: resolve the current Mostly Tiny user (if any), waiting once for
  // Firebase to restore the persisted session so a signed-in Pro user is recognised even on the
  // landing page. Returns the user or null. Cached after the first auth-state settle.
  let _authReady = null;
  function authUser() {
    const fb = initFirebase();
    if (!fb || !fb.auth) return Promise.resolve(null);
    if (!_authReady) {
      _authReady = new Promise((resolve) => {
        try { const un = fb.auth.onAuthStateChanged((u) => { try { un(); } catch (e) {} resolve(u || null); }); }
        catch (e) { resolve(fb.auth.currentUser || null); }
      });
    }
    return _authReady.then(() => fb.auth.currentUser || null);
  }
  // Attach the signed-in user's Firebase ID token so pdfProcess can honour a Pro plan (paid tools
  // + lifted limits) WITHOUT an API key. {} when signed out — the anonymous free tier is unchanged.
  async function authHeader() {
    try {
      const u = await authUser();
      if (!u) return {};
      const t = await u.getIdToken();
      return t ? { Authorization: 'Bearer ' + t } : {};
    } catch (e) { return {}; }
  }

  // Start a Stripe hosted-Checkout to upgrade to a paid plan. Checkout binds to the account, so a
  // signed-out user is first sent through "Sign in with Mostly Tiny" and returned to /pricing to
  // retry. On success the browser redirects to Stripe. Throws so the caller can clear its busy UI.
  async function startCheckout(plan, annual) {
    const base = (typeof window !== 'undefined' ? window.location.href : '').split('#')[0];
    const u = await authUser();
    if (!u) {
      try {
        window.MostlyAuth.redirectToIdentitySignIn({
          idpLoginUrl: CFG.idpLoginUrl || 'https://id.mostlytiny.io/login',
          product: 'mostlysign',
          continueUrl: base, // hashless — token handoff arrives as `#token=…`
        });
      } catch (e) { window.location.hash = '#/keys'; }
      return;
    }
    const fb = initFirebase();
    if (!fb || !fb.fns) throw new Error('Billing is unavailable right now.');
    const res = await fb.fns.httpsCallable('createCheckoutSession')({
      plan: plan,
      annual: !!annual,
      // ?upgraded=1 is the client funnel marker: App's mount effect turns it into a
      // one-shot PLAN_UPGRADED via MPAnalytics.trackCheckoutReturn (then strips it).
      success_url: base + '?upgraded=1&plan=' + encodeURIComponent(plan) + '#/keys',
      cancel_url: base + '#/pricing',
    });
    const url = res && res.data && res.data.checkout_url;
    if (!url) throw new Error('Could not start checkout.');
    try { if (window.MPAnalytics) window.MPAnalytics.track(window.MPAnalytics.EVENTS.CHECKOUT_STARTED, { plan: plan }); } catch (e) {}
    window.location.assign(url);
  }

  function Keys({ go }) {
    const [ready, setReady] = useState(false);      // firebase initialised?
    const [user, setUser] = useState(null);          // signed-in user | null
    const [authPhase, setAuthPhase] = useState('idle'); // idle | sending | sent | completing | closed | error
    const [email, setEmail] = useState('');
    const [authErr, setAuthErr] = useState('');
    const [keys, setKeys] = useState(null);          // null = not loaded; [] = loaded empty
    const [keysErr, setKeysErr] = useState('');
    const [newName, setNewName] = useState('');
    const [creating, setCreating] = useState(false);
    const [createErr, setCreateErr] = useState('');
    const [secret, setSecret] = useState(null);      // the one-time-shown new secret
    const [copied, setCopied] = useState(false);
    const [busyId, setBusyId] = useState('');        // id being revoked
    const fbRef = useRef(null);                       // { auth, fns }

    // Init firebase + subscribe to auth, and complete a magic-link landing if present.
    // All of this is DOM/firebase work → inside useEffect (never at module/render top-level).
    useEffect(() => {
      const fb = initFirebase();
      fbRef.current = fb;
      if (!fb) { setReady(true); return; }
      let unsub = function () {};
      try {
        unsub = window.MostlyAuth.onAuth(fb.auth, (u) => {
          setUser(u); setReady(true);
          // Stitch the anonymous funnel to the account (anonymous → known, PostHog
          // person_profiles 'identified_only'). No-op when analytics is off/unconsented.
          try { if (u && window.MPAnalytics) window.MPAnalytics.identify(u.uid, { email: u.email || undefined }); } catch (e) {}
        });
      } catch (err) { setReady(true); }
      // Returning from "Sign in with Mostly Tiny" (SSO) lands with an identity token in the
      // URL fragment; a magic-link email lands with the sign-in link. Handle SSO first — it's
      // the unified/primary path.
      try {
        if (window.MostlyAuth.isIdentityLanding && window.MostlyAuth.isIdentityLanding()) {
          setAuthPhase('completing');
          window.MostlyAuth.completeIdentitySignIn({ auth: fb.auth, fns: fb.fns }).catch((err) => {
            if (window.MostlyAuth.isClosedBetaError(err)) setAuthPhase('closed');
            else { setAuthErr(String((err && err.message) || err)); setAuthPhase('error'); }
          });
        } else if (window.MostlyAuth.isMagicLinkLanding(fb.auth)) {
          setAuthPhase('completing');
          window.MostlyAuth.completeMagicLink({ auth: fb.auth }).catch((err) => {
            if (window.MostlyAuth.isClosedBetaError(err)) setAuthPhase('closed');
            else { setAuthErr(String((err && err.message) || err)); setAuthPhase('error'); }
          });
        }
      } catch (err) { /* not a landing */ }
      return () => { try { unsub(); } catch (e) { /* ignore */ } };
    }, []);

    // Load the key list once signed in.
    useEffect(() => {
      const fb = fbRef.current;
      if (!user || !fb) return;
      setKeysErr('');
      fb.fns.httpsCallable('listApiKeys')({})
        .then((res) => setKeys((res && res.data && res.data.keys) || []))
        .catch((err) => setKeysErr(String((err && err.message) || err)));
    }, [user]);

    function sendLink(ev) {
      ev.preventDefault();
      const fb = fbRef.current;
      const addr = email.trim();
      if (!addr || !fb) return;
      setAuthPhase('sending'); setAuthErr('');
      window.MostlyAuth.requestMagicLink({ fns: fb.fns, email: addr, continueUrl: window.location.href })
        .then(() => setAuthPhase('sent'))
        .catch((err) => {
          if (window.MostlyAuth.isClosedBetaError(err)) setAuthPhase('closed');
          else { setAuthErr(String((err && err.message) || err)); setAuthPhase('error'); }
        });
    }

    // "Sign in with Mostly Tiny" — redirect to the MostlyID IdP, which signs the human in and
    // bounces back to this page with an identity token (handled in the mount effect above).
    function startSSO() {
      try {
        window.MostlyAuth.redirectToIdentitySignIn({
          idpLoginUrl: CFG.idpLoginUrl || 'https://id.mostlytiny.io/login',
          product: 'mostlysign',
          continueUrl: window.location.href.split('#')[0], // hashless — token arrives as `#token=…`
        });
      } catch (err) { setAuthErr(String((err && err.message) || err)); setAuthPhase('error'); }
    }

    function signOut() {
      const fb = fbRef.current;
      if (!fb) return;
      window.MostlyAuth.signOut(fb.auth).then(() => { setKeys(null); setSecret(null); });
    }

    function createKey(ev) {
      ev.preventDefault();
      const fb = fbRef.current;
      if (!fb) return;
      setCreating(true); setCreateErr(''); setCopied(false);
      fb.fns.httpsCallable('createApiKey')({ name: newName.trim() || 'API key', scopes: ['pdf:run'] })
        .then((res) => {
          const d = (res && res.data) || {};
          setSecret(d.secret || '');
          setNewName('');
          // Optimistically add the new key to the table (revoked:false), then refresh.
          setKeys((prev) => [{ id: d.id, name: d.name, last4: d.last4, prefix: 'sk', scopes: d.scopes || ['pdf:run'], revoked: false }].concat(prev || []));
        })
        .catch((err) => setCreateErr(String((err && err.message) || err)))
        .finally(() => setCreating(false));
    }

    function revoke(id) {
      const fb = fbRef.current;
      if (!fb) return;
      setBusyId(id);
      fb.fns.httpsCallable('revokeApiKey')({ id })
        .then(() => setKeys((prev) => (prev || []).map((k) => (k.id === id ? Object.assign({}, k, { revoked: true }) : k))))
        .catch((err) => setKeysErr(String((err && err.message) || err)))
        .finally(() => setBusyId(''));
    }

    function copySecret() {
      try {
        if (navigator.clipboard && secret) {
          navigator.clipboard.writeText(secret).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1800); });
        }
      } catch (err) { /* clipboard blocked */ }
    }

    const wrap = 'max-width:860px;margin:0 auto;padding:42px 28px 8px;';
    const headBlock = (
      <div style={S('margin-bottom:26px;')}>
        <span style={S('display:inline-block;font-size:11px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent);')}>{tr('mp.keys.developers')}</span>
        <h1 style={S('font-family:var(--font-display);font-size:34px;font-weight:600;letter-spacing:-0.034em;margin:10px 0 0;')}>{tr('mp.keys.title')}</h1>
        <p style={S('font-size:15px;line-height:1.55;color:var(--fg-muted);max-width:40em;margin:10px 0 0;text-wrap:pretty;')}>{tr('mp.keys.introPre')} <a href="/docs" style={S('color:var(--accent);font-weight:600;text-decoration:none;')}>{tr('mp.keys.apiReference')}</a> {tr('mp.keys.introPost')}</p>
      </div>
    );

    // 1) Booting firebase (also the SSR/prerender render: ready=false, no DOM touched).
    if (!ready) {
      return (
        <div style={S(wrap)}>
          {headBlock}
          <div className="forge-card" style={S('padding:40px;text-align:center;color:var(--fg-muted);font-size:14px;')}>{tr('mp.keys.loading')}</div>
        </div>
      );
    }

    // 2) Firebase unavailable (misconfigured / SDK blocked).
    if (!fbRef.current) {
      return (
        <div style={S(wrap)}>
          {headBlock}
          <div className="forge-card" style={S('padding:28px;')}>
            <div style={S('font-size:14px;color:var(--fg);font-weight:600;')}>{tr('mp.keys.unavailableTitle')}</div>
            <p style={S('font-size:13.5px;color:var(--fg-muted);margin:8px 0 0;line-height:1.55;')}>{tr('mp.keys.unavailablePre')} <a href="/docs" style={S('color:var(--accent);font-weight:600;text-decoration:none;')}>{tr('mp.keys.apiDocs')}</a> {tr('mp.keys.unavailablePost')}</p>
          </div>
        </div>
      );
    }

    // 3) Signed out → sign-in panel (magic link), with the private-beta case handled calmly.
    if (!user) {
      const card = 'forge-card';
      const cardStyle = S('padding:30px;max-width:460px;margin:0 auto;');
      let inner;
      if (authPhase === 'completing') {
        inner = (
          <div style={S('text-align:center;')}>
            <div style={S('font-family:var(--font-display);font-size:19px;font-weight:600;letter-spacing:-0.02em;')}>{tr('mp.keys.signingIn')}</div>
            <p style={S('font-size:13.5px;color:var(--fg-muted);margin:8px 0 0;')}>{tr('mp.keys.signingInBody')}</p>
          </div>
        );
      } else if (authPhase === 'closed') {
        inner = (
          <div style={S('text-align:center;')}>
            <span style={S('display:inline-grid;place-items:center;width:48px;height:48px;border-radius:50%;background:var(--accent-bg-soft);color:var(--accent);margin-bottom:6px;')}>{ic('lock', 22)}</span>
            <div style={S('font-family:var(--font-display);font-size:20px;font-weight:600;letter-spacing:-0.02em;margin-top:8px;')}>{tr('mp.keys.betaTitle')}</div>
            <p style={S('font-size:13.5px;color:var(--fg-muted);margin:10px 0 0;line-height:1.6;text-wrap:pretty;')}>{tr('mp.keys.betaBody')}</p>
          </div>
        );
      } else if (authPhase === 'sent') {
        inner = (
          <div style={S('text-align:center;')}>
            <span style={S('display:inline-grid;place-items:center;width:48px;height:48px;border-radius:50%;background:var(--good-soft);color:var(--good);margin-bottom:6px;')}>{check('currentColor', 24)}</span>
            <div style={S('font-family:var(--font-display);font-size:20px;font-weight:600;letter-spacing:-0.02em;margin-top:8px;')}>{tr('mp.keys.inboxTitle')}</div>
            <p style={S('font-size:13.5px;color:var(--fg-muted);margin:10px 0 0;line-height:1.6;')}>{tr('mp.keys.inboxPre')} <strong style={S('color:var(--fg);')}>{email}</strong>{tr('mp.keys.inboxPost')}</p>
            <button onClick={() => setAuthPhase('idle')} style={S('margin-top:14px;font-size:13px;font-weight:600;color:var(--accent);background:none;border:none;cursor:pointer;')}>{tr('mp.keys.differentEmail')}</button>
          </div>
        );
      } else {
        inner = (
          <div>
            <div style={S('font-family:var(--font-display);font-size:20px;font-weight:600;letter-spacing:-0.02em;')}>{tr('mp.keys.signInTitle')}</div>
            <p style={S('font-size:13.5px;color:var(--fg-muted);margin:8px 0 18px;line-height:1.55;')}>{tr('mp.keys.ssoBody')}</p>
            {/* Unified "Sign in with Mostly Tiny" is the SOLE path when identitySSO is on —
                the server refuses local magic-link (ADR 0031), so no email form here. */}
            <button onClick={startSSO} className="forge-btn forge-btn--primary forge-btn--lg forge-btn--block" style={S('display:flex;align-items:center;justify-content:center;gap:9px;')}>
              {Logo(18)}<span>{tr('mp.keys.ssoButton')}</span>
            </button>
            {authPhase === 'error' && <div style={S('font-size:12.5px;color:var(--bad);font-weight:500;margin-top:12px;text-align:center;')}>{authErr || tr('mp.keys.genericError')}</div>}
          </div>
        );
      }
      return (
        <div style={S(wrap)}>
          {headBlock}
          <div className={card} style={cardStyle}>{inner}</div>
        </div>
      );
    }

    // 4) Signed in → keys table + create form.
    const monoChip = 'font-family:var(--font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:12.5px;';
    return (
      <div style={S(wrap)}>
        <div style={S('display:flex;align-items:flex-start;justify-content:space-between;gap:16px;')}>
          {headBlock}
          <button onClick={signOut} className="forge-btn forge-btn--secondary forge-btn--sm" style={S('flex:none;margin-top:4px;')}>{tr('mp.keys.signOut')}</button>
        </div>

        {/* Create-key form */}
        <div className="forge-card" style={S('padding:22px;')}>
          <div style={S('font-family:var(--font-display);font-size:16px;font-weight:600;letter-spacing:-0.016em;margin-bottom:14px;')}>{tr('mp.keys.createKey')}</div>
          <form onSubmit={createKey} style={S('display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;')}>
            <div style={S('flex:1;min-width:220px;display:flex;flex-direction:column;gap:6px;')}>
              <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.keys.keyName')}</span>
              <input value={newName} onChange={(ev) => setNewName(ev.target.value)} className="forge-input" placeholder={tr('mp.keys.keyNamePlaceholder')} />
            </div>
            <div style={S('display:flex;flex-direction:column;gap:6px;')}>
              <span style={S('font-size:12px;font-weight:500;color:var(--fg);')}>{tr('mp.keys.scope')}</span>
              <span style={S('display:inline-flex;align-items:center;height:38px;padding:0 12px;border-radius:var(--radius);background:var(--bg-chip);color:var(--fg-muted);font-size:12.5px;font-weight:600;')}>pdf:run</span>
            </div>
            <button type="submit" disabled={creating} className="forge-btn forge-btn--primary"><span>{creating ? tr('mp.keys.creating') : tr('mp.keys.createKeyBtn')}</span></button>
          </form>
          {createErr && <div style={S('font-size:12.5px;color:var(--bad);font-weight:500;margin-top:10px;')}>{createErr}</div>}
        </div>

        {/* One-time secret reveal */}
        {secret && (
          <div className="forge-card" style={S('padding:22px;margin-top:16px;border:1px solid color-mix(in srgb,var(--accent) 40%,transparent);')}>
            <div style={S('display:flex;align-items:center;gap:10px;')}>
              <span style={S('display:inline-grid;place-items:center;width:34px;height:34px;border-radius:9px;background:var(--accent-bg-soft);color:var(--accent);flex:none;')}>{ic('lock', 18)}</span>
              <div style={S('font-family:var(--font-display);font-size:16px;font-weight:600;letter-spacing:-0.016em;')}>{tr('mp.keys.secretTitle')}</div>
            </div>
            <p style={S('font-size:13px;color:var(--fg-muted);margin:12px 0 0;line-height:1.55;')}><strong style={S('color:var(--fg);')}>{tr('mp.keys.secretWarnLead')}</strong> {tr('mp.keys.secretWarnBody')}</p>
            <div style={S('display:flex;align-items:center;gap:10px;margin-top:14px;padding:12px 14px;border-radius:var(--radius);background:var(--bg);border:1px solid var(--border);')}>
              <code style={S(monoChip + 'flex:1;min-width:0;overflow-x:auto;white-space:nowrap;color:var(--fg);')}>{secret}</code>
              <button onClick={copySecret} className="forge-btn forge-btn--secondary forge-btn--sm" style={S('flex:none;')}>{copied ? tr('mp.keys.copied') : tr('mp.keys.copy')}</button>
            </div>
            <button onClick={() => setSecret(null)} style={S('margin-top:12px;font-size:12.5px;font-weight:600;color:var(--fg-muted);background:none;border:none;cursor:pointer;')}>{tr('mp.keys.dismissSaved')}</button>
          </div>
        )}

        {/* Key list */}
        <div style={S('margin-top:24px;')}>
          <div style={S('font-family:var(--font-display);font-size:16px;font-weight:600;letter-spacing:-0.016em;margin-bottom:12px;')}>{tr('mp.keys.yourKeys')}</div>
          {keysErr && <div style={S('font-size:12.5px;color:var(--bad);font-weight:500;margin-bottom:10px;')}>{keysErr}</div>}
          {keys === null ? (
            <div className="forge-card" style={S('padding:28px;text-align:center;color:var(--fg-muted);font-size:13.5px;')}>{tr('mp.keys.loadingKeys')}</div>
          ) : keys.length === 0 ? (
            <div className="forge-card" style={S('padding:28px;text-align:center;color:var(--fg-muted);font-size:13.5px;')}>{tr('mp.keys.noKeys')}</div>
          ) : (
            <div className="forge-card" style={S('padding:0;overflow:hidden;')}>
              <div style={S('display:grid;grid-template-columns:1.4fr 1.1fr 1fr 0.8fr;align-items:center;padding:12px 18px;background:var(--bg);border-bottom:1px solid var(--border);font-size:12px;font-weight:600;color:var(--fg-subtle);text-transform:uppercase;letter-spacing:0.04em;')}>
                <span>{tr('mp.keys.colName')}</span><span>{tr('mp.keys.colKey')}</span><span>{tr('mp.keys.colScopes')}</span><span style={S('text-align:right;')}>{tr('mp.keys.colStatus')}</span>
              </div>
              {keys.map((k) => (
                <div key={k.id} style={S('display:grid;grid-template-columns:1.4fr 1.1fr 1fr 0.8fr;align-items:center;padding:14px 18px;border-bottom:1px solid var(--hairline);font-size:13.5px;')}>
                  <span style={S('font-weight:600;' + (k.revoked ? 'color:var(--fg-subtle);text-decoration:line-through;' : ''))}>{k.name || tr('mp.keys.defaultKeyName')}</span>
                  <span style={S(monoChip + 'color:var(--fg-muted);')}>{(k.prefix || 'sk')}_•••{k.last4 || '????'}</span>
                  <span style={S('display:flex;flex-wrap:wrap;gap:5px;')}>{(k.scopes || []).map((sc) => <span key={sc} className="forge-badge">{sc}</span>)}</span>
                  <span style={S('display:flex;align-items:center;justify-content:flex-end;gap:10px;')}>
                    {k.revoked ? (
                      <span style={S('font-size:12px;font-weight:600;color:var(--fg-subtle);')}>{tr('mp.keys.revoked')}</span>
                    ) : (
                      <button onClick={() => revoke(k.id)} disabled={busyId === k.id} style={S('font-size:12.5px;font-weight:600;color:var(--bad);background:none;border:none;cursor:pointer;')}>{busyId === k.id ? tr('mp.keys.revoking') : tr('mp.keys.revoke')}</button>
                    )}
                  </span>
                </div>
              ))}
            </div>
          )}
        </div>

        <div style={S('margin-top:28px;font-size:13px;color:var(--fg-subtle);')}>
          <span className="pdf-link" onClick={() => go('landing')} style={S('color:var(--fg-muted);font-weight:500;')}>{tr('mp.keys.backToTools')}</span>
        </div>
      </div>
    );
  }

  // ── root: hash router ──
  function App() {
    const parse = () => {
      if (typeof location === 'undefined') return { screen: 'landing', slug: null }; // SSR/no-DOM safe
      // Strip any `?…`/`&…` suffix: the IdP returns to `…#/<route>&token=…` (SSO handoff), and the
      // token must not leak into the route key or every signed-in return would fall through to landing.
      const h = (location.hash || '').replace(/^#\/?/, '').split(/[?&]/)[0];
      if (h === 'pricing') return { screen: 'pricing', slug: null };
      // The team's API-keys dashboard. Not advertised in the public Nav while PUBLIC_APP_OPEN is
      // false — reachable directly at #/keys (and #/dashboard as an alias).
      if (h === 'keys' || h === 'dashboard') return { screen: 'keys', slug: null };
      // E-sign screens (F185) — before the TM lookup: request-signature is also a grid
      // card, but it opens the send flow, not the file runner.
      if (h === 'request-signature') return { screen: 'request', slug: null };
      if (h === 'agreements') return { screen: 'agreements', slug: null };
      if (h === 'bulk') return { screen: 'bulk', slug: null };
      if (h === 'team') return { screen: 'team', slug: null };
      if (h && RENDER_SLUGS[h]) return { screen: 'render', slug: h };
      if (h && TM[h]) return { screen: 'runner', slug: h };
      // MostlySign's sender app lands on the request-signature flow, not a tool grid.
      return { screen: 'request', slug: null };
    };
    const [route, setRoute] = useState(parse());
    useEffect(() => { const f = () => { setRoute(parse()); window.scrollTo(0, 0); }; window.addEventListener('hashchange', f); return () => window.removeEventListener('hashchange', f); }, []);
    // Complete "Sign in with Mostly Tiny" on ANY route (the user may return to a tool, not just
    // #/keys): exchange the identity token in the URL for a session. The Keys screen handles its
    // own return too, but its guard sees the (now-scrubbed) token as absent, so there's no double.
    useEffect(() => {
      try {
        const MA = typeof window !== 'undefined' && window.MostlyAuth;
        if (MA && MA.isIdentityLanding && MA.isIdentityLanding()) {
          const fb = initFirebase();
          if (fb) MA.completeIdentitySignIn({ auth: fb.auth, fns: fb.fns }).catch(() => {});
        }
      } catch (e) { /* not a landing */ }
    }, []);
    // Drop the deep-link boot spinner (set by the inline shell script for #/route deep links)
    // now that the real screen has rendered — see .mp-booting in index.html.
    useEffect(() => { if (typeof document !== 'undefined') { const r = document.getElementById('mostlypdf-app'); if (r && r.classList) r.classList.remove('mp-booting'); } }, []);
    // Post-checkout landing (…?upgraded=1&plan=…#/keys): fire PLAN_UPGRADED once, then strip
    // the marker (mp/analytics.js mirrors the canonical trackCheckoutReturn — refresh-safe).
    useEffect(() => { try { if (window.MPAnalytics) window.MPAnalytics.trackCheckoutReturn(); } catch (e) {} }, []);
    const go = (screen) => { location.hash = screen === 'landing' ? '#/' : '#/' + screen; };
    const open = (slug) => { location.hash = '#/' + slug; };

    // ── ⌘K command palette (@mostly-tiny/cmdk) ──
    // The MostlySign sender surfaces as searchable/runnable commands. The header Trigger
    // opens it; the Palette also binds the ⌘K / Ctrl-K hotkey globally.
    const [searchOpen, setSearchOpen] = useState(false);
    const commands = useMemo(() => {
      const cmds = [];
      cmds.push({ id: 'page:send', group: 'Pages', title: 'Send a document', subtitle: 'Email someone a secure signing link', keywords: 'send sign signature request document envelope', perform: () => go('request-signature') });
      cmds.push({ id: 'page:agreements', group: 'Pages', title: 'Agreements', keywords: 'documents sent status inbox history', perform: () => go('agreements') });
      cmds.push({ id: 'page:pricing', group: 'Pages', title: 'Pricing', keywords: 'plans cost price billing pro business', perform: () => go('pricing') });
      cmds.push({ id: 'page:trust', group: 'Pages', title: 'Trust', keywords: 'seal pades security how it works', href: '/trust' });
      cmds.push({ id: 'page:verify', group: 'Pages', title: 'Verify a document', keywords: 'verify seal certificate check authenticity', href: '/verify' });
      cmds.push({ id: 'page:docs', group: 'Pages', title: 'API docs', keywords: 'api developer reference openapi', href: '/docs' });
      cmds.push({ id: 'page:keys', group: 'Pages', title: 'API keys', keywords: 'api token dashboard account', perform: () => go('keys') });
      return cmds;
    }, [route.screen]);

    const CmdK = (typeof window !== 'undefined' && window.ForgeCmdK) || null;
    let body;
    if (route.screen === 'render' && RenderRunner) body = <RenderRunner key={route.slug} slug={route.slug} go={go} open={open} />;
    else if (route.screen === 'render') body = <Runner key={route.slug} slug={route.slug} go={go} open={open} />; // fallback if module absent
    else if (route.screen === 'runner') body = <Runner key={route.slug} slug={route.slug} go={go} open={open} />;
    else if (route.screen === 'pricing') body = <Pricing />;
    else if (route.screen === 'keys') body = <Keys go={go} />;
    else if (route.screen === 'request' && SignScreens) body = <SignScreens.RequestSignature go={go} />;
    else if (route.screen === 'agreements' && SignScreens) body = <SignScreens.Agreements go={go} open={open} />;
    else if (route.screen === 'bulk' && SignScreens) body = <SignScreens.BulkSend go={go} />;
    else if (route.screen === 'team' && SignScreens) body = <SignScreens.Team go={go} />;
    else body = <Landing go={go} open={open} />;
    // Flex column so the footer is pinned to the bottom of the viewport on short pages
    // (the body region grows to fill); on long pages it just sits after the content.
    return (
      <div style={S('display:flex;flex-direction:column;min-height:100vh;background:var(--bg);color:var(--fg);font-family:var(--font-sans);')}>
        <Nav go={go} onSearch={() => setSearchOpen(true)} />
        <div style={S('flex:1 0 auto;padding-bottom:72px;')}>{body}</div>
        <Footer />
        {CmdK && <CmdK.Palette items={commands} open={searchOpen} onOpenChange={setSearchOpen} placeholder={'Search ' + BRAND.name + '…'} />}
      </div>
    );
  }

  // Root node picker (denylist guard). The prerendered /integrations/<sibling> suite pages
  // (F151) are SSR'd from window.MpSuiteIntegration, NOT this hash-routed tool app — so on
  // those clean paths we must hydrate that component, never <App/>, or hydration mismatches
  // (double-mount). Every other path is the tool app. Locale prefix is stripped first.
  function rootNode() {
    let p = (typeof location !== 'undefined' ? location.pathname : '/').replace(/\/+$/, '') || '/';
    p = p.replace(/^\/(es|fr|de|pt|it|nl|ja)(?=\/|$)/, '') || '/';
    const m = p.match(/^\/integrations\/(.+)$/);
    if (m && typeof window !== 'undefined' && window.MpSuiteIntegration) {
      return <window.MpSuiteIntegration sibling={m[1]} />;
    }
    // Public signer page (F185): its own prerendered page (mp/Signer.boot.jsx) — hydrate
    // the SignerApp here, never the hash-routed tool app (else double-mount / #418).
    if (/^\/sign(\.html|\/index\.html)?$/.test(p) && SignKit) {
      return <SignKit.SignerApp />;
    }
    return <App />;
  }

  // Hydrate the prerendered SSR markup when present; client-render if the shell is empty.
  const _root = document.getElementById('mostlypdf-app');
  if (_root) {
    const node = rootNode();
    if (_root.hasChildNodes()) ReactDOM.hydrateRoot(_root, node);
    else ReactDOM.createRoot(_root).render(node);
  }
})();
