// Saign — App router + pages (real backend, no dummy data)
// Reuses primitives from components/atoms.jsx

// ---------- API client ----------
// API base — empty string = same-origin; for split-origin (Bunny CDN ↔ Fly.io)
// inject window.SAIGN_API = 'https://saign-api.fly.dev' via config.js before app.jsx
const API_BASE = (typeof window !== 'undefined' && window.SAIGN_API) || '';

// Read the JS-readable CSRF cookie set by the API on auth + me responses.
const readCookie = (name) => {
  if (typeof document === 'undefined') return null;
  const m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.$?*|{}()[\]\\/+^]/g, '\\$&') + '=([^;]*)'));
  return m ? decodeURIComponent(m[1]) : null;
};
const csrfHeader = () => {
  const tok = readCookie('saign_csrf');
  return tok ? { 'X-CSRF-Token': tok } : {};
};

const api = async (path, opts = {}) => {
  const url = path.startsWith('http') ? path : API_BASE + path;
  const method = opts.method || 'GET';
  const headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) };
  if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
    const tok = readCookie('saign_csrf');
    if (tok) headers['X-CSRF-Token'] = tok;
  }
  const res = await fetch(url, {
    method, headers,
    credentials: API_BASE ? 'include' : 'same-origin',
    body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
  });
  let data = null;
  const ct = res.headers.get('content-type') || '';
  if (ct.includes('application/json')) data = await res.json();
  if (!res.ok) {
    const err = new Error(data?.error || `http_${res.status}`);
    err.status = res.status; err.data = data;
    throw err;
  }
  return data;
};

// ---------- Auth context ----------
const AuthCtx = React.createContext({ user: null, loading: true });
const useAuth = () => React.useContext(AuthCtx);

const AuthProvider = ({ children }) => {
  const [user, setUser] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    api('/api/auth/me').then(setUser).catch(() => setUser(null)).finally(() => setLoading(false));
  }, []);

  const login    = async (email, password) => {
    const out = await api('/api/auth/login', { method: 'POST', body: { email, password } });
    if (out?.mfa_required) return out;
    setUser(out); return out;
  };
  const register = async (data)            => api('/api/auth/register', { method: 'POST', body: data });
  const logout   = async ()                => { try { await api('/api/auth/logout', { method: 'POST' }); } finally { setUser(null); } };

  return <AuthCtx.Provider value={{ user, loading, login, logout, register }}>{children}</AuthCtx.Provider>;
};

// ---------- Router ----------
const useHash = () => {
  const [hash, setHash] = React.useState(window.location.hash || '#/');
  React.useEffect(() => {
    const h = () => setHash(window.location.hash || '#/');
    window.addEventListener('hashchange', h);
    return () => window.removeEventListener('hashchange', h);
  }, []);
  return hash;
};
const navigate = (to) => { window.location.hash = to; };
const Link = ({ to, children, className, style, onClick }) => (
  <a href={`#${to}`} className={className} style={style} onClick={onClick}>{children}</a>
);
const parsePath = (hash) => {
  const path = (hash || '#/').replace(/^#/, '') || '/';
  const seg = path.split('/').filter(Boolean);
  return { path, seg };
};

// ---------- Toast ----------
const ToastCtx = React.createContext({ push: () => {} });
const useToast = () => React.useContext(ToastCtx);
const ToastProvider = ({ children }) => {
  const [toasts, setToasts] = React.useState([]);
  const push = React.useCallback((msg, kind = 'ok') => {
    const id = Math.random().toString(36).slice(2);
    setToasts(t => [...t, { id, msg, kind }]);
    setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3500);
  }, []);
  return (
    <ToastCtx.Provider value={{ push }}>
      {children}
      <div style={{ position: 'fixed', top: 18, right: 18, zIndex: 1000, display: 'flex', flexDirection: 'column', gap: 8 }}>
        {toasts.map(t => (
          <div key={t.id} className="card" style={{ padding: '10px 14px', fontSize: 12, color: 'var(--ink)',
            boxShadow: '0 8px 24px rgba(26,24,20,0.12)', display: 'flex', alignItems: 'center', gap: 10, minWidth: 220 }}>
            <Icon name={t.kind === 'err' ? 'x' : 'check'} size={12}
                  color={t.kind === 'err' ? 'var(--danger)' : 'var(--positive)'} />
            {t.msg}
          </div>
        ))}
      </div>
    </ToastCtx.Provider>
  );
};

