// frontend/auth.jsx — ClerkJS bootstrap + sign-in gate.
// Uses the no-bundler ClerkJS browser script (see index.html), initializes
// window.Clerk, and exposes:
//   - <AuthGate>: renders children if signed in, sign-in UI otherwise
//   - window.getAuthToken(): Promise<string|null> for api.jsx to attach Bearer
//   - window.clerkAppearance: shared theming object consumed by TopBar's
//     UserButton mount so the signed-in chrome matches the sign-in card.
//   - window.signOut(): wrapper around Clerk.signOut for imperative use.
//
// Auth is required only when window.__CLERK_PUBLISHABLE_KEY is set. If it's
// empty, the gate passes through (useful for local dev without Clerk).

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

const CLERK_KEY = window.__CLERK_PUBLISHABLE_KEY || "";
const AUTH_ENABLED = Boolean(CLERK_KEY);

// ─── Clerk appearance ────────────────────────────────────────────────
// Single source of truth for Clerk theming; reused for <SignIn/> and
// <UserButton/>. Variables map to Clerk's internal design tokens; elements
// override specific internal classes when variables aren't enough.
const clerkAppearance = {
  variables: {
    colorPrimary: "#FF5631",
    colorText: "#24140D",
    colorTextSecondary: "#6B5147",
    colorTextOnPrimaryBackground: "#F9FAF6",
    colorBackground: "#FFFFFF",
    colorInputBackground: "#F9FAF6",
    colorInputText: "#24140D",
    colorNeutral: "#24140D",
    colorDanger: "#DC2626",
    colorSuccess: "#16A34A",
    colorWarning: "#D97706",
    fontFamily: "'Instrument Sans', Arial, sans-serif",
    fontFamilyButtons: "'Instrument Sans', Arial, sans-serif",
    fontSize: "14px",
    fontWeight: { normal: 400, medium: 500, bold: 600 },
    borderRadius: "7px",
    spacingUnit: "1rem",
  },
  elements: {
    rootBox: { width: "100%" },
    card: {
      background: "#FFFFFF",
      border: "1px solid rgba(36,20,13,0.08)",
      borderRadius: "14px",
      boxShadow:
        "0 1px 2px rgba(36,20,13,0.04), 0 18px 48px -12px rgba(36,20,13,0.18)",
      padding: "36px 32px 28px",
    },
    headerTitle: {
      fontWeight: 500,
      fontSize: "22px",
      letterSpacing: "-0.015em",
      color: "#24140D",
    },
    headerSubtitle: { color: "#6B5147", fontSize: "13px" },
    formFieldLabel: {
      color: "#24140D",
      fontSize: "12px",
      fontWeight: 500,
      letterSpacing: "0.01em",
    },
    formFieldInput: {
      background: "#F9FAF6",
      border: "1px solid rgba(36,20,13,0.14)",
      borderRadius: "7px",
      color: "#24140D",
      padding: "10px 12px",
      fontSize: "14px",
      transition: "border-color 120ms ease, box-shadow 120ms ease",
      "&:hover": { borderColor: "rgba(36,20,13,0.24)" },
      "&:focus": {
        borderColor: "#FF5631",
        boxShadow: "0 0 0 3px rgba(255,86,49,0.15)",
        outline: "none",
      },
    },
    formButtonPrimary: {
      background: "#FF5631",
      color: "#F9FAF6",
      textTransform: "none",
      fontSize: "13px",
      fontWeight: 600,
      letterSpacing: "0.01em",
      borderRadius: "7px",
      padding: "11px 16px",
      boxShadow: "0 4px 16px rgba(255,86,49,0.28)",
      transition: "background 120ms ease, transform 120ms ease",
      "&:hover": { background: "#E84A28", boxShadow: "0 6px 20px rgba(255,86,49,0.32)" },
      "&:focus": { background: "#E84A28" },
      "&:active": { background: "#D04021", transform: "translateY(1px)" },
    },
    footer: { background: "transparent" },
    footerAction: { color: "#6B5147", fontSize: "13px" },
    footerActionText: { color: "#6B5147" },
    footerActionLink: {
      color: "#FF5631",
      fontWeight: 500,
      "&:hover": { color: "#D04021", textDecoration: "underline" },
    },
    identityPreviewEditButton: { color: "#FF5631" },
    formResendCodeLink: { color: "#FF5631" },
    socialButtonsBlockButton: {
      borderColor: "rgba(36,20,13,0.12)",
      color: "#24140D",
      "&:hover": { background: "#F9FAF6", borderColor: "rgba(36,20,13,0.20)" },
    },
    socialButtonsBlockButtonText: { color: "#24140D", fontWeight: 500 },
    dividerLine: { background: "rgba(36,20,13,0.10)" },
    dividerText: { color: "#9A8A82", fontSize: "11px" },
    alternativeMethodsBlockButton: {
      borderColor: "rgba(36,20,13,0.12)",
      color: "#24140D",
      "&:hover": { background: "#F9FAF6" },
    },
    logoBox: { display: "none" }, // custom Vecton logo is rendered above the card
    // UserButton surfaces
    userButtonAvatarBox: {
      width: "32px",
      height: "32px",
      boxShadow: "0 0 0 1px rgba(36,20,13,0.10)",
    },
    userButtonTrigger: {
      borderRadius: "9999px",
      "&:focus": { boxShadow: "0 0 0 3px rgba(255,86,49,0.20)" },
    },
    userButtonPopoverCard: {
      border: "1px solid rgba(36,20,13,0.08)",
      borderRadius: "12px",
      boxShadow: "0 18px 40px -10px rgba(36,20,13,0.20)",
    },
    userButtonPopoverActionButton: {
      color: "#24140D",
      "&:hover": { background: "#F9FAF6" },
    },
    userButtonPopoverActionButtonIcon: { color: "#6B5147" },
    userPreviewMainIdentifier: { color: "#24140D", fontWeight: 500 },
    userPreviewSecondaryIdentifier: { color: "#6B5147" },
  },
  layout: {
    logoPlacement: "none",
    socialButtonsPlacement: "top",
    socialButtonsVariant: "blockButton",
    showOptionalFields: true,
  },
};

