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

const SUPABASE_URL = "https://ydlvzgxqlwqrdcmfdomt.supabase.co";
const SUPABASE_ANON_KEY = "sb_publishable_xhdTLuIKwuDIIoG_GXCfCQ_wMC93wxz";
const TURNSTILE_SITE_KEY = "0x4AAAAAADBX6Dyug7AUdDp0";
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
let stripeClientPromise = null;
async function getStripeClient() {
  if (!stripeClientPromise) {
    stripeClientPromise = (async () => {
      const response = await fetch(DETECT_API + "/stripe/public-config");
      const payload = await response.json().catch(() => ({}));
      const publishableKey = String(payload.stripe_publishable_key || "").trim();
      if (!response.ok || !publishableKey || !publishableKey.startsWith("pk_")) {
        throw new Error("Stripe checkout is not configured yet. Please try again later.");
      }
      if (!window.Stripe) {
        throw new Error("Stripe checkout could not load. Please refresh and try again.");
      }
      return window.Stripe(publishableKey);
    })().catch(error => {
      stripeClientPromise = null;
      throw error;
    });
  }
  return stripeClientPromise;
}

const PDF_LIB = "https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js";
const PDFJS = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
const PDFJS_W = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";

function useLibs() {
  const [r, setR] = useState(false);
  useEffect(() => {
    let c = 0; const ck = () => { c++; if (c >= 2) setR(true); };
    if (window.PDFLib) c++; else { const s = document.createElement("script"); s.src = PDF_LIB; s.onload = ck; document.head.appendChild(s); }
    if (window.pdfjsLib) c++; else { const s = document.createElement("script"); s.src = PDFJS; s.onload = () => { window.pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_W; ck(); }; document.head.appendChild(s); }
    if (c >= 2) setR(true);
    const link = document.createElement("link");
    link.href = "https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&family=Dancing+Script:wght@400;700&family=Playfair+Display:wght@400;700&family=Open+Sans:wght@400;700&family=Instrument+Serif:ital@0;1&display=swap";
    link.rel = "stylesheet"; document.head.appendChild(link);
  }, []);
  return r;
}

const readBuf = f => new Promise(r => { const x = new FileReader(); x.onload = () => r(x.result); x.readAsArrayBuffer(f); });
const readBytes = async f => new Uint8Array(await readBuf(f));
const readDataURL = f => new Promise(r => { const x = new FileReader(); x.onload = () => r(x.result); x.readAsDataURL(f); });
const dlB = (b, n) => { const u = URL.createObjectURL(b); const a = document.createElement("a"); a.href = u; a.download = n; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(u); };
const fmtB = b => b < 1024 ? b + " B" : b < 1048576 ? (b / 1024).toFixed(1) + " KB" : (b / 1048576).toFixed(1) + " MB";
const FREE_BATCH_FILE_LIMIT = 5;
const MERGE_FREE_FILE_LIMIT = 20;
const PRO_BATCH_FILE_LIMIT = 100;
const MERGE_BROWSER_WARNING_BYTES = 150 * 1024 * 1024;
async function downloadOrSharePdf(blob, filename) {
  const ua = navigator.userAgent || "";
  const isIOSWebKit =
    /iPhone|iPad|iPod/.test(ua) &&
    /WebKit/.test(ua) &&
    !/CriOS|FxiOS|EdgiOS/.test(ua);

  // iPadOS desktop-mode Safari reports MacIntel but still exposes touch points.
  const isIPadDesktopMode =
    navigator.platform === "MacIntel" &&
    navigator.maxTouchPoints &&
    navigator.maxTouchPoints > 1 &&
    /WebKit/.test(ua);

  const shouldTryShare = isIOSWebKit || isIPadDesktopMode;

  if (shouldTryShare && typeof navigator.canShare === "function" && typeof navigator.share === "function") {
    try {
      const file = new File([blob], filename, { type: "application/pdf" });

      if (navigator.canShare({ files: [file] })) {
        try {
          await navigator.share({
            files: [file],
            title: filename,
          });
          return;
        } catch (err) {
          if (err && err.name === "AbortError") {
            return;
          }

          console.warn("[DLOAD-1] navigator.share failed, falling back:", err);
        }
      }
    } catch (err) {
      console.warn("[DLOAD-1] Web Share file path unavailable, falling back:", err);
    }
  }

  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
const CONTINUITY_STORAGE_KEY = "nspdf-session-state";
const CONTINUITY_PENDING_KEY = "nspdf-pending-restore";
const CONTINUITY_TTL_MS = 30 * 60 * 1000;

function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = "";
  const chunkSize = 0x8000;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

function base64ToUint8Array(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return bytes;
}

function isContinuityMobileViewport() {
  return Boolean(
    (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) ||
    navigator.maxTouchPoints > 0
  );
}

function isLegalRoute(pathname) {
  const path = String(pathname || window.location.pathname || "").toLowerCase();
  return path === "/terms" || path === "/privacy" || path === "/cookies" || path.startsWith("/docs/legal/");
}

const SIGN_FONTS = [
  { id: "caveat", name: "Caveat", css: "'Caveat', cursive" },
  { id: "dancing", name: "Dancing Script", css: "'Dancing Script', cursive" },
  { id: "serif", name: "Classic", css: "'Playfair Display', serif" },
  { id: "print", name: "Print", css: "'Open Sans', sans-serif" },
];

// ─── SHARED APPEARANCE CORE ───
// Pure helper modules used by ACRO-APPEARANCE-1 and FONT-1A.
// See docs/launch/acroform-parity/shared-appearance-core.md for full spec.
// These are dormant until ACRO-APPEARANCE-1 Step 3+ integrates them.

// Module 1: DA_PARSER
// Parses PDF /DA (Default Appearance) string into structured appearance.
// Returns null if input is unparseable.
function parseDA(daString) {
  if (!daString || typeof daString !== "string") return null;

  const result = {
    fontName: null,
    fontSize: 0,
    color: { r: 0, g: 0, b: 0 }
  };

  // Font: /Name Size Tf
  const fontMatch = daString.match(/\/([\w-]+)\s+([\d.]+)\s+Tf/);
  if (fontMatch) {
    result.fontName = fontMatch[1];
    result.fontSize = parseFloat(fontMatch[2]);
  } else {
    return null;
  }

  // Color: RGB first, then grayscale
  const rgbMatch = daString.match(/([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+rg/);
  if (rgbMatch) {
    result.color = {
      r: parseFloat(rgbMatch[1]),
      g: parseFloat(rgbMatch[2]),
      b: parseFloat(rgbMatch[3])
    };
  } else {
    const grayMatch = daString.match(/(?:^|\s)([\d.]+)\s+g(?:\s|$)/);
    if (grayMatch) {
      const v = parseFloat(grayMatch[1]);
      result.color = { r: v, g: v, b: v };
    }
  }

  return result;
}

// Adapter: PDF.js defaultAppearanceData → parseDA() output shape
// PDF.js pre-parses /DA strings into a structured object. This
// adapter normalizes that into the same shape parseDA() produces
// so both flow through resolveAppearance() identically.
// See docs/launch/acroform-parity/runtime-da-discovery-findings.md
function adaptPdfJsAppearance(daData) {
  if (!daData || typeof daData !== "object") return null;
  if (!daData.fontName || typeof daData.fontSize !== "number") return null;
  
  // PDF.js exposes fontColor as Uint8ClampedArray(3) with byte values 0-255.
  // parseDA() output uses {r, g, b} with float values 0-1.
  const colorBytes = daData.fontColor;
  let color = { r: 0, g: 0, b: 0 };
  if (colorBytes && colorBytes.length >= 3) {
    color = {
      r: colorBytes[0] / 255,
      g: colorBytes[1] / 255,
      b: colorBytes[2] / 255
    };
  }
  
  return {
    fontName: daData.fontName,
    fontSize: daData.fontSize,
    color: color
  };
}

// Module 2: FONT_FALLBACK
// Maps PDF font names to pdf-lib StandardFonts and CSS equivalents.
// Used identically by browser preview and export (WYSIWYG mechanism).
const FONT_FALLBACK_TABLE = {
  "Helv":                { pdfLib: "Helvetica",     css: "Arial, Helvetica, sans-serif", weight: "normal" },
  "HeBo":                { pdfLib: "HelveticaBold", css: "Arial, Helvetica, sans-serif", weight: "bold" },
  "HelveticaLTStd-Bold": { pdfLib: "HelveticaBold", css: "Arial, Helvetica, sans-serif", weight: "bold" },
  "Arial":               { pdfLib: "Helvetica",     css: "Arial, Helvetica, sans-serif", weight: "normal" },
  "ZaDb":                { pdfLib: "ZapfDingbats",  css: "ZapfDingbats, sans-serif",     weight: "normal" },
  "Cour":                { pdfLib: "Courier",       css: "Courier New, monospace",       weight: "normal" },
  "CourierStd":          { pdfLib: "Courier",       css: "Courier New, monospace",       weight: "normal" }
};

const FONT_FALLBACK_DEFAULT = {
  pdfLib: "Helvetica",
  css: "Arial, Helvetica, sans-serif",
  weight: "normal"
};

function resolveFontFallback(fontName) {
  if (!fontName) return FONT_FALLBACK_DEFAULT;
  return FONT_FALLBACK_TABLE[fontName] || FONT_FALLBACK_DEFAULT;
}

// Module 3: APPEARANCE_RESOLVER
// Combines /DA + fallback + field rect into render-ready appearance object.
// Used by both browser preview and pdf-lib export paths.
function resolveAppearance(input, fieldRect) {
  // Polymorphic input: accept either a raw /DA string (parsed via
  // parseDA), a pre-parsed object (from parseDA or
  // adaptPdfJsAppearance), or null/undefined.
  // Note: typeof null === "object" in JavaScript, so explicit null
  // check comes first.
  let da;
  if (input === null || input === undefined) {
    da = null;
  } else if (typeof input === "string") {
    da = parseDA(input);
  } else if (typeof input === "object" && input.fontName && typeof input.fontSize === "number") {
    da = input;
  } else {
    da = null;
  }

  const safeRect = fieldRect || { width: 100, height: 14 };

  if (!da) {
    return {
      fontName: null,
      cssFamily: FONT_FALLBACK_DEFAULT.css,
      cssWeight: FONT_FALLBACK_DEFAULT.weight,
      pdfLibFont: FONT_FALLBACK_DEFAULT.pdfLib,
      fontSize: 10,
      cssColor: "rgb(0, 0, 0)",
      pdfLibColor: { r: 0, g: 0, b: 0 }
    };
  }

  const fallback = resolveFontFallback(da.fontName);

  // Auto-size: Tf 0 means compute from field height
  let resolvedSize = da.fontSize;
  if (resolvedSize === 0) {
    resolvedSize = Math.max(8, Math.min(18, safeRect.height * 0.7));
  }
  if (!resolvedSize || resolvedSize <= 0) resolvedSize = 10;

  return {
    fontName: da.fontName,
    cssFamily: fallback.css,
    cssWeight: fallback.weight,
    pdfLibFont: fallback.pdfLib,
    fontSize: resolvedSize,
    cssColor: `rgb(${Math.round(da.color.r * 255)}, ${Math.round(da.color.g * 255)}, ${Math.round(da.color.b * 255)})`,
    pdfLibColor: da.color
  };
}

// Module 4: BASELINE_CALC
// Computes vertical text baseline within field rectangle.
// Initial heuristic — may need tuning during verification.
function calculateBaselineFromBottom(fieldHeight, fontSize) {
  const verticalCenter = (fieldHeight - fontSize) / 2;
  const ascentRatio = 0.8;
  return verticalCenter + fontSize * ascentRatio;
}

function calculateBaselineCSSTop(fieldHeight, fontSize) {
  return Math.max(0, (fieldHeight - fontSize) / 2);
}

// ─── END SHARED APPEARANCE CORE ───

// ─── PDF OPS ───
async function mergePDFs(files) { const { PDFDocument: D } = window.PDFLib; const m = await D.create(); for (const f of files) { const d = await D.load(await readBuf(f)); (await m.copyPages(d, d.getPageIndices())).forEach(p => m.addPage(p)); } const o = await m.save(); return { blob: new Blob([o], { type: "application/pdf" }), pages: m.getPageCount() }; }
function parseSplitPageGroups(input, pageCount) {
  const spec = String(input || "").trim();
  if (!spec) throw new Error("Enter pages to split.");
  const pageRangeText = `${pageCount}-page PDF`;
  const validatePage = n => {
    if (n < 1 || n > pageCount) throw new Error(`Page ${n} is outside this ${pageRangeText}.`);
  };
  const makeGroup = token => {
    const seen = new Set();
    const pages = [];
    const addPage = n => {
      validatePage(n);
      if (!seen.has(n)) {
        seen.add(n);
        pages.push(n);
      }
    };
    const rangeMatch = token.match(/^(\d+)\s*-\s*(\d+)$/);
    if (rangeMatch) {
      const start = Number(rangeMatch[1]);
      const end = Number(rangeMatch[2]);
      if (start > end) throw new Error(`Range ${start}-${end} is reversed.`);
      validatePage(start);
      validatePage(end);
      for (let n = start; n <= end; n += 1) addPage(n);
      return { label: `${start}-${end}`, pages, pageIndices: pages.map(n => n - 1) };
    }
    if (/^\d+$/.test(token)) {
      const page = Number(token);
      addPage(page);
      return { label: String(page), pages, pageIndices: pages.map(n => n - 1) };
    }
    throw new Error("Use page numbers and ranges like 1-3.");
  };
  const groups = spec.split(",").map(rawToken => {
    const token = rawToken.trim();
    if (!token) throw new Error("Remove empty split groups. Each comma creates a separate PDF.");
    return makeGroup(token);
  });
  if (!groups.length) throw new Error("Enter pages to split.");
  return groups;
}
function normalizeSplitGroups(groups, pageCount) {
  if (!Array.isArray(groups) || !groups.length) throw new Error("Enter pages to split.");
  return groups.map(group => {
    const pages = (group.pages || []).map(n => Number(n));
    if (!pages.length) throw new Error("Split group cannot be empty.");
    pages.forEach(n => {
      if (!Number.isInteger(n) || n < 1 || n > pageCount) throw new Error(`Page ${n} is outside this ${pageCount}-page PDF.`);
    });
    return {
      label: String(group.label || (pages.length === 1 ? pages[0] : `${pages[0]}-${pages[pages.length - 1]}`)),
      pages,
      pageIndices: pages.map(n => n - 1)
    };
  });
}
function splitGroupsFromSplitPoints(splitPoints, pageCount) {
  const validPoints = [...new Set((splitPoints || []).map(n => Number(n)).filter(n => Number.isInteger(n) && n >= 1 && n < pageCount))].sort((a, b) => a - b);
  const groups = [];
  let start = 1;
  [...validPoints, pageCount].forEach(end => {
    const pages = [];
    for (let n = start; n <= end; n += 1) pages.push(n);
    groups.push({ label: pages.length === 1 ? String(start) : `${start}-${end}`, pages, pageIndices: pages.map(n => n - 1) });
    start = end + 1;
  });
  return groups;
}
function splitOutputBaseName(file) {
  const rawName = String(file?.name || "document.pdf").replace(/\.pdf$/i, "") || "document";
  return rawName.replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").trim() || "document";
}
function splitGroupFileName(baseName, group) {
  const prefix = group.pages.length === 1 ? "page" : "pages";
  return `${baseName}_${prefix}-${group.label}.pdf`;
}
async function zipSplitFiles(files) {
  if (!window.JSZip) throw new Error("ZIP support failed to load. Refresh the page and try again.");
  const zip = new window.JSZip();
  for (const f of files) zip.file(f.name, await f.blob.arrayBuffer());
  const zipBlob = await zip.generateAsync({ type: "blob", compression: "DEFLATE", mimeType: "application/zip" });
  return new Blob([zipBlob], { type: "application/zip" });
}
async function buildSplitPDFResult(file, sourceDoc, pageCount, groups) {
  const { PDFDocument: D } = window.PDFLib;
  const normalizedGroups = normalizeSplitGroups(groups, pageCount);
  const baseName = splitOutputBaseName(file);
  const files = [];
  for (const group of normalizedGroups) {
    const d = await D.create();
    (await d.copyPages(sourceDoc, group.pageIndices)).forEach(p => d.addPage(p));
    const o = await d.save();
    files.push({ blob: new Blob([o], { type: "application/pdf" }), name: splitGroupFileName(baseName, group), group });
  }
  const result = { totalPages: pageCount, files };
  if (files.length > 1) {
    result.zipBlob = await zipSplitFiles(files);
    result.zipName = `${baseName}_split-files.zip`;
  }
  return result;
}
async function splitPDF(file, pageSpec) {
  const { PDFDocument: D } = window.PDFLib;
  const s = await D.load(await readBuf(file));
  const t = s.getPageCount();
  return buildSplitPDFResult(file, s, t, parseSplitPageGroups(pageSpec, t));
}
async function splitPDFWithGroups(file, groups) {
  const { PDFDocument: D } = window.PDFLib;
  const s = await D.load(await readBuf(file));
  const t = s.getPageCount();
  return buildSplitPDFResult(file, s, t, groups);
}
function isProtectedPdfLoadError(error) {
  const message = String(error?.message || error || "");
  return /encrypted|password|permissions?|PDFDocument\.load|ignoreEncryption/i.test(message);
}
function formatStandardToolError(error, toolId) {
  if (toolId === "split" && isProtectedPdfLoadError(error)) {
    return "This PDF is protected. Split can't process protected PDFs yet. Remove this file and try an unrestricted copy. Some PDFs can be viewed but still restrict page changes like splitting or organizing.";
  }
  return String(error?.message || error || "Something went wrong. Please try again.");
}
async function compressPDF(file) {
  const { PDFDocument: D } = window.PDFLib;
  const input = await readBuf(file);
  const origSize = input.byteLength || file.size || 0;
  const d = await D.load(input);
  d.setTitle("");
  d.setAuthor("");
  d.setSubject("");
  d.setKeywords([]);
  d.setProducer("NoStringsPDF");
  const optimized = await d.save({ useObjectStreams: true });
  const savedBytes = origSize - optimized.length;
  const reduction = savedBytes > 0 && origSize > 0 ? Math.round((savedBytes / origSize) * 100) : 0;
  const reduced = reduction >= 1;
  const output = reduced ? optimized : input;
  return {
    blob: new Blob([output], { type: "application/pdf" }),
    origSize,
    newSize: reduced ? optimized.length : origSize,
    optimizedSize: optimized.length,
    reduction: reduced ? reduction : 0,
    reduced,
    returnedOriginal: !reduced
  };
}
const COMPRESS_SCAN_PRESETS = {
  quality: { label: "Better quality", note: "Larger file", dpi: 150, jpegQuality: 0.75 },
  recommended: { label: "Recommended", note: "Balanced", dpi: 120, jpegQuality: 0.68 },
  smallest: { label: "Smallest file", note: "Softer", dpi: 100, jpegQuality: 0.60 }
};
function compressedFileName(file) {
  const name = file?.name || "document.pdf";
  return /\.pdf$/i.test(name) ? name.replace(/\.pdf$/i, "-compressed.pdf") : `${name}-compressed.pdf`;
}
const waitForPaint = () => new Promise(resolve => setTimeout(resolve, 0));
async function canvasToJpegBytes(canvas, quality) {
  const blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", quality));
  if (!blob) throw new Error("High compression could not encode this page. Try Preserve text & forms instead.");
  return new Uint8Array(await blob.arrayBuffer());
}
async function compressPDFAsScans(file, preset, onProgress) {
  const { PDFDocument: D } = window.PDFLib;
  const input = await readBuf(file);
  const origSize = input.byteLength || file.size || 0;
  const sourceBytes = new Uint8Array(input.slice(0));
  const pdf = await window.pdfjsLib.getDocument({ data: sourceBytes }).promise;
  const out = await D.create();
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d", { alpha: false });
  if (!ctx) throw new Error("High compression is not available in this browser.");
  try {
    for (let i = 1; i <= pdf.numPages; i += 1) {
      onProgress?.({ page: i, total: pdf.numPages });
      await waitForPaint();
      const page = await pdf.getPage(i);
      const baseViewport = page.getViewport({ scale: 1 });
      const renderScale = preset.dpi / 72;
      const viewport = page.getViewport({ scale: renderScale });
      if (viewport.width > 16384 || viewport.height > 16384 || viewport.width * viewport.height > 120000000) {
        throw new Error("This PDF page is too large for browser high compression. Try Preserve text & forms instead.");
      }
      canvas.width = Math.max(1, Math.ceil(viewport.width));
      canvas.height = Math.max(1, Math.ceil(viewport.height));
      ctx.save();
      ctx.fillStyle = "#FFFFFF";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.restore();
      await page.render({ canvasContext: ctx, viewport, background: "white" }).promise;
      const jpgBytes = await canvasToJpegBytes(canvas, preset.jpegQuality);
      const image = await out.embedJpg(jpgBytes);
      const pdfPage = out.addPage([baseViewport.width, baseViewport.height]);
      pdfPage.drawImage(image, { x: 0, y: 0, width: baseViewport.width, height: baseViewport.height });
      page.cleanup?.();
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      canvas.width = 1;
      canvas.height = 1;
      await waitForPaint();
    }
  } finally {
    try { pdf.destroy?.(); } catch {}
    ctx.clearRect(0, 0, canvas.width || 1, canvas.height || 1);
    canvas.width = 1;
    canvas.height = 1;
  }
  const output = await out.save({ useObjectStreams: true });
  if (!output?.length) throw new Error("High compression did not create a PDF. Please try Preserve text & forms.");
  const savedBytes = origSize - output.length;
  const reduction = savedBytes > 0 && origSize > 0 ? Math.round((savedBytes / origSize) * 100) : 0;
  const reduced = reduction >= 1;
  return {
    blob: new Blob([reduced ? output : input], { type: "application/pdf" }),
    origSize,
    newSize: reduced ? output.length : origSize,
    optimizedSize: output.length,
    reduction: reduced ? reduction : 0,
    reduced,
    returnedOriginal: !reduced,
    mode: "scan",
    preset
  };
}
function sanitizeOutputFileName(name, fallback = "document.pdf") {
  const safe = String(name || fallback).replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").trim();
  return safe || fallback;
}
function uniqueOutputFileName(name, usedNames) {
  const safeName = sanitizeOutputFileName(name);
  const extMatch = safeName.match(/(\.[^.]+)$/);
  const ext = extMatch ? extMatch[1] : "";
  const base = ext ? safeName.slice(0, -ext.length) : safeName;
  let candidate = safeName;
  let n = 2;
  while (usedNames.has(candidate.toLowerCase())) {
    candidate = `${base}-${n}${ext}`;
    n += 1;
  }
  usedNames.add(candidate.toLowerCase());
  return candidate;
}
async function compressPDFsAsScanBatch(files, preset, onProgress) {
  const inputFiles = Array.from(files || []).filter(Boolean);
  if (inputFiles.length < 2) throw new Error("Choose at least two PDFs for batch compression.");
  const usedNames = new Set();
  const outputs = [];
  const failures = [];
  const fileResults = [];
  let totalInputBytes = 0;
  let totalOutputBytes = 0;
  for (let i = 0; i < inputFiles.length; i += 1) {
    const file = inputFiles[i];
    const fileName = file?.name || `document-${i + 1}.pdf`;
    const inputBytes = Number(file?.size) || 0;
    const looksPdf = file?.type === "application/pdf" || /\.pdf$/i.test(fileName);
    if (!looksPdf) {
      const failure = { status: "failure", originalName: fileName, inputBytes, error: "Skipped because it is not a PDF." };
      failures.push(failure);
      fileResults.push(failure);
      continue;
    }
    try {
      const result = await compressPDFAsScans(file, preset, progress => {
        onProgress?.({ ...progress, fileIndex: i + 1, fileTotal: inputFiles.length, fileName });
      });
      const outputName = uniqueOutputFileName(compressedFileName(file), usedNames);
      const success = {
        status: "success",
        originalName: fileName,
        name: outputName,
        blob: result.blob,
        inputBytes: result.origSize,
        outputBytes: result.newSize,
        origSize: result.origSize,
        newSize: result.newSize,
        reduction: result.reduction,
        reduced: result.reduced,
        returnedOriginal: result.returnedOriginal
      };
      outputs.push(success);
      fileResults.push(success);
      totalInputBytes += result.origSize || 0;
      totalOutputBytes += result.newSize || 0;
    } catch (err) {
      const failure = { status: "failure", originalName: fileName, inputBytes, error: String(err?.message || err || "Could not compress this PDF.") };
      failures.push(failure);
      fileResults.push(failure);
    }
  }
  if (!outputs.length) throw new Error(failures[0]?.error || "Batch compression could not create any PDFs.");
  const blob = await zipSplitFiles(outputs);
  const totalReduction = totalInputBytes > totalOutputBytes && totalInputBytes > 0 ? Math.round(((totalInputBytes - totalOutputBytes) / totalInputBytes) * 100) : 0;
  return {
    blob,
    name: "compressed-pdfs.zip",
    files: outputs,
    fileResults,
    failures,
    origSize: totalInputBytes,
    newSize: totalOutputBytes,
    totalInputBytes,
    totalOutputBytes,
    totalReduction,
    successCount: outputs.length,
    failureCount: failures.length,
    reducedCount: outputs.filter(f => f.reduced).length,
    fileTotal: inputFiles.length
  };
}
if (typeof window !== "undefined" && window.__NSPDF_ENABLE_TEST_HOOKS__) {
  window.__NSPDF_TEST_HOOKS__ = {
    ...(window.__NSPDF_TEST_HOOKS__ || {}),
    compressPDFsAsScanBatch,
    COMPRESS_SCAN_PRESETS,
    resolveDevProOverride
  };
}
async function imagesToPDF(files) { const { PDFDocument: D } = window.PDFLib; const d = await D.create(); for (const f of files) { const b = await readBuf(f); const img = f.type === "image/png" ? await d.embedPng(b) : await d.embedJpg(b); const pg = d.addPage([img.width, img.height]); pg.drawImage(img, { x: 0, y: 0, width: img.width, height: img.height }); } const o = await d.save(); return { blob: new Blob([o], { type: "application/pdf" }), pages: d.getPageCount() }; }
async function rotatePDF(file, deg) { const { PDFDocument: D } = window.PDFLib; const d = await D.load(await readBuf(file)); d.getPages().forEach(p => p.setRotation(window.PDFLib.degrees((p.getRotation().angle + deg) % 360))); const o = await d.save(); return { blob: new Blob([o], { type: "application/pdf" }) }; }
async function reorderPDF(file, order) { const { PDFDocument: D } = window.PDFLib; const s = await D.load(await readBuf(file)); const d = await D.create(); (await d.copyPages(s, order)).forEach(p => d.addPage(p)); const o = await d.save(); return { blob: new Blob([o], { type: "application/pdf" }), pages: d.getPageCount() }; }

function isServerDetectedSource(source) {
  return source === "azure" || source === "heuristic" || source === "template" || source === "server";
}

function clampManualItemsToPage(items, pageDims) {
  let repositioned = false;
  const nextItems = (items || []).map(function(it) {
    if (it.auto) return it;
    const pageIndex = it.page || 0;
    const dim = pageDims && pageDims[pageIndex];
    if (!dim) return it;
    const width = it.width || (it.type === "note" ? 120 : 40);
    const height = it.height || (it.type === "note" ? 24 : 20);
    const safeX = Math.min(Math.max(it.x || 0, 50), Math.max(0, dim.width - width - 50));
    const safeY = Math.min(Math.max(it.y || 0, 50), Math.max(0, dim.height - height - 50));
    if (safeX !== it.x || safeY !== it.y) {
      repositioned = true;
      return { ...it, x: safeX, y: safeY };
    }
    return it;
  });
  return { items: nextItems, repositioned };
}

async function addPageNumbers(file, position = "bottom-center", startNum = 1, fontSize = 10) {
  const { PDFDocument: D, rgb, StandardFonts } = window.PDFLib;
  const d = await D.load(await readBuf(file));
  const font = await d.embedFont(StandardFonts.Helvetica);
  const pages = d.getPages();
  pages.forEach((pg, i) => {
    const { width, height } = pg.getSize();
    const num = `${startNum + i}`;
    const tw = font.widthOfTextAtSize(num, fontSize);
    let x, y;
    if (position === "bottom-center") { x = (width - tw) / 2; y = 20; }
    else if (position === "bottom-right") { x = width - tw - 30; y = 20; }
    else if (position === "bottom-left") { x = 30; y = 20; }
    else if (position === "top-center") { x = (width - tw) / 2; y = height - 30; }
    else if (position === "top-right") { x = width - tw - 30; y = height - 30; }
    else { x = 30; y = height - 30; } // top-left
    pg.drawText(num, { x, y, size: fontSize, font, color: rgb(0.3, 0.3, 0.3) });
  });
  const o = await d.save();
  return { blob: new Blob([o], { type: "application/pdf" }), pages: pages.length };
}

function watermarkOutputBaseName(file) {
  const rawName = String(file?.name || "document.pdf").replace(/\.pdf$/i, "") || "document";
  return rawName.replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").trim() || "document";
}
function watermarkFileName(file) {
  return `${watermarkOutputBaseName(file)}_watermarked.pdf`;
}
function assertPdfBlob(blob, message = "PDF output could not be created. Please try another PDF.") {
  if (!(blob instanceof Blob) || blob.type !== "application/pdf" || blob.size <= 0) throw new Error(message);
  return blob;
}
async function addWatermark(file, text = "DRAFT", fontSize = 48, opacity = 0.15) {
  const { PDFDocument: D, rgb, StandardFonts, degrees } = window.PDFLib;
  const d = await D.load(await readBuf(file));
  const font = await d.embedFont(StandardFonts.HelveticaBold);
  d.getPages().forEach(pg => {
    const { width, height } = pg.getSize();
    const tw = font.widthOfTextAtSize(text, fontSize);
    pg.drawText(text, {
      x: (width - tw) / 2, y: height / 2 - fontSize / 2,
      size: fontSize, font, color: rgb(0.7, 0.7, 0.7), opacity,
      rotate: degrees(-35),
    });
  });
  const o = await d.save();
  return { blob: new Blob([o], { type: "application/pdf" }) };
}

function pdfImageOutputBaseName(file) {
  const rawName = String(file?.name || "document.pdf").replace(/\.pdf$/i, "") || "document";
  return rawName.replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").trim() || "document";
}
function assertPngBlob(blob, message = "PDF to Image output could not be created. Please try another PDF.") {
  if (!(blob instanceof Blob) || blob.type !== "image/png" || blob.size <= 0) throw new Error(message);
  return blob;
}
async function pdfToImages(fileBytes, file) {
  const pdf = await window.pdfjsLib.getDocument({ data: fileBytes }).promise;
  const results = [];
  const baseName = pdfImageOutputBaseName(file);
  for (let i = 0; i < pdf.numPages; i++) {
    const pg = await pdf.getPage(i + 1);
    const vp = pg.getViewport({ scale: 2 });
    const c = document.createElement("canvas"); c.width = vp.width; c.height = vp.height;
    await pg.render({ canvasContext: c.getContext("2d"), viewport: vp }).promise;
    const blob = await new Promise(r => c.toBlob(r, "image/png"));
    results.push({ blob: assertPngBlob(blob), name: `${baseName}_page-${i + 1}.png` });
  }
  const result = { files: results, pages: pdf.numPages };
  if (results.length > 1) {
    result.zipBlob = await zipSplitFiles(results);
    result.zipName = `${baseName}_images.zip`;
  }
  return result;
}
async function unlockPDF(file) {
  // pdf-lib can load password-protected PDFs if we pass ignoreEncryption
  const { PDFDocument: D } = window.PDFLib;
  const bytes = await readBuf(file);
  const d = await D.load(bytes, { ignoreEncryption: true });
  d.setProducer("NoStringsPDF");
  const o = await d.save();
  return { blob: new Blob([o], { type: "application/pdf" }) };
}

function getEditorPageRenderScale() {
  const dpr = window.devicePixelRatio || 1;
  return Math.max(2, Math.min(4, dpr * 2));
}

async function renderPages(bytes, renderScale = 2) {
  var copyForRender = new Uint8Array(bytes.buffer.slice(0));
  const rasterScale = Math.max(1, Number(renderScale) || 2);
  const pdf = await window.pdfjsLib.getDocument({ data: copyForRender }).promise; const imgs = [], dims = [];
  for (let i = 0; i < pdf.numPages; i++) {
    const pg = await pdf.getPage(i + 1); const vp = pg.getViewport({ scale: rasterScale });
    const c = document.createElement("canvas"); c.width = vp.width; c.height = vp.height;
    await pg.render({ canvasContext: c.getContext("2d"), viewport: vp }).promise;
    imgs.push(c.toDataURL("image/png")); dims.push({ width: vp.width / rasterScale, height: vp.height / rasterScale });
  }
  return { imgs, dims };
}



// Smart field type inference from field name / label text
function inferFieldType(name) {
  var n = (name || "").toLowerCase();
  // Date patterns
  if (/\bdate\b|dob|birth|expir|issued|ceremon|day\b|month\b|year\b/.test(n)) return { hint: "date", ph: "MM/DD/YYYY" };
  // Name patterns  
  if (/\bname\b|first|middle|last|maiden|spouse|applicant|witness|parent|registrar/.test(n)) return { hint: "name", ph: "Full name" };
  // Address patterns
  if (/address|street|city|municipality|borough|township|town\b/.test(n)) return { hint: "address", ph: "" };
  if (/\bstate\b/.test(n)) return { hint: "state", ph: "NJ" };
  if (/zip|postal/.test(n)) return { hint: "zip", ph: "00000" };
  if (/county/.test(n)) return { hint: "county", ph: "County" };
  // Number patterns
  if (/ssn|social.sec|tax.id|\bein\b/.test(n)) return { hint: "ssn", ph: "000-00-0000" };
  if (/phone|tel\b|fax/.test(n)) return { hint: "phone", ph: "(000) 000-0000" };
  if (/license|permit|case|number|no\b|num\b|id\b/.test(n)) return { hint: "id", ph: "" };
  // Signature
  if (/sign|initial/.test(n)) return { hint: "signature", ph: "Sign here" };
  // Age / numeric
  if (/\bage\b|amount|quantity|\bqty\b|times\b|number.of/.test(n)) return { hint: "number", ph: "#" };
  // Email
  if (/email|e-mail/.test(n)) return { hint: "email", ph: "email@example.com" };
  // Place / birthplace
  if (/place|birthplace|location|venue/.test(n)) return { hint: "place", ph: "" };
  return { hint: "text", ph: "" };
}

// ─── COMB INPUT CLASSIFICATION ───
// Pure helpers for COMB-INPUT-1 — field-aware combed text input.
// See docs/launch/acroform-parity/comb-input-1-design.md
//
// Step 1 adds these as pure functions. No call sites yet. The
// keyDown/paste handlers will consume them in Steps 2-3.

function classifyFromAcroActions(actions) {
  // Returns 'numeric' | null
  // Inspects PDF.js-exposed action source strings for content-type
  // signals. Does NOT execute any PDF JavaScript; only pattern-matches.
  if (!actions || typeof actions !== "object") return null;

  const actionGroups = [actions.Keystroke, actions.Format, actions.Validate, actions.K, actions.F, actions.V];
  for (const group of actionGroups) {
    if (!group) continue;
    const sources = Array.isArray(group) ? group : [group];
    for (const src of sources) {
      const s = String(src || "");
      // AFSpecial_Keystroke(0..3) → ZIP/ZIP+4/phone/SSN, all numeric
      if (/AFSpecial_Keystroke\s*\(\s*[0-3]\s*\)/.test(s)) return 'numeric';
      // AFNumber_Keystroke → general numeric
      if (/AFNumber_Keystroke/.test(s)) return 'numeric';
    }
  }
  return null;
}

function classifyFromFieldName(name) {
  // Returns 'numeric' | null
  // Conservative paired-trigger matching. Bare "number" is NOT a
  // trigger; restriction requires paired context.
  if (!name || typeof name !== "string") return null;
  const numericPattern = /(ssn|social\s*security|ein|tax\s*id|phone|telephone|tel\b|zip|postal\s*code|account\s*number|card\s*number)/i;
  if (numericPattern.test(name)) return 'numeric';
  return null;
}

function getCombCharClass(item) {
  // Returns 'numeric' | 'alpha' | 'alphanumeric'
  // Priority 1: AcroForm actions (authoritative)
  // Priority 2: Field-name heuristic
  // Priority 3: Permissive alphanumeric default
  if (!item || typeof item !== "object") return 'alphanumeric';

  const fromActions = classifyFromAcroActions(item.acroActions);
  if (fromActions) return fromActions;

  const nameToCheck = item.acroFieldName || item.label || "";
  const fromName = classifyFromFieldName(nameToCheck);
  if (fromName) return fromName;

  return 'alphanumeric';
}

function isCombAcceptedChar(ch, charClass) {
  if (charClass === 'numeric') return /^[0-9]$/.test(ch);
  if (charClass === 'alpha') return /^[\p{L}\s\-'.]$/u.test(ch);
  return /^[\p{L}\p{N}\s\-'.]$/u.test(ch);
}

function filterCombTextForCharClass(text, charClass) {
  return Array.from(String(text || "")).filter(ch => isCombAcceptedChar(ch, charClass));
}

function getCombItemMaxLen(item) {
  return Number(item && (item.maxLength || item.acroMaxLen)) || 0;
}

function getCombSplitGroupInfo(item) {
  if (!item || item.type !== "text" || item.acroIsComb !== true) return null;
  if (getCombCharClass(item) !== "numeric") return null;
  const name = String(item.acroFieldName || item.label || "").trim();
  const match = name.match(/^(.+?)\s+(SSN|Phone)\s+([1-3])$/i);
  if (!match) return null;
  const kind = match[2].toLowerCase() === "ssn" ? "ssn" : "phone";
  return {
    key: match[1].trim().toLowerCase() + ":" + kind,
    kind,
    index: Number(match[3]) - 1
  };
}

function getCombSplitExpectedLengths(kind) {
  if (kind === "ssn") return [3, 2, 4];
  if (kind === "phone") return [3, 3, 4];
  return null;
}

function findSafeCombPasteCascadeGroup(items, target) {
  const targetInfo = getCombSplitGroupInfo(target);
  if (!targetInfo) return null;
  const expectedLengths = getCombSplitExpectedLengths(targetInfo.kind);
  if (!expectedLengths) return null;
  const group = (items || [])
    .filter(function(item) {
      const info = getCombSplitGroupInfo(item);
      return info && info.key === targetInfo.key && info.kind === targetInfo.kind;
    })
    .map(function(item) { return { item, info: getCombSplitGroupInfo(item), maxLen: getCombItemMaxLen(item) }; })
    .sort(function(a, b) { return a.info.index - b.info.index; });

  if (group.length !== expectedLengths.length) return null;
  for (let i = 0; i < group.length; i++) {
    if (group[i].info.index !== i || group[i].maxLen !== expectedLengths[i]) return null;
    if (group[i].item.page !== target.page || getCombCharClass(group[i].item) !== "numeric") return null;
  }

  const yValues = group.map(function(entry) { return Number(entry.item.y) || 0; });
  if (Math.max.apply(null, yValues) - Math.min.apply(null, yValues) > 6) return null;

  for (let i = 1; i < group.length; i++) {
    const previous = group[i - 1].item;
    const current = group[i].item;
    const gap = (Number(current.x) || 0) - ((Number(previous.x) || 0) + (Number(previous.width) || 0));
    if (gap < -2 || gap > 24) return null;
  }

  return group.map(function(entry) { return entry.item; });
}

function fillCombTextValue(item, startIndex, charsToApply) {
  const maxLen = getCombItemMaxLen(item);
  const next = String(item.text || "").split("").slice(0, maxLen);
  while (next.length < maxLen) next.push("");
  for (let i = 0; i < charsToApply.length && startIndex + i < maxLen; i++) {
    next[startIndex + i] = charsToApply[i];
  }
  return next.join("").slice(0, maxLen);
}

function applyCombPasteCascade(items, target, focusedCellIndex, pastedChars) {
  const chars = Array.isArray(pastedChars) ? pastedChars : [];
  if (!target || !chars.length) return items;
  const targetMaxLen = getCombItemMaxLen(target);
  const startIndex = Math.max(0, Math.min(Number(focusedCellIndex) || 0, Math.max(0, targetMaxLen - 1)));
  const group = findSafeCombPasteCascadeGroup(items, target);
  if (!group) {
    const text = fillCombTextValue(target, startIndex, chars);
    return (items || []).map(function(item) { return item.id === target.id ? Object.assign({}, item, { text }) : item; });
  }

  const targetInfo = getCombSplitGroupInfo(target);
  const updates = {};
  let offset = 0;
  group.forEach(function(groupItem) {
    const info = getCombSplitGroupInfo(groupItem);
    if (!info || info.index < targetInfo.index || offset >= chars.length) return;
    const groupStartIndex = groupItem.id === target.id ? startIndex : 0;
    const capacity = Math.max(0, getCombItemMaxLen(groupItem) - groupStartIndex);
    const slice = chars.slice(offset, offset + capacity);
    if (!slice.length) return;
    updates[groupItem.id] = fillCombTextValue(groupItem, groupStartIndex, slice);
    offset += slice.length;
  });

  return (items || []).map(function(item) {
    return Object.prototype.hasOwnProperty.call(updates, item.id)
      ? Object.assign({}, item, { text: updates[item.id] })
      : item;
  });
}
// ─── END COMB INPUT CLASSIFICATION ───

function getFittedFontSize(item, scaleFactor) {
  return getTextLayout(item).fontSize;
}

function clampNumber(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

const FORM_TEXT_BASELINE_FONT_SIZE = 10.5;

function isTinyTextField(item) {
  var width = Number(item && item.width) || 0;
  return Boolean(item && (item.maxLength === 1 || width <= 18));
}

function getFieldFontFloor(item) {
  return isTinyTextField(item) ? 6 : 8;
}

function getDefaultFieldFontSize(item) {
  var height = Number(item && item.height) || 12;
  var floor = getFieldFontFloor(item);
  var baseline = isTinyTextField(item) ? Math.min(10, FORM_TEXT_BASELINE_FONT_SIZE) : FORM_TEXT_BASELINE_FONT_SIZE;
  // Normal fields keep an 8pt readable floor; only objectively tiny boxes may drop to 6pt.
  return Math.round(clampNumber(Math.min(baseline, height * 0.85), floor, 11.5) * 10) / 10;
}

function getVisibleFieldFontSize(item) {
  var floor = getFieldFontFloor(item);
  var fallback = getDefaultFieldFontSize(item);
  var raw = item && item.manualFontSize ? Number(item.fontSize) : fallback;
  return Math.max(floor, Number.isFinite(raw) ? raw : fallback);
}

function getSteppedFieldFontSize(item, delta) {
  return clampNumber(getVisibleFieldFontSize(item) + delta, getFieldFontFloor(item), 72);
}

function formatDisplayNumber(value) {
  var n = Number(value || 0);
  if (!Number.isFinite(n)) return "0";
  var rounded = Math.round(n * 10) / 10;
  if (Math.abs(rounded - Math.round(rounded)) < 0.001) return String(Math.round(rounded));
  return rounded.toFixed(1).replace(/\.0$/, "");
}

function itemPageIndex(it) {
  if (!it) return 0;
  const rawPage = Number(it.page);
  if (!Number.isFinite(rawPage)) return 0;

  // FIELD_RENDERING_RULE.md / PAGE-NORM-1:
  // Normalize all item page values to the frontend's 0-based page index.
  // AcroForm and manual items are 0-based. Server-detected auto fields are 1-based.
  if (it.source === "acroform") return rawPage;
  if (it.auto === true && rawPage > 0) return rawPage - 1;
  return rawPage;
}

function captureTypographyTelemetry(items, source) {
  try {
    const allItems = Array.isArray(items) ? items : [];
    const textItems = allItems.filter(function(it) {
      return it && (it.type === "text" || !it.type);
    });
    if (!textItems.length) return;

    const heightBuckets = {};
    const widthBuckets = {};
    const fontSizeBuckets = {};
    let tinyByCurrentCriterion = 0;

    textItems.forEach(function(it) {
      const height = Number(it.height);
      const width = Number(it.width);
      const fontSize = Number(it.fontSize);
      const h = Number.isFinite(height) ? Math.round(height) : 0;
      const w = Number.isFinite(width) ? Math.round(width) : 0;
      const fs = Number.isFinite(fontSize) ? fontSize : 0;

      heightBuckets[h] = (heightBuckets[h] || 0) + 1;
      widthBuckets[w] = (widthBuckets[w] || 0) + 1;

      const fsBucket = (Math.round(fs * 2) / 2).toFixed(1);
      fontSizeBuckets[fsBucket] = (fontSizeBuckets[fsBucket] || 0) + 1;

      if (it.maxLength === 1 || w <= 18) tinyByCurrentCriterion++;
    });

    const topHeights = Object.entries(heightBuckets)
      .sort(function(a, b) { return b[1] - a[1]; })
      .slice(0, 3)
      .map(function(entry) { return { height: Number(entry[0]), count: entry[1] }; });

    const topWidths = Object.entries(widthBuckets)
      .sort(function(a, b) { return b[1] - a[1]; })
      .slice(0, 3)
      .map(function(entry) { return { width: Number(entry[0]), count: entry[1] }; });

    const sortedHeights = textItems
      .map(function(it) {
        const height = Number(it.height);
        return Number.isFinite(height) ? height : 0;
      })
      .sort(function(a, b) { return a - b; });
    const sortedWidths = textItems
      .map(function(it) {
        const width = Number(it.width);
        return Number.isFinite(width) ? width : 0;
      })
      .sort(function(a, b) { return a - b; });
    const medianIndex = Math.floor(textItems.length / 2);
    const medianHeight = sortedHeights[medianIndex] || 0;
    const medianWidth = sortedWidths[medianIndex] || 0;

    const snapshot = {
      source: source || "unknown",
      textFieldCount: textItems.length,
      totalFieldCount: allItems.length,
      medianHeight: Math.round(medianHeight * 10) / 10,
      medianWidth: Math.round(medianWidth * 10) / 10,
      topHeights,
      topWidths,
      fontSizeHistogram: fontSizeBuckets,
      tinyByCurrentCriterion,
      timestamp: Date.now()
    };

    console.log("[SIZE-2-TELEMETRY]", snapshot);
  } catch (err) {
    console.warn("[SIZE-2-TELEMETRY] capture failed:", err);
  }
}

function getPersistKey(item) {
  if (!item) return "";
  if (item.persistKey) return String(item.persistKey);
  if (!item.auto) return String(item.id || "");
  return [
    "auto",
    item.page || 0,
    item.type || "text",
    item.label || "",
    Math.round((item.x || 0) * 10),
    Math.round((item.y || 0) * 10),
    Math.round((item.width || 0) * 10),
    Math.round((item.height || 0) * 10)
  ].join("|");
}

function extractPersistedFieldState(item) {
  if (!item || !item.auto) return null;
  return {
    text: item.type === "text" ? String(item.text || "") : "",
    checked: item.type === "checkbox" || item.type === "radio" ? Boolean(item.checked) : false,
    src: item.type === "image" ? item.src || "" : "",
    fontSize: item.manualFontSize ? item.fontSize : undefined,
    manualFontSize: Boolean(item.manualFontSize),
    width: item.type === "text" || item.type === "image" ? item.width : undefined,
    height: item.type === "text" || item.type === "image" ? item.height : undefined
  };
}

function samePersistedFieldState(a, b) {
  if (!a && !b) return true;
  if (!a || !b) return false;
  return a.text === b.text &&
    a.checked === b.checked &&
    a.src === b.src &&
    a.fontSize === b.fontSize &&
    a.manualFontSize === b.manualFontSize &&
    a.width === b.width &&
    a.height === b.height;
}

function applyButtonToggle(prev, target, nextChecked) {
  if (!target) return prev;
  const groupId = target.acroButtonGroupId || null;

  if (groupId) {
    return (prev || []).map(function(it) {
      if (it.acroButtonGroupId !== groupId) return it;
      return { ...it, checked: nextChecked ? it.id === target.id : false };
    });
  }

  return (prev || []).map(function(it) {
    return it.id === target.id ? { ...it, checked: nextChecked } : it;
  });
}

function sanitizeAcroButtonGroups(items) {
  const seenCheckedGroup = {};
  return (items || []).map(function(it) {
    const groupId = it.acroButtonGroupId || null;
    if (!groupId || !it.checked) return it;
    if (seenCheckedGroup[groupId]) {
      return { ...it, checked: false };
    }
    seenCheckedGroup[groupId] = true;
    return it;
  });
}

function normalizeFieldSource(item) {
  return String((item && item.source) || "").trim().toLowerCase();
}

function isKnownDetectedFlatSource(source) {
  return ["template", "heuristic", "azure", "server", "content_stream", "ocr_raster"].includes(source);
}

function isNativeAcroFormItem(item) {
  if (!item) return false;
  const source = normalizeFieldSource(item);
  if (source === "acroform" || source === "acroform_widgets") return true;
  if (item.acroFieldName != null && String(item.acroFieldName).trim() !== "") return true;
  return item.auto === true && !isKnownDetectedFlatSource(source);
}

function isDetectedFlatItem(item) {
  return Boolean(item && item.auto === true && !isNativeAcroFormItem(item));
}
function normalizeRotationAngle(angle) {
  return ((Number(angle || 0) % 360) + 360) % 360;
}

function suggestedRotationForAngle(angle) {
  var raw = Number(angle || 0);
  var candidates = [
    { target: 90, correction: raw < 0 ? 90 : -90, label: "sideways" },
    { target: 180, correction: raw < 0 ? 180 : -180, label: "upside-down" },
    { target: 270, correction: raw < 0 ? 270 : -270, label: "sideways" },
  ];
  var absAngle = Math.abs(raw);
  for (var i = 0; i < candidates.length; i++) {
    if (Math.abs(absAngle - candidates[i].target) <= 5) return candidates[i];
  }
  var normalized = normalizeRotationAngle(raw);
  for (var j = 0; j < candidates.length; j++) {
    if (Math.abs(normalized - candidates[j].target) <= 5) return candidates[j];
  }
  return null;
}

function hydrateDetectedFields(fields, persistedState) {
  const hydrated = (fields || []).map(function(field) {
    var key = getPersistKey(field);
    var saved = persistedState && persistedState[key];
    if (!saved) return { ...field, persistKey: key };
    return {
      ...field,
      persistKey: key,
      text: field.type === "text" ? String(saved.text || "") : field.text,
      checked: field.type === "checkbox" || field.type === "radio" ? Boolean(saved.checked) : field.checked,
      src: field.type === "image" ? (saved.src || field.src) : field.src,
      fontSize: saved.manualFontSize && typeof saved.fontSize === "number" ? saved.fontSize : field.fontSize,
      manualFontSize: Boolean(saved.manualFontSize),
      width: typeof saved.width === "number" ? saved.width : field.width,
      height: typeof saved.height === "number" ? saved.height : field.height
    };
  });
  return sanitizeAcroButtonGroups(hydrated);
}

function getApproxCharFactor(item) {
  if ((item.hint || "") === "signature" || (item.label || "").toLowerCase().includes("signature")) return 0.48;
  if (item.maxLength === 1) return 0.68;
  if (item.fieldStyle === "eden_fill") return 0.5;
  return 0.54;
}

function approximateTextWidth(text, item, fontSize) {
  return String(text || "").length * fontSize * getApproxCharFactor(item);
}

function wrapTextLines(text, item, fontSize, innerWidth) {
  var raw = String(text || "").replace(/\r/g, "");
  var explicitLines = raw.split("\n");
  var out = [];
  var charFactor = getApproxCharFactor(item);
  var maxChars = Math.max(1, Math.floor(innerWidth / Math.max(fontSize * charFactor, 1)));
  explicitLines.forEach(function(line) {
    var trimmed = line || "";
    if (!trimmed.trim()) {
      out.push("");
      return;
    }
    var words = trimmed.split(/\s+/);
    var current = "";
    words.forEach(function(word) {
      if (!current) {
        current = word;
        return;
      }
      if ((current + " " + word).length <= maxChars) {
        current += " " + word;
      } else {
        out.push(current);
        current = word;
      }
    });
    if (current) out.push(current);
  });
  return out.length ? out : [""];
}

function getTextLayout(item, overrideText) {
  var text = overrideText != null ? String(overrideText) : String(item.text || "");
  var singleChar = isTinyTextField(item) || (text.trim().length <= 1 && (item.width || 0) <= 18);
  var baseWidth = Math.max(Number(item.width) || 0, singleChar ? 10 : 28);
  var baseHeight = Math.max(Number(item.height) || 0, singleChar ? 10 : 12);
  var paddingX = singleChar ? 1 : Math.max(1, Math.min(4, baseHeight * 0.18));
  var paddingY = singleChar ? 0.5 : Math.max(0, Math.min(3, baseHeight * 0.08));
  var innerWidth = Math.max(baseWidth - paddingX * 2, 1);
  var minFont = getFieldFontFloor(item);
  var derivedDefault = getDefaultFieldFontSize(item);
  var chosenBase = item.manualFontSize ? (Number(item.fontSize) || derivedDefault) : derivedDefault;
  var fontSize = Math.max(minFont, chosenBase);
  var longestLine = String(text || "").split(/\r?\n/).reduce(function(max, line) { return line.length > max.length ? line : max; }, "");
  var estimatedWidth = approximateTextWidth(longestLine, item, fontSize);

  if (text.trim() && estimatedWidth > innerWidth) {
    fontSize = Math.max(minFont, fontSize * (innerWidth / Math.max(estimatedWidth, 1)));
  }

  var lineHeightFactor = singleChar ? 1.0 : 1.15;
  var lines = String(text || "").replace(/\r/g, "").split("\n");
  if (!lines.length) lines = [""];

  return {
    fontSize: Math.max(minFont, Math.round(fontSize * 10) / 10),
    lineHeight: Math.max(fontSize * lineHeightFactor, fontSize),
    paddingX,
    paddingY,
    lines
  };
}

function serializeEditorItems(items) {
  try {
    return JSON.stringify(items || []);
  } catch {
    return "[]";
  }
}

// ═══ DETECTION API CONFIG ═══
const host = window.location.hostname || "";
function isLocalHostName(hostname) {
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
}
function resolveDevProOverride(search = window.location.search, protocol = window.location.protocol, hostname = window.location.hostname) {
  const isLocalEnvironment = protocol === "file:" || hostname === "localhost" || hostname === "127.0.0.1";
  return isLocalEnvironment && new URLSearchParams(search || "").has("devPro");
}
const localDetectHost = host === "::1" ? "[::1]" : host;
const localDetectApi = host && window.location.protocol !== "file:"
  ? `http://${localDetectHost}:8000`
  : "http://127.0.0.1:8000";
const defaultDetectApi = isLocalHostName(host) ? localDetectApi : "https://api.nostringspdf.com";
function resolveApiBase(defaultBase, search = window.location.search, hostname = host) {
  const raw = new URLSearchParams(search).get("detectApi");
  if (!raw || !isLocalHostName(hostname)) return defaultBase;
  try {
    const parsed = new URL(raw);
    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return defaultBase;
    if (!isLocalHostName(parsed.hostname)) return defaultBase;
    return parsed.origin;
  } catch (e) {
    return defaultBase;
  }
}
const DETECT_API = resolveApiBase(defaultDetectApi);

function getButtonOnValue(ann) {
  try {
    if (ann.exportValue) return String(ann.exportValue);
    if (ann.buttonValue) return String(ann.buttonValue);
    return "On";
  } catch (e) {
    return "On";
  }
}

function isCheckboxChecked(fieldValue, onValue) {
  if (!fieldValue) return false;
  const v = String(fieldValue);
  if (v === "Off" || v === "/Off" || v === "") return false;
  if (onValue) {
    const ov = String(onValue);
    return v === ov || v === "/" + ov;
  }
  return true;
}

// ACRO-RADIO-GROUP-1 Commit 1: conservative AcroForm button group inference.
// Returns metadata only. Does not change field type, click behavior, render,
// or export. Group identity is assigned ONLY when evidence is strong:
//   - true PDF radio fields (acroButtonType === "radio")
//   - W-9 Boxes3a-b_ReadOrder[0].c1_1[0..6] (tax classification line 3a)
//   - W-4 c1_1[0..2] under topmostSubform[0].Page1[0] (filing status step 1c)
//   - CMS-L564 explicit Yes/No pair list (not a generic prefix rule)
//   - I-9 CB_1..CB_4 ONLY if alternativeText/fieldLabel contains
//     citizenship/immigration evidence; otherwise candidate-only.
// All other patterns get no group id. Independent checkboxes (W-4 c1_2, c1_3;
// W-9 c1_2; I-9 CB_Alt*) must remain ungrouped.
function inferAcroButtonGroupMeta(ann, fieldName, acroButtonType, acroOnValue) {
  var meta = {
    acroButtonGroupId: null,
    acroButtonGroupKind: null,
    acroButtonIndex: null,
    acroMutexCandidate: false,
    acroMutexRule: null,
    acroVisualStyle: null
  };

  if (!fieldName || typeof fieldName !== "string") return meta;

  var normalizedOnValue = acroOnValue != null ? String(acroOnValue).replace(/^\//, "") : "";
  var ds11Key = fieldName + "|" + normalizedOnValue;

  // DS-11 exact mutex groups. Key format is verbatim "fieldName|onValue".
  // Do not generalize these into prefix or Yes/No rules: false mutex is worse
  // than missing mutex. Book/Card Status and Clear intentionally remain ungrouped.
  var ds11Groups = {
    "Selection|Book": { groupId: "acro-mutex:ds11:passport-product", rule: "ds11-passport-product", index: 0 },
    "Selection|Card": { groupId: "acro-mutex:ds11:passport-product", rule: "ds11-passport-product", index: 1 },
    "Selection|Both": { groupId: "acro-mutex:ds11:passport-product", rule: "ds11-passport-product", index: 2 },

    "Regular or Large Book|Regular": { groupId: "acro-mutex:ds11:book-size", rule: "ds11-book-size", index: 0 },
    "Regular or Large Book|Large": { groupId: "acro-mutex:ds11:book-size", rule: "ds11-book-size", index: 1 },

    "Gender|M": { groupId: "acro-mutex:ds11:applicant-sex", rule: "ds11-applicant-sex", index: 0 },
    "Gender|F": { groupId: "acro-mutex:ds11:applicant-sex", rule: "ds11-applicant-sex", index: 1 },

    "Parent 1 Gender|M": { groupId: "acro-mutex:ds11:parent-1-gender", rule: "ds11-parent-1-gender", index: 0 },
    "Parent 1 Gender|F": { groupId: "acro-mutex:ds11:parent-1-gender", rule: "ds11-parent-1-gender", index: 1 },

    "Parent 2 Gender|M": { groupId: "acro-mutex:ds11:parent-2-gender", rule: "ds11-parent-2-gender", index: 0 },
    "Parent 2 Gender|F": { groupId: "acro-mutex:ds11:parent-2-gender", rule: "ds11-parent-2-gender", index: 1 },

    "Parent 1 US Citizen|Yes": { groupId: "acro-mutex:ds11:parent-1-us-citizen", rule: "ds11-parent-1-us-citizen", index: 0 },
    "Parent 1 US Citizen|No": { groupId: "acro-mutex:ds11:parent-1-us-citizen", rule: "ds11-parent-1-us-citizen", index: 1 },

    "Parent 2 US Citizen|Yes": { groupId: "acro-mutex:ds11:parent-2-us-citizen", rule: "ds11-parent-2-us-citizen", index: 0 },
    "Parent 2 US Citizen|No": { groupId: "acro-mutex:ds11:parent-2-us-citizen", rule: "ds11-parent-2-us-citizen", index: 1 },

    "Ever Married|Yes": { groupId: "acro-mutex:ds11:ever-married", rule: "ds11-ever-married", index: 0 },
    "Ever Married|No": { groupId: "acro-mutex:ds11:ever-married", rule: "ds11-ever-married", index: 1 },

    "Spouse US Citizen|Yes": { groupId: "acro-mutex:ds11:spouse-us-citizen", rule: "ds11-spouse-us-citizen", index: 0 },
    "Spouse US Citizen|No": { groupId: "acro-mutex:ds11:spouse-us-citizen", rule: "ds11-spouse-us-citizen", index: 1 },

    "Divorced|Yes": { groupId: "acro-mutex:ds11:divorced", rule: "ds11-divorced", index: 0 },
    "Divorced|No": { groupId: "acro-mutex:ds11:divorced", rule: "ds11-divorced", index: 1 },

    "Ever Applied or Issued|Yes": { groupId: "acro-mutex:ds11:ever-applied-or-issued", rule: "ds11-ever-applied-or-issued", index: 0 },
    "Ever Applied or Issued|No": { groupId: "acro-mutex:ds11:ever-applied-or-issued", rule: "ds11-ever-applied-or-issued", index: 1 },

    "Additional #|Home": { groupId: "acro-mutex:ds11:additional-phone-type", rule: "ds11-additional-phone-type", index: 0 },
    "Additional #|Work": { groupId: "acro-mutex:ds11:additional-phone-type", rule: "ds11-additional-phone-type", index: 1 },
    "Additional #|Cell": { groupId: "acro-mutex:ds11:additional-phone-type", rule: "ds11-additional-phone-type", index: 2 },
    "Additional #|Other": { groupId: "acro-mutex:ds11:additional-phone-type", rule: "ds11-additional-phone-type", index: 3 }
  };

  if (Object.prototype.hasOwnProperty.call(ds11Groups, ds11Key)) {
    var ds11 = ds11Groups[ds11Key];
    meta.acroButtonGroupId = ds11.groupId;
    meta.acroButtonGroupKind = "known-mutex-checkbox";
    meta.acroButtonIndex = ds11.index;
    meta.acroMutexCandidate = false;
    meta.acroMutexRule = ds11.rule;
    return meta;
  }

  // True radio fields — preserve group identity from base name.
  if (acroButtonType === "radio") {
    var radioBase = fieldName.replace(/\[\d+\]$/, "");
    meta.acroButtonGroupId = "acro-radio:" + radioBase;
    meta.acroButtonGroupKind = "true-radio";
    meta.acroMutexRule = "true-radio-field";
    var radioIdxMatch = fieldName.match(/\[(\d+)\]$/);
    if (radioIdxMatch) meta.acroButtonIndex = parseInt(radioIdxMatch[1], 10);
    return meta;
  }

  // W-9 tax classification line 3a — exact path match.
  var w9Match = fieldName.match(/^topmostSubform\[0\]\.Page1\[0\]\.Boxes3a-b_ReadOrder\[0\]\.c1_1\[(\d+)\]$/);
  if (w9Match) {
    var w9Idx = parseInt(w9Match[1], 10);
    if (w9Idx >= 0 && w9Idx <= 6) {
      meta.acroButtonGroupId = "acro-mutex:w9:tax-classification-line-3a";
      meta.acroButtonGroupKind = "known-mutex-checkbox";
      meta.acroButtonIndex = w9Idx;
      meta.acroMutexRule = "w9-tax-classification-line-3a";
      return meta;
    }
  }

  // W-4 filing status step 1(c) — exact path match.
  var w4Match = fieldName.match(/^topmostSubform\[0\]\.Page1\[0\]\.c1_1\[(\d+)\]$/);
  if (w4Match) {
    var w4Idx = parseInt(w4Match[1], 10);
    if (w4Idx >= 0 && w4Idx <= 2) {
      meta.acroButtonGroupId = "acro-mutex:w4:filing-status-step-1c";
      meta.acroButtonGroupKind = "known-mutex-checkbox";
      meta.acroButtonIndex = w4Idx;
      meta.acroMutexRule = "w4-filing-status-step-1c";
      return meta;
    }
  }

  // CMS-L564 explicit Yes/No pair list. NOT a generic CheckBox1-N-M rule.
  var cmsPairs = {
    "CheckBox1-10-1": "acro-mutex:cms-l564:checkbox1-10",
    "CheckBox1-10-2": "acro-mutex:cms-l564:checkbox1-10",
    "CheckBox1-11-1": "acro-mutex:cms-l564:checkbox1-11",
    "CheckBox1-11-2": "acro-mutex:cms-l564:checkbox1-11",
    "CheckBox1-12-5": "acro-mutex:cms-l564:checkbox1-12",
    "CheckBox1-12-6": "acro-mutex:cms-l564:checkbox1-12",
    "CheckBox1-14-1": "acro-mutex:cms-l564:checkbox1-14",
    "CheckBox1-14-2": "acro-mutex:cms-l564:checkbox1-14",
    "CheckBox1-15-1": "acro-mutex:cms-l564:checkbox1-15",
    "CheckBox1-15-2": "acro-mutex:cms-l564:checkbox1-15"
  };
  if (Object.prototype.hasOwnProperty.call(cmsPairs, fieldName)) {
    meta.acroButtonGroupId = cmsPairs[fieldName];
    meta.acroButtonGroupKind = "known-mutex-checkbox";
    meta.acroMutexRule = "cms-l564-checkbox-pair";
    meta.acroVisualStyle = "radio-circle";
    return meta;
  }

  // I-9 citizenship status — CB_1..CB_4 with alt-text evidence required.
  // Independent CB_Alt* fields must NOT match.
  var i9Match = fieldName.match(/^CB_([1-4])$/);
  if (i9Match) {
    var altText = (ann && (ann.alternativeText || ann.fieldLabel)) || "";
    var altLower = String(altText).toLowerCase();
    var citizenshipEvidence =
      altLower.indexOf("citizen") !== -1 ||
      altLower.indexOf("national") !== -1 ||
      altLower.indexOf("permanent resident") !== -1 ||
      altLower.indexOf("alien") !== -1 ||
      altLower.indexOf("immigration") !== -1;
    if (citizenshipEvidence) {
      meta.acroButtonGroupId = "acro-mutex:i9:citizenship-status";
      meta.acroButtonGroupKind = "known-mutex-checkbox";
      meta.acroButtonIndex = parseInt(i9Match[1], 10) - 1;
      meta.acroMutexRule = "i9-citizenship-status-with-alttext";
    } else {
      // No alt-text evidence — flag as candidate but do NOT assign group id.
      meta.acroMutexCandidate = true;
      meta.acroMutexRule = "i9-citizenship-candidate";
    }
    return meta;
  }

  return meta;
}

function getChoiceFieldOptions(ann) {
  try {
    if (Array.isArray(ann.options)) {
      return ann.options.map(opt => {
        if (typeof opt === "string") return { value: opt, label: opt };
        if (opt && typeof opt === "object") {
          return {
            value: String(opt.exportValue || opt.value || opt.displayValue || ""),
            label: String(opt.displayValue || opt.label || opt.exportValue || opt.value || "")
          };
        }
        return null;
      }).filter(Boolean);
    }
    return [];
  } catch (e) {
    return [];
  }
}

function formatDateInput(rawDigits) {
  const digits = String(rawDigits || "").replace(/\D/g, "").slice(0, 8);
  let result = digits.slice(0, 2);
  if (digits.length > 2) result += "/" + digits.slice(2, 4);
  if (digits.length > 4) result += "/" + digits.slice(4, 8);
  return result;
}

// Client-side AcroForm detection (free tier — works for interactive PDFs)
async function detectFieldsClient(bytes) {
  var results = [];
  var fontSizes = [];
  var used = [];

  function noOverlap(x, y, pg) {
    for (var i = 0; i < used.length; i++) {
      if (used[i].pg === pg && Math.abs(used[i].x - x) < 15 && Math.abs(used[i].y - y) < 6) return false;
    }
    return true;
  }
  function addF(f) {
    if (noOverlap(f.x, f.y, f.page)) {
      results.push(f);
      used.push({ x: f.x, y: f.y, pg: f.page });
    }
  }

  var pdf = await window.pdfjsLib.getDocument({ data: bytes.slice(0) }).promise;

  for (var pi = 0; pi < pdf.numPages; pi++) {
    var page = await pdf.getPage(pi + 1);
    var vp = page.getViewport({ scale: 1 });

    // ═══ AcroForm Widget Annotations ═══
    try {
      var annots = await page.getAnnotations();
      if (annots && annots.length > 0) {
        for (var ai = 0; ai < annots.length; ai++) {
          var ann = annots[ai];
          if (ann.subtype !== "Widget" || !ann.rect || ann.rect.length < 4) continue;

          var r = ann.rect;
          var fx = r[0];
          var fy = vp.height - r[3];
          var fw = r[2] - r[0];
          var fh = r[3] - r[1];
          if (fw < 3 || fh < 3) continue;

          var fieldName = ann.fieldName || ann.alternativeText || ann.fieldLabel || "";
          var fieldType = ann.fieldType || "";
          var fieldValue = ann.fieldValue || "";
          var fid = "w" + pi + "_" + ai;
          var fi = inferFieldType(fieldName);

          if (fieldType === "Btn" || ann.checkBox || ann.radioButton) {
            if (ann.pushButton) continue;
            var acroOnValue = getButtonOnValue(ann);
            // Commit 1 narrows radio classification to PDF.js's ann.radioButton.
            // Previously also checked a field-flag bit, but that widening is
            // suspicious without corpus evidence; defer to a later evidence-based slice if needed.
            var acroButtonType = ann.radioButton ? "radio" : "checkbox";
            var acroGroupMeta = inferAcroButtonGroupMeta(ann, fieldName, acroButtonType, acroOnValue);
            addF({
              id: fid, page: pi, x: fx, y: fy,
              width: Math.max(fw, 10), height: Math.max(fh, 10),
              fontSize: Math.round(fh * 0.7),
              type: "checkbox",
              checked: isCheckboxChecked(fieldValue, acroOnValue),
              auto: true, source: "acroform", label: fieldName, hint: "checkbox",
              acroButtonType: acroButtonType,
              acroIsCheckbox: acroButtonType === "checkbox",
              acroIsRadio: acroButtonType === "radio",
              acroFieldName: fieldName,
              acroOnValue: acroOnValue,
              acroExportValue: ann.exportValue != null ? String(ann.exportValue) : null,
              acroButtonValue: ann.buttonValue != null ? String(ann.buttonValue) : null,
              acroAlternativeText: ann.alternativeText || ann.fieldLabel || "",
              acroAppearanceState: ann.fieldValue ? String(ann.fieldValue) : null,
              acroFieldFlags: ann.fieldFlags || 0,
              acroDefaultAppearanceRaw: ann.defaultAppearanceData || null,
              acroAppearance: adaptPdfJsAppearance(ann.defaultAppearanceData),
              acroButtonGroupId: acroGroupMeta.acroButtonGroupId,
              acroButtonGroupKind: acroGroupMeta.acroButtonGroupKind,
              acroButtonIndex: acroGroupMeta.acroButtonIndex,
              acroMutexCandidate: acroGroupMeta.acroMutexCandidate,
              acroMutexRule: acroGroupMeta.acroMutexRule,
              acroVisualStyle: acroGroupMeta.acroVisualStyle
            });
          } else if (fieldType === "Ch") {
            const choiceOptions = getChoiceFieldOptions(ann);
            addF({
              id: fid, page: pi, x: fx, y: fy,
              width: fw, height: Math.max(fh, 12),
              fontSize: Math.max(Math.round(fh * 0.65), 8),
              type: "choice",
              text: String(fieldValue || ""),
              auto: true, source: "acroform", label: fieldName,
              hint: fi.hint, placeholder: fi.ph,
              acroFieldName: fieldName,
              acroFieldFlags: ann.fieldFlags || 0,
              acroChoiceOptions: choiceOptions,
              acroIsCombo: Boolean((ann.fieldFlags || 0) & 0x20000),
              acroIsEditable: Boolean((ann.fieldFlags || 0) & 0x40000),
              acroDefaultAppearanceRaw: ann.defaultAppearanceData || null,
              acroAppearance: adaptPdfJsAppearance(ann.defaultAppearanceData)
            });
          } else if (fieldType === "Sig") {
            addF({
              id: fid, page: pi, x: fx, y: fy,
              width: fw, height: Math.max(fh, 12),
              fontSize: Math.max(Math.round(fh * 0.55), 8),
              type: "signatureTarget",
              auto: true, source: "acroform",
              label: ann.alternativeText || ann.fieldLabel || fieldName || "Signature",
              hint: "signature", placeholder: "Sign here",
              acroFieldName: fieldName,
              acroFieldType: "Sig",
              acroIsSignature: true,
              acroFieldFlags: ann.fieldFlags || 0
            });
          } else if (fieldType === "Tx" || fw > 10) {
            const validateScripts = Array.isArray(ann.actions?.Validate)
              ? ann.actions.Validate
              : ann.actions?.Validate
              ? [ann.actions.Validate]
              : [];
            const acroIsDate = validateScripts.some(function(script) {
              return /util\.scand|util\.printd|mm\/dd\/yyyy/i.test(String(script || ""));
            });
            addF({
              id: fid, page: pi, x: fx, y: fy,
              width: fw, height: Math.max(fh, 12),
              fontSize: Math.max(Math.round(fh * 0.65), 8),
              type: "text", text: String(fieldValue || ""),
              auto: true, source: "acroform", label: fieldName,
              hint: fi.hint, placeholder: fi.ph,
              acroFieldName: fieldName,
              acroFieldFlags: ann.fieldFlags || 0,
              acroMaxLen: typeof ann.maxLen === "number" ? ann.maxLen : null,
              acroMultiline: Boolean((ann.fieldFlags || 0) & 0x1000),
              acroComb: Boolean(ann.comb),
              acroIsComb: Boolean(ann.comb),
              acroIsDate: acroIsDate,
              acroActions: ann.actions || null,
              acroDefaultAppearanceRaw: ann.defaultAppearanceData || null,
              acroAppearance: adaptPdfJsAppearance(ann.defaultAppearanceData)
            });
          }
        }
      }
    } catch (e) { /* fall through */ }

    // Collect font sizes for dominant size calc
    try {
      var tc = await page.getTextContent();
      for (var ti = 0; ti < (tc.items || []).length; ti++) {
        var item = tc.items[ti];
        if (!item.str || !item.str.trim()) continue;
        var tr = item.transform || [1,0,0,1,0,0];
        var fs = Math.sqrt(tr[0] * tr[0] + tr[1] * tr[1]);
        if (fs > 0) fontSizes.push(Math.round(fs));
      }
    } catch(e) {}
  }

  // Dominant font size
  var sizeCounts = {};
  for (var fsi = 0; fsi < fontSizes.length; fsi++) { sizeCounts[fontSizes[fsi]] = (sizeCounts[fontSizes[fsi]] || 0) + 1; }
  var dominant = 12, maxCount = 0;
  var sizeKeys = Object.keys(sizeCounts);
  for (var sk = 0; sk < sizeKeys.length; sk++) {
    if (sizeCounts[sizeKeys[sk]] > maxCount) { maxCount = sizeCounts[sizeKeys[sk]]; dominant = parseInt(sizeKeys[sk]); }
  }

  return { fields: results, dominantFontSize: dominant, textFound: fontSizes.length > 0, method: results.length > 0 ? "acroform" : "none" };
}

// Server-side detection via backend API (Pro tier — works for ALL PDFs)
async function detectFieldsServer(fileObj, accessToken, options) {
  if (!accessToken) {
    throw new Error("Please log in to use auto-detect.");
  }

  const formData = new FormData();
  formData.append("file", fileObj);
  const headers = {
    "Authorization": `Bearer ${accessToken}`,
  };
  if (options && options.postRotation && options.docHash) {
    headers["X-Post-Rotation"] = "true";
    headers["X-Doc-Hash"] = options.docHash;
  }

  const resp = await fetch(DETECT_API + "/detect-fields", {
    method: "POST",
    body: formData,
    headers,
  });

  if (resp.status === 401) {
    await supabase.auth.signOut();
    throw new Error("Your session expired. Please log in again.");
  }

  if (resp.status === 403 || resp.status === 429 || resp.status === 503) {
    const err = await resp.json().catch(() => ({ detail: "forbidden" }));
    const error = new Error(err.message || "Auto-detect is unavailable.");
    error.detail = err.detail;
    error.resets_at = err.resets_at;
    error.doc_hash = err.doc_hash;
    throw error;
  }

  if (!resp.ok) {
    const err = await resp.json().catch(() => ({ detail: "Server error" }));
    throw new Error(err.detail || "Detection failed");
  }

  const data = await resp.json();
  const detectionSource = data.source || data.method || "server";

  // Convert server response to client format
  var fields = (data.fields || []).map(function(f) {
  var fi = inferFieldType(f.label || "");
  var fieldType = f.type || "text";
  var isSelectionControl = fieldType === "checkbox" || fieldType === "radio";
  var checked = Boolean(f.checked || (f.value && f.value !== "/Off"));
  var bbox = Array.isArray(f.bbox) ? f.bbox : null;
  var x = bbox ? bbox[0] : f.x;
  var y = bbox ? bbox[1] : f.y;
  var width = bbox ? bbox[2] : f.width;
  var height = bbox ? bbox[3] : f.height;

  return {
    id: f.id || ("s_" + Math.random().toString(36).substr(2, 6)),
    page: f.page || 0,
    x,
    y,
    width,
    height,
    fontSize: f.fontSize || Math.max(Math.round((height || 14) * 0.55), 7),
    type: fieldType,
    text: isSelectionControl ? "" : String(f.value || ""),
    checked: isSelectionControl ? checked : false,
    auto: true,
    source: f.source || detectionSource,
    label: f.label || "Field",
    hint: isSelectionControl ? fieldType : fi.hint,
    placeholder: isSelectionControl ? "" : fi.ph,
    groupId: f.groupId,
    acroButtonType: f.acroButtonType,
    acroIsCheckbox: f.acroIsCheckbox,
    acroIsRadio: f.acroIsRadio,
    acroFieldName: f.acroFieldName,
    acroOnValue: f.acroOnValue,
    acroExportValue: f.acroExportValue,
    acroButtonValue: f.acroButtonValue,
    acroAlternativeText: f.acroAlternativeText,
    acroButtonGroupId: f.acroButtonGroupId,
    acroButtonGroupKind: f.acroButtonGroupKind,
    acroButtonIndex: f.acroButtonIndex,
    acroMutexCandidate: f.acroMutexCandidate,
    acroMutexRule: f.acroMutexRule,
    acroVisualStyle: f.acroVisualStyle,
    acroFieldType: f.acroFieldType,
    acroIsSignature: f.acroIsSignature,
    maxLength: f.maxLength || undefined,
    fieldStyle: f.fieldStyle || undefined,
    guideOffset: typeof f.guideOffset === "number" ? f.guideOffset : undefined,
    renderOffsetY: typeof f.renderOffsetY === "number" ? f.renderOffsetY : undefined,
    renderHeight: typeof f.renderHeight === "number" ? f.renderHeight : undefined,
    renderAnchor: typeof f.renderAnchor === "string" ? f.renderAnchor : undefined,
  };
});

  return {
    fields: fields,
    dominantFontSize: 10,
    textFound: true,
    method: detectionSource,
    fieldCount: data.fieldCount || fields.length,
    page_angles: data.page_angles || [],
    doc_hash: data.doc_hash || null,
  };
}

async function notifyRotationEvent(accessToken, docHash) {
  if (!accessToken || !docHash) return;
  const resp = await fetch(DETECT_API + "/detect/rotation-event", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ doc_hash: docHash }),
  });
  if (!resp.ok) {
    const err = await resp.json().catch(() => ({ detail: "rotation_event_failed" }));
    throw new Error(err.message || err.detail || "Rotation event failed");
  }
}

async function getSubscriptionStatus(accessToken) {
  if (!accessToken) {
    return { is_pro: false, subscription_status: null, subscription_plan: null };
  }

  const resp = await fetch(DETECT_API + "/stripe/subscription-status", {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${accessToken}`,
    },
  });

  if (resp.status === 401) {
    await supabase.auth.signOut();
    return { is_pro: false, subscription_status: null, subscription_plan: null };
  }

  if (!resp.ok) {
    console.error("Subscription status lookup failed:", await resp.text().catch(() => ""));
    return { is_pro: false, subscription_status: null, subscription_plan: null };
  }

  return await resp.json();
}

async function listSavedSignaturesForAccount(accessToken) {
  if (!accessToken) return [];
  const resp = await fetch(DETECT_API + "/signatures", {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${accessToken}`,
    },
  });
  if (resp.status === 401) {
    await supabase.auth.signOut();
    return [];
  }
  if (resp.status === 403) return [];
  if (!resp.ok) {
    console.error("Saved signature load failed:", await resp.text().catch(() => ""));
    return [];
  }
  const data = await resp.json();
  return Array.isArray(data.signatures) ? data.signatures : [];
}

async function createSavedSignatureForAccount(accessToken, imageData) {
  const resp = await fetch(DETECT_API + "/signatures", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ image_data: imageData }),
  });
  if (resp.status === 401) {
    await supabase.auth.signOut();
    return [];
  }
  if (!resp.ok) {
    console.error("Saved signature create failed:", await resp.text().catch(() => ""));
    throw new Error("saved_signature_create_failed");
  }
  const data = await resp.json();
  return Array.isArray(data.signatures) ? data.signatures : [];
}

async function deleteSavedSignatureForAccount(accessToken, signatureId) {
  const resp = await fetch(DETECT_API + "/signatures/" + encodeURIComponent(signatureId), {
    method: "DELETE",
    headers: {
      "Authorization": `Bearer ${accessToken}`,
    },
  });
  if (resp.status === 401) {
    await supabase.auth.signOut();
    return false;
  }
  if (resp.status === 404) return true;
  if (!resp.ok) {
    console.error("Saved signature delete failed:", await resp.text().catch(() => ""));
    throw new Error("saved_signature_delete_failed");
  }
  return true;
}




// ─── DRAGGABLE/RESIZABLE ITEM ───
function DragItem({ it, scale, sel, editorMode, onSel, onClearSel, onUp, onDel, editingId, setEditingId, onStartTyping, onStopTyping, manualFocusItemId, onManualFocusHandled, setItems }) {
  const [dragging, setDrag] = useState(false); const [resizing, setResize] = useState(false);
  const [hovered, setHovered] = useState(false);
  const [combFocusedCellIndex, setCombFocusedCellIndex] = useState(0);
  const [combHasFocus, setCombHasFocus] = useState(false);
  const isTouchRef = useRef(typeof window !== "undefined" && window.matchMedia && window.matchMedia("(hover: none)").matches);
  const ds = useRef({}); const rs = useRef({});
  const inputRef = useRef(null);
  const combRef = useRef(null);
  const isEditing = editingId === it.id;
  const isEditMode = editorMode === "edit";
  const isNativeAcroForm = isNativeAcroFormItem(it);
  const isDetectedFlat = isDetectedFlatItem(it);
  const isSystemField = it.auto === true;
  const isChoiceField = it.type === "choice";
  const isTextLikeField = it.type === "text" || isChoiceField;
  const isManualText = it.type === "text" && it.auto !== true;
  const canEditGeometry = !isNativeAcroForm && (isEditMode || isManualText);
  const isDateTextInput = it.source === "acroform" && it.type === "text" && it.acroIsComb !== true && it.acroIsDate === true;
  // FIELD_RENDERING_RULE.md: only manual text fields use direct Fill-mode inputs;
  // system text fields keep the document-native display path unless actively editing.
  const shouldRenderInput = isTextLikeField && (isEditing || (isManualText && !isEditMode));
  const textLayout = getTextLayout(it);
  const displayFontSize = textLayout.fontSize;
  const resolvedAppearance = it.acroAppearance
    ? resolveAppearance(it.acroAppearance, { width: it.width, height: it.height })
    : null;
  const appearanceFontSize = displayFontSize;
  const isCharBox = it.maxLength === 1;
  const combMaxLen = it.maxLength || it.acroMaxLen || 0;
  const shouldRenderCombInput = it.source === "acroform" && it.type === "text" && it.acroIsComb === true && combMaxLen > 0 && !isTouchRef.current;
  const isUnderlineOnly = it.fieldStyle === "underline_only";
  const legacyIosFill = it.fieldStyle === "ios_fill";
  const legacyEdenFill = it.fieldStyle === "eden_fill";
  // Phase A: system fields use unified watercolor treatment; legacy ios_fill/eden_fill paths preserved for compatibility.
  const isIosFill = !isSystemField && legacyIosFill;
  const isEdenFill = !isSystemField && legacyEdenFill;
  const systemChoiceBorder = sel && !isEditMode ? "1px solid rgba(0,122,255,0.30)" : "1px solid rgba(0,122,255,0.18)";
  const systemChoiceBackground = sel && !isEditMode ? "rgba(0,122,255,0.08)" : "rgba(0,122,255,0.05)";
  const baseTop = it.y * scale;
  const baseHeight = Math.max((it.height || 12) * scale, it.auto ? 6 : 8);
  const visualHeight = isEdenFill
    ? Math.max((it.height || 7) * scale, 6)
    : isIosFill
    ? ((typeof it.renderHeight === "number") ? Math.max(it.renderHeight * scale, 6) : Math.max(Math.round(baseHeight * 0.66), 7))
    : baseHeight;
  const visualTop = isEdenFill
    ? baseTop
    : isIosFill
    ? it.renderAnchor === "underline_center"
      ? baseTop - (visualHeight / 2) + ((typeof it.renderOffsetY === "number") ? it.renderOffsetY * scale : 0)
      : baseTop + ((typeof it.renderOffsetY === "number") ? it.renderOffsetY * scale : Math.max(1, Math.round(baseHeight * 0.12)))
    : baseTop;
  const underlineBottomInset = isUnderlineOnly ? Math.max(1, Math.round(visualHeight * 0.16)) : 0;

  const shouldHonorManualFocusRequest = isManualText && manualFocusItemId === it.id;
  useLayoutEffect(() => {
    if (!isEditing && !shouldHonorManualFocusRequest) return;
    const focusTarget = inputRef.current || (shouldRenderCombInput ? combRef.current : null);
    if (!focusTarget) return;
    const focusCurrentTarget = () => {
      const currentTarget = inputRef.current || (shouldRenderCombInput ? combRef.current : null);
      if (currentTarget !== focusTarget) return;
      try { focusTarget.focus({ preventScroll: true }); }
      catch (err) { focusTarget.focus(); }
      if (typeof focusTarget.setSelectionRange === "function") {
        const end = String(focusTarget.value || "").length;
        try { focusTarget.setSelectionRange(end, end); } catch (err) {}
      }
      if (shouldHonorManualFocusRequest && document.activeElement === focusTarget && onManualFocusHandled) {
        onManualFocusHandled(it.id);
      }
    };
    focusCurrentTarget();
    let raf2 = null;
    const timers = [];
    const raf = requestAnimationFrame(() => {
      focusCurrentTarget();
      raf2 = requestAnimationFrame(focusCurrentTarget);
    });
    if (shouldHonorManualFocusRequest) {
      timers.push(setTimeout(focusCurrentTarget, 0));
      timers.push(setTimeout(focusCurrentTarget, 60));
    }
    return () => {
      cancelAnimationFrame(raf);
      if (raf2 !== null) cancelAnimationFrame(raf2);
      timers.forEach(clearTimeout);
    };
  }, [isEditing, it.id, shouldRenderCombInput, shouldHonorManualFocusRequest, onManualFocusHandled]);
  const activateText = e => {
    e.stopPropagation();
    if (window.__nspdfSuppressCanvasClickUntil && Date.now() < window.__nspdfSuppressCanvasClickUntil) return;
    if (isEditMode && isNativeAcroForm) {
      setEditingId(null);
      return;
    }
    onSel();
    if (isEditMode) {
      if (isManualText && onStartTyping) onStartTyping();
      else setEditingId(null);
      return;
    }
    if (isTextLikeField) {
      setEditingId(it.id);
    }
  };

  const maxLen = combMaxLen;
  const focusedCellIndex = Math.max(0, Math.min(combFocusedCellIndex, Math.max(0, maxLen - 1)));
  const chars = (it.text || "").split("").slice(0, maxLen);
  while (chars.length < maxLen) chars.push("");
  const updateChars = nextChars => onUp({ ...it, text: nextChars.join("").slice(0, maxLen) });
  const focusCell = idx => {
    setCombFocusedCellIndex(Math.max(0, Math.min(maxLen - 1, idx)));
    requestAnimationFrame(() => {
      const target = inputRef.current || combRef.current;
      try { target?.focus({ preventScroll: true }); }
      catch (err) { target?.focus(); }
    });
  };
  const focusCombAtPoint = e => {
    const rect = combRef.current?.getBoundingClientRect();
    if (!rect || !maxLen) {
      focusCell(focusedCellIndex);
      return;
    }
    const clientX = e.touches?.[0]?.clientX ?? e.clientX;
    const cellWidth = rect.width / maxLen;
    const idx = Math.floor((clientX - rect.left) / Math.max(cellWidth, 1));
    focusCell(idx);
  };
  const handleKeyDown = e => {
    if (e.ctrlKey || e.metaKey || e.altKey) return;
    if (e.key === "Tab") return;
    const charClass = getCombCharClass(it);
    const isAcceptedChar = isCombAcceptedChar(e.key, charClass);
    if (isAcceptedChar) {
      e.preventDefault();
      const next = chars.slice();
      next[focusedCellIndex] = e.key;
      updateChars(next);
      focusCell(focusedCellIndex + 1);
    } else if (e.key === "Backspace") {
      e.preventDefault();
      const next = chars.slice();
      if (next[focusedCellIndex]) {
        next[focusedCellIndex] = "";
        updateChars(next);
        if (focusedCellIndex > 0) {
          focusCell(focusedCellIndex - 1);
        }
      } else if (focusedCellIndex > 0) {
        next[focusedCellIndex - 1] = "";
        updateChars(next);
        focusCell(focusedCellIndex - 1);
      }
    } else if (e.key === "ArrowLeft") {
      e.preventDefault();
      focusCell(focusedCellIndex - 1);
    } else if (e.key === "ArrowRight") {
      e.preventDefault();
      focusCell(focusedCellIndex + 1);
    } else if (e.key.length === 1) {
      e.preventDefault();
    }
  };
  const handlePaste = e => {
    e.preventDefault();
    const charClass = getCombCharClass(it);
    const pastedChars = filterCombTextForCharClass(e.clipboardData?.getData("text") || "", charClass);
    if (!pastedChars.length) return;
    setItems(prev => applyCombPasteCascade(prev, it, focusedCellIndex, pastedChars));
    focusCell(Math.min(maxLen - 1, focusedCellIndex + pastedChars.length));
  };
  const updateTextInputValue = (value, keepAutoSlash) => {
    if (!isDateTextInput) {
      onUp({ ...it, text: value });
      return;
    }
    const digits = String(value || "").replace(/\D/g, "").slice(0, 8);
    const previousDigits = String(it.text || "").replace(/\D/g, "");
    let text = formatDateInput(digits);
    if (keepAutoSlash && digits.length > previousDigits.length && (digits.length === 2 || digits.length === 4)) {
      text += "/";
    }
    onUp({ ...it, text: text });
  };
  const handleTextInputKeyDown = e => {
    if (e.key === "Enter" || e.key === "Escape") {
      e.preventDefault();
      setEditingId(null);
      if (onStopTyping) onStopTyping();
      return;
    }
    if (!isDateTextInput || e.ctrlKey || e.metaKey || e.altKey) return;
    if (e.key === "Backspace") {
      e.preventDefault();
      const digits = String(it.text || "").replace(/\D/g, "");
      updateTextInputValue(digits.slice(0, -1));
    } else if (e.key === "/") {
      e.preventDefault();
    } else if (e.key.length === 1 && !/^[0-9]$/.test(e.key)) {
      e.preventDefault();
    }
  };
  const handleTextInputPaste = e => {
    if (!isDateTextInput) return;
    e.preventDefault();
    updateTextInputValue(e.clipboardData?.getData("text") || "");
  };

  const startEditingDrag = e => {
    e.stopPropagation();
    const p = e.touches ? e.touches[0] : e;
    const start = { x: p.clientX, y: p.clientY, fx: it.x, fy: it.y, moved: false };
    const mv = ev => {
      const pt = ev.touches ? ev.touches[0] : ev;
      const dx = pt.clientX - start.x;
      const dy = pt.clientY - start.y;
      if (!start.moved && Math.hypot(dx, dy) <= 3) return;
      start.moved = true;
      ev.preventDefault();
      window.__nspdfSuppressCanvasClickUntil = Date.now() + 300;
      setEditingId(null);
      onSel();
      onUp({ ...it, x: start.fx + dx / scale, y: start.fy + dy / scale });
    };
    const up = () => {
      if (start.moved) window.__nspdfSuppressCanvasClickUntil = Date.now() + 300;
      window.removeEventListener("mousemove", mv);
      window.removeEventListener("mouseup", up);
      window.removeEventListener("touchmove", mv);
      window.removeEventListener("touchend", up);
    };
    window.addEventListener("mousemove", mv);
    window.addEventListener("mouseup", up);
    window.addEventListener("touchmove", mv, { passive: false });
    window.addEventListener("touchend", up);
  };

  // Drag
  const startDrag = (e, allowWhileEditing) => { if ((!allowWhileEditing && isEditing) || !canEditGeometry) return; e.stopPropagation(); e.preventDefault(); setDrag(true); onSel();
    const p = e.touches ? e.touches[0] : e; ds.current = { x: p.clientX, y: p.clientY, fx: it.x, fy: it.y, moved: false }; };
  useEffect(() => { if (!dragging) return;
    const mv = e => { e.preventDefault(); const p = e.touches ? e.touches[0] : e; const dx = p.clientX - ds.current.x; const dy = p.clientY - ds.current.y; if (Math.hypot(dx, dy) > 3) { ds.current.moved = true; window.__nspdfSuppressCanvasClickUntil = Date.now() + 300; } onUp({ ...it, x: ds.current.fx + dx / scale, y: ds.current.fy + dy / scale }); };
    const up = () => { if (ds.current.moved) window.__nspdfSuppressCanvasClickUntil = Date.now() + 300; setDrag(false); };
    window.addEventListener("mousemove", mv); window.addEventListener("mouseup", up); window.addEventListener("touchmove", mv, { passive: false }); window.addEventListener("touchend", up);
    return () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); window.removeEventListener("touchmove", mv); window.removeEventListener("touchend", up); };
  }, [dragging]);
  // Resize
  const startResize = e => { e.stopPropagation(); e.preventDefault(); setResize(true); onSel();
    const p = e.touches ? e.touches[0] : e; rs.current = { x: p.clientX, y: p.clientY, fw: it.width, fh: it.height }; };
  useEffect(() => { if (!resizing) return;
    const mv = e => { e.preventDefault(); const p = e.touches ? e.touches[0] : e; onUp({ ...it, width: Math.max(10, rs.current.fw + (p.clientX - rs.current.x) / scale), height: Math.max(3, rs.current.fh + (p.clientY - rs.current.y) / scale) }); };
    const up = () => setResize(false);
    window.addEventListener("mousemove", mv); window.addEventListener("mouseup", up); window.addEventListener("touchmove", mv, { passive: false }); window.addEventListener("touchend", up);
    return () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); window.removeEventListener("touchmove", mv); window.removeEventListener("touchend", up); };
  }, [resizing]);

  const deleteButton = canEditGeometry && sel ? (
    <button onClick={e => { e.stopPropagation(); onDel(); }}
      onMouseEnter={e => { e.currentTarget.style.background = "#E85D3A"; e.currentTarget.style.opacity = "1"; }}
      onMouseLeave={e => { e.currentTarget.style.background = "#888"; e.currentTarget.style.opacity = "0.75"; }}
      style={{ position: "absolute", top: "-7px", right: "-7px", width: "14px", height: "14px", borderRadius: "50%", background: "#888", color: "white", border: "none", fontSize: "10px", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 60, opacity: 0.75, transition: "all 0.15s", boxShadow: "0 1px 3px rgba(0,0,0,0.15)", lineHeight: 1, padding: 0 }}>×</button>
  ) : null;
  const resizeHandle = canEditGeometry && sel ? (
    <div onMouseDown={startResize} onTouchStart={startResize}
      onMouseEnter={e => e.currentTarget.style.opacity = "1"}
      onMouseLeave={e => e.currentTarget.style.opacity = "0.55"}
      style={{ position: "absolute", bottom: "-6px", right: "-6px", width: "12px", height: "12px", background: "#2D7FF9", borderRadius: "3px", cursor: "nwse-resize", zIndex: 55, display: "flex", alignItems: "center", justifyContent: "center", opacity: 0.55, transition: "opacity 0.15s", boxShadow: "0 1px 3px rgba(0,0,0,0.15)" }}><svg width="7" height="7" viewBox="0 0 14 14"><line x1="10" y1="4" x2="4" y2="10" stroke="white" strokeWidth="1.5"/><line x1="10" y1="8" x2="8" y2="10" stroke="white" strokeWidth="1.5"/></svg></div>
  ) : null;
  const moveHandle = canEditGeometry && sel && ((isManualText && isEditing) || isDetectedFlat) ? (
    <div onMouseDown={e => startDrag(e, true)} onTouchStart={e => startDrag(e, true)} title="Move field"
      style={{ position: "absolute", top: "-11px", left: "-1px", height: "10px", minWidth: "34px", padding: "0 5px", borderRadius: "999px", background: "#2D7FF9", color: "white", fontSize: "7px", fontWeight: 800, letterSpacing: "0.4px", lineHeight: "10px", cursor: "move", zIndex: 56, boxShadow: "0 1px 3px rgba(0,0,0,0.15)", userSelect: "none" }}>MOVE</div>
  ) : null;

  if (it.type === "radio") return (
    <div data-item
      onMouseDown={canEditGeometry ? startDrag : undefined}
      onTouchStart={canEditGeometry ? startDrag : undefined}
      onClick={e => {
        e.stopPropagation();
        if (window.__nspdfSuppressCanvasClickUntil && Date.now() < window.__nspdfSuppressCanvasClickUntil) return;
        if (canEditGeometry) {
          onSel();
          return;
        }
        onClearSel?.();
        if (it.acroButtonGroupId) {
          setItems(prev => applyButtonToggle(prev, it, !it.checked));
        } else if (it.groupId) {
          setItems(prev => prev.map(other => other.type === "radio" && other.groupId === it.groupId ? { ...other, checked: other.id === it.id } : other));
        } else {
          onUp({ ...it, checked: !it.checked });
        }
      }}
      style={{ position: "absolute", left: it.x*scale, top: it.y*scale, width: it.width*scale, height: it.height*scale, cursor: canEditGeometry ? "move" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", border: isSystemField ? systemChoiceBorder : (isEditMode ? (sel ? "2px solid #2D7FF9" : "1px solid rgba(255,106,0,0.45)") : "1px solid rgba(255,106,0,0.55)"), borderRadius: "50%", background: isSystemField ? systemChoiceBackground : (isEditMode ? "rgba(255,106,0,0.08)" : (it.checked ? "rgba(255,106,0,0.10)" : "rgba(255,245,235,0.06)")), zIndex: canEditGeometry && sel ? 35 : 30, pointerEvents: "auto", overflow: "visible" }}>
      {it.checked && <span style={{ width: `${Math.max(3, Math.min(it.width,it.height)*scale*0.6)}px`, height: `${Math.max(3, Math.min(it.width,it.height)*scale*0.6)}px`, borderRadius: "50%", background: isSystemField ? "#007AFF" : "#FF6A00", display: "block" }} />}
      {deleteButton}
      {resizeHandle}
    </div>
  );
  if (it.type === "checkbox") {
    const renderAsRadioCircle = it.acroVisualStyle === "radio-circle";
    return (
    <div data-item
      {...(it.source === "acroform" && it.acroButtonType === "checkbox" ? { role: "checkbox", "aria-checked": Boolean(it.checked), "aria-label": it.label || it.acroFieldName || "Checkbox", "data-field-type": "checkbox", "data-acro-field-name": it.source === "acroform" && it.acroButtonType === "checkbox" ? (it.acroFieldName || "") : undefined, "data-acro-on-value": it.source === "acroform" && it.acroButtonType === "checkbox" ? String(it.acroOnValue || "").replace(/^\//, "") : undefined } : {})}
      onMouseDown={canEditGeometry ? startDrag : undefined}
      onTouchStart={canEditGeometry ? startDrag : undefined}
      onClick={e => {
        e.stopPropagation();
        if (window.__nspdfSuppressCanvasClickUntil && Date.now() < window.__nspdfSuppressCanvasClickUntil) return;
        if (canEditGeometry) {
          onSel();
          return;
        }
        onClearSel?.();
        if (it.acroButtonGroupId) {
          setItems(prev => applyButtonToggle(prev, it, !it.checked));
        } else {
          onUp({ ...it, checked: !it.checked });
        }
      }}
      style={{ position: "absolute", left: it.x*scale, top: it.y*scale, width: it.width*scale, height: it.height*scale, cursor: canEditGeometry ? "move" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", border: isSystemField ? systemChoiceBorder : (isEditMode ? (sel ? "2px solid #2D7FF9" : "1px solid rgba(45,127,249,0.35)") : "1px solid rgba(45,127,249,0.45)"), borderRadius: renderAsRadioCircle ? "50%" : "2px", background: isSystemField ? systemChoiceBackground : (isEditMode ? "rgba(45,127,249,0.08)" : (it.checked ? "rgba(45,127,249,0.10)" : "rgba(200,220,255,0.06)")), zIndex: canEditGeometry && sel ? 20 : 10, overflow: "visible" }}>
      {it.checked && (renderAsRadioCircle
        ? <span style={{ width: `${Math.max(3, Math.min(it.width,it.height)*scale*0.6)}px`, height: `${Math.max(3, Math.min(it.width,it.height)*scale*0.6)}px`, borderRadius: "50%", background: isSystemField ? "#007AFF" : "#1A1A1A", display: "block" }} />
        : <span style={{ fontSize: `${Math.min(it.width,it.height)*scale*0.7}px`, color: isSystemField ? "#007AFF" : "#1A1A1A", fontWeight: 700, lineHeight: 1 }}>✓</span>)}
      {deleteButton}
      {resizeHandle}
    </div>
  );
  }
  if (it.type === "check") return (
    <div data-item onMouseDown={canEditGeometry ? startDrag : undefined} onTouchStart={canEditGeometry ? startDrag : undefined}
      onClick={e => { e.stopPropagation(); if (canEditGeometry) onSel(); }}
      style={{ position: "absolute", left: it.x*scale, top: it.y*scale, transform: "translateY(-100%)", zIndex: canEditGeometry && sel ? 20 : 10, cursor: canEditGeometry ? "move" : "default" }}>
      <div style={{ fontSize: `${14*scale}px`, fontWeight: 700, color: "#1A1A1A", background: canEditGeometry && sel ? "rgba(232,93,58,0.08)" : "transparent", border: canEditGeometry && sel ? "1px dashed #E85D3A" : "none", borderRadius: "2px", padding: "0 3px", position: "relative" }}>✓
        {deleteButton}
      </div>
    </div>
  );
  if (it.type === "highlight") return (
    <div data-item onMouseDown={canEditGeometry ? startDrag : undefined} onTouchStart={canEditGeometry ? startDrag : undefined} onClick={e => { e.stopPropagation(); if (canEditGeometry) onSel(); }}
      style={{ position: "absolute", left: it.x*scale, top: it.y*scale, width: (it.width||140)*scale, height: (it.height||16)*scale, background: "rgba(255,230,0,0.35)", borderRadius: "2px", cursor: canEditGeometry ? "move" : "default", zIndex: canEditGeometry && sel ? 20 : 5, border: canEditGeometry && sel ? "1px dashed #DAA520" : "none" }}>
      {deleteButton}
      {resizeHandle}
    </div>
  );
  if (it.type === "note") return (
    <div data-item onMouseDown={canEditGeometry ? startDrag : undefined} onTouchStart={canEditGeometry ? startDrag : undefined} onClick={e => { e.stopPropagation(); if (canEditGeometry) onSel(); }}
      style={{ position: "absolute", left: it.x*scale, top: it.y*scale, background: "#FFF8DC", border: canEditGeometry && sel ? "2px solid #DAA520" : "1px solid #F0E68C", borderRadius: "4px", padding: "3px 6px", fontSize: `${9*scale}px`, color: "#333", maxWidth: `${160*scale}px`, boxShadow: "0 1px 4px rgba(0,0,0,0.1)", zIndex: canEditGeometry && sel ? 20 : 10, cursor: canEditGeometry ? "move" : "default" }}>
      📝 {it.text}
      {deleteButton}
    </div>
  );
  if (it.type === "signatureTarget") return (
    <div data-item
      data-field-type="signatureTarget"
      aria-label={it.label || "Signature field"}
      onMouseDown={canEditGeometry ? startDrag : undefined}
      onTouchStart={canEditGeometry ? startDrag : undefined}
      onClick={e => { e.stopPropagation(); if (canEditGeometry) onSel(); }}
      style={{ position: "absolute", left: it.x*scale, top: it.y*scale, width: it.width*scale, height: it.height*scale, border: canEditGeometry && sel ? "2px solid #4F46E5" : "1px dashed rgba(79,70,229,0.55)", borderRadius: "4px", background: "rgba(79,70,229,0.06)", color: "#4F46E5", fontSize: `${Math.max(9, Math.min(12, it.height*scale*0.55))}px`, fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "center", letterSpacing: "0.2px", cursor: canEditGeometry ? "move" : "default", zIndex: canEditGeometry && sel ? 25 : 12, overflow: "hidden", pointerEvents: "auto" }}>
      <span style={{ whiteSpace: "nowrap", opacity: 0.85 }}>Signature</span>
      {deleteButton}
      {resizeHandle}
    </div>
  );
  if (it.type === "image") return (
    <div data-item onMouseDown={canEditGeometry ? startDrag : undefined} onTouchStart={canEditGeometry ? startDrag : undefined} onClick={e => { e.stopPropagation(); if (canEditGeometry) onSel(); }}
      style={{ position: "absolute", left: it.x*scale, top: it.y*scale, width: it.width*scale, height: it.height*scale, border: canEditGeometry && sel ? "2px solid #4F46E5" : "1px dashed rgba(79,70,229,0.25)", borderRadius: "4px", cursor: canEditGeometry ? "move" : "default", zIndex: canEditGeometry && sel ? 20 : 10, overflow: "hidden" }}>
      <img src={it.src} style={{ width: "100%", height: "100%", objectFit: "contain", pointerEvents: "none" }} />
      {deleteButton}
      {resizeHandle}
    </div>
  );
  if (isChoiceField) {
    const isAcroChoice = it.source === "acroform";
    const options = Array.isArray(it.acroChoiceOptions) ? it.acroChoiceOptions : [];
    if (options.length > 0) return (
      <div data-item
        {...(isAcroChoice ? { "data-field-type": "choice" } : {})}
        onMouseDown={canEditGeometry ? startDrag : undefined}
        onTouchStart={canEditGeometry ? startDrag : undefined}
        onClick={e => { e.stopPropagation(); if (canEditGeometry) onSel(); }}
        style={{ position: "absolute", left: it.x * scale, top: it.y * scale, width: it.width * scale, height: visualHeight, border: canEditGeometry && sel ? "1.5px solid #2D7FF9" : "1px solid rgba(0,122,255,0.16)", borderRadius: "2px", background: canEditGeometry && sel ? "rgba(0,122,255,0.08)" : "rgba(0,122,255,0.05)", zIndex: canEditGeometry && sel ? 20 : 10, boxSizing: "border-box", overflow: "visible" }}>
        <select
          {...(isAcroChoice ? { "aria-label": it.label || it.acroFieldName || "Dropdown" } : {})}
          value={it.text || ""}
          onChange={e => { e.stopPropagation(); onUp({ ...it, text: e.target.value }); onSel(); }}
          onClick={e => { e.stopPropagation(); onSel(); }}
          onMouseDown={e => e.stopPropagation()}
          onTouchStart={e => e.stopPropagation()}
          disabled={isEditMode}
          style={{ width: "100%", height: "100%", border: "none", background: "transparent", outline: "none", fontSize: `${appearanceFontSize * scale}px`, color: resolvedAppearance?.cssColor || "#1A1A1A", padding: `0 ${Math.max(2, 2 * scale)}px`, fontFamily: resolvedAppearance?.cssFamily || "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", fontWeight: resolvedAppearance?.cssWeight || "normal", cursor: isEditMode ? "default" : "pointer", WebkitAppearance: "menulist", appearance: "menulist", boxSizing: "border-box", userSelect: "none", WebkitUserSelect: "none", MozUserSelect: "none", msUserSelect: "none" }}>
          <option value=""></option>
          {options.map((opt, i) => (
            <option key={`${opt.value}-${i}`} value={opt.value}>{opt.label}</option>
          ))}
        </select>
        {deleteButton}
        {resizeHandle}
      </div>
    );
  }
  if (shouldRenderCombInput) return (
    <div data-item ref={combRef}
      aria-label={it.label || it.acroFieldName || "Comb input"}
      onMouseDown={canEditGeometry ? startDrag : undefined}
      onTouchStart={canEditGeometry ? startDrag : undefined}
      onClick={e => { e.stopPropagation(); if (canEditGeometry || !isEditMode) onSel(); focusCombAtPoint(e); if (!isEditMode) setEditingId(it.id); }}
      onFocus={() => setCombHasFocus(true)}
      onBlur={() => setCombHasFocus(false)}
      style={{ display: "flex", position: "absolute", left: it.x * scale, top: it.y * scale, width: it.width * scale, height: visualHeight, border: "1px solid #C8C8CD", borderRadius: "2px", background: "rgba(255,255,255,0.95)", overflow: "hidden", boxSizing: "border-box", zIndex: canEditGeometry && sel ? 18 : 10 }}>
      <input ref={inputRef}
        aria-label={`${it.label || it.acroFieldName || "Comb input"} entry`}
        value=""
        onChange={() => {}}
        onKeyDown={handleKeyDown}
        onPaste={handlePaste}
        onClick={e => { e.stopPropagation(); if (canEditGeometry || !isEditMode) onSel(); focusCombAtPoint(e); if (!isEditMode) setEditingId(it.id); }}
        onMouseDown={e => e.stopPropagation()}
        onTouchStart={e => e.stopPropagation()}
        style={{ position: "absolute", inset: 0, width: "100%", height: "100%", opacity: 0, border: "none", background: "transparent", color: "transparent", caretColor: "transparent", outline: "none", padding: 0, margin: 0, zIndex: 1, cursor: isEditMode ? "default" : "text", pointerEvents: isEditMode || !combHasFocus ? "none" : "auto" }}
      />
      {chars.map((ch, idx) => (
        <div key={idx}
          onClick={e => { e.stopPropagation(); onSel(); focusCell(idx); if (!isEditMode) setEditingId(it.id); }}
          style={{ flex: 1, height: "100%", borderRight: idx === maxLen - 1 ? "none" : "1px solid #E5E5EA", display: "flex", alignItems: "center", justifyContent: "center", fontSize: `${appearanceFontSize * scale}px`, fontFamily: resolvedAppearance?.cssFamily || "-apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif", fontWeight: resolvedAppearance?.cssWeight || "normal", color: resolvedAppearance?.cssColor || "#1A1A1A", cursor: "text", userSelect: "none", boxSizing: "border-box", background: combHasFocus && idx === focusedCellIndex ? "rgba(0,122,255,0.08)" : ch ? "transparent" : "rgba(0,122,255,0.05)", outline: combHasFocus && idx === focusedCellIndex ? "2px solid #2D7FF9" : "none", outlineOffset: "-2px" }}>
          {ch}
        </div>
      ))}
      {deleteButton}
      {resizeHandle}
    </div>
  );
  // TEXT
  return (
    <div data-item
      onMouseDown={canEditGeometry && !isEditing && !isManualText ? startDrag : undefined}
      onTouchStart={canEditGeometry && !isEditing && !isManualText ? startDrag : undefined}
      onClick={activateText}
      onDoubleClick={e => { e.stopPropagation(); if (isManualText && onStartTyping) onStartTyping(); else if (!isEditMode) setEditingId(it.id); }}
      onMouseEnter={isSystemField ? () => setHovered(true) : undefined}
      onMouseLeave={isSystemField ? () => setHovered(false) : undefined}
      style={(() => {
        return {
          position: "absolute",
          left: it.x * scale,
          top: visualTop,
          width: it.width * scale,
          height: visualHeight,
          overflow: "visible",
          border: isSystemField && !isEditMode
            ? "none"
            : isUnderlineOnly && !sel
            ? "none"
            : (isIosFill || isEdenFill) && !(canEditGeometry && sel)
            ? "none"
            : canEditGeometry && sel
            ? "1.5px solid #2D7FF9"
            : canEditGeometry
            ? "1px solid rgba(45,127,249,0.30)"
            : it.auto && !it.text
              ? "1px solid rgba(0,122,255,0.16)"
              : "1px solid rgba(0,122,255,0.08)",
          borderRadius: (isIosFill || isEdenFill) ? "2px" : "1px",
          background: isSystemField && !isEditMode
            ? (hovered ? "rgba(0,122,255,0.09)" : (it.text ? "transparent" : "rgba(0,122,255,0.06)"))
            : isUnderlineOnly && !sel
            ? "transparent"
            : isEdenFill && !sel
            ? "rgba(177, 214, 255, 0.72)"
            : isIosFill && !sel
            ? "rgba(177, 214, 255, 0.62)"
            : canEditGeometry && sel
            ? "rgba(0,122,255,0.08)"
            : canEditGeometry
            ? "rgba(0,122,255,0.06)"
            : it.auto && !it.text
              ? "rgba(0,122,255,0.05)"
              : "transparent",
          cursor: "text",
          zIndex: canEditGeometry && sel ? 18 : 10,
          boxShadow: "none",
          boxSizing: "border-box"
        };
      })()}>
      {shouldRenderInput ? (
        <input ref={inputRef} type="text" inputMode={it.acroIsComb || isDateTextInput ? "numeric" : "text"} enterKeyHint="done" autoFocus
          value={it.text} onChange={e => updateTextInputValue(e.target.value, true)}
          onFocus={() => { onSel(); if (!isEditMode) setEditingId(it.id); }}
          onBlur={() => { if (isEditMode) setTimeout(() => { setEditingId(null); if (onStopTyping) onStopTyping(); }, 150); }}
          onKeyDown={handleTextInputKeyDown}
          onPaste={handleTextInputPaste}
          onClick={e => e.stopPropagation()} onMouseDown={e => e.stopPropagation()} onTouchStart={e => e.stopPropagation()}
          placeholder={isDateTextInput ? "MM/DD/YYYY" : (it.source === "acroform" && it.type === "text" ? "Type here" : (it.label || "Type here"))}
          maxLength={isDateTextInput ? 10 : (it.maxLength || it.acroMaxLen || undefined)}
          style={{ position: "absolute", left: -1, top: -1, width: "calc(100% + 2px)", height: "calc(100% + 2px)", border: isSystemField ? "none" : "2px solid #2D7FF9", background: isSystemField ? "rgba(0,122,255,0.08)" : "rgba(255,255,255,0.95)", outline: "none", fontSize: `${appearanceFontSize*scale}px`, color: resolvedAppearance?.cssColor || "#1A1A1A", padding: isCharBox ? "0 1px" : `${Math.max(0, textLayout.paddingY) * scale}px ${Math.max(1, textLayout.paddingX) * scale}px`, textAlign: isCharBox ? "center" : "left", boxSizing: "border-box", WebkitAppearance: "none", fontFamily: resolvedAppearance?.cssFamily || "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", fontWeight: resolvedAppearance?.cssWeight || "normal", borderRadius: "2px", zIndex: 50, overflow: "hidden", textOverflow: "clip", whiteSpace: "nowrap", userSelect: "text", WebkitUserSelect: "text", MozUserSelect: "text", msUserSelect: "text" }}
        />
      ) : (
        <div style={{
          position: "absolute",
          left:0,
          top:0,
          width: "100%",
          height: "100%",
          fontSize: `${appearanceFontSize*scale}px`,
          color: resolvedAppearance?.cssColor || "#1A1A1A",
          fontFamily: resolvedAppearance?.cssFamily || "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
          fontWeight: resolvedAppearance?.cssWeight || "normal",
          padding: isUnderlineOnly ? (isCharBox ? "0 1px 1px 1px" : `0 ${Math.max(1, textLayout.paddingX) * scale}px 1px ${Math.max(1, textLayout.paddingX) * scale}px`) : (isCharBox ? "0 1px" : `${Math.max(0, textLayout.paddingY) * scale}px ${Math.max(1, textLayout.paddingX) * scale}px`),
          lineHeight: isUnderlineOnly ? "1.05" : `${textLayout.lineHeight * scale}px`,
          overflow: "hidden",
          whiteSpace: "pre",
          userSelect: "none",
          boxSizing: "border-box",
          textAlign: isCharBox ? "center" : "left",
          display: isUnderlineOnly ? "flex" : "block",
          alignItems: isUnderlineOnly ? "flex-end" : "initial"
        }}>
          {(textLayout.lines && textLayout.lines.length ? textLayout.lines : [it.text || ""]).map(function(line, idx) {
            return <div key={idx}>{line || "\u00A0"}</div>;
          })}
        </div>
      )}
      {isUnderlineOnly && !isEditing && (
        <div style={{
          position: "absolute",
          left: 0,
          right: 0,
          bottom: underlineBottomInset,
          borderBottom: canEditGeometry && sel ? "2px solid #2D7FF9" : "1.5px solid rgba(45,127,249,0.45)",
          pointerEvents: "none"
        }} />
      )}
      {canEditGeometry && sel && (
        <>
          {deleteButton}
          {moveHandle}
          {resizeHandle}
        </>
      )}
    </div>
  );
}

// ─── TOOL DEFS ───
const TOOLS = [
  { id: "edit", label: "Edit PDF", icon: "✏️", desc: "Fill, sign, annotate & add images — all in one", accent: "#E85D3A", accepts: ".pdf", multi: false, cat: "Edit" },
  { id: "merge", label: "Merge", icon: "📎", desc: "Combine PDFs into one", accent: "#2D7FF9", accepts: ".pdf", multi: true, cat: "Organize" },
  { id: "split", label: "Split", icon: "✂️", desc: "Extract specific pages", accent: "#E84393", accepts: ".pdf", multi: false, cat: "Organize" },
  { id: "rotate", label: "Rotate", icon: "🔄", desc: "Rotate all pages", accent: "#14B8A6", accepts: ".pdf", multi: false, cat: "Organize" },
  { id: "reorder", label: "Reorder", icon: "📑", desc: "Rearrange page order", accent: "#F97316", accepts: ".pdf", multi: false, cat: "Organize" },
  { id: "pagenums", label: "Page #s", icon: "🔢", desc: "Add page numbers", accent: "#6366F1", accepts: ".pdf", multi: false, cat: "Organize" },
  { id: "compress", label: "Compress", icon: "📦", desc: "Optimize metadata & structure", accent: "#18A558", accepts: ".pdf", multi: false, cat: "Optimize" },
  { id: "convert", label: "Convert", icon: "↔️", desc: "Image → PDF", accent: "#7C5CFC", accepts: ".png,.jpg,.jpeg", multi: true, cat: "Optimize" },
  { id: "pdftoimg", label: "PDF→Image", icon: "🖼️", desc: "Export pages as PNG", accent: "#0891B2", accepts: ".pdf", multi: false, cat: "Optimize" },
  { id: "watermark", label: "Watermark", icon: "💧", desc: "Add text watermark", accent: "#64748B", accepts: ".pdf", multi: false, cat: "Optimize" },
  { id: "unlock", label: "Try unlock", icon: "🔓", desc: "Create an unlocked copy where supported", accent: "#DC2626", accepts: ".pdf", multi: false, cat: "Optimize" },
  { id: "password-protect-coming-soon", label: "Password protect", icon: "⏳", desc: "Add a password to your PDF. Real client-side encryption is coming soon.", badge: "Coming soon", disabled: true, accent: "#94A3B8", accepts: ".pdf", multi: false, cat: "Optimize" },
];

const EDIT_MODES = [
  { id: "text", label: "✏️ Text", desc: "Add text anywhere" },
  { id: "check", label: "✓ Check", desc: "Place checkmarks" },
  { id: "sign", label: "🖊️ Sign", desc: "Draw or type signature" },
  { id: "image", label: "🖼️ Image", desc: "Insert photos & logos" },
  { id: "highlight", label: "🖍️ Highlight", desc: "Highlight areas" },
  { id: "note", label: "📝 Note", desc: "Add sticky notes" },
];

const PROMISES = [
  { icon: "✂️", title: "No strings", sub: "No sign-up required" },
  { icon: "🚫", title: "No surprises", sub: "No hidden fees" },
  { icon: "🔒", title: "No storage", sub: "Advanced detection is transient" },
  { icon: "♾️", title: "No watermarks", sub: "Core tools stay clean" },
];

function Lg({ s = 24 }) { return <svg width={s} height={s} viewBox="0 0 32 32" fill="none"><circle cx="10" cy="8" r="4" stroke="#E85D3A" strokeWidth="2.5" fill="none"/><circle cx="10" cy="24" r="4" stroke="#E85D3A" strokeWidth="2.5" fill="none"/><line x1="13" y1="10" x2="28" y2="22" stroke="#E85D3A" strokeWidth="2.5" strokeLinecap="round"/><line x1="13" y1="22" x2="28" y2="10" stroke="#E85D3A" strokeWidth="2.5" strokeLinecap="round"/></svg>; }

function Drop({ tool, onFiles }) {
  const ref = useRef(null);
  return (
    <div onClick={() => ref.current?.click()} onDragOver={e => e.preventDefault()} onDrop={e => { e.preventDefault(); onFiles(Array.from(e.dataTransfer.files)); }}
      style={{ border: "2px dashed #D4D4D4", borderRadius: "20px", padding: "44px 24px", textAlign: "center", cursor: "pointer", background: "#FAFAFA" }}
      onMouseOver={e => e.currentTarget.style.borderColor = tool.accent} onMouseOut={e => e.currentTarget.style.borderColor = "#D4D4D4"}>
      <input ref={ref} type="file" accept={tool.accepts} multiple={tool.multi} onChange={e => onFiles(Array.from(e.target.files))} style={{ display: "none" }} />
      <div style={{ fontSize: "36px", marginBottom: "10px" }}>{tool.icon}</div>
      <div style={{ fontSize: "15px", fontWeight: 700, color: "#1A1A1A", marginBottom: "4px" }}>Drop file{tool.multi ? "s" : ""} here</div>
      <div style={{ fontSize: "13px", color: "#AAA" }}>or tap to browse</div>
    </div>
  );
}

// ─── INLINE TEXT INPUT (iOS fix) ───
// This renders a real input at the annotation position so iOS Safari triggers the keyboard
function InlineInput({ value, onChange, onDone, onDelete, fontSize, scale, fontCss }) {
  const ref = useRef(null);
  useEffect(() => {
    // Small delay helps iOS Safari focus correctly
    const t = setTimeout(() => { if (ref.current) { ref.current.focus(); ref.current.click(); } }, 100);
    return () => clearTimeout(t);
  }, []);
  return (
    <input
      ref={ref}
      type="text"
      inputMode="text"
      enterKeyHint="done"
      autoFocus
      value={value}
      onChange={e => onChange(e.target.value)}
      onBlur={() => { if (value.trim()) onDone(); else onDelete(); }}
      onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); if (value.trim()) onDone(); else onDelete(); } }}
      placeholder="Type here..."
      onClick={e => e.stopPropagation()}
      onMouseDown={e => e.stopPropagation()}
      onTouchStart={e => e.stopPropagation()}
      style={{
        border: "none", borderBottom: "2px solid #E85D3A",
        background: "rgba(232,93,58,0.06)", outline: "none",
        fontSize: `${(fontSize || 12) * scale}px`,
        fontFamily: fontCss || "'Open Sans', sans-serif",
        color: "#1A1A1A", padding: "4px 6px", minWidth: "120px",
        borderRadius: "3px", boxSizing: "border-box",
        WebkitAppearance: "none",
      }}
    />
  );
}

// ─── SIGNATURE PAD ───
function SignPad({ onSave, onCancel }) {
  const [mode, setMode] = useState("draw");
  const [name, setName] = useState(""); const [font, setFont] = useState("caveat");
  const [uploadSrc, setUploadSrc] = useState(null);
  const [strokeVersion, setStrokeVersion] = useState(0);
  const cRef = useRef(null);
  const strokesRef = useRef([]);
  const drawing = useRef(false);
  const padSizeRef = useRef({ width: 400, height: 140 });

  const renderStrokeModel = useCallback((canvas, strokes, opts = {}) => {
    if (!canvas) return;
    const cssWidth = opts.cssWidth || padSizeRef.current.width || 400;
    const cssHeight = opts.cssHeight || padSizeRef.current.height || 140;
    const dpr = opts.dpr || 1;
    const targetWidth = opts.targetWidth || Math.max(1, Math.round(cssWidth * dpr));
    const targetHeight = opts.targetHeight || Math.max(1, Math.round(cssHeight * dpr));
    if (canvas.width !== targetWidth) canvas.width = targetWidth;
    if (canvas.height !== targetHeight) canvas.height = targetHeight;
    const ctx = canvas.getContext("2d");
    ctx.setTransform(targetWidth / cssWidth, 0, 0, targetHeight / cssHeight, 0, 0);
    ctx.clearRect(0, 0, cssWidth, cssHeight);
    ctx.strokeStyle = "#1A1A1A";
    ctx.lineWidth = 2.6;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    for (const stroke of strokes) {
      const points = stroke.points || [];
      if (!points.length) continue;
      ctx.beginPath();
      ctx.moveTo(points[0].x, points[0].y);
      if (points.length === 1) {
        ctx.lineTo(points[0].x + 0.01, points[0].y + 0.01);
      } else {
        for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
      }
      ctx.stroke();
    }
  }, []);

  const renderDisplayCanvas = useCallback(() => {
    const canvas = cRef.current;
    if (!canvas || mode !== "draw") return;
    const r = canvas.getBoundingClientRect();
    const cssWidth = Math.max(1, r.width || 400);
    const cssHeight = Math.max(1, r.height || 140);
    const dpr = Math.max(1, Math.min(4, window.devicePixelRatio || 1));
    padSizeRef.current = { width: cssWidth, height: cssHeight };
    renderStrokeModel(canvas, strokesRef.current, { cssWidth, cssHeight, dpr });
  }, [mode, renderStrokeModel]);

  useLayoutEffect(() => {
    if (mode !== "draw") return;
    renderDisplayCanvas();
    const canvas = cRef.current;
    if (!canvas || !window.ResizeObserver) return;
    const ro = new ResizeObserver(renderDisplayCanvas);
    ro.observe(canvas);
    return () => ro.disconnect();
  }, [mode, strokeVersion, renderDisplayCanvas]);

  const pos = e => {
    const r = cRef.current.getBoundingClientRect();
    const p = e.touches ? e.touches[0] : e;
    return { x: p.clientX - r.left, y: p.clientY - r.top };
  };
  const down = e => {
    e.preventDefault();
    const pt = pos(e);
    drawing.current = true;
    strokesRef.current = [...strokesRef.current, { points: [pt] }];
    setStrokeVersion(v => v + 1);
  };
  const move = e => {
    if (!drawing.current) return;
    e.preventDefault();
    const pt = pos(e);
    const strokes = strokesRef.current.slice();
    const lastStroke = strokes[strokes.length - 1];
    if (!lastStroke) return;
    strokes[strokes.length - 1] = { points: [...lastStroke.points, pt] };
    strokesRef.current = strokes;
    renderDisplayCanvas();
  };
  const up = () => { drawing.current = false; setStrokeVersion(v => v + 1); };

  const clearDrawnSignature = () => {
    strokesRef.current = [];
    drawing.current = false;
    setStrokeVersion(v => v + 1);
  };

  const exportDrawnSignature = () => {
    if (!strokesRef.current.length) return null;
    const c = document.createElement("canvas");
    const css = padSizeRef.current || { width: 400, height: 140 };
    renderStrokeModel(c, strokesRef.current, { cssWidth: css.width || 400, cssHeight: css.height || 140, targetWidth: 800, targetHeight: 280 });
    return c.toDataURL("image/png");
  };

  const save = () => {
    let d;
    if (mode === "draw") { d = exportDrawnSignature(); }
    else if (mode === "type") { const c = document.createElement("canvas"); c.width = 400; c.height = 100; const ctx = c.getContext("2d"); ctx.font = `36px ${SIGN_FONTS.find(f => f.id === font)?.css}`; ctx.fillStyle = "#1A1A1A"; ctx.fillText(name, 10, 60); d = c.toDataURL("image/png"); }
    else { d = uploadSrc; }
    if (d) {
      onSave(d);
      clearDrawnSignature();
      setName("");
      setUploadSrc(null);
    }
  };

  return (
    <div role="dialog" aria-modal="true" aria-label="Create your signature" style={{ position: "fixed", inset: 0, zIndex: 180, display: "flex", alignItems: "center", justifyContent: "center", padding: "18px", background: "rgba(26,26,26,0.18)", backdropFilter: "blur(5px)" }}>
      <div style={{ width: "min(560px, 96vw)", maxHeight: "min(620px, 92vh)", overflow: "auto", background: "white", borderRadius: "18px", border: "1px solid #E8E8E8", padding: "14px", boxShadow: "0 18px 60px rgba(0,0,0,0.18)" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "10px", marginBottom: "10px" }}>
          <div>
            <div style={{ fontSize: "15px", fontWeight: 800, color: "#1A1A1A" }}>Create your signature</div>
            <div style={{ fontSize: "10px", color: "#888", marginTop: "2px" }}>Draw, type, or upload. Your signature stays in this session unless saved to Pro.</div>
          </div>
          <button onClick={onCancel} aria-label="Cancel signature" style={{ width: "30px", height: "30px", borderRadius: "999px", border: "1px solid #EEE", background: "white", color: "#888", fontSize: "18px", fontWeight: 800, cursor: "pointer", lineHeight: 1 }}>×</button>
        </div>
        <div style={{ display: "flex", gap: "5px", marginBottom: "10px" }}>
          {[{ id: "draw", l: "✍️ Draw" }, { id: "type", l: "⌨️ Type" }, { id: "upload", l: "📷 Upload" }].map(m => (
            <button key={m.id} onClick={() => setMode(m.id)} style={{ flex: 1, padding: "7px", borderRadius: "8px", border: "none", fontSize: "11px", fontWeight: 700, background: mode === m.id ? "#4F46E5" : "#F0F0F0", color: mode === m.id ? "white" : "#888", cursor: "pointer" }}>{m.l}</button>
          ))}
        </div>
        {mode === "draw" && (
          <>
            <canvas ref={cRef}
              onMouseDown={down} onMouseMove={move} onMouseUp={up} onMouseLeave={up}
              onTouchStart={down} onTouchMove={move} onTouchEnd={up}
              style={{ width: "100%", height: "132px", border: "1px solid #DDD", borderRadius: "12px", cursor: "crosshair", touchAction: "none", background: "#FAFAFA", display: "block" }} />
            <button onClick={clearDrawnSignature} style={{ marginTop: "6px", background: "none", border: "none", fontSize: "11px", color: "#AAA", cursor: "pointer" }}>Clear</button>
          </>
        )}
        {mode === "type" && (
          <>
            <input value={name} onChange={e => setName(e.target.value)} placeholder="Your name..." inputMode="text"
              style={{ width: "100%", padding: "10px", border: "1px solid #EEE", borderRadius: "10px", fontSize: "22px", fontFamily: SIGN_FONTS.find(f => f.id === font)?.css, outline: "none", boxSizing: "border-box" }} />
            <div style={{ display: "flex", gap: "5px", marginTop: "8px", flexWrap: "wrap" }}>
              {SIGN_FONTS.map(f => <button key={f.id} onClick={() => setFont(f.id)} style={{ padding: "5px 10px", borderRadius: "7px", border: font === f.id ? "2px solid #4F46E5" : "1px solid #EEE", background: "white", fontSize: "13px", fontFamily: f.css, cursor: "pointer" }}>{f.name}</button>)}
            </div>
            {name && <div style={{ marginTop: "10px", padding: "12px", background: "#FAFAFA", borderRadius: "10px", textAlign: "center" }}><span style={{ fontSize: "28px", fontFamily: SIGN_FONTS.find(f => f.id === font)?.css }}>{name}</span></div>}
          </>
        )}
        {mode === "upload" && (
          !uploadSrc ? (
            <label style={{ display: "block", padding: "24px", border: "2px dashed #DDD", borderRadius: "12px", textAlign: "center", cursor: "pointer" }}>
              <input type="file" accept="image/*" onChange={async e => { const f = e.target.files[0]; if (f) setUploadSrc(await readDataURL(f)); }} style={{ display: "none" }} />
              <div style={{ fontSize: "24px", marginBottom: "6px" }}>📷</div>
              <div style={{ fontSize: "12px", color: "#AAA" }}>Upload signature image</div>
            </label>
          ) : (
            <div style={{ textAlign: "center" }}><img src={uploadSrc} style={{ maxWidth: "100%", maxHeight: "100px", borderRadius: "8px", border: "1px solid #EEE" }} /><br /><button onClick={() => setUploadSrc(null)} style={{ marginTop: "6px", background: "none", border: "none", fontSize: "11px", color: "#AAA", cursor: "pointer" }}>Change</button></div>
          )
        )}
        <div style={{ display: "flex", gap: "8px", marginTop: "12px" }}>
          <button onClick={onCancel} style={{ flex: 1, padding: "10px", background: "#F0F0F0", border: "none", borderRadius: "10px", fontSize: "13px", fontWeight: 600, color: "#888", cursor: "pointer" }}>Cancel</button>
          <button onClick={save} style={{ flex: 1, padding: "10px", background: "#4F46E5", border: "none", borderRadius: "10px", fontSize: "13px", fontWeight: 700, color: "white", cursor: "pointer" }}>Place Signature</button>
        </div>
      </div>
    </div>
  );
}

// ─── SPLIT VISUAL PLANNER ───
function SplitVisualPlanner({ file, splitPoints, onSplitPointsChange, onPageCount, processing, onProcess, result, onDownload }) {
  const [pages, setPages] = useState([]);
  const [loading, setLoading] = useState(false);
  const [renderedCount, setRenderedCount] = useState(0);
  const [thumbError, setThumbError] = useState("");
  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true); setThumbError(""); setRenderedCount(0); setPages([]);
      if (onPageCount) onPageCount(0);
      const bytes = await readBytes(file);
      const pdf = await window.pdfjsLib.getDocument({ data: new Uint8Array(bytes.buffer.slice(0)) }).promise;
      const initialPages = Array.from({ length: pdf.numPages }, (_, pageIndex) => ({ pageIndex, src: null, width: 0, height: 0 }));
      if (cancelled) return;
      setPages(initialPages);
      if (onPageCount) onPageCount(pdf.numPages);
      setLoading(false);
      for (let i = 0; i < pdf.numPages; i += 1) {
        if (cancelled) break;
        const pg = await pdf.getPage(i + 1);
        const baseVp = pg.getViewport({ scale: 1 });
        const thumbScale = Math.min(190 / baseVp.width, 245 / baseVp.height, 0.46);
        const vp = pg.getViewport({ scale: thumbScale });
        const c = document.createElement("canvas");
        c.width = Math.max(1, Math.round(vp.width));
        c.height = Math.max(1, Math.round(vp.height));
        await pg.render({ canvasContext: c.getContext("2d"), viewport: vp }).promise;
        const src = c.toDataURL("image/png");
        if (cancelled) break;
        setPages(prev => prev.map(p => p.pageIndex === i ? { pageIndex: i, src, width: Math.round(vp.width), height: Math.round(vp.height) } : p));
        setRenderedCount(i + 1);
      }
    })().catch(e => {
      console.error("Split thumbnail rendering failed", e);
      if (!cancelled) { setLoading(false); setThumbError("Could not prepare page thumbnails. Try Advanced ranges instead."); }
    });
    return () => { cancelled = true; };
  }, [file]);
  const pageCount = pages.length;
  const activePoints = new Set(splitPoints || []);
  const groups = pageCount ? splitGroupsFromSplitPoints(splitPoints, pageCount) : [];
  const toggleSplitPoint = pageNumber => {
    const next = activePoints.has(pageNumber)
      ? (splitPoints || []).filter(n => n !== pageNumber)
      : [...(splitPoints || []), pageNumber];
    onSplitPointsChange([...new Set(next)].sort((a, b) => a - b));
  };
  const rangeText = group => {
    const first = group.pages[0];
    const last = group.pages[group.pages.length - 1];
    return `${first}-${last}`;
  };
  const outputCount = groups.length;
  const plannedFileCount = activePoints.size ? outputCount : 0;
  const readyToSplit = pageCount > 0 && activePoints.size > 0 && !result;
  const downloadLabel = result?.out > 1 ? "↓ Download ZIP" : "↓ Download PDF";
  const planText = activePoints.size ? `${outputCount} file${outputCount !== 1 ? "s" : ""} will be created` : "Choose at least one split point.";
  return (
    <div data-split-visual-planner style={{ margin: "10px 0", padding: "12px", background: "#F8FAFC", border: "1px solid #E6EEF8", borderRadius: "13px" }}>
      <div style={{ display: "flex", alignItems: "flex-start", gap: "16px", flexWrap: "wrap" }}>
        <div data-split-planner-main style={{ flex: "1 1 880px", minWidth: "280px" }}>
          <div style={{ display: "flex", justifyContent: "space-between", gap: "10px", alignItems: "center", flexWrap: "wrap", marginBottom: "8px" }}>
            <div>
              <div data-split-page-count style={{ fontSize: "13px", fontWeight: 900, color: "#1A1A1A" }}>{pageCount ? `${pageCount} pages loaded` : "Preparing pages..."}</div>
              <div style={{ fontSize: "11px", color: "#667085", lineHeight: 1.45 }}>Choose where this PDF should split. Page 1 is the first page in this file.</div>
            </div>
            <div data-split-render-progress style={{ fontSize: "10px", color: "#94A3B8", fontWeight: 800 }}>{pageCount ? `${Math.min(renderedCount, pageCount)} / ${pageCount} thumbnails` : ""}</div>
          </div>
          <div style={{ fontSize: "11px", color: "#475467", marginBottom: "10px", fontWeight: 700 }}>Click between pages to create separate PDFs.</div>
          {thumbError && <div role="status" style={{ marginBottom: "10px", padding: "8px 10px", background: "#FFF7ED", border: "1px solid #FED7AA", borderRadius: "9px", color: "#C2410C", fontSize: "11px", fontWeight: 800 }}>{thumbError}</div>}
          <div data-split-thumbnail-grid data-thumbnail-scale-limit="0.46" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(192px, 1fr))", gap: "14px" }}>
            {pages.map((page, index) => {
              const physicalPage = page.pageIndex + 1;
              const canSplitAfter = index < pages.length - 1;
              const selected = activePoints.has(physicalPage);
              return (
                <div key={page.pageIndex} data-split-page-card data-physical-page={physicalPage} style={{ background: "white", border: selected ? "2px solid #2D7FF9" : "1px solid #E5E7EB", borderRadius: "12px", overflow: "hidden", boxShadow: selected ? "0 10px 24px rgba(45,127,249,0.16)" : "0 2px 9px rgba(15,23,42,0.06)" }}>
                  <div style={{ position: "relative", minHeight: "236px", display: "flex", alignItems: "center", justifyContent: "center", padding: "12px", background: "#F1F5F9" }}>
                    {page.src ? <img src={page.src} alt={`Page ${physicalPage} thumbnail`} draggable="false" style={{ width: page.width ? `${Math.round(page.width * 1.08)}px` : "100%", maxWidth: "210px", height: "auto", maxHeight: "250px", display: "block", background: "white", boxShadow: "0 2px 8px rgba(0,0,0,0.13)" }} /> : <div aria-label={`Rendering page ${physicalPage}`} style={{ width: "150px", height: "194px", borderRadius: "8px", background: "linear-gradient(135deg,#F8FAFC,#FFFFFF)", border: "1px solid #E5E7EB", display: "flex", alignItems: "center", justifyContent: "center", color: "#94A3B8", fontSize: "10px", fontWeight: 800 }}>Rendering...</div>}
                    <div style={{ position: "absolute", top: "7px", left: "7px", background: "rgba(15,23,42,0.72)", color: "white", borderRadius: "999px", padding: "2px 7px", fontSize: "10px", fontWeight: 900 }}>Page {physicalPage}</div>
                  </div>
                  {canSplitAfter && <button type="button" data-split-point data-page={physicalPage} aria-pressed={selected ? "true" : "false"} aria-label={selected ? `Remove split after page ${physicalPage}` : `Split after page ${physicalPage}`} title={selected ? `Remove split after page ${physicalPage}` : `Split after page ${physicalPage}`} onClick={() => toggleSplitPoint(physicalPage)} style={{ width: "100%", minHeight: "34px", border: "none", borderTop: "1px solid #E5E7EB", background: selected ? "#2D7FF9" : "white", color: selected ? "white" : "#2D7FF9", cursor: "pointer", fontSize: "11px", fontWeight: 900 }}>{selected ? `Split after page ${physicalPage}` : "+ Split here"}</button>}
                </div>
              );
            })}
            {loading && !pages.length && <div style={{ gridColumn: "1 / -1", textAlign: "center", padding: "28px", color: "#94A3B8", fontSize: "12px", fontWeight: 800 }}>Preparing page thumbnails...</div>}
          </div>
        </div>
        <div data-split-plan-rail style={{ flex: "0 0 320px", minWidth: "280px", position: "sticky", top: "64px", alignSelf: "flex-start", maxHeight: "calc(100vh - 84px)", overflowY: "auto", padding: "13px", background: "white", border: "1px solid #DCEBFF", borderRadius: "14px", boxShadow: "0 10px 28px rgba(15,23,42,0.08)" }}>
          <div style={{ fontSize: "10px", color: "#2D7FF9", fontWeight: 900, letterSpacing: "1px", marginBottom: "6px" }}>SPLIT PLAN</div>
          <div style={{ fontSize: "18px", color: "#1A1A1A", fontWeight: 900, marginBottom: "4px" }}>{result ? `${result.out} file${result.out !== 1 ? "s" : ""} ready` : planText}</div>
          <div data-split-plan-count style={{ fontSize: "11px", color: "#64748B", fontWeight: 800, marginBottom: "10px" }}>{pageCount ? `${pageCount} pages · ${plannedFileCount} output file${plannedFileCount === 1 ? "" : "s"}` : "Preparing pages..."}</div>
          <div data-split-output-summary aria-label="Split output summary" style={{ display: "grid", gap: "6px", marginBottom: "12px" }}>
            {pageCount > 0 && groups.map((group, index) => <div key={`${group.label}-${index}`} data-split-output-row style={{ padding: "7px 9px", border: "1px solid #EEF2F7", borderRadius: "9px", background: index % 2 ? "#FAFBFF" : "#F8FAFC", fontSize: "12px", color: "#334155", fontWeight: 800 }}>Output {index + 1}: pages {rangeText(group)}</div>)}
          </div>
          {!activePoints.size && !result && <div data-split-empty-plan style={{ marginBottom: "10px", padding: "8px 10px", background: "#FFF7ED", border: "1px solid #FED7AA", borderRadius: "9px", color: "#C2410C", fontSize: "11px", fontWeight: 800 }}>Choose at least one split point.</div>}
          {result ? (
            <button type="button" data-split-rail-download onClick={onDownload} style={{ width: "100%", minHeight: "42px", border: "none", borderRadius: "11px", background: "#E85D3A", color: "white", cursor: "pointer", fontSize: "13px", fontWeight: 900 }}>{downloadLabel}</button>
          ) : (
            <button type="button" data-split-rail-action onClick={onProcess} disabled={!readyToSplit || processing} style={{ width: "100%", minHeight: "42px", border: "none", borderRadius: "11px", background: readyToSplit ? "#E85D3A" : "#E5E7EB", color: readyToSplit ? "white" : "#94A3B8", cursor: readyToSplit && !processing ? "pointer" : "not-allowed", fontSize: "13px", fontWeight: 900 }}>{processing ? "Processing..." : "Split · free"}</button>
          )}
        </div>
      </div>
    </div>
  );
}

// ─── REORDER VIEW ───
function PageThumbnailGrid({ pages, order, activePageIndex, lastMovedPageIndex, onSelectPage, onMoveEarlier, onMoveLater, onMovePage }) {
  const [dragIndex, setDragIndex] = useState(null);
  const [dropIndex, setDropIndex] = useState(null);
  const orderedPages = (order || []).map(pageIndex => pages[pageIndex] || { pageIndex });
  const clearDragState = () => { setDragIndex(null); setDropIndex(null); };
  const handleDropAt = position => {
    if (dragIndex === null || dragIndex === undefined) return clearDragState();
    if (onMovePage) onMovePage(dragIndex, position);
    clearDragState();
  };
  return (
    <div data-page-thumbnail-grid onDragOver={e => e.preventDefault()} style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(176px, 1fr))", gap: "14px" }}>
      {orderedPages.map((page, position) => {
        const physicalPage = page.pageIndex + 1;
        const active = activePageIndex === page.pageIndex;
        const lastMoved = lastMovedPageIndex === page.pageIndex;
        const indicatorVisible = dragIndex !== null && dropIndex === position && dragIndex !== position;
        return (
          <div key={`${page.pageIndex}-${position}`} data-page-thumbnail data-draggable-page-thumbnail="true" data-last-moved={lastMoved ? "true" : "false"} data-physical-page={physicalPage} data-order-position={position + 1} aria-label={`Original PDF page ${physicalPage}, currently position ${position + 1}`} title={`Original PDF page ${physicalPage}, currently position ${position + 1}`} draggable="true"
            onClick={() => onSelectPage && onSelectPage(page.pageIndex)}
            onDragStart={e => { setDragIndex(position); setDropIndex(position); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(position)); }}
            onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDropIndex(position); }}
            onDrop={e => { e.preventDefault(); handleDropAt(position); }}
            onDragEnd={clearDragState}
            style={{ position: "relative", border: active || lastMoved ? "2px solid #F97316" : "1px solid #E8E8E8", borderRadius: "14px", background: lastMoved ? "#FFF7ED" : "white", overflow: "hidden", boxShadow: active || lastMoved ? "0 10px 26px rgba(249,115,22,0.18)" : "0 2px 10px rgba(0,0,0,0.04)", cursor: "grab", opacity: dragIndex === position ? 0.55 : 1 }}>
            <div data-drop-indicator data-visible={indicatorVisible ? "true" : "false"} style={{ position: "absolute", top: "6px", bottom: "6px", left: "-1px", width: "5px", borderRadius: "999px", background: "#F97316", boxShadow: "0 0 0 3px rgba(249,115,22,0.16)", opacity: indicatorVisible ? 1 : 0, transition: "opacity 0.12s", zIndex: 4, pointerEvents: "none" }} />
            <div style={{ position: "relative", background: "#F7F7F7", minHeight: "206px", display: "flex", alignItems: "center", justifyContent: "center", padding: "10px" }}>
              {page.src ? <img src={page.src} alt={`Page ${physicalPage} thumbnail`} draggable="false" style={{ width: page.width ? `${Math.round(page.width * 1.22)}px` : "100%", maxWidth: "190px", height: "auto", maxHeight: "230px", display: "block", boxShadow: "0 2px 8px rgba(0,0,0,0.12)", background: "white" }} /> : <div aria-label={`Rendering page ${physicalPage}`} style={{ width: "145px", height: "185px", borderRadius: "8px", background: "linear-gradient(135deg,#F1F5F9,#FFFFFF)", border: "1px solid #E5E7EB", display: "flex", alignItems: "center", justifyContent: "center", color: "#AAA", fontSize: "11px", fontWeight: 700 }}>Rendering...</div>}
              <div style={{ position: "absolute", top: "8px", left: "8px", background: "rgba(0,0,0,0.64)", color: "white", borderRadius: "999px", padding: "2px 7px", fontSize: "10px", fontWeight: 800 }}>#{position + 1}</div>
              {lastMoved && <div data-last-moved-chip style={{ position: "absolute", right: "8px", top: "8px", background: "#F97316", color: "white", borderRadius: "999px", padding: "2px 7px", fontSize: "9px", fontWeight: 900, letterSpacing: "0.02em" }}>Moved</div>}
            </div>
            <div style={{ padding: "8px 8px 9px", borderTop: "1px solid #F0F0F0" }}>
              <div style={{ textAlign: "center", color: "#333", fontSize: "11px", fontWeight: 800, marginBottom: "7px" }}>Page {physicalPage}</div>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "5px" }}>
                <button type="button" aria-label={`Move page ${physicalPage} earlier`} onClick={e => { e.stopPropagation(); onMoveEarlier(position); }} disabled={position === 0} style={{ minHeight: "30px", padding: "6px 5px", border: "1px solid #EEE", borderRadius: "7px", background: position === 0 ? "#F6F6F6" : "white", cursor: position === 0 ? "not-allowed" : "pointer", color: position === 0 ? "#BBB" : "#666", fontSize: "10px", fontWeight: 800 }}>Earlier</button>
                <button type="button" aria-label={`Move page ${physicalPage} later`} onClick={e => { e.stopPropagation(); onMoveLater(position); }} disabled={position === orderedPages.length - 1} style={{ minHeight: "30px", padding: "6px 5px", border: "1px solid #EEE", borderRadius: "7px", background: position === orderedPages.length - 1 ? "#F6F6F6" : "white", cursor: position === orderedPages.length - 1 ? "not-allowed" : "pointer", color: position === orderedPages.length - 1 ? "#BBB" : "#666", fontSize: "10px", fontWeight: 800 }}>Later</button>
              </div>
            </div>
          </div>
        );
      })}
      <div data-drop-tail-zone onDragOver={e => { e.preventDefault(); setDropIndex(orderedPages.length); }} onDrop={e => { e.preventDefault(); handleDropAt(orderedPages.length); }} style={{ gridColumn: "1 / -1", height: "10px", borderRadius: "999px", background: dragIndex !== null && dropIndex === orderedPages.length ? "#F97316" : "transparent", boxShadow: dragIndex !== null && dropIndex === orderedPages.length ? "0 0 0 3px rgba(249,115,22,0.16)" : "none" }} />
    </div>
  );
}

function ReorderView({ file, fileBytes }) {
  const [pages, setPages] = useState([]); const [order, setOrder] = useState([]);
  const [originalOrder, setOriginalOrder] = useState([]); const [orderHistory, setOrderHistory] = useState([]);
  const [lastMovedPageIndex, setLastMovedPageIndex] = useState(null);
  const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [completed, setCompleted] = useState(false); const [activePageIndex, setActivePageIndex] = useState(0);
  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true); setCompleted(false);
      const bytes = new Uint8Array(fileBytes.buffer.slice(0));
      const pdf = await window.pdfjsLib.getDocument({ data: bytes }).promise;
      const initialPages = Array.from({ length: pdf.numPages }, (_, pageIndex) => ({ pageIndex, src: null, width: 0, height: 0 }));
      if (cancelled) return;
      const initialOrder = initialPages.map(p => p.pageIndex);
      setPages(initialPages); setOrder(initialOrder); setOriginalOrder(initialOrder); setOrderHistory([]); setLastMovedPageIndex(null); setActivePageIndex(0); setLoading(false);
      for (let i = 0; i < pdf.numPages; i++) {
        if (cancelled) break;
        const pg = await pdf.getPage(i + 1);
        const baseVp = pg.getViewport({ scale: 1 });
        const thumbScale = Math.min(160 / baseVp.width, 200 / baseVp.height, 0.32);
        const vp = pg.getViewport({ scale: thumbScale });
        const c = document.createElement("canvas"); c.width = Math.max(1, Math.round(vp.width)); c.height = Math.max(1, Math.round(vp.height));
        await pg.render({ canvasContext: c.getContext("2d"), viewport: vp }).promise;
        const src = c.toDataURL("image/png");
        if (cancelled) break;
        setPages(prev => prev.map(p => p.pageIndex === i ? { pageIndex: i, src, width: Math.round(vp.width), height: Math.round(vp.height) } : p));
      }
    })().catch(e => { console.error("Thumbnail rendering failed", e); if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [fileBytes]);
  const ordersEqual = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
  const restoreOrder = nextOrder => {
    setOrder(nextOrder);
    setActivePageIndex(prev => nextOrder.includes(prev) ? prev : (nextOrder[0] || 0));
    setLastMovedPageIndex(null);
    setCompleted(false);
  };
  const movePageInOrder = (fromIndex, toIndex) => {
    if (fromIndex === toIndex || fromIndex < 0 || fromIndex >= order.length || toIndex < 0 || toIndex > order.length) return;
    const movedPage = order[fromIndex];
    const n = [...order];
    const moving = n.splice(fromIndex, 1)[0];
    const insertIndex = fromIndex < toIndex ? toIndex - 1 : toIndex;
    if (insertIndex === fromIndex) return;
    n.splice(insertIndex, 0, moving);
    setOrderHistory(prev => [...prev, order]);
    setOrder(n); setActivePageIndex(movedPage); setLastMovedPageIndex(movedPage); setCompleted(false);
  };
  const mv = (i, dir) => movePageInOrder(i, dir > 0 ? i + 2 : i - 1);
  const undoLastMove = () => {
    if (!orderHistory.length) return;
    const previousOrder = orderHistory[orderHistory.length - 1];
    setOrderHistory(prev => prev.slice(0, -1));
    restoreOrder(previousOrder);
  };
  const resetOrder = () => {
    if (ordersEqual(order, originalOrder)) return;
    setOrderHistory([]);
    restoreOrder(originalOrder);
  };
  const canUndo = orderHistory.length > 0;
  const canReset = !ordersEqual(order, originalOrder);
  const movedCount = order.filter((pageIndex, index) => pageIndex !== originalOrder[index]).length;
  const save = async () => { setSaving(true); setCompleted(false); const r = await reorderPDF(file, order); await downloadOrSharePdf(r.blob, "reordered_" + file.name); setCompleted(true); setSaving(false); };
  if (loading) return <div style={{ textAlign: "center", padding: "50px", color: "#AAA" }}>Preparing page thumbnails...</div>;
  return (
    <div>
      <div data-reorder-sticky-actions style={{ position: "sticky", top: "8px", zIndex: 30, margin: "0 0 12px", padding: "10px 12px", background: "rgba(255,247,237,0.96)", backdropFilter: "blur(10px)", border: "1px solid #FED7AA", borderRadius: "12px", color: "#C2410C", fontSize: "12px", lineHeight: 1.45, fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px", flexWrap: "wrap", boxShadow: "0 8px 24px rgba(194,65,12,0.08)" }}>
        <span><strong>{order.length} pages</strong> · {canReset ? `${movedCount} moved` : "original order"} · Drag pages to reorder, or use Move earlier / Move later.</span>
        <span style={{ display: "flex", gap: "6px", alignItems: "center" }}>
          <button type="button" aria-label="Undo last move" onClick={undoLastMove} disabled={!canUndo} style={{ minHeight: "30px", padding: "6px 10px", border: "1px solid #FED7AA", borderRadius: "8px", background: canUndo ? "white" : "#FFF1E8", color: canUndo ? "#C2410C" : "#D6A582", cursor: canUndo ? "pointer" : "not-allowed", fontSize: "11px", fontWeight: 800 }}>Undo</button>
          <button type="button" aria-label="Reset order" onClick={resetOrder} disabled={!canReset} style={{ minHeight: "30px", padding: "6px 10px", border: "1px solid #FED7AA", borderRadius: "8px", background: canReset ? "white" : "#FFF1E8", color: canReset ? "#C2410C" : "#D6A582", cursor: canReset ? "pointer" : "not-allowed", fontSize: "11px", fontWeight: 800 }}>Reset order</button>
          <button type="button" onClick={save} disabled={saving} style={{ minHeight: "30px", padding: "6px 12px", border: "none", borderRadius: "8px", background: "#F97316", color: "white", cursor: saving ? "wait" : "pointer", fontSize: "11px", fontWeight: 900 }}>{saving ? "Saving..." : "Download reordered"}</button>
        </span>
      </div>
      <PageThumbnailGrid pages={pages} order={order} activePageIndex={activePageIndex} lastMovedPageIndex={lastMovedPageIndex} onSelectPage={setActivePageIndex} onMoveEarlier={i => mv(i, -1)} onMoveLater={i => mv(i, 1)} onMovePage={movePageInOrder} />
      {completed && <div role="status" style={{ marginTop: "12px", padding: "10px", background: "#F0FAF4", border: "1px solid #D4F0E0", borderRadius: "9px", color: "#18A558", textAlign: "center", fontSize: "12px", fontWeight: 800 }}>Reordered PDF downloaded. Change page order to create a new output.</div>}
      <button onClick={save} disabled={saving} style={{ width: "100%", marginTop: "14px", padding: "14px", background: "#F97316", color: "white", border: "none", borderRadius: "12px", fontSize: "15px", fontWeight: 700, cursor: "pointer" }}>{saving ? "Saving..." : "↓ Download reordered · free"}</button>
    </div>
  );
}

// ─── UPGRADE MODAL ───
function UpgradeModal({ onClose, isAuthenticated = false, accessToken = null, onBeforeCheckout = null }) {
  return <AuthUpgradeModal onClose={onClose} isAuthenticated={isAuthenticated} accessToken={accessToken} onBeforeCheckout={onBeforeCheckout} />;
}

function AuthUpgradeModal({ onClose, initialMode = "plan", isAuthenticated = false, accessToken = null, onBeforeCheckout = null, reason = "smart-detection" }) {
  const [plan, setPlan] = useState("annual");
  const [step, setStep] = useState(initialMode === "login" ? "auth" : "plan");
  const [authMode, setAuthMode] = useState(initialMode === "login" ? "login" : "signup");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [turnstileToken, setTurnstileToken] = useState("");
  const [authBusy, setAuthBusy] = useState(false);
  const [checkoutBusy, setCheckoutBusy] = useState(false);
  const [authError, setAuthError] = useState("");
  const [authMessage, setAuthMessage] = useState("");
  const turnstileRef = useRef(null);
  const turnstileWidgetRef = useRef(null);
  const planCopy = reason === "merge-limit"
    ? {
        icon: "📎",
        title: <>Merge more files with <span style={{ fontStyle: "italic", color: "#E85D3A" }}>Pro</span></>,
        body: "Merge up to 20 PDFs free. Upgrade to Pro to merge more files at once. Your current files are still here.",
      }
    : reason === "compress-batch"
    ? {
        icon: "📦",
        title: <>Batch compression with <span style={{ fontStyle: "italic", color: "#E85D3A" }}>Pro</span></>,
        body: "Compress multiple PDFs at once and download them as a ZIP. Single-file compression stays free.",
      }
    : {
        icon: "✂️",
        title: <>Unlock <span style={{ fontStyle: "italic", color: "#E85D3A" }}>smart detection</span></>,
        body: "Automatically detect common fields, then review and adjust before export. Works on many fillable and flat forms.",
      };

  useEffect(() => {
    if (step !== "auth" || authMode !== "signup") return;
    setTurnstileToken("");
    let cancelled = false;
    let tries = 0;
    const timer = window.setInterval(() => {
      tries += 1;
      if (cancelled) return;
      if (window.turnstile && turnstileRef.current && turnstileWidgetRef.current === null) {
        try {
          turnstileWidgetRef.current = window.turnstile.render(turnstileRef.current, {
            sitekey: TURNSTILE_SITE_KEY,
            callback: token => {
              setTurnstileToken(token || "");
              setAuthError("");
            },
            "error-callback": () => {
              setTurnstileToken("");
              setAuthError("Verification failed. Please try again.");
            },
            "expired-callback": () => setTurnstileToken(""),
          });
        } catch (e) {
          console.error("Turnstile render failed:", e);
          setAuthError("Verification failed. Please try again.");
        }
        window.clearInterval(timer);
      } else if (tries > 80) {
        setAuthError("Verification failed. Please try again.");
        window.clearInterval(timer);
      }
    }, 100);
    return () => {
      cancelled = true;
      window.clearInterval(timer);
      if (window.turnstile && turnstileWidgetRef.current !== null) {
        try { window.turnstile.remove(turnstileWidgetRef.current); } catch {}
      }
      turnstileWidgetRef.current = null;
    };
  }, [step, authMode]);

  const readAuthError = async (response) => {
    const payload = await response.json().catch(() => ({}));
    return payload.detail || payload.message || "Authentication failed";
  };

  const handleSignup = async (event) => {
    event.preventDefault();
    const normalizedEmail = email.trim();
    setAuthError("");
    setAuthMessage("");
    if (!normalizedEmail || !normalizedEmail.includes("@")) {
      setAuthError("Please enter a valid email address.");
      return;
    }
    if (!password || password.length < 8) {
      setAuthError("Password must be at least 8 characters.");
      return;
    }
    if (!turnstileToken) {
      setAuthError("Verification failed. Please try again.");
      return;
    }
    setAuthBusy(true);
    try {
      const response = await fetch(DETECT_API + "/auth/signup", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: normalizedEmail, password, turnstile_token: turnstileToken }),
      });
      if (!response.ok) throw new Error(await readAuthError(response));
      setAuthMessage("Check your email to verify your account before logging in.");
      setAuthMode("login");
      if (window.turnstile && turnstileWidgetRef.current !== null) {
        try { window.turnstile.remove(turnstileWidgetRef.current); } catch {}
      }
      turnstileWidgetRef.current = null;
      setTurnstileToken("");
    } catch (e) {
      setAuthError(e.message || "Signup failed");
      if (window.turnstile && turnstileWidgetRef.current !== null) {
        try { window.turnstile.reset(turnstileWidgetRef.current); } catch {}
      }
      setTurnstileToken("");
    }
    setAuthBusy(false);
  };

  const handleLogin = async (event) => {
    event.preventDefault();
    const normalizedEmail = email.trim();
    setAuthError("");
    setAuthMessage("");
    if (!normalizedEmail || !normalizedEmail.includes("@")) {
      setAuthError("Please enter a valid email address.");
      return;
    }
    if (!password) {
      setAuthError("Please enter your password.");
      return;
    }
    setAuthBusy(true);
    try {
      const { error } = await supabase.auth.signInWithPassword({ email: normalizedEmail, password });
      if (error) throw error;
      onClose();
    } catch (e) {
      setAuthError(e.message || "Login failed");
    }
    setAuthBusy(false);
  };

  const handlePlanContinue = async () => {
    setAuthError("");
    setAuthMessage("");
    if (!isAuthenticated || !accessToken) {
      setStep("auth");
      setAuthMode("login");
      return;
    }
    setCheckoutBusy(true);
    try {
      await getStripeClient();
      const response = await fetch(DETECT_API + "/stripe/create-checkout-session", {
        method: "POST",
        headers: {
          "Authorization": "Bearer " + accessToken,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ plan }),
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) throw new Error(payload.detail || "Could not start checkout");
      if (!payload.url) throw new Error("Checkout URL missing");
      if (onBeforeCheckout) await onBeforeCheckout();
      window.location.href = payload.url;
    } catch (e) {
      setAuthError(e.message || "Could not start checkout");
      setCheckoutBusy(false);
    }
  };

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 100, display: "flex", alignItems: "center", justifyContent: "center", padding: "20px" }}>
      <div onClick={e => e.stopPropagation()} style={{ background: "white", borderRadius: "20px", padding: "24px 20px", maxWidth: "400px", width: "100%", boxShadow: "0 20px 60px rgba(0,0,0,0.2)", maxHeight: "90vh", overflowY: "auto" }}>
        {step === "plan" && (
          <>
            <div style={{ textAlign: "center", marginBottom: "18px" }}>
              <div style={{ fontSize: "32px", marginBottom: "8px" }}>{planCopy.icon}</div>
              <div style={{ fontFamily: "'Instrument Serif', serif", fontSize: "22px", color: "#1A1A1A", marginBottom: "5px" }}>{planCopy.title}</div>
              <div style={{ fontSize: "12px", color: "#999", lineHeight: 1.5 }}>{planCopy.body}</div>
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px", marginBottom: "16px" }}>
              <div onClick={() => setPlan("monthly")} style={{ padding: "16px 12px", background: plan === "monthly" ? "#FFF5F2" : "#FAFAFA", borderRadius: "12px", textAlign: "center", border: plan === "monthly" ? "2px solid #E85D3A" : "1px solid #EEE", cursor: "pointer" }}>
                <div style={{ fontSize: "9px", fontWeight: 700, letterSpacing: "1px", color: plan === "monthly" ? "#E85D3A" : "#CCC", marginBottom: "4px" }}>MONTHLY</div>
                <div style={{ fontFamily: "'Instrument Serif', serif", fontSize: "26px", color: "#1A1A1A" }}>$9<span style={{ fontSize: "13px", color: "#999" }}>/mo</span></div>
                <div style={{ fontSize: "9px", color: "#AAA", marginTop: "3px" }}>Cancel anytime</div>
              </div>
              <div onClick={() => setPlan("annual")} style={{ padding: "16px 12px", background: plan === "annual" ? "#1A1A1A" : "#F5F5F5", borderRadius: "12px", textAlign: "center", border: plan === "annual" ? "2px solid #E85D3A" : "1px solid #EEE", cursor: "pointer", position: "relative" }}>
                <div style={{ position: "absolute", top: "6px", right: "6px", fontSize: "7px", fontWeight: 800, background: "#E85D3A", color: "white", padding: "2px 6px", borderRadius: "3px" }}>SAVE 22%</div>
                <div style={{ fontSize: "9px", fontWeight: 700, letterSpacing: "1px", color: plan === "annual" ? "rgba(255,255,255,0.4)" : "#CCC", marginBottom: "4px" }}>ANNUAL</div>
                <div style={{ fontFamily: "'Instrument Serif', serif", fontSize: "26px", color: plan === "annual" ? "white" : "#1A1A1A" }}>$84<span style={{ fontSize: "13px", color: plan === "annual" ? "rgba(255,255,255,0.6)" : "#999" }}>/year</span></div>
                <div style={{ fontSize: "9px", color: plan === "annual" ? "rgba(255,255,255,0.5)" : "#AAA", marginTop: "3px" }}>$7/month effective</div>
              </div>
            </div>
            {authError && <div style={{ padding: "10px 12px", background: "#FFF0ED", borderRadius: "8px", border: "1px solid #FDDDD4", marginBottom: "12px", fontSize: "11px", color: "#E85D3A", lineHeight: 1.5 }}>{authError}</div>}
            <button onClick={handlePlanContinue} disabled={checkoutBusy} style={{ width: "100%", padding: "14px", background: checkoutBusy ? "#EEE" : "#E85D3A", color: checkoutBusy ? "#AAA" : "white", border: "none", borderRadius: "12px", fontSize: "15px", fontWeight: 700, cursor: checkoutBusy ? "default" : "pointer", marginBottom: "8px" }}>{checkoutBusy ? "Redirecting to checkout..." : `Continue — ${plan === "annual" ? "$84/year" : "$9/month"}`}</button>
            <button onClick={onClose} style={{ width: "100%", padding: "10px", background: "none", border: "none", fontSize: "12px", color: "#CCC", cursor: "pointer" }}>Maybe later</button>
          </>
        )}

        {step === "auth" && (
          <>
            <button onClick={() => setStep("plan")} style={{ background: "none", border: "none", fontSize: "12px", color: "#BBB", cursor: "pointer", padding: 0, marginBottom: "16px" }}>← Back</button>
            <div style={{ textAlign: "center", marginBottom: "20px" }}>
              <div style={{ fontSize: "28px", marginBottom: "8px" }}>{authMode === "signup" ? "📧" : "🔐"}</div>
              <div style={{ fontFamily: "'Instrument Serif', serif", fontSize: "20px", color: "#1A1A1A", marginBottom: "5px" }}>{authMode === "signup" ? "Create your account" : "Log in"}</div>
              <div style={{ fontSize: "12px", color: "#999", lineHeight: 1.5 }}>{authMode === "signup" ? "Create an account to use authenticated field detection." : "Log in to use auto-detect."}</div>
            </div>
            {authMessage && <div style={{ padding: "10px 12px", background: "#F0FAF4", borderRadius: "8px", border: "1px solid #D4F0E0", marginBottom: "12px", fontSize: "11px", color: "#18A558", lineHeight: 1.5 }}>{authMessage}</div>}
            {authError && <div style={{ padding: "10px 12px", background: "#FFF0ED", borderRadius: "8px", border: "1px solid #FDDDD4", marginBottom: "12px", fontSize: "11px", color: "#E85D3A", lineHeight: 1.5 }}>{authError}</div>}
            <form onSubmit={authMode === "signup" ? handleSignup : handleLogin}>
              <div style={{ marginBottom: "12px" }}>
                <label style={{ fontSize: "11px", fontWeight: 600, color: "#888", display: "block", marginBottom: "5px" }}>Email address</label>
                <input type="email" inputMode="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="you@example.com" autoFocus style={{ width: "100%", padding: "12px 14px", border: "1px solid #EEE", borderRadius: "10px", fontSize: "15px", outline: "none", boxSizing: "border-box", WebkitAppearance: "none" }} />
              </div>
              <div style={{ marginBottom: "12px" }}>
                <label style={{ fontSize: "11px", fontWeight: 600, color: "#888", display: "block", marginBottom: "5px" }}>Password</label>
                <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder={authMode === "signup" ? "At least 8 characters" : "Your password"} style={{ width: "100%", padding: "12px 14px", border: "1px solid #EEE", borderRadius: "10px", fontSize: "15px", outline: "none", boxSizing: "border-box", WebkitAppearance: "none" }} />
              </div>
              {authMode === "signup" && <div style={{ display: "flex", justifyContent: "center", marginBottom: "12px", minHeight: "65px" }}><div ref={turnstileRef}></div></div>}
              <div style={{ padding: "10px 12px", background: "#F0FAF4", borderRadius: "8px", border: "1px solid #D4F0E0", marginBottom: "16px" }}><div style={{ fontSize: "10px", color: "#18A558", lineHeight: 1.5 }}>🔒 We use your account only to authenticate auto-detect requests. PDF files are analyzed transiently and are not stored.</div></div>
              <button type="submit" disabled={authBusy} style={{ width: "100%", padding: "14px", background: !authBusy ? "#E85D3A" : "#EEE", color: !authBusy ? "white" : "#CCC", border: "none", borderRadius: "12px", fontSize: "15px", fontWeight: 700, cursor: !authBusy ? "pointer" : "default", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px" }}>
                {authBusy && <div style={{ width: "16px", height: "16px", border: "2px solid rgba(255,255,255,0.25)", borderTopColor: "white", borderRadius: "50%", animation: "nsS 0.6s linear infinite" }} />}
                {authBusy ? "Please wait..." : authMode === "signup" ? "Sign up" : "Log in"}
              </button>
            </form>
            <button type="button" onClick={() => { setAuthMode(authMode === "signup" ? "login" : "signup"); setAuthError(""); setAuthMessage(""); }} style={{ width: "100%", padding: "10px", background: "none", border: "none", fontSize: "12px", color: "#E85D3A", cursor: "pointer", marginTop: "8px" }}>{authMode === "signup" ? "Already have an account? Log in" : "Need an account? Sign up"}</button>
            <div style={{ fontSize: "9px", color: "#CCC", textAlign: "center", marginTop: "10px", lineHeight: 1.5 }}>Billing and Pro access will be handled separately.</div>
          </>
        )}
      </div>
    </div>
  );
}

function App() {
  const ready = useLibs();
  const [view, setView] = useState("home"); // home | tool
  const [toolId, setToolId] = useState(null);
  const [files, setFiles] = useState([]);
  const [result, setResult] = useState(null); const [dlData, setDlData] = useState(null);
  const [error, setError] = useState(null); const [processing, setPr] = useState(false);
  const [mergeDragIndex, setMergeDragIndex] = useState(null); const [mergeDropIndex, setMergeDropIndex] = useState(null);
  const [splitIn, setSplitIn] = useState(""); const [splitMode, setSplitMode] = useState("visual"); const [splitPoints, setSplitPoints] = useState([]); const [splitPageCount, setSplitPageCount] = useState(0); const [tasks, setTasks] = useState(2);
  const [compressMode, setCompressMode] = useState("standard"); const [compressPreset, setCompressPreset] = useState("recommended"); const [compressProgress, setCompressProgress] = useState(null);
  const [compressWorkflow, setCompressWorkflow] = useState("single");
  const [compressBatchOpen, setCompressBatchOpen] = useState(false); const [compressBatchFiles, setCompressBatchFiles] = useState([]); const [compressBatchPreset, setCompressBatchPreset] = useState("recommended"); const [compressBatchProgress, setCompressBatchProgress] = useState(null);
  // Watermark & page number options
  const [wmText, setWmText] = useState("DRAFT"); const [wmSize, setWmSize] = useState(48); const [wmOpacity, setWmOpacity] = useState(0.15);
  const [pageNumPos, setPageNumPos] = useState("bottom-center"); const [pageNumSize, setPageNumSize] = useState(10);
  // Editor state
  const [pdfBytes, setPdfBytes] = useState(null);
  const [imgs, setImgs] = useState([]); const [dims, setDims] = useState([]);
  const [items, setItems] = useState([]); // all annotations/fields/images/signatures
  const [autoFieldState, setAutoFieldState] = useState({});
  const [undoStack, setUndoStack] = useState([]);
  const [redoStack, setRedoStack] = useState([]);
  const HISTORY_LIMIT = 500;
  const [selId, setSelId] = useState(null); const [pg, setPg] = useState(0);
  const [editMode, setEditMode] = useState("text"); // text|check|sign|image|highlight|note
  const [editorMode, setEditorMode] = useState("fill"); // fill|edit
  const [placingTextField, setPlacingTextField] = useState(false);
  const [editSubmode, setEditSubmode] = useState("idle"); // idle|selected|typing
  const [manualFocusItemId, setManualFocusItemId] = useState(null);
  const [editorV2MoreOpen, setEditorV2MoreOpen] = useState(false);
  const [editorV2ConfirmAction, setEditorV2ConfirmAction] = useState(null);
  const [fontSize, setFontSize] = useState(10);
  const [editingItemId, setEditingItemId] = useState(null); // which text item is being edited (for iOS keyboard)
  const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false);
  const [detecting, setDetecting] = useState(false);
  const [showFields, setShowFields] = useState(true); // toggle auto-detected field visibility
  const [detectError, setDetectError] = useState(null);
  const [lastDetectionTextFound, setLastDetectionTextFound] = useState(null);
  const [pageAngles, setPageAngles] = useState([]);
  const [docHash, setDocHash] = useState(null);
  const [ignoredRotationFiles, setIgnoredRotationFiles] = useState({});
  const [rotationNotice, setRotationNotice] = useState(null);
  const [rotatingEditorPdf, setRotatingEditorPdf] = useState(false);
  const [showSignPad, setShowSignPad] = useState(false);
  const [signatureCreationRequested, setSignatureCreationRequested] = useState(false);
  const [signatureImg, setSignatureImg] = useState(null); const [placingSign, setPlacingSign] = useState(false);
  const [addImgSrc, setAddImgSrc] = useState(null); const [placingImg, setPlacingImg] = useState(false);
  const [editorV2FitMode, setEditorV2FitMode] = useState("auto");
  const [zoom, setZoom] = useState(1);
  const [cw, setCw] = useState(360); const [pdfViewportHeight, setPdfViewportHeight] = useState(0); const cRef = useRef(null); const pdfContainerRef = useRef(null); const pdfViewportRef = useRef(null);
  const hiddenInputRef = useRef(null);
  const autoFieldStateRef = useRef({});
  const historyMuteRef = useRef(false);
  const historyInitializedRef = useRef(false);
  const suppressNextEditorLoadRef = useRef(false);
  const postRotationDetectRef = useRef(false);
  const handleManualFocusHandled = useCallback((id) => {
    setManualFocusItemId(current => current === id ? null : current);
  }, []);
  const flatAutoDetectAttemptRef = useRef("");
  const lastItemsSnapshotRef = useRef("[]");
  const subscriptionRequestRef = useRef(0);
  const appendHistorySnapshot = useCallback(function(stack, snapshot) {
    return [...stack.slice(-(HISTORY_LIMIT - 1)), snapshot];
  }, [HISTORY_LIMIT]);

  // ─── PRO SYSTEM ───
  const [isPro, setIsPro] = useState(false);
  // Saved signatures (Pro: account-backed via API, Free: session only)
  const [savedSignatures, setSavedSignatures] = useState([]);
  const saveSignature = async (dataUrl) => {
    if (isPro && accessToken) {
      try {
        setSavedSignatures(await createSavedSignatureForAccount(accessToken, dataUrl));
      } catch (err) {
        console.error("Could not save signature to account:", err);
      }
      return;
    }
    const updated = [{ id: Date.now().toString(), src: dataUrl, created: new Date().toISOString() }, ...savedSignatures].slice(0, 10);
    setSavedSignatures(updated);
  };
  const deleteSignature = async (id) => {
    const previous = savedSignatures;
    const updated = savedSignatures.filter(s => s.id !== id);
    setSavedSignatures(updated);
    if (isPro && accessToken) {
      try {
        await deleteSavedSignatureForAccount(accessToken, id);
      } catch (err) {
        console.error("Could not delete saved signature:", err);
        setSavedSignatures(previous);
      }
    }
  };
  // Recent files (Pro only, stored locally)
  const [recentFiles, setRecentFiles] = useState(() => {
    try { const r = localStorage.getItem("nspdf_recent"); return r ? JSON.parse(r) : []; } catch { return []; }
  });
  const devProOverride = resolveDevProOverride();
  const hasCompressBatchAccess = isPro || devProOverride;
  const addToRecent = (name, tool) => {
    if (!isPro) return;
    const updated = [{ name, tool, date: new Date().toISOString() }, ...recentFiles.filter(r => r.name !== name)].slice(0, 10);
    setRecentFiles(updated);
    try { localStorage.setItem("nspdf_recent", JSON.stringify(updated)); } catch {}
  };
  // Pro font list — free gets first 3, pro gets all
  const FREE_FONTS = ["helvetica", "times", "courier"];
  const ALL_FONTS = SIGN_FONTS;
  // Batch mode detection
  const isBatchTool = ["merge", "compress", "convert"].includes(toolId);
  const batchLimit = isPro ? PRO_BATCH_FILE_LIMIT : (toolId === "merge" ? MERGE_FREE_FILE_LIMIT : FREE_BATCH_FILE_LIMIT);
  const mergeTotalBytes = toolId === "merge" ? files.reduce((total, f) => total + (Number(f && f.size) || 0), 0) : 0;
  const mergeIsVeryLarge = toolId === "merge" && mergeTotalBytes > MERGE_BROWSER_WARNING_BYTES;
  // Show upgrade prompt
  const [showUpgrade, setShowUpgrade] = useState(false);
  const [authModalMode, setAuthModalMode] = useState("plan");
  const [proModalReason, setProModalReason] = useState("smart-detection");
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [userEmail, setUserEmail] = useState(null);
  const [accessToken, setAccessToken] = useState(null);
  const [checkoutNotice, setCheckoutNotice] = useState(null);
  const [restoreBannerVisible, setRestoreBannerVisible] = useState(false);
  const [postPlacementHintVisible, setPostPlacementHintVisible] = useState(false);
  const [restoreRequested, setRestoreRequested] = useState(false);
  const [usageLimitModal, setUsageLimitModal] = useState(null);
  const pendingRestoreStateRef = useRef(null);
  const pendingRestoreApplyRef = useRef(null);
  const restoreInProgressRef = useRef(false);

  const continuityFieldOrder = useCallback(function(sourceItems) {
    return (sourceItems || [])
      .filter(function(it) { return it && !it.hidden && (it.type === "text" || it.type === "checkbox" || it.type === "radio" || it.type === "check"); })
      .slice()
      .sort(function(a, b) {
        var ap = a.auto && a.page > 0 ? a.page - 1 : (a.page || 0);
        var bp = b.auto && b.page > 0 ? b.page - 1 : (b.page || 0);
        if (ap !== bp) return ap - bp;
        var ay = Math.round((a.y || 0) / 6) * 6;
        var by = Math.round((b.y || 0) / 6) * 6;
        if (ay !== by) return ay - by;
        return (a.x || 0) - (b.x || 0);
      });
  }, []);

  const saveEditorState = useCallback(async function(reason) {
    try {
      if (!files.length || !files[0]) return;
      const file = files[0];
      const isMobile = isContinuityMobileViewport();
      const cap = isMobile ? 3 * 1024 * 1024 : 8 * 1024 * 1024;
      if (file.size > cap) {
        console.log("[CONT-1] PDF exceeds cap (" + cap + " bytes), skipping persistence");
        return;
      }
      const pdfData = arrayBufferToBase64(await readBuf(file));
      const filledValues = continuityFieldOrder(items).map(function(it) {
        if (it.type === "text") return String(it.text || "");
        if (it.type === "checkbox" || it.type === "radio" || it.type === "check") return it.checked ? "true" : "";
        return "";
      });
      const state = {
        pdfData,
        filename: file.name || "document.pdf",
        currentPage: pg + 1,
        filledValues,
        timestamp: Date.now(),
        reason,
      };
      sessionStorage.setItem(CONTINUITY_STORAGE_KEY, JSON.stringify(state));
      sessionStorage.setItem(CONTINUITY_PENDING_KEY, "true");
      console.log("[CONT-1] Saved editor state:", reason);
    } catch (err) {
      console.log("[CONT-1] Could not save editor state:", err);
    }
  }, [continuityFieldOrder, files, items, pg]);

  const clearEditorState = useCallback(function() {
    try {
      sessionStorage.removeItem(CONTINUITY_STORAGE_KEY);
      sessionStorage.removeItem(CONTINUITY_PENDING_KEY);
    } catch (err) {
      console.log("[CONT-1] Could not clear editor state:", err);
    }
  }, []);

  const restoreEditorState = useCallback(function() {
    let raw = null;
    try {
      raw = sessionStorage.getItem(CONTINUITY_STORAGE_KEY);
    } catch (err) {
      console.log("[CONT-1] Could not read editor state:", err);
      return null;
    }
    if (!raw) return null;
    try {
      const state = JSON.parse(raw);
      if (!state || Date.now() - Number(state.timestamp || 0) > CONTINUITY_TTL_MS) {
        clearEditorState();
        return null;
      }
      return state;
    } catch (err) {
      console.log("[CONT-1] Stored editor state could not be parsed:", err);
      clearEditorState();
      return null;
    }
  }, [clearEditorState]);

  const hasPendingRestore = useCallback(function() {
    try {
      return sessionStorage.getItem(CONTINUITY_PENDING_KEY) === "true";
    } catch {
      return false;
    }
  }, []);

  useEffect(() => {
    let mounted = true;
    const applySession = async (session) => {
      if (!mounted) return;
      if (session) {
        const requestId = ++subscriptionRequestRef.current;
        setIsAuthenticated(true);
        setUserEmail(session.user?.email || null);
        setAccessToken(session.access_token || null);
        setIsPro(false);
        const subscription = await getSubscriptionStatus(session.access_token || null);
        if (!mounted || requestId !== subscriptionRequestRef.current) return;
        setIsPro(subscription.is_pro === true);
      } else {
        subscriptionRequestRef.current += 1;
        clearEditorState();
        setIsAuthenticated(false);
        setUserEmail(null);
        setAccessToken(null);
        setIsPro(false);
        setItems(prev => prev.filter(it => !(it.auto && isServerDetectedSource(it.source))));
        setAutoFieldState({});
        setSelId(null);
        setEditingItemId(null);
        setSignatureImg(null);
        setPlacingSign(false);
        setShowSignPad(false);
        setSignatureCreationRequested(false);
        setSavedSignatures([]);
        setDetectError(null);
        setPageAngles([]);
        setDocHash(null);
        postRotationDetectRef.current = false;
      }
    };
    supabase.auth.getSession().then(({ data }) => applySession(data?.session || null));
    const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
      applySession(session || null);
    });
    return () => {
      mounted = false;
      authListener?.subscription?.unsubscribe?.();
    };
  }, []);

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const stripeStatus = params.get("stripe");
    const shouldAttemptRestore = stripeStatus === "success"
      || (!stripeStatus && hasPendingRestore() && !isLegalRoute(window.location.pathname));
    if (shouldAttemptRestore) {
      const restoredState = restoreEditorState();
      if (restoredState) {
        pendingRestoreStateRef.current = restoredState;
        setRestoreRequested(true);
      }
    }
    if (stripeStatus === "success") {
      setCheckoutNotice("Payment received. Checking your Pro status... If your Pro access does not appear immediately, refresh in a few seconds.");
      let cancelled = false;
      (async () => {
        const { data } = await supabase.auth.getSession();
        const session = data?.session || null;
        if (cancelled || !session?.access_token) return;
        const requestId = ++subscriptionRequestRef.current;
        const subscription = await getSubscriptionStatus(session.access_token);
        if (cancelled || requestId !== subscriptionRequestRef.current) return;
        setIsAuthenticated(true);
        setUserEmail(session.user?.email || null);
        setAccessToken(session.access_token || null);
        setIsPro(subscription.is_pro === true);
        if (subscription.is_pro === true) {
          setCheckoutNotice("Payment received. Pro access is active.");
        }
      })();
      window.history.replaceState({}, "", "/index.html");
      return () => { cancelled = true; };
    } else if (stripeStatus === "cancel") {
      setCheckoutNotice("Checkout canceled.");
      window.history.replaceState({}, "", "/index.html");
    }
  }, []);

  // Tool boot hint for future static per-tool shells (TOOL-SPLITTING-ARCH-1).
  // A static shell such as /compress-pdf/index.html sets
  // <body data-tool="compress"> to open directly into that tool. The homepage
  // sets no hint, so its behavior is unchanged. Runs once on mount and is
  // independent of the ?editorV2 / ?stripe param handling above. An unknown or
  // missing hint safely falls back to the homepage.
  useEffect(() => {
    const hint = (document.body.getAttribute("data-tool") || "").trim().toLowerCase();
    if (!hint) return;
    if (TOOLS.some(t => t.id === hint)) openTool(hint);
  }, []);

  useEffect(() => {
    let cancelled = false;
    if (!isAuthenticated || !isPro || !accessToken) return;
    (async () => {
      const signatures = await listSavedSignaturesForAccount(accessToken);
      if (!cancelled) setSavedSignatures(signatures);
    })();
    return () => { cancelled = true; };
  }, [isAuthenticated, isPro, accessToken]);

  useEffect(() => {
    if (editorMode !== "edit" || editMode !== "sign") return;
    if (!showSignPad || signatureCreationRequested || savedSignatures.length === 0) return;
    setShowSignPad(false);
    if (!signatureImg) setSignatureImg(savedSignatures[0].src);
  }, [editorMode, editMode, showSignPad, signatureCreationRequested, savedSignatures, signatureImg]);

  const handleLogout = async () => {
    clearEditorState();
    await supabase.auth.signOut();
  };
  const openUpgradeModal = (reason = "smart-detection") => { setProModalReason(reason); setAuthModalMode("plan"); setShowUpgrade(true); };
  const openLoginModal = () => { setProModalReason("smart-detection"); setAuthModalMode("login"); setShowUpgrade(true); };

  useEffect(() => {
    const m = () => {
      const el = pdfViewportRef.current || cRef.current;
      if (el) {
        setCw(el.clientWidth || el.offsetWidth);
        setPdfViewportHeight(el.clientHeight || el.offsetHeight || 0);
      }
    };
    m();
    const raf = window.requestAnimationFrame ? window.requestAnimationFrame(m) : null;
    const el = pdfViewportRef.current || cRef.current;
    const ro = el && window.ResizeObserver ? new ResizeObserver(m) : null;
    if (ro && el) ro.observe(el);
    window.addEventListener("resize", m);
    return () => {
      if (raf !== null && window.cancelAnimationFrame) window.cancelAnimationFrame(raf);
      if (ro) ro.disconnect();
      window.removeEventListener("resize", m);
    };
  }, [imgs, view, toolId, editorV2Active]);
  useEffect(() => { autoFieldStateRef.current = autoFieldState; }, [autoFieldState]);
  const resetHistory = useCallback(function(nextItems) {
    const snap = serializeEditorItems(nextItems || []);
    historyMuteRef.current = false;
    historyInitializedRef.current = true;
    lastItemsSnapshotRef.current = snap;
    setUndoStack([]);
    setRedoStack([]);
  }, []);
  useEffect(() => {
    const snap = serializeEditorItems(items);
    if (!historyInitializedRef.current) {
      historyInitializedRef.current = true;
      lastItemsSnapshotRef.current = snap;
      return;
    }
    if (historyMuteRef.current) {
      historyMuteRef.current = false;
      lastItemsSnapshotRef.current = snap;
      return;
    }
    if (snap === lastItemsSnapshotRef.current) return;
    try {
      const prior = JSON.parse(lastItemsSnapshotRef.current);
      setUndoStack(prev => appendHistorySnapshot(prev, prior));
      setRedoStack([]);
    } catch {}
    lastItemsSnapshotRef.current = snap;
  }, [appendHistorySnapshot, items]);
  useEffect(() => {
    setAutoFieldState(function(prev) {
      var changed = false;
      var next = { ...prev };
      items.forEach(function(item) {
        if (!item.auto) return;
        var key = getPersistKey(item);
        var value = extractPersistedFieldState(item);
        if (!samePersistedFieldState(prev[key], value)) {
          next[key] = value;
          changed = true;
        }
      });
      return changed ? next : prev;
    });
  }, [items]);
  const performUndo = useCallback(function() {
    if (!undoStack.length) return;
    var previous = undoStack[undoStack.length - 1];
    historyMuteRef.current = true;
    setRedoStack(function(r) { return appendHistorySnapshot(r, items); });
    setItems(previous);
    setUndoStack(function(prev) { return prev.slice(0, -1); });
  }, [appendHistorySnapshot, items, undoStack]);
  const performRedo = useCallback(function() {
    if (!redoStack.length) return;
    var next = redoStack[redoStack.length - 1];
    historyMuteRef.current = true;
    setUndoStack(function(u) { return appendHistorySnapshot(u, items); });
    setItems(next);
    setRedoStack(function(prev) { return prev.slice(0, -1); });
  }, [appendHistorySnapshot, items, redoStack]);

  const deleteSelectedItem = useCallback(function() {
    if (!selId) return;
    setItems(function(prev) {
      const target = prev.find(function(it) { return it.id === selId; });
      if (isNativeAcroFormItem(target)) return prev;
      return prev.filter(function(it) { return it.id !== selId; });
    });
    setSelId(null);
    setEditingItemId(null);
  }, [selId]);

  const sortedFillItems = useCallback(function() {
    const TAB_ROW_GAP_PX = 12;
    const ordered = [];
    const byPage = {};
    items
      .filter(function(it) { return it && !it.hidden && (!it.auto || showFields); })
      .forEach(function(it) {
        var pageIndex = itemPageIndex(it);
        if (!byPage[pageIndex]) byPage[pageIndex] = [];
        byPage[pageIndex].push(it);
      });
    Object.keys(byPage).map(Number).sort(function(a, b) { return a - b; }).forEach(function(pageIndex) {
      var rows = [];
      byPage[pageIndex].slice().sort(function(a, b) {
        var ay = Number(a.y || 0);
        var by = Number(b.y || 0);
        if (ay !== by) return ay - by;
        return (a.x || 0) - (b.x || 0);
      }).forEach(function(it) {
        var y = Number(it.y || 0);
        var row = rows[rows.length - 1];
        if (!row || Math.abs(y - row.lastY) > TAB_ROW_GAP_PX) {
          rows.push({ lastY: y, items: [it] });
        } else {
          row.items.push(it);
          row.lastY = y;
        }
      });
      rows.forEach(function(row) {
        row.items.sort(function(a, b) { return (a.x || 0) - (b.x || 0); }).forEach(function(it) {
          ordered.push(it);
        });
      });
    });
    return ordered;
  }, [items, showFields]);

  const focusEditorItem = useCallback(function(item) {
    if (!item) return;
    setPg(itemPageIndex(item));
    setSelId(item.id);
    if (editorMode === "fill" && item.type === "text") {
      setEditingItemId(item.id);
      if (item.fontSize) setFontSize(item.fontSize);
    } else {
      setEditingItemId(null);
    }
  }, [editorMode]);

  const moveTabFocus = useCallback(function(backward) {
    var ordered = sortedFillItems();
    if (!ordered.length) return;
    var currentIndex = ordered.findIndex(function(it) { return it.id === selId; });
    var nextIndex = currentIndex < 0
      ? (backward ? ordered.length - 1 : 0)
      : (currentIndex + (backward ? -1 : 1) + ordered.length) % ordered.length;
    focusEditorItem(ordered[nextIndex]);
  }, [focusEditorItem, selId, sortedFillItems]);

  const toggleSelectedChoice = useCallback(function() {
    if (!selId) return;
    var selected = items.find(function(it) { return it.id === selId; });
    if (!selected) return;
    if (selected.acroButtonGroupId) {
      setItems(function(prev) { return applyButtonToggle(prev, selected, !selected.checked); });
    } else if (selected.type === "radio" && selected.groupId) {
      setItems(function(prev) { return prev.map(function(it) { return it.type === "radio" && it.groupId === selected.groupId ? { ...it, checked: it.id === selected.id } : it; }); });
    } else if (selected.type === "radio" || selected.type === "checkbox") {
      setItems(function(prev) { return prev.map(function(it) { return it.id === selected.id ? { ...it, checked: !it.checked } : it; }); });
    }
  }, [items, selId]);

  useEffect(() => {
    const onKeyDown = function(e) {
      const target = e.target;
      const isTypingTarget = target && (
        target.tagName === "INPUT" ||
        target.tagName === "TEXTAREA" ||
        target.isContentEditable
      );
      if (showUpgrade || usageLimitModal) return;
      const editorActive = toolId === "edit";
      if (editorActive && files.length && e.key === "Tab") {
        e.preventDefault();
        moveTabFocus(e.shiftKey);
        return;
      }
      if (editorActive && files.length && e.key === "Escape") {
        e.preventDefault();
        setSelId(null);
        setEditingItemId(null);
        if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
        return;
      }
      if (editorActive && files.length && editorMode === "edit" && selId && !isTypingTarget && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
        e.preventDefault();
        const step = e.shiftKey ? 10 : 1;
        const dx = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
        const dy = e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0;
        setItems(function(prev) { return prev.map(function(it) { return it.id === selId ? { ...it, x: (it.x || 0) + dx, y: (it.y || 0) + dy } : it; }); });
        return;
      }
      if (editorActive && files.length && (e.key === "Delete" || e.key === "Backspace") && selectedManualItem && !isTypingTarget) {
        e.preventDefault();
        deleteSelectedItem();
        return;
      }
      if (editorActive && files.length && editorMode === "fill" && e.key === " " && selId && !isTypingTarget) {
        e.preventDefault();
        toggleSelectedChoice();
        return;
      }
      const meta = e.ctrlKey || e.metaKey;
      if (!meta) return;
      const key = String(e.key || "").toLowerCase();
      if (key === "z" && !e.shiftKey) {
        e.preventDefault();
        performUndo();
      } else if (key === "y" || (key === "z" && e.shiftKey)) {
        e.preventDefault();
        performRedo();
      }
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [deleteSelectedItem, editorMode, files.length, moveTabFocus, performUndo, performRedo, selectedManualItem, selId, showUpgrade, toggleSelectedChoice, toolId, usageLimitModal]);

  const clearEditorEdits = useCallback(function() {
    setItems(function(prev) {
      const nextItems = prev
        .filter(function(it) { return it.auto; })
        .map(function(it) {
        if (it.type === "text") return { ...it, text: "", manualFontSize: false };
          if (it.type === "checkbox" || it.type === "radio") return { ...it, checked: false };
          return it;
        });
      setAutoFieldState({});
      resetHistory(nextItems);
      return nextItems;
    });
    setSelId(null);
    setEditingItemId(null);
  }, [resetHistory]);

  const resetEditorForNewFile = useCallback(function(nextFiles) {
    setFiles(nextFiles || []);
    setItems([]);
    setAutoFieldState({});
    resetHistory([]);
    setSelId(null);
    setEditingItemId(null);
    setDetectError(null);
    setPageAngles([]);
    setDocHash(null);
    postRotationDetectRef.current = false;
    setRotationNotice(null);
    setRotatingEditorPdf(false);
    setImgs([]);
    setDims([]);
    setPdfBytes(null);
    setSignatureImg(null);
    setAddImgSrc(null);
    setPlacingSign(false);
    setPlacingImg(false);
    setShowSignPad(false);
    setSignatureCreationRequested(false);
    setShowFields(true);
    setEditorV2MoreOpen(false);
    setEditorV2ConfirmAction(null);
    setEditorV2FitMode("auto");
    setCompressWorkflow("single");
    setCompressBatchOpen(false);
    setCompressBatchFiles([]);
    setCompressBatchPreset("recommended");
    setCompressBatchProgress(null);
    setZoom(1);
    setEditorMode("fill");
    setPlacingTextField(false);
    setEditSubmode("idle");
    setEditMode("text");
  }, [resetHistory]);

  const tool = TOOLS.find(t => t.id === toolId);
  const goHome = () => { setView("home"); setToolId(null); setFiles([]); setResult(null); setDlData(null); setError(null); setPdfBytes(null); setImgs([]); setDims([]); setItems([]); setAutoFieldState({}); resetHistory([]); setSelId(null); setPr(false); setSplitIn(""); setSplitMode("visual"); setSplitPoints([]); setSplitPageCount(0); setCompressMode("standard"); setCompressPreset("recommended"); setCompressProgress(null); setCompressWorkflow("single"); setCompressBatchOpen(false); setCompressBatchFiles([]); setCompressBatchPreset("recommended"); setCompressBatchProgress(null); setShowSignPad(false); setSignatureCreationRequested(false); setPlacingSign(false); setAddImgSrc(null); setPlacingImg(false); setEditingItemId(null); setEditorV2MoreOpen(false); setEditorV2ConfirmAction(null); setEditorV2FitMode("auto"); setZoom(1); setPageAngles([]); setDocHash(null); postRotationDetectRef.current = false; setRotationNotice(null); setRotatingEditorPdf(false); setEditorMode("fill"); setPlacingTextField(false); setEditSubmode("idle"); setEditMode("text"); };
  const openTool = id => { goHome(); setToolId(id); setView("tool"); };
  const addFiles = f => {
    const current = files.length;
    const incoming = f.length;
    const hitBatchLimit = isBatchTool && current + incoming > batchLimit && !isPro;
    if (hitBatchLimit) {
      const isMergeLimit = toolId === "merge";
      setError(isMergeLimit ? "Merge up to 20 PDFs free. Upgrade to Pro to merge more files at once." : `Free tier supports up to ${batchLimit} files. Upgrade to Pro for batch processing up to 100 files.`);
      openUpgradeModal(isMergeLimit ? "merge-limit" : "smart-detection");
      setFiles(p => [...p, ...f.slice(0, batchLimit - current)]);
    } else {
      setFiles(p => [...p, ...f]);
    }
    setResult(null);
    if (toolId === "compress") { setCompressProgress(null); setCompressBatchProgress(null); }
    if (!hitBatchLimit) setError(null);
    if (toolId === "split") { setSplitPoints([]); setSplitPageCount(0); }
  };
  const removeFileAt = index => {
    setFiles(p => p.filter((_, idx) => idx !== index));
    setError(null);
    setResult(null);
    setDlData(null);
    if (toolId === "compress") { setCompressProgress(null); setCompressBatchProgress(null); }
    setMergeDragIndex(null);
    setMergeDropIndex(null);
    if (toolId === "split") { setSplitIn(""); setSplitMode("visual"); setSplitPoints([]); setSplitPageCount(0); }
  };
  const moveFileToIndex = (fromIndex, toIndex) => {
    setFiles(p => {
      if (fromIndex < 0 || fromIndex >= p.length) return p;
      const target = Math.max(0, Math.min(toIndex, p.length - 1));
      if (fromIndex === target) return p;
      const next = [...p];
      const [moved] = next.splice(fromIndex, 1);
      next.splice(Math.max(0, Math.min(target, next.length)), 0, moved);
      return next;
    });
    setError(null);
    setResult(null);
    setDlData(null);
    setMergeDragIndex(null);
    setMergeDropIndex(null);
  };
  const moveFileAt = (index, direction) => {
    moveFileToIndex(index, index + direction);
  };
  const dropMergeFileAt = (fromIndex, dropIndex) => {
    const target = fromIndex < dropIndex ? dropIndex - 1 : dropIndex;
    moveFileToIndex(fromIndex, target);
  };

  const currentFileKey = files[0] ? [files[0].name, files[0].size, files[0].lastModified || 0].join("|") : "";
  const rotationSuggestion = currentFileKey && !ignoredRotationFiles[currentFileKey] && pageAngles.length
    ? suggestedRotationForAngle(pageAngles[0])
    : null;

  const applyEditorRotation = useCallback(async function(deg) {
    if (!files.length || rotatingEditorPdf) return;
    setRotatingEditorPdf(true);
    setRotationNotice(null);
    try {
      const r = await rotatePDF(files[0], deg);
      const rotatedFile = new File([r.blob], files[0].name, { type: "application/pdf", lastModified: Date.now() });
      const bytes = await readBytes(rotatedFile);
      suppressNextEditorLoadRef.current = true;
      setFiles([rotatedFile]);
      setPdfBytes(bytes);
      window.__nspdf_bytes__ = bytes.slice(0);
      const rendered = await renderPages(bytes, getEditorPageRenderScale());
      setImgs(rendered.imgs);
      setDims(rendered.dims);
      setPg(0);
      setSelId(null);
      setEditingItemId(null);
      setEditorMode("fill");
      setPlacingTextField(false);
      setEditSubmode("idle");
      setEditMode("text");
      const clamped = clampManualItemsToPage(items.filter(function(it) { return !it.auto; }), rendered.dims);
      const nextItems = clamped.items;
      setItems(nextItems);
      resetHistory(nextItems);
      setAutoFieldState({});
      setShowFields(false);
      setDetectError("flat");
      setPageAngles([]);
      if (docHash && accessToken) {
        postRotationDetectRef.current = true;
        notifyRotationEvent(accessToken, docHash).catch(function(err) {
          console.warn("[NoStringsPDF] Rotation grace event failed:", err);
        });
      }
      setRotationNotice(clamped.repositioned ? "Some items were repositioned to stay visible after rotation" : "PDF rotated. Re-run detection to refresh fields.");
    } catch(e) {
      console.error("PDF rotation error:", e);
      setRotationNotice("Rotation failed: " + (e.message || "Could not rotate PDF."));
    }
    setRotatingEditorPdf(false);
  }, [accessToken, docHash, files, items, resetHistory, rotatingEditorPdf]);

  // Load PDF for editor tools
  const isEditor = toolId === "edit";
  // Day 1: V2 is the default Edit PDF shell on DESKTOP widths. Mobile keeps V1
  // until the dedicated mobile-editor slice (V2's mobile layout is not designed
  // yet). Precedence: explicit opt-out (?editorV2=0 / localStorage "0") -> V1;
  // explicit opt-in (any ?editorV2 param incl. bare, /editorV2 path with the
  // param, or legacy localStorage "1") -> V2 on any device; otherwise default
  // to V2 only on desktop-width viewports.
  const editorV2Flag = (() => {
    try {
      const params = new URLSearchParams(window.location.search);
      if (params.get("editorV2") === "0") return false;
      if (window.localStorage.getItem("nsp_editor_v2") === "0") return false;
      if (params.has("editorV2")) return true;
      if (window.localStorage.getItem("nsp_editor_v2") === "1") return true;
      return Boolean(window.matchMedia && window.matchMedia("(min-width: 900px)").matches);
    } catch (e) {
      return true;
    }
  })();
  const editorV2Active = editorV2Flag && isEditor && files.length > 0;
  useEffect(() => {
    if (suppressNextEditorLoadRef.current) {
      suppressNextEditorLoadRef.current = false;
      return;
    }
    if (isEditor && files.length === 1 && ready) {
      (async () => {
        setLoading(true); const bytes = await readBytes(files[0]); setPdfBytes(bytes);
        window.__nspdf_bytes__ = bytes.slice(0);
        var detectCopy = bytes.slice(0);
        const r = await renderPages(bytes, getEditorPageRenderScale()); setImgs(r.imgs); setDims(r.dims); setPg(0); setSelId(null); setItems([]); setAutoFieldState({}); resetHistory([]); setEditorV2MoreOpen(false); setEditorV2ConfirmAction(null); setEditorV2FitMode("auto"); setZoom(1); setPageAngles([]); setDocHash(null); postRotationDetectRef.current = false; setRotationNotice(null); setEditorMode("fill"); setPlacingTextField(false); setEditSubmode("idle"); setEditMode("text");
        setLoading(false);
        // Client-side AcroForm detection (free — works for interactive PDFs)
        setDetecting(true); setDetectError(null); setLastDetectionTextFound(null);
        try {
          window.__nspdf_bytes__ = detectCopy.slice(0);
          const det = await detectFieldsClient(detectCopy);
          setLastDetectionTextFound(det.textFound);
          if (det.fields.length > 0) {
            const hydratedFields = hydrateDetectedFields(det.fields, autoFieldStateRef.current);
            setItems(hydratedFields);
            resetHistory(hydratedFields);
            setFontSize(det.dominantFontSize);
            captureTypographyTelemetry(hydratedFields, "acroform");
            console.log("[NoStringsPDF] Client AcroForm detected " + det.fields.length + " fields");
          } else {
            // No AcroForm fields — this is a flat PDF, suggest Pro server detection
            setDetectError("flat");
          }
        } catch(e) { setDetectError("Error: " + (e.message || String(e))); console.error("Detection error:", e); }
        setDetecting(false);
      })();
    }
  }, [isEditor, files, ready]);

  // Pro server-side detection handler
  const runServerDetection = async (options = {}) => {
    const fileForDetection = options.file || files[0];
    if (!fileForDetection) return null;
    if (!accessToken) {
      setDetectError("Please log in to use auto-detect.");
      await saveEditorState("pre-auth");
      openLoginModal();
      return null;
    }
    if (!isPro) {
      setDetectError("Auto-detect requires Pro.");
      openUpgradeModal();
      return null;
    }
    setDetecting(true); setDetectError(null);
    try {
      const usePostRotationGrace = Boolean(postRotationDetectRef.current && docHash);
      const graceDocHash = docHash;
      postRotationDetectRef.current = false;
      const det = await detectFieldsServer(fileForDetection, accessToken, {
        postRotation: usePostRotationGrace,
        docHash: graceDocHash,
      });
      if (det.doc_hash) setDocHash(det.doc_hash);
      setPageAngles(det.page_angles || []);
      setRotationNotice(null);
      if (det.fields.length > 0) {
        const manualItems = options.replaceExisting ? [] : items.filter(it => !it.auto);
        const nextItems = [...hydrateDetectedFields(det.fields, autoFieldStateRef.current), ...manualItems];
        setItems(nextItems);
        resetHistory(nextItems);
        setFontSize(10);
        setShowFields(true);
        captureTypographyTelemetry(nextItems, "server");
        console.log("[NoStringsPDF] Server detected " + det.fields.length + " fields via " + det.method);
        setDetecting(false);
        return nextItems;
      } else {
        setDetectError("Advanced auto-detect could not find fields on this document.");
      }
    } catch(e) {
      console.error("Server detection error:", e);
      if (e.doc_hash) setDocHash(e.doc_hash);
      if (e.detail === "pro_subscription_required") {
        setIsPro(false);
        setDetectError("Auto-detect requires Pro.");
        openUpgradeModal();
        setDetecting(false);
        return null;
      }
      if (e.detail === "monthly_limit_exceeded") {
        setUsageLimitModal({
          title: "Monthly auto-detect limit reached",
          message: `You've reached this month's auto-detect limit.${e.resets_at ? " It resets " + new Date(e.resets_at).toLocaleDateString() + "." : ""}`,
        });
        setDetectError("Auto-detect limit reached.");
        setDetecting(false);
        return null;
      }
      if (e.detail === "service_capacity_reached") {
        setUsageLimitModal({
          title: "Auto-detect is temporarily at capacity",
          message: "Auto-detect is temporarily at capacity. Please try again tomorrow.",
        });
        setDetectError("Auto-detect is temporarily at capacity.");
        setDetecting(false);
        return null;
      }
      setDetectError(e.message || "Server error. Is the API running?");
    }
    setDetecting(false);
    return null;
  };

  useEffect(() => {
    if (detectError !== "flat") return;
    if (!isPro || !accessToken) return;
    if (!pdfBytes || detecting || loading) return;
    if (!currentFileKey) return;
    if (items.some(function(it) { return it.auto; })) return;
    if (flatAutoDetectAttemptRef.current === currentFileKey) return;
    flatAutoDetectAttemptRef.current = currentFileKey;
    runServerDetection();
  }, [accessToken, currentFileKey, detectError, detecting, isPro, items, loading, pdfBytes]);

  useEffect(() => {
    if (!restoreRequested || restoreInProgressRef.current || !ready || !accessToken || !isPro) return;
    const state = pendingRestoreStateRef.current;
    if (!state) {
      setRestoreRequested(false);
      return;
    }
    restoreInProgressRef.current = true;
    (async () => {
      try {
        const bytes = base64ToUint8Array(state.pdfData);
        const restoredFile = new File([bytes], state.filename || "restored.pdf", { type: "application/pdf", lastModified: Date.now() });
        suppressNextEditorLoadRef.current = true;
        setView("tool");
        setToolId("edit");
        setFiles([restoredFile]);
        setPdfBytes(bytes);
        window.__nspdf_bytes__ = bytes.slice(0);
        const rendered = await renderPages(bytes, getEditorPageRenderScale());
        setImgs(rendered.imgs);
        setDims(rendered.dims);
        setPg(0);
        setSelId(null);
        setEditingItemId(null);
        setItems([]);
        setAutoFieldState({});
        resetHistory([]);
        setZoom(1);
        setPageAngles([]);
        setDocHash(null);
        postRotationDetectRef.current = false;
        setRotationNotice(null);
        setEditorMode("fill");
        setPlacingTextField(false);
        setEditSubmode("idle");
        setEditMode("text");
        setShowFields(true);
        pendingRestoreApplyRef.current = {
          filledValues: Array.isArray(state.filledValues) ? state.filledValues : [],
          currentPage: Number(state.currentPage || 1),
          totalPages: rendered.imgs.length || 1,
        };
        const hydrated = await runServerDetection({ file: restoredFile, replaceExisting: true });
        if (!hydrated || !hydrated.length) {
          pendingRestoreApplyRef.current = null;
          clearEditorState();
          setRestoreRequested(false);
        }
      } catch (err) {
        console.error("[CONT-1] Restore failed:", err);
        pendingRestoreApplyRef.current = null;
        pendingRestoreStateRef.current = null;
        clearEditorState();
        setRestoreRequested(false);
      } finally {
        restoreInProgressRef.current = false;
      }
    })();
  }, [restoreRequested, ready, accessToken, isPro]);

  useEffect(() => {
    const pending = pendingRestoreApplyRef.current;
    if (!pending || detecting || loading) return;
    const fields = continuityFieldOrder(items).filter(function(it) { return it.auto; });
    if (!fields.length) return;
    let nextItems = items.slice();
    fields.forEach(function(field, i) {
      const savedValue = pending.filledValues[i];
      if (savedValue === undefined || savedValue === "") return;
      if (field.type === "text") {
        nextItems = nextItems.map(function(it) { return it.id === field.id ? { ...it, text: String(savedValue) } : it; });
      } else if (field.type === "radio" && field.groupId) {
        nextItems = nextItems.map(function(it) {
          return it.type === "radio" && it.groupId === field.groupId ? { ...it, checked: it.id === field.id } : it;
        });
      } else if (field.type === "radio" || field.type === "checkbox" || field.type === "check") {
        nextItems = nextItems.map(function(it) { return it.id === field.id ? { ...it, checked: true } : it; });
      }
    });
    nextItems = sanitizeAcroButtonGroups(nextItems);
    setItems(nextItems);
    resetHistory(nextItems);
    setPg(Math.max(0, Math.min((pending.currentPage || 1) - 1, (pending.totalPages || 1) - 1)));
    setEditorMode("fill");
    setSelId(null);
    setEditingItemId(null);
    setEditSubmode("idle");
    pendingRestoreApplyRef.current = null;
    pendingRestoreStateRef.current = null;
    clearEditorState();
    setRestoreRequested(false);
    setRestoreBannerVisible(true);
  }, [items, detecting, loading, continuityFieldOrder, resetHistory, clearEditorState]);

  useEffect(() => {
    if (!restoreBannerVisible) return;
    const timer = window.setTimeout(() => setRestoreBannerVisible(false), 8000);
    return () => window.clearTimeout(timer);
  }, [restoreBannerVisible]);

  useEffect(() => {
    if (!postPlacementHintVisible) return;
    const timer = window.setTimeout(() => setPostPlacementHintVisible(false), 4500);
    return () => window.clearTimeout(timer);
  }, [postPlacementHintVisible]);

  // Reorder/Annotate also needs bytes
  useEffect(() => {
    if (["reorder"].includes(toolId) && files.length === 1 && ready && !pdfBytes) {
      readBytes(files[0]).then(b => setPdfBytes(b));
    }
  }, [toolId, files, ready, pdfBytes]);

  const dim = dims[pg] || dims[pg + 1] || dims[0];
  const editorV2FitWidth = editorV2Active ? Math.max(320, cw - 56) : cw;
  const editorV2FitHeight = editorV2Active && pdfViewportHeight ? Math.max(240, pdfViewportHeight - 36) : null;
  const widthFitScale = dim ? editorV2FitWidth / dim.width : 1;
  const heightFitScale = dim && editorV2FitHeight ? editorV2FitHeight / dim.height : null;
  const editorV2ReadableMaxScale = 1.4;
  const editorV2ReadableFitScale = Math.min(widthFitScale, editorV2ReadableMaxScale);
  const editorV2PageFitScale = heightFitScale ? Math.min(widthFitScale, heightFitScale) : editorV2ReadableFitScale;
  const editorV2WidthFitScale = editorV2ReadableFitScale;
  // Day 1 full-page gate: on load ("auto"), prefer Fit Page whenever the
  // measured workspace height yields a usable page scale, so the whole page is
  // visible without scrolling at proper desktop sizes (1440x900 / 1920x1080).
  // Users zoom in for precise editing. The 0.60 floor only guards genuinely
  // tiny viewports, where Fit Width (internally scrollable) is kinder.
  const editorV2PageFitReadable = Boolean(dim && heightFitScale && editorV2PageFitScale >= 0.60);
  const editorV2ResolvedFitMode = editorV2FitMode === "auto" ? (editorV2PageFitReadable ? "page" : "width") : editorV2FitMode;
  const editorV2BaseFitScale = editorV2ResolvedFitMode === "page" ? editorV2PageFitScale : (editorV2ResolvedFitMode === "width" ? editorV2WidthFitScale : 1);
  const fitScale = dim ? (editorV2Active ? editorV2BaseFitScale : (heightFitScale ? Math.min(widthFitScale, heightFitScale) : widthFitScale)) : 1;
  const scale = fitScale * zoom;
  const editorV2DockAvailable = Boolean(editorV2Active && dim && (cw - dim.width * scale) >= 120);
  const EditorWorkspaceFrame = editorV2Active ? "div" : React.Fragment;
  const editorWorkspaceFrameProps = editorV2Active ? { style: { width: "100%", maxWidth: "1360px", margin: "0 auto", display: "flex", flexDirection: "column", flex: "1 1 0", minHeight: 0, overflow: "hidden" } } : {};

const pageItems = items.filter(function(it) {
  if (itemPageIndex(it) !== pg) return false;

  if (!it.auto) return true;
  if (!showFields) return false;

  return true;
});

const filledCount = items.filter(it => ((it.type === "text" || it.type === "choice") && it.text?.trim()) || it.type === "check" || it.type === "image" || it.type === "highlight" || it.type === "note" || (it.type === "checkbox" && it.checked) || (it.type === "radio" && it.checked)).length;
const hasEditableOverlays = items.some(function(it) { return isDetectedFlatItem(it) || it.auto !== true; });
const reviewFieldsTitle = hasEditableOverlays ? "Review fields to move, resize, or delete detected fields before filling." : "No editable overlays to review. Native PDF fields can be filled but not moved.";
const isToolVisuallyActive = function(modeId) {
  return editMode === modeId && (modeId !== "text" || placingTextField || editorMode === "edit");
};
const selectedItem = selId ? items.find(function(it) { return it.id === selId; }) : null;
const selectedManualItem = selectedItem && selectedItem.auto !== true ? selectedItem : null;
const selectedManualTextItem = selectedManualItem && selectedManualItem.type === "text" ? selectedManualItem : null;

useEffect(() => {
  const isReviewIdle = editMode === "text" && !placingTextField && !placingSign && !placingImg;
  if (editorMode !== "edit" || hasEditableOverlays || !isReviewIdle) return;
  setEditorMode("fill");
  setSelId(null);
  setEditingItemId(null);
  setEditSubmode("idle");
  setPlacingTextField(false);
}, [editorMode, editMode, hasEditableOverlays, placingTextField, placingSign, placingImg]);

useEffect(() => {
  const onManualEditKeyDown = e => {
    if (editorMode !== "edit" && !placingTextField) return;
    const tag = String(e.target?.tagName || "").toLowerCase();
    const isFormControl = tag === "input" || tag === "textarea" || tag === "select" || e.target?.isContentEditable;
    if (e.key === "Enter" && editorMode === "edit" && !isFormControl && selectedManualTextItem && !editingItemId) {
      e.preventDefault();
      setEditingItemId(selectedManualTextItem.id);
      setEditSubmode("typing");
      setManualFocusItemId(selectedManualTextItem.id);
      return;
    }
    if (e.key !== "Escape") return;
    if (editingItemId) {
      e.preventDefault();
      setEditingItemId(null);
      setEditSubmode(selectedManualItem ? "selected" : "idle");
      return;
    }
    if (placingTextField) {
      e.preventDefault();
      setPlacingTextField(false);
      setEditSubmode(editorMode === "edit" && selectedManualItem ? "selected" : "idle");
      return;
    }
    if (editorMode !== "edit") return;
    if (selectedManualItem) {
      e.preventDefault();
      setSelId(null);
      setEditSubmode("idle");
    }
  };
  window.addEventListener("keydown", onManualEditKeyDown);
  return () => window.removeEventListener("keydown", onManualEditKeyDown);
}, [editorMode, editingItemId, placingTextField, selectedManualItem, selectedManualTextItem]);

const focusFieldNearPoint = useCallback((x, y) => {

let best = null;
    let bestScore = Infinity;
    for (const it of pageItems) {
      const pad = it.maxLength === 1 ? 8 : (it.type === "checkbox" || it.type === "radio" ? 6 : 4);
      const left = it.x - pad;
      const top = it.y - pad;
      const right = it.x + (it.width || 0) + pad;
      const bottom = it.y + (it.height || 0) + pad;
      if (x < left || x > right || y < top || y > bottom) continue;
      const cx = it.x + (it.width || 0) / 2;
      const cy = it.y + (it.height || 0) / 2;
      const score = Math.abs(x - cx) + Math.abs(y - cy) + (it.maxLength === 1 ? -3 : 0) + (it.type === "radio" ? -1000 : 0);
      if (score < bestScore) { best = it; bestScore = score; }
    }
    if (!best) return false;
    setSelId(best.id);
    if (best.acroButtonGroupId) {
      setItems(function(p) { return applyButtonToggle(p, best, !best.checked); });
    } else if (best.type === "radio" && best.groupId) {
      setItems(p => p.map(it => it.type === "radio" && it.groupId === best.groupId ? { ...it, checked: it.id === best.id } : it));
    } else if (best.type === "radio") {
      setItems(p => p.map(it => it.id === best.id ? { ...it, checked: !it.checked } : it));
    } else if (best.type === "checkbox") {
      setItems(p => p.map(it => it.id === best.id ? { ...it, checked: !it.checked } : it));
    } else if (best.type === "text") {
      setEditingItemId(best.id);
    }
    return true;
  }, [pageItems]);

  // ─── CANVAS CLICK ───
  const handleCanvasClick = useCallback((e) => {
    if (window.__nspdfSuppressCanvasClickUntil && Date.now() < window.__nspdfSuppressCanvasClickUntil) return;
    if (e.target.closest("[data-item]")) return; // clicked on an item
    const r = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - r.left) / scale; const y = (e.clientY - r.top) / scale;
    const id = Date.now().toString();

    if (editorMode === "edit" && editingItemId) {
      setSelId(null);
      setEditingItemId(null);
      setEditSubmode("idle");
      return;
    }

    // Clear any editing state first
    setSelId(null); setEditingItemId(null);
    if (editorMode === "edit") setEditSubmode("idle");

    const isFillManualPlacementTool = ["check", "sign", "image", "highlight", "note"].includes(editMode);
    if (editorMode === "fill" && !placingTextField && !isFillManualPlacementTool) {
      focusFieldNearPoint(x, y);
      return;
    }

    if (placingSign && signatureImg) {
      setItems(p => [...p, { id, page: pg, x, y: y - 25, width: 150, height: 50, type: "image", src: signatureImg }]);
      setPlacingSign(false); return;
    }
    if (placingImg && addImgSrc) {
      setItems(p => [...p, { id, page: pg, x, y, width: 120, height: 120, type: "image", src: addImgSrc }]);
      setPlacingImg(false); return;
    }

    if (placingTextField) {
      return;
    } else if (editMode === "check") {
      setItems(p => [...p, { id, page: pg, x, y, type: "check", fontSize: 14 }]);
      setSelId(id);
    } else if (editMode === "sign") {
      if (!signatureImg) {
        if (savedSignatures.length > 0) {
          setSignatureCreationRequested(false);
          setShowSignPad(false);
        } else {
          setSignatureCreationRequested(false);
          setShowSignPad(true);
        }
      } else {
        setPlacingSign(true);
      }
    } else if (editMode === "highlight") {
      setItems(p => [...p, { id, page: pg, x, y: y - 5, width: 100, height: 10, type: "highlight" }]);
      setSelId(id);
    } else if (editMode === "note") {
      const txt = prompt("Add a note:");
      if (txt) setItems(p => [...p, { id, page: pg, x, y, type: "note", text: txt }]);
    } else if (editMode === "image") {
      const inp = document.createElement("input"); inp.type = "file"; inp.accept = "image/*";
      inp.onchange = async () => { const f = inp.files[0]; if (f) { const url = await readDataURL(f); setItems(p => [...p, { id, page: pg, x, y, width: 120, height: 120, type: "image", src: url }]); } };
      inp.click();
    }
  }, [editorMode, editingItemId, selId, pg, scale, placingTextField, editMode, placingSign, signatureImg, placingImg, addImgSrc, focusFieldNearPoint, items, savedSignatures]);

  const handleCanvasDoubleClick = useCallback((e) => {
    if (editorMode !== "fill") return;
    if (e.target.closest("[data-item]")) return;
    if (placingSign || placingImg) return;
    e.preventDefault();
    e.stopPropagation();
    const r = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - r.left) / scale;
    const y = (e.clientY - r.top) / scale;
    const id = Date.now().toString();
    const boxW = 200;
    const boxH = 16;
    const nextField = { id, page: pg, x: Math.max(0, x - 4), y: Math.max(0, y - boxH / 2), width: boxW, height: boxH, fontSize: getDefaultFieldFontSize({ height: boxH }), text: "", type: "text" };
    const nextItems = [...items, nextField];
    setItems(nextItems);
    setSelId(id);
    setEditingItemId(id);
    captureTypographyTelemetry(nextItems, "manual_fill_doubletap");
    setPostPlacementHintVisible(true);
  }, [editorMode, pg, scale, items, placingSign, placingImg]);

  // ─── SAVE EDITOR ───
  const saveEditor = async () => {
    setSaving(true);
    try {
      const { PDFDocument, PDFName, rgb, StandardFonts, degrees } = window.PDFLib;
      const { pushGraphicsState, popGraphicsState, rectangle, clip, endPath } = window.PDFLib;
      const doc = await PDFDocument.load(pdfBytes);
      const fontRegistry = {
        Helvetica: await doc.embedFont(StandardFonts.Helvetica),
        HelveticaBold: await doc.embedFont(StandardFonts.HelveticaBold),
        Courier: await doc.embedFont(StandardFonts.Courier),
        ZapfDingbats: await doc.embedFont(StandardFonts.ZapfDingbats),
      };
      // Resolve which embedded font this field should use based on
      // its /DA-derived appearance metadata (Step 3 propagation).
      // Falls back to Helvetica if no appearance metadata is present.
      function getFontForField(it) {
        if (!it.acroAppearance) return fontRegistry.Helvetica;
        const resolved = resolveAppearance(it.acroAppearance, {
          width: it.width || 0,
          height: it.height || 0,
        });
        return fontRegistry[resolved.pdfLibFont] || fontRegistry.Helvetica;
      }
      function barePdfName(value) {
        return String(value || "").replace(/^\//, "");
      }
      function acroButtonOnValue(it) {
        return barePdfName(it.acroOnValue || it.acroExportValue || it.acroButtonValue || "On");
      }
      function widgetOnValue(widget) {
        try {
          if (widget && typeof widget.getOnValue === "function") return barePdfName(widget.getOnValue());
        } catch (e) {}
        return "";
      }
      function setCheckBoxExactValue(checkBox, value) {
        const selected = barePdfName(value);
        const widgets = checkBox.acroField.getWidgets ? checkBox.acroField.getWidgets() : [];
        if (!selected || selected === "Off") {
          checkBox.uncheck();
          widgets.forEach(widget => widget.dict.set(PDFName.of("AS"), PDFName.of("Off")));
          return;
        }
        checkBox.acroField.dict.set(PDFName.of("V"), PDFName.of(selected));
        widgets.forEach(widget => {
          const on = widgetOnValue(widget);
          widget.dict.set(PDFName.of("AS"), PDFName.of(on === selected ? selected : "Off"));
        });
      }
      function drawCheckMark(page, x, y, size, color) {
        const thickness = Math.max(1, size * 0.11);
        page.drawLine({
          start: { x: x + size * 0.12, y: y + size * 0.42 },
          end: { x: x + size * 0.38, y: y + size * 0.16 },
          thickness,
          color
        });
        page.drawLine({
          start: { x: x + size * 0.38, y: y + size * 0.16 },
          end: { x: x + size * 0.88, y: y + size * 0.82 },
          thickness,
          color
        });
      }
      function drawTextSafe(page, text, options) {
        try {
          page.drawText(String(text || ""), options);
          return true;
        } catch (e) {
          try {
            const printable = String(text || "").replace(/[^\x09\x0A\x0D\x20-\x7E]/g, "");
            if (printable) page.drawText(printable, options);
            return Boolean(printable);
          } catch (e2) {
            return false;
          }
        }
      }
      function normalizeExportRotationAngle(angle) {
        const normalized = normalizeRotationAngle(angle);
        if (normalized === 90 || normalized === 180 || normalized === 270) return normalized;
        return 0;
      }
      function getVisualScaleForRotation(pageW, pageH, visualW, visualH, rotation) {
        if (rotation === 90 || rotation === 270) {
          return {
            x: pageH / Math.max(visualW || pageH, 1),
            y: pageW / Math.max(visualH || pageW, 1)
          };
        }
        return {
          x: pageW / Math.max(visualW || pageW, 1),
          y: pageH / Math.max(visualH || pageH, 1)
        };
      }
      function mapVisualBoxToPdfDrawBox(box, pageW, pageH, rotation) {
        const x = Number(box.x) || 0;
        const y = Number(box.y) || 0;
        const w = Number(box.w) || 0;
        const h = Number(box.h) || 0;
        if (rotation === 90) return { anchorX: y + h, anchorY: x, width: w, height: h, rotateDeg: 90 };
        if (rotation === 180) return { anchorX: pageW - x, anchorY: y + h, width: w, height: h, rotateDeg: 180 };
        if (rotation === 270) return { anchorX: pageW - (y + h), anchorY: pageH - x, width: w, height: h, rotateDeg: 270 };
        return { anchorX: x, anchorY: pageH - (y + h), width: w, height: h, rotateDeg: 0 };
      }
      function rotateLocalPoint(drawBox, localX, localY) {
        const rad = (drawBox.rotateDeg || 0) * Math.PI / 180;
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);
        return {
          x: drawBox.anchorX + localX * cos - localY * sin,
          y: drawBox.anchorY + localX * sin + localY * cos
        };
      }
      function visualLocalPoint(drawBox, localX, localYFromTop) {
        return rotateLocalPoint(drawBox, localX, drawBox.height - localYFromTop);
      }
      function drawLineLocal(page, drawBox, start, end, options) {
        page.drawLine({
          start: visualLocalPoint(drawBox, start.x, start.y),
          end: visualLocalPoint(drawBox, end.x, end.y),
          thickness: options.thickness,
          color: options.color
        });
      }
      function drawCheckMarkLocal(page, drawBox, offsetX, baselineFromTop, size, color) {
        const thickness = Math.max(1, size * 0.11);
        drawLineLocal(page, drawBox,
          { x: offsetX + size * 0.12, y: baselineFromTop - size * 0.42 },
          { x: offsetX + size * 0.38, y: baselineFromTop - size * 0.16 },
          { thickness, color });
        drawLineLocal(page, drawBox,
          { x: offsetX + size * 0.38, y: baselineFromTop - size * 0.16 },
          { x: offsetX + size * 0.88, y: baselineFromTop - size * 0.82 },
          { thickness, color });
      }
      function drawTextLocal(page, drawBox, text, localX, localY, options) {
        const pt = rotateLocalPoint(drawBox, localX, localY);
        return drawTextSafe(page, text, {
          ...options,
          x: pt.x,
          y: pt.y,
          rotate: degrees(drawBox.rotateDeg || 0)
        });
      }
      function getChoiceFieldForExport(fieldName) {
        try {
          return form.getDropdown(fieldName);
        } catch (dropdownErr) {
          if (typeof form.getOptionList === "function") {
            return form.getOptionList(fieldName);
          }
          throw dropdownErr;
        }
      }
      function clearChoiceFieldForExport(choiceField) {
        if (choiceField && typeof choiceField.clear === "function") {
          choiceField.clear();
          return;
        }
        const dict = choiceField && choiceField.acroField && choiceField.acroField.dict;
        if (!dict || typeof dict.delete !== "function") {
          throw new Error("Choice field does not support clearing");
        }
        dict.delete(PDFName.of("V"));
        dict.delete(PDFName.of("I"));
      }
      function writeChoiceFieldForExport(choiceField, value) {
        const choiceValue = String(value || "");
        if (choiceValue.trim()) {
          choiceField.select(choiceValue);
          return "selected";
        }
        clearChoiceFieldForExport(choiceField);
        return "cleared";
      }
      const pages = doc.getPages();
      const form = doc.getForm();
      const acroformExportDebug = [];
      let acroformNativeExportUsed = false;
      const exportItems = sanitizeAcroButtonGroups(items);
      const acroButtonExportState = {};
      exportItems.forEach(it => {
        if (!(it.source === "acroform" && it.acroFieldName && it.type === "checkbox")) return;
        const key = it.acroFieldName;
        if (!acroButtonExportState[key]) acroButtonExportState[key] = { items: [], checked: null, processed: false };
        acroButtonExportState[key].items.push(it);
        if (it.checked && !acroButtonExportState[key].checked) acroButtonExportState[key].checked = it;
      });
      if (window.__NSPDF_EXPORT_DEBUG) {
        const acroformItems = exportItems.filter(it => it.source === "acroform" && it.acroFieldName);
        const overlayItems = exportItems.filter(it => !(it.source === "acroform" && it.acroFieldName));
        console.log("[ExportRouting]", {
          acroformItems: acroformItems.length,
          overlayItems: overlayItems.length,
          acroformNames: acroformItems.map(it => ({ name: it.acroFieldName, type: it.type, hasText: !!it.text, checked: it.checked }))
        });
      }
      for (const it of exportItems) {
        const pageIndex = itemPageIndex(it);
        const p = pages[pageIndex];
        if (!p) continue;
        const { height, width } = p.getSize();
        const d = dims[pageIndex];
        if (!d) continue;
        const rotation = normalizeExportRotationAngle(p.getRotation().angle);
        const visualScale = getVisualScaleForRotation(width, height, d.width, d.height, rotation);
        const sx = visualScale.x; const sy = visualScale.y;
        const fontScale = Math.min(sx, sy);
        const drawBoxForItem = function(item, box) {
          return mapVisualBoxToPdfDrawBox({
            x: (box?.x ?? item.x) * sx,
            y: (box?.y ?? item.y) * sy,
            w: ((box?.w ?? item.width) || 0) * sx,
            h: ((box?.h ?? item.height) || 0) * sy
          }, width, height, rotation);
        };
        // Native AcroForm export path. Non-AcroForm items keep the overlay path below.
        if (it.source === "acroform" && it.acroFieldName) {
          try {
            if (it.type === "choice" && it.text != null) {
              const choiceField = getChoiceFieldForExport(it.acroFieldName);
              const choiceExportAction = writeChoiceFieldForExport(choiceField, it.text);
              if (typeof choiceField.updateAppearances === "function") {
                choiceField.updateAppearances(getFontForField(it));
              }
              acroformNativeExportUsed = true;
              if (window.__NSPDF_EXPORT_DEBUG) {
                acroformExportDebug.push({
                  name: it.acroFieldName,
                  type: it.type,
                  value: it.text,
                  action: choiceExportAction,
                  appearanceRegenerated: "pending form.updateFieldAppearances(font)"
                });
              }
              continue;
            } else if (it.type === "text" && it.text != null) {
              const textField = form.getTextField(it.acroFieldName);
              textField.setText(String(it.text || ""));
              textField.updateAppearances(getFontForField(it));
              acroformNativeExportUsed = true;
              if (window.__NSPDF_EXPORT_DEBUG && String(it.text || "")) {
                acroformExportDebug.push({
                  name: it.acroFieldName,
                  type: it.type,
                  value: textField.getText(),
                  acroMaxLen: it.acroMaxLen,
                  acroComb: it.acroComb,
                  appearanceRegenerated: "pending form.updateFieldAppearances(font)"
                });
              }
              continue;
            } else if (it.type === "checkbox") {
              const buttonState = acroButtonExportState[it.acroFieldName];
              if (buttonState && buttonState.processed) {
                acroformNativeExportUsed = true;
                continue;
              }
              if (buttonState) buttonState.processed = true;
              const selectedButton = buttonState ? buttonState.checked : (it.checked ? it : null);
              const selectedOnValue = selectedButton ? acroButtonOnValue(selectedButton) : "Off";
              if (it.acroButtonType === "radio" || it.acroIsRadio) {
                const radioGroupName = String(it.acroFieldName || "").replace(/\[\d+\]$/, "");
                if (selectedButton) {
                  const radioGroup = form.getRadioGroup(radioGroupName);
                  radioGroup.select(selectedOnValue);
                }
              } else {
                const checkBox = form.getCheckBox(it.acroFieldName);
                setCheckBoxExactValue(checkBox, selectedOnValue);
              }
              acroformNativeExportUsed = true;
              if (window.__NSPDF_EXPORT_DEBUG) {
                acroformExportDebug.push({
                  name: it.acroFieldName,
                  type: it.type,
                  checked: Boolean(selectedButton),
                  selectedOnValue: selectedOnValue,
                  acroMaxLen: it.acroMaxLen,
                  acroComb: it.acroComb,
                  appearanceRegenerated: "pending form.updateFieldAppearances(font)"
                });
              }
              continue;
            }
          } catch (err) {
            console.warn("[AcroForm native export] field '" + it.acroFieldName + "' fell back to overlay:", err.message);
          }
        }
        if ((it.type === "text" || it.type === "choice") && it.text?.trim()) {
          const layout = getTextLayout(it);
          const drawBox = drawBoxForItem(it);
          const minFs = 6 * fontScale;
          const lines = String(it.text || "").replace(/\r/g, "").split("\n");
          let fs = layout.fontSize * fontScale;
          const innerW = Math.max(1, drawBox.width - (layout.paddingX * sx * 2));
          const longest = lines.reduce((max, line) => line.length > max.length ? line : max, "");
          const measuredWidth = fontRegistry.Helvetica.widthOfTextAtSize ? fontRegistry.Helvetica.widthOfTextAtSize(longest, fs) : longest.length * fs * 0.54;
          if (measuredWidth > innerW) fs = Math.max(minFs, fs * (innerW / Math.max(measuredWidth, 1)));
          const textHeight = fontRegistry.Helvetica.heightAtSize ? fontRegistry.Helvetica.heightAtSize(fs) : fs;
          const lineGap = Math.max(fs * 1.15, fs);
          const blockHeight = Math.max(textHeight, lines.length * lineGap);
          const baseLocalY = Math.max(0.5, (drawBox.height - blockHeight) / 2);
          const canClipText = rotation === 0 && Boolean(pushGraphicsState && rectangle && clip && endPath && popGraphicsState);
          try {
            if (canClipText) {
              p.pushOperators(pushGraphicsState(), rectangle(drawBox.anchorX, drawBox.anchorY, drawBox.width, drawBox.height), clip(), endPath());
            }
            lines.forEach((l, li) => {
              try {
                let printable = l;
                if (!canClipText && fontRegistry.Helvetica.widthOfTextAtSize) {
                  while (printable.length && fontRegistry.Helvetica.widthOfTextAtSize(printable, fs) > innerW) {
                    printable = printable.slice(0, -1);
                  }
                }
                const textWidth = fontRegistry.Helvetica.widthOfTextAtSize ? fontRegistry.Helvetica.widthOfTextAtSize(printable, fs) : printable.length * fs * 0.56;
                const localX = it.maxLength === 1
                  ? Math.max(0.5, (drawBox.width - textWidth) / 2)
                  : Math.max(layout.paddingX * sx, 1 * sx);
                const localY = baseLocalY + Math.max(0, (lines.length - li - 1) * lineGap);
                drawTextLocal(p, drawBox, printable, localX, localY, { size: fs, font: fontRegistry.Helvetica, color: rgb(0.1, 0.1, 0.1) });
              } catch(e) {}
            });
          } finally {
            if (canClipText) p.pushOperators(popGraphicsState());
          }
        } else if (it.type === "checkbox" && it.checked) {
          const drawBox = drawBoxForItem(it);
          const cs = Math.min(drawBox.width, drawBox.height) * 0.7;
          try { drawCheckMarkLocal(p, drawBox, drawBox.width * 0.15, drawBox.height * 0.85, cs, rgb(0.1, 0.1, 0.1)); } catch(e) {}
        } else if (it.type === "radio" && it.checked) {
          const drawBox = drawBoxForItem(it);
          const radius = Math.min(Math.min(drawBox.width, drawBox.height) / 3, 5 * fontScale);
          const center = visualLocalPoint(drawBox, drawBox.width / 2, drawBox.height / 2);
          try { p.drawCircle({ x: center.x, y: center.y, size: radius, color: rgb(1, 0.415, 0) }); } catch(e) {}
        } else if (it.type === "check") {
          const checkSize = 14 * fontScale;
          const drawBox = drawBoxForItem(it, { x: it.x, y: it.y, w: 14, h: 14 });
          drawCheckMarkLocal(p, drawBox, 0, drawBox.height, checkSize, rgb(0.1, 0.1, 0.1));
        } else if (it.type === "highlight") {
          const drawBox = drawBoxForItem(it);
          p.drawRectangle({ x: drawBox.anchorX, y: drawBox.anchorY, width: drawBox.width, height: drawBox.height, rotate: degrees(drawBox.rotateDeg), color: rgb(1, 0.9, 0), opacity: 0.35 });
        } else if (it.type === "note") {
          const noteWidth = (it.text.length * 5 + 16) * sx;
          const drawBox = drawBoxForItem(it, { x: it.x - 2 / sx, y: it.y - 4 / sy, w: noteWidth / sx, h: 18 / sy });
          p.drawRectangle({ x: drawBox.anchorX, y: drawBox.anchorY, width: drawBox.width, height: drawBox.height, rotate: degrees(drawBox.rotateDeg), color: rgb(1, 0.95, 0.7) });
          drawTextLocal(p, drawBox, it.text, 4 * sx, 4 * sy, { size: 9 * fontScale, font: fontRegistry.Helvetica, color: rgb(0.2, 0.2, 0.2) });
        } else if (it.type === "image" && it.src) {
          try {
            const resp = await fetch(it.src); const buf = await resp.arrayBuffer();
            let img;
            try { img = await doc.embedPng(buf); } catch { img = await doc.embedJpg(buf); }
            const drawBox = drawBoxForItem(it);
            p.drawImage(img, { x: drawBox.anchorX, y: drawBox.anchorY, width: drawBox.width, height: drawBox.height, rotate: degrees(drawBox.rotateDeg) });
          } catch (e) { console.log("Image embed failed", e); }
        }
      }
      if (acroformNativeExportUsed) form.updateFieldAppearances(fontRegistry.Helvetica);
      if (window.__NSPDF_EXPORT_DEBUG && acroformExportDebug.length) {
        console.table(acroformExportDebug.map(row => ({ ...row, appearanceRegenerated: "form.updateFieldAppearances(font) called before save" })));
      }
      const o = await doc.save();
      await downloadOrSharePdf(new Blob([o], { type: "application/pdf" }), "edited_" + files[0].name);
    } catch (e) { alert("Error: " + e.message); }
    setSaving(false);
  };

  const selectCompressWorkflow = (workflow) => {
    setError(null);
    setResult(null);
    setDlData(null);
    setCompressProgress(null);
    setCompressBatchProgress(null);
    if (workflow === "batch" && !hasCompressBatchAccess) {
      setCompressWorkflow("single");
      openUpgradeModal("compress-batch");
      return;
    }
    setCompressWorkflow(workflow);
    if (workflow === "batch") setCompressBatchOpen(true);
  };

  const addCompressBatchFiles = (incomingFiles) => {
    const incoming = Array.from(incomingFiles || []);
    const pdfs = incoming.filter(f => f?.type === "application/pdf" || /\.pdf$/i.test(f?.name || ""));
    const rejected = incoming.length - pdfs.length;
    setCompressBatchFiles(prev => [...prev, ...pdfs].slice(0, PRO_BATCH_FILE_LIMIT));
    setResult(null);
    setDlData(null);
    setCompressBatchProgress(null);
    if (rejected) setError("Only PDF files can be added to batch compression.");
    else if (incoming.length + compressBatchFiles.length > PRO_BATCH_FILE_LIMIT) setError(`Batch compression supports up to ${PRO_BATCH_FILE_LIMIT} PDFs at once.`);
    else setError(null);
  };
  const removeCompressBatchFileAt = (index) => {
    setCompressBatchFiles(prev => prev.filter((_, i) => i !== index));
    setResult(null);
    setDlData(null);
    setCompressBatchProgress(null);
    setError(null);
  };
  const openCompressBatch = () => {
    setError(null);
    if (!hasCompressBatchAccess) {
      openUpgradeModal("compress-batch");
      return;
    }
    setCompressBatchOpen(true);
  };
  const processCompressBatch = async () => {
    if (!hasCompressBatchAccess) {
      openUpgradeModal("compress-batch");
      return;
    }
    if (compressBatchFiles.length < 2) {
      setError("Choose at least two PDFs for batch compression.");
      return;
    }
    setPr(true);
    setResult(null);
    setDlData(null);
    setError(null);
    setCompressProgress(null);
    setCompressBatchProgress(null);
    try {
      const preset = COMPRESS_SCAN_PRESETS[compressBatchPreset] || COMPRESS_SCAN_PRESETS.recommended;
      const r = await compressPDFsAsScanBatch(compressBatchFiles, preset, progress => setCompressBatchProgress(progress));
      setCompressBatchProgress(null);
      setDlData({ blob: r.blob, name: r.name });
      setResult({ t: "compress-batch", count: r.successCount, failed: r.failureCount, fileTotal: r.fileTotal, orig: r.totalInputBytes, now: r.totalOutputBytes, red: r.totalReduction, reducedCount: r.reducedCount, files: r.fileResults, preset: preset.label });
      setTasks(p => p + 1);
    } catch (e) {
      setError(String(e?.message || e || "Batch compression failed."));
    }
    setCompressBatchProgress(null);
    setPr(false);
  };

  // Standard process
  const process = async () => {
    if (!files.length || !tool) return; setPr(true); setResult(null); setError(null); setDlData(null); setCompressProgress(null); setCompressBatchProgress(null);
    try {
      let r;
      switch (toolId) {
        case "merge": r = await mergePDFs(files); setDlData({ blob: r.blob, name: "merged.pdf" }); setResult({ t: "merge", pages: r.pages, size: r.blob.size }); break;
        case "compress": {
          if (compressMode === "scan") {
            const preset = COMPRESS_SCAN_PRESETS[compressPreset] || COMPRESS_SCAN_PRESETS.recommended;
            r = await compressPDFAsScans(files[0], preset, progress => setCompressProgress(progress));
            setCompressProgress(null);
            setDlData({ blob: r.blob, name: compressedFileName(files[0]) });
            setResult({ t: "compress", mode: "scan", preset: preset.label, orig: r.origSize, now: r.newSize, red: r.reduction, reduced: r.reduced, returnedOriginal: r.returnedOriginal, optimizedSize: r.optimizedSize });
          } else {
            r = await compressPDF(files[0]);
            setDlData({ blob: r.blob, name: "compressed_" + files[0].name });
            setResult({ t: "compress", mode: "standard", orig: r.origSize, now: r.newSize, red: r.reduction, reduced: r.reduced, returnedOriginal: r.returnedOriginal, optimizedSize: r.optimizedSize });
          }
          break;
        }
        case "split": {
          if (splitMode === "visual") {
            if (!splitPoints.length) throw new Error("Choose at least one split point.");
            if (!splitPageCount) throw new Error("Wait for page thumbnails to finish loading.");
            r = await splitPDFWithGroups(files[0], splitGroupsFromSplitPoints(splitPoints, splitPageCount));
          } else {
            r = await splitPDF(files[0], splitIn);
          }
          setDlData(r.files.length === 1 ? { blob: r.files[0].blob, name: r.files[0].name } : { blob: r.zipBlob, name: r.zipName }); setResult({ t: "split", total: r.totalPages, out: r.files.length, zip: r.files.length > 1 }); break;
        }
        case "convert": r = await imagesToPDF(files); setDlData({ blob: r.blob, name: "converted.pdf" }); setResult({ t: "convert", pages: r.pages, size: r.blob.size }); break;
        case "rotate": r = await rotatePDF(files[0], 90); setDlData({ blob: r.blob, name: "rotated_" + files[0].name }); setResult({ t: "rotate" }); break;
        case "pagenums": r = await addPageNumbers(files[0], pageNumPos, 1, pageNumSize); setDlData({ blob: r.blob, name: "numbered_" + files[0].name }); setResult({ t: "pagenums", pages: r.pages }); break;
        case "watermark": r = await addWatermark(files[0], wmText || "DRAFT", wmSize, wmOpacity); setDlData({ blob: assertPdfBlob(r.blob, "Watermark output could not be created. Please try another PDF."), name: watermarkFileName(files[0]) }); setResult({ t: "watermark" }); break;
        case "pdftoimg": const bytes2 = await readBytes(files[0]); r = await pdfToImages(bytes2, files[0]); setDlData(r.files.length === 1 ? { blob: r.files[0].blob, name: r.files[0].name } : { blob: r.zipBlob, name: r.zipName }); setResult({ t: "pdftoimg", pages: r.pages, images: r.files.length, zip: r.files.length > 1 }); break;
        case "unlock": r = await unlockPDF(files[0]); setDlData({ blob: r.blob, name: "unlocked_" + files[0].name }); setResult({ t: "unlock" }); break;
      }
      setTasks(p => p + 1);
    } catch (e) { setError(formatStandardToolError(e, toolId)); }
    setCompressProgress(null);
    setPr(false);
  };
  const download = async () => {
    if (!dlData) return;
    const saveBlob = async (blob, name) => {
      const isPdf = blob?.type === "application/pdf" || /\.pdf$/i.test(name || "");
      if (isPdf) await downloadOrSharePdf(blob, name);
      else dlB(blob, name);
    };
    if (dlData.multi) {
      for (const f of dlData.multi) await saveBlob(f.blob, f.name);
    } else {
      await saveBlob(dlData.blob, dlData.name);
      addToRecent(dlData.name, toolId);
    }
  };

  const renderRes = () => {
    if (!result || !tool) return null; const a = tool.accent; let inner;
    switch (result.t) {
      case "compress": {
        const noReductionCopy = result.mode === "scan" ? "High compression did not make this file smaller - original file kept" : "Already optimized - no further reduction";
        inner = <><div style={{ display: "flex", alignItems: "center", gap: "14px", justifyContent: "center", marginBottom: "14px" }}><div><div style={{ fontSize: "10px", color: "#AAA", letterSpacing: "1px", fontWeight: 600 }}>BEFORE</div><div style={{ fontSize: "18px", fontWeight: 700, color: result.reduced ? "#CCC" : "#777", textDecoration: result.reduced ? "line-through" : "none" }}>{fmtB(result.orig)}</div></div><div style={{ fontSize: "20px", color: result.reduced ? a : "#AAA" }}>→</div><div><div style={{ fontSize: "10px", color: "#AAA", letterSpacing: "1px", fontWeight: 600 }}>AFTER</div><div style={{ fontSize: "18px", fontWeight: 700, color: result.reduced ? a : "#777" }}>{fmtB(result.now)}</div></div></div><div style={{ background: result.reduced ? `${a}10` : "#F7F7F7", borderRadius: "8px", padding: "8px", textAlign: "center", color: result.reduced ? a : "#777", fontWeight: 700, fontSize: "13px" }}>{result.reduced ? `Compressed from ${fmtB(result.orig)} to ${fmtB(result.now)} - ${result.red}% smaller` : noReductionCopy}</div>{result.mode === "scan" && <div style={{ marginTop: "8px", fontSize: "10px", lineHeight: 1.45, textAlign: "center", color: "#777" }}>High compression rebuilds pages as images, so text may no longer be selectable and form fields may no longer be editable.</div>}</>;
        break;
      }
      case "compress-batch": {
        const reducedCopy = result.reducedCount > 0
          ? `${result.reducedCount} of ${result.count} PDF${result.count === 1 ? "" : "s"} got smaller.`
          : "No PDFs got smaller; originals were kept where needed.";
        inner = (
          <div style={{ color: a }}>
            <div style={{ textAlign: "center", fontSize: "16px", fontWeight: 800, marginBottom: "8px" }}>📦 {result.count} compressed PDF{result.count === 1 ? "" : "s"} ready in a ZIP</div>
            <div style={{ textAlign: "center", fontSize: "12px", lineHeight: 1.45, color: "#666" }}>{reducedCopy} {result.failed ? `${result.failed} file${result.failed === 1 ? "" : "s"} could not be processed.` : ""}</div>
            <div style={{ marginTop: "10px", padding: "10px", borderRadius: "10px", background: `${a}10`, color: a, fontSize: "12px", fontWeight: 900, textAlign: "center" }}>Total: {fmtB(result.orig)} → {fmtB(result.now)} · {result.red}% smaller</div>
            {result.files?.length > 0 && (
              <div data-compress-batch-result-files style={{ marginTop: "10px", display: "grid", gap: "6px", textAlign: "left" }}>
                {result.files.map((file, i) => {
                  const ok = file.status === "success";
                  const reduction = ok && file.reduced ? `${file.reduction}% smaller` : "Original kept";
                  return (
                    <div key={`${file.originalName || file.name || i}-${i}`} style={{ padding: "8px 9px", borderRadius: "9px", border: ok ? "1px solid #E3F5EA" : "1px solid #FDDDD4", background: ok ? "#FBFFFC" : "#FFF7F5" }}>
                      <div style={{ fontSize: "11px", color: "#1A1A1A", fontWeight: 900, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{file.originalName || file.name}</div>
                      <div style={{ marginTop: "2px", fontSize: "10px", color: ok ? "#55705D" : "#B45309", lineHeight: 1.4, fontWeight: 700 }}>
                        {ok ? `${fmtB(file.inputBytes)} → ${fmtB(file.outputBytes)} · ${reduction}` : `Could not process · ${file.error || "Unknown error"}`}
                      </div>
                    </div>
                  );
                })}
              </div>
            )}
            <div style={{ marginTop: "8px", fontSize: "10px", lineHeight: 1.45, color: "#777", textAlign: "center" }}>Batch High Compression rebuilds pages as images, so text may no longer be selectable and form fields may no longer be editable.</div>
          </div>
        );
        break;
      }
      case "merge": inner = <div style={{ textAlign: "center", fontSize: "16px", fontWeight: 700, color: a }}>📎 {result.pages} pages merged · {fmtB(result.size)}</div>; break;
      case "split": inner = <div style={{ textAlign: "center", fontSize: "16px", fontWeight: 700, color: a }}>✂️ {result.out} file{result.out > 1 ? "s" : ""} ready</div>; break;
      case "convert": inner = <div style={{ textAlign: "center", fontSize: "16px", fontWeight: 700, color: a }}>↔️ {result.pages} pages · {fmtB(result.size)}</div>; break;
      case "rotate": inner = <div style={{ textAlign: "center", fontSize: "16px", fontWeight: 700, color: a }}>🔄 Rotated 90°</div>; break;
      case "pagenums": inner = <div style={{ textAlign: "center", fontSize: "16px", fontWeight: 700, color: a }}>🔢 Page numbers added to {result.pages} pages</div>; break;
      case "watermark": inner = <div style={{ textAlign: "center", fontSize: "16px", fontWeight: 700, color: a }}>💧 Watermark applied</div>; break;
      case "pdftoimg": inner = <div style={{ textAlign: "center", fontSize: "16px", fontWeight: 700, color: a }}>🖼️ {result.images || result.pages} image{(result.images || result.pages) > 1 ? "s" : ""} ready</div>; break;
      case "unlock": inner = <div style={{ textAlign: "center", color: a }}><div style={{ fontSize: "16px", fontWeight: 700, marginBottom: "6px" }}>🔓 Unlock attempt complete</div><div style={{ fontSize: "11px", lineHeight: 1.45, color: "#777" }}>Creates an unlocked copy where supported. Cannot recover unknown passwords; some encrypted PDFs may still require the correct password.</div></div>; break;
    }
    const downloadLabel = result.t === "split" ? (result.out > 1 ? "↓ Download ZIP" : "↓ Download PDF") : result.t === "pdftoimg" ? (result.zip ? "↓ Download ZIP" : "↓ Download PNG") : result.t === "compress-batch" ? "↓ Download ZIP" : "↓ Download PDF";
    return <div style={{ background: "white", borderRadius: "14px", padding: "20px", border: `1px solid ${a}20`, boxShadow: `0 6px 24px ${a}08`, marginTop: "12px" }}><div style={{ fontSize: "10px", fontWeight: 700, letterSpacing: "2px", color: a, marginBottom: "14px", textAlign: "center" }}>✓ DONE</div>{inner}<button onClick={download} style={{ width: "100%", marginTop: "16px", padding: "14px", background: a, color: "white", border: "none", borderRadius: "12px", fontSize: "14px", fontWeight: 700, cursor: "pointer" }}>{downloadLabel}</button></div>;
  };

  const isStd = ["merge", "compress", "split", "convert", "rotate", "pagenums", "watermark", "pdftoimg", "unlock"].includes(toolId);

  return (
    <div ref={cRef} style={{ fontFamily: "'Satoshi', sans-serif", background: "#FFF", minHeight: "100vh", height: editorV2Active ? "100vh" : undefined, overflow: editorV2Active ? "hidden" : undefined, WebkitTapHighlightColor: "transparent" }}>
      <link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
      <style>{`@keyframes nsFU{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes nsS{to{transform:rotate(360deg)}}@keyframes nsSt{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}@keyframes nsSn{0%{transform:rotate(0)}25%{transform:rotate(-8deg)}50%{transform:rotate(0)}75%{transform:rotate(8deg)}100%{transform:rotate(0)}}@keyframes nsF{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
      *{-webkit-tap-highlight-color:transparent} input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}`}</style>

      {/* NAV */}
      <nav style={{ display: editorV2Active ? "none" : "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 16px", borderBottom: "1px solid #F0F0F0", position: "sticky", top: 0, background: "rgba(255,255,255,0.95)", backdropFilter: "blur(12px)", zIndex: 50 }}>
        <div onClick={goHome} style={{ cursor: "pointer", display: "flex", alignItems: "center", gap: "6px" }}><Lg s={24} /><span style={{ fontFamily: "'Instrument Serif', serif", fontSize: "17px", color: "#1A1A1A" }}>NoStrings</span><span style={{ fontFamily: "'Instrument Serif', serif", fontSize: "17px", color: "#E85D3A" }}>PDF</span></div>
        <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
          {isPro && <div style={{ fontSize: "9px", fontWeight: 800, letterSpacing: "1px", color: "#E85D3A", background: "#FFF5F2", padding: "3px 8px", borderRadius: "10px", border: "1px solid #FDDDD4" }}>PRO</div>}
          {isAuthenticated && <div title={userEmail || "Logged in"} style={{ maxWidth: "150px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: "9px", fontWeight: 700, color: "#555", background: "#FAFAFA", border: "1px solid #EEE", padding: "3px 8px", borderRadius: "10px" }}>{userEmail || "Logged in"}</div>}
          {isAuthenticated && <button onClick={handleLogout} style={{ fontSize: "9px", fontWeight: 700, color: "#777", background: "none", border: "1px solid #EEE", padding: "3px 8px", borderRadius: "10px", cursor: "pointer" }}>Logout</button>}
          {!isAuthenticated && <button onClick={openLoginModal} style={{ fontSize: "9px", fontWeight: 700, color: "#555", background: "none", border: "1px solid #EEE", padding: "3px 8px", borderRadius: "10px", cursor: "pointer" }}>Log in</button>}
          {!isAuthenticated && !isPro && <button onClick={openUpgradeModal} style={{ fontSize: "9px", fontWeight: 700, color: "#E85D3A", background: "none", border: "1px solid #FDDDD4", padding: "3px 8px", borderRadius: "10px", cursor: "pointer" }}>Go Pro</button>}
        </div>
      </nav>

      {checkoutNotice && (
        <div style={{ maxWidth: "600px", margin: "10px auto 0", padding: "0 16px" }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "10px", padding: "10px 12px", background: "#F0FAF4", border: "1px solid #D4F0E0", borderRadius: "10px", color: "#18A558", fontSize: "12px", fontWeight: 700 }}>
            <span>{checkoutNotice}</span>
            <button onClick={() => setCheckoutNotice(null)} style={{ border: "none", background: "transparent", color: "#18A558", cursor: "pointer", fontWeight: 800 }}>×</button>
          </div>
        </div>
      )}

      {/* HOME */}
      {view === "home" && (
        <div style={{ maxWidth: "600px", margin: "0 auto", padding: "0 16px 60px" }}>
          <div style={{ textAlign: "center", padding: "36px 0 8px", animation: "nsFU 0.5s ease" }}>
            <div style={{ animation: "nsSn 3s ease-in-out infinite", display: "inline-block", marginBottom: "12px" }}><Lg s={42} /></div>
            <h1 style={{ fontFamily: "'Instrument Serif', serif", fontSize: "32px", fontWeight: 400, color: "#1A1A1A", lineHeight: 1.15, margin: "0 0 10px" }}>PDF tools with<br /><span style={{ fontStyle: "italic", color: "#E85D3A" }}>no strings attached.</span></h1>
            <p style={{ fontSize: "14px", color: "#999", lineHeight: 1.5, maxWidth: "300px", margin: "0 auto" }}>No sign-up. No sneaky charges. Browser-first PDF tools with transient advanced detection.</p>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "6px", margin: "18px 0 20px" }}>
            {PROMISES.map((p, i) => <div key={i} style={{ padding: "11px", background: "#FAFAFA", borderRadius: "11px", border: "1px solid #F0F0F0", animation: "nsSt 0.4s ease both", animationDelay: `${i * 0.05}s` }}><div style={{ fontSize: "15px", marginBottom: "3px" }}>{p.icon}</div><div style={{ fontSize: "11px", fontWeight: 700, color: "#1A1A1A" }}>{p.title}</div><div style={{ fontSize: "9px", color: "#AAA" }}>{p.sub}</div></div>)}
          </div>

          {/* Recent files (Pro) */}
          {isPro && recentFiles.length > 0 && (
            <div style={{ marginBottom: "14px", padding: "12px 14px", background: "#FAFAFA", borderRadius: "12px", border: "1px solid #F0F0F0" }}>
              <div style={{ fontSize: "9px", fontWeight: 700, letterSpacing: "1.5px", color: "#CCC", marginBottom: "8px" }}>RECENT FILES</div>
              {recentFiles.slice(0, 5).map((r, i) => (
                <div key={i} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "6px 0", borderBottom: i < Math.min(recentFiles.length, 5) - 1 ? "1px solid #F0F0F0" : "none" }}>
                  <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
                    <span style={{ fontSize: "14px" }}>📄</span>
                    <div>
                      <div style={{ fontSize: "11px", fontWeight: 600, color: "#1A1A1A" }}>{r.name}</div>
                      <div style={{ fontSize: "9px", color: "#CCC" }}>{r.tool} · {new Date(r.date).toLocaleDateString()}</div>
                    </div>
                  </div>
                </div>
              ))}
            </div>
          )}

          {/* Privacy callout */}
          <div style={{ padding: "14px 16px", background: "#F0FAF4", borderRadius: "12px", border: "1px solid #D4F0E0", marginBottom: "14px", animation: "nsSt 0.4s ease both", animationDelay: "0.18s" }}>
            <div style={{ display: "flex", alignItems: "flex-start", gap: "10px" }}>
              <span style={{ fontSize: "18px", flexShrink: 0 }}>🔒</span>
              <div>
                <div style={{ fontSize: "12px", fontWeight: 700, color: "#18A558", marginBottom: "3px" }}>Browser-first privacy, no document storage</div>
                <div style={{ fontSize: "10px", color: "#6DA88A", lineHeight: 1.5 }}>Free tools run on-device. Advanced flat-PDF field detection may send the file to our detection service for a moment so we can find fillable areas, then it is discarded and not stored.</div>
              </div>
            </div>
          </div>

          {/* Featured: Edit PDF */}
          <button onClick={() => openTool("edit")} style={{ width: "100%", background: "linear-gradient(135deg, #FFF5F2, #FFF)", border: "1.5px solid #FDDDD4", borderRadius: "16px", padding: "22px 18px", cursor: "pointer", textAlign: "left", marginBottom: "14px", transition: "all 0.15s", animation: "nsSt 0.4s ease both", animationDelay: "0.2s" }}
            onMouseOver={e => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "0 8px 24px rgba(232,93,58,0.1)"; }}
            onMouseOut={e => { e.currentTarget.style.transform = "none"; e.currentTarget.style.boxShadow = "none"; }}>
            <div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
              <div style={{ fontSize: "32px" }}>✏️</div>
              <div>
                <div style={{ fontSize: "16px", fontWeight: 700, color: "#1A1A1A" }}>Edit PDF</div>
                <div style={{ fontSize: "11px", color: "#AAA", marginTop: "2px" }}>Fill forms, sign, annotate, add images — all in one editor</div>
              </div>
              <div style={{ marginLeft: "auto", fontSize: "18px", color: "#CCC" }}>→</div>
            </div>
          </button>

          {/* Other tools by category */}
          {["Organize", "Optimize"].map(cat => (
            <div key={cat} style={{ marginBottom: "12px" }}>
              <div style={{ fontSize: "9px", fontWeight: 700, letterSpacing: "1.5px", color: "#CCC", marginBottom: "6px", textTransform: "uppercase" }}>{cat}</div>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(100px, 1fr))", gap: "7px" }}>
                {TOOLS.filter(t => t.cat === cat).map((t, i) => {
                  const disabled = t.disabled === true;
                  return (
                    <button key={t.id} onClick={() => { if (!disabled) openTool(t.id); }} disabled={disabled} aria-disabled={disabled ? "true" : undefined} style={{ background: disabled ? "#F8FAFC" : "#FFF", border: disabled ? "1px dashed #CBD5E1" : "1px solid #EEE", borderRadius: "12px", padding: "14px 8px 12px", cursor: disabled ? "not-allowed" : "pointer", textAlign: "center", transition: "all 0.15s", opacity: disabled ? 0.86 : 1 }}
                      onMouseOver={e => { if (disabled) return; e.currentTarget.style.borderColor = t.accent + "50"; e.currentTarget.style.transform = "translateY(-2px)"; }}
                      onMouseOut={e => { if (disabled) return; e.currentTarget.style.borderColor = "#EEE"; e.currentTarget.style.transform = "none"; }}>
                      {t.badge && <div style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "2px 6px", borderRadius: "999px", background: "#EEF2F7", color: "#64748B", fontSize: "7px", fontWeight: 800, letterSpacing: "0.4px", textTransform: "uppercase", marginBottom: "5px" }}>{t.badge}</div>}
                      <div style={{ fontSize: "22px", marginBottom: "4px" }}>{t.icon}</div>
                      <div style={{ fontSize: "11px", fontWeight: 700, color: disabled ? "#475569" : "#1A1A1A" }}>{t.label}</div>
                      <div style={{ fontSize: "8px", color: disabled ? "#64748B" : "#BBB", marginTop: "2px", lineHeight: 1.2 }}>{t.desc}</div>
                    </button>
                  );
                })}
              </div>
            </div>
          ))}

          {/* Pro Features Showcase */}
          <div style={{ marginTop: "16px", padding: "18px 16px", background: "linear-gradient(135deg, #FFF9F7, #FFF)", borderRadius: "14px", border: "1.5px solid #FDDDD4" }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "12px" }}>
              <div style={{ fontSize: "10px", fontWeight: 700, letterSpacing: "1.5px", color: "#E85D3A" }}>{isPro ? "✓ PRO ENABLED" : "✨ PRO FEATURES"}</div>
              {!isPro && <button onClick={openUpgradeModal} style={{ fontSize: "10px", fontWeight: 700, color: "white", background: "#E85D3A", border: "none", padding: "4px 12px", borderRadius: "6px", cursor: "pointer" }}>Go Pro →</button>}
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "6px" }}>
              {[
                { icon: "🖊️", name: "Saved Signatures", desc: "Draw once, reuse forever", proDesc: "Account-backed signatures enabled" },
                { icon: "📦", name: "Batch Processing", desc: "Up to 100 files at once", proDesc: "Up to 100 files enabled for Merge, Compress, Convert" },
                { icon: "🧠", name: "Advanced Detection", desc: "Help find fields on flat PDFs", proDesc: "Advanced auto-detect enabled" },
                { icon: "📋", name: "Recent Files", desc: "Quick access to past work", proDesc: "Recent-file shortcuts enabled" },
                { icon: "🚫", name: "No Ads", desc: "Clean, distraction-free", proDesc: "Sponsor banner removed" },
                { icon: "🚀", name: "Early Access", desc: "OCR, AI tools & more", proDesc: "Coming soon for Pro users", comingSoon: true },
              ].map((f, i) => (
                <div key={i} onClick={isPro ? undefined : openUpgradeModal} style={{ padding: "10px", background: "white", borderRadius: "10px", border: "1px solid #F5E6E0", cursor: isPro ? "default" : "pointer", transition: "all 0.1s" }}
                  onMouseOver={e => e.currentTarget.style.borderColor = "#E85D3A50"} onMouseOut={e => e.currentTarget.style.borderColor = "#F5E6E0"}>
                  <div style={{ fontSize: "16px", marginBottom: "4px" }}>{f.icon}</div>
                  <div style={{ fontSize: "10px", fontWeight: 700, color: "#1A1A1A" }}>{f.name}</div>
                  <div style={{ fontSize: "8px", color: isPro && f.comingSoon ? "#B7791F" : "#E85D3A", fontWeight: 600, marginTop: "1px", lineHeight: 1.25 }}>{isPro ? f.proDesc : "PRO"}</div>
                </div>
              ))}
            </div>
          </div>

          {/* Pricing */}
          {isPro ? (
          <div style={{ marginTop: "12px", padding: "16px", background: "#F0FAF4", borderRadius: "12px", border: "1px solid #D4F0E0", textAlign: "center" }}>
            <div style={{ fontSize: "10px", fontWeight: 800, letterSpacing: "1.5px", color: "#18A558", marginBottom: "6px" }}>YOU'RE ON PRO</div>
            <div style={{ fontSize: "13px", color: "#4F8F70", lineHeight: 1.5, fontWeight: 600 }}>Saved signatures, premium features, and Pro tools are enabled.</div>
            <div style={{ fontSize: "10px", color: "#8EB9A1", marginTop: "6px" }}>Billing self-service is coming soon. For billing help, use the contact on your payment receipt.</div>
          </div>
          ) : (
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px", marginTop: "12px" }}>
            <div onClick={openUpgradeModal} style={{ padding: "16px", background: "white", borderRadius: "12px", border: "1px solid #EEE", textAlign: "center", cursor: "pointer", transition: "all 0.15s" }}
              onMouseOver={e => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.borderColor = "#E85D3A50"; }}
              onMouseOut={e => { e.currentTarget.style.transform = "none"; e.currentTarget.style.borderColor = "#EEE"; }}>
              <div style={{ fontSize: "9px", fontWeight: 700, letterSpacing: "1.5px", color: "#CCC", marginBottom: "6px" }}>MONTHLY</div>
              <div style={{ fontFamily: "'Instrument Serif', serif", fontSize: "28px", color: "#1A1A1A" }}>$9<span style={{ fontSize: "14px", color: "#CCC" }}>/mo</span></div>
              <div style={{ fontSize: "10px", color: "#AAA", marginTop: "2px" }}>Cancel anytime. No strings.</div>
              <div style={{ fontSize: "10px", color: "#E85D3A", fontWeight: 700, marginTop: "6px" }}>Select →</div>
            </div>
            <div onClick={openUpgradeModal} style={{ padding: "16px", background: "#1A1A1A", borderRadius: "12px", textAlign: "center", position: "relative", cursor: "pointer", transition: "transform 0.1s" }}
              onMouseOver={e => e.currentTarget.style.transform = "translateY(-2px)"} onMouseOut={e => e.currentTarget.style.transform = "none"}>
              <div style={{ position: "absolute", top: "8px", right: "8px", fontSize: "7px", fontWeight: 800, background: "#E85D3A", color: "white", padding: "2px 6px", borderRadius: "3px" }}>SAVE 22%</div>
              <div style={{ fontSize: "9px", fontWeight: 700, letterSpacing: "1.5px", color: "rgba(255,255,255,0.3)", marginBottom: "6px" }}>ANNUAL</div>
              <div style={{ fontFamily: "'Instrument Serif', serif", fontSize: "28px", color: "white" }}>$84<span style={{ fontSize: "14px", color: "rgba(255,255,255,0.55)" }}>/year</span></div>
              <div style={{ fontSize: "9px", color: "rgba(255,255,255,0.5)", marginTop: "3px" }}>$7/month effective</div>
              <div style={{ fontSize: "10px", color: "#E85D3A", fontWeight: 700, marginTop: "6px" }}>Get Pro →</div>
            </div>
          </div>
          )}
        </div>
      )}

      {/* TOOL */}
      {view === "tool" && tool && (
        <div style={{ maxWidth: editorV2Active ? "none" : (isEditor && files.length > 0 ? "min(1100px, 92vw)" : (toolId === "reorder" ? "min(1200px, 94vw)" : (toolId === "split" && files.length > 0 && splitMode === "visual" ? "min(1500px, 96vw)" : "600px"))), margin: editorV2Active ? "0" : "0 auto", padding: editorV2Active ? "0 10px 10px" : "0 16px 60px", animation: "nsFU 0.3s ease", height: editorV2Active ? "100vh" : undefined, overflow: editorV2Active ? "hidden" : undefined, display: editorV2Active ? "flex" : undefined, flexDirection: editorV2Active ? "column" : undefined, background: editorV2Active ? "var(--nsp-bg-workspace)" : undefined }}>
          <div style={{ padding: editorV2Active ? "4px 0 3px" : "14px 0 12px", flexShrink: editorV2Active ? 0 : undefined, display: editorV2Active ? "flex" : undefined, alignItems: editorV2Active ? "center" : undefined, gap: editorV2Active ? "8px" : undefined, flexWrap: editorV2Active ? "wrap" : undefined }}>
            <button onClick={goHome} style={{ background: editorV2Active ? "var(--nsp-surface)" : "none", border: editorV2Active ? "1px solid var(--nsp-border)" : "none", borderRadius: editorV2Active ? "999px" : undefined, fontSize: editorV2Active ? "11px" : "12px", color: "#BBB", cursor: "pointer", padding: editorV2Active ? "4px 8px" : 0, marginBottom: editorV2Active ? 0 : "12px", fontWeight: 500 }}>← All tools</button>
            <div style={{ display: "flex", alignItems: "center", gap: editorV2Active ? "7px" : "10px" }}>
              <div style={{ width: editorV2Active ? "28px" : "40px", height: editorV2Active ? "28px" : "40px", background: `${tool.accent}08`, borderRadius: editorV2Active ? "8px" : "10px", display: "flex", alignItems: "center", justifyContent: "center", fontSize: editorV2Active ? "15px" : "20px" }}>{tool.icon}</div>
              <div><h2 style={{ fontFamily: "'Instrument Serif', serif", fontSize: editorV2Active ? "16px" : "20px", margin: 0 }}>{tool.label}</h2><p style={{ display: editorV2Active ? "none" : undefined, fontSize: "11px", color: "#AAA", margin: "1px 0 0" }}>{tool.desc}</p></div>
            </div>
            {editorV2Active && (
              <div style={{ display: "flex", alignItems: "center", gap: "5px", flexWrap: "wrap" }}>
                <span style={{ display: "inline-flex", alignItems: "center", gap: "4px", padding: "4px 8px", background: "#F0FAF4", borderRadius: "999px", border: "1px solid #D4F0E0", fontSize: "10px", color: "#18A558", fontWeight: 700 }}><span>🔒</span>Browser-first · transient</span>
                {!detecting && !loading && items.filter(i => i.auto).length > 0 && (
                  <button onClick={() => setShowFields(!showFields)} style={{ display: "inline-flex", alignItems: "center", gap: "5px", padding: "4px 8px", background: showFields ? "#EEF6FF" : "#F5F5F5", border: showFields ? "1px solid #D0E8FF" : "1px solid #EEE", borderRadius: "999px", color: showFields ? "#2D7FF9" : "#AAA", fontSize: "10px", fontWeight: 800, cursor: "pointer" }}>
                    <span>{showFields ? "🧠" : "👁️"}</span>{items.filter(i => i.auto).length} fillable
                  </button>
                )}
              </div>
            )}
          </div>
          <div style={{ display: editorV2Active ? "none" : "inline-flex", alignItems: "center", alignSelf: editorV2Active ? "flex-start" : undefined, gap: "5px", padding: editorV2Active ? "4px 8px" : "7px 10px", background: "#F0FAF4", borderRadius: editorV2Active ? "999px" : "8px", marginBottom: editorV2Active ? "4px" : "14px", border: "1px solid #D4F0E0", flexShrink: editorV2Active ? 0 : undefined }}><span style={{ fontSize: "12px" }}>🔒</span><span style={{ fontSize: "10px", color: "#18A558", fontWeight: 600 }}>{editorV2Active ? "Browser-first · advanced detection transient · not stored" : "Browser-first processing — advanced detection is transient and not stored"}</span></div>

          {!ready && <div style={{ textAlign: "center", padding: "40px", color: "#AAA" }}><div style={{ width: "22px", height: "22px", border: "3px solid #EEE", borderTopColor: tool.accent, borderRadius: "50%", animation: "nsS 0.6s linear infinite", margin: "0 auto 10px" }} />Loading...</div>}
          {ready && files.length === 0 && toolId !== "compress" && <Drop tool={tool} onFiles={addFiles} />}

          {/* ═══ UNIFIED EDITOR ═══ */}
          {ready && isEditor && files.length > 0 && (
            <>
              {loading && <div style={{ textAlign: "center", padding: "50px", color: "#AAA" }}><div style={{ width: "22px", height: "22px", border: "3px solid #EEE", borderTopColor: "#E85D3A", borderRadius: "50%", animation: "nsS 0.6s linear infinite", margin: "0 auto 10px" }} />Rendering...</div>}

              {showSignPad && <SignPad onSave={d => { setSignatureCreationRequested(false); setSignatureImg(d); saveSignature(d); setShowSignPad(false); setPlacingSign(true); setEditMode("sign"); }} onCancel={() => { setSignatureCreationRequested(false); setShowSignPad(false); }} />}

              {/* Saved signatures gallery (Pro feature) */}
              {!showSignPad && editorMode === "edit" && editMode === "sign" && savedSignatures.length > 0 && (
                <div style={{ marginBottom: "10px", padding: "12px 14px", background: "#FFFDF9", borderRadius: "12px", border: "1px solid #F2D6C7", boxShadow: "0 2px 10px rgba(232,93,58,0.08)" }}>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "8px", marginBottom: "7px", flexWrap: "wrap" }}>
                    <span style={{ fontSize: "10px", fontWeight: 700, color: "#888", letterSpacing: "1px" }}>
                      {isPro ? "SAVED SIGNATURES" : "SESSION SIGNATURES"}
                    </span>
                    <div style={{ display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" }}>
                      {!isPro && <button onClick={() => {
                        openUpgradeModal();
                      }} style={{ fontSize: "10px", fontWeight: 800, color: "#E85D3A", background: "#FFF5F2", border: "1px solid #FDDDD4", borderRadius: "999px", padding: "5px 10px", cursor: "pointer" }}>Upgrade to save signatures permanently</button>}
                      <button onClick={() => { setSignatureCreationRequested(true); setPlacingSign(false); setSignatureImg(null); setShowSignPad(true); }}
                        style={{ fontSize: "10px", fontWeight: 800, color: "white", background: "#1A1A1A", border: "none", borderRadius: "999px", padding: "5px 10px", cursor: "pointer" }}>New signature</button>
                    </div>
                  </div>
                  <div style={{ fontSize: "11px", color: "#8A5A44", fontWeight: 600, marginBottom: "9px" }}>Select a saved signature to place it, or create a new one.</div>
                  <div style={{ display: "flex", gap: "8px", overflowX: "auto", WebkitOverflowScrolling: "touch", padding: "4px 2px 2px" }}>
                    {savedSignatures.map(sig => (
                      <div key={sig.id} onClick={() => { setSignatureImg(sig.src); setPlacingSign(true); }}
                        style={{ flexShrink: 0, padding: "8px", background: "white", borderRadius: "10px", border: signatureImg === sig.src ? "2px solid #4F46E5" : "1px solid #E8D8CE", cursor: "pointer", position: "relative", boxShadow: "0 1px 5px rgba(0,0,0,0.06)" }}>
                        <button onClick={e => { e.stopPropagation(); deleteSignature(sig.id); }}
                          aria-label="Delete signature"
                          title="Delete signature"
                          style={{ position: "absolute", top: "-8px", right: "-8px", width: "24px", height: "24px", borderRadius: "999px", border: "1px solid #F4B8A6", background: "#FFF5F2", color: "#E85D3A", fontSize: "15px", fontWeight: 900, cursor: "pointer", lineHeight: "20px", padding: 0, boxShadow: "0 1px 4px rgba(0,0,0,0.12)" }}>×</button>
                        <img src={sig.src} style={{ height: "34px", display: "block", borderRadius: "4px" }} />
                      </div>
                    ))}
                  </div>
                </div>
              )}

              {restoreBannerVisible && (
                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px", padding: "9px 12px", background: "#EBF5FF", color: "#1E3A5F", borderRadius: "6px", marginBottom: "8px", border: "1px solid #CFE6FF", fontSize: "13px", fontWeight: 600 }}>
                  <span>Your document was restored successfully.</span>
                  <button onClick={() => setRestoreBannerVisible(false)} aria-label="Dismiss restored document message" style={{ border: "none", background: "transparent", color: "#1E3A5F", cursor: "pointer", fontSize: "16px", lineHeight: 1, padding: "0 2px" }}>×</button>
                </div>
              )}

              {!loading && !detecting && rotationSuggestion && (
                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", padding: "9px 12px", background: "#FFF8E8", borderRadius: "9px", marginBottom: "8px", border: "1px solid #F4D48A" }}>
                  <span style={{ fontSize: "11px", color: "#8A5A00", fontWeight: 600 }}>This PDF appears {rotationSuggestion.label}. Apply suggested rotation?</span>
                  <div style={{ display: "flex", gap: "6px", flexShrink: 0 }}>
                    <button onClick={() => applyEditorRotation(rotationSuggestion.correction)} disabled={rotatingEditorPdf} style={{ padding: "6px 10px", background: "#E85D3A", color: "white", border: "none", borderRadius: "7px", fontSize: "10px", fontWeight: 700, cursor: rotatingEditorPdf ? "default" : "pointer", opacity: rotatingEditorPdf ? 0.7 : 1 }}>Apply</button>
                    <button onClick={() => setIgnoredRotationFiles(prev => ({ ...prev, [currentFileKey]: true }))} style={{ padding: "6px 10px", background: "white", color: "#8A5A00", border: "1px solid #F4D48A", borderRadius: "7px", fontSize: "10px", fontWeight: 700, cursor: "pointer" }}>Ignore</button>
                  </div>
                </div>
              )}

              {rotationNotice && (
                <div style={{ padding: "8px 12px", background: "#FFF5F2", borderRadius: "8px", marginBottom: "8px", border: "1px solid #FDDDD4", fontSize: "11px", color: "#E85D3A", fontWeight: 600 }}>{rotationNotice}</div>
              )}

              {detecting && <div style={{ display: "flex", alignItems: "center", gap: "6px", padding: "8px 12px", background: "#EEF6FF", borderRadius: "8px", marginBottom: "8px", border: "1px solid #D0E8FF" }}><div style={{ width: "12px", height: "12px", border: "2px solid #BDD", borderTopColor: "#2D7FF9", borderRadius: "50%", animation: "nsS 0.6s linear infinite" }} /><span style={{ fontSize: "11px", color: "#2D7FF9", fontWeight: 600 }}>{isPro && accessToken && flatAutoDetectAttemptRef.current === currentFileKey ? "Running advanced auto-detect..." : "Detecting fillable areas..."}</span></div>}
              {!editorV2Active && !detecting && !loading && items.filter(i => i.auto).length > 0 && (
                <div style={{ display: "flex", alignItems: "center", justifyContent: editorV2Active ? "flex-start" : "space-between", gap: editorV2Active ? "8px" : undefined, width: editorV2Active ? "fit-content" : undefined, alignSelf: editorV2Active ? "flex-start" : undefined, padding: editorV2Active ? "4px 8px" : "8px 12px", background: showFields ? "#EEF6FF" : "#F5F5F5", borderRadius: editorV2Active ? "999px" : "8px", marginBottom: editorV2Active ? "4px" : "8px", border: showFields ? "1px solid #D0E8FF" : "1px solid #EEE", transition: "all 0.2s" }}>
                  <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
                    <span>{showFields ? "🧠" : "👁️"}</span>
                    <span style={{ fontSize: "11px", color: showFields ? "#2D7FF9" : "#AAA", fontWeight: 600 }}>{items.filter(i => i.auto).length} fillable areas</span>
                  </div>
                  <button onClick={() => setShowFields(!showFields)}
                    style={{ position: "relative", width: "44px", height: "24px", borderRadius: "12px", border: "none", background: showFields ? "#2D7FF9" : "#DDD", cursor: "pointer", transition: "background 0.2s", padding: 0 }}>
                    <div style={{ position: "absolute", top: "2px", left: showFields ? "22px" : "2px", width: "20px", height: "20px", borderRadius: "50%", background: "white", boxShadow: "0 1px 3px rgba(0,0,0,0.2)", transition: "left 0.2s" }} />
                  </button>
                </div>
              )}
              {/* Manual-first fallback for PDFs with no detected AcroForm fields */}
              {!detecting && !loading && items.filter(i => i.auto).length === 0 && pdfBytes && (
                <div style={{ padding: editorV2Active ? "9px 14px" : "12px", background: "linear-gradient(135deg, #FFF8F5, #F8FAFF)", borderRadius: "12px", margin: editorV2Active ? "0 auto 6px" : "0 0 8px", border: "1px solid #F1D7CC", maxWidth: editorV2Active ? "560px" : undefined, width: editorV2Active ? "100%" : undefined, boxSizing: editorV2Active ? "border-box" : undefined, flexShrink: editorV2Active ? 0 : undefined, display: editorV2Active ? "flex" : undefined, flexDirection: editorV2Active ? "column" : undefined, alignItems: editorV2Active ? "center" : undefined, textAlign: editorV2Active ? "center" : undefined, gap: editorV2Active ? "6px" : undefined }}>
                  <div style={{ marginBottom: editorV2Active ? 0 : "8px" }}>
                    <div style={{ fontSize: editorV2Active ? "12px" : "13px", color: "#1A1A1A", fontWeight: 800, marginBottom: "3px" }}>No embedded fillable fields were found.</div>
                    <div style={{ fontSize: editorV2Active ? "10px" : "11px", color: "#666", lineHeight: editorV2Active ? 1.35 : 1.45 }}>
                      This PDF has no extractable text or built-in form fields, so it may be a scanned or image-only document. You can still fill it manually.
                    </div>
                  </div>
                  <button onClick={() => {
                    setEditorMode("fill");
                    setEditMode("text");
                    setPlacingTextField(true);
                    setEditSubmode("idle");
                    setPlacingSign(false);
                    setPlacingImg(false);
                    setSelId(null);
                    setEditingItemId(null);
                  }} style={{ width: editorV2Active ? "auto" : "100%", padding: editorV2Active ? "7px 11px" : "10px", background: "#E85D3A", color: "white", border: "none", borderRadius: "9px", fontSize: editorV2Active ? "11px" : "12px", fontWeight: 800, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: "6px", marginBottom: editorV2Active ? 0 : "8px", whiteSpace: editorV2Active ? "nowrap" : undefined }}>
                    <span>{placingTextField ? "Text placement armed - double-click the page" : "Place fields manually"}</span>
                  </button>
                  <div style={{ fontSize: "10px", color: "#777", lineHeight: editorV2Active ? 1.25 : 1.45, marginBottom: editorV2Active ? 0 : "8px", gridColumn: editorV2Active ? "1 / -1" : undefined }}>
                    {placingTextField ? "Double-click the page to place text. Press Esc or choose another tool to cancel." : "Choose Text, Check, or Signature, then click the document where you want to place the field. Use Edit mode to move or resize fields."}
                  </div>
                  {!isPro && <button onClick={() => {
                    runServerDetection();
                  }} style={{ width: editorV2Active ? "auto" : "100%", padding: editorV2Active ? "6px 10px" : "8px", background: "white", color: "#2D7FF9", border: "1px solid #CFE0FF", borderRadius: "8px", fontSize: editorV2Active ? "10px" : "11px", fontWeight: 700, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: "6px", whiteSpace: editorV2Active ? "nowrap" : undefined }}>
                    <span>{!isAuthenticated ? "Log in or upgrade to Pro for advanced auto-detect" : "Upgrade to Pro for advanced auto-detect"}</span>
                  </button>}
                  {detectError && detectError !== "flat" && <div style={{ fontSize: "9px", color: "#E85D3A", background: "#FFF0ED", padding: "6px 8px", borderRadius: "4px", marginTop: "6px", wordBreak: "break-all" }}>{detectError}</div>}
                  <div style={{ display: editorV2Active ? "none" : undefined, fontSize: "9px", color: "#BBB", textAlign: "center", marginTop: "4px", lineHeight: 1.4 }}>Transient detection only. We do not store the PDF after analysis.</div>
                </div>
              )}

              {imgs.length > 0 && !loading && (
                <>
                  <EditorWorkspaceFrame {...editorWorkspaceFrameProps}>
                  {(editorMode === "edit" || placingTextField || editorV2Active) && (
                    <div style={{ display: "flex", gap: editorV2Active ? "3px" : "4px", padding: editorV2Active ? "4px 0" : "8px 0", position: "sticky", top: editorV2Active ? "0" : "48px", background: "var(--nsp-surface)", backdropFilter: "blur(8px)", zIndex: editorV2Active ? 60 : 40, border: "1px solid var(--nsp-border)", borderRadius: "var(--nsp-radius-md)", boxShadow: "var(--nsp-shadow-soft)", overflowX: editorV2Active ? "visible" : "auto", overflowY: editorV2Active ? "visible" : undefined, WebkitOverflowScrolling: "touch", flexShrink: editorV2Active ? 0 : undefined }}>
                      {(!editorV2Active || !editorV2DockAvailable) && EDIT_MODES.map(m => (
                        <button key={m.id} onClick={() => {
                          if (editorV2Active) setEditorMode(m.id === "text" ? "fill" : "edit");
                          setEditMode(m.id); setEditingItemId(null); setEditSubmode("idle"); setPlacingTextField(editorV2Active && m.id === "text");
                          if (m.id === "sign") {
                            setPlacingSign(false);
                            if (savedSignatures.length > 0) {
                              setSignatureCreationRequested(false);
                              setShowSignPad(false);
                              if (!signatureImg) setSignatureImg(savedSignatures[0].src);
                            } else if (!signatureImg) {
                              setSignatureCreationRequested(false);
                              setShowSignPad(true);
                            }
                          } else {
                            setSignatureCreationRequested(false);
                            setShowSignPad(false);
                          }
                          if (m.id === "image") {
                            const inp = document.createElement("input"); inp.type = "file"; inp.accept = "image/*";
                            inp.onchange = async () => { const f = inp.files[0]; if (f) { setAddImgSrc(await readDataURL(f)); setPlacingImg(true); } };
                            inp.click();
                          }
                        }}
                          style={{ padding: editorV2Active ? "4px 8px" : "5px 10px", borderRadius: "8px", border: isToolVisuallyActive(m.id) ? "1px solid var(--nsp-orange)" : "1px solid var(--nsp-border)", fontSize: "11px", fontWeight: 700, background: isToolVisuallyActive(m.id) ? "var(--nsp-orange)" : "var(--nsp-surface-raised)", color: isToolVisuallyActive(m.id) ? "var(--nsp-surface)" : "var(--nsp-text-muted)", cursor: "pointer", whiteSpace: "nowrap", flexShrink: 0, transition: "all 0.1s" }}>
                          {m.label}
                        </button>
                      ))}
                      {editorV2Active && <>
                        {!editorV2DockAvailable && <button disabled={!hasEditableOverlays} title={editorMode === "edit" ? "Finish review" : reviewFieldsTitle} aria-label={editorMode === "edit" ? "Finish review" : "Review fields"} onClick={() => {
                          if (!hasEditableOverlays && editorMode !== "edit") return;
                          const nextMode = editorMode === "edit" ? "fill" : "edit";
                          setEditorMode(nextMode);
                          if (nextMode === "edit") setEditMode("text");
                          setSelId(null);
                          setEditingItemId(null);
                          setEditSubmode("idle");
                          setPlacingTextField(false);
                        }} style={{ padding: "4px 10px", borderRadius: "8px", border: "1px solid #C0C0C0", fontSize: "10px", fontWeight: 700, background: editorMode === "edit" ? "#666666" : "white", color: !hasEditableOverlays && editorMode !== "edit" ? "#B8B8B8" : (editorMode === "edit" ? "white" : "#666666"), cursor: !hasEditableOverlays && editorMode !== "edit" ? "not-allowed" : "pointer", opacity: !hasEditableOverlays && editorMode !== "edit" ? 0.72 : 1, flexShrink: 0 }}>
                          {editorMode === "edit" ? "Done" : "Review fields"}
                        </button>}
                        {!editorV2DockAvailable && <span style={{ width: "1px", alignSelf: "stretch", background: "var(--nsp-border)", margin: "0 3px", flexShrink: 0 }} />}
                        <button onClick={performUndo} disabled={!undoStack.length}
                          style={{ padding: "4px 8px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "10px", fontWeight: 700, background: undoStack.length ? "#FFF" : "#F7F7F7", color: undoStack.length ? "#555" : "#BBB", cursor: undoStack.length ? "pointer" : "default", opacity: undoStack.length ? 1 : 0.7, flexShrink: 0 }}>↶ Undo</button>
                        <button onClick={performRedo} disabled={!redoStack.length}
                          style={{ padding: "4px 8px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "10px", fontWeight: 700, background: redoStack.length ? "#FFF" : "#F7F7F7", color: redoStack.length ? "#555" : "#BBB", cursor: redoStack.length ? "pointer" : "default", opacity: redoStack.length ? 1 : 0.7, flexShrink: 0 }}>↷ Redo</button>
                        <button onClick={() => applyEditorRotation(-90)} disabled={rotatingEditorPdf}
                          style={{ padding: "4px 8px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "10px", fontWeight: 700, background: rotatingEditorPdf ? "#F7F7F7" : "#FFF", color: rotatingEditorPdf ? "#BBB" : "#555", cursor: rotatingEditorPdf ? "default" : "pointer", opacity: rotatingEditorPdf ? 0.7 : 1, flexShrink: 0 }}>Rotate Left</button>
                        <button onClick={() => applyEditorRotation(90)} disabled={rotatingEditorPdf}
                          style={{ padding: "4px 8px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "10px", fontWeight: 700, background: rotatingEditorPdf ? "#F7F7F7" : "#FFF", color: rotatingEditorPdf ? "#BBB" : "#555", cursor: rotatingEditorPdf ? "default" : "pointer", opacity: rotatingEditorPdf ? 0.7 : 1, flexShrink: 0 }}>Rotate Right</button>
                        <span style={{ fontSize: "9px", color: "#AAA", fontWeight: 700, letterSpacing: "0.5px", alignSelf: "center", flexShrink: 0 }}>VIEW</span>
                        <button onClick={() => { setEditorV2FitMode("page"); setZoom(1); }} style={{ padding: "4px 8px", borderRadius: "6px", border: "none", fontSize: "10px", fontWeight: editorV2ResolvedFitMode === "page" ? 800 : 600, background: editorV2ResolvedFitMode === "page" ? "#1A1A1A" : "#F0F0F0", color: editorV2ResolvedFitMode === "page" ? "white" : "#888", cursor: "pointer", flexShrink: 0 }}>Fit Page</button>
                        <button onClick={() => { setEditorV2FitMode("width"); setZoom(1); }} style={{ padding: "4px 8px", borderRadius: "6px", border: "none", fontSize: "10px", fontWeight: editorV2ResolvedFitMode === "width" ? 800 : 600, background: editorV2ResolvedFitMode === "width" ? "#1A1A1A" : "#F0F0F0", color: editorV2ResolvedFitMode === "width" ? "white" : "#888", cursor: "pointer", flexShrink: 0 }}>Fit Width</button>
                        {[1, 1.25, 1.5].map(z => <button key={z} onClick={() => { setEditorV2FitMode("custom"); setZoom(z); }} style={{ padding: "4px 8px", borderRadius: "6px", border: "none", fontSize: "10px", fontWeight: editorV2FitMode === "custom" && zoom === z ? 800 : 600, background: editorV2FitMode === "custom" && zoom === z ? "#1A1A1A" : "#F0F0F0", color: editorV2FitMode === "custom" && zoom === z ? "white" : "#888", cursor: "pointer", flexShrink: 0 }}>{Math.round(z * 100)}%</button>)}
                        <button onClick={() => { setEditorV2FitMode("custom"); setZoom(Math.max(0.5, Math.round((scale - 0.1) * 100) / 100)); }} style={{ width: "24px", height: "24px", borderRadius: "6px", border: "1px solid #EEE", background: "white", fontSize: "12px", fontWeight: 700, color: "#888", cursor: "pointer", flexShrink: 0 }}>−</button>
                        <button onClick={() => { setEditorV2FitMode("custom"); setZoom(Math.min(2.5, Math.round((scale + 0.1) * 100) / 100)); }} style={{ width: "24px", height: "24px", borderRadius: "6px", border: "1px solid #EEE", background: "white", fontSize: "12px", fontWeight: 700, color: "#888", cursor: "pointer", flexShrink: 0 }}>+</button>
                        <div style={{ display: "flex", alignItems: "center", gap: "5px", marginLeft: "auto", flexWrap: "wrap", padding: "3px", background: "var(--nsp-surface)", border: "1px solid var(--nsp-border)", borderRadius: "999px", boxShadow: "var(--nsp-shadow-soft)", flexShrink: 0 }}>
                          <label style={{ padding: "6px 10px", background: "#F8FAFF", color: "#2D7FF9", border: "1px solid #CFE0FF", borderRadius: "999px", fontSize: "10px", fontWeight: 800, cursor: "pointer", textAlign: "center", boxSizing: "border-box" }}>
                            Load different file
                            <input type="file" accept=".pdf" onChange={e => { if (e.target.files[0]) { resetEditorForNewFile([e.target.files[0]]); } }} style={{ display: "none" }} />
                          </label>
                          <button onClick={saveEditor} disabled={saving || !filledCount} style={{ padding: "6px 12px", background: filledCount ? "var(--nsp-orange)" : "white", color: filledCount ? "white" : "#AAA", border: filledCount ? "1px solid var(--nsp-orange)" : "1px solid #E5E5E5", borderRadius: "999px", fontSize: "10px", fontWeight: 800, cursor: filledCount ? "pointer" : "default", display: "flex", alignItems: "center", gap: "6px", boxShadow: filledCount ? "0 6px 16px rgba(232,93,58,0.24)" : "none" }}>
                            {saving && <div style={{ width: "13px", height: "13px", border: "2px solid rgba(255,255,255,0.25)", borderTopColor: "white", borderRadius: "50%", animation: "nsS 0.6s linear infinite" }} />}
                            {saving ? "Generating..." : "Download edited PDF"}
                          </button>
                          <div style={{ position: "relative", display: "flex", alignItems: "center" }}>
                            <button onClick={() => setEditorV2MoreOpen(v => !v)} title="More document actions" aria-label="More document actions" style={{ width: "28px", height: "28px", borderRadius: "999px", border: "1px solid var(--nsp-border)", background: editorV2MoreOpen ? "#F5F5F5" : "white", color: "#777", fontSize: "15px", fontWeight: 900, lineHeight: 1, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }}>⋯</button>
                            {editorV2MoreOpen && (
                              <div style={{ position: "absolute", top: "calc(100% + 6px)", right: 0, minWidth: "150px", padding: "6px", background: "white", border: "1px solid var(--nsp-border)", borderRadius: "12px", boxShadow: "0 12px 28px rgba(0,0,0,0.16)", zIndex: 120, display: "flex", flexDirection: "column", gap: "4px" }}>
                                {items.length > 0 && <button onClick={() => { setEditorV2MoreOpen(false); setEditorV2ConfirmAction("clear"); }} style={{ padding: "8px 9px", background: "transparent", color: "#C64D32", border: "none", borderRadius: "8px", fontSize: "10px", fontWeight: 800, cursor: "pointer", textAlign: "left" }}>Clear all fields</button>}
                                <button onClick={() => { setEditorV2MoreOpen(false); setEditorV2ConfirmAction("startOver"); }} style={{ padding: "8px 9px", background: "transparent", color: "#777", border: "none", borderRadius: "8px", fontSize: "10px", fontWeight: 800, cursor: "pointer", textAlign: "left" }}>Start over</button>
                              </div>
                            )}
                            {editorV2ConfirmAction && (
                              <div role="dialog" aria-modal="false" aria-label={editorV2ConfirmAction === "clear" ? "Clear all fields confirmation" : "Start over confirmation"} style={{ position: "absolute", top: "calc(100% + 6px)", right: 0, width: "260px", padding: "14px", background: "white", border: "1px solid var(--nsp-border)", borderRadius: "16px", boxShadow: "0 18px 44px rgba(0,0,0,0.18)", zIndex: 130 }}>
                                <div style={{ fontFamily: "'Instrument Serif', serif", fontSize: "21px", color: "#1A1A1A", marginBottom: "6px" }}>{editorV2ConfirmAction === "clear" ? "Clear all fields?" : "Start over?"}</div>
                                <div style={{ fontSize: "11px", lineHeight: 1.45, color: "#777", marginBottom: "13px" }}>{editorV2ConfirmAction === "clear" ? "This removes all detected and manually placed fields from this PDF." : "This removes the current PDF and all edits from this session."}</div>
                                <div style={{ display: "flex", justifyContent: "flex-end", gap: "7px" }}>
                                  <button onClick={() => setEditorV2ConfirmAction(null)} style={{ padding: "7px 10px", background: "white", color: "#666", border: "1px solid var(--nsp-border)", borderRadius: "999px", fontSize: "10px", fontWeight: 800, cursor: "pointer" }}>Cancel</button>
                                  <button onClick={() => { const action = editorV2ConfirmAction; setEditorV2ConfirmAction(null); if (action === "clear") { clearEditorEdits(); } else { resetEditorForNewFile([]); } }} style={{ padding: "7px 11px", background: "#C64D32", color: "white", border: "1px solid #C64D32", borderRadius: "999px", fontSize: "10px", fontWeight: 800, cursor: "pointer", boxShadow: "0 7px 16px rgba(198,77,50,0.22)" }}>{editorV2ConfirmAction === "clear" ? "Clear fields" : "Start over"}</button>
                                </div>
                              </div>
                            )}
                          </div>
                        </div>
                      </>}
                    </div>
                  )}

                  <div style={{ display: editorV2Active ? "none" : "flex", gap: editorV2Active ? "4px" : "6px", padding: editorV2Active ? "4px 0 3px" : "8px 0 4px", alignItems: "center", flexWrap: "wrap" }}>
                    <button onClick={performUndo} disabled={!undoStack.length}
                      style={{ padding: "6px 10px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "11px", fontWeight: 700, background: undoStack.length ? "#FFF" : "#F7F7F7", color: undoStack.length ? "#555" : "#BBB", cursor: undoStack.length ? "pointer" : "default", opacity: undoStack.length ? 1 : 0.7 }}>
                      ↶ Undo
                    </button>
                    <button onClick={performRedo} disabled={!redoStack.length}
                      style={{ padding: "6px 10px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "11px", fontWeight: 700, background: redoStack.length ? "#FFF" : "#F7F7F7", color: redoStack.length ? "#555" : "#BBB", cursor: redoStack.length ? "pointer" : "default", opacity: redoStack.length ? 1 : 0.7 }}>
                      ↷ Redo
                    </button>
                    <button onClick={() => applyEditorRotation(-90)} disabled={rotatingEditorPdf}
                      style={{ padding: "6px 10px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "11px", fontWeight: 700, background: rotatingEditorPdf ? "#F7F7F7" : "#FFF", color: rotatingEditorPdf ? "#BBB" : "#555", cursor: rotatingEditorPdf ? "default" : "pointer", opacity: rotatingEditorPdf ? 0.7 : 1 }}>
                      Rotate Left
                    </button>
                    <button onClick={() => applyEditorRotation(90)} disabled={rotatingEditorPdf}
                      style={{ padding: "6px 10px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "11px", fontWeight: 700, background: rotatingEditorPdf ? "#F7F7F7" : "#FFF", color: rotatingEditorPdf ? "#BBB" : "#555", cursor: rotatingEditorPdf ? "default" : "pointer", opacity: rotatingEditorPdf ? 0.7 : 1 }}>
                      Rotate Right
                    </button>
                    {(editorMode === "edit" || placingTextField || editorV2Active) && (
                      <button onClick={() => {
                        const nextArmed = !placingTextField;
                        setEditMode("text");
                        setPlacingTextField(nextArmed);
                        setEditingItemId(null);
                        setEditSubmode(selId ? "selected" : "idle");
                      }}
                        title={placingTextField ? "Stop placing text fields" : "Place text field"}
                        aria-pressed={placingTextField}
                        style={{ padding: "6px 10px", borderRadius: "8px", border: placingTextField ? "1px solid #E85D3A" : "1px solid #EEE", fontSize: "11px", fontWeight: 800, background: placingTextField ? "#E85D3A" : "#FFF", color: placingTextField ? "white" : "#555", cursor: "pointer", boxShadow: placingTextField ? "0 0 0 3px rgba(232,93,58,0.16)" : "none" }}>
                        {placingTextField ? "Text armed" : "+ Text"}
                      </button>
                    )}
                    <span style={{ display: editorV2Active ? "none" : undefined, fontSize: "9px", color: "#BBB" }}>Ctrl/Cmd+Z · Ctrl+Y</span>
                    <button disabled={!hasEditableOverlays} title={editorMode === "edit" ? "Finish review" : reviewFieldsTitle} aria-label={editorMode === "edit" ? "Finish review" : "Review fields"} onClick={() => {
                      if (!hasEditableOverlays && editorMode !== "edit") return;
                      const nextMode = editorMode === "edit" ? "fill" : "edit";
                      setEditorMode(nextMode);
                      if (nextMode === "edit") setEditMode("text");
                      setSelId(null);
                      setEditingItemId(null);
                      setEditSubmode("idle");
                      setPlacingTextField(false);
                    }} style={{ marginLeft: "auto", padding: "6px 12px", borderRadius: "8px", border: "1px solid #C0C0C0", fontSize: "11px", fontWeight: 600, background: editorMode === "edit" ? "#666666" : "white", color: !hasEditableOverlays && editorMode !== "edit" ? "#B8B8B8" : (editorMode === "edit" ? "white" : "#666666"), cursor: !hasEditableOverlays && editorMode !== "edit" ? "not-allowed" : "pointer", opacity: !hasEditableOverlays && editorMode !== "edit" ? 0.72 : 1 }}>
                      {editorMode === "edit" ? "Done" : "Review fields"}
                    </button>
                  </div>

                  {editorMode === "edit" && (
                    <div style={{ display: "flex", gap: "8px", padding: "7px 8px", margin: "4px 0 6px", alignItems: "center", flexWrap: "wrap", background: "#FFF8F5", border: "1px solid #FDDDD4", borderRadius: "9px" }}>
                      <span style={{ fontSize: "9px", color: "#E85D3A", fontWeight: 800, letterSpacing: "0.6px" }}>INSPECTOR</span>
                      {!selectedItem && <span style={{ fontSize: "10px", color: "#AAA" }}>Select a field to edit type, size, or delete.</span>}
                      {selectedItem && <>
                        <label style={{ display: "flex", alignItems: "center", gap: "4px", fontSize: "10px", color: "#888", fontWeight: 700 }}>Type
                          <select value={selectedItem.type === "image" ? "signature" : selectedItem.type} onChange={e => {
                            const type = e.target.value;
                          setItems(function(prev) {
                            return prev.map(function(it) {
                              if (it.id !== selectedItem.id) return it;
                              var next = { ...it, type: type === "signature" ? "image" : type };
                              if (type === "text") {
                                next.text = next.text || "";
                                next.width = next.width || 120;
                                next.height = next.height || 14;
                              } else if (type === "signature") {
                                next.src = next.src || signatureImg || "";
                                next.width = next.width || 150;
                                next.height = next.height || 50;
                              } else {
                                next.checked = Boolean(next.checked);
                                next.width = next.width || 12;
                                next.height = next.height || 12;
                                if (type === "checkbox") delete next.groupId;
                              }
                              return next;
                            });
                          });
                          if (type === "text") setEditingItemId(selectedItem.id);
                          else setEditingItemId(null);
                          }} style={{ padding: "5px 8px", borderRadius: "7px", border: "1px solid #EEE", background: "white", color: "#555", fontSize: "10px", fontWeight: 700 }}>
                            <option value="text">Text</option>
                            <option value="checkbox">Checkbox</option>
                            <option value="radio">Radio</option>
                            <option value="signature" disabled={!signatureImg && selectedItem.type !== "image"}>Signature</option>
                          </select>
                        </label>
                        {selectedManualTextItem && (
                          <button onClick={() => {
                            if (editingItemId === selectedManualTextItem.id) {
                              setEditingItemId(null);
                              setEditSubmode("selected");
                            } else {
                              setEditingItemId(selectedManualTextItem.id);
                              setEditSubmode("typing");
                              setManualFocusItemId(selectedManualTextItem.id);
                            }
                          }} style={{ padding: "5px 10px", borderRadius: "7px", border: "1px solid #2D7FF9", background: editingItemId === selectedManualTextItem.id ? "#2D7FF9" : "white", color: editingItemId === selectedManualTextItem.id ? "white" : "#2D7FF9", fontSize: "10px", fontWeight: 800, cursor: "pointer" }}>
                            {editingItemId === selectedManualTextItem.id ? "Done typing" : "Type"}
                          </button>
                        )}
                        {selectedItem.type === "text" && (
                          <div style={{ display: "flex", alignItems: "center", gap: "3px" }}>
                            <span style={{ fontSize: "10px", color: "#888", fontWeight: 700 }}>Size</span>
                            <button onClick={() => { const nv = getSteppedFieldFontSize(selectedItem, -1); setFontSize(nv); setItems(p => p.map(it => it.id === selectedItem.id ? { ...it, fontSize: nv, manualFontSize: true } : it)); }} style={{ width: "22px", height: "22px", borderRadius: "5px", border: "1px solid #EEE", background: "white", color: "#888", cursor: "pointer", fontWeight: 700 }}>−</button>
                            <span style={{ minWidth: "22px", textAlign: "center", fontSize: "10px", fontWeight: 800, color: "#555" }}>{formatDisplayNumber(getVisibleFieldFontSize(selectedItem))}</span>
                            <button onClick={() => { const nv = getSteppedFieldFontSize(selectedItem, 1); setFontSize(nv); setItems(p => p.map(it => it.id === selectedItem.id ? { ...it, fontSize: nv, manualFontSize: true } : it)); }} style={{ width: "22px", height: "22px", borderRadius: "5px", border: "1px solid #EEE", background: "white", color: "#888", cursor: "pointer", fontWeight: 700 }}>+</button>
                          </div>
                        )}
                        <button onClick={deleteSelectedItem} style={{ marginLeft: "auto", padding: "5px 9px", borderRadius: "7px", border: "none", background: "#E85D3A", color: "white", fontSize: "10px", fontWeight: 800, cursor: "pointer" }}>Delete</button>
                      </>}
                    </div>
                  )}

                  {/* SIZE CONTROLS for text mode */}
                  {false && editorMode === "edit" && editMode === "text" && (
                    <div style={{ display: "flex", alignItems: "center", gap: "4px", padding: "6px 0", flexWrap: "wrap" }}>
                      {[8, 10, 12, 14, 18, 24].map(s => <button key={s} onClick={() => {
                        setFontSize(s);
                        // Also update the selected field if one is selected
                        if (selId) setItems(p => p.map(it => it.id === selId && it.type === "text" ? { ...it, fontSize: s, manualFontSize: true } : it));
                      }} style={{ width: "22px", height: "22px", borderRadius: "5px", border: "none", fontSize: "9px", fontWeight: fontSize === s ? 800 : 500, background: fontSize === s ? "#1A1A1A" : "#F0F0F0", color: fontSize === s ? "white" : "#AAA", cursor: "pointer" }}>{s}</button>)}
                      <div style={{ display: "flex", alignItems: "center" }}>
                        <button onClick={() => {
                          const nv = Math.max(4, fontSize - 1); setFontSize(nv);
                          if (selId) setItems(p => p.map(it => it.id === selId && it.type === "text" ? { ...it, fontSize: nv, manualFontSize: true } : it));
                        }} style={{ width: "20px", height: "22px", borderRadius: "5px 0 0 5px", border: "1px solid #EEE", borderRight: "none", background: "#FAFAFA", fontSize: "12px", color: "#888", cursor: "pointer", fontWeight: 700 }}>−</button>
                        <input type="number" value={fontSize} onChange={e => { const v = parseInt(e.target.value); if (v >= 4 && v <= 72) { setFontSize(v); if (selId) setItems(p => p.map(it => it.id === selId && it.type === "text" ? { ...it, fontSize: v, manualFontSize: true } : it)); } }} style={{ width: "28px", height: "22px", border: "1px solid #EEE", textAlign: "center", fontSize: "10px", fontWeight: 700, outline: "none", padding: 0, boxSizing: "border-box" }} />
                        <button onClick={() => {
                          const nv = Math.min(72, fontSize + 1); setFontSize(nv);
                          if (selId) setItems(p => p.map(it => it.id === selId && it.type === "text" ? { ...it, fontSize: nv, manualFontSize: true } : it));
                        }} style={{ width: "20px", height: "22px", borderRadius: "0 5px 5px 0", border: "1px solid #EEE", borderLeft: "none", background: "#FAFAFA", fontSize: "12px", color: "#888", cursor: "pointer", fontWeight: 700 }}>+</button>
                      </div>
                      {selId && <span style={{ fontSize: "8px", color: "#BBB", marginLeft: "4px" }}>selected field</span>}
                    </div>
                  )}
                  <div style={{ display: editorV2Active ? "none" : "flex", alignItems: "center", gap: editorV2Active ? "4px" : "6px", padding: editorV2Active ? "0 0 4px" : "0 0 8px", flexWrap: "wrap" }}>
                    <span style={{ fontSize: "9px", color: "#AAA", fontWeight: 700, letterSpacing: "0.5px" }}>ZOOM</span>
                    {[1, 1.25, 1.5].map(z => <button key={z} onClick={() => setZoom(z)} style={{ padding: "4px 8px", borderRadius: "6px", border: "none", fontSize: "10px", fontWeight: zoom === z ? 800 : 600, background: zoom === z ? "#1A1A1A" : "#F0F0F0", color: zoom === z ? "white" : "#888", cursor: "pointer" }}>{Math.round(z * 100)}%</button>)}
                    <button onClick={() => setZoom(z => Math.max(0.75, Math.round((z - 0.1) * 100) / 100))} style={{ width: "24px", height: "24px", borderRadius: "6px", border: "1px solid #EEE", background: "white", fontSize: "12px", fontWeight: 700, color: "#888", cursor: "pointer" }}>−</button>
                    <button onClick={() => setZoom(z => Math.min(2.5, Math.round((z + 0.1) * 100) / 100))} style={{ width: "24px", height: "24px", borderRadius: "6px", border: "1px solid #EEE", background: "white", fontSize: "12px", fontWeight: 700, color: "#888", cursor: "pointer" }}>+</button>
                    <button onClick={() => setZoom(1)} style={{ padding: "4px 8px", borderRadius: "6px", border: "1px solid #EEE", background: "white", fontSize: "10px", fontWeight: 700, color: "#888", cursor: "pointer" }}>Fit</button>
                    <span style={{ display: editorV2Active ? "none" : undefined, fontSize: "8px", color: "#BBB" }}>zoom in for tiny boxes instead of forcing oversized text</span>
                  </div>

                  {placingTextField && (
                    <div role="status" aria-live="polite" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", padding: "9px 12px", background: "#FFF5F2", border: "1px solid #FDDDD4", borderRadius: "10px", margin: "6px 0 8px", color: "#9A3412", fontSize: "12px", fontWeight: 800 }}>
                      <span>Text placement armed. Double-click the page to place text. Press Esc to cancel. Review Fields is separate.</span>
                      <button onClick={() => { setPlacingTextField(false); setEditSubmode("idle"); }} style={{ border: "1px solid #FDDDD4", background: "white", color: "#9A3412", borderRadius: "999px", padding: "4px 9px", fontSize: "10px", fontWeight: 800, cursor: "pointer", flexShrink: 0 }}>Cancel</button>
                    </div>
                  )}
                  {/* Sign placement hint */}
                  {editorMode === "edit" && placingSign && <div style={{ padding: "7px 12px", background: "#EEF6FF", borderRadius: "8px", marginBottom: "8px", border: "1px solid #D0E8FF", fontSize: "11px", color: "#2D7FF9", fontWeight: 600, textAlign: "center" }}>👆 Tap to place your signature</div>}
                  {editorMode === "edit" && placingImg && <div style={{ padding: "7px 12px", background: "#EEF6FF", borderRadius: "8px", marginBottom: "8px", border: "1px solid #D0E8FF", fontSize: "11px", color: "#2D7FF9", fontWeight: 600, textAlign: "center" }}>👆 Tap to place the image</div>}
                  {editorMode === "edit" && signatureImg && editMode === "sign" && (
                    <div style={{ display: "flex", gap: "6px", marginBottom: "8px", alignItems: "center" }}>
                      <button onClick={() => setPlacingSign(true)} style={{ padding: "6px 12px", borderRadius: "8px", border: "1px solid #4F46E5", fontSize: "11px", fontWeight: 700, background: "white", color: "#4F46E5", cursor: "pointer" }}>📌 Place again</button>
                      <button onClick={() => { setSignatureCreationRequested(true); setPlacingSign(false); setSignatureImg(null); setShowSignPad(true); }} style={{ padding: "6px 12px", borderRadius: "8px", border: "1px solid #EEE", fontSize: "11px", fontWeight: 600, background: "white", color: "#888", cursor: "pointer" }}>New signature</button>
                      <img src={signatureImg} style={{ height: "28px", borderRadius: "4px", border: "1px solid #EEE" }} />
                    </div>
                  )}

                  <div style={{ display: editorV2Active ? "none" : undefined, fontSize: editorV2Active ? "8px" : "9px", color: "#CCC", textAlign: "center", padding: editorV2Active ? "0" : "3px 0" }}>
                    {editorMode === "fill" ? (hasEditableOverlays ? "Fill mode · tap fields to type, check, or choose · Review fields to adjust overlays" : "Fill mode · native PDF fields are fillable but locked") : placingTextField ? "Double-click the page to place a text field" : "Review fields · move, resize, or delete editable overlays"}
                  </div>

                  {postPlacementHintVisible && (
                    <div style={{ padding: "8px 12px", background: "#EBF5FF", color: "#1E3A5F", borderRadius: "6px", marginBottom: "8px", border: "1px solid #CFE6FF", fontSize: "13px", fontWeight: 600, textAlign: "center" }}>
                      Field placed. Drag or resize it, or choose Type to fill it.
                    </div>
                  )}

                  {/* PDF CANVAS */}
                  <div style={{ position: "relative", display: editorV2Active ? "flex" : undefined, flexDirection: editorV2Active ? "column" : undefined, flex: editorV2Active ? "1 1 0" : undefined, minHeight: editorV2Active ? 0 : undefined, overflow: editorV2Active ? "hidden" : undefined }}>
                  {editorV2Active && editorV2DockAvailable && (
                    <div style={{ position: "absolute", top: "18px", left: `calc(50% - ${(dim.width * scale) / 2 + 66}px)`, width: "54px", zIndex: 70, display: "flex", flexDirection: "column", gap: "6px", padding: "7px 6px", background: "rgba(255,255,255,0.97)", border: "1px solid var(--nsp-border)", borderRadius: "18px", boxShadow: "0 12px 30px rgba(0,0,0,0.14)", backdropFilter: "blur(8px)" }}>
                      {[
                        { label: "ADD", ids: ["text", "check", "sign", "image"] },
                        { label: "MARK", ids: ["highlight", "note"] }
                      ].map(group => (
                        <React.Fragment key={group.label}>
                          <div style={{ margin: group.label === "ADD" ? "0 0 -2px" : "2px 0 -2px", color: "#9A9A9A", fontSize: "7px", fontWeight: 900, letterSpacing: "0.08em", textAlign: "center" }}>{group.label}</div>
                          {EDIT_MODES.filter(m => group.ids.includes(m.id)).map(m => (
                            <button key={`dock-${m.id}`} onClick={() => {
                              if (editorV2Active) setEditorMode(m.id === "text" ? "fill" : "edit");
                              setEditMode(m.id); setEditingItemId(null); setEditSubmode("idle"); setPlacingTextField(editorV2Active && m.id === "text");
                              if (m.id === "sign") {
                                setPlacingSign(false);
                                if (savedSignatures.length > 0) {
                                  setSignatureCreationRequested(false);
                                  setShowSignPad(false);
                                  if (!signatureImg) setSignatureImg(savedSignatures[0].src);
                                } else if (!signatureImg) {
                                  setSignatureCreationRequested(false);
                                  setShowSignPad(true);
                                }
                              } else {
                                setSignatureCreationRequested(false);
                                setShowSignPad(false);
                              }
                              if (m.id === "image") {
                                const inp = document.createElement("input"); inp.type = "file"; inp.accept = "image/*";
                                inp.onchange = async () => { const f = inp.files[0]; if (f) { setAddImgSrc(await readDataURL(f)); setPlacingImg(true); } };
                                inp.click();
                              }
                            }}
                              title={m.label}
                              aria-label={m.label}
                              style={{ minHeight: "34px", padding: "0", borderRadius: "12px", border: isToolVisuallyActive(m.id) ? "1px solid #B8DBFF" : "1px solid var(--nsp-border)", fontSize: "16px", fontWeight: 800, lineHeight: 1, background: isToolVisuallyActive(m.id) ? "#EEF6FF" : "var(--nsp-surface-raised)", color: isToolVisuallyActive(m.id) ? "#2D7FF9" : "var(--nsp-text-muted)", cursor: "pointer", transition: "all 0.1s", width: "100%", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: isToolVisuallyActive(m.id) ? "0 5px 14px rgba(45,127,249,0.16)" : "none" }}>
                              <span aria-hidden="true">{m.label.split(" ")[0]}</span>
                            </button>
                          ))}
                        </React.Fragment>
                      ))}
                      <div style={{ margin: "2px 0 -2px", color: "#9A9A9A", fontSize: "7px", fontWeight: 900, letterSpacing: "0.08em", textAlign: "center" }}>REVIEW</div>
                      <button disabled={!hasEditableOverlays} title={editorMode === "edit" ? "Finish review" : reviewFieldsTitle} aria-label={editorMode === "edit" ? "Finish review" : "Review fields"} onClick={() => {
                        if (!hasEditableOverlays && editorMode !== "edit") return;
                        const nextMode = editorMode === "edit" ? "fill" : "edit";
                        setEditorMode(nextMode);
                        if (nextMode === "edit") setEditMode("text");
                        setSelId(null);
                        setEditingItemId(null);
                        setEditSubmode("idle");
                        setPlacingTextField(false);
                      }} style={{ minHeight: "34px", padding: "0", borderRadius: "12px", border: editorMode === "edit" ? "1px solid #C0C0C0" : "1px solid var(--nsp-border)", fontSize: "14px", fontWeight: 900, lineHeight: 1, background: editorMode === "edit" ? "#555" : "white", color: !hasEditableOverlays && editorMode !== "edit" ? "#B8B8B8" : (editorMode === "edit" ? "white" : "#666"), cursor: !hasEditableOverlays && editorMode !== "edit" ? "not-allowed" : "pointer", opacity: !hasEditableOverlays && editorMode !== "edit" ? 0.72 : 1, width: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}>
                        {editorMode === "edit" ? "✓" : "↕"}
                      </button>
                    </div>
                  )}
                  <div ref={pdfViewportRef} style={{ overflow: "auto", borderRadius: editorV2Active ? "12px" : "8px", border: "1px solid #EEE", boxShadow: "0 2px 14px rgba(0,0,0,0.07)", background: "var(--nsp-bg-workspace)", maxHeight: editorV2Active ? "none" : "76vh", height: editorV2Active ? 0 : undefined, minHeight: editorV2Active ? 0 : undefined, position: "relative", display: editorV2Active ? "flex" : undefined, justifyContent: editorV2Active ? "center" : undefined, alignItems: editorV2Active ? "flex-start" : undefined, padding: editorV2Active ? "12px" : undefined, boxSizing: editorV2Active ? "border-box" : undefined, flex: editorV2Active ? "1 1 0" : undefined }}>
                    <div ref={pdfContainerRef} onClick={handleCanvasClick} onDoubleClick={handleCanvasDoubleClick} style={{ position: "relative", width: dim ? `${dim.width * scale}px` : "100%", minWidth: editorV2Active ? "0" : "100%", margin: editorV2Active ? "0 auto" : undefined, cursor: placingTextField ? "text" : (editorMode === "edit" ? (placingSign || placingImg ? "copy" : "default") : "default"), userSelect: "none", WebkitUserSelect: "none", MozUserSelect: "none", msUserSelect: "none" }}>
                      <img src={imgs[pg]} style={{ width: dim ? `${dim.width * scale}px` : "100%", display: "block", pointerEvents: "none", WebkitUserDrag: "none", userSelect: "none", WebkitUserSelect: "none", MozUserSelect: "none", msUserSelect: "none", borderRadius: "8px" }} />

                      {/* RENDER ITEMS */}
                      {pageItems.map(it => (
                        <DragItem key={`${it.id}_${it.page}`} it={it} scale={scale} sel={selId === it.id} editorMode={editorMode}
                          onSel={() => { setSelId(it.id); if (editorMode === "edit" && it.auto !== true) setEditSubmode("selected"); if (it.type === "text" && it.fontSize) setFontSize(it.fontSize); }}
                          onClearSel={() => { setSelId(null); setEditingItemId(null); setEditSubmode("idle"); }}
                          onUp={u => setItems(p => p.map(x => x.id === u.id ? u : x))}
                          onDel={() => { setItems(p => p.filter(x => x.id !== it.id)); setSelId(null); setEditingItemId(null); setEditSubmode("idle"); }}
                          editingId={editingItemId} setEditingId={setEditingItemId}
                          onStartTyping={() => { if (it.auto === true || it.type !== "text") return; setSelId(it.id); setEditingItemId(it.id); setEditSubmode("typing"); setManualFocusItemId(it.id); if (it.fontSize) setFontSize(it.fontSize); }}
                          onStopTyping={() => { if (editorMode === "edit" && it.auto !== true) setEditSubmode("selected"); }}
                          manualFocusItemId={manualFocusItemId}
                          onManualFocusHandled={handleManualFocusHandled}
                          setItems={setItems}
                        />
                      ))}
                    </div>
                  </div>
                  </div>

                  {/* Page nav */}
                  {imgs.length > 1 && <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "12px", padding: editorV2Active ? "8px 0 6px" : "10px 0", flexShrink: editorV2Active ? 0 : undefined }}><button onClick={() => setPg(p => Math.max(0, p - 1))} disabled={pg === 0} style={{ background: editorV2Active ? "white" : "none", border: "1px solid #EEE", borderRadius: "7px", padding: "5px 12px", fontSize: "11px", cursor: pg === 0 ? "default" : "pointer", color: pg === 0 ? "#DDD" : "#888", fontWeight: 600 }}>←</button><span style={{ fontSize: "11px", color: "#777", fontWeight: 700 }}>{editorV2Active ? `Page ${pg + 1} of ${imgs.length}` : `${pg + 1} / ${imgs.length}`}</span><button onClick={() => setPg(p => Math.min(imgs.length - 1, p + 1))} disabled={pg === imgs.length - 1} style={{ background: editorV2Active ? "white" : "none", border: "1px solid #EEE", borderRadius: "7px", padding: "5px 12px", fontSize: "11px", cursor: pg === imgs.length - 1 ? "default" : "pointer", color: pg === imgs.length - 1 ? "#DDD" : "#888", fontWeight: 600 }}>→</button></div>}
                  </EditorWorkspaceFrame>

                  {!editorV2Active && <>
                    <div style={{ fontSize: "10px", color: "#CCC", textAlign: "center", marginBottom: "6px" }}>{filledCount} edit{filledCount !== 1 ? "s" : ""} made</div>
                    <button onClick={saveEditor} disabled={saving || !filledCount} style={{ width: "100%", padding: "14px", background: filledCount ? "#E85D3A" : "#EEE", color: filledCount ? "white" : "#CCC", border: "none", borderRadius: "12px", fontSize: "14px", fontWeight: 700, cursor: filledCount ? "pointer" : "default", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px" }}>
                      {saving && <div style={{ width: "16px", height: "16px", border: "2px solid rgba(255,255,255,0.25)", borderTopColor: "white", borderRadius: "50%", animation: "nsS 0.6s linear infinite" }} />}
                      {saving ? "Generating..." : "↓ Download edited PDF · free"}
                    </button>
                    {items.length > 0 && <button onClick={() => { if (confirm("Clear all fields and edits?")) { clearEditorEdits(); } }} style={{ width: "100%", marginTop: "8px", padding: "10px", background: "none", border: "1px solid #FDDDD4", borderRadius: "10px", fontSize: "12px", color: "#E85D3A", cursor: "pointer", fontWeight: 600 }}>Clear all fields</button>}
                    <label style={{ display: "block", width: "100%", marginTop: "8px", padding: "10px", background: "none", border: "1px solid #2D7FF9", borderRadius: "10px", fontSize: "12px", color: "#2D7FF9", cursor: "pointer", textAlign: "center", fontWeight: 600, boxSizing: "border-box" }}>
                      📄 Load different file
                      <input type="file" accept=".pdf" onChange={e => { if (e.target.files[0]) { resetEditorForNewFile([e.target.files[0]]); } }} style={{ display: "none" }} />
                    </label>
                    <button onClick={() => { resetEditorForNewFile([]); }} style={{ width: "100%", marginTop: "8px", padding: "10px", background: "none", border: "1px solid #EEE", borderRadius: "10px", fontSize: "12px", color: "#CCC", cursor: "pointer" }}>Start over</button>
                  </>}
                </>
              )}
            </>
          )}

          {/* REORDER */}
          {ready && toolId === "reorder" && files.length > 0 && pdfBytes && <ReorderView file={files[0]} fileBytes={pdfBytes} />}

          {/* STANDARD TOOLS */}
          {ready && isStd && (files.length > 0 || toolId === "compress") && (
            <div>
              {toolId === "merge" && (
                <div data-merge-action-bar="true" style={{ position: "sticky", top: editorV2Flag ? "64px" : "8px", zIndex: 20, margin: "0 0 10px", padding: "10px", background: "rgba(255,255,255,0.96)", border: "1px solid #DCEBFF", borderRadius: "13px", boxShadow: "0 10px 28px rgba(45,127,249,0.12)", backdropFilter: "blur(10px)" }}>
                  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px" }}>
                    <div style={{ minWidth: 0 }}>
                      <div style={{ fontSize: "12px", color: "#1A1A1A", fontWeight: 900, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{result ? `${files.length} file${files.length !== 1 ? "s" : ""} merged` : `${files.length} file${files.length !== 1 ? "s" : ""}`} · {fmtB(mergeTotalBytes)}</div>
                      <div style={{ fontSize: "10px", color: "#2D7FF9", fontWeight: 800, lineHeight: 1.4 }}>{result ? "Your merged PDF is ready." : "Files merge in the order shown."}</div>
                      {!result && !mergeIsVeryLarge && <div style={{ fontSize: "9px", color: "#7AA3D7", fontWeight: 700, lineHeight: 1.4 }}>Merge up to 20 PDFs free. Upgrade to Pro for more.</div>}
                      {mergeIsVeryLarge && !result && <div role="status" aria-live="polite" style={{ marginTop: "5px", padding: "6px 8px", background: "#FFF8E8", border: "1px solid #F4D48A", borderRadius: "8px", fontSize: "10px", lineHeight: 1.35, color: "#8A5A00", fontWeight: 800 }}>Large merge may be slow in your browser. Try fewer or smaller files.</div>}
                    </div>
                    <button type="button" onClick={result ? download : process} disabled={processing} style={{ minHeight: "38px", padding: "9px 14px", background: processing ? "#9CC7FF" : "#2D7FF9", color: "white", border: "none", borderRadius: "11px", fontSize: "12px", fontWeight: 900, cursor: processing ? "wait" : "pointer", whiteSpace: "nowrap", boxShadow: "0 8px 18px rgba(45,127,249,0.24)" }}>{processing ? "Merging..." : result ? "Download merged" : mergeIsVeryLarge ? "Merge anyway" : "Merge"}</button>
                  </div>
                </div>
              )}
              {mergeIsVeryLarge && !result && <div role="status" aria-live="polite" style={{ margin: "0 0 8px", padding: "9px 10px", background: "#FFF8E8", border: "1px solid #F4D48A", borderRadius: "9px", fontSize: "11px", lineHeight: 1.45, color: "#8A5A00", fontWeight: 700 }}>This merge is very large and may be slow or unstable in your browser. Try fewer or smaller files.</div>}
              {!(toolId === "compress" && compressWorkflow === "batch") && files.map((f, i) => {
                const isMerge = toolId === "merge";
                const dropActive = isMerge && mergeDropIndex === i && mergeDragIndex !== null && mergeDragIndex !== i;
                return (
                  <div key={`${f.name}-${f.size}-${f.lastModified || i}-${i}`} data-merge-file-row={isMerge ? "true" : undefined} data-file-name={isMerge ? f.name : undefined} draggable={isMerge}
                    onDragStart={isMerge ? e => { setMergeDragIndex(i); setMergeDropIndex(i); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(i)); } : undefined}
                    onDragOver={isMerge ? e => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setMergeDropIndex(i); } : undefined}
                    onDrop={isMerge ? e => { e.preventDefault(); const from = mergeDragIndex ?? Number(e.dataTransfer.getData("text/plain")); if (Number.isFinite(from)) dropMergeFileAt(from, i); } : undefined}
                    onDragEnd={isMerge ? () => { setMergeDragIndex(null); setMergeDropIndex(null); } : undefined}
                    style={{ position: "relative", display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px", padding: "9px 11px", background: isMerge && mergeDragIndex === i ? "#F1F7FF" : "#FAFAFA", border: isMerge ? "1px solid #E6EEF8" : "1px solid transparent", borderRadius: "9px", marginBottom: "4px", cursor: isMerge ? "grab" : "default", opacity: isMerge && mergeDragIndex === i ? 0.72 : 1 }}>
                    {dropActive && <div data-merge-drop-indicator="true" style={{ position: "absolute", left: "8px", right: "8px", top: "-3px", height: "3px", borderRadius: "999px", background: "#2D7FF9", boxShadow: "0 0 0 2px rgba(45,127,249,0.14)" }} />}
                    <div style={{ display: "flex", alignItems: "center", gap: "7px", minWidth: 0 }}>
                      {isMerge && <span data-merge-drag-handle="true" aria-hidden="true" title="Drag to reorder" style={{ width: "18px", height: "24px", display: "inline-flex", alignItems: "center", justifyContent: "center", borderRadius: "7px", background: "#EEF6FF", color: "#2D7FF9", fontSize: "15px", fontWeight: 900, cursor: "grab", flexShrink: 0 }}>☰</span>}
                      <span aria-hidden="true">{isMerge ? `${i + 1}.` : "📄"}</span>
                      <div style={{ minWidth: 0 }}>
                        <div style={{ fontSize: "11px", fontWeight: 600, color: "#1A1A1A", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{f.name}</div>
                        <div style={{ fontSize: "9px", color: "#CCC" }}>{fmtB(f.size)}</div>
                      </div>
                    </div>
                    <div style={{ display: "flex", alignItems: "center", gap: "5px", flexShrink: 0 }}>
                      {isMerge && <>
                        <button type="button" aria-label={`Move ${f.name} up`} title="Move up" disabled={i === 0} onClick={() => moveFileAt(i, -1)} style={{ minHeight: "30px", padding: "6px 8px", background: i === 0 ? "#F5F5F5" : "white", border: "1px solid #E6EEF8", borderRadius: "8px", fontSize: "11px", cursor: i === 0 ? "not-allowed" : "pointer", color: i === 0 ? "#BBB" : "#2D7FF9", fontWeight: 700 }}>Up</button>
                        <button type="button" aria-label={`Move ${f.name} down`} title="Move down" disabled={i === files.length - 1} onClick={() => moveFileAt(i, 1)} style={{ minHeight: "30px", padding: "6px 8px", background: i === files.length - 1 ? "#F5F5F5" : "white", border: "1px solid #E6EEF8", borderRadius: "8px", fontSize: "11px", cursor: i === files.length - 1 ? "not-allowed" : "pointer", color: i === files.length - 1 ? "#BBB" : "#2D7FF9", fontWeight: 700 }}>Down</button>
                      </>}
                      <button type="button" aria-label={isMerge ? `Remove ${f.name}` : "Remove file"} title="Remove file" onClick={() => removeFileAt(i)} onMouseEnter={e => { e.currentTarget.style.background = "#FFF5F5"; e.currentTarget.style.borderColor = "#FCA5A5"; }} onMouseLeave={e => { e.currentTarget.style.background = "white"; e.currentTarget.style.borderColor = "#FDD"; }} onFocus={e => { e.currentTarget.style.background = "#FFF5F5"; e.currentTarget.style.borderColor = "#FCA5A5"; }} onBlur={e => { e.currentTarget.style.background = "white"; e.currentTarget.style.borderColor = "#FDD"; }} style={{ minHeight: "30px", padding: "6px 10px", background: "white", border: "1px solid #FDD", borderRadius: "8px", fontSize: "11px", cursor: "pointer", color: "#DC2626", fontWeight: 700 }}>Remove</button>
                    </div>
                  </div>
                );
              })}
              {toolId === "merge" && files.length > 1 && !result && <div data-merge-drop-tail="true" onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setMergeDropIndex(files.length); }} onDrop={e => { e.preventDefault(); const from = mergeDragIndex ?? Number(e.dataTransfer.getData("text/plain")); if (Number.isFinite(from)) dropMergeFileAt(from, files.length); }} style={{ position: "relative", height: "10px", marginTop: "-2px", marginBottom: "4px" }}>{mergeDropIndex === files.length && mergeDragIndex !== null && <div data-merge-drop-indicator="true" style={{ position: "absolute", left: "8px", right: "8px", top: "3px", height: "3px", borderRadius: "999px", background: "#2D7FF9", boxShadow: "0 0 0 2px rgba(45,127,249,0.14)" }} />}</div>}
              {tool.multi && !result && <label style={{ display: "block", padding: "9px", border: "1px dashed #E0E0E0", borderRadius: "9px", textAlign: "center", fontSize: "11px", color: "#CCC", cursor: "pointer", marginBottom: "5px" }}><input type="file" accept={tool.accepts} multiple onChange={e => addFiles(Array.from(e.target.files))} style={{ display: "none" }} />+ Add more</label>}
              {toolId === "compress" && !result && <div data-compress-workflow style={{ margin: "10px 0", padding: "12px", background: "#FAFAFA", border: "1px solid #EEE", borderRadius: "12px" }}>
                <div data-compress-workflow-selector role="tablist" aria-label="Compression workflow" style={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: "8px", marginBottom: "12px" }}>
                  {[
                    { id: "single", title: "Single PDF", desc: "Compress one PDF. No signup." },
                    { id: "batch", title: "Batch PDFs", desc: "Compress multiple scanned PDFs at once and download one ZIP.", pro: true },
                  ].map(wf => {
                    const selected = compressWorkflow === wf.id;
                    return (
                      <button key={wf.id} type="button" role="tab" aria-selected={selected ? "true" : "false"} onClick={() => selectCompressWorkflow(wf.id)} style={{ textAlign: "left", padding: "11px", borderRadius: "12px", border: selected ? "2px solid #18A558" : "1px solid #E5E7EB", background: selected ? "#F0FAF4" : "white", cursor: "pointer" }}>
                        <span style={{ display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap", fontSize: "13px", color: "#1A1A1A", fontWeight: 900 }}>{wf.title}{wf.pro && <span style={{ fontSize: "9px", color: "#E85D3A", background: "#FFF5F2", border: "1px solid #FDDDD4", borderRadius: "999px", padding: "2px 6px", fontWeight: 900, letterSpacing: "0.06em" }}>PRO</span>}</span>
                        <span style={{ display: "block", marginTop: "4px", fontSize: "11px", color: "#666", lineHeight: 1.4 }}>{wf.desc}</span>
                      </button>
                    );
                  })}
                </div>

                {compressWorkflow === "single" && (
                  <div data-compress-single-workflow>
                    {files.length === 0 ? (
                      <Drop tool={tool} onFiles={addFiles} />
                    ) : (
                      <div data-compress-mode-panel>
                        <div style={{ fontSize: "10px", fontWeight: 800, color: "#777", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: "8px" }}>Compression mode</div>
                        <div style={{ display: "grid", gap: "8px" }}>
                          {[{ id: "standard", title: "Preserve text & forms", desc: "Safest option. Optimizes without flattening pages." }, { id: "scan", title: "High compression for scans", desc: "Much smaller files. Best for scanned PDFs." }].map(mode => (
                            <label key={mode.id} style={{ display: "flex", gap: "9px", alignItems: "flex-start", padding: "10px", border: compressMode === mode.id ? "2px solid #18A558" : "1px solid #E5E7EB", borderRadius: "10px", background: compressMode === mode.id ? "#F0FAF4" : "white", cursor: "pointer" }}>
                              <input type="radio" name="compress-mode" value={mode.id} checked={compressMode === mode.id} onChange={() => { setCompressMode(mode.id); setCompressProgress(null); setError(null); }} style={{ marginTop: "2px" }} />
                              <span style={{ minWidth: 0 }}>
                                <span style={{ display: "block", fontSize: "13px", fontWeight: 900, color: "#1A1A1A" }}>{mode.title}</span>
                                <span style={{ display: "block", fontSize: "11px", lineHeight: 1.45, color: "#666" }}>{mode.desc}</span>
                              </span>
                            </label>
                          ))}
                        </div>
                        {compressMode === "scan" && <div style={{ marginTop: "10px" }}>
                          <div role="note" style={{ padding: "10px", borderRadius: "10px", background: "#FFF7ED", border: "1px solid #FED7AA", color: "#9A3412", fontSize: "11px", lineHeight: 1.45, fontWeight: 700 }}>Rebuilds pages as images, so text may no longer be selectable and form fields may no longer be editable.</div>
                          <div style={{ marginTop: "10px", fontSize: "10px", fontWeight: 800, color: "#777", letterSpacing: "0.08em", textTransform: "uppercase" }}>Quality</div>
                          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(130px, 1fr))", gap: "8px", marginTop: "7px" }}>
                            {Object.entries(COMPRESS_SCAN_PRESETS).map(([id, preset]) => (
                              <label key={id} style={{ display: "block", padding: "9px", borderRadius: "10px", border: compressPreset === id ? "2px solid #18A558" : "1px solid #E5E7EB", background: compressPreset === id ? "#F0FAF4" : "white", cursor: "pointer" }}>
                                <input type="radio" name="compress-preset" value={id} checked={compressPreset === id} onChange={() => { setCompressPreset(id); setCompressProgress(null); }} style={{ marginRight: "6px" }} />
                                <span style={{ fontSize: "12px", fontWeight: 900, color: "#1A1A1A" }}>{preset.label}</span>
                                <span style={{ display: "block", marginLeft: "22px", fontSize: "10px", color: "#777", lineHeight: 1.35 }}>{preset.note}</span>
                              </label>
                            ))}
                          </div>
                        </div>}
                        {compressProgress && <div role="status" aria-live="polite" style={{ marginTop: "10px", padding: "9px 10px", borderRadius: "10px", background: "#EEF6FF", color: "#1D4ED8", fontSize: "11px", fontWeight: 800 }}>Compressing page {compressProgress.page} of {compressProgress.total}...</div>}
                      </div>
                    )}
                  </div>
                )}

                {compressWorkflow === "batch" && hasCompressBatchAccess && (
                  <div data-compress-batch-ui style={{ display: "grid", gap: "10px" }}>
                    <label onDragOver={e => e.preventDefault()} onDrop={e => { e.preventDefault(); addCompressBatchFiles(Array.from(e.dataTransfer.files)); }} style={{ display: "block", padding: "12px", border: "1px dashed #BFE6C8", borderRadius: "11px", textAlign: "center", fontSize: "12px", color: "#118048", fontWeight: 800, cursor: "pointer", background: "#F8FFF9" }}>
                      <input aria-label="Choose PDFs for batch compression" type="file" accept=".pdf" multiple onChange={e => { addCompressBatchFiles(Array.from(e.target.files)); e.target.value = ""; }} style={{ display: "none" }} />
                      Select or drop PDFs for batch compression
                    </label>
                    {compressBatchFiles.length > 0 && (
                      <div style={{ display: "grid", gap: "5px" }}>
                        {compressBatchFiles.map((f, i) => (
                          <div key={`${f.name}-${f.size}-${f.lastModified || i}-${i}`} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "8px", padding: "7px 9px", border: "1px solid #EEF2F0", borderRadius: "9px", background: "#FAFAFA" }}>
                            <span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: "11px", color: "#333", fontWeight: 700 }}>{i + 1}. {f.name} · {fmtB(f.size)}</span>
                            <button type="button" onClick={() => removeCompressBatchFileAt(i)} aria-label={`Remove ${f.name}`} style={{ padding: "5px 8px", border: "1px solid #FDD", borderRadius: "8px", background: "white", color: "#DC2626", fontSize: "10px", fontWeight: 800, cursor: "pointer" }}>Remove</button>
                          </div>
                        ))}
                      </div>
                    )}
                    <div>
                      <div style={{ fontSize: "10px", fontWeight: 800, color: "#777", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: "7px" }}>Batch quality</div>
                      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))", gap: "8px" }}>
                        {Object.entries(COMPRESS_SCAN_PRESETS).map(([id, preset]) => (
                          <label key={id} style={{ display: "block", padding: "8px", borderRadius: "10px", border: compressBatchPreset === id ? "2px solid #18A558" : "1px solid #E5E7EB", background: compressBatchPreset === id ? "#F0FAF4" : "white", cursor: "pointer" }}>
                            <input type="radio" name="compress-batch-preset" value={id} checked={compressBatchPreset === id} onChange={() => { setCompressBatchPreset(id); setCompressBatchProgress(null); }} style={{ marginRight: "6px" }} />
                            <span style={{ fontSize: "12px", fontWeight: 900, color: "#1A1A1A" }}>{preset.label}</span>
                            <span style={{ display: "block", marginLeft: "22px", fontSize: "10px", color: "#777", lineHeight: 1.35 }}>{preset.note}</span>
                          </label>
                        ))}
                      </div>
                    </div>
                    <div role="note" style={{ padding: "9px 10px", borderRadius: "10px", background: "#FFF7ED", border: "1px solid #FED7AA", color: "#9A3412", fontSize: "11px", lineHeight: 1.45, fontWeight: 700 }}>Batch High Compression can use a lot of browser memory. It rebuilds pages as images, so text may no longer be selectable and form fields may no longer be editable.</div>
                    {compressBatchProgress && <div role="status" aria-live="polite" style={{ padding: "9px 10px", borderRadius: "10px", background: "#EEF6FF", color: "#1D4ED8", fontSize: "11px", fontWeight: 800 }}>Compressing {compressBatchProgress.fileName} ({compressBatchProgress.fileIndex} of {compressBatchProgress.fileTotal}), page {compressBatchProgress.page} of {compressBatchProgress.total}...</div>}
                    <button type="button" onClick={processCompressBatch} disabled={processing || compressBatchFiles.length < 2} style={{ width: "100%", padding: "12px", background: processing || compressBatchFiles.length < 2 ? "#9AD8B1" : "#18A558", color: "white", border: "none", borderRadius: "11px", fontSize: "13px", fontWeight: 900, cursor: processing || compressBatchFiles.length < 2 ? "not-allowed" : "pointer" }}>{processing && compressBatchProgress ? "Compressing batch..." : "Compress batch to ZIP"}</button>
                  </div>
                )}
              </div>}
              {toolId === "split" && (splitMode === "visual" || !result) && <div style={{ margin: "8px auto", maxWidth: splitMode === "visual" ? "100%" : "600px" }}>
                <div role="tablist" aria-label="Split mode" style={{ display: "flex", gap: "6px", marginBottom: "8px" }}>
                  {[{ id: "visual", label: "Visual split" }, { id: "advanced", label: "Advanced ranges" }].map(mode => (
                    <button key={mode.id} type="button" role="tab" aria-selected={splitMode === mode.id ? "true" : "false"} onClick={() => { setSplitMode(mode.id); setError(null); }} style={{ flex: 1, minHeight: "34px", borderRadius: "9px", border: splitMode === mode.id ? "2px solid #2D7FF9" : "1px solid #E5E7EB", background: splitMode === mode.id ? "#EEF6FF" : "white", color: splitMode === mode.id ? "#2D7FF9" : "#64748B", fontSize: "12px", fontWeight: 900, cursor: "pointer" }}>{mode.label}</button>
                  ))}
                </div>
                {splitMode === "visual" ? (
                  <SplitVisualPlanner file={files[0]} splitPoints={splitPoints} onSplitPointsChange={points => { setSplitPoints(points); setError(null); setResult(null); setDlData(null); }} onPageCount={count => setSplitPageCount(count)} processing={processing} onProcess={process} result={result} onDownload={download} />
                ) : (
                  <div data-split-advanced-ranges style={{ padding: "11px", background: "#FAFAFA", border: "1px solid #EEE", borderRadius: "12px" }}>
                    <label style={{ fontSize: "10px", fontWeight: 600, color: "#888", display: "block", marginBottom: "4px" }}>Pages (e.g. 1, 3-5, 8)</label>
                    <input value={splitIn} onChange={e => setSplitIn(e.target.value)} placeholder="1, 3-5, 8" inputMode="text" style={{ width: "100%", padding: "9px", border: "1px solid #EEE", borderRadius: "8px", fontSize: "13px", outline: "none", boxSizing: "border-box" }} />
                    <div style={{ marginTop: "6px", fontSize: "10px", lineHeight: 1.45, color: "#888" }}>Use physical PDF page numbers: 1 is the first page in this file, even if Adobe or the printed label shows i, ii, or 7. Each comma creates a separate PDF. Example: 1-14, 15-20</div>
                  </div>
                )}
              </div>}

              {/* Page Numbers options */}
              {toolId === "pagenums" && !result && (
                <div style={{ margin: "8px 0" }}>
                  <label style={{ fontSize: "10px", fontWeight: 600, color: "#888", display: "block", marginBottom: "6px" }}>Position</label>
                  <div style={{ display: "flex", gap: "4px", flexWrap: "wrap", marginBottom: "10px" }}>
                    {[{ v: "bottom-center", l: "Bottom Center" }, { v: "bottom-right", l: "Bottom Right" }, { v: "bottom-left", l: "Bottom Left" }, { v: "top-center", l: "Top Center" }, { v: "top-right", l: "Top Right" }, { v: "top-left", l: "Top Left" }].map(p => (
                      <button key={p.v} onClick={() => setPageNumPos(p.v)} style={{ padding: "5px 10px", borderRadius: "6px", border: pageNumPos === p.v ? "2px solid #6366F1" : "1px solid #EEE", background: pageNumPos === p.v ? "#EEF2FF" : "white", fontSize: "10px", fontWeight: 600, color: pageNumPos === p.v ? "#6366F1" : "#888", cursor: "pointer" }}>{p.l}</button>
                    ))}
                  </div>
                  <label style={{ fontSize: "10px", fontWeight: 600, color: "#888", display: "block", marginBottom: "4px" }}>Font size</label>
                  <div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
                    {[8, 10, 12, 14].map(s => <button key={s} onClick={() => setPageNumSize(s)} style={{ width: "28px", height: "28px", borderRadius: "6px", border: "none", fontSize: "10px", fontWeight: pageNumSize === s ? 800 : 500, background: pageNumSize === s ? "#1A1A1A" : "#F0F0F0", color: pageNumSize === s ? "white" : "#AAA", cursor: "pointer" }}>{s}</button>)}
                  </div>
                </div>
              )}

              {/* Watermark options */}
              {toolId === "watermark" && !result && (
                <div style={{ margin: "8px 0" }}>
                  <label style={{ fontSize: "10px", fontWeight: 600, color: "#888", display: "block", marginBottom: "4px" }}>Watermark text</label>
                  <input value={wmText} onChange={e => setWmText(e.target.value)} placeholder="DRAFT" inputMode="text" style={{ width: "100%", padding: "9px", border: "1px solid #EEE", borderRadius: "8px", fontSize: "13px", outline: "none", boxSizing: "border-box", marginBottom: "10px" }} />
                  <label style={{ fontSize: "10px", fontWeight: 600, color: "#888", display: "block", marginBottom: "4px" }}>Size</label>
                  <div style={{ display: "flex", alignItems: "center", gap: "4px", marginBottom: "10px" }}>
                    {[24, 36, 48, 64, 80].map(s => <button key={s} onClick={() => setWmSize(s)} style={{ padding: "5px 10px", borderRadius: "6px", border: "none", fontSize: "10px", fontWeight: wmSize === s ? 800 : 500, background: wmSize === s ? "#1A1A1A" : "#F0F0F0", color: wmSize === s ? "white" : "#AAA", cursor: "pointer" }}>{s}</button>)}
                  </div>
                  <label style={{ fontSize: "10px", fontWeight: 600, color: "#888", display: "block", marginBottom: "4px" }}>Opacity</label>
                  <div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
                    {[{ v: 0.08, l: "Light" }, { v: 0.15, l: "Medium" }, { v: 0.3, l: "Bold" }].map(o => (
                      <button key={o.v} onClick={() => setWmOpacity(o.v)} style={{ padding: "5px 12px", borderRadius: "6px", border: wmOpacity === o.v ? "2px solid #64748B" : "1px solid #EEE", background: wmOpacity === o.v ? "#F1F5F9" : "white", fontSize: "10px", fontWeight: 600, color: wmOpacity === o.v ? "#64748B" : "#888", cursor: "pointer" }}>{o.l}</button>
                    ))}
                  </div>
                </div>
              )}
              {error && <div style={{ padding: "10px", background: "#FFF5F5", borderRadius: "8px", border: "1px solid #FDD", margin: "8px 0", fontSize: "11px", color: "#E85D3A", fontWeight: 600 }}>⚠️ {error}</div>}
              {!result && toolId !== "merge" && !(toolId === "split" && splitMode === "visual") && !(toolId === "compress" && (compressWorkflow === "batch" || files.length === 0)) && <button onClick={process} disabled={processing} style={{ width: "100%", padding: "13px", background: processing ? `${tool.accent}90` : tool.accent, color: "white", border: "none", borderRadius: "11px", fontSize: "14px", fontWeight: 700, cursor: processing ? "wait" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px", marginTop: "8px" }}>{processing && <div style={{ width: "15px", height: "15px", border: "2px solid rgba(255,255,255,0.25)", borderTopColor: "white", borderRadius: "50%", animation: "nsS 0.6s linear infinite" }} />}{processing ? (compressProgress && toolId === "compress" ? `Compressing page ${compressProgress.page} of ${compressProgress.total}...` : "Processing...") : `${tool.label} · free`}</button>}
              {toolId !== "merge" && !(toolId === "split" && splitMode === "visual") && renderRes()}
              {result && <button onClick={() => { setFiles([]); setResult(null); setDlData(null); setCompressProgress(null); setCompressWorkflow("single"); setCompressBatchOpen(false); setCompressBatchFiles([]); setCompressBatchPreset("recommended"); setCompressBatchProgress(null); }} style={{ width: "100%", marginTop: "8px", padding: "10px", background: "none", border: "1px solid #EEE", borderRadius: "9px", fontSize: "12px", color: "#BBB", cursor: "pointer" }}>Process another</button>}
            </div>
          )}

          <div style={{ marginTop: "16px", padding: "14px", background: "#FAFAFA", borderRadius: "10px" }}>
            <div style={{ fontSize: "10px", color: "#CCC", textAlign: "center", marginBottom: "6px" }}>✂️ No sign-up · No watermarks · Browser-first processing</div>
            <div style={{ fontSize: "9px", color: "#DDD", textAlign: "center", lineHeight: 1.5 }}>🔒 Free tools stay on-device. Advanced flat-PDF detection may briefly hit our detection service so we can locate fields, but the document is not stored.</div>
          </div>
        </div>
      )}

      {/* ─── UPGRADE MODAL ─── */}
      {showUpgrade && (
        <AuthUpgradeModal initialMode={authModalMode} onClose={() => setShowUpgrade(false)} isAuthenticated={isAuthenticated} accessToken={accessToken} onBeforeCheckout={() => saveEditorState("pre-checkout")} reason={proModalReason} />
      )}

      {usageLimitModal && (
        <div onClick={() => setUsageLimitModal(null)} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.45)", zIndex: 105, display: "flex", alignItems: "center", justifyContent: "center", padding: "20px" }}>
          <div onClick={e => e.stopPropagation()} style={{ background: "white", borderRadius: "18px", padding: "22px 20px", maxWidth: "380px", width: "100%", boxShadow: "0 18px 50px rgba(0,0,0,0.2)", textAlign: "center" }}>
            <div style={{ fontFamily: "'Instrument Serif', serif", fontSize: "22px", color: "#1A1A1A", marginBottom: "8px" }}>{usageLimitModal.title}</div>
            <div style={{ fontSize: "13px", lineHeight: 1.5, color: "#777", marginBottom: "18px" }}>{usageLimitModal.message}</div>
            <button onClick={() => setUsageLimitModal(null)} style={{ width: "100%", padding: "12px", background: "#E85D3A", color: "white", border: "none", borderRadius: "11px", fontSize: "13px", fontWeight: 700, cursor: "pointer" }}>Got it</button>
          </div>
        </div>
      )}

      {/* ─── SPONSOR BANNER (free tier only) ─── */}
      {!isPro && view === "tool" && (
        <div style={{ maxWidth: "600px", margin: "0 auto", padding: "0 16px 8px" }}>
          <div style={{ padding: "10px 14px", background: "#FAFAFA", borderRadius: "8px", border: "1px solid #F0F0F0", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
            <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
              <span style={{ fontSize: "9px", fontWeight: 600, color: "#CCC", letterSpacing: "0.5px" }}>SPONSOR</span>
              <span style={{ fontSize: "11px", color: "#999" }}>Your ad here — <a href="mailto:sponsor@nostringspdf.com" style={{ color: "#E85D3A", textDecoration: "none", fontWeight: 600 }}>become a sponsor</a></span>
            </div>
            <button onClick={openUpgradeModal} style={{ fontSize: "9px", color: "#CCC", background: "none", border: "none", cursor: "pointer", textDecoration: "underline" }}>Remove ads</button>
          </div>
        </div>
      )}

      <footer style={{ borderTop: "1px solid #F0F0F0", padding: "18px 16px", textAlign: "center", maxWidth: "600px", margin: "0 auto" }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "5px", marginBottom: "4px" }}>
          <Lg s={13} />
          <span style={{ fontFamily: "'Instrument Serif', serif", fontSize: "12px", color: "#CCC" }}>NoStringsPDF</span>
        </div>
        <div style={{ fontSize: "9px", color: "#DDD" }}>No strings. No surprises. No nonsense.</div>
        <div style={{ fontSize: "12px", color: "#AAA", marginTop: "8px" }}>
          <a href="/terms" style={{ color: "#777", textDecoration: "none" }} onMouseEnter={(e) => e.currentTarget.style.textDecoration = "underline"} onMouseLeave={(e) => e.currentTarget.style.textDecoration = "none"}>Terms</a>
          <span style={{ color: "#CCC" }}> · </span>
          <a href="/privacy" style={{ color: "#777", textDecoration: "none" }} onMouseEnter={(e) => e.currentTarget.style.textDecoration = "underline"} onMouseLeave={(e) => e.currentTarget.style.textDecoration = "none"}>Privacy</a>
          <span style={{ color: "#CCC" }}> · </span>
          <a href="/cookies" style={{ color: "#777", textDecoration: "none" }} onMouseEnter={(e) => e.currentTarget.style.textDecoration = "underline"} onMouseLeave={(e) => e.currentTarget.style.textDecoration = "none"}>Cookies</a>
        </div>
      </footer>
    </div>
  );
}


const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