// ---------- PDF viewer (pdf.js) ----------
// `renderOverlay(pageNum, { width, height })` returns nodes positioned absolutely
// on top of each rendered page (used for signature-field placement & display).
const PdfViewer = ({ url, initialZoom = 1, maxPages = 50, withCredentials = true,
                     renderOverlay, onPageMeta, viewerRef }) => {
  const [pages, setPages] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [zoom, setZoom] = React.useState(initialZoom);
  const [current, setCurrent] = React.useState(1);
  const containerRef = React.useRef(null);
  const pageRefs = React.useRef({});

  // Load + render the PDF once per URL change.
  React.useEffect(() => {
    let cancelled = false;
    setPages(null); setError(null); setCurrent(1);
    if (!window.pdfjsLib) { setError('pdf.js not loaded'); return; }
    (async () => {
      try {
        const pdf = await pdfjsLib.getDocument({ url, withCredentials }).promise;
        const out = [];
        const limit = Math.min(pdf.numPages, maxPages);
        for (let i = 1; i <= limit; i++) {
          if (cancelled) return;
          const page = await pdf.getPage(i);
          const vp = page.getViewport({ scale: 2 });   // render high-res once, scale via CSS
          const canvas = document.createElement('canvas');
          canvas.width = vp.width; canvas.height = vp.height;
          await page.render({ canvasContext: canvas.getContext('2d'), viewport: vp }).promise;
          if (cancelled) return;
          out.push({ n: i, src: canvas.toDataURL('image/jpeg', 0.85),
                     w: vp.width / 2, h: vp.height / 2 });   // base CSS dims (= scale 1)
        }
        if (!cancelled) {
          setPages(out);
          onPageMeta?.(out.map(p => ({ page: p.n, width: p.w, height: p.h })));
        }
      } catch (e) {
        if (!cancelled) setError(e.message || 'render failed');
      }
    })();
    return () => { cancelled = true; };
  }, [url, maxPages]);

  // Track which page is currently in view (for the toolbar indicator).
  React.useEffect(() => {
    if (!pages || !containerRef.current) return;
    const io = new IntersectionObserver((entries) => {
      const visible = entries.filter(e => e.isIntersecting)
        .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
      if (visible) {
        const n = +visible.target.dataset.page;
        if (n) setCurrent(n);
      }
    }, { root: containerRef.current, threshold: [0.25, 0.5, 0.75] });
    Object.values(pageRefs.current).forEach(el => el && io.observe(el));
    return () => io.disconnect();
  }, [pages]);

  const goTo = React.useCallback((n) => {
    const el = pageRefs.current[n];
    if (el && containerRef.current) {
      el.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  }, []);

  if (viewerRef) viewerRef.current = { goTo, getPages: () => pages, getZoom: () => zoom };

  if (error) return <div className="pdfv-loading" style={{ color: 'var(--danger)' }}>PDF could not be rendered: {error}</div>;
  if (!pages) return <div className="pdfv-loading">Rendering PDF…</div>;

  return (
    <div className="pdfv-shell">
      <div className="pdfv-toolbar">
        <div className="pdfv-tb-group">
          <button className="pdfv-tb-btn" onClick={() => goTo(Math.max(1, current - 1))} disabled={current === 1} title="Previous page">
            <Icon name="arrowL" size={11} />
          </button>
          <input className="pdfv-page-input" type="number" min={1} max={pages.length}
                 value={current} onChange={(e) => { const n = +e.target.value; if (n) goTo(n); }} />
          <span className="pdfv-tb-label">/ {pages.length}</span>
          <button className="pdfv-tb-btn" onClick={() => goTo(Math.min(pages.length, current + 1))} disabled={current === pages.length} title="Next page">
            <Icon name="arrow" size={11} />
          </button>
        </div>
        <div className="pdfv-tb-group">
          <button className="pdfv-tb-btn" onClick={() => setZoom(z => Math.max(0.4, +(z - 0.2).toFixed(2)))} title="Zoom out">−</button>
          <span className="pdfv-tb-label" style={{ minWidth: 44, textAlign: 'center' }}>{Math.round(zoom * 100)}%</span>
          <button className="pdfv-tb-btn" onClick={() => setZoom(z => Math.min(3, +(z + 0.2).toFixed(2)))} title="Zoom in">+</button>
          <button className="pdfv-tb-btn" onClick={() => setZoom(1)} title="Reset zoom">⤓</button>
        </div>
      </div>
      <div ref={containerRef} className="pdfv">
        {pages.map(p => (
          <div key={p.n} ref={el => pageRefs.current[p.n] = el} data-page={p.n}
               className="pdfv-page"
               style={{ width: p.w * zoom, height: p.h * zoom, position: 'relative' }}>
            <img src={p.src} alt={`page ${p.n}`} draggable={false}
                 style={{ width: '100%', height: '100%', display: 'block', userSelect: 'none' }} />
            {renderOverlay && (
              <div className="pdfv-overlay" style={{ position: 'absolute', inset: 0 }}>
                {renderOverlay(p.n, { width: p.w * zoom, height: p.h * zoom, baseWidth: p.w, baseHeight: p.h })}
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
};

// ---------- Signature canvas ----------
const SignatureCanvas = ({ onChange, width = 480, height = 140 }) => {
  const ref = React.useRef(null);
  const drawing = React.useRef(false);
  const last = React.useRef({ x: 0, y: 0 });
  const [hasInk, setHasInk] = React.useState(false);

  const pos = React.useCallback((e) => {
    const r = ref.current.getBoundingClientRect();
    const t = e.touches?.[0];
    return {
      x: ((t?.clientX ?? e.clientX) - r.left) * (ref.current.width / r.width),
      y: ((t?.clientY ?? e.clientY) - r.top)  * (ref.current.height / r.height),
    };
  }, []);

  React.useEffect(() => {
    const c = ref.current;
    const ctx = c.getContext('2d');
    ctx.lineWidth = 2.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
    ctx.strokeStyle = '#1a1814';
    const start = (e) => { e.preventDefault(); drawing.current = true; last.current = pos(e); };
    const move  = (e) => {
      if (!drawing.current) return;
      e.preventDefault();
      const p = pos(e);
      ctx.beginPath(); ctx.moveTo(last.current.x, last.current.y); ctx.lineTo(p.x, p.y); ctx.stroke();
      last.current = p;
      if (!hasInk) setHasInk(true);
    };
    const end = () => {
      if (!drawing.current) return;
      drawing.current = false;
      onChange?.(c.toDataURL('image/png'));
    };
    c.addEventListener('mousedown', start);
    c.addEventListener('mousemove', move);
    window.addEventListener('mouseup', end);
    c.addEventListener('touchstart', start, { passive: false });
    c.addEventListener('touchmove', move,  { passive: false });
    c.addEventListener('touchend', end);
    return () => {
      c.removeEventListener('mousedown', start);
      c.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', end);
      c.removeEventListener('touchstart', start);
      c.removeEventListener('touchmove', move);
      c.removeEventListener('touchend', end);
    };
  }, [hasInk, onChange, pos]);

  const clear = () => {
    const c = ref.current;
    c.getContext('2d').clearRect(0, 0, c.width, c.height);
    setHasInk(false);
    onChange?.(null);
  };

  return (
    <div style={{ position: 'relative' }}>
      <canvas ref={ref} className="sigpad" width={width} height={height} />
      {!hasInk && <div className="sigpad-placeholder">Draw your signature here</div>}
      <div style={{ marginTop: 8, display: 'flex', justifyContent: 'flex-end' }}>
        <button onClick={clear} type="button" className="btn-link" style={{ fontSize: 11 }}>Clear</button>
      </div>
    </div>
  );
};

// ---------- Loading + error boundaries ----------
const FullPageSpinner = () => (
  <div className="saign-app" style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
    <div style={{ textAlign: 'center' }}>
      <Logo size={16} />
      <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 18 }}>Loading…</div>
    </div>
  </div>
);

// ---------- Shell chrome ----------
const SideNavWired = ({ active, counts = {} }) => {
  const { user, logout } = useAuth();
  const items = [
    { id: 'all',      label: 'Inbox',    icon: 'inbox',   count: counts.all,      to: '/dashboard' },
    { id: 'draft',    label: 'Drafts',   icon: 'file',    count: counts.draft,    to: '/dashboard/drafts' },
    { id: 'sent',     label: 'Sent',     icon: 'send',    count: counts.sent,     to: '/dashboard/sent' },
    { id: 'signed',   label: 'Signed',   icon: 'check',   count: counts.signed,   to: '/dashboard/signed' },
    { id: 'archived', label: 'Archived', icon: 'archive', count: counts.archived, to: '/dashboard/archived' },
  ];
  const onLogout = async (e) => { e.preventDefault(); await logout(); navigate('/login'); };
  return (
    <div className="sidenav">
      <div style={{ marginBottom: 18, padding: '0 4px' }}>
        <Link to="/dashboard" style={{ textDecoration: 'none' }}><Logo size={15} /></Link>
      </div>
      <Link to="/documents/new" className="btn" style={{ marginBottom: 10, justifyContent: 'center', textDecoration: 'none' }}>
        <Icon name="plus" size={11} color="var(--paper)" /> New document
      </Link>
      {items.map(it => (
        <Link key={it.id} to={it.to} className="sidenav-item" style={{ textDecoration: 'none' }} data-active={active === it.id}>
          <span className="sidenav-ico"><Icon name={it.icon} size={13} /></span>
          <span style={{ flex: 1, color: active === it.id ? 'var(--ink)' : 'inherit', fontWeight: active === it.id ? 500 : 400 }}>{it.label}</span>
          {it.count != null && it.count > 0 && <span className="mono" style={{ opacity: 0.6, fontSize: 10 }}>{it.count}</span>}
        </Link>
      ))}
      {user?.role === 'admin' && (
        <>
          <div className="sidenav-section">Admin</div>
          <Link to="/admin/users" className="sidenav-item" style={{ textDecoration: 'none' }}>
            <span className="sidenav-ico"><Icon name="user" size={13} /></span>Users
          </Link>
          <Link to="/admin/audit" className="sidenav-item" style={{ textDecoration: 'none' }}>
            <span className="sidenav-ico"><Icon name="shield" size={13} /></span>Global audit
          </Link>
        </>
      )}
      <div style={{ flex: 1 }} />
      <Link to="/settings" className="sidenav-item" style={{ textDecoration: 'none' }} data-active={active === 'settings'}>
        <span className="sidenav-ico"><Icon name="settings" size={13} /></span>
        <span style={{ flex: 1, color: active === 'settings' ? 'var(--ink)' : 'inherit', fontWeight: active === 'settings' ? 500 : 400 }}>Settings</span>
      </Link>
      <div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '8px 4px', fontSize: 11, color: 'var(--ink-2)' }}>
        <Link to="/settings" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 9, flex: 1, minWidth: 0, color: 'inherit' }}>
          <Avatar name={user?.full_name || user?.email || '?'} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontWeight: 500, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{user?.full_name}</div>
            <div style={{ fontSize: 10, color: 'var(--ink-3)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{user?.email}</div>
          </div>
        </Link>
        <button type="button" onClick={onLogout} title="Sign out"
                style={{ background: 'transparent', border: 0, padding: 4, cursor: 'pointer', color: 'var(--ink-3)' }}>
          <Icon name="arrow" size={11} />
        </button>
      </div>
    </div>
  );
};

const TopBarWired = ({ title, right }) => (
  <div className="topnav">
    <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 10 }}>
      <h1 className="display" style={{ fontSize: 18, margin: 0 }}>{title}</h1>
    </div>
    <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
      <div style={{ position: 'relative' }}>
        <input placeholder="Search documents…"
          style={{ border: '1px solid var(--line)', background: 'var(--card)', padding: '6px 10px 6px 28px',
            borderRadius: 2, fontSize: 12, width: 200, fontFamily: 'var(--sans)', outline: 'none', color: 'var(--ink)' }} />
        <span style={{ position: 'absolute', left: 9, top: 8, color: 'var(--ink-3)' }}><Icon name="search" size={12} /></span>
      </div>
      <button className="btn btn-ghost btn-sm"><Icon name="bell" size={11} /></button>
      {right}
    </div>
  </div>
);

const AppShell = ({ active, title, counts, children, topRight }) => (
  <div className="saign-app" style={{ display: 'flex', height: '100vh' }}>
    <SideNavWired active={active} counts={counts} />
    <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
      <TopBarWired title={title} right={topRight} />
      <div style={{ flex: 1, overflow: 'auto' }}>{children}</div>
    </div>
  </div>
);

// ---------- Public pages ----------
const LandingPage = () => {
  const { user } = useAuth();
  return (
    <div className="saign-app" style={{ minHeight: '100vh', background: 'var(--paper)' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '20px 48px', borderBottom: '1px solid var(--line)' }}>
        <Logo size={16} />
        <div style={{ display: 'flex', gap: 18, alignItems: 'center', fontSize: 12 }}>
          <Link to="/verify" className="btn-link">Verify a document</Link>
          {user ? (
            <Link to="/dashboard" className="btn" style={{ textDecoration: 'none' }}>
              Open dashboard <Icon name="arrow" size={11} color="var(--paper)" />
            </Link>
          ) : (
            <>
              <Link to="/login" className="btn-link">Sign in</Link>
              <Link to="/register" className="btn" style={{ textDecoration: 'none' }}>
                Start free <Icon name="arrow" size={11} color="var(--paper)" />
              </Link>
            </>
          )}
        </div>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', minHeight: 'calc(100vh - 73px)', alignItems: 'stretch' }}>
        <div style={{ padding: '80px 64px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
          <div className="eyebrow" style={{ marginBottom: 20 }}>EU‑hosted · eIDAS · GDPR</div>
          <h1 className="display" style={{ fontSize: 64, lineHeight: 1.02, margin: 0, marginBottom: 24, letterSpacing: '-0.025em' }}>
            Sign and send,<br/>
            <em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>without ceremony.</em>
          </h1>
          <p style={{ fontSize: 16, color: 'var(--ink-2)', maxWidth: 480, margin: 0, marginBottom: 32, lineHeight: 1.55 }}>
            Drop a PDF, add signers, get it signed. Auditable, encrypted, and quietly fast — built from day one for European compliance.
          </p>
          <div style={{ display: 'flex', gap: 12, marginBottom: 40 }}>
            {user ? (
              <Link to="/dashboard" className="btn btn-lg" style={{ textDecoration: 'none' }}>
                Continue to dashboard <Icon name="arrow" size={12} color="var(--paper)" />
              </Link>
            ) : (
              <>
                <Link to="/register" className="btn btn-lg" style={{ textDecoration: 'none' }}>
                  Create free account <Icon name="arrow" size={12} color="var(--paper)" />
                </Link>
                <Link to="/login" className="btn btn-ghost btn-lg" style={{ textDecoration: 'none' }}>I have an account</Link>
              </>
            )}
          </div>
          <div style={{ display: 'flex', gap: 28, fontSize: 11, color: 'var(--ink-3)' }}>
            <span>SOC 2 Type II</span><span>eIDAS · SES + QES</span><span>GDPR</span><span>EU data residency</span>
          </div>
        </div>
        <div style={{ background: 'var(--paper-2)', borderLeft: '1px solid var(--line)', padding: '64px 48px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <div style={{ transform: 'rotate(-2deg)', boxShadow: '0 24px 48px rgba(26,24,20,0.10)' }}>
            <PdfPageMock width={360} height={460} withSigField sigStyle="signed" sigMethod="draw" signerName="Casey Morgan" />
          </div>
        </div>
      </div>

      {/* — Stats strip — */}
      <div style={{ borderTop: '1px solid var(--line)', borderBottom: '1px solid var(--line)',
                    background: 'var(--paper-2)', padding: '36px 48px',
                    display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 32 }}>
        {[
          { n: '3 min',   l: 'From upload to first signature' },
          { n: '99.99%',  l: 'Edge availability (Cloudflare)' },
          { n: 'Frankfurt', l: 'Primary data residency · EU' },
          { n: '0',       l: 'Cookies set before consent' },
        ].map((s, i) => (
          <div key={i}>
            <div className="display" style={{ fontSize: 28, lineHeight: 1, marginBottom: 6, color: 'var(--ink)' }}>{s.n}</div>
            <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>{s.l}</div>
          </div>
        ))}
      </div>

      {/* — Features — */}
      <div style={{ padding: '96px 48px', maxWidth: 1180, margin: '0 auto', width: '100%', boxSizing: 'border-box' }}>
        <div className="eyebrow" style={{ marginBottom: 12 }}>What you get</div>
        <h2 className="display" style={{ fontSize: 40, margin: 0, marginBottom: 16, letterSpacing: '-0.02em' }}>
          Boring on purpose. <em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>Reliable by design.</em>
        </h2>
        <p style={{ fontSize: 15, color: 'var(--ink-2)', maxWidth: 640, margin: 0, marginBottom: 56, lineHeight: 1.6 }}>
          Signing software shouldn't get in the way. Saign keeps the workflow tight, the audit trail tamper-evident, and the legal footing solid — without forcing you to read a 40-page manual.
        </p>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
          {[
            { icon: 'upload',   t: 'Drop, add, send',
              d: 'Upload a PDF, type in signer emails, click send. The first email is out before your coffee cools.' },
            { icon: 'shield',   t: 'Tamper-evident audit',
              d: 'Every event hashes the previous one. Break the chain anywhere and the whole trail flags red.' },
            { icon: 'lock',     t: 'Encrypted at rest',
              d: 'PDFs in R2, metadata in D1, secrets sealed. EU-only by default — no transatlantic round-trips.' },
            { icon: 'pen',      t: 'Draw, type, or upload',
              d: 'Three signature methods, one consistent receipt. SES today, QES (eID) on the same flow.' },
            { icon: 'map',      t: 'Geo + IP forensics',
              d: 'Browser geolocation with consent, IP fallback, reverse-geocoded label. Stored masked on every display.' },
            { icon: 'check',    t: 'Public verification',
              d: 'Each completed document gets a code. Anyone can paste it on /verify and check the signature themselves.' },
          ].map((f, i) => (
            <div key={i} className="card" style={{ padding: 24 }}>
              <div style={{ width: 36, height: 36, borderRadius: 2, background: 'var(--paper-2)',
                            display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 16 }}>
                <Icon name={f.icon} size={16} color="var(--ink)" />
              </div>
              <div style={{ fontSize: 15, fontWeight: 500, marginBottom: 6 }}>{f.t}</div>
              <div style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.55 }}>{f.d}</div>
            </div>
          ))}
        </div>
      </div>

      {/* — How it works — */}
      <div style={{ background: 'var(--paper-2)', borderTop: '1px solid var(--line)', borderBottom: '1px solid var(--line)' }}>
        <div style={{ padding: '96px 48px', maxWidth: 1180, margin: '0 auto', width: '100%', boxSizing: 'border-box' }}>
          <div className="eyebrow" style={{ marginBottom: 12 }}>How it works</div>
          <h2 className="display" style={{ fontSize: 40, margin: 0, marginBottom: 56, letterSpacing: '-0.02em' }}>
            Three steps. <em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>No training required.</em>
          </h2>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 32 }}>
            {[
              { n: '01', t: 'Upload',
                d: 'Drag the PDF in. We hash it on arrival, count the pages, and pin the original to EU storage.' },
              { n: '02', t: 'Add signers',
                d: 'Type in names and emails. Sequential or parallel. Each signer gets a unique, single-use link.' },
              { n: '03', t: 'Get it signed',
                d: 'They draw, type, or upload a signature. The signed PDF, certificate, and audit chain land in your inbox.' },
            ].map((s, i) => (
              <div key={i}>
                <div className="mono" style={{ fontSize: 11, color: 'var(--accent)', marginBottom: 12, letterSpacing: '0.1em' }}>STEP {s.n}</div>
                <div className="display" style={{ fontSize: 22, marginBottom: 10, letterSpacing: '-0.01em' }}>{s.t}</div>
                <div style={{ fontSize: 14, color: 'var(--ink-2)', lineHeight: 1.6 }}>{s.d}</div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* — Use cases — */}
      <div style={{ padding: '96px 48px', maxWidth: 1180, margin: '0 auto', width: '100%', boxSizing: 'border-box' }}>
        <div className="eyebrow" style={{ marginBottom: 12 }}>Built for</div>
        <h2 className="display" style={{ fontSize: 40, margin: 0, marginBottom: 56, letterSpacing: '-0.02em' }}>
          Whoever signs the most paperwork in the room.
        </h2>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 0,
                      border: '1px solid var(--line)', borderRadius: 2, overflow: 'hidden' }}>
          {[
            { t: 'Sales & contracts',
              d: 'Order forms, MSAs, NDAs. Send before the demo ends, sign before the call drops.',
              k: ['MSA', 'NDA', 'Order form', 'SoW'] },
            { t: 'HR & people ops',
              d: 'Offer letters, contracts, onboarding paperwork. Templates, sequential flows, deadline reminders.',
              k: ['Offer', 'Employment contract', 'Onboarding', 'Termination'] },
            { t: 'Legal & compliance',
              d: 'Tamper-evident chain, eIDAS-ready signatures, GDPR-clean retention. Built for audits, not theatre.',
              k: ['DPA', 'Power of attorney', 'Settlement', 'Disclosure'] },
            { t: 'Real estate & finance',
              d: 'Lease agreements, loan docs, advisory mandates. KYC-friendly, geolocation-backed, certificate-issued.',
              k: ['Lease', 'Mandate', 'KYC', 'Loan'] },
          ].map((u, i) => (
            <div key={i} style={{
              padding: 32,
              borderRight: i % 2 === 0 ? '1px solid var(--line)' : 'none',
              borderBottom: i < 2 ? '1px solid var(--line)' : 'none',
            }}>
              <div style={{ fontSize: 17, fontWeight: 500, marginBottom: 10 }}>{u.t}</div>
              <div style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.6, marginBottom: 18 }}>{u.d}</div>
              <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                {u.k.map((tag, j) => (
                  <span key={j} className="mono" style={{
                    fontSize: 10, padding: '3px 8px', background: 'var(--paper-2)',
                    border: '1px solid var(--line-2)', color: 'var(--ink-3)', borderRadius: 1,
                  }}>{tag}</span>
                ))}
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* — Security & Compliance — */}
      <div style={{ background: 'var(--ink)', color: 'var(--paper)' }}>
        <div style={{ padding: '96px 48px', maxWidth: 1180, margin: '0 auto', width: '100%', boxSizing: 'border-box',
                      display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 64, alignItems: 'center' }}>
          <div>
            <div className="eyebrow" style={{ marginBottom: 12, color: 'var(--accent)' }}>Security &amp; compliance</div>
            <h2 className="display" style={{ fontSize: 36, margin: 0, marginBottom: 18, letterSpacing: '-0.02em', color: 'var(--paper)' }}>
              Compliance isn't a checkbox.
            </h2>
            <p style={{ fontSize: 14, lineHeight: 1.65, color: 'rgba(247,245,240,0.75)', marginBottom: 28 }}>
              We sweat the details so your legal team doesn't have to. Hash-chained audit logs, EU data residency, MFA, encrypted verification codes, rate-limited public lookups, and a retention policy you can actually enforce.
            </p>
            <Link to="/security" className="btn-link" style={{ color: 'var(--accent)', fontSize: 13 }}>
              Read the security overview <Icon name="arrow" size={11} color="var(--accent)" />
            </Link>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
            {[
              { t: 'eIDAS-aligned',     d: 'SES today, QES on the way' },
              { t: 'GDPR by default',   d: 'Art. 17 + 20 endpoints' },
              { t: 'EU data residency', d: 'Frankfurt-pinned D1 + R2' },
              { t: 'Hash-chained logs', d: 'Tamper-evident audit' },
              { t: 'MFA / TOTP',        d: 'Authenticator-app ready' },
              { t: 'Rate limiting',     d: 'Per-account + per-IP' },
            ].map((b, i) => (
              <div key={i} style={{ padding: 16, border: '1px solid rgba(247,245,240,0.15)', borderRadius: 2 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                  <Icon name="check" size={11} color="var(--accent)" />
                  <span style={{ fontSize: 12, fontWeight: 500, color: 'var(--paper)' }}>{b.t}</span>
                </div>
                <div style={{ fontSize: 11, color: 'rgba(247,245,240,0.55)' }}>{b.d}</div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* — Pricing teaser — */}
      <div style={{ padding: '96px 48px', maxWidth: 1180, margin: '0 auto', width: '100%', boxSizing: 'border-box' }}>
        <div className="eyebrow" style={{ marginBottom: 12 }}>Pricing</div>
        <h2 className="display" style={{ fontSize: 40, margin: 0, marginBottom: 16, letterSpacing: '-0.02em' }}>
          Start free. <em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>Pay when it matters.</em>
        </h2>
        <p style={{ fontSize: 15, color: 'var(--ink-2)', maxWidth: 580, margin: 0, marginBottom: 56, lineHeight: 1.6 }}>
          Simple, transparent tiers. No per-seat games, no surprise overage. Cancel any time, export everything.
        </p>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
          {[
            { name: 'Solo',      price: '€0',  per: 'forever', tag: 'Free',
              feats: ['Up to 3 documents / mo', 'Single signer per doc', 'Email signatures + receipts', 'Public verification page'] },
            { name: 'Team',      price: '€19', per: 'per user / month', tag: 'Most popular', highlight: true,
              feats: ['Unlimited documents', 'Sequential + parallel flows', 'Templates and reminders', 'Audit export (PDF + JSON)'] },
            { name: 'Business',  price: '€49', per: 'per user / month', tag: 'For regulated teams',
              feats: ['Everything in Team', 'SSO + SCIM provisioning', 'Custom retention windows', 'Priority support + DPA'] },
          ].map((p, i) => (
            <div key={i} className="card" style={{
              padding: 28,
              border: p.highlight ? '1.5px solid var(--ink)' : '1px solid var(--line)',
              position: 'relative',
            }}>
              {p.highlight && (
                <div style={{
                  position: 'absolute', top: -10, left: 24, padding: '3px 10px',
                  background: 'var(--ink)', color: 'var(--paper)', fontSize: 10,
                  letterSpacing: '0.08em', textTransform: 'uppercase',
                }}>{p.tag}</div>
              )}
              <div style={{ fontSize: 14, color: 'var(--ink-3)', marginBottom: 6 }}>{p.name}</div>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
                <span className="display" style={{ fontSize: 36, letterSpacing: '-0.02em' }}>{p.price}</span>
                <span style={{ fontSize: 12, color: 'var(--ink-3)' }}>{p.per}</span>
              </div>
              {!p.highlight && <div style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 18 }}>{p.tag}</div>}
              {p.highlight && <div style={{ height: 18 }} />}
              <ul style={{ listStyle: 'none', padding: 0, margin: '0 0 22px', fontSize: 13, color: 'var(--ink-2)' }}>
                {p.feats.map((f, j) => (
                  <li key={j} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, marginBottom: 8 }}>
                    <span style={{ marginTop: 4 }}><Icon name="check" size={10} color="var(--positive)" /></span>
                    <span>{f}</span>
                  </li>
                ))}
              </ul>
              <Link to="/register" className={p.highlight ? 'btn' : 'btn btn-ghost'}
                    style={{ textDecoration: 'none', width: '100%', justifyContent: 'center', display: 'flex' }}>
                {p.price === '€0' ? 'Start free' : 'Try ' + p.name}
              </Link>
            </div>
          ))}
        </div>
      </div>

      {/* — FAQ — */}
      <div style={{ background: 'var(--paper-2)', borderTop: '1px solid var(--line)', borderBottom: '1px solid var(--line)' }}>
        <div style={{ padding: '96px 48px', maxWidth: 880, margin: '0 auto', width: '100%', boxSizing: 'border-box' }}>
          <div className="eyebrow" style={{ marginBottom: 12 }}>Frequently asked</div>
          <h2 className="display" style={{ fontSize: 36, margin: 0, marginBottom: 40, letterSpacing: '-0.02em' }}>
            The questions we always get.
          </h2>
          <div>
            {[
              { q: 'Are Saign signatures legally binding in the EU?',
                a: 'Yes — they qualify as Simple Electronic Signatures (SES) under eIDAS, which are admissible in court across all EU member states. For documents that legally require an Advanced or Qualified signature (some real-estate, banking and government use cases), QES is on the roadmap.' },
              { q: 'Where is my data stored?',
                a: 'PDFs live in Cloudflare R2, metadata in D1, both pinned to the EU (Frankfurt primary). Nothing crosses the Atlantic by default. Subprocessors are listed on the /subprocessors page.' },
              { q: 'How does verification work?',
                a: 'Every completed document gets a unique code, printed on the certificate. Anyone can drop it into /verify (or upload the signed PDF) to check it against our database — without an account. The lookup goes through an HMAC, the codes are encrypted at rest.' },
              { q: 'Can I get my data out?',
                a: 'Always. There\'s a one-click export in account settings (GDPR Art. 20) that returns a full JSON dump of your account, documents and audit chain. Account deletion (Art. 17) wipes everything within 30 days.' },
              { q: 'Do you support 2FA?',
                a: 'Yes — TOTP via any standard authenticator app (Google Authenticator, 1Password, Bitwarden, etc). Enrolment lives in account settings; recovery codes on the way.' },
              { q: 'What happens to documents we no longer need?',
                a: 'You set a retention period in settings. Once a document hits it, the PDF and PII are purged on the next nightly sweep, with the audit chain preserved as a hash-only stub.' },
            ].map((f, i) => (
              <details key={i} style={{ borderTop: '1px solid var(--line)', padding: '20px 0' }}>
                <summary style={{ cursor: 'pointer', fontSize: 15, fontWeight: 500, listStyle: 'none',
                                   display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                  <span>{f.q}</span>
                  <Icon name="plus" size={12} color="var(--ink-3)" />
                </summary>
                <div style={{ fontSize: 13, color: 'var(--ink-2)', marginTop: 12, lineHeight: 1.65, paddingRight: 32 }}>
                  {f.a}
                </div>
              </details>
            ))}
          </div>
        </div>
      </div>

      {/* — Final CTA — */}
      <div style={{ padding: '96px 48px', maxWidth: 880, margin: '0 auto', width: '100%', boxSizing: 'border-box', textAlign: 'center' }}>
        <h2 className="display" style={{ fontSize: 48, margin: 0, marginBottom: 18, letterSpacing: '-0.025em' }}>
          Ready to <em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>send the first one?</em>
        </h2>
        <p style={{ fontSize: 16, color: 'var(--ink-2)', maxWidth: 560, margin: '0 auto 32px', lineHeight: 1.6 }}>
          Free to start. No card, no sales call. Three documents a month on the house — upgrade only if you out-grow it.
        </p>
        <div style={{ display: 'flex', gap: 12, justifyContent: 'center' }}>
          {user ? (
            <Link to="/dashboard" className="btn btn-lg" style={{ textDecoration: 'none' }}>
              Open dashboard <Icon name="arrow" size={12} color="var(--paper)" />
            </Link>
          ) : (
            <>
              <Link to="/register" className="btn btn-lg" style={{ textDecoration: 'none' }}>
                Create free account <Icon name="arrow" size={12} color="var(--paper)" />
              </Link>
              <Link to="/verify" className="btn btn-ghost btn-lg" style={{ textDecoration: 'none' }}>
                Verify a document
              </Link>
            </>
          )}
        </div>
      </div>

      <div style={{ borderTop: '1px solid var(--line)', padding: '20px 48px',
                    display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    fontSize: 11, color: 'var(--ink-3)', flexWrap: 'wrap', gap: 12 }}>
        <div>© 2026 Saign. EU-hosted electronic signatures.</div>
        <div style={{ display: 'flex', gap: 16 }}>
          <Link to="/verify" className="btn-link">Verify document</Link>
          <Link to="/privacy" className="btn-link">Privacy</Link>
          <Link to="/terms" className="btn-link">Terms</Link>
          <Link to="/cookies" className="btn-link">Cookies</Link>
          <Link to="/subprocessors" className="btn-link">Subprocessors</Link>
          <Link to="/security" className="btn-link">Security</Link>
        </div>
      </div>
    </div>
  );
};

const LoginPage = () => {
  const { user, login } = useAuth();
  const toast = useToast();
  const [email, setEmail] = React.useState('admin@saign.local');
  const [password, setPassword] = React.useState('admin123');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [mfaPending, setMfaPending] = React.useState(false);
  const [code, setCode] = React.useState('');

  React.useEffect(() => { if (user) navigate('/dashboard'); }, [user]);

  const submit = async (e) => {
    e.preventDefault();
    setErr(null); setBusy(true);
    try {
      const out = await login(email, password);
      if (out?.mfa_required) { setMfaPending(true); return; }
      toast.push(`Signed in as ${out.email}`);
      navigate('/dashboard');
    } catch (ex) {
      const code = ex.data?.error;
      setErr(
        code === 'rate_limited'      ? 'Too many attempts from this network. Try again in a few minutes.' :
        code === 'account_locked'    ? 'Account temporarily locked after failed attempts. Try again later.' :
        code === 'email_not_verified' ? 'Please verify your email first. Check your inbox or resend below.' :
        ex.status === 401            ? 'Wrong email or password.' :
                                       'Login failed. Try again.'
      );
    } finally { setBusy(false); }
  };

  const submitTotp = async (e) => {
    e.preventDefault();
    setErr(null); setBusy(true);
    try {
      const u = await api('/api/auth/login/totp', { method: 'POST', body: { code } });
      // refetch /me to populate context
      const me = await api('/api/auth/me');
      toast.push(`Signed in as ${me.email}`);
      window.location.hash = '/dashboard';
      window.location.reload();   // simplest way to refresh AuthCtx with the new session
    } catch (ex) {
      setErr(ex.data?.error === 'invalid_code' ? 'That code didn\'t match. Try the next 30s window.' :
             ex.data?.error === 'no_pending_mfa' ? 'Login attempt expired. Sign in again.' :
             'Verification failed.');
    } finally { setBusy(false); }
  };

  const resendVerify = async () => {
    try {
      const r = await api('/api/auth/resend-verify', { method: 'POST', body: { email } });
      if (r.dev_url) toast.push('Dev mode: link logged below');
      else toast.push('Verification email re-sent');
    } catch { toast.push('Could not resend', 'err'); }
  };

  return (
    <div className="saign-app" style={{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', height: '100vh' }}>
      <div style={{ background: 'var(--paper-2)', padding: '40px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', borderRight: '1px solid var(--line)' }}>
        <Link to="/" style={{ textDecoration: 'none' }}><Logo size={16} /></Link>
        <div>
          <div className="eyebrow" style={{ marginBottom: 18 }}>Simple electronic signatures</div>
          <h1 className="display" style={{ fontSize: 48, lineHeight: 1.05, margin: 0, marginBottom: 18 }}>
            Sign and send,<br/>
            <em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>without ceremony.</em>
          </h1>
          <p style={{ fontSize: 14, color: 'var(--ink-2)', maxWidth: 360, margin: 0 }}>
            Drop a PDF, add signers, get it signed. Auditable, encrypted, and quietly fast.
          </p>
        </div>
        <div style={{ display: 'flex', gap: 24, fontSize: 11, color: 'var(--ink-3)' }}>
          <span>SOC 2 Type II</span><span>eIDAS · SES</span><span>GDPR</span>
        </div>
      </div>
      <div style={{ padding: '40px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center', maxWidth: 460 }}>
        <div style={{ marginBottom: 22 }}>
          <h2 className="display" style={{ fontSize: 24, margin: 0, marginBottom: 6 }}>
            {mfaPending ? 'Two-factor code' : 'Welcome back.'}
          </h2>
          <p style={{ fontSize: 12, color: 'var(--ink-3)', margin: 0 }}>
            {mfaPending ? 'Enter the 6-digit code from your authenticator app.' : 'Sign in to continue.'}
          </p>
        </div>
        {!mfaPending ? (
          <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div className="field"><label>Email</label>
              <input value={email} onChange={e => setEmail(e.target.value)} type="email" autoComplete="email" required />
            </div>
            <div className="field"><label>Password</label>
              <input value={password} onChange={e => setPassword(e.target.value)} type="password" autoComplete="current-password" required />
            </div>
            {err && (
              <div style={{ background: '#fbeae6', border: '1px solid #e8b8a8', color: 'var(--danger)', padding: '8px 12px', fontSize: 12, borderRadius: 2 }}>
                {err}
                {err.includes('verify your email') && (
                  <div style={{ marginTop: 6 }}>
                    <button type="button" onClick={resendVerify} className="btn-link" style={{ fontSize: 11 }}>
                      Resend verification email
                    </button>
                  </div>
                )}
              </div>
            )}
            <button type="submit" disabled={busy} className="btn btn-lg" style={{ marginTop: 6, justifyContent: 'center', opacity: busy ? 0.6 : 1 }}>
              {busy ? 'Signing in…' : <>Sign in <Icon name="arrow" size={12} /></>}
            </button>
          </form>
        ) : (
          <form onSubmit={submitTotp} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div className="field"><label>6-digit code</label>
              <input value={code} onChange={e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
                     inputMode="numeric" pattern="[0-9]*" autoComplete="one-time-code" autoFocus required
                     maxLength={6}
                     style={{ fontFamily: 'var(--mono)', fontSize: 22, letterSpacing: '0.4em', textAlign: 'center' }} />
            </div>
            {err && (
              <div style={{ background: '#fbeae6', border: '1px solid #e8b8a8', color: 'var(--danger)', padding: '8px 12px', fontSize: 12, borderRadius: 2 }}>
                {err}
              </div>
            )}
            <button type="submit" disabled={busy || code.length !== 6} className="btn btn-lg"
                    style={{ marginTop: 6, justifyContent: 'center', opacity: (busy || code.length !== 6) ? 0.6 : 1 }}>
              {busy ? 'Verifying…' : <>Verify <Icon name="arrow" size={12} /></>}
            </button>
            <button type="button" className="btn-link" onClick={() => { setMfaPending(false); setCode(''); }}>
              Back to sign-in
            </button>
          </form>
        )}
        <hr className="divider" style={{ margin: '24px 0' }} />
        <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>
          New to Saign? <Link to="/register" className="btn-link">Create an account</Link>
        </div>
        <div className="card" style={{ marginTop: 24, padding: 12, fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.5 }}>
          <div style={{ fontWeight: 500, color: 'var(--ink-2)', marginBottom: 6 }}>Debug accounts</div>
          <div className="mono">admin@saign.local / admin123</div>
          <div className="mono">alex@studio.co / demo1234</div>
        </div>
      </div>
    </div>
  );
};

const RegisterPage = () => {
  const { user, register } = useAuth();
  const [form, setForm] = React.useState({ full_name: '', email: '', password: '' });
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [devUrl, setDevUrl] = React.useState(null);

  React.useEffect(() => { if (user) navigate('/dashboard'); }, [user]);

  const submit = async (e) => {
    e.preventDefault();
    setErr(null); setBusy(true);
    try {
      const r = await register(form);
      const target = '/verify-email?email=' + encodeURIComponent(form.email)
                   + (r?.verification?.dev_url ? '&dev_url=' + encodeURIComponent(r.verification.dev_url) : '');
      navigate(target);
    } catch (ex) {
      setErr(ex.data?.error === 'email_taken' ? 'Email is already registered.' :
             ex.data?.error === 'invalid_input' ? 'Please check your inputs.' : 'Registration failed.');
    } finally { setBusy(false); }
  };

  return (
    <div className="saign-app" style={{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', height: '100vh' }}>
      <div style={{ background: 'var(--paper-2)', padding: '40px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', borderRight: '1px solid var(--line)' }}>
        <Link to="/" style={{ textDecoration: 'none' }}><Logo size={16} /></Link>
        <div>
          <div className="eyebrow" style={{ marginBottom: 18 }}>Free for solo · 14‑day team trial</div>
          <h1 className="display" style={{ fontSize: 44, lineHeight: 1.05, margin: 0, marginBottom: 18 }}>
            Start signing in<br/>
            <em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>under a minute.</em>
          </h1>
          <p style={{ fontSize: 14, color: 'var(--ink-2)', maxWidth: 360, margin: 0 }}>
            No credit card. Cancel anytime. Your documents stay in the EU.
          </p>
        </div>
        <div style={{ display: 'flex', gap: 24, fontSize: 11, color: 'var(--ink-3)' }}>
          <span>SOC 2 Type II</span><span>eIDAS · SES</span><span>GDPR</span>
        </div>
      </div>
      <div style={{ padding: '40px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center', maxWidth: 460 }}>
        <div style={{ marginBottom: 22 }}>
          <h2 className="display" style={{ fontSize: 24, margin: 0, marginBottom: 6 }}>Create your account.</h2>
          <p style={{ fontSize: 12, color: 'var(--ink-3)', margin: 0 }}>Login takes effect immediately. Email verification optional in dev.</p>
        </div>
        <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div className="field"><label>Full name</label>
            <input value={form.full_name} onChange={e => setForm({ ...form, full_name: e.target.value })} required />
          </div>
          <div className="field"><label>Email</label>
            <input value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} type="email" required />
          </div>
          <div className="field"><label>Password</label>
            <input value={form.password} onChange={e => setForm({ ...form, password: e.target.value })} type="password" required minLength={8} />
            <div className="field-hint">Min. 8 characters. Hashed with scrypt (N=16384).</div>
          </div>
          {err && (
            <div style={{ background: '#fbeae6', border: '1px solid #e8b8a8', color: 'var(--danger)', padding: '8px 12px', fontSize: 12, borderRadius: 2 }}>
              {err}
            </div>
          )}
          <button type="submit" disabled={busy} className="btn btn-lg" style={{ marginTop: 6, justifyContent: 'center', opacity: busy ? 0.6 : 1 }}>
            {busy ? 'Creating…' : <>Create account <Icon name="arrow" size={12} /></>}
          </button>
        </form>
        <hr className="divider" style={{ margin: '24px 0' }} />
        <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>
          Already have an account? <Link to="/login" className="btn-link">Sign in</Link>
        </div>
      </div>
    </div>
  );
};

const VerifyEmailPage = () => {
  const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
  const email = params.get('email') || 'your email';
  const devUrl = params.get('dev_url');
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState(null);
  const resend = async () => {
    setBusy(true); setMsg(null);
    try {
      const r = await api('/api/auth/resend-verify', { method: 'POST', body: { email } });
      setMsg(r.dev_url ? `Dev URL: ${r.dev_url}` : 'New verification email sent.');
    } catch { setMsg('Could not resend.'); }
    finally { setBusy(false); }
  };
  return (
    <div className="saign-app" style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div style={{ width: 460, textAlign: 'center' }}>
        <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 28 }}><Logo size={16} /></div>
        <div style={{ width: 56, height: 56, margin: '0 auto 22px', borderRadius: '50%',
          background: 'var(--paper-2)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid var(--line)' }}>
          <Icon name="mail" size={22} color="var(--accent)" />
        </div>
        <h2 className="display" style={{ fontSize: 24, margin: 0, marginBottom: 8 }}>Verify your email</h2>
        <p style={{ fontSize: 13, color: 'var(--ink-2)', margin: 0, marginBottom: 18, lineHeight: 1.5 }}>
          We sent a confirmation link to<br/>
          <strong style={{ color: 'var(--ink)' }}>{email}</strong>.<br/>
          Click the link there to activate your account.
        </p>
        {devUrl && (
          <div className="card" style={{ padding: 12, fontSize: 11, marginBottom: 18, textAlign: 'left', wordBreak: 'break-all' }}>
            <div className="eyebrow" style={{ marginBottom: 6 }}>Dev mode link</div>
            <a href={devUrl} className="mono" style={{ color: 'var(--accent)' }}>{devUrl}</a>
          </div>
        )}
        {msg && <div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 12 }}>{msg}</div>}
        <button onClick={resend} disabled={busy} type="button" className="btn btn-ghost"
                style={{ justifyContent: 'center', width: '100%', marginBottom: 10, opacity: busy ? 0.5 : 1 }}>
          <Icon name="mail" size={11} /> {busy ? 'Resending…' : 'Resend verification email'}
        </button>
        <Link to="/login" className="btn" style={{ textDecoration: 'none', justifyContent: 'center', width: '100%' }}>
          Continue to sign in <Icon name="arrow" size={11} color="var(--paper)" />
        </Link>
      </div>
    </div>
  );
};

const VerifyTokenPage = () => {
  const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
  const token = params.get('token');
  const [state, setState] = React.useState('busy'); // busy | ok | err
  React.useEffect(() => {
    if (!token) { setState('err'); return; }
    api('/api/auth/verify-email', { method: 'POST', body: { token } })
      .then(() => setState('ok'))
      .catch(() => setState('err'));
  }, [token]);
  return (
    <div className="saign-app" style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div style={{ width: 420, textAlign: 'center' }}>
        <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 28 }}><Logo size={16} /></div>
        <div style={{ width: 56, height: 56, margin: '0 auto 22px', borderRadius: '50%',
          background: 'var(--paper-2)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid var(--line)' }}>
          <Icon name={state === 'ok' ? 'check' : state === 'err' ? 'x' : 'clock'} size={22}
                color={state === 'ok' ? 'var(--positive)' : state === 'err' ? 'var(--danger)' : 'var(--ink-3)'} />
        </div>
        <h2 className="display" style={{ fontSize: 24, margin: 0, marginBottom: 8 }}>
          {state === 'busy' ? 'Verifying…' : state === 'ok' ? 'Email verified.' : 'Link invalid or expired.'}
        </h2>
        <p style={{ fontSize: 13, color: 'var(--ink-2)', margin: 0, marginBottom: 24, lineHeight: 1.5 }}>
          {state === 'ok' ? 'You can now sign in.' :
           state === 'err' ? 'Request a new verification email below.' : ' '}
        </p>
        <Link to="/login" className="btn" style={{ textDecoration: 'none', justifyContent: 'center', width: '100%' }}>
          Continue to sign in <Icon name="arrow" size={11} color="var(--paper)" />
        </Link>
      </div>
    </div>
  );
};

// ---------- Authenticated pages ----------
const RequireAuth = ({ children }) => {
  const { user, loading } = useAuth();
  React.useEffect(() => { if (!loading && !user) navigate('/login'); }, [loading, user]);
  if (loading) return <FullPageSpinner />;
  if (!user) return null;
  return children;
};

const StatusBadge = ({ state, label }) => <StatusPill state={state} label={label} />;

// Mask IP for display — preserve coarse network info, hide host. GDPR-friendly.
//   IPv4: 192.168.1.42  → 192.168.1.•
//   IPv6: 2001:db8::1234 → 2001:db8::•••  (keep first 4 hextets max)
const maskIp = (ip) => {
  if (!ip) return '—';
  if (ip.includes(':')) {
    const parts = ip.split(':');
    if (parts.length <= 4) {
      // Compact form like "2001:db8::1" — replace tail group with •••
      return ip.replace(/[^:]+$/, '•••');
    }
    return parts.slice(0, 4).join(':') + ':•••';
  }
  const m = ip.match(/^(\d+\.\d+\.\d+)\.\d+$/);
  if (m) return m[1] + '.•';
  return '•••';
};

const RowMenu = ({ doc, onChange }) => {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  const toast = useToast();
  const isArchived = !!doc.archived_at;

  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);

  const archive = async (e) => {
    e.stopPropagation(); setOpen(false);
    const verb = isArchived ? 'unarchive' : 'archive';
    try {
      await api(`/api/documents/${doc.id}/${verb}`, { method: 'POST' });
      toast.push(isArchived ? 'Restored from archive' : 'Archived');
      onChange?.();
    } catch { toast.push('Could not update', 'err'); }
  };
  const del = async (e) => {
    e.stopPropagation(); setOpen(false);
    if (!confirm(`Delete "${doc.title}"?\n\nSigning links will stop working. The audit trail is kept for compliance retention.`)) return;
    try {
      await api(`/api/documents/${doc.id}`, { method: 'DELETE' });
      toast.push('Document deleted');
      onChange?.();
    } catch { toast.push('Could not delete', 'err'); }
  };

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button type="button" className="btn-link" style={{ color: 'var(--ink-3)' }}
              onClick={(e) => { e.stopPropagation(); setOpen(o => !o); }}>
        <Icon name="moreV" size={12} />
      </button>
      {open && (
        <div className="card" style={{ position: 'absolute', right: 0, top: 22, minWidth: 170, zIndex: 100, padding: 4,
                                       boxShadow: '0 8px 24px rgba(26,24,20,0.12)' }}>
          <button type="button" className="row-menu-item"
                  onClick={(e) => { e.stopPropagation(); setOpen(false); navigate(`/documents/${doc.id}`); }}>
            <Icon name="eye" size={11} /> Open
          </button>
          <button type="button" className="row-menu-item" onClick={archive}>
            <Icon name="archive" size={11} /> {isArchived ? 'Restore' : 'Archive'}
          </button>
          <button type="button" className="row-menu-item" data-danger="true" onClick={del}>
            <Icon name="x" size={11} /> Delete
          </button>
        </div>
      )}
    </div>
  );
};