window.clerkAppearance = clerkAppearance;

// Wait for Clerk to finish loading. The <script> tag in index.html boots
// Clerk asynchronously; we poll briefly until `Clerk.load()` resolves.
async function waitForClerk() {
  const start = Date.now();
  while (!window.Clerk || typeof window.Clerk.load !== "function") {
    if (Date.now() - start > 10000) throw new Error("Clerk failed to load");
    await new Promise(r => setTimeout(r, 50));
  }
  await window.Clerk.load({ appearance: clerkAppearance });
  return window.Clerk;
}

// Used by api.jsx on every fetch. Returns null when auth disabled so callers
// simply omit the Authorization header.
window.getAuthToken = async function getAuthToken() {
  if (!AUTH_ENABLED) return null;
  const clerk = await waitForClerk();
  if (!clerk.session) return null;
  return clerk.session.getToken();
};

window.signOut = async function signOut() {
  if (!AUTH_ENABLED) return;
  const clerk = await waitForClerk();
  await clerk.signOut();
};

// ─── Premium branded background ─────────────────────────────────────
// Three-phase ambient story that mirrors what the product actually does:
//   Phase A (0.0–3.5s): voice is captured — mic pulses, 28 waveform bars
//     animate with organic amplitudes, "Listening…" label breathes.
//   Phase B (3.5–7.5s): audio is transcribed — a panel fades in on the
//     left, four real transcript lines type in one by one (clipPath sweep)
//     with a blinking caret, echoing the actual DEMO_SALARIED transcript.
//   Phase C (7.5–13.5s): six real form fields (Full Name, PAN, Mobile,
//     Salary Bank, Gross Monthly, Loan Amount) fill with real values from
//     the salaried demo — packet launches from transcript → arc → scanline
//     sweep → value text fades in → green checkmark appears.
//   Phase D (13.5–15s): brief hold + cross-fade back to Phase A.
//
// Full cycle: 15s, looping indefinitely.
//
// Interactivity:
//   - Mouse-move parallax on the whole SVG via a requestAnimationFrame
//     loop that writes a transform to a single <g>; React doesn't re-render.
//   - prefers-reduced-motion swaps in a static composite (final frame of
//     Phase C with all fields filled).
//
// All looped motion is SMIL-driven (<animate>, <animateMotion>) so the main
// thread stays idle while the browser paints.
function SignInBackdrop() {
  const [animated, setAnimated] = useState(true);
  const parallaxRef = useRef(null);
  const pointerRef = useRef({ x: 0.5, y: 0.5 });

  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setAnimated(!mq.matches);
    update();
    (mq.addEventListener || mq.addListener).call(mq, "change", update);
    return () => (mq.removeEventListener || mq.removeListener).call(mq, "change", update);
  }, []);

  // Mouse parallax — throttled via rAF so it stays at 60fps on laptops.
  useEffect(() => {
    if (!animated) return;
    const onMove = (e) => {
      pointerRef.current.x = e.clientX / window.innerWidth;
      pointerRef.current.y = e.clientY / window.innerHeight;
    };
    window.addEventListener("mousemove", onMove);
    let raf;
    let cx = 0, cy = 0;
    const tick = () => {
      const tx = (pointerRef.current.x - 0.5) * 22;
      const ty = (pointerRef.current.y - 0.5) * 14;
      // Ease toward target — removes jitter and adds a tactile feel
      cx += (tx - cx) * 0.08;
      cy += (ty - cy) * 0.08;
      if (parallaxRef.current) {
        parallaxRef.current.setAttribute("transform", `translate(${cx.toFixed(2)} ${cy.toFixed(2)})`);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("mousemove", onMove);
    };
  }, [animated]);

  const CYCLE = 15; // seconds
  const DUR = `${CYCLE}s`;

  // ── Phase A: waveform bars ──────────────────────────────────────────
  // 28 bars across x=260..540, centered at y=420. Each bar has a short
  // per-bar <animate> (height + y) that repeats quickly within Phase A,
  // then damped to near-zero during Phases B/C via a second <animate> on
  // the cycle clock that modulates opacity.
  const BAR_COUNT = 28;
  const BAR_X0 = 240;
  const BAR_STEP = 11;
  // Pseudo-random but deterministic amplitudes — gives organic speech
  // cadence without needing to seed Math.random (which would desync on
  // re-renders). Three peak heights per bar, cycling every ~1.1s.
  const bars = useMemo(() => {
    const rng = (i, s) => {
      const x = Math.sin(i * 12.9898 + s * 78.233) * 43758.5453;
      return x - Math.floor(x);
    };
    return Array.from({ length: BAR_COUNT }, (_, i) => ({
      x: BAR_X0 + i * BAR_STEP,
      h0: 6 + rng(i, 0) * 14,
      h1: 18 + rng(i, 1) * 58,
      h2: 10 + rng(i, 2) * 38,
      h3: 22 + rng(i, 3) * 50,
      dur: 0.8 + rng(i, 4) * 0.6, // 0.8–1.4s per bar
      begin: -rng(i, 5) * 1.2,     // offset starting phase
    }));
  }, []);

  // ── Phase B: transcript lines ───────────────────────────────────────
  // Lines are drawn from the actual Rahul Sharma demo transcript so what
  // the user sees pre-login matches what the product produces post-login.
  const transcriptStart = 3.5;     // seconds into cycle
  const lineStagger = 0.7;         // each line reveals 0.7s after the previous
  const lineDur = 1.1;             // reveal duration per line
  const lines = [
    { text: "Rahul Sharma, 32, married", width: 220 },
    { text: "PAN A-B-C-D-E-1-2-3-4-F",   width: 230 },
    { text: "HDFC salary credit, 1.5L",  width: 210 },
    { text: "Loan 5L, 36 months",        width: 170 },
  ];

  // ── Phase C: fields filling ─────────────────────────────────────────
  // Six fields, real labels + real values. Packets launch from the
  // transcript panel (580, 720) and arrive at (1060, y). Each field
  // begins filling at phaseC_begin = 7.5s + i*0.85s, runs for 2.0s of
  // scan+reveal animation inside the 15s cycle.
  const phaseCBegin = 7.5;
  const fieldStagger = 0.85;
  const fieldFillDur = 2.0;
  const fields = [
    { label: "Full Name",     value: "Rahul Sharma",    y: 260, w: 200, mono: false },
    { label: "PAN",           value: "ABCDE1234F",      y: 350, w: 200, mono: true  },
    { label: "Mobile",        value: "+91 98765 43210", y: 440, w: 210, mono: true  },
    { label: "Salary Bank",   value: "HDFC Bank",       y: 530, w: 200, mono: false },
    { label: "Gross Monthly", value: "₹1,50,000",       y: 620, w: 200, mono: true  },
    { label: "Loan Amount",   value: "₹5,00,000",       y: 710, w: 200, mono: true  },
  ];

  // Helper: build a keyTimes/values pair that holds at 0, ramps 0→1 over
  // `dur` seconds starting at `start`, holds at 1 until `holdEnd`, then
  // fades back to 0 by `fadeEnd`. All times normalized to CYCLE.
  const ramp = (start, dur, holdEnd, fadeEnd) => {
    const s = start / CYCLE;
    const r = (start + dur) / CYCLE;
    const h = holdEnd / CYCLE;
    const f = fadeEnd / CYCLE;
    return {
      keyTimes: `0; ${s.toFixed(4)}; ${r.toFixed(4)}; ${h.toFixed(4)}; ${f.toFixed(4)}; 1`,
      values:   "0; 0; 1; 1; 0; 0",
    };
  };

  const phaseAFade = ramp(0.1, 0.4, 3.3, 3.7);   // bars/mic visible 0.1–3.7s
  const phaseBFade = ramp(3.5, 0.4, 7.3, 7.7);   // transcript visible 3.5–7.7s

  return (
    <div
      aria-hidden="true"
      style={{
        position: "fixed",
        inset: 0,
        overflow: "hidden",
        pointerEvents: "none",
        zIndex: 0,
      }}
    >
      {/* Base warm wash — off-white with beige undertone radials */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background:
            "radial-gradient(ellipse 900px 600px at 8% 12%, rgba(232,233,215,0.75) 0%, rgba(232,233,215,0) 60%)," +
            "radial-gradient(ellipse 1100px 700px at 92% 88%, rgba(232,233,215,0.65) 0%, rgba(232,233,215,0) 62%)," +
            "linear-gradient(180deg, #F9FAF6 0%, #F4F5EA 100%)",
        }}
      />

      {/* Center calm — keeps the card on a neutral field */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background:
            "radial-gradient(circle 500px at 50% 50%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 70%)",
        }}
      />

      {/* Orange accent glow — one sharp punctuation, bottom-left */}
      <div
        style={{
          position: "absolute",
          left: "-120px",
          bottom: "-140px",
          width: "520px",
          height: "520px",
          borderRadius: "50%",
          background:
            "radial-gradient(circle, rgba(255,86,49,0.14) 0%, rgba(255,86,49,0.04) 40%, transparent 70%)",
          filter: "blur(4px)",
        }}
      />

      <svg
        viewBox="0 0 1600 1000"
        preserveAspectRatio="xMidYMid slice"
        style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}
      >
        <defs>
          <linearGradient id="scanline" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0%"   stopColor="#FF5631" stopOpacity="0" />
            <stop offset="50%"  stopColor="#FF5631" stopOpacity="0.55" />
            <stop offset="100%" stopColor="#FF5631" stopOpacity="0" />
          </linearGradient>
          <linearGradient id="bar-grad" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%"   stopColor="#FF5631" stopOpacity="0.85" />
            <stop offset="100%" stopColor="#24140D" stopOpacity="0.55" />
          </linearGradient>
          <radialGradient id="packet-glow">
            <stop offset="0%"   stopColor="#FF5631" stopOpacity="1" />
            <stop offset="60%"  stopColor="#FF5631" stopOpacity="0.35" />
            <stop offset="100%" stopColor="#FF5631" stopOpacity="0" />
          </radialGradient>
          <radialGradient id="mic-pulse">
            <stop offset="0%"   stopColor="#FF5631" stopOpacity="0.5" />
            <stop offset="70%"  stopColor="#FF5631" stopOpacity="0.1" />
            <stop offset="100%" stopColor="#FF5631" stopOpacity="0" />
          </radialGradient>
          <pattern id="dotgrid" width="22" height="22" patternUnits="userSpaceOnUse">
            <circle cx="1.2" cy="1.2" r="1.2" fill="#24140D" fillOpacity="0.09" />
          </pattern>
          {/* Arc paths from transcript panel (580, 720) → each field's left edge.
              The path dips low then rises, so packets visibly travel "around"
              the sign-in card rather than behind it. */}
          {fields.map((f, i) => (
            <path
              key={i}
              id={`v2f-path-${i}`}
              d={`M 580 720 C 720 ${880 - i * 40}, 920 ${f.y + 40}, 1060 ${f.y + 13}`}
            />
          ))}
          <clipPath id="field-clip-generic">
            <rect x="0" y="0" width="220" height="30" rx="6" />
          </clipPath>
          {/* Per-line clip rects for the typewriter reveal on the transcript. */}
          {lines.map((l, i) => (
            <clipPath key={`lineclip-${i}`} id={`line-clip-${i}`}>
              <rect x="0" y="-14" width={l.width} height="22">
                {animated && (
                  <animate
                    attributeName="width"
                    dur={DUR}
                    values={`0; 0; ${l.width}; ${l.width}; 0; 0`}
                    keyTimes={(() => {
                      const s = (transcriptStart + i * lineStagger) / CYCLE;
                      const r = (transcriptStart + i * lineStagger + lineDur) / CYCLE;
                      const h = 7.3 / CYCLE;
                      const f = 7.7 / CYCLE;
                      return `0; ${s.toFixed(4)}; ${r.toFixed(4)}; ${h.toFixed(4)}; ${f.toFixed(4)}; 1`;
                    })()}
                    repeatCount="indefinite"
                  />
                )}
              </rect>
            </clipPath>
          ))}
        </defs>

        {/* Dot-grid panels — top-right + bottom-left, pure texture */}
        <rect x="1180" y="40" width="380" height="220" fill="url(#dotgrid)" />
        <rect x="40"   y="860" width="300" height="120" fill="url(#dotgrid)" />

        {/* Everything inside this group gets the mouse parallax transform. */}
        <g ref={parallaxRef}>

          {/* ── PHASE A: Mic + waveform bars + "Listening…" label ──────── */}
          <g opacity={animated ? 0 : 1}>
            {animated && (
              <animate
                attributeName="opacity"
                dur={DUR}
                values={phaseAFade.values}
                keyTimes={phaseAFade.keyTimes}
                repeatCount="indefinite"
              />
            )}

            {/* Mic glow halo */}
            <circle cx="170" cy="420" r="48" fill="url(#mic-pulse)">
              {animated && (
                <animate attributeName="r" values="42;58;42" dur="2.2s" repeatCount="indefinite" />
              )}
            </circle>
            {/* Outer pulsing ring */}
            <circle cx="170" cy="420" r="40" fill="none" stroke="#FF5631" strokeOpacity="0.35" strokeWidth="1.5">
              {animated && (
                <animate attributeName="r"       values="36;64" dur="2.2s" repeatCount="indefinite" />
              )}
              {animated && (
                <animate attributeName="opacity" values="0.55;0" dur="2.2s" repeatCount="indefinite" />
              )}
            </circle>
            {/* Mic body — capsule */}
            <g transform="translate(170 420)">
              <rect x="-10" y="-22" width="20" height="34" rx="10" fill="#24140D" fillOpacity="0.85" />
              <path d="M -18 4 A 18 18 0 0 0 18 4" stroke="#24140D" strokeOpacity="0.85" strokeWidth="2" fill="none" strokeLinecap="round" />
              <line x1="0" y1="22" x2="0" y2="32" stroke="#24140D" strokeOpacity="0.85" strokeWidth="2" strokeLinecap="round" />
              <line x1="-10" y1="32" x2="10" y2="32" stroke="#24140D" strokeOpacity="0.85" strokeWidth="2" strokeLinecap="round" />
            </g>

            {/* "Listening…" label with breathing dot */}
            <g transform="translate(218 376)">
              <circle cx="0" cy="0" r="3.5" fill="#FF5631">
                {animated && (
                  <animate attributeName="opacity" values="0.4;1;0.4" dur="1.3s" repeatCount="indefinite" />
                )}
              </circle>
              <text
                x="10"
                y="4"
                fontSize="12"
                fontWeight="500"
                letterSpacing="0.18em"
                fontFamily="'Instrument Sans', sans-serif"
                fill="#24140D"
                fillOpacity="0.7"
              >
                LISTENING
              </text>
            </g>

            {/* 28 waveform bars — each has its own oscillating height */}
            {bars.map((b, i) => (
              <g key={i}>
                <rect
                  x={b.x}
                  y={420 - b.h0 / 2}
                  width="5"
                  height={b.h0}
                  rx="2.5"
                  fill="url(#bar-grad)"
                  opacity="0.85"
                >
                  {animated && (
                    <animate
                      attributeName="height"
                      values={`${b.h0};${b.h1};${b.h2};${b.h3};${b.h0}`}
                      dur={`${b.dur}s`}
                      begin={`${b.begin}s`}
                      repeatCount="indefinite"
                    />
                  )}
                  {animated && (
                    <animate
                      attributeName="y"
                      values={`${420 - b.h0 / 2};${420 - b.h1 / 2};${420 - b.h2 / 2};${420 - b.h3 / 2};${420 - b.h0 / 2}`}
                      dur={`${b.dur}s`}
                      begin={`${b.begin}s`}
                      repeatCount="indefinite"
                    />
                  )}
                </rect>
              </g>
            ))}
          </g>

          {/* ── PHASE B: Transcript panel — typed lines ─────────────────── */}
          <g transform="translate(60 600)" opacity={animated ? 0 : 1}>
            {animated && (
              <animate
                attributeName="opacity"
                dur={DUR}
                values={phaseBFade.values}
                keyTimes={phaseBFade.keyTimes}
                repeatCount="indefinite"
              />
            )}

            {/* Panel chrome */}
            <rect
              x="0"
              y="0"
              width="500"
              height="180"
              rx="12"
              fill="#FFFFFF"
              fillOpacity="0.72"
              stroke="#24140D"
              strokeOpacity="0.10"
              strokeWidth="1"
            />
            {/* Header */}
            <g transform="translate(20 26)">
              <circle cx="0" cy="-4" r="3.5" fill="#16A34A">
                {animated && (
                  <animate attributeName="opacity" values="0.6;1;0.6" dur="1.8s" repeatCount="indefinite" />
                )}
              </circle>
              <text
                x="12"
                y="0"
                fontSize="10"
                fontWeight="500"
                letterSpacing="0.2em"
                fontFamily="'Instrument Sans', sans-serif"
                fill="#24140D"
                fillOpacity="0.55"
              >
                TRANSCRIPT
              </text>
              <text
                x="480"
                y="0"
                fontSize="10"
                fontWeight="500"
                fontFamily="'Instrument Sans', sans-serif"
                fill="#6B5147"
                textAnchor="end"
              >
                00:52
              </text>
            </g>

            {/* Each line is clipped by its own reveal rect so text appears
                left-to-right like a typewriter. A caret is drawn at the
                current reveal edge during the line's reveal window. */}
            {lines.map((l, i) => {
              const baseY = 70 + i * 26;
              const revealStart = transcriptStart + i * lineStagger;
              const revealEnd = revealStart + lineDur;
              return (
                <g key={i}>
                  {/* Faded ghost line — shows where text will land */}
                  <text
                    x="24"
                    y={baseY}
                    fontSize="14"
                    fontFamily="'Instrument Sans', sans-serif"
                    fill="#24140D"
                    fillOpacity="0.08"
                  >
                    {l.text}
                  </text>
                  {/* Revealed text — clipped by sweeping rect */}
                  <g clipPath={`url(#line-clip-${i})`} transform="translate(24 0)">
                    <text
                      x="0"
                      y={baseY}
                      fontSize="14"
                      fontFamily="'Instrument Sans', sans-serif"
                      fill="#24140D"
                      fillOpacity="0.82"
                    >
                      {l.text}
                    </text>
                  </g>
                  {/* Caret — blinks at the end of each revealed line during
                      its active window, then disappears. */}
                  {animated && (
                    <rect
                      x="0"
                      y={baseY - 12}
                      width="1.5"
                      height="16"
                      fill="#FF5631"
                      opacity="0"
                    >
                      <animate
                        attributeName="x"
                        dur={DUR}
                        keyTimes={`0; ${(revealStart / CYCLE).toFixed(4)}; ${(revealEnd / CYCLE).toFixed(4)}; 1`}
                        values={`24; 24; ${24 + l.width}; ${24 + l.width}`}
                        repeatCount="indefinite"
                      />
                      <animate
                        attributeName="opacity"
                        dur={DUR}
                        keyTimes={`0; ${(revealStart / CYCLE).toFixed(4)}; ${((revealStart + 0.05) / CYCLE).toFixed(4)}; ${(revealEnd / CYCLE).toFixed(4)}; ${((revealEnd + 0.2) / CYCLE).toFixed(4)}; 1`}
                        values="0; 0; 0.9; 0.9; 0; 0"
                        repeatCount="indefinite"
                      />
                    </rect>
                  )}
                </g>
              );
            })}
          </g>

          {/* Connector paths — dashed guides so the eye predicts trajectories */}
          <g stroke="#24140D" strokeOpacity="0.10" fill="none" strokeLinecap="round" strokeWidth="1" strokeDasharray="2 6" opacity={animated ? 0 : 0.8}>
            {animated && (
              <animate
                attributeName="opacity"
                dur={DUR}
                keyTimes={`0; ${(phaseCBegin / CYCLE).toFixed(4)}; ${((phaseCBegin + 0.5) / CYCLE).toFixed(4)}; ${(13.3 / CYCLE).toFixed(4)}; ${(13.8 / CYCLE).toFixed(4)}; 1`}
                values="0; 0; 0.8; 0.8; 0; 0"
                repeatCount="indefinite"
              />
            )}
            {fields.map((f, i) => (
              <path key={i} d={`M 580 720 C 720 ${880 - i * 40}, 920 ${f.y + 40}, 1060 ${f.y + 13}`} />
            ))}
          </g>

          {/* ── PHASE C: Fields filling with real demo values ────────────── */}
          {fields.map((f, i) => {
            const begin = phaseCBegin + i * fieldStagger;
            const scanStart = begin + 0.35;
            const scanEnd = scanStart + 0.5;
            const valueStart = scanEnd;
            const valueHold = 13.3;
            const fadeEnd = 13.8;
            const k = (t) => (t / CYCLE).toFixed(4);

            return (
              <g key={i} transform={`translate(1060 ${f.y})`}>
                {/* Label */}
                <text
                  x="2"
                  y="-8"
                  fontSize="10"
                  fontWeight="500"
                  letterSpacing="0.08em"
                  fontFamily="'Instrument Sans', sans-serif"
                  fill="#24140D"
                  fillOpacity={animated ? "0" : "0.55"}
                >
                  {f.label.toUpperCase()}
                  {animated && (
                    <animate
                      attributeName="fill-opacity"
                      dur={DUR}
                      keyTimes={`0; ${k(begin - 0.1)}; ${k(begin + 0.3)}; ${k(valueHold)}; ${k(fadeEnd)}; 1`}
                      values="0; 0; 0.55; 0.55; 0; 0"
                      repeatCount="indefinite"
                    />
                  )}
                </text>

                {/* Field outline */}
                <rect
                  x="0"
                  y="0"
                  width={f.w}
                  height="30"
                  rx="6"
                  fill="#FFFFFF"
                  fillOpacity={animated ? 0 : 0.72}
                  stroke="#24140D"
                  strokeOpacity={animated ? 0 : 0.18}
                  strokeWidth="1"
                >
                  {animated && (
                    <animate
                      attributeName="fill-opacity"
                      dur={DUR}
                      keyTimes={`0; ${k(begin - 0.1)}; ${k(begin + 0.3)}; ${k(valueHold)}; ${k(fadeEnd)}; 1`}
                      values="0; 0; 0.72; 0.72; 0; 0"
                      repeatCount="indefinite"
                    />
                  )}
                  {animated && (
                    <animate
                      attributeName="stroke-opacity"
                      dur={DUR}
                      keyTimes={`0; ${k(begin - 0.1)}; ${k(begin + 0.3)}; ${k(valueHold)}; ${k(fadeEnd)}; 1`}
                      values="0; 0; 0.18; 0.18; 0; 0"
                      repeatCount="indefinite"
                    />
                  )}
                </rect>

                {/* Scanline sweep */}
                <g clipPath="url(#field-clip-generic)">
                  <rect x="-60" y="0" width="70" height="30" fill="url(#scanline)" opacity="0">
                    {animated && (
                      <animate
                        attributeName="x"
                        dur={DUR}
                        keyTimes={`0; ${k(scanStart)}; ${k(scanEnd)}; 1`}
                        values={`-70; -70; ${f.w + 10}; ${f.w + 10}`}
                        repeatCount="indefinite"
                      />
                    )}
                    {animated && (
                      <animate
                        attributeName="opacity"
                        dur={DUR}
                        keyTimes={`0; ${k(scanStart)}; ${k(scanStart + 0.05)}; ${k(scanEnd)}; ${k(scanEnd + 0.05)}; 1`}
                        values="0; 0; 1; 1; 0; 0"
                        repeatCount="indefinite"
                      />
                    )}
                  </rect>
                </g>

                {/* Value text — fades in after scan passes */}
                <text
                  x="12"
                  y="20"
                  fontSize="13"
                  fontFamily={f.mono ? "ui-monospace, Menlo, Consolas, monospace" : "'Instrument Sans', sans-serif"}
                  fill="#24140D"
                  fillOpacity={animated ? 0 : 0.82}
                >
                  {f.value}
                  {animated && (
                    <animate
                      attributeName="fill-opacity"
                      dur={DUR}
                      keyTimes={`0; ${k(valueStart)}; ${k(valueStart + 0.35)}; ${k(valueHold)}; ${k(fadeEnd)}; 1`}
                      values="0; 0; 0.82; 0.82; 0; 0"
                      repeatCount="indefinite"
                    />
                  )}
                </text>

                {/* Arrival pulse — orange dot flashes when the packet lands */}
                <circle cx="0" cy="15" r="3" fill="#FF5631" fillOpacity="0">
                  {animated && (
                    <animate
                      attributeName="fill-opacity"
                      dur={DUR}
                      keyTimes={`0; ${k(scanStart - 0.05)}; ${k(scanStart)}; ${k(scanStart + 0.3)}; 1`}
                      values="0; 0; 1; 0; 0"
                      repeatCount="indefinite"
                    />
                  )}
                  {animated && (
                    <animate
                      attributeName="r"
                      dur={DUR}
                      keyTimes={`0; ${k(scanStart)}; ${k(scanStart + 0.3)}; 1`}
                      values="2; 8; 2; 2"
                      repeatCount="indefinite"
                    />
                  )}
                </circle>

                {/* Success checkmark — appears after value lands */}
                <g opacity={animated ? 0 : 1} transform={`translate(${f.w + 14} 15)`}>
                  <circle cx="0" cy="0" r="8" fill="#16A34A" fillOpacity="0.12" stroke="#16A34A" strokeOpacity="0.65" strokeWidth="1" />
                  <path d="M -3.5 0 L -1 2.5 L 3.5 -2.5" stroke="#16A34A" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round" />
                  {animated && (
                    <animate
                      attributeName="opacity"
                      dur={DUR}
                      keyTimes={`0; ${k(valueStart + 0.25)}; ${k(valueStart + 0.55)}; ${k(valueHold)}; ${k(fadeEnd)}; 1`}
                      values="0; 0; 1; 1; 0; 0"
                      repeatCount="indefinite"
                    />
                  )}
                </g>
              </g>
            );
          })}

          {/* ── Packets — one per field, flying transcript → field ──────── */}
          {animated && fields.map((f, i) => {
            const begin = phaseCBegin + i * fieldStagger;
            const flyDur = 0.35; // seconds in flight
            const k = (t) => (t / CYCLE).toFixed(4);
            return (
              <g key={`pkt-${i}`}>
                {/* Halo */}
                <circle r="8" fill="url(#packet-glow)" opacity="0">
                  <animateMotion
                    dur={DUR}
                    repeatCount="indefinite"
                    keyPoints="0;0;1;1"
                    keyTimes={`0; ${k(begin)}; ${k(begin + flyDur)}; 1`}
                    calcMode="linear"
                  >
                    <mpath href={`#v2f-path-${i}`} />
                  </animateMotion>
                  <animate
                    attributeName="opacity"
                    dur={DUR}
                    keyTimes={`0; ${k(begin - 0.05)}; ${k(begin)}; ${k(begin + flyDur - 0.05)}; ${k(begin + flyDur)}; 1`}
                    values="0; 0; 0.9; 0.9; 0; 0"
                    repeatCount="indefinite"
                  />
                </circle>
                {/* Solid core */}
                <circle r="3.5" fill="#FF5631" opacity="0">
                  <animateMotion
                    dur={DUR}
                    repeatCount="indefinite"
                    keyPoints="0;0;1;1"
                    keyTimes={`0; ${k(begin)}; ${k(begin + flyDur)}; 1`}
                    calcMode="linear"
                  >
                    <mpath href={`#v2f-path-${i}`} />
                  </animateMotion>
                  <animate
                    attributeName="opacity"
                    dur={DUR}
                    keyTimes={`0; ${k(begin - 0.05)}; ${k(begin)}; ${k(begin + flyDur - 0.05)}; ${k(begin + flyDur)}; 1`}
                    values="0; 0; 1; 1; 0; 0"
                    repeatCount="indefinite"
                  />
                </circle>
              </g>
            );
          })}

        </g>
      </svg>

      {/* Focus-corner brackets — signature Vecton motif */}
      <FocusCorner pos="tl" />
      <FocusCorner pos="tr" />
      <FocusCorner pos="bl" />
      <FocusCorner pos="br" />
    </div>
  );
}

