/* ============================================================
   Avatr FX — shared animated-component library (React Bits ports)
   Loaded as <script type="text/babel" src="/avatr-fx.jsx"> on both
   index.html and campaigns.html. Exposes window.FX.

   Brand rules baked in: monochrome (white on #0a0a0a), minimal,
   premium. Every effect degrades under prefers-reduced-motion.
   Self-contained: no external libraries (Threads uses raw WebGL).
   ============================================================ */
(function () {
  const { useRef, useEffect, useState } = React;

  const REDUCED =
    typeof window.matchMedia === "function" &&
    window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  // Inject shared keyframes / helper classes once.
  (function injectCSS() {
    if (document.getElementById("fx-styles")) return;
    const css = `
    .fx-shiny{
      background:linear-gradient(110deg,rgba(255,255,255,.62) 35%,#ffffff 50%,rgba(255,255,255,.62) 65%);
      background-size:220% 100%;
      -webkit-background-clip:text;background-clip:text;
      -webkit-text-fill-color:transparent;color:transparent;
      animation:fxShine 5s linear infinite;
    }
    .fx-shiny.fx-static{animation:none;-webkit-text-fill-color:#fff;color:#fff;background:none;}
    @keyframes fxShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

    .fx-spot{position:relative;overflow:hidden;}

    .fx-sb{position:relative;display:inline-block;overflow:hidden;border-radius:14px;}
    .fx-sb .fx-sb-line{position:absolute;width:300%;height:50%;opacity:.7;border-radius:50%;
      filter:blur(1px);z-index:0;pointer-events:none;}
    .fx-sb .fx-sb-bottom{bottom:-11px;right:-250%;
      background:radial-gradient(circle,#ffffff,transparent 12%);
      animation:fxSbBottom var(--fx-sb-speed,6s) linear infinite alternate;}
    .fx-sb .fx-sb-top{top:-11px;left:-250%;
      background:radial-gradient(circle,#ffffff,transparent 12%);
      animation:fxSbTop var(--fx-sb-speed,6s) linear infinite alternate;}
    .fx-sb .fx-sb-inner{position:relative;z-index:1;}
    @keyframes fxSbBottom{0%{transform:translateX(0)}100%{transform:translateX(-100%)}}
    @keyframes fxSbTop{0%{transform:translateX(0)}100%{transform:translateX(100%)}}
    .fx-sb.fx-static .fx-sb-line{animation:none;opacity:.25;}

    .fx-canvas-bg{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;}

    /* Custom dropdown — matches the UI (dark menu, portal-rendered) */
    .fx-dd-btn{ -webkit-appearance:none;appearance:none; display:inline-flex;align-items:center;justify-content:space-between;gap:10px;
      background-color:rgba(255,255,255,.05); border:1px solid var(--line); color:var(--ink);
      border-radius:999px; padding:8px 14px 8px 16px; font-size:12.5px; line-height:1.3; cursor:pointer;
      transition:border-color .15s ease, background-color .15s ease, box-shadow .15s ease; }
    .fx-dd-btn:hover{ border-color:rgba(255,255,255,.3); background-color:rgba(255,255,255,.09); }
    .fx-dd-btn:focus-visible{ outline:none; border-color:rgba(255,255,255,.6); box-shadow:0 0 0 3px rgba(255,255,255,.07); }
    .fx-dd-btn[disabled]{ opacity:.5; cursor:not-allowed; }
    .fx-dd-chev{ transition:transform .18s ease; flex:none; opacity:.7; }
    .fx-dd-open .fx-dd-chev{ transform:rotate(180deg); }
    .fx-dd-menu{ position:fixed; z-index:9999; background:#141414; border:1px solid rgba(255,255,255,.12);
      border-radius:14px; padding:5px; box-shadow:0 18px 50px rgba(0,0,0,.6); overflow:auto;
      animation:fxDDin .14s ease both; }
    @keyframes fxDDin{ from{opacity:0; transform:translateY(-4px);} to{opacity:1; transform:translateY(0);} }
    .fx-dd-opt{ display:flex;align-items:center;justify-content:space-between;gap:10px; width:100%;
      text-align:left; padding:8px 12px; border-radius:9px; font-size:13px; color:var(--ink);
      background:transparent; border:none; cursor:pointer; }
    .fx-dd-opt:hover{ background:rgba(255,255,255,.07); }
    .fx-dd-opt[data-sel="1"]{ background:rgba(255,255,255,.10); }
    .fx-dd-opt[data-sel="1"] .fx-dd-tick{ opacity:1; }
    .fx-dd-tick{ opacity:0; flex:none; }
    .fx-dd-empty{ padding:10px 12px; font-size:12px; color:var(--faint); }

    /* Import progress bar */
    .fx-prog-track{ height:6px; border-radius:999px; overflow:hidden; background:rgba(255,255,255,.08); }
    .fx-prog-fill{ height:100%; border-radius:999px; background:linear-gradient(90deg, var(--muted), #ffffff);
      transition:width .25s cubic-bezier(.2,.7,.2,1); }
    `;
    const el = document.createElement("style");
    el.id = "fx-styles";
    el.textContent = css;
    document.head.appendChild(el);
  })();

  // ----------------------------------------------------------
  // ShinyText — moving white sheen over text (CSS, no deps)
  // ----------------------------------------------------------
  function ShinyText({ text, children, className = "", as = "span", style = {} }) {
    const Tag = as;
    return (
      <Tag className={"fx-shiny " + (REDUCED ? "fx-static " : "") + className} style={style}>
        {text != null ? text : children}
      </Tag>
    );
  }

  // ----------------------------------------------------------
  // DecryptedText — scramble-in reveal (rAF/interval, no deps)
  // ----------------------------------------------------------
  const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&".split("");
  function DecryptedText({ text, className = "", style = {}, speed = 38 }) {
    const [out, setOut] = useState(REDUCED ? text : "");
    useEffect(() => {
      if (REDUCED) { setOut(text); return; }
      let frame = 0;
      const total = text.length;
      const id = setInterval(() => {
        frame++;
        const revealed = Math.floor(frame / 2);
        let s = "";
        for (let i = 0; i < total; i++) {
          if (i < revealed || text[i] === " ") s += text[i];
          else s += GLYPHS[(Math.random() * GLYPHS.length) | 0];
        }
        setOut(s);
        if (revealed >= total) clearInterval(id);
      }, speed);
      return () => clearInterval(id);
    }, [text]);
    return <span className={className} style={style}>{out || " "}</span>;
  }

  // ----------------------------------------------------------
  // CountUp — animate number into view (rAF + IntersectionObserver)
  // ----------------------------------------------------------
  function CountUp({ to, from = 0, duration = 1200, decimals = 0, prefix = "", suffix = "", className = "", style = {} }) {
    const ref = useRef(null);
    const [val, setVal] = useState(REDUCED ? to : from);
    const started = useRef(false);
    const lastTo = useRef(to);

    useEffect(() => {
      // When `to` changes after first run (live values), tween quickly to it.
      if (started.current && !REDUCED && to !== lastTo.current) {
        const a = lastTo.current, b = to, t0 = performance.now(), d = 420;
        let raf;
        const step = (now) => {
          const p = Math.min(1, (now - t0) / d);
          setVal(a + (b - a) * (1 - Math.pow(1 - p, 3)));
          if (p < 1) raf = requestAnimationFrame(step);
        };
        raf = requestAnimationFrame(step);
        lastTo.current = to;
        return () => cancelAnimationFrame(raf);
      }
      lastTo.current = to;
    }, [to]);

    useEffect(() => {
      if (REDUCED) { setVal(to); return; }
      const node = ref.current;
      if (!node) return;
      let raf;
      const run = () => {
        const t0 = performance.now();
        const step = (now) => {
          const p = Math.min(1, (now - t0) / duration);
          setVal(from + (to - from) * (1 - Math.pow(1 - p, 3)));
          if (p < 1) raf = requestAnimationFrame(step);
        };
        raf = requestAnimationFrame(step);
      };
      const io = new IntersectionObserver((es) => {
        es.forEach((e) => {
          if (e.isIntersecting && !started.current) { started.current = true; run(); }
        });
      }, { threshold: 0.3 });
      io.observe(node);
      return () => { io.disconnect(); cancelAnimationFrame(raf); };
    }, []);

    const shown = prefix + Number(val).toLocaleString("en-US", {
      minimumFractionDigits: decimals, maximumFractionDigits: decimals,
    }) + suffix;
    return <span ref={ref} className={className} style={style}>{shown}</span>;
  }

  // ----------------------------------------------------------
  // SpotlightCard — cursor-follow radial glow wrapper
  // ----------------------------------------------------------
  // Plain card wrapper — glow removed per brand direction (keep it solid/minimal).
  // Kept as a component so existing <FX.SpotlightCard> call-sites stay unchanged.
  function SpotlightCard({ children, className = "", style = {}, ...rest }) {
    return (
      <div className={"fx-spot " + className} style={style} {...rest}>{children}</div>
    );
  }

  // ----------------------------------------------------------
  // StarBorder — animated sweeping border around a CTA
  // ----------------------------------------------------------
  function StarBorder({ children, className = "", style = {}, speed = "6s" }) {
    return (
      <div className={"fx-sb " + (REDUCED ? "fx-static " : "") + className}
           style={{ "--fx-sb-speed": speed, ...style }}>
        <div className="fx-sb-line fx-sb-bottom" />
        <div className="fx-sb-line fx-sb-top" />
        <div className="fx-sb-inner">{children}</div>
      </div>
    );
  }

  // ----------------------------------------------------------
  // Magnet — translate children toward cursor on hover
  // ----------------------------------------------------------
  function Magnet({ children, className = "", strength = 14, style = {} }) {
    const ref = useRef(null);
    const [t, setT] = useState({ x: 0, y: 0 });
    if (REDUCED) return <div className={className} style={style}>{children}</div>;
    function onMove(e) {
      const el = ref.current; if (!el) return;
      const r = el.getBoundingClientRect();
      const dx = (e.clientX - (r.left + r.width / 2)) / (r.width / 2);
      const dy = (e.clientY - (r.top + r.height / 2)) / (r.height / 2);
      setT({ x: dx * strength, y: dy * strength });
    }
    return (
      <div ref={ref} className={className}
           onPointerMove={onMove} onPointerLeave={() => setT({ x: 0, y: 0 })}
           style={{ transform: `translate(${t.x}px,${t.y}px)`, transition: "transform .25s cubic-bezier(.2,.7,.2,1)", ...style }}>
        {children}
      </div>
    );
  }

  // ----------------------------------------------------------
  // ClickSpark — white sparks on click (canvas overlay, mount once)
  // ----------------------------------------------------------
  function ClickSpark() {
    const ref = useRef(null);
    useEffect(() => {
      if (REDUCED) return;
      const canvas = ref.current;
      const ctx = canvas.getContext("2d");
      let sparks = [], raf = null, dpr = Math.min(window.devicePixelRatio || 1, 2);
      function resize() {
        canvas.width = window.innerWidth * dpr;
        canvas.height = window.innerHeight * dpr;
        canvas.style.width = window.innerWidth + "px";
        canvas.style.height = window.innerHeight + "px";
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      }
      resize();
      window.addEventListener("resize", resize);
      function spawn(x, y) {
        const n = 8;
        for (let i = 0; i < n; i++) {
          const a = (Math.PI * 2 * i) / n;
          sparks.push({ x, y, a, life: 1, len: 11 });
        }
        if (!raf) raf = requestAnimationFrame(tick);
      }
      function tick() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        sparks = sparks.filter((s) => s.life > 0);
        sparks.forEach((s) => {
          s.life -= 0.045;
          const d = (1 - s.life) * 18;
          const x1 = s.x + Math.cos(s.a) * d;
          const y1 = s.y + Math.sin(s.a) * d;
          const x2 = x1 + Math.cos(s.a) * s.len * s.life;
          const y2 = y1 + Math.sin(s.a) * s.len * s.life;
          ctx.strokeStyle = `rgba(255,255,255,${s.life})`;
          ctx.lineWidth = 1.4;
          ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
        });
        if (sparks.length) raf = requestAnimationFrame(tick);
        else { raf = null; ctx.clearRect(0, 0, canvas.width, canvas.height); }
      }
      const onClick = (e) => spawn(e.clientX, e.clientY);
      window.addEventListener("click", onClick);
      return () => {
        window.removeEventListener("resize", resize);
        window.removeEventListener("click", onClick);
        if (raf) cancelAnimationFrame(raf);
      };
    }, []);
    return <canvas ref={ref} className="fx-canvas-bg"
      style={{ position: "fixed", inset: 0, zIndex: 60, pointerEvents: "none" }} aria-hidden="true" />;
  }

  // ----------------------------------------------------------
  // ThreadsBG — flowing white lines (OGL/WebGL). Static fallback.
  // ----------------------------------------------------------
  const THREADS_FRAG = `
    precision highp float;
    uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse; uniform float uAmp;
    float hash(float n){ return fract(sin(n)*43758.5453123); }
    float noise(vec2 p){
      vec2 i=floor(p); vec2 f=fract(p); f=f*f*(3.0-2.0*f);
      float n=i.x+i.y*57.0;
      return mix(mix(hash(n),hash(n+1.0),f.x), mix(hash(n+57.0),hash(n+58.0),f.x), f.y);
    }
    void main(){
      vec2 uv = gl_FragCoord.xy/uRes;
      float acc = 0.0;
      const int N = 30;
      for(int i=0;i<N;i++){
        float p = float(i)/float(N);
        float t = uTime*0.05;
        float amp = uAmp * smoothstep(0.0,0.55,uv.x) * (0.55+0.45*(1.0-p));
        float nz = noise(vec2(uv.x*3.0 + t + p*7.0, p*9.0 + t));
        float y = p + (nz-0.5)*amp + (uMouse.y-0.5)*0.03;
        float w = 0.0014 + 0.0042*uv.x;
        acc += smoothstep(w, 0.0, abs(uv.y - y)) * (0.45+0.55*(1.0-p));
      }
      float v = clamp(acc, 0.0, 1.0);
      gl_FragColor = vec4(vec3(1.0), v*0.55);
    }`;
  const THREADS_VERT = "attribute vec2 p; void main(){ gl_Position=vec4(p,0.0,1.0); }";
  const FALLBACK_BG = "radial-gradient(120% 90% at 100% 0%, rgba(255,255,255,.05), transparent 55%)";

  // Self-contained raw-WebGL threads — no external dependency. Falls back to a
  // static gradient when WebGL is unavailable or reduced-motion is requested.
  function ThreadsBG({ amplitude = 0.65, className = "", style = {} }) {
    const ref = useRef(null);
    useEffect(() => {
      const host = ref.current;
      if (!host) return;
      if (REDUCED) { host.style.background = FALLBACK_BG; return; }
      const canvas = document.createElement("canvas");
      canvas.style.width = "100%";
      canvas.style.height = "100%";
      host.appendChild(canvas);
      const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
      if (!gl) { host.style.background = FALLBACK_BG; try { host.removeChild(canvas); } catch (e) {} return; }

      let raf = null, running = true;
      const mouse = { x: 0.5, y: 0.5 }, sm = { x: 0.5, y: 0.5 };
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      try {
        const compile = (type, src) => {
          const s = gl.createShader(type);
          gl.shaderSource(s, src); gl.compileShader(s);
          if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
          return s;
        };
        const prog = gl.createProgram();
        gl.attachShader(prog, compile(gl.VERTEX_SHADER, THREADS_VERT));
        gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, THREADS_FRAG));
        gl.linkProgram(prog);
        if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));
        gl.useProgram(prog);

        const buf = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buf);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
        const loc = gl.getAttribLocation(prog, "p");
        gl.enableVertexAttribArray(loc);
        gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);

        const uTime = gl.getUniformLocation(prog, "uTime");
        const uRes = gl.getUniformLocation(prog, "uRes");
        const uMouse = gl.getUniformLocation(prog, "uMouse");
        gl.uniform1f(gl.getUniformLocation(prog, "uAmp"), amplitude);
        gl.clearColor(0, 0, 0, 0);
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

        const resize = () => {
          const w = host.clientWidth || window.innerWidth;
          const h = host.clientHeight || window.innerHeight;
          canvas.width = w * dpr; canvas.height = h * dpr;
          gl.viewport(0, 0, canvas.width, canvas.height);
        };
        resize();
        window.addEventListener("resize", resize);
        const onMove = (e) => {
          const r = host.getBoundingClientRect();
          mouse.x = (e.clientX - r.left) / r.width;
          mouse.y = 1 - (e.clientY - r.top) / r.height;
        };
        window.addEventListener("pointermove", onMove);

        const frame = (t) => {
          if (!running) { raf = null; return; }
          sm.x += (mouse.x - sm.x) * 0.05;
          sm.y += (mouse.y - sm.y) * 0.05;
          gl.uniform1f(uTime, t * 0.001);
          gl.uniform2f(uRes, canvas.width, canvas.height);
          gl.uniform2f(uMouse, sm.x, sm.y);
          gl.clear(gl.COLOR_BUFFER_BIT);
          gl.drawArrays(gl.TRIANGLES, 0, 3);
          raf = requestAnimationFrame(frame);
        };
        raf = requestAnimationFrame(frame);
        const onVis = () => { running = !document.hidden; if (running && !raf) raf = requestAnimationFrame(frame); };
        document.addEventListener("visibilitychange", onVis);

        return () => {
          running = false;
          if (raf) cancelAnimationFrame(raf);
          window.removeEventListener("resize", resize);
          window.removeEventListener("pointermove", onMove);
          document.removeEventListener("visibilitychange", onVis);
          try { host.removeChild(canvas); } catch (e) {}
        };
      } catch (e) {
        host.style.background = FALLBACK_BG;
        try { host.removeChild(canvas); } catch (_) {}
      }
    }, []);
    return <div ref={ref} className={"fx-canvas-bg " + className} style={style} aria-hidden="true" />;
  }

  // ----------------------------------------------------------
  // DotGridBG — cursor-reactive dot grid (canvas, light). For the
  // data-dense dialer where a full WebGL pass would be wasteful.
  // ----------------------------------------------------------
  function DotGridBG({ gap = 30, className = "", style = {} }) {
    const ref = useRef(null);
    useEffect(() => {
      const canvas = ref.current;
      if (!canvas) return;
      const ctx = canvas.getContext("2d");
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      let w = 0, h = 0, mouse = { x: -9999, y: -9999 }, raf = null;
      function resize() {
        w = canvas.parentElement.clientWidth || window.innerWidth;
        h = canvas.parentElement.clientHeight || window.innerHeight;
        canvas.width = w * dpr; canvas.height = h * dpr;
        canvas.style.width = w + "px"; canvas.style.height = h + "px";
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        draw();
      }
      function draw() {
        ctx.clearRect(0, 0, w, h);
        const R = 120;
        for (let x = gap; x < w; x += gap) {
          for (let y = gap; y < h; y += gap) {
            let o = 0.08, r = 1;
            if (!REDUCED) {
              const d = Math.hypot(x - mouse.x, y - mouse.y);
              if (d < R) { const k = 1 - d / R; o = 0.08 + k * 0.5; r = 1 + k * 1.4; }
            }
            ctx.fillStyle = "rgba(255,255,255," + o + ")";
            ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
          }
        }
      }
      function onMove(e) {
        const rct = canvas.getBoundingClientRect();
        mouse.x = e.clientX - rct.left; mouse.y = e.clientY - rct.top;
        if (!raf) raf = requestAnimationFrame(() => { raf = null; draw(); });
      }
      resize();
      window.addEventListener("resize", resize);
      if (!REDUCED) window.addEventListener("pointermove", onMove);
      return () => {
        window.removeEventListener("resize", resize);
        window.removeEventListener("pointermove", onMove);
        if (raf) cancelAnimationFrame(raf);
      };
    }, []);
    return <canvas ref={ref} className={"fx-canvas-bg " + className} style={style} aria-hidden="true" />;
  }

  // ----------------------------------------------------------
  // Dropdown — custom select that matches the UI. The menu is
  // portal-rendered (fixed) so no ancestor overflow can clip it,
  // and the OS-native option list is never shown.
  // ----------------------------------------------------------
  function Dropdown({ value, onChange, options = [], placeholder = "Select…",
                      className = "", style = {}, disabled = false, mono = false, block = false }) {
    const [open, setOpen] = useState(false);
    const [pos, setPos] = useState(null);
    const btnRef = useRef(null);
    const menuRef = useRef(null);
    const opts = options.map((o) => (typeof o === "string" ? { value: o, label: o } : o));
    const current = opts.find((o) => String(o.value) === String(value));

    function place() {
      const el = btnRef.current; if (!el) return;
      const r = el.getBoundingClientRect();
      const below = window.innerHeight - r.bottom;
      const above = r.top;
      const openUp = below < 220 && above > below;
      const maxHeight = Math.min(300, (openUp ? above : below) - 14);
      setPos({
        left: r.left, width: r.width,
        top: openUp ? undefined : r.bottom + 6,
        bottom: openUp ? (window.innerHeight - r.top + 6) : undefined,
        maxHeight,
      });
    }
    function toggle() { if (disabled) return; if (!open) place(); setOpen((o) => !o); }

    useEffect(() => {
      if (!open) return;
      const onDown = (e) => {
        if (menuRef.current && menuRef.current.contains(e.target)) return;
        if (btnRef.current && btnRef.current.contains(e.target)) return;
        setOpen(false);
      };
      const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
      const close = () => setOpen(false);
      document.addEventListener("pointerdown", onDown, true);
      document.addEventListener("keydown", onKey);
      window.addEventListener("scroll", close, true);
      window.addEventListener("resize", close);
      return () => {
        document.removeEventListener("pointerdown", onDown, true);
        document.removeEventListener("keydown", onKey);
        window.removeEventListener("scroll", close, true);
        window.removeEventListener("resize", close);
      };
    }, [open]);

    const menu = open && pos ? ReactDOM.createPortal(
      <div ref={menuRef} className="fx-dd-menu"
        style={{ left: pos.left, width: pos.width, top: pos.top, bottom: pos.bottom, maxHeight: pos.maxHeight }}
        role="listbox">
        {opts.length === 0 && <div className="fx-dd-empty">No options</div>}
        {opts.map((o) => (
          <button key={String(o.value)} type="button" role="option"
            aria-selected={String(o.value) === String(value)}
            className={"fx-dd-opt " + (mono ? "font-mono" : "")}
            data-sel={String(o.value) === String(value) ? "1" : "0"}
            onClick={() => { onChange(o.value); setOpen(false); }}>
            <span className="truncate">{o.label}</span>
            <span className="fx-dd-tick">✓</span>
          </button>
        ))}
      </div>, document.body) : null;

    return (
      <div className={(block ? "relative w-full " : "relative inline-block ") + className} style={style}>
        <button ref={btnRef} type="button" disabled={disabled} onClick={toggle}
          className={"fx-dd-btn w-full " + (open ? "fx-dd-open " : "") + (mono ? "font-mono " : "")}
          aria-haspopup="listbox" aria-expanded={open}>
          <span className="truncate" style={{ color: current ? "var(--ink)" : "var(--faint)" }}>
            {current ? current.label : placeholder}
          </span>
          <svg className="fx-dd-chev" width="14" height="14" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
            <polyline points="6 9 12 15 18 9" />
          </svg>
        </button>
        {menu}
      </div>
    );
  }

  // ----------------------------------------------------------
  // ImportProgress — stepped progress bar shown while the AI maps
  // spreadsheet columns. Simulated stepping (the call is one shot).
  // ----------------------------------------------------------
  function ImportProgress({ rowCount = 0, steps }) {
    const labels = steps || [
      rowCount ? `Reading ${rowCount.toLocaleString("en-US")} rows…` : "Reading rows…",
      "Detecting your columns…",
      "Matching them to the agent's variables…",
      "Validating phone numbers…",
      "Finalising the mapping…",
    ];
    const [i, setI] = useState(0);
    const [p, setP] = useState(6);
    useEffect(() => {
      const st = setInterval(() => setI((x) => Math.min(labels.length - 1, x + 1)), 950);
      const pt = setInterval(() => setP((x) => (x < 93 ? x + Math.max(0.6, (93 - x) * 0.05) : x)), 130);
      return () => { clearInterval(st); clearInterval(pt); };
    }, []);
    return (
      <div className="rounded-2xl border p-5" style={{ borderColor: "var(--line)", background: "var(--panel)" }}>
        <div className="flex items-center gap-2 mb-3">
          <span className="w-3.5 h-3.5 rounded-full border-2 animate-spin"
            style={{ borderColor: "rgba(255,255,255,.25)", borderTopColor: "#fff" }} />
          <span className="text-[10px] tracking-[0.3em]" style={{ color: "var(--faint)" }}>AVATR · ANALYZING</span>
        </div>
        <div className="text-[14px] mb-3" style={{ color: "var(--ink)" }}>{labels[i]}</div>
        <div className="fx-prog-track"><div className="fx-prog-fill" style={{ width: p + "%" }} /></div>
        <div className="mt-2 flex items-center justify-between text-[10px]" style={{ color: "var(--faint)" }}>
          <span className="tracking-[0.18em]">MATCHING COLUMNS → AGENT VARIABLES</span>
          <span className="font-mono">{Math.round(p)}%</span>
        </div>
      </div>
    );
  }

  window.FX = {
    REDUCED,
    ShinyText, DecryptedText, CountUp,
    SpotlightCard, StarBorder, Magnet,
    ClickSpark, ThreadsBG, DotGridBG,
    Dropdown, ImportProgress,
  };
})();