const useDocuments = (filter) => {
  const [data, setData] = React.useState({ documents: [], counts: {} });
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const reload = React.useCallback(() => {
    setLoading(true);
    const url = '/api/documents' + (filter && filter !== 'all' ? `?filter=${filter}` : '');
    api(url).then(setData).catch(setError).finally(() => setLoading(false));
  }, [filter]);
  React.useEffect(reload, [reload]);
  return { ...data, loading, error, reload };
};

const DashboardPage = ({ filter = 'all' }) => {
  const { documents, counts, loading, reload } = useDocuments(filter);
  const tabs = [
    { id: 'all',      label: 'All',      to: '/dashboard' },
    { id: 'sent',     label: 'Awaiting', to: '/dashboard/sent' },
    { id: 'signed',   label: 'Signed',   to: '/dashboard/signed' },
    { id: 'draft',    label: 'Drafts',   to: '/dashboard/drafts' },
    { id: 'archived', label: 'Archived', to: '/dashboard/archived' },
  ];
  const tabCounts = {
    all:      counts.all || 0,
    sent:     counts.sent || 0,
    signed:   counts.signed || 0,
    draft:    counts.draft || 0,
    archived: counts.archived || 0,
  };
  const sideCounts = {
    all: tabCounts.all, draft: counts.draft||0, sent: counts.sent||0,
    signed: counts.signed||0, archived: counts.archived||0,
  };
  return (
    <AppShell active={filter} title="Documents" counts={sideCounts}>
      <div style={{ padding: '20px 24px' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
          <div className="tabs" style={{ borderBottom: 'none' }}>
            {tabs.map(t => (
              <Link key={t.id} to={t.to} aria-selected={filter === t.id} style={{ textDecoration: 'none' }}>
                {t.label} <span style={{ color: 'var(--ink-3)', marginLeft: 4 }}>{tabCounts[t.id]}</span>
              </Link>
            ))}
          </div>
          <Link to="/documents/new" className="btn btn-sm" style={{ textDecoration: 'none' }}>
            <Icon name="plus" size={11} color="var(--paper)" /> New document
          </Link>
        </div>
        {loading ? (
          <div className="card" style={{ padding: 64, textAlign: 'center', color: 'var(--ink-3)', fontSize: 13 }}>Loading…</div>
        ) : documents.length === 0 ? (
          <div className="card" style={{ padding: 64, textAlign: 'center' }}>
            <div style={{ fontSize: 14, color: 'var(--ink-3)', marginBottom: 12 }}>No documents here yet.</div>
            <Link to="/documents/new" className="btn-link">Create your first document →</Link>
          </div>
        ) : (
          <div className="card">
            <table className="tbl">
              <thead>
                <tr><th style={{ width: 24 }}><input type="checkbox" className="chk" /></th>
                  <th>Document</th><th>Signers</th><th>Status</th><th>Updated</th>
                  <th style={{ width: 32 }}></th></tr>
              </thead>
              <tbody>
                {documents.map(d => (
                  <tr key={d.id} style={{ cursor: 'pointer' }} onClick={() => navigate(`/documents/${d.id}`)}>
                    <td onClick={e => e.stopPropagation()}><input type="checkbox" className="chk" /></td>
                    <td>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                        <DocThumb size={28} signed={d.status === 'signed'} />
                        <div>
                          <div style={{ fontWeight: 500, color: 'var(--ink)' }}>{d.title}</div>
                          <div className="mono" style={{ color: 'var(--ink-3)', fontSize: 10, marginTop: 2 }}>{d.code}</div>
                        </div>
                      </div>
                    </td>
                    <td>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                        <div style={{ display: 'flex' }}>
                          {d.signers.slice(0, 3).map((s, j) => (
                            <span key={j} style={{ marginLeft: j > 0 ? -6 : 0 }}><Avatar name={s.full_name} /></span>
                          ))}
                        </div>
                        {d.total > 0 && (
                          <span className="mono" style={{ color: 'var(--ink-3)', fontSize: 10, marginLeft: 4 }}>{d.signed}/{d.total}</span>
                        )}
                      </div>
                    </td>
                    <td>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                        <StatusBadge state={d.status === 'draft' ? 'draft' : d.status === 'signed' ? 'signed' : d.status === 'declined' ? 'declined' : 'sent'} />
                        {d.archived_at && (
                          <span className="pill" data-state="draft" style={{ fontSize: 9 }}>
                            <span className="pill-dot" />Archived
                          </span>
                        )}
                      </div>
                    </td>
                    <td style={{ color: 'var(--ink-3)', fontSize: 12 }}>{new Date(d.updated_at + 'Z').toLocaleString()}</td>
                    <td onClick={e => e.stopPropagation()}>
                      <RowMenu doc={d} onChange={reload} />
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </AppShell>
  );
};

const DevMailBanner = () => {
  const { user } = useAuth();
  if (user?.mail_provider !== 'dev') return null;
  return (
    <div style={{ marginBottom: 18, padding: '12px 14px',
                  background: '#fbf4e6', border: '1px solid #e8d4b0',
                  borderRadius: 2, fontSize: 12, color: '#8a5a18',
                  display: 'flex', alignItems: 'flex-start', gap: 10 }}>
      <Icon name="mail" size={13} color="#a86b1a" />
      <div style={{ flex: 1, lineHeight: 1.5 }}>
        <strong style={{ color: '#5a3a08', fontWeight: 500 }}>Dev mode — emails are not delivered.</strong>{' '}
        Use the <span className="mono" style={{ background: '#fff', padding: '1px 4px', borderRadius: 1 }}>link</span> button
        next to each signer to copy their unique signing URL. Set{' '}
        <span className="mono" style={{ background: '#fff', padding: '1px 4px', borderRadius: 1 }}>RESEND_API_KEY</span>
        {' '}to enable real delivery.
      </div>
    </div>
  );
};

const SignerRow = ({ signer: s, docId, isLast }) => {
  const toast = useToast();
  const { user } = useAuth();
  const [busy, setBusy] = React.useState(false);
  const apiBase = (typeof window !== 'undefined' && window.SAIGN_API) || '';
  const publicBase = user?.public_url || (typeof window !== 'undefined' ? window.location.origin : '');
  const link = `${publicBase}/#/sign/${s.access_token}`;
  const isPending = !['signed', 'declined'].includes(s.status);

  const copyLink = async () => {
    try {
      await navigator.clipboard.writeText(link);
      toast.push('Signing link copied');
    } catch {
      // fallback for clipboard-blocked contexts
      const ta = document.createElement('textarea');
      ta.value = link; document.body.appendChild(ta); ta.select();
      document.execCommand('copy'); document.body.removeChild(ta);
      toast.push('Signing link copied');
    }
  };

  const resend = async () => {
    setBusy(true);
    try {
      const res = await fetch(apiBase + `/api/documents/${docId}/signers/${s.id}/resend`, {
        method: 'POST', credentials: 'same-origin', headers: csrfHeader(),
      });
      const d = await res.json();
      if (d.mode === 'resend' && d.ok) toast.push(`Email re-sent to ${s.email}`);
      else if (d.mode === 'dev')        toast.push('No mail provider — copy link instead', 'err');
      else                              toast.push(`Resend failed: ${d.reason || 'error'}`, 'err');
    } catch (ex) { toast.push('Resend failed', 'err'); }
    finally { setBusy(false); }
  };

  return (
    <div style={{ padding: '12px 14px', display: 'flex', alignItems: 'center', gap: 10,
                  borderBottom: isLast ? 'none' : '1px solid var(--line-2)' }}>
      <Avatar name={s.full_name} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 12, fontWeight: 500 }}>{s.full_name}</div>
        <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>{s.email}</div>
        {s.signed_geo_label && (
          <div style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 2, display: 'flex', alignItems: 'center', gap: 4 }}>
            <Icon name="map" size={9} color="var(--ink-3)" />
            {s.signed_geo_label}
          </div>
        )}
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
        <StatusBadge state={s.status === 'signed' ? 'signed' : s.status === 'declined' ? 'declined' : 'sent'}
                     label={s.status === 'signed' ? 'Signed' : s.status === 'declined' ? 'Declined' :
                            s.status === 'viewed' ? 'Viewed' : 'Pending'} />
        {isPending && (
          <div style={{ display: 'flex', gap: 4 }}>
            <button onClick={copyLink} type="button" title="Copy signing link"
                    className="btn btn-ghost btn-sm" style={{ padding: '4px 8px', fontSize: 10 }}>
              <Icon name="file" size={10} /> link
            </button>
            <button onClick={resend} type="button" disabled={busy} title="Resend invitation email"
                    className="btn btn-ghost btn-sm" style={{ padding: '4px 8px', fontSize: 10, opacity: busy ? 0.5 : 1 }}>
              <Icon name="mail" size={10} /> {busy ? '…' : 'mail'}
            </button>
          </div>
        )}
      </div>
    </div>
  );
};

const DocumentDetailPage = ({ id }) => {
  const [doc, setDoc] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [certBusy, setCertBusy] = React.useState(false);
  const [actBusy, setActBusy] = React.useState(false);
  const { user } = useAuth();
  const toast = useToast();
  const reload = React.useCallback(() => api(`/api/documents/${id}`).then(setDoc).catch(setError), [id]);
  React.useEffect(() => { reload(); }, [reload]);

  const downloadCert = async () => {
    if (!doc) return;
    setCertBusy(true);
    try {
      const audit = await api(`/api/documents/${doc.id}/audit`);
      const bytes = await buildCertificatePdf({
        doc, signers: doc.signers, events: audit.events,
        generatedFor: user?.email || '',
      });
      downloadBlob(bytes, `${doc.code || doc.id}-certificate.pdf`);
      toast.push('Certificate downloaded');
    } catch (ex) {
      toast.push('Certificate failed: ' + ex.message, 'err');
    } finally { setCertBusy(false); }
  };

  const isArchived = !!doc?.archived_at;
  const toggleArchive = async () => {
    setActBusy(true);
    try {
      await api(`/api/documents/${doc.id}/${isArchived ? 'unarchive' : 'archive'}`, { method: 'POST' });
      toast.push(isArchived ? 'Restored from archive' : 'Archived');
      await reload();
    } catch { toast.push('Could not update', 'err'); }
    finally { setActBusy(false); }
  };
  const del = async () => {
    if (!confirm(`Delete "${doc.title}"?\n\nSigning links will stop working. The audit trail is kept for compliance retention.`)) return;
    setActBusy(true);
    try {
      await api(`/api/documents/${doc.id}`, { method: 'DELETE' });
      toast.push('Document deleted');
      navigate('/dashboard');
    } catch { toast.push('Could not delete', 'err'); setActBusy(false); }
  };

  if (error) return <NotFoundPage />;
  if (!doc) return <AppShell active="all" title="Document"><div style={{ padding: 24, color: 'var(--ink-3)' }}>Loading…</div></AppShell>;

  const firstPending = doc.signers.find(s => s.status !== 'signed' && s.status !== 'declined');
  return (
    <AppShell active="all" title="Document">
      <div style={{ padding: '24px 28px', maxWidth: 980 }}>
        <Link to="/dashboard" className="btn-link" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginBottom: 18 }}>
          <Icon name="arrowL" size={11} /> Back to documents
        </Link>
        {doc.status !== 'signed' && doc.status !== 'declined' && firstPending && (
          <DevMailBanner />
        )}
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 28, gap: 24 }}>
          <div>
            <div className="eyebrow" style={{ marginBottom: 6 }}>Document · {doc.code}</div>
            <h1 className="display" style={{ fontSize: 28, margin: 0, marginBottom: 6 }}>{doc.title}</h1>
            <div className="mono" style={{ color: 'var(--ink-3)', fontSize: 11 }}>SHA‑256 {doc.sha256} · {doc.pages} pages · {(doc.size_bytes/1024).toFixed(0)} KB</div>
            {doc.verify_code && (
              <div style={{ marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 8,
                            padding: '6px 10px', background: 'var(--paper-2)',
                            border: '1px solid var(--line)', borderRadius: 2, fontSize: 11 }}>
                <Icon name="shield" size={11} color="var(--ink-3)" />
                <span style={{ color: 'var(--ink-3)' }}>Verify code</span>
                <Link to={`/verify?code=${doc.verify_code}`} className="mono btn-link"
                      style={{ fontWeight: 500, letterSpacing: '0.05em' }}>
                  {doc.verify_code}
                </Link>
              </div>
            )}
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 10 }}>
            <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
              <StatusBadge state={doc.status === 'draft' ? 'draft' : doc.status === 'signed' ? 'signed' : doc.status === 'declined' ? 'declined' : 'sent'} />
              {isArchived && (
                <span className="pill" data-state="draft" style={{ fontSize: 10 }}>
                  <span className="pill-dot" />Archived
                </span>
              )}
            </div>
            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
              <a className="btn btn-ghost btn-sm"
                 href={(window.SAIGN_API || '') + `/api/documents/${doc.id}/file?variant=${doc.status === 'signed' ? 'signed' : 'original'}`}
                 target="_blank" rel="noopener" style={{ textDecoration: 'none' }}>
                <Icon name="download" size={11} /> {doc.status === 'signed' ? 'Signed PDF' : 'Original PDF'}
              </a>
              <button className="btn btn-ghost btn-sm" onClick={downloadCert} disabled={certBusy}
                      style={{ opacity: certBusy ? 0.5 : 1 }}>
                <Icon name="shield" size={11} /> {certBusy ? 'Building…' : 'Certificate'}
              </button>
              <Link to={`/documents/${doc.id}/audit`} className="btn btn-ghost btn-sm" style={{ textDecoration: 'none' }}>
                <Icon name="activity" size={11} /> Audit trail
              </Link>
              <button className="btn btn-ghost btn-sm" onClick={toggleArchive} disabled={actBusy}
                      style={{ opacity: actBusy ? 0.5 : 1 }}>
                <Icon name="archive" size={11} /> {isArchived ? 'Restore' : 'Archive'}
              </button>
              <button className="btn btn-ghost btn-sm" onClick={del} disabled={actBusy}
                      style={{ opacity: actBusy ? 0.5 : 1, color: 'var(--danger)' }}>
                <Icon name="x" size={11} color="var(--danger)" /> Delete
              </button>
            </div>
          </div>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: 24, alignItems: 'flex-start' }}>
          <div style={{ background: 'var(--paper-2)', height: 'calc(100vh - 240px)', minHeight: 480 }}>
            <PdfViewer
              url={(window.SAIGN_API || '') + `/api/documents/${doc.id}/file?variant=${doc.status === 'signed' ? 'signed' : 'original'}`}
              renderOverlay={doc.status === 'signed' ? null : (pageNum) => (doc.fields || [])
                .filter(f => f.page === pageNum)
                .map(f => {
                  const signerIdx = doc.signers.findIndex(s => s.id === f.signer_id);
                  const colors = ['#4a3a2a', '#3d6b4d', '#a86b1a', '#9a3a2a', '#3a4a6b'];
                  const c = colors[signerIdx % colors.length];
                  return (
                    <div key={f.id} className="sigfield"
                      style={{
                        left: `${f.x_pct * 100}%`, top: `${f.y_pct * 100}%`,
                        width: `${f.w_pct * 100}%`, height: `${f.h_pct * 100}%`,
                        borderColor: c, color: c, background: `${c}11`, cursor: 'default',
                      }}>
                      {doc.signers[signerIdx]?.full_name || 'sign'}
                    </div>
                  );
                })} />
          </div>
          <div>
            <div className="eyebrow" style={{ marginBottom: 10 }}>Signers ({doc.total})</div>
            <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
              {doc.signers.map((s, i) => (
                <SignerRow key={s.id} signer={s} docId={doc.id}
                           isLast={i === doc.signers.length - 1} />
              ))}
            </div>
            {firstPending && (
              <Link to={`/sign/${firstPending.access_token}`} className="btn" style={{ textDecoration: 'none', justifyContent: 'center', width: '100%', marginTop: 14 }}>
                <Icon name="pen" size={11} color="var(--paper)" /> Preview signer view
              </Link>
            )}
            <div className="eyebrow" style={{ marginTop: 22, marginBottom: 10 }}>Verification</div>
            <div style={{ fontSize: 12, color: 'var(--ink-2)', display: 'flex', flexDirection: 'column', gap: 6 }}>
              <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}><Icon name="check" size={11} color="var(--positive)" /> Email verification</div>
              <div style={{ display: 'flex', gap: 8, alignItems: 'center', opacity: 0.4 }}><Icon name="x" size={11} color="var(--ink-3)" /> SMS code</div>
              <div style={{ display: 'flex', gap: 8, alignItems: 'center', opacity: 0.4 }}><Icon name="x" size={11} color="var(--ink-3)" /> Qualified signature (QES)</div>
            </div>
          </div>
        </div>
      </div>
    </AppShell>
  );
};

// ---------- Send flow ----------
const Stepper2 = ({ step }) => {
  const steps = ['Upload', 'Signers', 'Fields', 'Review'];
  return (
    <div className="stepper">
      {steps.map((s, i) => (
        <React.Fragment key={s}>
          <div className="stepper-step" data-active={i === step} data-done={i < step}>
            <span className="stepper-dot">{i < step ? <Icon name="check" size={9} color="currentColor" /> : i + 1}</span>
            <span className="stepper-label">{s}</span>
          </div>
          {i < steps.length - 1 && <div className="stepper-line" />}
        </React.Fragment>
      ))}
    </div>
  );
};

// ---------- FieldPlacer (drag-drop signature boxes onto PDF pages) ----------
// Coordinates stored as fractions of page dims (page-relative) so they
// survive any zoom level and PDF page size differences.
const FieldPlacer = ({ url, signers, fields, setFields, withCredentials = false }) => {
  const SIGNER_COLORS = ['#4a3a2a', '#3d6b4d', '#a86b1a', '#9a3a2a', '#3a4a6b'];
  const [activeSigner, setActiveSigner] = React.useState(signers[0]?.signer_id);
  const [dragId, setDragId] = React.useState(null);
  const dragInfo = React.useRef(null);

  // Keep activeSigner in sync if signers list changes
  React.useEffect(() => {
    if (!activeSigner || !signers.find(s => s.signer_id === activeSigner)) {
      setActiveSigner(signers[0]?.signer_id);
    }
  }, [signers, activeSigner]);

  const ptOf = (e) => {
    const t = e.touches?.[0] || e.changedTouches?.[0];
    return { clientX: t?.clientX ?? e.clientX, clientY: t?.clientY ?? e.clientY };
  };

  const placeField = (e, pageNum) => {
    if (e.target.closest('.sigfield')) return;  // existing field gets its own handler
    if (!activeSigner) return;
    e.preventDefault();
    const rect = e.currentTarget.getBoundingClientRect();
    const { clientX, clientY } = ptOf(e);
    const x_pct = (clientX - rect.left) / rect.width;
    const y_pct = (clientY - rect.top)  / rect.height;
    const w_pct = 0.22, h_pct = 0.06;
    const id = 'tmp_' + Math.random().toString(36).slice(2, 10);
    setFields(fs => [...fs, {
      id, signer_id: activeSigner,
      page: pageNum,
      x_pct: Math.max(0, Math.min(1 - w_pct, x_pct - w_pct/2)),
      y_pct: Math.max(0, Math.min(1 - h_pct, y_pct - h_pct/2)),
      w_pct, h_pct,
      field_type: 'signature',
    }]);
  };

  const startDrag = (e, f, kind = 'move') => {
    e.stopPropagation(); e.preventDefault();
    const pageEl = e.currentTarget.closest('.pdfv-page');
    const r = pageEl.getBoundingClientRect();
    const { clientX, clientY } = ptOf(e);
    dragInfo.current = {
      kind, fieldId: f.id, pageRect: r, startX: clientX, startY: clientY,
      origX: f.x_pct, origY: f.y_pct, origW: f.w_pct, origH: f.h_pct,
    };
    setDragId(f.id);
  };

  React.useEffect(() => {
    if (!dragId) return;
    const move = (e) => {
      const d = dragInfo.current;
      if (!d) return;
      const t = e.touches?.[0];
      const cx = t?.clientX ?? e.clientX;
      const cy = t?.clientY ?? e.clientY;
      const dxPct = (cx - d.startX) / d.pageRect.width;
      const dyPct = (cy - d.startY) / d.pageRect.height;
      setFields(fs => fs.map(f => {
        if (f.id !== d.fieldId) return f;
        if (d.kind === 'move') {
          return {
            ...f,
            x_pct: Math.max(0, Math.min(1 - f.w_pct, d.origX + dxPct)),
            y_pct: Math.max(0, Math.min(1 - f.h_pct, d.origY + dyPct)),
          };
        }
        return {
          ...f,
          w_pct: Math.max(0.05, Math.min(1 - f.x_pct, d.origW + dxPct)),
          h_pct: Math.max(0.02, Math.min(1 - f.y_pct, d.origH + dyPct)),
        };
      }));
    };
    const up = () => { dragInfo.current = null; setDragId(null); };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
    window.addEventListener('touchmove', move, { passive: false });
    window.addEventListener('touchend', up);
    return () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
      window.removeEventListener('touchmove', move);
      window.removeEventListener('touchend', up);
    };
  }, [dragId, setFields]);

  const removeField = (id) => setFields(fs => fs.filter(f => f.id !== id));

  const fieldsByPage = React.useMemo(() => {
    const m = {};
    for (const f of fields) (m[f.page] ||= []).push(f);
    return m;
  }, [fields]);

  const signerLabel = (id) => {
    const idx = signers.findIndex(s => s.signer_id === id);
    return { name: signers[idx]?.full_name || 'signer', color: SIGNER_COLORS[idx % SIGNER_COLORS.length] };
  };

  const renderOverlay = (pageNum) => (
    <div
      className="sigfield-canvas"
      data-armed={!!activeSigner}
      onMouseDown={(e) => placeField(e, pageNum)}
      onTouchStart={(e) => placeField(e, pageNum)}
      style={{ position: 'absolute', inset: 0, zIndex: 2,
               cursor: activeSigner ? 'crosshair' : 'not-allowed',
               touchAction: 'none' }}>
      {(fieldsByPage[pageNum] || []).map(f => {
        const lbl = signerLabel(f.signer_id);
        return (
          <div key={f.id}
               className="sigfield"
               data-mine={f.signer_id === activeSigner}
               style={{
                 left: `${f.x_pct * 100}%`, top: `${f.y_pct * 100}%`,
                 width: `${f.w_pct * 100}%`, height: `${f.h_pct * 100}%`,
                 borderColor: lbl.color, color: lbl.color,
                 background: f.signer_id === activeSigner ? `${lbl.color}22` : `${lbl.color}11`,
                 zIndex: 3,
               }}
               onMouseDown={(e) => startDrag(e, f, 'move')}
               onTouchStart={(e) => startDrag(e, f, 'move')}>
            <span style={{ pointerEvents: 'none' }}>{lbl.name} · sign</span>
            <button className="sigfield-del" onClick={(e) => { e.stopPropagation(); removeField(f.id); }} type="button">×</button>
            <div className="sigfield-handle"
                 onMouseDown={(e) => startDrag(e, f, 'resize')}
                 onTouchStart={(e) => startDrag(e, f, 'resize')} />
          </div>
        );
      })}
    </div>
  );

  if (signers.length === 0) {
    return (
      <div style={{ padding: '60px 32px', textAlign: 'center', maxWidth: 480, margin: '0 auto' }}>
        <div style={{ width: 56, height: 56, margin: '0 auto 18px', borderRadius: '50%',
          background: 'var(--paper-2)', display: 'flex', alignItems: 'center', justifyContent: 'center',
          border: '1px solid var(--line)' }}>
          <Icon name="user" size={22} color="var(--ink-3)" />
        </div>
        <h3 className="display" style={{ fontSize: 20, margin: 0, marginBottom: 8 }}>Add a signer first</h3>
        <p style={{ fontSize: 13, color: 'var(--ink-2)', margin: 0, marginBottom: 20, lineHeight: 1.5 }}>
          To place signature fields, you need at least one signer with both <strong>name</strong> and <strong>email</strong>.
          Go back to the previous step.
        </p>
        <Link to="/documents/new/signers" className="btn" style={{ textDecoration: 'none' }}>
          <Icon name="arrowL" size={11} color="var(--paper)" /> Back to signers
        </Link>
      </div>
    );
  }

  const activeSignerObj = signers.find(s => s.signer_id === activeSigner);
  const activeColor = activeSignerObj ? SIGNER_COLORS[signers.indexOf(activeSignerObj) % SIGNER_COLORS.length] : 'var(--ink-3)';

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 280px', height: '100%', minHeight: 0 }}>
      <div style={{ minHeight: 0, height: '100%', display: 'flex', flexDirection: 'column' }}>
        <div style={{ flexShrink: 0, padding: '8px 14px', background: '#fbf4e6', borderBottom: '1px solid #e8d4b0',
                      fontSize: 12, color: '#8a5a18', display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ width: 10, height: 10, borderRadius: 2, background: activeColor }} />
          {activeSignerObj ? (
            <>Click anywhere on the PDF below to place a signature field for <strong style={{ fontWeight: 500 }}>{activeSignerObj.full_name}</strong></>
          ) : (
            <>Pick a signer on the right →</>
          )}
        </div>
        <div style={{ flex: 1, minHeight: 0 }}>
          <PdfViewer url={url} initialZoom={1} withCredentials={withCredentials} renderOverlay={renderOverlay} />
        </div>
      </div>
      <div style={{ borderLeft: '1px solid var(--line)', padding: 18, overflow: 'auto', background: 'var(--paper)' }}>
        <div className="eyebrow" style={{ marginBottom: 8 }}>Signers</div>
        <p style={{ fontSize: 11, color: 'var(--ink-3)', margin: 0, marginBottom: 14, lineHeight: 1.4 }}>
          Pick a signer, then click anywhere on the document to drop a signature field for them.
        </p>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {signers.map((s, i) => {
            const c = SIGNER_COLORS[i % SIGNER_COLORS.length];
            const count = fields.filter(f => f.signer_id === s.signer_id).length;
            const active = activeSigner === s.signer_id;
            return (
              <button key={s.signer_id} onClick={() => setActiveSigner(s.signer_id)} type="button"
                style={{
                  border: `1.5px solid ${active ? c : 'var(--line)'}`,
                  background: active ? `${c}11` : 'var(--card)',
                  padding: '8px 10px', borderRadius: 2, cursor: 'pointer',
                  display: 'flex', alignItems: 'center', gap: 8,
                  fontFamily: 'var(--sans)', fontSize: 12, color: 'var(--ink)',
                  textAlign: 'left',
                }}>
                <span style={{ width: 10, height: 10, borderRadius: 2, background: c }} />
                <span style={{ flex: 1 }}>{s.full_name}</span>
                <span className="mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{count}</span>
              </button>
            );
          })}
        </div>
        <div className="eyebrow" style={{ marginTop: 22, marginBottom: 8 }}>Fields placed</div>
        <div className="mono" style={{ fontSize: 11, color: 'var(--ink-2)' }}>
          {fields.length} total · {Object.keys(fieldsByPage).length} page(s)
        </div>
        {fields.length === 0 && (
          <div style={{ marginTop: 14, padding: 12, background: 'var(--paper-2)', borderRadius: 2,
                        fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.5 }}>
            Click on the PDF to place the first signature field. You can drag fields around or resize from the corner.
          </div>
        )}
      </div>
    </div>
  );
};