function FocusCorner({ pos }) {
  const size = 22;
  const thick = 1.25;
  const edge = 40;
  const color = "rgba(36,20,13,0.28)";
  const styleMap = {
    tl: { top: edge, left: edge, borderTop: `${thick}px solid ${color}`, borderLeft: `${thick}px solid ${color}` },
    tr: { top: edge, right: edge, borderTop: `${thick}px solid ${color}`, borderRight: `${thick}px solid ${color}` },
    bl: { bottom: edge, left: edge, borderBottom: `${thick}px solid ${color}`, borderLeft: `${thick}px solid ${color}` },
    br: { bottom: edge, right: edge, borderBottom: `${thick}px solid ${color}`, borderRight: `${thick}px solid ${color}` },
  };
  return (
    <div
      style={{
        position: "absolute",
        width: size,
        height: size,
        ...styleMap[pos],
      }}
    />
  );
}

// Custom factor-two step. Replaces Clerk's pre-built widget for the
// email_code second-factor step, which ships an empty card for no clear
// reason (no inputs, no buttons — just a spinner). We drive the SDK
// directly: prepareSecondFactor on mount to send the code, OTP input
// below, attemptSecondFactor on submit.
function Factor2Step({ onBack }) {
  const signIn = window.Clerk.client.signIn;
  const factor = signIn.supportedSecondFactors?.find(f => f.strategy === "email_code");
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    // Only prepare if Clerk hasn't already sent the code for this attempt.
    const v = signIn.secondFactorVerification;
    if (v && v.status === "unverified") return;
    (async () => {
      try {
        await signIn.prepareSecondFactor({
          strategy: "email_code",
          emailAddressId: factor?.emailAddressId,
        });
      } catch (e) {
        setError(e?.errors?.[0]?.longMessage || e?.errors?.[0]?.message || String(e));
      }
    })();
  }, []);

  const verify = async () => {
    if (code.length < 4) { setError("Enter the code first"); return; }
    setBusy(true); setError("");
    try {
      const r = await signIn.attemptSecondFactor({ strategy: "email_code", code });
      if (r.createdSessionId) await window.Clerk.setActive({ session: r.createdSessionId });
    } catch (e) {
      setError(e?.errors?.[0]?.longMessage || e?.errors?.[0]?.message || String(e));
      setBusy(false);
    }
  };

  return (
    <div style={{
      background: "#fff",
      border: "1px solid rgba(36,20,13,0.08)",
      borderRadius: 14,
      boxShadow: "0 1px 2px rgba(36,20,13,0.04), 0 18px 48px -12px rgba(36,20,13,0.18)",
      padding: "32px 28px",
      width: "100%",
      fontFamily: "'Instrument Sans', Arial, sans-serif",
    }}>
      <h2 style={{ margin: "0 0 4px", fontSize: 22, fontWeight: 500, letterSpacing: "-0.015em", color: "#24140D" }}>
        Enter verification code
      </h2>
      <p style={{ margin: "0 0 20px", color: "#6B5147", fontSize: 13 }}>
        We sent a 6-digit code to {factor?.safeIdentifier || signIn.identifier}
      </p>
      <input
        value={code}
        onChange={(e) => setCode(e.target.value.replace(/\s/g, "").slice(0, 6))}
        onKeyDown={(e) => e.key === "Enter" && !busy && verify()}
        inputMode="numeric"
        autoComplete="one-time-code"
        maxLength={6}
        placeholder="123456"
        autoFocus
        style={{
          width: "100%",
          padding: "12px",
          fontSize: 18,
          letterSpacing: "0.4em",
          textAlign: "center",
          border: "1px solid rgba(36,20,13,0.14)",
          borderRadius: 7,
          background: "#F9FAF6",
          color: "#24140D",
          outline: "none",
          marginBottom: 12,
          fontFamily: "inherit",
          boxSizing: "border-box",
        }}
      />
      <button
        onClick={verify}
        disabled={busy}
        style={{
          width: "100%",
          padding: "11px",
          background: busy ? "#D04021" : "#FF5631",
          color: "#F9FAF6",
          border: 0,
          borderRadius: 7,
          fontWeight: 600,
          fontSize: 13,
          cursor: busy ? "default" : "pointer",
          boxShadow: "0 4px 16px rgba(255,86,49,0.28)",
          fontFamily: "inherit",
        }}
      >
        {busy ? "Verifying…" : "Verify"}
      </button>
      <div style={{ marginTop: 10, color: "#DC2626", fontSize: 12, minHeight: 16 }}>{error}</div>
      <button
        onClick={onBack}
        style={{
          marginTop: 4,
          background: "transparent",
          border: 0,
          color: "#6B5147",
          fontSize: 12,
          cursor: "pointer",
          padding: 0,
          fontFamily: "inherit",
        }}
      >
        ← Back to sign in
      </button>
    </div>
  );
}

