/* ============================================================
   TAURIST — VISUALS
   Hero sunrise backdrop (photo + code light) and the four bespoke
   product-card canvas animations. One coherent motion language
   (build → settle), a distinct metaphor per product.
   Exports to window: HeroBackdrop, ProductCanvas, useReducedMotion.
   ============================================================ */
const { useRef, useEffect, useState } = React;

/* ---------- shared helpers ---------- */
function useReducedMotion() {
  const [reduced, setReduced] = useState(
    () => typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches
  );
  useEffect(() => {
    const mq = matchMedia("(prefers-reduced-motion: reduce)");
    const on = () => setReduced(mq.matches);
    mq.addEventListener("change", on);
    return () => mq.removeEventListener("change", on);
  }, []);
  return reduced;
}

const easeOut = (t) => 1 - Math.pow(1 - t, 3);
const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
const clamp01 = (t) => Math.max(0, Math.min(1, t));
const lerp = (a, b, t) => a + (b - a) * t;

function hx2rgb(hex) {
  const h = hex.replace("#", "");
  return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
}
function rgba(hex, a) {
  const [r, g, b] = hx2rgb(hex);
  return `rgba(${r},${g},${b},${a})`;
}
function rr(ctx, x, y, w, h, r) {
  r = Math.min(r, w / 2, h / 2);
  if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(x, y, w, h, r); return; }
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

/* a canvas hook that wires DPR scaling + ResizeObserver and hands a
   2d context to a per-frame painter. */