// In-memory draft (per tab; File can't be serialised so it's only on window.__saign_draft)
const getDraft = () => (window.__saign_draft ||= {
  file: null, title: '', signers: [], fields: [], requireIdVerification: false,
});
const useDraft = () => {
  const d = getDraft();
  const [, force] = React.useReducer(x => x + 1, 0);
  return {
    file: d.file,
    title: d.title,
    signers: d.signers,
    fields: d.fields || [],
    requireIdVerification: !!d.requireIdVerification,
    setFile: (f) => { d.file = f; if (f && !d.title) d.title = f.name.replace(/\.pdf$/i, ''); force(); },
    setTitle: (t) => { d.title = t; force(); },
    setSigners: (s) => { d.signers = typeof s === 'function' ? s(d.signers) : s; force(); },
    setFields: (fs) => { d.fields = typeof fs === 'function' ? fs(d.fields || []) : fs; force(); },
    setRequireIdVerification: (v) => { d.requireIdVerification = !!v; force(); },
    reset: () => {
      window.__saign_draft = { file: null, title: '', signers: [], fields: [], requireIdVerification: false };
      force();
    },
  };
};

const NewDocStep1 = () => {
  const draft = useDraft();
  const inputRef = React.useRef(null);
  const [active, setActive] = React.useState(false);
  const [error, setError] = React.useState(null);

  const accept = (f) => {
    setError(null);
    if (!f) return;
    if (f.type !== 'application/pdf' && !f.name.toLowerCase().endsWith('.pdf')) {
      setError('Only PDF files are accepted.'); return;
    }
    if (f.size > 10 * 1024 * 1024) {
      setError('File too large (max 10 MB on the free tier).'); return;
    }
    if (f.size === 0) { setError('Empty file.'); return; }
    draft.setFile(f);
    navigate('/documents/new/signers');
  };

  const onDrop = (e) => { e.preventDefault(); setActive(false); accept(e.dataTransfer.files?.[0]); };
  const onDragOver = (e) => { e.preventDefault(); setActive(true); };
  const onDragLeave = () => setActive(false);

  return (
    <div className="saign-app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <div className="topnav">
        <Link to="/dashboard" className="btn-link" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <Icon name="arrowL" size={11} /> Documents
        </Link>
        <div style={{ flex: 1 }} />
        <Stepper2 step={0} />
      </div>
      <div style={{ flex: 1, padding: '36px 48px', overflow: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
        <div style={{ width: '100%', maxWidth: 560 }}>
          <h2 className="display" style={{ fontSize: 24, margin: 0, marginBottom: 6 }}>New document</h2>
          <p style={{ fontSize: 13, color: 'var(--ink-2)', margin: 0, marginBottom: 24 }}>Upload a PDF to get started. Max 10 MB.</p>
          <input ref={inputRef} type="file" accept="application/pdf,.pdf" style={{ display: 'none' }}
                 onChange={e => accept(e.target.files?.[0])} />
          <div className="dropzone" data-active={active}
               onClick={() => inputRef.current?.click()}
               onDrop={onDrop} onDragOver={onDragOver} onDragLeave={onDragLeave}>
            <div style={{ width: 48, height: 48, margin: '0 auto 14px', background: 'var(--paper-2)',
              display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 2 }}>
              <Icon name="upload" size={20} color="var(--ink-2)" />
            </div>
            <div className="display" style={{ fontSize: 17, marginBottom: 4 }}>
              {active ? 'Drop to upload' : 'Drop a PDF here'}
            </div>
            <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 18 }}>or</div>
            <button className="btn" type="button">Browse files</button>
            <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 18 }}>
              Stored on Cloudflare R2 (EU). SHA-256 of bytes goes into the audit trail.
            </div>
          </div>
          {error && (
            <div style={{ marginTop: 14, background: '#fbeae6', border: '1px solid #e8b8a8', color: 'var(--danger)', padding: '8px 12px', fontSize: 12, borderRadius: 2 }}>
              {error}
            </div>
          )}
          {draft.file && (
            <div style={{ marginTop: 14, fontSize: 12, color: 'var(--ink-2)' }}>
              Pending: <span className="file-pill"><Icon name="pdf" size={11} /> {draft.file.name} · {(draft.file.size/1024).toFixed(0)} KB</span>
              {' '}<Link to="/documents/new/signers" className="btn-link">Continue →</Link>
            </div>
          )}
          <div style={{ marginTop: 20, display: 'flex', alignItems: 'center', gap: 10, fontSize: 11, color: 'var(--ink-3)' }}>
            <Icon name="lock" size={11} /> Files stored in Cloudflare R2 (EU). Owner-only access.
          </div>
        </div>
      </div>
    </div>
  );
};

const NewDocStep2 = () => {
  const { file, title, setTitle, signers, setSigners } = useDraft();
  React.useEffect(() => { if (!file) navigate('/documents/new'); }, [file]);
  React.useEffect(() => {
    if (signers.length === 0) setSigners([{ full_name: '', email: '', role: 'Counterparty' }]);
  }, []);
  const update = (i, patch) => setSigners(s => s.map((x, idx) => idx === i ? { ...x, ...patch } : x));
  const add = () => setSigners(s => [...s, { full_name: '', email: '', role: 'Counterparty' }]);
  const remove = (i) => setSigners(s => s.filter((_, idx) => idx !== i));
  const cont = () => navigate('/documents/new/fields');
  if (!file) return null;
  return (
    <div className="saign-app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <div className="topnav">
        <Link to="/documents/new" className="btn-link" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <Icon name="arrowL" size={11} /> Upload
        </Link>
        <div style={{ flex: 1 }} />
        <Stepper2 step={1} />
      </div>
      <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '1fr 320px', overflow: 'hidden' }}>
        <div style={{ padding: '28px 48px', overflow: 'auto' }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 18 }}>
            <div>
              <h2 className="display" style={{ fontSize: 22, margin: 0, marginBottom: 4 }}>Add signers</h2>
              <p style={{ fontSize: 12, color: 'var(--ink-3)', margin: 0 }}>They'll receive an email with a unique signing link.</p>
            </div>
            <button onClick={add} className="btn btn-ghost btn-sm">
              <Icon name="plus" size={11} /> Add signer
            </button>
          </div>
          <div className="field" style={{ marginBottom: 18 }}>
            <label>Document title</label>
            <input value={title} onChange={e => setTitle(e.target.value)} />
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {signers.map((s, i) => (
              <div key={i} className="card" style={{ padding: 14, display: 'flex', alignItems: 'center', gap: 12 }}>
                <span style={{ color: 'var(--ink-3)' }}><Icon name="drag" size={14} /></span>
                <div style={{ width: 22, height: 22, borderRadius: '50%', background: 'var(--ink)', color: 'var(--paper)',
                  display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 500 }}>{i + 1}</div>
                <div className="field" style={{ flex: '0 0 180px' }}>
                  <input value={s.full_name} placeholder="Full name" onChange={e => update(i, { full_name: e.target.value })} />
                </div>
                <div className="field" style={{ flex: 1 }}>
                  <input value={s.email} placeholder="email@company.com" type="email" onChange={e => update(i, { email: e.target.value })} />
                </div>
                <div className="field" style={{ flex: '0 0 140px' }}>
                  <select value={s.role} onChange={e => update(i, { role: e.target.value })}>
                    <option>Counterparty</option><option>Witness</option><option>Reviewer</option>
                  </select>
                </div>
                <button onClick={() => remove(i)} className="btn-link" style={{ color: 'var(--ink-3)' }}>
                  <Icon name="x" size={12} />
                </button>
              </div>
            ))}
          </div>
        </div>
        <div style={{ borderLeft: '1px solid var(--line)', padding: '28px 24px', background: 'var(--paper)', overflow: 'auto' }}>
          <div className="eyebrow" style={{ marginBottom: 10 }}>Document</div>
          <div className="card" style={{ padding: 12, display: 'flex', alignItems: 'center', gap: 10 }}>
            <DocThumb size={36} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontWeight: 500, fontSize: 12, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{file.name}</div>
              <div className="mono" style={{ color: 'var(--ink-3)', fontSize: 10 }}>{(file.size/1024).toFixed(0)} KB · application/pdf</div>
            </div>
          </div>
          <div className="eyebrow" style={{ marginTop: 22, marginBottom: 10 }}>Signers ({signers.length})</div>
          {signers.map((s, i) => (
            <div key={i} style={{ fontSize: 12, color: 'var(--ink-2)', marginBottom: 4 }}>
              <strong style={{ fontWeight: 500 }}>{s.full_name || '(unnamed)'}</strong> · {s.email || 'no email'}
            </div>
          ))}
        </div>
      </div>
      <div style={{ borderTop: '1px solid var(--line)', padding: '14px 24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'var(--paper)' }}>
        <Link to="/documents/new" className="btn btn-ghost" style={{ textDecoration: 'none' }}>← Change file</Link>
        <button onClick={cont} className="btn">Place signature fields <Icon name="arrow" size={11} color="var(--paper)" /></button>
      </div>
    </div>
  );
};

// Step 3 — drag/drop signature fields onto the PDF (uses local file URL for preview)
const NewDocStep3Fields = () => {
  const draft = useDraft();
  const { file, signers, fields, setFields } = draft;
  const previewUrl = React.useMemo(() => file ? URL.createObjectURL(file) : null, [file]);
  React.useEffect(() => () => previewUrl && URL.revokeObjectURL(previewUrl), [previewUrl]);
  React.useEffect(() => { if (!file) navigate('/documents/new'); }, [file]);
  if (!file) return null;

  // Map draft signers (no real IDs yet) → stable per-position IDs for the placer.
  // We map ALL signers (even incomplete ones) so the placer can show a clear
  // hint when none are valid yet.
  const placerSigners = signers
    .map((s, idx) => ({ ...s, signer_id: `local_${idx}` }))
    .filter(s => s.email && s.full_name);

  const validFields = fields.filter(f => placerSigners.some(s => s.signer_id === f.signer_id));
  const cont = () => navigate('/documents/new/review');

  return (
    <div className="saign-app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <div className="topnav">
        <Link to="/documents/new/signers" className="btn-link" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <Icon name="arrowL" size={11} /> Signers
        </Link>
        <div style={{ flex: 1 }} />
        <Stepper2 step={2} />
      </div>
      <div style={{ flex: 1, minHeight: 0 }}>
        <FieldPlacer url={previewUrl} signers={placerSigners} fields={validFields} setFields={setFields} withCredentials={false} />
      </div>
      <div style={{ borderTop: '1px solid var(--line)', padding: '14px 24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'var(--paper)' }}>
        <Link to="/documents/new/signers" className="btn btn-ghost" style={{ textDecoration: 'none' }}>← Back to signers</Link>
        <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>
          {validFields.length === 0
            ? 'Tip: skipping field placement = a default field on the last page'
            : `${validFields.length} field(s) placed`}
        </div>
        <button onClick={cont} className="btn">Continue to review <Icon name="arrow" size={11} color="var(--paper)" /></button>
      </div>
    </div>
  );
};

const NewDocStep3 = () => {
  const draft = useDraft();
  const { file, title, signers, fields, reset, requireIdVerification, setRequireIdVerification } = draft;
  const toast = useToast();
  const [busy, setBusy] = React.useState(false);
  const previewUrl = React.useMemo(() => file ? URL.createObjectURL(file) : null, [file]);
  React.useEffect(() => () => previewUrl && URL.revokeObjectURL(previewUrl), [previewUrl]);
  React.useEffect(() => { if (!file) navigate('/documents/new'); }, [file]);
  if (!file) return null;

  const send = async (asDraft = false) => {
    setBusy(true);
    try {
      const valid = signers.filter(s => s.email && s.full_name);
      if (valid.length === 0) { toast.push('Add at least one signer', 'err'); setBusy(false); return; }
      const apiBase = (typeof window !== 'undefined' && window.SAIGN_API) || '';

      // 1. upload doc + signers
      const fd = new FormData();
      fd.append('file', file, file.name);
      fd.append('title', title || file.name.replace(/\.pdf$/i, ''));
      fd.append('signers', JSON.stringify(valid));
      fd.append('send', asDraft ? 'false' : 'true');
      if (requireIdVerification) fd.append('require_id_verification', 'true');
      const res = await fetch(apiBase + '/api/documents', { method: 'POST', body: fd, credentials: 'same-origin', headers: csrfHeader() });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'upload_failed');

      // 2. resolve local signer indices → server signer ids, then PUT fields
      const serverFields = (fields || []).map(f => {
        const idx = parseInt((f.signer_id || '').replace('local_', ''), 10);
        const target = data.signers?.[idx];
        return target ? {
          signer_id: target.id, page: f.page,
          x_pct: f.x_pct, y_pct: f.y_pct, w_pct: f.w_pct, h_pct: f.h_pct,
          field_type: f.field_type || 'signature',
        } : null;
      }).filter(Boolean);
      if (serverFields.length > 0) {
        await fetch(apiBase + `/api/documents/${data.id}/fields`, {
          method: 'PUT', credentials: 'same-origin',
          headers: { 'Content-Type': 'application/json', ...csrfHeader() },
          body: JSON.stringify({ fields: serverFields }),
        });
      }

      reset();
      toast.push(asDraft ? 'Saved as draft' : `Sent to ${valid.length} signer(s)`);
      navigate(asDraft ? `/documents/${data.id}` : '/dashboard');
    } catch (ex) {
      toast.push(ex.message || 'Failed to send', 'err');
    } finally { setBusy(false); }
  };

  return (
    <div className="saign-app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <div className="topnav">
        <Link to="/documents/new/fields" className="btn-link" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <Icon name="arrowL" size={11} /> Fields
        </Link>
        <div style={{ flex: 1 }} />
        <Stepper2 step={3} />
      </div>
      <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '1fr 360px', overflow: 'hidden' }}>
        <div style={{ background: 'var(--paper-2)' }}>
          <PdfViewer url={previewUrl} maxPages={5} withCredentials={false} />
        </div>
        <div style={{ borderLeft: '1px solid var(--line)', padding: '28px 24px', overflow: 'auto' }}>
          <h2 className="display" style={{ fontSize: 20, margin: 0, marginBottom: 4 }}>Review and send</h2>
          <p style={{ fontSize: 12, color: 'var(--ink-3)', margin: 0, marginBottom: 20 }}>Once sent, signers will receive a unique link.</p>
          <div className="eyebrow" style={{ marginBottom: 10 }}>Document</div>
          <div className="card" style={{ padding: 12, marginBottom: 20 }}>
            <div style={{ fontWeight: 500, fontSize: 12, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{file.name}</div>
            <div className="mono" style={{ color: 'var(--ink-3)', fontSize: 10, marginTop: 2 }}>SES · {(file.size/1024).toFixed(0)} KB · {file.type || 'application/pdf'}</div>
          </div>
          <div className="eyebrow" style={{ marginBottom: 10 }}>Signers ({signers.length})</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 20 }}>
            {signers.map((s, i) => (
              <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0',
                                    borderBottom: i < signers.length - 1 ? '1px solid var(--line-2)' : 'none' }}>
                <span style={{ width: 18, fontSize: 10, color: 'var(--ink-3)', fontFamily: 'var(--mono)' }}>{i + 1}.</span>
                <Avatar name={s.full_name} />
                <div style={{ flex: 1 }}>
                  <div style={{ fontSize: 12, fontWeight: 500 }}>{s.full_name}</div>
                  <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>{s.email}</div>
                </div>
              </div>
            ))}
          </div>
          <div className="eyebrow" style={{ marginBottom: 10 }}>Verification</div>
          <div style={{ fontSize: 12, color: 'var(--ink-2)', display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 14 }}>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}><Icon name="check" size={11} color="var(--positive)" /> Email verification</div>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center', opacity: 0.4 }}><Icon name="x" size={11} color="var(--ink-3)" /> SMS code</div>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center', opacity: 0.4 }}><Icon name="x" size={11} color="var(--ink-3)" /> Qualified signature (QES)</div>
          </div>
          <label className="card" style={{
            padding: 12, marginBottom: 24, display: 'flex', gap: 10, alignItems: 'flex-start',
            cursor: 'pointer', borderColor: requireIdVerification ? 'var(--ink)' : 'var(--line)',
          }}>
            <input type="checkbox" className="chk" style={{ marginTop: 2 }}
                   checked={requireIdVerification}
                   onChange={e => setRequireIdVerification(e.target.checked)} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 12, fontWeight: 500, marginBottom: 3 }}>
                Require ID verification
              </div>
              <div style={{ fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.45 }}>
                Each signer uploads an ID photo and confirms name, date of birth and address before they can sign. Details land on the certificate.
              </div>
            </div>
          </label>
          <button onClick={() => send(false)} disabled={busy} className="btn btn-lg" style={{ width: '100%', justifyContent: 'center', opacity: busy ? 0.6 : 1 }}>
            <Icon name="send" size={12} color="var(--paper)" /> {busy ? 'Sending…' : 'Send for signature'}
          </button>
          <button onClick={() => send(true)} disabled={busy} className="btn btn-ghost" style={{ width: '100%', justifyContent: 'center', marginTop: 8, opacity: busy ? 0.6 : 1 }}>
            Save as draft
          </button>
        </div>
      </div>
    </div>
  );
};

// ---------- Public signing ----------
const typedSignatureToPng = (text, w = 480, h = 140) => {
  const c = document.createElement('canvas');
  c.width = w; c.height = h;
  const ctx = c.getContext('2d');
  ctx.fillStyle = '#1a1814';
  ctx.font = '64px "Caveat", cursive, serif';
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';
  ctx.fillText(text, w / 2, h / 2);
  return c.toDataURL('image/png');
};

const dataUrlToBytes = (dataUrl) => {
  const b64 = dataUrl.split(',')[1];
  const bin = atob(b64);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
};

const downloadBlob = (bytes, filename, mime = 'application/pdf') => {
  const url = URL.createObjectURL(new Blob([bytes], { type: mime }));
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  setTimeout(() => URL.revokeObjectURL(url), 1000);
};