function SignInScreen() {
  // Renders one of three UIs depending on Clerk's signIn/signUp state:
  //   - primary  : mount Clerk's <SignIn/> or <SignUp/> widget
  //   - factor2  : render our custom Factor2Step (Clerk's built-in step for
  //                email_code second factor ships empty; we call the SDK
  //                directly to unstick it)
  const ref = useRef(null);
  const [view, setView] = useState("signIn"); // signIn | signUp
  const [step, setStep] = useState("primary"); // primary | factor2

  // Watch Clerk's signIn status so we can flip to Factor2Step the moment the
  // first factor is accepted. Polling because Clerk's listener fires on
  // session/user changes, not on sign-in attempt transitions.
  //
  // Guard: AuthGate abandons stale attempts on mount, but if Clerk's internal
  // state hasn't flushed by the first tick we'd still see hydrated
  // `needs_second_factor` and render Factor2Step → trigger an unwanted email.
  // Require the poller to observe status as anything other than NSF at least
  // once before honoring a NSF transition. After abandon, status goes to null
  // first; only a real primary submission this session can take it back to NSF.
  // `armed` is a ref so backToSignIn can re-disarm and the next NSF observed
  // by the poller will be ignored until status clears again. (Without this,
  // hitting "Back" then re-submitting credentials would instantly bounce the
  // user back to factor2 because status is still NSF when the poll fires.)
  const armedRef = useRef(false);
  useEffect(() => {
    if (!window.Clerk) return;
    const tick = () => {
      const status = window.Clerk.client?.signIn?.status;
      if (!armedRef.current) {
        if (status !== "needs_second_factor") armedRef.current = true;
        setStep("primary");
        return;
      }
      setStep(status === "needs_second_factor" ? "factor2" : "primary");
    };
    tick();
    const id = setInterval(tick, 300);
    return () => clearInterval(id);
  }, []);

  useEffect(() => {
    if (step !== "primary" || !ref.current || !window.Clerk) return;
    const el = ref.current;
    const clerk = window.Clerk;
    const opts = {
      appearance: clerkAppearance,
      routing: "virtual",
      afterSignInUrl: "/",
      afterSignUpUrl: "/",
    };

    // Clerk retains the previous sign-in/sign-up attempt on window.Clerk.client
    // across sign-out. When mounting the primary widget, abandon any prior
    // attempt that isn't already complete — including `needs_second_factor`,
    // since reaching the primary view means the user backed out of factor2 or
    // is starting over.
    const stale = (attempt) =>
      attempt && attempt.status && attempt.status !== "complete";
    if (stale(clerk.client?.signIn)) {
      try { clerk.client.signIn.abandon?.(); } catch (_) {}
    }
    if (stale(clerk.client?.signUp)) {
      try { clerk.client.signUp.abandon?.(); } catch (_) {}
    }

    if (view === "signIn") {
      clerk.mountSignIn(el, opts);
    } else {
      clerk.mountSignUp(el, opts);
    }

    // Intercept clicks on Clerk's footer "Sign up" / "Sign in" anchors before
    // they trigger a full-page navigation; flip local view instead.
    const onClick = (e) => {
      const a = e.target.closest("a");
      if (!a) return;
      const href = a.getAttribute("href") || "";
      if (href.includes("sign-up")) { e.preventDefault(); setView("signUp"); }
      else if (href.includes("sign-in")) { e.preventDefault(); setView("signIn"); }
    };
    el.addEventListener("click", onClick, true);

    return () => {
      el.removeEventListener("click", onClick, true);
      try { view === "signIn" ? clerk.unmountSignIn(el) : clerk.unmountSignUp(el); } catch (_) {}
    };
  }, [view, step]);

  const backToSignIn = async () => {
    // Disarm the poller before abandon resolves so the next tick (which may
    // still see NSF for a frame) doesn't bounce us back to factor2.
    armedRef.current = false;
    try { await window.Clerk?.client?.signIn?.abandon?.(); } catch (_) {}
    try { await window.Clerk?.client?.signUp?.abandon?.(); } catch (_) {}
    if (location.hash && location.hash.includes("factor")) {
      history.replaceState(null, "", location.pathname + location.search);
    }
    setStep("primary");
  };
  return (
    <div
      style={{
        position: "relative",
        minHeight: "100vh",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        padding: "48px 24px",
        background: "#F9FAF6",
        fontFamily: "'Instrument Sans', Arial, sans-serif",
        overflow: "hidden",
      }}
    >
      <SignInBackdrop />
      <div
        style={{
          position: "relative",
          zIndex: 1,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 24,
          width: "100%",
          maxWidth: 420,
        }}
      >
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10 }}>
          <img
            src="./logo-full-light.svg"
            style={{ height: 30 }}
            alt="Vecton"
          />
          <div
            style={{
              fontSize: 11,
              fontWeight: 500,
              letterSpacing: "0.14em",
              textTransform: "uppercase",
              color: "#FF5631",
            }}
          >
            Voice-to-Form
          </div>
        </div>
        <div style={{ width: "100%" }}>
          {step === "factor2"
            ? <Factor2Step onBack={backToSignIn} />
            : <div ref={ref} style={{ width: "100%" }} />}
        </div>
        <div
          style={{
            fontSize: 11,
            color: "#9A8A82",
            letterSpacing: "0.04em",
            display: "flex",
            alignItems: "center",
            gap: 6,
          }}
        >
          <span
            style={{
              width: 6,
              height: 6,
              borderRadius: "50%",
              background: "#16A34A",
              boxShadow: "0 0 0 3px rgba(22,163,74,0.12)",
            }}
          />
          Secure internal workspace · Vecton
        </div>
      </div>
    </div>
  );
}