function useCanvas(painterRef, deps) {
  const ref = useRef(null);
  useEffect(() => {
    const cv = ref.current;
    if (!cv) return;
    const ctx = cv.getContext("2d");
    let w = 0, h = 0, dpr = 1, raf = 0;
    const measure = () => {
      const r = cv.getBoundingClientRect();
      dpr = Math.min(2, window.devicePixelRatio || 1);
      w = Math.max(1, r.width); h = Math.max(1, r.height);
      cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(cv);
    // immediate synchronous first paint (rAF may be throttled/paused)
    try { painterRef.current(ctx, w, h, performance.now()); } catch (e) {}
    const loop = (now) => {
      const cont = painterRef.current(ctx, w, h, now);
      if (cont !== false) raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => { cancelAnimationFrame(raf); ro.disconnect(); };
  }, deps); // eslint-disable-line
  return ref;
}

/* ============================================================
   HERO BACKDROP — photographic limb + travelling specular light
   ============================================================ */
function HeroBackdrop({ variant, accentMult, motionMult, reduced }) {
  // horizon curve for the moving light
  const painterRef = useRef(() => {});
  const start = useRef(performance.now());

  painterRef.current = (ctx, w, h, now) => {
    ctx.clearRect(0, 0, w, h);
    if (reduced || variant === "image") return false; // static, no loop
    const el = now - start.current;
    const horizonY = (x) => {
      const k = x / w - 0.5;
      return h * 0.86 - Math.cos(k * Math.PI) * h * 0.05;
    };

    if (variant === "abstract") {
      // draw a code-generated luminous horizon arc
      ctx.save();
      ctx.lineCap = "round";
      const grad = ctx.createLinearGradient(0, 0, w, 0);
      grad.addColorStop(0.0, rgba("#7C3CE0", 0.0));
      grad.addColorStop(0.18, rgba("#7C3CE0", 0.55 * accentMult));
      grad.addColorStop(0.42, rgba("#F222B0", 0.8 * accentMult));
      grad.addColorStop(0.6, rgba("#F22248", 0.9 * accentMult));
      grad.addColorStop(0.8, rgba("#F26422", 0.95 * accentMult));
      grad.addColorStop(1.0, rgba("#FFD9A0", 0.7 * accentMult));
      ctx.strokeStyle = grad;
      ctx.shadowColor = rgba("#F26422", 0.5 * accentMult);
      ctx.shadowBlur = 34;
      ctx.lineWidth = 2.4;
      ctx.beginPath();
      for (let x = 0; x <= w; x += 6) {
        const y = horizonY(x);
        x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
      }
      ctx.stroke();
      ctx.restore();
    }

    // travelling specular hotspot along the horizon
    const speed = 0.00007 * motionMult;
    const phase = Math.sin(el * speed * Math.PI) * 0.5 + 0.5;
    const hxp = w * (0.12 + 0.76 * phase);
    const hyp = horizonY(hxp);
    const R = w * 0.3;
    const g = ctx.createRadialGradient(hxp, hyp, 0, hxp, hyp, R);
    const a = accentMult;
    g.addColorStop(0, rgba("#FFE8C2", 0.5 * a));
    g.addColorStop(0.28, rgba("#F2843C", 0.26 * a));
    g.addColorStop(0.6, rgba("#F22248", 0.08 * a));
    g.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, w, h);
    return true;
  };

  const canvasRef = useCanvas(painterRef, [variant, accentMult, motionMult, reduced]);

  const showPhoto = variant !== "abstract";
  return (
    React.createElement("div", { className: "hero-stage", "aria-hidden": "true" },
      React.createElement("div", { className: "hero-planet" }),
      React.createElement("div", {
        className: "hero-sky",
        style: showPhoto ? undefined : { display: "none" },
      }),
      React.createElement("div", { className: "hero-bloom" }),
      React.createElement("canvas", { ref: canvasRef, className: "hero-canvas" }),
      React.createElement("div", { className: "hero-grain" })
    )
  );
}

/* ============================================================
   PRODUCT CANVAS — build-once-then-settle engine + 4 painters
   ============================================================ */
function ProductCanvas({ kind, accent, active, reduced, motionMult, playToken }) {
  const phaseRef = useRef({ p: 0, played: false, t0: 0 });
  const painterRef = useRef(() => {});
  const liveAccent = useRef(accent);
  liveAccent.current = accent;

  // build duration scales gently with the motion tweak
  const dur = 2200 * (0.7 + 0.5 * motionMult);

  // replay the build whenever the play token changes (hover / focus)
  useEffect(() => {
    if (!reduced) phaseRef.current = { p: 0, played: false, t0: 0 };
  }, [playToken, reduced]);

  painterRef.current = (ctx, w, h, now) => {
    const st = phaseRef.current;

    if (reduced) {
      // static finished state, drawn once
      ctx.clearRect(0, 0, w, h);
      PAINTERS[kind](ctx, w, h, 1, 0, liveAccent.current, 0.4);
      return false;
    }

    // delta clock for the ambient idle motion
    const dt = st.last ? now - st.last : 0;
    st.last = now;

    // advance build progress only while active (and not yet completed)
    if (active && !st.played) {
      if (!st.t0) st.t0 = now;
      st.p = clamp01((now - st.t0) / dur);
      if (st.p >= 1) st.played = true;
    } else if (!active && !st.played) {
      // gently relax back toward 0 if it scrolls away before completing
      st.p = Math.max(0, st.p - 0.012);
      st.t0 = 0;
    }

    // the ambient idle clock only ticks while this card is the active (centred /
    // hovered) one — off-centre cards freeze on their last frame instead of looping
    if (active) st.idle = (st.idle || 0) + dt;
    const idle = (st.idle || 0) * 0.001;
    const intensity = active || st.played ? 1 : 0.45;
    ctx.clearRect(0, 0, w, h);
    PAINTERS[kind](ctx, w, h, easeOut(st.p), idle * motionMult, liveAccent.current, intensity);
    return true;
  };

  const ref = useCanvas(painterRef, [kind, active, reduced, motionMult]);
  return React.createElement("canvas", { ref });
}

/* ---------- coordinate frame: paint inside a centred safe box ---------- */
function frame(w, h, pad) {
  const m = Math.min(w, h) * (pad ?? 0.13);
  return { x: m, y: m, w: w - m * 2, h: h - m * 2, cx: w / 2, cy: h / 2, s: Math.min(w, h) };
}

/* ===== SITEWORKS — assembly: wireframe blocks snap into a page ===== */
function paintAssemble(ctx, w, h, p, idle, accent, inten) {
  const f = frame(w, h, 0.12);
  // browser shell
  const bw = Math.min(f.w, f.h * 1.36);
  const bh = bw * 0.7;
  const bx = f.cx - bw / 2;
  const by = f.cy - bh / 2;
  const shell = easeOut(clamp01(p / 0.18));
  ctx.save();
  ctx.globalAlpha = shell;
  rr(ctx, bx, by, bw, bh, 12);
  ctx.fillStyle = "rgba(255,255,255,0.025)";
  ctx.fill();
  ctx.lineWidth = 1;
  ctx.strokeStyle = "rgba(255,255,255,0.14)";
  ctx.stroke();
  // top bar + dots
  const barH = bh * 0.1;
  ctx.fillStyle = "rgba(255,255,255,0.05)";
  rr(ctx, bx, by, bw, barH, 12); ctx.fill();
  for (let i = 0; i < 3; i++) {
    ctx.beginPath();
    ctx.arc(bx + barH * 0.55 + i * barH * 0.42, by + barH / 2, barH * 0.12, 0, 7);
    ctx.fillStyle = i === 0 ? rgba(accent, 0.8) : "rgba(255,255,255,0.18)";
    ctx.fill();
  }
  ctx.restore();

  // content blocks (target rects in browser space) + appear thresholds
  const pad = bw * 0.06;
  const ix = bx + pad, iw = bw - pad * 2;
  const iy = by + barH + bh * 0.06;
  const blocks = [
    { x: ix, y: iy, w: iw * 0.46, h: bh * 0.14, a: 0.2, fill: true },                 // headline
    { x: ix, y: iy + bh * 0.18, w: iw * 0.7, h: bh * 0.06, a: 0.32 },                 // subline
    { x: ix + iw * 0.55, y: iy, w: iw * 0.45, h: bh * 0.24, a: 0.45, hero: true },    // hero media
    { x: ix, y: iy + bh * 0.3, w: iw * 0.3, h: bh * 0.26, a: 0.58, card: true },      // card 1
    { x: ix + iw * 0.35, y: iy + bh * 0.3, w: iw * 0.3, h: bh * 0.26, a: 0.66, card: true },
    { x: ix + iw * 0.7, y: iy + bh * 0.3, w: iw * 0.3, h: bh * 0.26, a: 0.74, card: true },
    { x: ix, y: iy + bh * 0.62, w: iw, h: bh * 0.08, a: 0.84, foot: true },           // footer
  ];

  blocks.forEach((b) => {
    const local = clamp01((p - b.a) / 0.16);
    if (local <= 0) return;
    const e = easeOut(local);
    const dy = (1 - e) * bh * 0.08;
    const al = e;
    ctx.save();
    ctx.globalAlpha = al;
    rr(ctx, b.x, b.y + dy, b.w, b.h, 5);
    if (b.hero) {
      const gg = ctx.createLinearGradient(b.x, b.y, b.x + b.w, b.y + b.h);
      gg.addColorStop(0, rgba(accent, 0.42 * (0.5 + 0.5 * e)));
      gg.addColorStop(1, rgba(accent, 0.08));
      ctx.fillStyle = gg;
    } else if (b.fill) {
      ctx.fillStyle = "rgba(255,255,255,0.82)";
    } else if (b.foot) {
      ctx.fillStyle = "rgba(255,255,255,0.05)";
    } else {
      ctx.fillStyle = "rgba(255,255,255,0.10)";
    }
    ctx.fill();
    // wireframe → solid: stroke fades as block settles
    ctx.lineWidth = 1;
    ctx.strokeStyle = rgba(accent, 0.5 * (1 - e));
    ctx.stroke();
    if (b.card) {
      ctx.globalAlpha = al * 0.7;
      ctx.fillStyle = "rgba(255,255,255,0.18)";
      rr(ctx, b.x + b.w * 0.12, b.y + dy + b.h * 0.66, b.w * 0.5, b.h * 0.12, 3); ctx.fill();
    }
    ctx.restore();
  });

  // conversion highlight sweeps through once assembled
  const sweep = clamp01((p - 0.86) / 0.14);
  if (sweep > 0 || p >= 1) {
    const t = p >= 1 ? (0.5 + 0.5 * Math.sin(idle * 0.8)) : easeInOut(sweep);
    const ly = by + barH + (bh - barH) * (0.08 + 0.84 * t);
    ctx.save();
    const lg = ctx.createLinearGradient(bx, 0, bx + bw, 0);
    lg.addColorStop(0, rgba(accent, 0));
    lg.addColorStop(0.5, rgba(accent, 0.85 * inten));
    lg.addColorStop(1, rgba(accent, 0));
    ctx.strokeStyle = lg;
    ctx.shadowColor = rgba(accent, 0.8);
    ctx.shadowBlur = 14;
    ctx.lineWidth = 2;
    ctx.beginPath(); ctx.moveTo(bx + 6, ly); ctx.lineTo(bx + bw - 6, ly); ctx.stroke();
    ctx.restore();
  }
}

/* ===== WORKERS — task flow into a calm operations hub ===== */
const TASK_GLYPHS = ["mail", "cal", "crm", "loop", "doc"];
function drawGlyph(ctx, type, x, y, s, col) {
  ctx.save();
  ctx.translate(x, y);
  ctx.strokeStyle = col; ctx.lineWidth = 1.4; ctx.lineCap = "round"; ctx.lineJoin = "round";
  const u = s * 0.5;
  if (type === "mail") {
    ctx.strokeRect(-u, -u * 0.7, s, s * 0.66);
    ctx.beginPath(); ctx.moveTo(-u, -u * 0.7); ctx.lineTo(0, 0); ctx.lineTo(u, -u * 0.7); ctx.stroke();
  } else if (type === "cal") {
    ctx.strokeRect(-u, -u * 0.6, s, s * 0.8);
    ctx.beginPath(); ctx.moveTo(-u, -u * 0.25); ctx.lineTo(u, -u * 0.25); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(-u * 0.5, -u * 0.7); ctx.lineTo(-u * 0.5, -u * 0.4);
    ctx.moveTo(u * 0.5, -u * 0.7); ctx.lineTo(u * 0.5, -u * 0.4); ctx.stroke();
  } else if (type === "crm") {
    ctx.beginPath(); ctx.arc(0, -u * 0.15, u * 0.4, 0, 7); ctx.stroke();
    ctx.beginPath(); ctx.arc(0, u * 0.55, u * 0.7, Math.PI * 1.15, Math.PI * 1.85); ctx.stroke();
  } else if (type === "loop") {
    ctx.beginPath(); ctx.arc(0, 0, u * 0.6, Math.PI * 0.4, Math.PI * 2.1); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(u * 0.55, -u * 0.35); ctx.lineTo(u * 0.62, u * 0.05); ctx.lineTo(u * 0.2, -u * 0.05); ctx.stroke();
  } else {
    ctx.strokeRect(-u * 0.7, -u * 0.8, s * 0.7, s); 
    ctx.beginPath();
    ctx.moveTo(-u * 0.45, -u * 0.4); ctx.lineTo(u * 0.1, -u * 0.4);
    ctx.moveTo(-u * 0.45, -u * 0.05); ctx.lineTo(u * 0.1, -u * 0.05);
    ctx.moveTo(-u * 0.45, u * 0.3); ctx.lineTo(-u * 0.05, u * 0.3); ctx.stroke();
  }
  ctx.restore();
}
function paintFlow(ctx, w, h, p, idle, accent, inten) {
  const f = frame(w, h, 0.12);
  // hub in the centre-right
  const hub = { w: f.w * 0.3, h: f.h * 0.74, x: f.x + f.w * 0.4, y: f.y + f.h * 0.13 };
  const appear = easeOut(clamp01(p / 0.2));
  ctx.save();
  ctx.globalAlpha = appear;
  rr(ctx, hub.x, hub.y, hub.w, hub.h, 14);
  const hg = ctx.createLinearGradient(hub.x, hub.y, hub.x, hub.y + hub.h);
  hg.addColorStop(0, "rgba(255,255,255,0.05)");
  hg.addColorStop(1, "rgba(255,255,255,0.015)");
  ctx.fillStyle = hg; ctx.fill();
  ctx.lineWidth = 1; ctx.strokeStyle = rgba(accent, 0.35); ctx.stroke();
  ctx.restore();

  // 5 tasks: travel in from left along a lane, enter hub, settle as done rows on the right
  const lanes = 5;
  const laneGap = hub.h / (lanes + 0.4);
  const startX = f.x;
  const enterX = hub.x;
  const doneX = hub.x + hub.w * 0.12;
  for (let i = 0; i < lanes; i++) {
    const yc = hub.y + laneGap * (i + 0.7);
    const stagger = i * 0.1;
    // continuous progress: each task loops in slowly even after build (continuity)
    const base = clamp01((p - stagger) / (1 - stagger));
    const loop = (idle * 0.16 + i * 0.21) % 1;
    const travel = p < 1 ? easeInOut(base) : loop;
    const settled = p < 1 ? base : 1; // when built, rows shown as done permanently
    // connector lane
    ctx.save();
    ctx.globalAlpha = appear * 0.8;
    ctx.strokeStyle = "rgba(255,255,255,0.08)";
    ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(startX, yc); ctx.lineTo(enterX, yc); ctx.stroke();
    ctx.restore();

    // done row inside hub
    if (settled > 0.2) {
      const ra = easeOut(clamp01((settled - 0.2) / 0.6));
      ctx.save();
      ctx.globalAlpha = ra;
      rr(ctx, doneX, yc - laneGap * 0.3, hub.w * 0.76, laneGap * 0.6, 6);
      ctx.fillStyle = "rgba(255,255,255,0.06)"; ctx.fill();
      // check
      const ck = doneX + hub.w * 0.66;
      ctx.strokeStyle = rgba(accent, 0.95); ctx.lineWidth = 1.6; ctx.lineCap = "round";
      ctx.beginPath();
      ctx.moveTo(ck, yc); ctx.lineTo(ck + laneGap * 0.12, yc + laneGap * 0.14);
      ctx.lineTo(ck + laneGap * 0.34, yc - laneGap * 0.16); ctx.stroke();
      ctx.restore();
    }

    // travelling token (in-flight)
    if (p < 1 || true) {
      const tx = lerp(startX + 8, enterX - 6, p < 1 ? travel : loop);
      const fade = p < 1 ? appear : 0.5 + 0.5 * Math.sin(loop * Math.PI);
      ctx.save();
      ctx.globalAlpha = fade * inten;
      rr(ctx, tx - laneGap * 0.34, yc - laneGap * 0.34, laneGap * 0.68, laneGap * 0.68, 6);
      ctx.fillStyle = "rgba(12,12,14,0.95)";
      ctx.fill();
      ctx.strokeStyle = rgba(accent, 0.55); ctx.lineWidth = 1; ctx.stroke();
      drawGlyph(ctx, TASK_GLYPHS[i % TASK_GLYPHS.length], tx, yc, laneGap * 0.42, rgba(accent, 0.95));
      ctx.restore();
    }
  }
  // hub pulse — calm heartbeat of a well-run operation
  const pulse = 0.5 + 0.5 * Math.sin(idle * 1.1);
  ctx.save();
  ctx.globalAlpha = appear * (0.3 + 0.3 * pulse) * inten;
  ctx.strokeStyle = rgba(accent, 0.5);
  ctx.lineWidth = 1.4;
  rr(ctx, hub.x - 3, hub.y - 3, hub.w + 6, hub.h + 6, 16);
  ctx.stroke();
  ctx.restore();
}

/* ===== CONVERSIONWORKS — scattered signals resolve to one clear path ===== */
function paintClarify(ctx, w, h, p, idle, accent, inten) {
  const f = frame(w, h, 0.13);
  const A = { x: f.x, y: f.y + f.h * 0.82 };
  const B = { x: f.x + f.w, y: f.y + f.h * 0.18 };
  // clean target path (S-curve) control points
  const path = (t) => {
    const c1 = { x: f.x + f.w * 0.32, y: f.y + f.h * 0.86 };
    const c2 = { x: f.x + f.w * 0.62, y: f.y + f.h * 0.1 };
    const mt = 1 - t;
    return {
      x: mt * mt * mt * A.x + 3 * mt * mt * t * c1.x + 3 * mt * t * t * c2.x + t * t * t * B.x,
      y: mt * mt * mt * A.y + 3 * mt * mt * t * c1.y + 3 * mt * t * t * c2.y + t * t * t * B.y,
    };
  };

  // scattered signal dots that migrate onto the path
  const N = 26;
  for (let i = 0; i < N; i++) {
    const seed = i * 12.9898;
    const sx = f.x + (Math.sin(seed) * 0.5 + 0.5) * f.w;
    const sy = f.y + (Math.cos(seed * 1.7) * 0.5 + 0.5) * f.h;
    const tt = i / (N - 1);
    const onPath = path(tt);
    const conv = easeInOut(clamp01((p - 0.2 - tt * 0.25) / 0.4));
    const x = lerp(sx, onPath.x, conv);
    const y = lerp(sy, onPath.y, conv);
    const r = lerp(1.6, 2.4, conv) * (f.s / 380);
    ctx.beginPath();
    ctx.arc(x, y, r, 0, 7);
    ctx.fillStyle = conv > 0.5 ? rgba(accent, 0.9) : "rgba(255,255,255,0.28)";
    ctx.fill();
  }

  // tangled candidate paths fade out as clarity emerges
  const tangle = 1 - easeInOut(clamp01((p - 0.25) / 0.4));
  if (tangle > 0.01) {
    ctx.save();
    ctx.globalAlpha = tangle * 0.5;
    ctx.strokeStyle = "rgba(255,255,255,0.16)";
    ctx.lineWidth = 1;
    for (let k = 0; k < 3; k++) {
      ctx.beginPath();
      ctx.moveTo(A.x, A.y);
      ctx.bezierCurveTo(
        f.x + f.w * (0.2 + k * 0.18), f.y + f.h * (0.2 + k * 0.22),
        f.x + f.w * (0.5 - k * 0.12), f.y + f.h * (0.7 - k * 0.2),
        B.x, B.y
      );
      ctx.stroke();
    }
    ctx.restore();
  }

  // friction marker, flagged mid-build then resolved
  const fr = path(0.46);
  const frShow = easeOut(clamp01((p - 0.32) / 0.2)) * (1 - easeInOut(clamp01((p - 0.62) / 0.2)));
  if (frShow > 0.01) {
    const pl = 0.5 + 0.5 * Math.sin(idle * 4);
    ctx.save();
    ctx.globalAlpha = frShow;
    ctx.strokeStyle = rgba("#F22248", 0.9);
    ctx.lineWidth = 1.6;
    ctx.beginPath(); ctx.arc(fr.x, fr.y, (8 + pl * 4) * (f.s / 380), 0, 7); ctx.stroke();
    ctx.restore();
  }

  // the clarified path draws in
  const drawT = easeInOut(clamp01((p - 0.45) / 0.45));
  if (drawT > 0) {
    ctx.save();
    ctx.lineCap = "round";
    ctx.strokeStyle = rgba(accent, 0.95 * inten);
    ctx.shadowColor = rgba(accent, 0.6); ctx.shadowBlur = 12;
    ctx.lineWidth = 2.4;
    ctx.beginPath();
    const steps = 60;
    for (let i = 0; i <= steps * drawT; i++) {
      const pt = path(i / steps);
      i === 0 ? ctx.moveTo(pt.x, pt.y) : ctx.lineTo(pt.x, pt.y);
    }
    ctx.stroke();
    ctx.restore();
  }

  // target lock at the goal (echoes the ConversionWorks reticle)
  const lock = easeOut(clamp01((p - 0.8) / 0.2));
  if (lock > 0.01) {
    const rad = (10 + (1 - lock) * 22) * (f.s / 380);
    const idlePulse = p >= 1 ? 0.5 + 0.5 * Math.sin(idle * 1.4) : 1;
    ctx.save();
    ctx.globalAlpha = lock;
    ctx.strokeStyle = rgba(accent, 0.95);
    ctx.lineWidth = 1.6;
    ctx.beginPath(); ctx.arc(B.x, B.y, rad, 0, 7); ctx.stroke();
    ctx.beginPath(); ctx.arc(B.x, B.y, rad * 0.42, 0, 7); ctx.stroke();
    const tick = rad * 1.5;
    [0, 90, 180, 270].forEach((deg) => {
      const a = (deg * Math.PI) / 180;
      ctx.beginPath();
      ctx.moveTo(B.x + Math.cos(a) * rad * 1.15, B.y + Math.sin(a) * rad * 1.15);
      ctx.lineTo(B.x + Math.cos(a) * tick, B.y + Math.sin(a) * tick);
      ctx.stroke();
    });
    ctx.globalAlpha = lock * idlePulse;
    ctx.beginPath(); ctx.arc(B.x, B.y, 2.6 * (f.s / 380), 0, 7);
    ctx.fillStyle = rgba(accent, 1); ctx.fill();
    ctx.restore();
  }
}

/* ===== /CRO — always-on scan that surfaces + prioritises signals ===== */
function paintScan(ctx, w, h, p, idle, accent, inten) {
  const f = frame(w, h, 0.12);
  // left: abstract storefront / journey wireframe
  const pw = f.w * 0.42, ph = f.h * 0.92;
  const px = f.x, py = f.y + (f.h - ph) / 2;
  const appear = easeOut(clamp01(p / 0.16));
  ctx.save();
  ctx.globalAlpha = appear;
  rr(ctx, px, py, pw, ph, 10);
  ctx.fillStyle = "rgba(255,255,255,0.022)";
  ctx.fill();
  ctx.strokeStyle = "rgba(255,255,255,0.12)"; ctx.lineWidth = 1; ctx.stroke();
  // page sections
  const secs = [0.0, 0.16, 0.36, 0.52, 0.72];
  secs.forEach((sy, i) => {
    const yy = py + ph * (0.06 + sy);
    ctx.fillStyle = "rgba(255,255,255,0.05)";
    rr(ctx, px + pw * 0.08, yy, pw * (i % 2 ? 0.6 : 0.84), ph * 0.1, 4); ctx.fill();
  });
  ctx.restore();

  // signals fixed to the page, surfaced as the scan passes them
  const signals = [
    { sx: 0.7, sy: 0.12, w: 0.42 },
    { sx: 0.3, sy: 0.3, w: 0.86 },
    { sx: 0.78, sy: 0.5, w: 0.64 },
    { sx: 0.4, sy: 0.66, w: 0.95 },
    { sx: 0.66, sy: 0.82, w: 0.55 },
  ];
  // scan line position: sweeps during build, keeps looping slowly after (always-on)
  const scanT = p < 1 ? easeInOut(p) : (idle * 0.12) % 1;
  const scanY = py + ph * scanT;
  // queue on the right, ranked by signal weight
  const ranked = signals.map((s, i) => ({ ...s, i })).sort((a, b) => b.w - a.w);
  const qx = f.x + f.w * 0.56, qw = f.w * 0.44;
  const qTop = f.y + f.h * 0.12, qGap = f.h * 0.16;

  signals.forEach((s, i) => {
    const yy = py + ph * (0.06 + s.sy);
    const xx = px + pw * s.sx;
    const surfaced = clamp01((scanT - s.sy) / 0.06) * (scanT >= s.sy ? 1 : 0);
    const sShow = p < 1 ? clamp01((p - s.sy * 0.6) / 0.2) : 1;
    if (sShow > 0.01) {
      const pulse = 0.5 + 0.5 * Math.sin(idle * 3 + i);
      ctx.save();
      ctx.globalAlpha = sShow;
      ctx.beginPath(); ctx.arc(xx, yy + ph * 0.05, (3 + pulse * 2) * (f.s / 380), 0, 7);
      ctx.fillStyle = rgba(accent, 0.9); ctx.fill();
      ctx.beginPath(); ctx.arc(xx, yy + ph * 0.05, (7 + pulse * 4) * (f.s / 380), 0, 7);
      ctx.strokeStyle = rgba(accent, 0.4 * (1 - pulse * 0.5)); ctx.lineWidth = 1; ctx.stroke();
      ctx.restore();
    }
  });

  // scan beam
  ctx.save();
  ctx.globalAlpha = appear * inten;
  const bg = ctx.createLinearGradient(px, scanY - 10, px, scanY + 10);
  bg.addColorStop(0, rgba(accent, 0));
  bg.addColorStop(0.5, rgba(accent, 0.5));
  bg.addColorStop(1, rgba(accent, 0));
  ctx.fillStyle = bg;
  ctx.fillRect(px, scanY - 10, pw, 20);
  ctx.strokeStyle = rgba(accent, 0.9);
  ctx.lineWidth = 1.4;
  ctx.shadowColor = rgba(accent, 0.7); ctx.shadowBlur = 10;
  ctx.beginPath(); ctx.moveTo(px, scanY); ctx.lineTo(px + pw, scanY); ctx.stroke();
  ctx.restore();

  // prioritised queue
  const queueShow = easeOut(clamp01((p - 0.45) / 0.4));
  ranked.forEach((s, rank) => {
    const qy = qTop + rank * qGap;
    const ra = p < 1 ? queueShow * clamp01(1 - rank * 0.1) : 1;
    if (ra <= 0.01) return;
    const isTop = rank === 0;
    ctx.save();
    ctx.globalAlpha = ra;
    const bw2 = qw * (0.5 + 0.5 * s.w);
    rr(ctx, qx, qy, bw2, qGap * 0.56, 7);
    if (isTop) {
      const tg = ctx.createLinearGradient(qx, qy, qx + bw2, qy);
      tg.addColorStop(0, rgba(accent, 0.85));
      tg.addColorStop(1, rgba(accent, 0.4));
      ctx.fillStyle = tg;
      const glow = p >= 1 ? 0.5 + 0.5 * Math.sin(idle * 1.6) : 1;
      ctx.shadowColor = rgba(accent, 0.6 * glow); ctx.shadowBlur = 16 * glow;
    } else {
      ctx.fillStyle = "rgba(255,255,255,0.07)";
    }
    ctx.fill();
    ctx.restore();
    // rank number ticks
    ctx.save();
    ctx.globalAlpha = ra * (isTop ? 1 : 0.5);
    ctx.fillStyle = isTop ? "#0a0a0b" : "rgba(255,255,255,0.5)";
    ctx.fillRect(qx + 8, qy + qGap * 0.24, qGap * (0.06 + 0.04 * (3 - Math.min(rank, 3))), qGap * 0.1);
    ctx.restore();
  });

  // arrow: top item rises to the top of the queue
  const rise = easeOut(clamp01((p - 0.7) / 0.25));
  if (rise > 0.01 && queueShow > 0) {
    ctx.save();
    ctx.globalAlpha = rise * inten;
    ctx.strokeStyle = rgba(accent, 0.8); ctx.lineWidth = 1.4; ctx.lineCap = "round";
    const ax = qx - f.w * 0.04, ay = qTop + qGap * 0.28;
    ctx.beginPath(); ctx.moveTo(ax, ay + qGap * 0.3); ctx.lineTo(ax, ay - qGap * 0.1);
    ctx.moveTo(ax - 4, ay + qGap * 0.04); ctx.lineTo(ax, ay - qGap * 0.1); ctx.lineTo(ax + 4, ay + qGap * 0.04);
    ctx.stroke();
    ctx.restore();
  }
}

const PAINTERS = {
  assemble: paintAssemble,
  flow: paintFlow,
  clarify: paintClarify,
  scan: paintScan,
};

Object.assign(window, { HeroBackdrop, ProductCanvas, useReducedMotion, PAINTERS });