// Build a PDF "Certificate of Completion" client-side using pdf-lib.
// Single source of truth for both owner-side and signer-side downloads.
const buildCertificatePdf = async ({ doc, signers, events, generatedFor }) => {
  const PDFLib = window.PDFLib;
  if (!PDFLib) throw new Error('pdf-lib not loaded');
  const pdfDoc = await PDFLib.PDFDocument.create();
  const helv = await pdfDoc.embedFont(PDFLib.StandardFonts.Helvetica);
  const helvB = await pdfDoc.embedFont(PDFLib.StandardFonts.HelveticaBold);
  const mono = await pdfDoc.embedFont(PDFLib.StandardFonts.Courier);

  const PAGE_W = 595, PAGE_H = 842;
  const M = 48;
  const ink = PDFLib.rgb(0.10, 0.094, 0.078);
  const ink2 = PDFLib.rgb(0.29, 0.275, 0.243);
  const ink3 = PDFLib.rgb(0.54, 0.518, 0.471);
  const accent = PDFLib.rgb(0.722, 0.459, 0.290);
  const line = PDFLib.rgb(0.847, 0.827, 0.78);
  const paper2 = PDFLib.rgb(0.937, 0.925, 0.898);
  const positive = PDFLib.rgb(0.18, 0.42, 0.27);
  const danger = PDFLib.rgb(0.62, 0.22, 0.18);

  let page = pdfDoc.addPage([PAGE_W, PAGE_H]);
  let y = PAGE_H - M;

  const newPage = () => { page = pdfDoc.addPage([PAGE_W, PAGE_H]); y = PAGE_H - M; };
  const ensure = (h) => { if (y - h < M) newPage(); };

  const text = (s, x, opts = {}) => {
    const f = opts.bold ? helvB : (opts.mono ? mono : helv);
    page.drawText(String(s), { x, y, size: opts.size || 10, font: f, color: opts.color || ink });
  };
  const wrap = (s, x, maxW, opts = {}) => {
    const f = opts.bold ? helvB : (opts.mono ? mono : helv);
    const sz = opts.size || 10;
    const words = String(s ?? '').split(/\s+/);
    const lines = []; let cur = '';
    for (const w of words) {
      const next = cur ? cur + ' ' + w : w;
      if (f.widthOfTextAtSize(next, sz) > maxW && cur) { lines.push(cur); cur = w; } else cur = next;
    }
    if (cur) lines.push(cur);
    for (const ln of lines) {
      ensure(sz + 4);
      page.drawText(ln, { x, y, size: sz, font: f, color: opts.color || ink });
      y -= sz + 2;
    }
  };
  const hr = (color = line) => { ensure(8); page.drawLine({
    start: { x: M, y }, end: { x: PAGE_W - M, y }, thickness: 0.5, color }); y -= 10; };
  const gap = (h) => { y -= h; };
  const kv = (k, v, opts = {}) => {
    ensure(14);
    text(k, M, { size: 9, color: ink3 });
    const vText = (v == null || v === '') ? '—' : String(v);
    const f = opts.mono ? mono : helv;
    const w = f.widthOfTextAtSize(vText, 10);
    page.drawText(vText, { x: PAGE_W - M - w, y, size: 10, font: f, color: opts.color || ink });
    y -= 14;
  };

  // ----- Header -----
  text('SAIGN', M, { bold: true, size: 13 });
  page.drawText('CERTIFICATE OF COMPLETION', {
    x: PAGE_W - M - helvB.widthOfTextAtSize('CERTIFICATE OF COMPLETION', 9),
    y, size: 9, font: helvB, color: ink3,
  });
  y -= 18;
  page.drawLine({ start: { x: M, y }, end: { x: PAGE_W - M, y }, thickness: 0.8, color: ink });
  y -= 22;

  // ----- Document title -----
  ensure(28);
  page.drawText('Document', { x: M, y, size: 9, font: helvB, color: ink3 });
  y -= 12;
  ensure(22);
  page.drawText(doc.title || '—', { x: M, y, size: 18, font: helvB, color: ink });
  y -= 18;
  text(`${doc.code || ''}  ·  ${doc.pages || '?'} pages  ·  ${doc.size_bytes ? (doc.size_bytes/1024).toFixed(1) + ' KB' : ''}`,
       M, { size: 10, color: ink2 });
  y -= 18;

  hr();

  // ----- Verification banner — most prominent thing on the page besides the title -----
  if (doc.verify_code) {
    ensure(60);
    const bannerH = 50;
    page.drawRectangle({
      x: M, y: y - bannerH, width: PAGE_W - 2 * M, height: bannerH,
      color: paper2, borderColor: ink, borderWidth: 0.8,
    });
    page.drawText('VERIFY THIS DOCUMENT', {
      x: M + 14, y: y - 14, size: 8, font: helvB, color: ink3,
    });
    page.drawText(doc.verify_code, {
      x: M + 14, y: y - 32, size: 18, font: helvB, color: ink,
    });
    const verifyUrl = (typeof window !== 'undefined' && window.SAIGN_API ? window.SAIGN_API
                       : (typeof window !== 'undefined' ? window.location.origin : 'https://saign.pages.dev'))
                       + `/#/verify?code=${doc.verify_code}`;
    page.drawText(verifyUrl, {
      x: M + 14, y: y - 44, size: 7, font: mono, color: ink2,
    });
    const hint = 'Anyone with this code can confirm authenticity at the URL above';
    page.drawText(hint, {
      x: PAGE_W - M - helv.widthOfTextAtSize(hint, 7) - 14,
      y: y - 14, size: 7, font: helv, color: ink3,
    });
    y -= bannerH + 16;
  }

  // ----- Document hashes -----
  kv('Status', (doc.status || 'unknown').toUpperCase(),
     { color: doc.status === 'signed' ? positive : doc.status === 'declined' ? danger : ink });
  kv('Document ID', doc.id || '—', { mono: true });
  kv('Verify code', doc.verify_code || '—', { mono: true });
  kv('SHA-256 (original)', doc.sha256 || '—', { mono: true });
  kv('SHA-256 (signed)', doc.sha256_signed || '—', { mono: true });
  if (doc.created_at) kv('Created at', doc.created_at, { mono: true });
  if (doc.completed_at) kv('Completed at', doc.completed_at, { mono: true });
  gap(6);
  hr();

  // ----- Signers -----
  ensure(20);
  page.drawText('Signers', { x: M, y, size: 9, font: helvB, color: ink3 });
  y -= 14;
  const docTypeLabel = (t) => ({
    passport: 'Passport',
    id_card: 'National ID card',
    drivers_license: "Driver's licence",
  }[t] || t || '—');
  for (const s of signers) {
    const hasId = !!s.id_verified_at;
    // Base card height + extra room for the identity sub-block when present.
    const idBlockH = hasId ? 76 : 0;
    const cardH = 86 + idBlockH;
    ensure(cardH + 10);
    const cardTop = y + 4;
    page.drawRectangle({ x: M, y: cardTop - cardH, width: PAGE_W - 2 * M, height: cardH,
      color: paper2, borderColor: line, borderWidth: 0.5 });
    let yy = cardTop - 14;
    page.drawText(s.full_name || s.email, { x: M + 12, y: yy, size: 11, font: helvB, color: ink });
    const stPill = (s.status || 'pending').toUpperCase();
    const pillColor = s.status === 'signed' ? positive : s.status === 'declined' ? danger : ink2;
    const pillW = helvB.widthOfTextAtSize(stPill, 8) + 12;
    page.drawText(stPill, { x: PAGE_W - M - 12 - pillW + 6, y: yy + 2, size: 8, font: helvB, color: pillColor });
    yy -= 14;
    page.drawText(s.email || '', { x: M + 12, y: yy, size: 9, font: helv, color: ink2 });
    yy -= 14;
    const row = (label, val, opts = {}) => {
      page.drawText(label, { x: M + 12, y: yy, size: 8, font: helv, color: ink3 });
      const vstr = (val == null || val === '') ? '—' : String(val);
      const f = opts.mono ? mono : helv;
      page.drawText(vstr, { x: M + 110, y: yy, size: 9, font: f, color: opts.color || ink });
      yy -= 11;
    };
    row('Method', s.signed_method || '—');
    row('Signed at', s.signed_at || '—');
    row('IP address', maskIp(s.signed_ip));
    const locLabel = s.signed_geo_label
      ? (s.signed_geo_source === 'browser' ? `${s.signed_geo_label} (precise)` : `${s.signed_geo_label} (ip)`)
      : '—';
    row('Location', locLabel);
    if (s.signed_geo_lat != null && s.signed_geo_lon != null) {
      page.drawText('Coords', { x: M + 12, y: yy, size: 8, font: helv, color: ink3 });
      page.drawText(`${s.signed_geo_lat.toFixed(5)}, ${s.signed_geo_lon.toFixed(5)}` +
                    (s.signed_geo_accuracy ? ` ±${Math.round(s.signed_geo_accuracy)}m` : ''),
                    { x: M + 110, y: yy, size: 9, font: mono, color: ink });
      yy -= 11;
    }
    if (hasId) {
      // Divider above the identity block — keep it visually distinct.
      yy -= 4;
      page.drawLine({ start: { x: M + 12, y: yy + 2 }, end: { x: PAGE_W - M - 12, y: yy + 2 },
                      thickness: 0.4, color: line });
      yy -= 6;
      page.drawText('IDENTITY (self-attested)', {
        x: M + 12, y: yy, size: 7, font: helvB, color: positive,
      });
      const verifTime = s.id_verified_at || '';
      const w = mono.widthOfTextAtSize(verifTime, 7);
      page.drawText(verifTime, { x: PAGE_W - M - 12 - w, y: yy, size: 7, font: mono, color: ink3 });
      yy -= 11;
      row('Name', s.id_full_name || '—');
      row('Date of birth', s.id_birth_date || '—');
      const cc = s.id_country ? ` · ${s.id_country}` : '';
      const docLine = `${docTypeLabel(s.id_doc_type)}${cc}${s.id_doc_number_last4 ? '  ····' + s.id_doc_number_last4 : ''}`;
      row('ID document', docLine);
      if (s.id_mrz_text) {
        page.drawText('MRZ', { x: M + 12, y: yy, size: 8, font: helv, color: ink3 });
        page.drawText('captured', { x: M + 110, y: yy, size: 9, font: helv, color: positive });
        yy -= 11;
      }
      // Address may wrap; render on a separate label/value pair using wrap()
      page.drawText('Address', { x: M + 12, y: yy, size: 8, font: helv, color: ink3 });
      const addrLines = String(s.id_address || '—').split(/\r?\n/);
      let firstLine = true;
      for (const ln of addrLines) {
        page.drawText(ln, { x: M + 110, y: yy, size: 9, font: helv, color: ink });
        yy -= 11;
        firstLine = false;
      }
      if (firstLine) yy -= 11;
    }
    y = cardTop - cardH - 8;
  }
  gap(2);
  hr();

  // ----- Audit trail -----
  ensure(20);
  page.drawText('Audit trail', { x: M, y, size: 9, font: helvB, color: ink3 });
  page.drawText('hash-chained · sha-256', {
    x: PAGE_W - M - helv.widthOfTextAtSize('hash-chained · sha-256', 8),
    y, size: 8, font: helv, color: ink3,
  });
  y -= 14;
  for (const e of events) {
    ensure(28);
    page.drawText(e.created_at || '', { x: M, y, size: 8, font: mono, color: ink3 });
    page.drawText(e.action || '', { x: M + 110, y, size: 9, font: helvB, color: ink });
    if (e.actor_email) {
      const w = helv.widthOfTextAtSize(e.actor_email, 8);
      page.drawText(e.actor_email, { x: PAGE_W - M - w, y, size: 8, font: helv, color: ink2 });
    }
    y -= 11;
    if (e.detail) {
      wrap(e.detail, M + 110, PAGE_W - 2 * M - 110, { size: 8, color: ink2 });
    }
    if (e.row_hash) {
      ensure(11);
      page.drawText('hash ' + e.row_hash.slice(0, 24) + '…',
        { x: M + 110, y, size: 7, font: mono, color: ink3 });
      y -= 10;
    }
    if (e.ip) {
      const ipt = 'ip ' + maskIp(e.ip);
      const w = mono.widthOfTextAtSize(ipt, 7);
      page.drawText(ipt, { x: PAGE_W - M - w, y: y + 10, size: 7, font: mono, color: ink3 });
    }
    y -= 4;
  }

  // ----- Footer on every page -----
  const pages = pdfDoc.getPages();
  const total = pages.length;
  pages.forEach((p, i) => {
    const footY = 24;
    p.drawLine({ start: { x: M, y: footY + 14 }, end: { x: PAGE_W - M, y: footY + 14 }, thickness: 0.3, color: line });
    p.drawText(`Generated ${new Date().toISOString().slice(0, 19).replace('T', ' ')} UTC`,
      { x: M, y: footY, size: 8, font: helv, color: ink3 });
    if (generatedFor) {
      p.drawText(`for ${generatedFor}`, {
        x: M + helv.widthOfTextAtSize(`Generated ${new Date().toISOString().slice(0, 19).replace('T', ' ')} UTC `, 8),
        y: footY, size: 8, font: helv, color: ink3,
      });
    }
    const pn = `${i + 1} / ${total}`;
    p.drawText(pn, {
      x: PAGE_W - M - helv.widthOfTextAtSize(pn, 8),
      y: footY, size: 8, font: helv, color: ink3,
    });
    p.drawText(doc.code || doc.id || '', {
      x: PAGE_W / 2 - mono.widthOfTextAtSize(doc.code || doc.id || '', 8) / 2,
      y: footY, size: 8, font: mono, color: ink3,
    });
  });

  return pdfDoc.save();
};

// Best-effort browser geolocation. Resolves with { lat, lon, accuracy } or null
// (timed out / denied / unsupported). Never throws — signing must keep working
// regardless of location consent.
const getBrowserLocation = () => new Promise((resolve) => {
  if (!navigator.geolocation) return resolve(null);
  let done = false;
  const timer = setTimeout(() => { if (!done) { done = true; resolve(null); } }, 8000);
  navigator.geolocation.getCurrentPosition(
    (pos) => { if (done) return; done = true; clearTimeout(timer);
      resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude, accuracy: pos.coords.accuracy }); },
    () => { if (done) return; done = true; clearTimeout(timer); resolve(null); },
    { enableHighAccuracy: false, timeout: 7000, maximumAge: 60_000 }
  );
});

// Reverse-geocode lat/lon to a "City, Region, Country" label using the public
// Nominatim API. Best-effort — falls back to coordinate string on failure.
const reverseGeocode = async (lat, lon) => {
  try {
    const url = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}&zoom=10`;
    const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
    if (!r.ok) throw new Error('http_' + r.status);
    const j = await r.json();
    const a = j.address || {};
    const city = a.city || a.town || a.village || a.municipality || a.county || null;
    const region = a.state || a.region || null;
    const country = a.country_code ? a.country_code.toUpperCase() : (a.country || null);
    const parts = [city, region, country].filter(Boolean);
    return parts.length ? parts.join(', ') : null;
  } catch { return null; }
};

// ---------- ID document support: countries, MRZ parser, OCR loader ----------

// Countries that we have explicit rules for. Anything else falls back to the
// per-doctype DEFAULT rule from the server. Sorted by likely audience.
const ID_COUNTRIES = [
  { code: 'DE', name: 'Germany' },
  { code: 'AT', name: 'Austria' },
  { code: 'CH', name: 'Switzerland' },
  { code: 'FR', name: 'France' },
  { code: 'IT', name: 'Italy' },
  { code: 'ES', name: 'Spain' },
  { code: 'NL', name: 'Netherlands' },
  { code: 'BE', name: 'Belgium' },
  { code: 'PL', name: 'Poland' },
  { code: 'LU', name: 'Luxembourg' },
  { code: 'GB', name: 'United Kingdom' },
  { code: 'IE', name: 'Ireland' },
  { code: 'US', name: 'United States' },
  { code: 'CA', name: 'Canada' },
  { code: 'OTHER', name: 'Other / not listed' },
];

// MRZ check-digit algorithm (ICAO 9303). Used to validate the dates and
// document number we read out of the machine-readable zone.
const _mrzCharVal = (ch) => {
  if (ch === '<') return 0;
  if (ch >= '0' && ch <= '9') return ch.charCodeAt(0) - 48;
  if (ch >= 'A' && ch <= 'Z') return ch.charCodeAt(0) - 55;
  return 0;
};
const mrzCheckDigit = (s) => {
  const w = [7, 3, 1];
  let sum = 0;
  for (let i = 0; i < s.length; i++) sum += _mrzCharVal(s[i]) * w[i % 3];
  return sum % 10;
};
// Convert a 2-digit MRZ year (YYMMDD) to a full ISO date. Birth dates pivot
// at +20: 25 → 2025 if it would otherwise be in the future, else 1925.
const mrzDateToIso = (yy, mm, dd, kind) => {
  const Y = parseInt(yy, 10), M = parseInt(mm, 10), D = parseInt(dd, 10);
  if (!(Y >= 0 && M >= 1 && M <= 12 && D >= 1 && D <= 31)) return null;
  const cur = new Date().getFullYear() % 100;
  let full;
  if (kind === 'birth') full = (Y > cur + 20) ? 1900 + Y : 2000 + Y;
  else /* expiry */     full = (Y < cur - 50) ? 2100 + Y : 2000 + Y;
  return `${full}-${String(M).padStart(2, '0')}-${String(D).padStart(2, '0')}`;
};

// Pull the MRZ block out of an arbitrary OCR string, then parse it.
// Supports TD1 (3×30, modern EU IDs) and TD3 (2×44, passports).
// Returns null if no recognisable MRZ found.
const parseMrz = (raw) => {
  if (!raw) return null;
  const lines = raw.toUpperCase()
    .replace(/[ ]/g, ' ')
    .split(/\r?\n/)
    .map(l => l.replace(/[^A-Z0-9<]/g, ''))
    .filter(Boolean);

  // TD1 — three lines of 30 chars each.
  for (let i = 0; i + 2 < lines.length; i++) {
    const a = lines[i], b = lines[i + 1], c = lines[i + 2];
    if (a.length === 30 && b.length === 30 && c.length === 30) {
      try {
        const docNumber = a.substring(5, 14).replace(/</g, '');
        const docNumChk = a.substring(14, 15);
        const docNumOk  = String(mrzCheckDigit(a.substring(5, 14))) === docNumChk;
        const birth     = mrzDateToIso(b.substring(0, 2), b.substring(2, 4), b.substring(4, 6), 'birth');
        const birthChk  = b.substring(6, 7);
        const birthOk   = String(mrzCheckDigit(b.substring(0, 6))) === birthChk;
        const sex       = b.substring(7, 8);
        const expiry    = mrzDateToIso(b.substring(8, 10), b.substring(10, 12), b.substring(12, 14), 'expiry');
        const country   = a.substring(2, 5).replace(/</g, '');
        const nameField = c.substring(0, 30);
        const [surname, given] = nameField.split('<<').map(p => p.replace(/</g, ' ').trim());
        return {
          format: 'TD1', country,
          doc_number: docNumber, doc_number_ok: docNumOk,
          birth_date: birth, birth_date_ok: birthOk,
          expiry_date: expiry,
          sex,
          full_name: [given, surname].filter(Boolean).join(' '),
          raw: [a, b, c].join('\n'),
        };
      } catch { /* try next window */ }
    }
  }

  // TD3 — two lines of 44 chars (passports).
  for (let i = 0; i + 1 < lines.length; i++) {
    const a = lines[i], b = lines[i + 1];
    if (a.length === 44 && b.length === 44) {
      try {
        const country   = a.substring(2, 5).replace(/</g, '');
        const nameField = a.substring(5, 44);
        const [surname, given] = nameField.split('<<').map(p => p.replace(/</g, ' ').trim());
        const docNumber = b.substring(0, 9).replace(/</g, '');
        const docNumChk = b.substring(9, 10);
        const docNumOk  = String(mrzCheckDigit(b.substring(0, 9))) === docNumChk;
        const birth     = mrzDateToIso(b.substring(13, 15), b.substring(15, 17), b.substring(17, 19), 'birth');
        const birthChk  = b.substring(19, 20);
        const birthOk   = String(mrzCheckDigit(b.substring(13, 19))) === birthChk;
        const sex       = b.substring(20, 21);
        const expiry    = mrzDateToIso(b.substring(21, 23), b.substring(23, 25), b.substring(25, 27), 'expiry');
        return {
          format: 'TD3', country,
          doc_number: docNumber, doc_number_ok: docNumOk,
          birth_date: birth, birth_date_ok: birthOk,
          expiry_date: expiry,
          sex,
          full_name: [given, surname].filter(Boolean).join(' '),
          raw: [a, b].join('\n'),
        };
      } catch { /* try next window */ }
    }
  }
  return null;
};

// Compare a user-typed value against the MRZ-extracted value. We're lenient
// about case, whitespace and obvious OCR confusions (0/O, 1/I/L) so we don't
// reject a perfectly valid document over a single misread character.
const _normIdValue = (s) => String(s || '').toUpperCase()
  .replace(/[\s<]/g, '')
  .replace(/[OQ]/g, '0')
  .replace(/[ILT]/g, '1');

// Cross-check a parsed MRZ result against the form values. Returns
// { ok, mismatches: [{ field, mrz, typed }] }. Missing-on-typed counts as
// mismatch — the user has to confirm the field even if the MRZ is right.
const crossCheckMrz = (mrz, typed) => {
  if (!mrz) return { ok: true, mismatches: [] };
  const mismatches = [];
  if (mrz.doc_number && mrz.doc_number_ok) {
    if (_normIdValue(typed.docNum) !== _normIdValue(mrz.doc_number)) {
      mismatches.push({ field: 'doc_number', mrz: mrz.doc_number, typed: typed.docNum });
    }
  }
  if (mrz.birth_date && mrz.birth_date_ok) {
    if (typed.birth !== mrz.birth_date) {
      mismatches.push({ field: 'birth_date', mrz: mrz.birth_date, typed: typed.birth });
    }
  }
  if (mrz.full_name && typed.fullName) {
    // Tolerant name match: every word in the MRZ name must appear (case-
    // insensitive) somewhere in the typed name. Catches OCR noise + the user
    // adding/removing a middle name without rejecting valid IDs.
    const mrzWords = mrz.full_name.toUpperCase().split(/\s+/).filter(w => w.length > 1);
    const typedUp  = typed.fullName.toUpperCase();
    const allFound = mrzWords.every(w => typedUp.includes(w));
    if (!allFound && mrzWords.length > 0) {
      mismatches.push({ field: 'full_name', mrz: mrz.full_name, typed: typed.fullName });
    }
  }
  return { ok: mismatches.length === 0, mismatches };
};

// Lazy-load Tesseract.js from a CDN. Resolves with the global `Tesseract`
// object, or rejects on script load failure / network error. Cached after
// the first successful load — we only ever inject the script tag once.
let _tesseractPromise = null;
const loadTesseract = () => {
  if (typeof window === 'undefined') return Promise.reject(new Error('no_window'));
  if (window.Tesseract) return Promise.resolve(window.Tesseract);
  if (_tesseractPromise) return _tesseractPromise;
  _tesseractPromise = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = 'https://cdn.jsdelivr.net/npm/tesseract.js@5.0.5/dist/tesseract.min.js';
    s.async = true;
    s.onload = () => window.Tesseract ? resolve(window.Tesseract) : reject(new Error('tesseract_missing'));
    s.onerror = () => { _tesseractPromise = null; reject(new Error('tesseract_load_failed')); };
    document.head.appendChild(s);
  });
  return _tesseractPromise;
};

// Run OCR over an image File and try to extract MRZ + raw text. Best-effort:
// returns `{ text, mrz, confidence }` even when MRZ parsing fails. Throws
// on engine load failure so the caller can let the user keep typing manually.
const ocrIdImage = async (file, onProgress) => {
  const Tesseract = await loadTesseract();
  // Tesseract.recognize accepts File/Blob directly. We pass language 'eng' —
  // names + MRZ characters are A-Z/0-9 so English data is enough.
  const { data } = await Tesseract.recognize(file, 'eng', {
    logger: (m) => onProgress?.(m),
  });
  const text = data?.text || '';
  return { text, mrz: parseMrz(text), confidence: (data?.confidence ?? 0) / 100 };
};

// Self-attested identity verification card. Country picker + front/back
// upload + optional client-side OCR (Tesseract.js) that auto-fills the form
// from the MRZ when present. Once submitted successfully, the panel
// collapses to a confirmation block.
const IdVerificationCard = ({ token, apiBase, initialName, verified, disabled, onVerified }) => {
  const [rules, setRules]       = React.useState(null);
  const [docType, setDocType]   = React.useState('id_card');
  const [country, setCountry]   = React.useState('DE');
  const [fullName, setFullName] = React.useState(initialName || '');
  const [birth, setBirth]       = React.useState('');
  const [address, setAddress]   = React.useState('');
  const [docNum, setDocNum]     = React.useState('');
  const [front, setFront]       = React.useState(null);
  const [back, setBack]         = React.useState(null);
  const [mrz, setMrz]           = React.useState(null);
  const [ocrConf, setOcrConf]   = React.useState(null);
  const [ocrBusy, setOcrBusy]   = React.useState(null);  // 'front' | 'back' | null
  const [ocrMsg, setOcrMsg]     = React.useState(null);
  const [busy, setBusy]         = React.useState(false);
  const [err, setErr]           = React.useState(null);
  const frontRef = React.useRef(null);
  const backRef  = React.useRef(null);

  // Fetch rules once.
  React.useEffect(() => {
    fetch(`${apiBase}/api/id-rules`)
      .then(r => r.json())
      .then(d => setRules(d.rules))
      .catch(() => { /* server old? rules will be null, UI uses safe defaults */ });
  }, [apiBase]);

  if (verified) {
    return (
      <div className="card" style={{
        padding: 14, marginBottom: 18, display: 'flex', alignItems: 'center', gap: 12,
        background: '#e7f0ea', borderColor: '#9fbfa8',
      }}>
        <div style={{
          width: 28, height: 28, borderRadius: '50%', background: 'var(--positive)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <Icon name="check" size={14} color="#fff" />
        </div>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--ink)' }}>Identity verified</div>
          <div style={{ fontSize: 11, color: 'var(--ink-2)' }}>
            Self-attested. Details printed on the certificate.
          </div>
        </div>
      </div>
    );
  }

  // Resolve the active rule for the chosen country/type. Falls back to the
  // doctype default when the country isn't in our explicit list.
  const rule = (() => {
    const fam = rules?.[docType];
    if (!fam) return null;
    return fam[country] || fam.DEFAULT || null;
  })();
  const requiresBack    = !!rule?.has_back && docType !== 'passport';
  const docNumberRegex  = rule?.doc_number_pattern ? new RegExp('^' + rule.doc_number_pattern + '$', 'i') : null;
  const docNumberLooksOk = !docNumberRegex || (docNum && docNumberRegex.test(docNum));

  // Handle a file pick: store the file, then attempt OCR + MRZ parse.
  const handleImage = async (file, side) => {
    if (!file) return;
    if (side === 'front') setFront(file); else setBack(file);
    setOcrMsg(null);
    setOcrBusy(side);
    try {
      let progressShown = false;
      const result = await ocrIdImage(file, (m) => {
        if (m?.status === 'recognizing text') {
          progressShown = true;
          setOcrMsg(`Reading ${side}… ${Math.round((m.progress || 0) * 100)}%`);
        } else if (!progressShown && m?.status) {
          setOcrMsg(`Loading scanner: ${m.status}…`);
        }
      });
      setOcrConf(prev => (prev != null ? Math.max(prev, result.confidence) : result.confidence));
      if (result.mrz) {
        setMrz(result.mrz);
        // Auto-fill from MRZ — the most reliable source we have.
        if (result.mrz.full_name && !fullName) setFullName(result.mrz.full_name);
        if (result.mrz.birth_date && result.mrz.birth_date_ok) setBirth(result.mrz.birth_date);
        if (result.mrz.doc_number && result.mrz.doc_number_ok) setDocNum(result.mrz.doc_number);
        // Map MRZ country code to alpha-2 for the picker.
        const c2 = (result.mrz.country || '').slice(0, 2);
        if (c2 && ID_COUNTRIES.some(x => x.code === c2)) setCountry(c2);
        setOcrMsg('MRZ recognised — fields pre-filled. Please double-check.');
      } else {
        setOcrMsg('No MRZ detected on this side. You can still fill the fields manually below.');
      }
    } catch (ex) {
      setOcrMsg('Couldn\'t run automatic scanner. Please fill the fields manually.');
    } finally {
      setOcrBusy(null);
    }
  };

  // Cross-check user input against MRZ if we found one. With a Personalausweis,
  // passport, or any other MRZ-bearing document we treat the MRZ as the
  // authoritative source — typed fields that disagree with it are rejected.
  const crossCheck = React.useMemo(
    () => crossCheckMrz(mrz, { docNum, birth, fullName }),
    [mrz, docNum, birth, fullName]
  );

  // Hard requirement: when the issuing rules say a document is MRZ-readable
  // (modern EU IDs, all passports), we refuse to verify until OCR could
  // actually read the MRZ. Otherwise the form would just rubber-stamp
  // anything the user types — exactly the failure mode we just fixed.
  const mrzGateOpen = !!rule?.has_mrz && !mrz;

  const valid =
    front &&
    (!requiresBack || back) &&
    fullName.trim().length >= 2 &&
    /^\d{4}-\d{2}-\d{2}$/.test(birth) &&
    address.trim().length >= 4 &&
    docNumberLooksOk &&
    !mrzGateOpen &&
    crossCheck.ok;

  const submit = async (e) => {
    e.preventDefault();
    if (!valid || busy || disabled) return;
    setBusy(true); setErr(null);
    try {
      const fd = new FormData();
      fd.append('id_image_front', front);
      if (back) fd.append('id_image_back', back);
      fd.append('doc_type', docType);
      if (docType !== 'passport' || country !== 'OTHER') fd.append('country', country);
      fd.append('full_name', fullName.trim());
      fd.append('birth_date', birth);
      fd.append('address', address.trim());
      fd.append('doc_number', docNum.trim());
      if (mrz?.raw) fd.append('mrz_text', mrz.raw);
      if (ocrConf != null) fd.append('ocr_confidence', String(ocrConf));
      const res = await fetch(`${apiBase}/api/sign/${token}/id-verify`, {
        method: 'POST', body: fd, credentials: apiBase ? 'include' : 'same-origin',
      });
      const out = await res.json().catch(() => ({}));
      if (!res.ok) {
        throw Object.assign(new Error(out.error || 'verify_failed'), { hint: out.hint });
      }
      onVerified?.(out);
    } catch (ex) {
      setErr(
        ex.message === 'invalid_birth_date'    ? 'Date of birth must be YYYY-MM-DD.' :
        ex.message === 'image_too_large'       ? 'ID image larger than 8 MB.' :
        ex.message === 'unsupported_image_type'? 'Use a JPG, PNG or WebP image.' :
        ex.message === 'invalid_doc_type'      ? 'Pick a document type.' :
        ex.message === 'invalid_country'       ? 'Pick a country for the document.' :
        ex.message === 'doc_number_format_mismatch'
          ? `ID number doesn't match the expected format. ${ex.hint || ''}` :
        ex.message === 'missing_back'          ? 'Please upload the back side of the document.' :
        ex.message === 'missing_front'         ? 'Please upload the front side of the document.' :
                                                  'Identity verification failed. Please check the fields and try again.'
      );
    } finally { setBusy(false); }
  };

  const fileBtn = (file, side, ref, busyHere) => (
    <div>
      <input ref={ref} type="file" accept="image/jpeg,image/png,image/webp"
             onChange={e => handleImage(e.target.files?.[0] || null, side)}
             style={{ display: 'none' }} />
      <button type="button" onClick={() => ref.current?.click()} disabled={busy || ocrBusy === side}
              className="btn btn-ghost" style={{ width: '100%', justifyContent: 'flex-start', gap: 8 }}>
        <Icon name="upload" size={11} />
        <span style={{ flex: 1, textAlign: 'left', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
          {busyHere ? 'Scanning…' : (file ? file.name : `Upload ${side === 'front' ? 'front' : 'back'} side`)}
        </span>
        {file && !busyHere && <Icon name="check" size={10} color="var(--positive)" />}
      </button>
      {file && !busyHere && (
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 3 }}>
          {(file.size / 1024).toFixed(0)} KB · {file.type.split('/')[1]}
        </div>
      )}
    </div>
  );

  return (
    <div className="card" style={{ padding: 16, marginBottom: 18 }}>
      <div className="eyebrow" style={{ marginBottom: 6 }}>Step 1 of 2 · required</div>
      <h2 className="display" style={{ fontSize: 18, margin: 0, marginBottom: 4 }}>Confirm your identity</h2>
      <p style={{ fontSize: 11, color: 'var(--ink-3)', margin: 0, marginBottom: 14, lineHeight: 1.5 }}>
        Upload your ID. We try to read it with on-device OCR and pre-fill the fields. Self-attested — no third-party KYC.
      </p>

      <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
          <div className="field" style={{ marginBottom: 0 }}>
            <label>Document type</label>
            <select value={docType} onChange={e => setDocType(e.target.value)} disabled={busy}>
              <option value="id_card">National ID card</option>
              <option value="passport">Passport</option>
              <option value="drivers_license">Driver's licence</option>
            </select>
          </div>
          <div className="field" style={{ marginBottom: 0 }}>
            <label>Issuing country</label>
            <select value={country} onChange={e => setCountry(e.target.value)} disabled={busy}>
              {ID_COUNTRIES.map(c => <option key={c.code} value={c.code}>{c.name}</option>)}
            </select>
          </div>
        </div>

        {rule && (
          <div style={{ fontSize: 10, color: 'var(--ink-3)', lineHeight: 1.5,
                        padding: '6px 10px', background: 'var(--paper-2)',
                        border: '1px solid var(--line-2)', borderRadius: 2 }}>
            {rule.label}{rule.has_mrz ? ' · MRZ-readable' : ''}{requiresBack ? ' · front + back required' : ' · front only'}
          </div>
        )}

        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {fileBtn(front, 'front', frontRef, ocrBusy === 'front')}
          {requiresBack && fileBtn(back, 'back', backRef, ocrBusy === 'back')}
        </div>

        {ocrMsg && (
          <div style={{ fontSize: 11, color: mrz ? 'var(--positive)' : 'var(--ink-3)',
                        background: mrz ? '#e7f0ea' : 'var(--paper-2)',
                        border: '1px solid ' + (mrz ? '#9fbfa8' : 'var(--line-2)'),
                        padding: '8px 10px', borderRadius: 2, lineHeight: 1.5 }}>
            {ocrMsg}
            {mrz && (
              <div className="mono" style={{ fontSize: 10, marginTop: 4, color: 'var(--ink-2)', wordBreak: 'break-all' }}>
                MRZ {mrz.format} · {mrz.country} · doc {mrz.doc_number_ok ? '✓' : '✗'} · DOB {mrz.birth_date_ok ? '✓' : '✗'}
              </div>
            )}
          </div>
        )}
        {/* MRZ gate — block submit when the issuing rules require an MRZ but
            we couldn't read one. Without this the form would happily accept
            any typed-in data, which is exactly the bug a user just hit. */}
        {mrzGateOpen && front && ocrBusy === null && (
          <div style={{ fontSize: 11, color: 'var(--danger)', background: '#fbeae6',
                        border: '1px solid #e8b8a8', padding: '10px 12px',
                        borderRadius: 2, lineHeight: 1.5 }}>
            <div style={{ fontWeight: 500, marginBottom: 3 }}>MRZ couldn't be read.</div>
            {rule?.has_back
              ? <>This document type carries a machine-readable zone on the back side. Please re-take the back-side photo with better light, no glare, and the whole MRZ inside the frame. Without a readable MRZ we can't verify the ID.</>
              : <>This document carries a machine-readable zone. Please re-take the photo with better light and the whole MRZ visible.</>}
          </div>
        )}
        {/* Cross-check failure — the typed fields don't match what the MRZ
            actually says. Show the conflicts inline; block submit until fixed. */}
        {!crossCheck.ok && mrz && (
          <div style={{ fontSize: 11, color: 'var(--danger)', background: '#fbeae6',
                        border: '1px solid #e8b8a8', padding: '10px 12px',
                        borderRadius: 2, lineHeight: 1.5 }}>
            <div style={{ fontWeight: 500, marginBottom: 4 }}>
              The fields you typed don't match the document.
            </div>
            {crossCheck.mismatches.map((m, i) => (
              <div key={i} className="mono" style={{ fontSize: 10, marginTop: 2 }}>
                {m.field === 'doc_number' ? 'ID number' :
                 m.field === 'birth_date' ? 'Date of birth' : 'Name'}:
                {' '}MRZ says <strong>{m.mrz || '—'}</strong>, you typed <strong>{m.typed || '—'}</strong>
              </div>
            ))}
            <div style={{ fontSize: 10, marginTop: 6, color: 'var(--ink-2)' }}>
              Update the fields to match the document, or upload a clearer photo so the MRZ reads correctly.
            </div>
          </div>
        )}

        <div className="field" style={{ marginBottom: 0 }}>
          <label>Full name (as printed on ID)</label>
          <input value={fullName} onChange={e => setFullName(e.target.value)} disabled={busy} required />
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
          <div className="field" style={{ marginBottom: 0 }}>
            <label>Date of birth</label>
            <input type="date" value={birth} onChange={e => setBirth(e.target.value)} disabled={busy} required />
          </div>
          <div className="field" style={{ marginBottom: 0 }}>
            <label>ID number</label>
            <input value={docNum} onChange={e => setDocNum(e.target.value)} disabled={busy} required
                   placeholder={rule?.doc_number_pattern ? 'Format will be checked' : 'As printed'}
                   className="mono"
                   style={{ borderColor: docNum && !docNumberLooksOk ? 'var(--danger)' : undefined }} />
            {docNum && !docNumberLooksOk && (
              <div style={{ fontSize: 10, color: 'var(--danger)', marginTop: 2 }}>
                Doesn't match the expected format for {rule?.label || 'this document'}.
              </div>
            )}
          </div>
        </div>
        <div className="field" style={{ marginBottom: 0 }}>
          <label>Address</label>
          <textarea value={address} onChange={e => setAddress(e.target.value)} disabled={busy} required
                    rows={2} placeholder="Street, postcode, city, country" />
        </div>

        {err && (
          <div style={{ fontSize: 11, color: 'var(--danger)', background: '#fbeae6',
                        padding: '8px 10px', borderRadius: 2, border: '1px solid #e8b8a8' }}>
            {err}
          </div>
        )}
        <button type="submit" disabled={!valid || busy || disabled} className="btn"
                style={{ width: '100%', justifyContent: 'center', opacity: (!valid || busy) ? 0.5 : 1 }}>
          {busy ? 'Submitting…' : <>Verify identity <Icon name="arrow" size={11} color="var(--paper)" /></>}
        </button>
        <div style={{ fontSize: 10, color: 'var(--ink-3)', lineHeight: 1.5 }}>
          Only the last 4 characters of the ID number are persisted in the database. Your photos and the rest of your details go on the certificate that the sender (and you) get. Public verification only shows the country and document type, never your name, date of birth or address.
        </div>
      </form>
    </div>
  );
};