function AuthGate({ children }) {
  const [status, setStatus] = useState(AUTH_ENABLED ? "loading" : "signed-in");
  // Tracks whether the tab was previously signed in, so the listener can
  // distinguish "initial load, signed out" from "just signed out". Only the
  // latter should trigger a hard reload to flush Clerk's lingering client
  // state (stale sign-in attempt object, cached device trust token, etc.).
  const wasSignedInRef = useRef(false);

  useEffect(() => {
    if (!AUTH_ENABLED) return;
    let cancelled = false;
    (async () => {
      try {
        const clerk = await waitForClerk();
        if (cancelled) return;
        // ClerkJS persists the previous signIn/signUp attempt in the browser's
        // __client cookie. On a fresh page load with no active session, any
        // resumed attempt is stale by definition — including ones stuck at
        // `needs_second_factor` from a session when MFA was enabled. If we
        // don't abandon them here, the SignInScreen poller below sees the
        // resumed status and immediately renders Factor2Step, which calls
        // prepareSecondFactor and emails an OTP the user never asked for.
        if (!clerk.user) {
          const before = {
            si: clerk.client?.signIn?.status,
            su: clerk.client?.signUp?.status,
          };
          try { await clerk.client?.signIn?.abandon?.(); } catch (_) {}
          try { await clerk.client?.signUp?.abandon?.(); } catch (_) {}
          const after = {
            si: clerk.client?.signIn?.status,
            su: clerk.client?.signUp?.status,
          };
          if (before.si || before.su) {
            console.info("[auth] abandoned stale clerk attempts", { before, after });
          }
          // Strip stale Clerk hash routes (#/factor-two, #/sso-callback, etc.)
          // so the URL doesn't lie about what the gate is showing.
          if (location.hash && location.hash.includes("factor")) {
            history.replaceState(null, "", location.pathname + location.search);
          }
        }
        const sync = () => {
          const signedIn = Boolean(clerk.user);
          if (wasSignedInRef.current && !signedIn) {
            // Just signed out. Reload so the next sign-in starts from a
            // pristine Clerk client — avoids replaying stale sign-in
            // attempts into #/factor-two.
            window.location.reload();
            return;
          }
          wasSignedInRef.current = signedIn;
          setStatus(signedIn ? "signed-in" : "signed-out");
        };
        sync();
        clerk.addListener(sync);
      } catch (e) {
        console.error("clerk init failed", e);
        if (!cancelled) setStatus("error");
      }
    })();
    return () => { cancelled = true; };
  }, []);

  if (status === "loading") {
    return (
      <div
        style={{
          minHeight: "100vh",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          background: "#F9FAF6",
          fontFamily: "'Instrument Sans', Arial, sans-serif",
          color: "#6B5147",
          fontSize: 13,
        }}
      >
        Loading workspace…
      </div>
    );
  }
  if (status === "error") {
    return <div style={{ padding: 48, color: "crimson" }}>Auth failed to initialize. Check console.</div>;
  }
  if (status === "signed-out") {
    return <SignInScreen />;
  }
  return children;
}

window.AuthGate = AuthGate;
window.AUTH_ENABLED = AUTH_ENABLED;