// Modal shown when the browser cannot resolve a precise location (permission
// denied, OS-level location off, or timeout). The signer still continues
// without precise location — only the country from CF headers is recorded.
const GeolocationHelpModal = ({ onChoice, retried = false }) => {
  const [tab, setTab] = React.useState('win11');
  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(26,24,20,0.45)', zIndex: 1000,
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
    }}>
      <div className="card" style={{ width: 'min(560px, 100%)', maxHeight: '90vh', overflow: 'auto', padding: 0 }}>
        <div style={{ padding: '18px 22px', borderBottom: '1px solid var(--line)',
                      display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{
            width: 32, height: 32, borderRadius: '50%',
            background: retried ? '#fbeae6' : '#fbf4e6',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Icon name={retried ? 'x' : 'map'} size={16} color={retried ? 'var(--danger)' : '#a86b1a'} />
          </div>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 14, fontWeight: 500 }}>
              {retried ? 'Still no location' : 'Location services are off'}
            </div>
            <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>
              {retried
                ? 'The browser still can\'t read a precise location. Settings sometimes only take effect after a page reload.'
                : 'Your browser couldn\'t read a precise location.'}
            </div>
          </div>
        </div>
        <div style={{ padding: '18px 22px', fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.6 }}>
          You can still sign — we'll fall back to the country derived from your IP. If you'd like a precise location on the certificate, here's how to enable location services on Windows:

          <div style={{ display: 'flex', gap: 0, marginTop: 18, marginBottom: 14,
                        border: '1px solid var(--line)', borderRadius: 2, padding: 2 }}>
            {[{ id: 'win11', label: 'Windows 11' }, { id: 'win10', label: 'Windows 10' }].map(o => (
              <button key={o.id} onClick={() => setTab(o.id)} type="button"
                style={{ flex: 1, border: 0, padding: '8px 6px',
                  background: tab === o.id ? 'var(--ink)' : 'transparent',
                  color: tab === o.id ? 'var(--paper)' : 'var(--ink-2)',
                  fontFamily: 'var(--sans)', fontSize: 11, fontWeight: 500,
                  cursor: 'pointer', borderRadius: 1 }}>
                {o.label}
              </button>
            ))}
          </div>

          {tab === 'win11' && (
            <ol style={{ paddingLeft: 18, margin: 0, fontSize: 12, color: 'var(--ink-2)' }}>
              <li style={{ marginBottom: 6 }}>Press <strong>Windows&nbsp;+&nbsp;I</strong> to open <strong>Settings</strong>.</li>
              <li style={{ marginBottom: 6 }}>Go to <strong>Privacy &amp; security → Location</strong>.</li>
              <li style={{ marginBottom: 6 }}>Switch <strong>Location services</strong> to <strong>On</strong>.</li>
              <li style={{ marginBottom: 6 }}>Scroll down and switch <strong>Let apps access your location</strong> to <strong>On</strong>.</li>
              <li style={{ marginBottom: 6 }}>Find your browser in the list below and switch it to <strong>On</strong>.</li>
              <li>Reload this page and click the location icon in the address bar to allow access.</li>
            </ol>
          )}
          {tab === 'win10' && (
            <ol style={{ paddingLeft: 18, margin: 0, fontSize: 12, color: 'var(--ink-2)' }}>
              <li style={{ marginBottom: 6 }}>Press <strong>Windows&nbsp;+&nbsp;I</strong> to open <strong>Settings</strong>.</li>
              <li style={{ marginBottom: 6 }}>Go to <strong>Privacy → Location</strong>.</li>
              <li style={{ marginBottom: 6 }}>Click <strong>Change</strong> and switch <strong>Location for this device</strong> to <strong>On</strong>.</li>
              <li style={{ marginBottom: 6 }}>Switch <strong>Allow apps to access your location</strong> to <strong>On</strong>.</li>
              <li style={{ marginBottom: 6 }}>Find your browser in the apps list and switch it to <strong>On</strong>.</li>
              <li>Reload this page and accept the browser's location prompt when it appears.</li>
            </ol>
          )}

          <div style={{ marginTop: 16, padding: '10px 12px', background: 'var(--paper-2)',
                        border: '1px solid var(--line-2)', fontSize: 11, color: 'var(--ink-3)', borderRadius: 2 }}>
            Tip: if the browser already blocked Saign, click the lock icon next to the URL → Site settings → Location → Allow, then retry.
          </div>
        </div>
        <div style={{ padding: '14px 22px', borderTop: '1px solid var(--line)',
                      display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
          <button type="button" onClick={() => onChoice('cancel')} className="btn btn-ghost">
            Cancel signing
          </button>
          <button type="button" onClick={() => onChoice('retry')} className="btn btn-ghost">
            I enabled it — retry
          </button>
          <button type="button" onClick={() => onChoice('continue')} className="btn">
            Sign without precise location
          </button>
        </div>
      </div>
    </div>
  );
};

const PublicSignPage = ({ token }) => {
  const [data, setData] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [method, setMethod] = React.useState('draw');
  const [accepted, setAccepted] = React.useState(true);
  const [shareLoc, setShareLoc] = React.useState(true);
  const [busy, setBusy] = React.useState(false);
  const [signed, setSigned] = React.useState(false);
  const [drawnSig, setDrawnSig] = React.useState(null);     // dataURL from canvas
  const [typedName, setTypedName] = React.useState('');
  const [progress, setProgress] = React.useState('');
  const [idVerified, setIdVerified] = React.useState(false);
  const [geoHelp, setGeoHelp] = React.useState(null);       // null | (choice => void)
  const apiBase = (typeof window !== 'undefined' && window.SAIGN_API) || '';
  const fileUrl = `${apiBase}/api/sign/${token}/file`;

  React.useEffect(() => {
    api(`/api/sign/${token}`).then(d => {
      setData(d);
      if (d.signer.full_name) setTypedName(d.signer.full_name);
      if (d.signer.status === 'signed') setSigned(true);
      if (d.signer.id_verified_at) setIdVerified(true);
    }).catch(setError);
  }, [token]);

  if (error) return <ErrorTokenPage />;
  if (!data) return <FullPageSpinner />;

  const currentSig = method === 'type' ? (typedName ? typedSignatureToPng(typedName) : null) : drawnSig;
  const idGateOpen = !!data?.document?.require_id_verification && !idVerified;
  const canSubmit = !!currentSig && accepted && !signed && !busy && !idGateOpen;

  const apply = async () => {
    if (!canSubmit) return;
    setBusy(true);

    // Capture geolocation up-front (best-effort — never blocks signing).
    let geo = null;
    if (shareLoc) {
      setProgress('Confirming location…');
      geo = await getBrowserLocation();
      // If the browser denies / OS location is off / it times out, keep
      // re-prompting with the help modal until the user explicitly chooses
      // to continue without precise location, or cancels signing.
      let retried = false;
      while (!geo) {
        setProgress('');
        const choice = await new Promise(res => setGeoHelp({ onChoice: res, retried }));
        setGeoHelp(null);
        if (choice === 'cancel')   { setBusy(false); return; }
        if (choice === 'continue') break;
        // 'retry' — try once more; loop re-shows modal if still nothing.
        retried = true;
        setProgress('Retrying location…');
        geo = await getBrowserLocation();
      }
      if (geo) {
        setProgress('Resolving location…');
        const label = await reverseGeocode(geo.lat, geo.lon);
        if (label) geo.label = label;
      }
    }

    setProgress('Loading PDF…');
    try {
      // 1. fetch the original PDF
      const pdfRes = await fetch(fileUrl, { credentials: apiBase ? 'include' : 'same-origin' });
      if (!pdfRes.ok) throw new Error('Could not load original PDF');
      const pdfBytes = new Uint8Array(await pdfRes.arrayBuffer());

      // 2. embed the signature using pdf-lib — at every assigned field, or
      //    fall back to a single box on the last page if no fields exist.
      setProgress('Embedding signature…');
      const PDFLib = window.PDFLib;
      if (!PDFLib) throw new Error('pdf-lib not loaded');
      const pdfDoc = await PDFLib.PDFDocument.load(pdfBytes);
      const png = await pdfDoc.embedPng(dataUrlToBytes(currentSig));
      const pages = pdfDoc.getPages();
      const ts = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
      const locSuffix = geo?.label ? ` · ${geo.label}` : '';

      const drawSigAt = (page, x, y, w, h) => {
        const aspect = png.height / png.width;
        let dw = w, dh = aspect * dw;
        if (dh > h) { dh = h; dw = dh / aspect; }
        page.drawRectangle({ x, y, width: w, height: h,
          color: PDFLib.rgb(1, 1, 1), borderColor: PDFLib.rgb(0.85, 0.83, 0.78),
          borderWidth: 0.5, opacity: 0.95 });
        page.drawImage(png, { x: x + (w - dw) / 2, y: y + (h - dh) / 2 + 3, width: dw, height: dh });
        page.drawText(`${data.signer.full_name} · ${ts}${locSuffix}`, {
          x: x + 2, y: y + 1, size: 5.5, color: PDFLib.rgb(0.4, 0.34, 0.26),
        });
      };

      const fields = data.fields || [];
      if (fields.length > 0) {
        for (const f of fields) {
          const page = pages[f.page - 1];
          if (!page) continue;
          const { width, height } = page.getSize();
          // y_pct is from top in our coords; pdf-lib y is from bottom
          const x = f.x_pct * width;
          const w = f.w_pct * width;
          const h = f.h_pct * height;
          const y = height - (f.y_pct * height) - h;
          drawSigAt(page, x, y, w, h);
        }
      } else {
        // Fallback: bottom-right of last page
        const last = pages[pages.length - 1];
        const { width } = last.getSize();
        const w = Math.min(180, width * 0.45);
        const h = (png.height / png.width) * w + 20;
        drawSigAt(last, width - w - 48, 64, w, h);
      }

      const signedBytes = await pdfDoc.save();

      // 3. POST to backend
      setProgress('Uploading signed copy…');
      const fd = new FormData();
      fd.append('signed_pdf', new Blob([signedBytes], { type: 'application/pdf' }), `${data.document.code}-signed.pdf`);
      fd.append('method', method);
      fd.append('signature_image', currentSig);
      if (geo) {
        fd.append('geo_lat', String(geo.lat));
        fd.append('geo_lon', String(geo.lon));
        if (geo.accuracy != null) fd.append('geo_accuracy', String(Math.round(geo.accuracy)));
        if (geo.label) fd.append('geo_label', geo.label);
      }
      const res = await fetch(`${apiBase}/api/sign/${token}/apply`, {
        method: 'POST', body: fd, credentials: apiBase ? 'include' : 'same-origin',
      });
      const out = await res.json();
      if (!res.ok) throw new Error(out.error || 'apply_failed');

      setSigned(true); setProgress('');
      setTimeout(() => navigate(`/sign/${token}/done`), 600);
    } catch (ex) {
      setError({ message: ex.message });
    } finally { setBusy(false); }
  };

  return (
    <div className="saign-app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <div style={{ height: 52, borderBottom: '1px solid var(--line)', background: 'var(--paper)',
                    display: 'flex', alignItems: 'center', padding: '0 24px', gap: 16, flexShrink: 0 }}>
        <Logo size={14} />
        <div style={{ width: 1, height: 18, background: 'var(--line)' }} />
        <div style={{ fontSize: 12, color: 'var(--ink-2)' }}>
          <strong style={{ color: 'var(--ink)', fontWeight: 500 }}>{data.sender.name}</strong> sent you{' '}
          <strong style={{ fontWeight: 500 }}>{data.document.title}</strong>
        </div>
        <div style={{ flex: 1 }} />
        <a href={fileUrl} target="_blank" rel="noopener" className="btn btn-ghost btn-sm" style={{ textDecoration: 'none' }}>
          <Icon name="download" size={11} /> Download
        </a>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 24px',
                    background: '#fbf4e6', borderBottom: '1px solid #e8d4b0', fontSize: 12, flexShrink: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#8a5a18' }}>
          <Icon name="pen" size={12} color="#a86b1a" />
          {(data.fields?.length > 0)
            ? `${data.fields.length} signature${data.fields.length > 1 ? 's' : ''} required`
            : '1 signature required · last page'}
        </div>
        <div className="mono" style={{ color: '#a86b1a' }}>{data.document.code}</div>
      </div>
      <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '1fr 380px', overflow: 'hidden' }}>
        <div style={{ background: 'var(--paper-2)', minHeight: 0 }}>
          <PdfViewer url={fileUrl} withCredentials={!!apiBase}
            renderOverlay={(pageNum) => (data.fields || [])
              .filter(f => f.page === pageNum)
              .map(f => (
                <div key={f.id} className="sigfield" data-signed={signed} data-mine={!signed}
                  style={{
                    left: `${f.x_pct * 100}%`, top: `${f.y_pct * 100}%`,
                    width: `${f.w_pct * 100}%`, height: `${f.h_pct * 100}%`,
                    cursor: 'default',
                  }}>
                  {signed ? <Icon name="check" size={11} color="var(--positive)" /> : 'Sign here'}
                </div>
              ))} />
        </div>
        <div style={{ borderLeft: '1px solid var(--line)', padding: 24, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
          {data.document.require_id_verification && (
            <IdVerificationCard
              token={token}
              apiBase={apiBase}
              initialName={data.signer.full_name}
              verified={idVerified}
              disabled={signed}
              onVerified={() => setIdVerified(true)}
            />
          )}
          <div style={{ marginBottom: 18 }}>
            <div className="eyebrow" style={{ marginBottom: 6 }}>
              {data.document.require_id_verification ? 'Step 2 of 2' : 'Step 1 of 1'}
            </div>
            <h2 className="display" style={{ fontSize: 20, margin: 0, marginBottom: 4 }}>Add your signature</h2>
            <p style={{ fontSize: 12, color: 'var(--ink-3)', margin: 0 }}>
              {idGateOpen
                ? <>First confirm your identity above, then choose how you'd like to sign.</>
                : <>Hi {data.signer.full_name}. Choose how you'd like to sign.</>}
            </p>
          </div>
          <div style={{ display: 'flex', gap: 0, marginBottom: 16, border: '1px solid var(--line)', borderRadius: 2, padding: 2 }}>
            {[{ id: 'draw', label: 'Draw', icon: 'draw' },
              { id: 'type', label: 'Type', icon: 'type' }].map(o => (
              <button key={o.id} onClick={() => setMethod(o.id)} disabled={signed} type="button"
                style={{ flex: 1, border: 0, padding: '8px 6px',
                  background: method === o.id ? 'var(--ink)' : 'transparent',
                  color: method === o.id ? 'var(--paper)' : 'var(--ink-2)',
                  fontFamily: 'var(--sans)', fontSize: 11, fontWeight: 500,
                  cursor: signed ? 'default' : 'pointer', display: 'flex', alignItems: 'center',
                  justifyContent: 'center', gap: 6, borderRadius: 1 }}>
                <Icon name={o.icon} size={11} color="currentColor" /> {o.label}
              </button>
            ))}
          </div>

          {!signed && method === 'draw' && (
            <SignatureCanvas onChange={setDrawnSig} />
          )}
          {!signed && method === 'type' && (
            <div style={{ background: '#fff', border: '1px solid var(--line)', borderRadius: 2, padding: '24px 16px',
                          minHeight: 100, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <input value={typedName} onChange={e => setTypedName(e.target.value)}
                style={{ background: 'transparent', border: 'none', outline: 'none', fontFamily: '"Caveat", cursive',
                         fontSize: 44, color: 'var(--accent)', textAlign: 'center', width: '100%' }} />
            </div>
          )}
          {signed && (
            <div style={{ background: 'var(--paper-2)', border: '1px solid var(--line)', borderRadius: 2, padding: 24,
                          textAlign: 'center' }}>
              <Icon name="check" size={20} color="var(--positive)" />
              <div style={{ fontSize: 12, color: 'var(--positive)', marginTop: 6 }}>Signature applied</div>
            </div>
          )}

          <div style={{ fontSize: 11, color: 'var(--ink-3)', display: 'flex', gap: 8, alignItems: 'flex-start', margin: '18px 0 10px' }}>
            <input type="checkbox" className="chk" checked={accepted} onChange={e => setAccepted(e.target.checked)} />
            <span>By signing, I agree this constitutes my electronic signature under eIDAS (SES). Audit trail will record IP, device, and SHA-256 of the signed PDF.</span>
          </div>
          <div style={{ fontSize: 11, color: 'var(--ink-3)', display: 'flex', gap: 8, alignItems: 'flex-start', margin: '0 0 14px' }}>
            <input type="checkbox" className="chk" checked={shareLoc} onChange={e => setShareLoc(e.target.checked)} />
            <span>Include my location on the certificate (optional). Your browser may ask for permission. If denied, only your country is recorded.</span>
          </div>
          {progress && (
            <div className="mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 10 }}>{progress}</div>
          )}
          <div style={{ flex: 1 }} />
          {idGateOpen && (
            <div style={{
              padding: '10px 12px', background: '#fbf4e6', border: '1px solid #e8d4b0',
              fontSize: 11, color: '#8a5a18', borderRadius: 2, marginBottom: 10,
            }}>
              Identity verification is required before you can sign this document.
            </div>
          )}
          <button onClick={apply} disabled={!canSubmit} className="btn btn-lg" type="button"
                  style={{ width: '100%', justifyContent: 'center', opacity: canSubmit ? 1 : 0.5 }}>
            {signed ? 'Done' : busy ? (progress || 'Submitting…') : <>Apply signature <Icon name="arrow" size={11} color="var(--paper)" /></>}
          </button>
        </div>
        {geoHelp && <GeolocationHelpModal onChoice={geoHelp.onChoice} retried={geoHelp.retried} />}
      </div>
    </div>
  );
};

const SignedReceiptPage = ({ token }) => {
  const [data, setData] = React.useState(null);
  const [certBusy, setCertBusy] = React.useState(false);
  React.useEffect(() => { api(`/api/sign/${token}`).then(setData).catch(() => {}); }, [token]);

  const downloadCert = async () => {
    if (!data) return;
    setCertBusy(true);
    try {
      const a = await api(`/api/sign/${token}/audit`);
      const bytes = await buildCertificatePdf({
        doc: a.document, signers: a.signers, events: a.events,
        generatedFor: data.signer.email,
      });
      downloadBlob(bytes, `${a.document.code || a.document.id}-certificate.pdf`);
    } catch {}
    finally { setCertBusy(false); }
  };

  if (!data) return <FullPageSpinner />;
  return (
    <div className="saign-app" style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32, background: 'var(--paper)' }}>
      <div style={{ width: 480, textAlign: 'center' }}>
        <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 28 }}><Logo size={16} /></div>
        <div style={{ width: 64, height: 64, margin: '0 auto 22px', borderRadius: '50%',
          background: 'var(--paper-2)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid var(--line)' }}>
          <Icon name="check" size={26} color="var(--positive)" />
        </div>
        <h2 className="display" style={{ fontSize: 28, margin: 0, marginBottom: 10 }}>You signed.</h2>
        <p style={{ fontSize: 13, color: 'var(--ink-2)', margin: 0, marginBottom: 28, lineHeight: 1.5 }}>
          Your signature was applied to <strong style={{ color: 'var(--ink)' }}>{data.document.title}</strong>.<br/>
          A copy will be emailed once everyone has signed.
        </p>
        <div className="card" style={{ padding: 16, marginBottom: 22, textAlign: 'left' }}>
          <div className="eyebrow" style={{ marginBottom: 10 }}>Receipt</div>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 6 }}>
            <span style={{ color: 'var(--ink-3)' }}>Document</span><span>{data.document.code}</span>
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 6 }}>
            <span style={{ color: 'var(--ink-3)' }}>Signed at</span><span className="mono">{data.signer.signed_at || 'just now'}</span>
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 6 }}>
            <span style={{ color: 'var(--ink-3)' }}>Signer</span><span>{data.signer.full_name}</span>
          </div>
          {data.document.verify_code && (
            <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 6 }}>
              <span style={{ color: 'var(--ink-3)' }}>Verify code</span>
              <Link to={`/verify?code=${data.document.verify_code}`} className="mono btn-link">{data.document.verify_code}</Link>
            </div>
          )}
          {data.signer.signed_geo_label && (
            <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 6 }}>
              <span style={{ color: 'var(--ink-3)' }}>Location</span>
              <span>{data.signer.signed_geo_label}{' '}
                <span style={{ color: 'var(--ink-3)', fontSize: 10 }}>
                  ({data.signer.signed_geo_source === 'browser' ? 'precise' : 'ip'})
                </span>
              </span>
            </div>
          )}
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
            <span style={{ color: 'var(--ink-3)' }}>SHA‑256</span><span className="mono">{data.document.sha256}</span>
          </div>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          <a href={`${(window.SAIGN_API||'')}/api/sign/${token}/file`} target="_blank" rel="noopener"
             className="btn" style={{ textDecoration: 'none', justifyContent: 'center' }}>
            <Icon name="download" size={11} color="var(--paper)" /> Download signed PDF
          </a>
          <button onClick={downloadCert} disabled={certBusy} type="button"
                  className="btn btn-ghost" style={{ justifyContent: 'center', opacity: certBusy ? 0.5 : 1 }}>
            <Icon name="shield" size={11} /> {certBusy ? 'Building…' : 'Download signing certificate'}
          </button>
        </div>
        <div style={{ marginTop: 14 }}>
          <Link to="/" className="btn-link" style={{ fontSize: 12 }}>Back to home</Link>
        </div>
      </div>
    </div>
  );
};

// ---------- Audit ----------
const AuditPage = ({ id }) => {
  const [doc, setDoc] = React.useState(null);
  const [events, setEvents] = React.useState([]);
  const [error, setError] = React.useState(null);
  const [certBusy, setCertBusy] = React.useState(false);
  const { user } = useAuth();
  const toast = useToast();
  React.useEffect(() => {
    Promise.all([
      api(`/api/documents/${id}`),
      api(`/api/documents/${id}/audit`),
    ]).then(([d, a]) => { setDoc(d); setEvents(a.events); }).catch(setError);
  }, [id]);
  const downloadCert = async () => {
    if (!doc) return;
    setCertBusy(true);
    try {
      const bytes = await buildCertificatePdf({
        doc, signers: doc.signers, events,
        generatedFor: user?.email || '',
      });
      downloadBlob(bytes, `${doc.code || doc.id}-certificate.pdf`);
      toast.push('Certificate downloaded');
    } catch (ex) {
      toast.push('Certificate failed: ' + ex.message, 'err');
    } finally { setCertBusy(false); }
  };
  if (error) return <NotFoundPage />;
  if (!doc) return <AppShell active="all" title="Audit"><div style={{ padding: 24, color: 'var(--ink-3)' }}>Loading…</div></AppShell>;
  const actionIcon  = { signed: 'check', viewed: 'eye', sent: 'send', created: 'upload', verified: 'shield', declined: 'x', completed: 'check' };
  const actionColor = { signed: 'var(--positive)', completed: 'var(--positive)', declined: 'var(--danger)', sent: 'var(--accent-2)', created: 'var(--ink-3)', viewed: 'var(--ink-3)', verified: 'var(--ink-2)' };
  return (
    <AppShell active="all" title="Audit trail">
      <div style={{ padding: '28px 48px', maxWidth: 980 }}>
        <Link to={`/documents/${doc.id}`} className="btn-link" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginBottom: 18 }}>
          <Icon name="arrowL" size={11} /> Back to document
        </Link>
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 28 }}>
          <div>
            <div className="eyebrow" style={{ marginBottom: 6 }}>Audit trail</div>
            <h2 className="display" style={{ fontSize: 24, margin: 0, marginBottom: 4 }}>{doc.title}</h2>
            <div className="mono" style={{ color: 'var(--ink-3)', fontSize: 11 }}>{doc.id} · SHA‑256 {doc.sha256}</div>
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <StatusBadge state={doc.status === 'draft' ? 'draft' : doc.status === 'signed' ? 'signed' : doc.status === 'declined' ? 'declined' : 'sent'} />
            <button className="btn btn-ghost btn-sm" onClick={downloadCert} disabled={certBusy}
                    style={{ opacity: certBusy ? 0.5 : 1 }}>
              <Icon name="download" size={11} /> {certBusy ? 'Building…' : 'Export certificate'}
            </button>
          </div>
        </div>
        <div className="card" style={{ padding: 0 }}>
          <div style={{ padding: '14px 20px', borderBottom: '1px solid var(--line-2)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <div style={{ fontWeight: 500, fontSize: 13 }}>{events.length} events</div>
            <div className="mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>tamper‑evident · sha256 hash chain</div>
          </div>
          <div style={{ padding: '8px 0' }}>
            {events.map((e, i) => (
              <div key={e.id} style={{ display: 'grid', gridTemplateColumns: '32px 1fr 240px', gap: 16, padding: '12px 20px',
                                       borderBottom: i < events.length - 1 ? '1px solid var(--line-2)' : 'none', alignItems: 'flex-start' }}>
                <div style={{ width: 24, height: 24, borderRadius: '50%', background: 'var(--paper-2)',
                              display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid var(--line)' }}>
                  <Icon name={actionIcon[e.action] || 'activity'} size={11} color={actionColor[e.action] || 'var(--ink-3)'} />
                </div>
                <div>
                  <div style={{ fontSize: 12, color: 'var(--ink)', marginBottom: 2 }}>
                    <strong style={{ fontWeight: 500 }}>{e.actor_email || 'system'}</strong>
                    <span style={{ color: 'var(--ink-2)' }}> {e.action}</span>
                    {e.detail && <span style={{ color: 'var(--ink-2)' }}> — {e.detail}</span>}
                  </div>
                  <div className="mono" style={{ fontSize: 9, color: 'var(--ink-3)', wordBreak: 'break-all' }}>
                    hash {e.row_hash?.slice(0, 24)}…
                  </div>
                </div>
                <div style={{ textAlign: 'right' }}>
                  <div className="mono" style={{ fontSize: 10, color: 'var(--ink-2)' }}>{e.created_at}</div>
                  <div className="mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 2 }} title="IP masked for privacy">{maskIp(e.ip)}</div>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </AppShell>
  );
};

// ---------- Admin ----------
const AdminUsersPage = () => {
  const [users, setUsers] = React.useState(null);
  React.useEffect(() => { api('/api/admin/users').then(d => setUsers(d.users)).catch(() => setUsers([])); }, []);
  return (
    <AppShell active="all" title="Admin · Users">
      <div style={{ padding: '24px 28px' }}>
        <h2 className="display" style={{ fontSize: 22, margin: 0, marginBottom: 6 }}>All users</h2>
        <p style={{ fontSize: 12, color: 'var(--ink-3)', margin: 0, marginBottom: 22 }}>Admin‑only view of every account.</p>
        {!users ? <div style={{ color: 'var(--ink-3)' }}>Loading…</div> : (
          <div className="card">
            <table className="tbl">
              <thead><tr><th>Email</th><th>Name</th><th>Role</th><th>Verified</th><th>Docs</th><th>Created</th></tr></thead>
              <tbody>
                {users.map(u => (
                  <tr key={u.id}>
                    <td className="mono" style={{ fontSize: 11 }}>{u.email}</td>
                    <td>{u.full_name || '—'}</td>
                    <td><span className="pill" data-state={u.role === 'admin' ? 'sent' : 'draft'}><span className="pill-dot" />{u.role}</span></td>
                    <td>{u.email_verified ? <Icon name="check" size={12} color="var(--positive)" /> : <Icon name="x" size={12} color="var(--ink-3)" />}</td>
                    <td className="mono" style={{ fontSize: 11 }}>{u.doc_count}</td>
                    <td style={{ fontSize: 11, color: 'var(--ink-3)' }}>{u.created_at}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </AppShell>
  );
};

const AdminAuditPage = () => {
  const [events, setEvents] = React.useState(null);
  React.useEffect(() => { api('/api/admin/audit').then(d => setEvents(d.events)).catch(() => setEvents([])); }, []);
  return (
    <AppShell active="all" title="Admin · Global audit">
      <div style={{ padding: '24px 28px' }}>
        <h2 className="display" style={{ fontSize: 22, margin: 0, marginBottom: 6 }}>Global audit log</h2>
        <p style={{ fontSize: 12, color: 'var(--ink-3)', margin: 0, marginBottom: 22 }}>Last 200 events across all tenants. Hash‑chained.</p>
        {!events ? <div style={{ color: 'var(--ink-3)' }}>Loading…</div> : (
          <div className="card">
            <table className="tbl">
              <thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Detail</th><th>Doc</th><th>IP</th></tr></thead>
              <tbody>
                {events.map(e => (
                  <tr key={e.id}>
                    <td className="mono" style={{ fontSize: 11 }}>{e.created_at}</td>
                    <td className="mono" style={{ fontSize: 11 }}>{e.actor_email || 'system'}</td>
                    <td><span style={{ fontWeight: 500 }}>{e.action}</span></td>
                    <td style={{ fontSize: 12, color: 'var(--ink-2)' }}>{e.detail || '—'}</td>
                    <td className="mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{e.document_id?.slice(0, 12) || '—'}</td>
                    <td className="mono" style={{ fontSize: 11 }} title="IP masked for privacy">{maskIp(e.ip)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </AppShell>
  );
};

// ---------- Public verification ----------
const VerifyPage = () => {
  const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
  const initialCode = params.get('code') || '';
  const [code, setCode] = React.useState(initialCode);
  const [busy, setBusy] = React.useState(false);
  const [result, setResult] = React.useState(null); // {verified, document, signers, ...} | {error}
  const [err, setErr] = React.useState(null);
  const fileRef = React.useRef(null);

  // Auto-verify when ?code= is in the URL
  React.useEffect(() => { if (initialCode && initialCode.length >= 12) verifyByCode(initialCode); }, []);

  const verifyByCode = async (raw) => {
    const c = String(raw || code).trim().toUpperCase();
    setErr(null); setResult(null); setBusy(true);
    try {
      const r = await api(`/api/verify/${encodeURIComponent(c)}`);
      setResult(r);
    } catch (ex) {
      setErr(ex.data?.error === 'invalid_format' ? 'Code format should look like VRF-XXXX-XXXX.' :
             ex.data?.error === 'rate_limited'   ? 'Too many checks from this network. Try again later.' :
             ex.status === 404                   ? 'No document matches that code.' :
                                                   'Verification failed.');
    } finally { setBusy(false); }
  };

  const verifyByFile = async (file) => {
    if (!file) return;
    setErr(null); setResult(null); setBusy(true);
    try {
      const buf = await file.arrayBuffer();
      const h = await crypto.subtle.digest('SHA-256', buf);
      const hex = Array.from(new Uint8Array(h)).map(b => b.toString(16).padStart(2, '0')).join('');
      const r = await api('/api/verify/by-hash', { method: 'POST', body: { sha256: hex } });
      setResult(r);
    } catch (ex) {
      setErr(ex.status === 404                  ? 'This file does not match any document on Saign.' :
             ex.data?.error === 'rate_limited'  ? 'Too many checks from this network. Try again later.' :
                                                  'Verification failed.');
    } finally { setBusy(false); }
  };

  return (
    <div className="saign-app" style={{ minHeight: '100vh', background: 'var(--paper)' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    padding: '20px 48px', borderBottom: '1px solid var(--line)' }}>
        <Link to="/" style={{ textDecoration: 'none' }}><Logo size={15} /></Link>
        <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>
          Independent verification — no account needed
        </div>
      </div>
      <div style={{ maxWidth: 760, margin: '0 auto', padding: '48px 32px 64px' }}>
        <div className="eyebrow" style={{ marginBottom: 6 }}>Document verification</div>
        <h1 className="display" style={{ fontSize: 32, margin: 0, marginBottom: 8 }}>Is this signature legitimate?</h1>
        <p style={{ fontSize: 14, color: 'var(--ink-2)', margin: 0, marginBottom: 32, lineHeight: 1.55 }}>
          Enter the verification code printed on a Saign certificate, or drop in the signed PDF directly. We'll check it against the database and show you what we have on record.
        </p>

        <div className="card" style={{ padding: 24, marginBottom: 18 }}>
          <h3 className="display" style={{ fontSize: 16, margin: 0, marginBottom: 12 }}>Verify by code</h3>
          <form onSubmit={(e) => { e.preventDefault(); verifyByCode(); }}
                style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
            <div className="field" style={{ flex: 1, marginBottom: 0 }}>
              <label>Verification code</label>
              <input value={code} onChange={e => setCode(e.target.value.toUpperCase())}
                     placeholder="VRF-XXXX-XXXX" maxLength={13} required
                     className="mono" style={{ fontSize: 16, letterSpacing: '0.1em' }} />
            </div>
            <button type="submit" className="btn" disabled={busy || code.length < 12}
                    style={{ opacity: (busy || code.length < 12) ? 0.5 : 1 }}>
              {busy ? 'Checking…' : <>Verify <Icon name="arrow" size={11} color="var(--paper)" /></>}
            </button>
          </form>
        </div>

        <div className="card" style={{ padding: 24, marginBottom: 18 }}>
          <h3 className="display" style={{ fontSize: 16, margin: 0, marginBottom: 12 }}>Verify by file</h3>
          <p style={{ fontSize: 12, color: 'var(--ink-3)', margin: 0, marginBottom: 14, lineHeight: 1.55 }}>
            Drop a signed PDF (or any file) here. We compute its SHA-256 in your browser and check if it matches a document we have on record. The file never leaves your machine — only the hash is sent.
          </p>
          <input ref={fileRef} type="file" accept="application/pdf,.pdf"
                 onChange={(e) => verifyByFile(e.target.files?.[0])}
                 style={{ display: 'none' }} />
          <button type="button" onClick={() => fileRef.current?.click()}
                  className="btn btn-ghost" disabled={busy}>
            <Icon name="upload" size={11} /> Choose PDF to verify
          </button>
        </div>

        {err && (
          <div style={{ background: '#fbeae6', border: '1px solid #e8b8a8', color: 'var(--danger)',
                        padding: '12px 14px', fontSize: 13, borderRadius: 2, marginBottom: 18 }}>
            {err}
          </div>
        )}

        {result && <VerifyReport result={result} />}

        <div style={{ marginTop: 32, padding: '14px 16px', background: 'var(--paper-2)',
                      border: '1px solid var(--line)', borderRadius: 2, fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.55 }}>
          <strong style={{ color: 'var(--ink-2)', fontWeight: 500 }}>What this proves.</strong>{' '}
          A green result means the code or hash matches a record in Saign's database, and the document is referenced
          there with the audit trail shown. It does <em>not</em> currently prove eIDAS qualified-signature compliance —
          PAdES-LTA / TSA timestamps are on the roadmap.
        </div>
      </div>
    </div>
  );
};

const VerifyReport = ({ result }) => {
  const d = result.document;
  const state = result.state || (result.verified ? 'signed_complete' : 'in_progress');
  const verified = !!result.verified;
  const tone =
    state === 'signed_complete' ? 'ok' :
    state === 'declined'        ? 'bad' :
                                  'warn';
  const banner = {
    ok:   { bg: '#e7f0ea', dot: 'var(--positive)', icon: 'check', title: 'Verified — signed and complete' },
    warn: { bg: '#fbf4e6', dot: '#a86b1a',         icon: 'clock', title: 'Found, but signing is not complete' },
    bad:  { bg: '#fbeae6', dot: 'var(--danger)',   icon: 'x',     title: 'Found, but a signer declined' },
  }[tone];
  const subline =
    state === 'signed_complete' ? 'All signers have signed. Saign vouches for the audit trail below.' :
    state === 'declined'        ? 'This document has not been signed — at least one signer declined.' :
    state === 'draft'           ? 'This document was never sent for signing.' :
    state === 'in_progress'     ? 'This document was sent, but not every signer has signed yet.' :
                                  'Found in Saign\'s database.';
  return (
    <div className="card" style={{ padding: 0, marginBottom: 18, overflow: 'hidden' }}>
      <div style={{
        padding: '18px 22px', display: 'flex', alignItems: 'center', gap: 12,
        background: banner.bg,
        borderBottom: '1px solid var(--line)',
      }}>
        <div style={{
          width: 36, height: 36, borderRadius: '50%',
          background: banner.dot,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <Icon name={banner.icon} size={18} color="#fff" />
        </div>
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 500, fontSize: 14, color: 'var(--ink)' }}>
            {banner.title}
          </div>
          <div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 2 }}>
            {subline} · {result.matched ? `matched ${result.matched} PDF` : `looked up by code`}
          </div>
        </div>
      </div>
      <div style={{ padding: '18px 22px' }}>
        <div className="eyebrow" style={{ marginBottom: 10 }}>Document</div>
        <div style={{ fontSize: 16, fontWeight: 500, marginBottom: 4 }}>{d.title}</div>
        <div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 16 }}>
          {d.code} · {d.pages || '?'} pages · {d.verify_code}
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px 16px', fontSize: 12, marginBottom: 18 }}>
          <span style={{ color: 'var(--ink-3)' }}>Status</span>
          <span style={{ textAlign: 'right' }}>
            <span className="pill" data-state={d.status}><span className="pill-dot" />{d.status}</span>
          </span>
          <span style={{ color: 'var(--ink-3)' }}>Sender</span>
          <span style={{ textAlign: 'right' }}>{result.sender?.name} <span style={{ color: 'var(--ink-3)' }}>· {result.sender?.email}</span></span>
          <span style={{ color: 'var(--ink-3)' }}>Created</span>
          <span className="mono" style={{ textAlign: 'right' }}>{d.created_at || '—'}</span>
          {d.completed_at && (<>
            <span style={{ color: 'var(--ink-3)' }}>Completed</span>
            <span className="mono" style={{ textAlign: 'right' }}>{d.completed_at}</span>
          </>)}
          <span style={{ color: 'var(--ink-3)' }}>Progress</span>
          <span className="mono" style={{ textAlign: 'right' }}>{result.progress?.signed} / {result.progress?.total} signed</span>
        </div>

        <div className="eyebrow" style={{ marginBottom: 8 }}>Hashes</div>
        <div style={{ fontSize: 11, marginBottom: 18 }}>
          <div style={{ display: 'flex', gap: 8, alignItems: 'baseline', marginBottom: 6 }}>
            <span style={{ color: 'var(--ink-3)', minWidth: 90 }}>Original</span>
            <span className="mono" style={{ wordBreak: 'break-all', flex: 1 }}>{d.sha256 || '—'}</span>
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'baseline' }}>
            <span style={{ color: 'var(--ink-3)', minWidth: 90 }}>Signed copy</span>
            <span className="mono" style={{ wordBreak: 'break-all', flex: 1 }}>{d.sha256_signed || '—'}</span>
          </div>
        </div>

        <div className="eyebrow" style={{ marginBottom: 8 }}>Signers</div>
        <div style={{ borderTop: '1px solid var(--line-2)' }}>
          {result.signers?.map((s, i) => (
            <div key={i} style={{ padding: '12px 0', borderBottom: '1px solid var(--line-2)' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4, flexWrap: 'wrap' }}>
                <span style={{ fontWeight: 500, fontSize: 13 }}>{s.name}</span>
                <span style={{ fontSize: 11, color: 'var(--ink-3)' }}>·</span>
                <span style={{ fontSize: 11, color: 'var(--ink-3)' }}>{s.email}</span>
                <span style={{ flex: 1 }} />
                {s.id_verified && (
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4,
                                 fontSize: 10, padding: '2px 7px', background: '#e7f0ea',
                                 color: 'var(--positive)', border: '1px solid #9fbfa8',
                                 borderRadius: 1, fontWeight: 500 }}>
                    <Icon name="shield" size={9} color="var(--positive)" /> ID verified
                    {s.id_country && <span className="mono" style={{ opacity: 0.75 }}>· {s.id_country}</span>}
                  </span>
                )}
                <span className="pill" data-state={s.status === 'signed' ? 'signed' : s.status === 'declined' ? 'declined' : 'sent'}>
                  <span className="pill-dot" />{s.status}
                </span>
              </div>
              <div className="mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>
                {s.signed_at ? <>signed {s.signed_at}</> : 'not yet signed'}
                {s.signed_method && <> · {s.signed_method}</>}
                {s.ip_masked && <> · ip {s.ip_masked}</>}
                {s.location && <> · {s.location} ({s.location_source})</>}
              </div>
            </div>
          ))}
        </div>

        {result.audit_chain_tip && (
          <>
            <div className="eyebrow" style={{ marginTop: 18, marginBottom: 6 }}>Audit chain tip</div>
            <div className="mono" style={{ fontSize: 10, color: 'var(--ink-3)', wordBreak: 'break-all' }}>
              {result.audit_chain_tip.action} @ {result.audit_chain_tip.created_at} · {result.audit_chain_tip.hash}
            </div>
          </>
        )}
      </div>
    </div>
  );
};

// ---------- Legal / static pages ----------
const LegalShell = ({ title, eyebrow, children }) => (
  <div className="saign-app" style={{ minHeight: '100vh', background: 'var(--paper)' }}>
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                  padding: '20px 48px', borderBottom: '1px solid var(--line)' }}>
      <Link to="/" style={{ textDecoration: 'none' }}><Logo size={15} /></Link>
      <div style={{ display: 'flex', gap: 18, alignItems: 'center', fontSize: 12 }}>
        <Link to="/privacy" className="btn-link">Privacy</Link>
        <Link to="/terms" className="btn-link">Terms</Link>
        <Link to="/cookies" className="btn-link">Cookies</Link>
        <Link to="/subprocessors" className="btn-link">Subprocessors</Link>
        <Link to="/security" className="btn-link">Security</Link>
      </div>
    </div>
    <div style={{ maxWidth: 760, margin: '0 auto', padding: '48px 32px 80px' }}>
      <div className="eyebrow" style={{ marginBottom: 8 }}>{eyebrow}</div>
      <h1 className="display" style={{ fontSize: 36, margin: 0, marginBottom: 28 }}>{title}</h1>
      <div style={{ fontSize: 14, color: 'var(--ink-2)', lineHeight: 1.65 }}>{children}</div>
      <div style={{ marginTop: 40, fontSize: 11, color: 'var(--ink-3)' }}>
        Last updated 2026-04-27. This is a template — adapt to your jurisdiction with a lawyer before going live.
      </div>
    </div>
  </div>
);

const LegalPage = ({ which }) => {
  if (which === 'privacy') return (
    <LegalShell eyebrow="Privacy" title="Privacy Policy">
      <p>Saign processes personal data to deliver electronic-signature services. We are the data controller for account data and the processor for documents you upload on behalf of your customers.</p>
      <h3>What we collect</h3>
      <ul>
        <li><strong>Account data:</strong> email, name, password hash (PBKDF2-SHA256), TOTP secret if you enable 2FA.</li>
        <li><strong>Documents:</strong> PDF files you upload, signer email addresses, signature images, signing timestamps.</li>
        <li><strong>Audit data:</strong> IP address, user-agent, geolocation (with consent) for every signing event. Stored in a tamper-evident hash chain.</li>
        <li><strong>Operational logs:</strong> request metadata at Cloudflare's edge.</li>
      </ul>
      <h3>Where it lives</h3>
      <p>Documents and database rows are stored in Cloudflare's EU region (Frankfurt). All transit is TLS 1.3.</p>
      <h3>How long we keep it</h3>
      <p>Signed documents and audit trails are kept for the retention period set in your account settings (default 10 years). After that they are auto-purged. You can delete your account self-service under "Settings &rarr; Danger zone".</p>
      <h3>Your rights (Articles 15–22 GDPR)</h3>
      <p>You can <strong>export</strong> all your data as JSON at <Link to="/settings" className="btn-link">Settings</Link>, request <strong>rectification</strong> by editing your profile, or <strong>delete</strong> your account. For other rights (objection, restriction) write to privacy@saign.example.</p>
      <h3>Subprocessors</h3>
      <p>See the <Link to="/subprocessors" className="btn-link">subprocessors page</Link>.</p>
      <h3>Cookies</h3>
      <p>We use only functional cookies for session and CSRF. No tracking, no analytics. See <Link to="/cookies" className="btn-link">cookie policy</Link>.</p>
    </LegalShell>
  );
  if (which === 'terms') return (
    <LegalShell eyebrow="Legal" title="Terms of Service">
      <p>By using Saign you agree to the following.</p>
      <h3>Service</h3>
      <p>Saign provides simple electronic signatures (eIDAS SES). Signed PDFs include a visual signature, hashed audit trail, and an optional certificate of completion.</p>
      <h3>What we are not</h3>
      <p>This is currently <strong>not</strong> a Qualified Electronic Signature (QES) service. PAdES-LTA / TSA timestamps / QTSP integration are roadmap items.</p>
      <h3>Acceptable use</h3>
      <p>No illegal content, no impersonation, no automated abuse, no document repository for material you do not have rights to.</p>
      <h3>Liability</h3>
      <p>Service provided "as-is" during the demo phase. Liability limited to fees paid in the prior 12 months, except where local law mandates otherwise.</p>
      <h3>Termination</h3>
      <p>Either party can terminate at any time. After termination, audit data is retained as required by law and your account retention setting.</p>
    </LegalShell>
  );
  if (which === 'cookies') return (
    <LegalShell eyebrow="Cookies" title="Cookie Policy">
      <p>Saign uses three first-party cookies. None of them are used for tracking or advertising.</p>
      <table className="tbl" style={{ marginTop: 16 }}>
        <thead><tr><th>Name</th><th>Purpose</th><th>Lifetime</th></tr></thead>
        <tbody>
          <tr><td className="mono">saign_sid</td><td>Authenticated session (HttpOnly + Secure)</td><td>7 days</td></tr>
          <tr><td className="mono">saign_csrf</td><td>CSRF double-submit token</td><td>7 days</td></tr>
          <tr><td className="mono">saign_pre</td><td>Short-lived pre-auth cookie during 2FA</td><td>5 minutes</td></tr>
        </tbody>
      </table>
      <h3>Third-party cookies</h3>
      <p>None on the application itself. The optional reverse-geocoding feature on the signing page makes a request to <span className="mono">nominatim.openstreetmap.org</span> only with the signer's explicit consent.</p>
    </LegalShell>
  );
  if (which === 'subprocessors') return (
    <LegalShell eyebrow="Compliance" title="Subprocessors">
      <p>The third parties Saign relies on to deliver the service. Each is bound by a DPA where applicable.</p>
      <table className="tbl" style={{ marginTop: 16 }}>
        <thead><tr><th>Subprocessor</th><th>Purpose</th><th>Region</th><th>Data</th></tr></thead>
        <tbody>
          <tr><td>Cloudflare, Inc.</td><td>Hosting, edge, D1 database, R2 storage</td><td>EU (Frankfurt)</td><td>All</td></tr>
          <tr><td>Resend, Inc.</td><td>Transactional email delivery</td><td>EU/US</td><td>Recipient email + name</td></tr>
          <tr><td>OpenStreetMap Nominatim</td><td>Reverse-geocoding (opt-in)</td><td>EU</td><td>Latitude/longitude only</td></tr>
        </tbody>
      </table>
      <h3>Notification of changes</h3>
      <p>We will update this page and (for paying customers) email at least 30 days before adding a new subprocessor.</p>
    </LegalShell>
  );
  if (which === 'security') return (
    <LegalShell eyebrow="Security" title="Vulnerability Disclosure">
      <p>We welcome security researchers. If you believe you've found a vulnerability:</p>
      <ul>
        <li>Email <span className="mono">security@saign.example</span> with a description and reproduction steps.</li>
        <li>Allow us 90 days before public disclosure.</li>
        <li>Don't access data that isn't yours; don't degrade service for others.</li>
      </ul>
      <p>Acting in good faith under this policy means we will not pursue legal action. Machine-readable contact at <Link to="/.well-known/security.txt" className="mono btn-link">/.well-known/security.txt</Link>.</p>
      <h3>Current security controls</h3>
      <ul>
        <li>TLS 1.3, HSTS, strict CSP, CSRF tokens, X-Frame-Options DENY</li>
        <li>PBKDF2-SHA256 password hashing, optional TOTP-2FA</li>
        <li>Rate-limited login with per-account and per-IP lockout</li>
        <li>Hash-chained audit log (tamper-evident)</li>
        <li>Soft-delete with retention policy, IP masking on display</li>
        <li>EU data residency</li>
      </ul>
    </LegalShell>
  );
  return <NotFoundPage />;
};

const CookieBanner = () => {
  const [hidden, setHidden] = React.useState(() => {
    try { return localStorage.getItem('saign_cookie_ack') === '1'; } catch { return false; }
  });
  if (hidden) return null;
  const accept = () => {
    try { localStorage.setItem('saign_cookie_ack', '1'); } catch {}
    setHidden(true);
  };
  return (
    <div style={{ position: 'fixed', bottom: 16, left: 16, right: 16, zIndex: 999, display: 'flex', justifyContent: 'center' }}>
      <div className="card" style={{ maxWidth: 720, padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 16,
                                     boxShadow: '0 12px 32px rgba(26,24,20,0.12)' }}>
        <div style={{ flex: 1, fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.55 }}>
          Saign uses functional cookies only — session, CSRF, and a temporary 2FA cookie. No tracking, no analytics.
          See <Link to="/cookies" className="btn-link">cookie policy</Link>.
        </div>
        <button onClick={accept} className="btn btn-sm">OK</button>
      </div>
    </div>
  );
};

// ---------- Settings ----------
const SettingsPage = () => {
  const { user } = useAuth();
  const toast = useToast();
  const [me, setMe] = React.useState(null);
  const [name, setName] = React.useState('');
  const [savingName, setSavingName] = React.useState(false);

  const [pwCur, setPwCur] = React.useState('');
  const [pwNew, setPwNew] = React.useState('');
  const [pwNew2, setPwNew2] = React.useState('');
  const [pwBusy, setPwBusy] = React.useState(false);
  const [pwErr, setPwErr] = React.useState(null);

  const [sessions, setSessions] = React.useState(null);

  const reload = React.useCallback(() => {
    Promise.all([
      api('/api/auth/me'),
      api('/api/auth/sessions').catch(() => ({ sessions: [] })),
    ]).then(([m, s]) => {
      setMe(m); setName(m.full_name || ''); setSessions(s.sessions || []);
    });
  }, []);
  React.useEffect(() => { reload(); }, [reload]);

  const saveName = async (e) => {
    e.preventDefault();
    setSavingName(true);
    try {
      await api('/api/auth/me', { method: 'PATCH', body: { full_name: name.trim() } });
      toast.push('Profile updated');
      reload();
    } catch (ex) {
      toast.push('Update failed', 'err');
    } finally { setSavingName(false); }
  };

  const changePw = async (e) => {
    e.preventDefault();
    setPwErr(null);
    if (pwNew !== pwNew2) { setPwErr('New passwords do not match.'); return; }
    if (pwNew.length < 8) { setPwErr('New password must be at least 8 characters.'); return; }
    setPwBusy(true);
    try {
      await api('/api/auth/change-password', { method: 'POST',
        body: { current_password: pwCur, new_password: pwNew } });
      setPwCur(''); setPwNew(''); setPwNew2('');
      toast.push('Password changed — other sessions revoked');
      reload();
    } catch (ex) {
      setPwErr(ex.data?.error === 'wrong_password' ? 'Current password is incorrect.' :
               ex.data?.error === 'same_password' ? 'New password must differ from current.' :
               ex.data?.error === 'invalid_input' ? 'Please check the password.' :
               'Could not change password.');
    } finally { setPwBusy(false); }
  };

  const revoke = async (sid) => {
    try {
      await api(`/api/auth/sessions/${sid}/revoke`, { method: 'POST' });
      toast.push('Session signed out');
      reload();
    } catch { toast.push('Could not sign out session', 'err'); }
  };

  const SectionCard = ({ title, hint, children }) => (
    <div className="card" style={{ padding: 20, marginBottom: 18 }}>
      <div style={{ marginBottom: 14 }}>
        <h3 className="display" style={{ fontSize: 16, margin: 0, marginBottom: 4 }}>{title}</h3>
        {hint && <p style={{ fontSize: 11, color: 'var(--ink-3)', margin: 0 }}>{hint}</p>}
      </div>
      {children}
    </div>
  );

  return (
    <AppShell active="settings" title="Account">
      <div style={{ padding: '28px 48px', maxWidth: 720 }}>
        <div className="eyebrow" style={{ marginBottom: 6 }}>Account</div>
        <h2 className="display" style={{ fontSize: 24, margin: 0, marginBottom: 22 }}>Settings</h2>

        <SectionCard title="Profile" hint="Visible on signed documents and invitation emails.">
          {!me ? <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>Loading…</div> : (
            <form onSubmit={saveName} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
              <div className="field"><label>Email</label>
                <input value={me.email} disabled
                       style={{ background: 'var(--paper-2)', color: 'var(--ink-3)' }} />
                <div className="field-hint">Email cannot be changed self-service.</div>
              </div>
              <div className="field"><label>Full name</label>
                <input value={name} onChange={e => setName(e.target.value)} required maxLength={120} />
              </div>
              <div className="field"><label>Role</label>
                <input value={me.role} disabled style={{ background: 'var(--paper-2)', color: 'var(--ink-3)' }} />
              </div>
              <div className="field"><label>Member since</label>
                <input value={me.created_at || ''} disabled className="mono"
                       style={{ background: 'var(--paper-2)', color: 'var(--ink-3)', fontSize: 11 }} />
              </div>
              <div>
                <button type="submit" className="btn" disabled={savingName || name.trim() === (me.full_name || '')}
                        style={{ opacity: (savingName || name.trim() === (me.full_name || '')) ? 0.5 : 1 }}>
                  {savingName ? 'Saving…' : 'Save profile'}
                </button>
              </div>
            </form>
          )}
        </SectionCard>

        <SectionCard title="Password" hint="Changing your password will sign out all other devices.">
          <form onSubmit={changePw} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            <div className="field"><label>Current password</label>
              <input type="password" autoComplete="current-password"
                     value={pwCur} onChange={e => setPwCur(e.target.value)} required />
            </div>
            <div className="field"><label>New password</label>
              <input type="password" autoComplete="new-password" minLength={8}
                     value={pwNew} onChange={e => setPwNew(e.target.value)} required />
              <div className="field-hint">Min. 8 characters. Hashed with PBKDF2-SHA256.</div>
            </div>
            <div className="field"><label>Confirm new password</label>
              <input type="password" autoComplete="new-password" minLength={8}
                     value={pwNew2} onChange={e => setPwNew2(e.target.value)} required />
            </div>
            {pwErr && (
              <div style={{ background: '#fbeae6', border: '1px solid #e8b8a8', color: 'var(--danger)',
                            padding: '8px 12px', fontSize: 12, borderRadius: 2 }}>{pwErr}</div>
            )}
            <div>
              <button type="submit" className="btn" disabled={pwBusy} style={{ opacity: pwBusy ? 0.5 : 1 }}>
                {pwBusy ? 'Changing…' : 'Change password'}
              </button>
            </div>
          </form>
        </SectionCard>

        <SectionCard title="Active sessions" hint="Devices currently signed in. Sign out anything you don't recognise.">
          {!sessions ? <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>Loading…</div> :
           sessions.length === 0 ? <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>No active sessions.</div> : (
            <div style={{ borderTop: '1px solid var(--line-2)' }}>
              {sessions.map(s => (
                <div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 12,
                                          padding: '10px 0', borderBottom: '1px solid var(--line-2)' }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 12, color: 'var(--ink)' }}>
                      {s.user_agent ? s.user_agent.slice(0, 60) + (s.user_agent.length > 60 ? '…' : '') : 'Unknown device'}
                      {s.current && (
                        <span className="pill" data-state="signed" style={{ marginLeft: 8, fontSize: 9 }}>
                          <span className="pill-dot" />Current
                        </span>
                      )}
                    </div>
                    <div className="mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 2 }}>
                      {s.ip || '—'} · started {s.created_at} · expires {s.expires_at}
                    </div>
                  </div>
                  {!s.current && (
                    <button className="btn btn-ghost btn-sm" onClick={() => revoke(s.id)}>
                      <Icon name="x" size={11} /> Sign out
                    </button>
                  )}
                </div>
              ))}
            </div>
          )}
        </SectionCard>

        <SectionCard title="Two-factor authentication" hint="Adds a 6-digit code from an authenticator app on every sign-in.">
          <TwoFactorBlock me={me} onChanged={reload} />
        </SectionCard>

        <SectionCard title="Mail delivery" hint="How invitation emails leave Saign.">
          <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
            <span className="pill" data-state={me?.mail_provider === 'resend' ? 'signed' : 'draft'}>
              <span className="pill-dot" />
              {me?.mail_provider === 'resend' ? 'Resend (live)' : 'Dev (no provider)'}
            </span>
            <span style={{ fontSize: 11, color: 'var(--ink-3)' }}>
              {me?.mail_provider === 'resend'
                ? 'Invitations are delivered via Resend.'
                : 'Set RESEND_API_KEY to enable real delivery. Until then, copy links manually.'}
            </span>
          </div>
        </SectionCard>

        <SectionCard title="Data & retention"
                     hint="Export your data (GDPR Art. 20) or set how long signed documents stay before auto-purge.">
          <RetentionBlock me={me} onChanged={reload} />
        </SectionCard>

        <SectionCard title="Danger zone"
                     hint="Permanently delete your account and all associated documents (GDPR Art. 17).">
          <DeleteAccountBlock />
        </SectionCard>
      </div>
    </AppShell>
  );
};

const TotpQrCode = ({ uri, size = 200 }) => {
  const ref = React.useRef(null);
  const [ready, setReady] = React.useState(typeof window !== 'undefined' && !!window.qrcode);
  React.useEffect(() => {
    if (typeof window === 'undefined') return;
    if (window.qrcode) { setReady(true); return; }
    // Lib still loading — poll briefly
    let attempts = 0;
    const t = setInterval(() => {
      attempts += 1;
      if (window.qrcode) { setReady(true); clearInterval(t); }
      else if (attempts > 20) clearInterval(t);
    }, 100);
    return () => clearInterval(t);
  }, []);
  React.useEffect(() => {
    if (!ready || !ref.current || !uri) return;
    const qr = window.qrcode(0, 'M');     // type 0 = auto-size, error correction M
    qr.addData(uri);
    qr.make();
    ref.current.innerHTML = qr.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
    const svg = ref.current.querySelector('svg');
    if (svg) {
      svg.setAttribute('width', size);
      svg.setAttribute('height', size);
      svg.style.display = 'block';
    }
  }, [uri, size, ready]);
  if (!ready) {
    return (
      <div style={{ width: size, height: size, background: 'var(--paper-2)', border: '1px solid var(--line)',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    fontSize: 11, color: 'var(--ink-3)' }}>
        Loading QR…
      </div>
    );
  }
  return <div ref={ref} style={{ display: 'inline-block', background: '#fff',
                                 padding: 12, border: '1px solid var(--line)', borderRadius: 2 }} />;
};

const TwoFactorBlock = ({ me, onChanged }) => {
  const toast = useToast();
  const [enrolling, setEnrolling] = React.useState(null); // { secret, otpauth_uri }
  const [code, setCode] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [pw, setPw] = React.useState('');
  const [showDisable, setShowDisable] = React.useState(false);

  const begin = async () => {
    setBusy(true);
    try {
      const r = await api('/api/auth/mfa/begin', { method: 'POST' });
      setEnrolling(r); setCode('');
    } catch (ex) { toast.push('Could not start 2FA enrolment', 'err'); }
    finally { setBusy(false); }
  };
  const confirm = async (e) => {
    e.preventDefault(); setBusy(true);
    try {
      await api('/api/auth/mfa/confirm', { method: 'POST', body: { code } });
      toast.push('Two-factor authentication enabled');
      setEnrolling(null); setCode(''); onChanged?.();
    } catch (ex) {
      toast.push(ex.data?.error === 'invalid_code' ? 'That code didn\'t match.' : 'Could not enable 2FA', 'err');
    } finally { setBusy(false); }
  };
  const disable = async (e) => {
    e.preventDefault(); setBusy(true);
    try {
      await api('/api/auth/mfa/disable', { method: 'POST', body: { password: pw } });
      toast.push('Two-factor authentication disabled');
      setShowDisable(false); setPw(''); onChanged?.();
    } catch (ex) {
      toast.push(ex.data?.error === 'wrong_password' ? 'Wrong password.' : 'Could not disable 2FA', 'err');
    } finally { setBusy(false); }
  };

  if (me?.mfa_enabled) {
    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
          <span className="pill" data-state="signed"><span className="pill-dot" />Enabled</span>
          <span style={{ fontSize: 12, color: 'var(--ink-2)' }}>You'll need a code from your authenticator at every sign-in.</span>
        </div>
        {!showDisable ? (
          <button className="btn btn-ghost btn-sm" onClick={() => setShowDisable(true)}>
            <Icon name="x" size={11} /> Disable 2FA
          </button>
        ) : (
          <form onSubmit={disable} style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
            <div className="field" style={{ flex: 1, marginBottom: 0 }}>
              <label>Confirm with password</label>
              <input type="password" value={pw} onChange={e => setPw(e.target.value)} required autoFocus />
            </div>
            <button type="submit" className="btn btn-sm" disabled={busy} style={{ color: 'var(--danger)', opacity: busy ? 0.5 : 1 }}>
              Disable
            </button>
            <button type="button" className="btn btn-ghost btn-sm" onClick={() => { setShowDisable(false); setPw(''); }}>
              Cancel
            </button>
          </form>
        )}
      </div>
    );
  }

  if (!enrolling) {
    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
          <span className="pill" data-state="draft"><span className="pill-dot" />Off</span>
          <span style={{ fontSize: 12, color: 'var(--ink-2)' }}>Recommended for any account that signs documents.</span>
        </div>
        <button className="btn" onClick={begin} disabled={busy} style={{ opacity: busy ? 0.5 : 1 }}>
          <Icon name="lock" size={11} color="var(--paper)" /> {busy ? 'Starting…' : 'Enable 2FA'}
        </button>
      </div>
    );
  }

  return (
    <form onSubmit={confirm}>
      <div style={{ marginBottom: 14, fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.55 }}>
        1. Scan this QR code with your authenticator app (Google Authenticator, Authy, 1Password, Microsoft Authenticator, etc.).
      </div>
      <div style={{ display: 'flex', gap: 16, marginBottom: 14, flexWrap: 'wrap', alignItems: 'flex-start' }}>
        <TotpQrCode uri={enrolling.otpauth_uri} size={200} />
        <div style={{ flex: 1, minWidth: 220 }}>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Or enter the secret manually</div>
          <div className="mono" style={{ fontSize: 13, letterSpacing: '0.1em', wordBreak: 'break-all',
                                         padding: '8px 10px', background: 'var(--paper-2)',
                                         border: '1px solid var(--line)', borderRadius: 2, marginBottom: 8 }}>
            {enrolling.secret}
          </div>
          <button type="button" className="btn-link" style={{ fontSize: 11 }}
                  onClick={() => navigator.clipboard?.writeText(enrolling.secret).then(
                    () => toast.push('Secret copied'),
                    () => {})}>
            Copy secret
          </button>
          <div style={{ marginTop: 14, fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.5 }}>
            Algorithm: SHA-1 · Digits: 6 · Period: 30s. The standard Google
            Authenticator defaults — should "just work" in any compliant app.
          </div>
        </div>
      </div>
      <div className="field"><label>2. Enter the current 6-digit code from your app</label>
        <input value={code} onChange={e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
               maxLength={6} inputMode="numeric" autoFocus required
               style={{ fontFamily: 'var(--mono)', fontSize: 22, letterSpacing: '0.4em', textAlign: 'center' }} />
      </div>
      <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
        <button type="submit" className="btn" disabled={busy || code.length !== 6}
                style={{ opacity: (busy || code.length !== 6) ? 0.5 : 1 }}>
          {busy ? 'Verifying…' : 'Confirm and enable'}
        </button>
        <button type="button" className="btn btn-ghost" onClick={() => setEnrolling(null)}>Cancel</button>
      </div>
    </form>
  );
};

const RetentionBlock = ({ me, onChanged }) => {
  const toast = useToast();
  const [years, setYears] = React.useState(me?.retention_years ?? 10);
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => { if (me?.retention_years != null) setYears(me.retention_years); }, [me?.retention_years]);

  const save = async () => {
    setBusy(true);
    try {
      await api('/api/auth/retention', { method: 'PATCH', body: { years: Number(years) } });
      toast.push('Retention updated'); onChanged?.();
    } catch { toast.push('Update failed', 'err'); }
    finally { setBusy(false); }
  };

  const exportData = () => {
    // Authenticated GET — open in new tab triggers download via Content-Disposition.
    const url = (typeof window !== 'undefined' && window.SAIGN_API ? window.SAIGN_API : '') + '/api/auth/export';
    window.open(url, '_blank', 'noopener');
  };

  return (
    <div>
      <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', marginBottom: 16 }}>
        <div className="field" style={{ flex: 1, marginBottom: 0 }}>
          <label>Retention period (years)</label>
          <input type="number" min={1} max={30} value={years}
                 onChange={e => setYears(e.target.value)} />
          <div className="field-hint">Signed documents auto-purge after this many years (default 10).</div>
        </div>
        <button className="btn btn-sm" onClick={save} disabled={busy || Number(years) === me?.retention_years}
                style={{ opacity: busy || Number(years) === me?.retention_years ? 0.5 : 1 }}>
          {busy ? 'Saving…' : 'Save'}
        </button>
      </div>
      <button className="btn btn-ghost btn-sm" onClick={exportData}>
        <Icon name="download" size={11} /> Export all my data (JSON)
      </button>
    </div>
  );
};

const DeleteAccountBlock = () => {
  const { logout } = useAuth();
  const toast = useToast();
  const [open, setOpen] = React.useState(false);
  const [pw, setPw] = React.useState('');
  const [confirm, setConfirm] = React.useState('');
  const [busy, setBusy] = React.useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setBusy(true);
    try {
      await api('/api/auth/delete-account', { method: 'POST', body: { password: pw, confirm } });
      toast.push('Account deleted');
      try { localStorage.clear(); } catch {}
      navigate('/');
      window.location.reload();
    } catch (ex) {
      toast.push(ex.data?.error === 'wrong_password' ? 'Wrong password.' :
                 ex.data?.error === 'invalid_input' ? 'Type DELETE to confirm.' :
                 'Could not delete account', 'err');
    } finally { setBusy(false); }
  };

  if (!open) return (
    <button className="btn btn-ghost" onClick={() => setOpen(true)} style={{ color: 'var(--danger)' }}>
      <Icon name="x" size={11} color="var(--danger)" /> Delete my account
    </button>
  );
  return (
    <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{ background: '#fbeae6', border: '1px solid #e8b8a8', color: 'var(--danger)',
                    padding: '10px 12px', fontSize: 12, borderRadius: 2, lineHeight: 1.5 }}>
        <strong>This is permanent.</strong> All your draft and sent documents are removed from your view.
        Audit data is retained per legal retention policy. Signing links stop working immediately.
      </div>
      <div className="field"><label>Password</label>
        <input type="password" value={pw} onChange={e => setPw(e.target.value)} required autoFocus />
      </div>
      <div className="field"><label>Type <span className="mono">DELETE</span> to confirm</label>
        <input value={confirm} onChange={e => setConfirm(e.target.value)} required pattern="DELETE" />
      </div>
      <div style={{ display: 'flex', gap: 8 }}>
        <button type="submit" className="btn" disabled={busy || confirm !== 'DELETE'}
                style={{ background: 'var(--danger)', opacity: (busy || confirm !== 'DELETE') ? 0.5 : 1 }}>
          {busy ? 'Deleting…' : 'Permanently delete account'}
        </button>
        <button type="button" className="btn btn-ghost" onClick={() => { setOpen(false); setPw(''); setConfirm(''); }}>
          Cancel
        </button>
      </div>
    </form>
  );
};

// ---------- Error pages ----------
const NotFoundPage = () => (
  <div className="saign-app" style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32 }}>
    <div style={{ textAlign: 'center', maxWidth: 420 }}>
      <Logo size={16} />
      <h2 className="display" style={{ fontSize: 28, margin: '24px 0 8px' }}>Not found</h2>
      <p style={{ fontSize: 13, color: 'var(--ink-2)', margin: 0, marginBottom: 24 }}>That page doesn't exist or you don't have access.</p>
      <Link to="/dashboard" className="btn" style={{ textDecoration: 'none' }}>Back to documents</Link>
    </div>
  </div>
);

const ErrorTokenPage = () => (
  <div className="saign-app" style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32 }}>
    <div style={{ textAlign: 'center', maxWidth: 420 }}>
      <div style={{ width: 56, height: 56, margin: '0 auto 22px', borderRadius: '50%',
        background: 'var(--paper-2)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid var(--line)' }}>
        <Icon name="x" size={20} color="var(--danger)" />
      </div>
      <h2 className="display" style={{ fontSize: 24, margin: 0, marginBottom: 8 }}>This link is invalid or expired.</h2>
      <p style={{ fontSize: 13, color: 'var(--ink-2)', margin: 0, marginBottom: 24, lineHeight: 1.5 }}>
        Signing links are single‑use. Ask the sender to resend.
      </p>
      <Link to="/" className="btn-link">Back to home</Link>
    </div>
  </div>
);

// ---------- Router ----------
const Router = () => {
  const hash = useHash();
  const { path, seg } = parsePath(hash.split('?')[0]);

  if (path === '/' || path === '')      return <LandingPage />;
  if (path === '/login')                return <LoginPage />;
  if (path === '/register')             return <RegisterPage />;
  if (path.startsWith('/verify-email')) return <VerifyEmailPage />;
  if (path.startsWith('/verify-token')) return <VerifyTokenPage />;
  if (path.startsWith('/verify'))       return <VerifyPage />;
  if (path === '/privacy')              return <LegalPage which="privacy" />;
  if (path === '/terms')                return <LegalPage which="terms" />;
  if (path === '/cookies')              return <LegalPage which="cookies" />;
  if (path === '/subprocessors')        return <LegalPage which="subprocessors" />;
  if (path === '/security')             return <LegalPage which="security" />;

  if (seg[0] === 'sign' && seg[1]) {
    if (seg[2] === 'done') return <SignedReceiptPage token={seg[1]} />;
    return <PublicSignPage token={seg[1]} />;
  }

  // Protected from here
  const protectedRoute = () => {
    if (seg[0] === 'dashboard') {
      const f = seg[1];
      const filter = f === 'drafts' ? 'draft' :
                     (f === 'sent' || f === 'signed' || f === 'archived') ? f : 'all';
      return <DashboardPage filter={filter} />;
    }
    if (seg[0] === 'documents' && seg[1] === 'new') {
      if (!seg[2])              return <NewDocStep1 />;
      if (seg[2] === 'signers') return <NewDocStep2 />;
      if (seg[2] === 'fields')  return <NewDocStep3Fields />;
      if (seg[2] === 'review')  return <NewDocStep3 />;
    }
    if (seg[0] === 'documents' && seg[1]) {
      if (seg[2] === 'audit') return <AuditPage id={seg[1]} />;
      return <DocumentDetailPage id={seg[1]} />;
    }
    if (seg[0] === 'admin') {
      if (seg[1] === 'users') return <AdminUsersPage />;
      if (seg[1] === 'audit') return <AdminAuditPage />;
    }
    if (seg[0] === 'settings' || seg[0] === 'account') return <SettingsPage />;
    return <NotFoundPage />;
  };

  return <RequireAuth>{protectedRoute()}</RequireAuth>;
};

const App = () => (
  <AuthProvider>
    <ToastProvider>
      <Router />
      <CookieBanner />
    </ToastProvider>
  </AuthProvider>
);

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
