---
/**
 * Ink diffusing into wet paper.
 *
 * Follows the pointer, and keeps drifting on its own when nobody is
 * touching it — the system runs whether or not you are watching. That is
 * the "agentic" idea expressed in ink rather than neon: no particles, no
 * constellation lines, no glow.
 *
 * Reads --ca-ink from its own parent rather than :root, because it sits
 * inside .ca-ink-surface where that token is the light value. Reading
 * :root would paint near-black onto near-black and show nothing.
 */
---

<canvas class="ca-wash" aria-hidden="true"></canvas>

<style>
  .ca-wash {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity 1.6s linear;
  }

  .ca-wash[data-on="true"] {
    opacity: 1;
  }

  @media (prefers-reduced-motion: reduce) {
    .ca-wash {
      display: none;
    }
  }
</style>

<script>
  (() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

    const canvas = document.querySelector(".ca-wash");
    if (!(canvas instanceof HTMLCanvasElement)) return;
    const host = canvas.parentElement;
    const ctx = canvas.getContext("2d");
    if (!ctx || !host) return;

    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    let w = 0;
    let h = 0;
    let ink = [237, 236, 232];
    const blooms = [];
    const last = { x: 0, y: 0 };

    const readInk = () => {
      const raw = getComputedStyle(host).getPropertyValue("--ca-ink").trim();
      const m = raw.match(/^#?([0-9a-f]{6})$/i);
      if (!m) return;
      const n = parseInt(m[1], 16);
      ink = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
    };

    const size = () => {
      const r = canvas.getBoundingClientRect();
      w = r.width;
      h = r.height;
      canvas.width = w * dpr;
      canvas.height = h * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };

    const bloom = (x, y, r, a) => {
      blooms.push({ x, y, r, a, life: 1, grow: 0.4 + Math.random() * 0.7 });
      if (blooms.length > 90) blooms.shift();
    };

    readInk();
    size();
    window.addEventListener("resize", size);

    host.addEventListener(
      "pointermove",
      (event) => {
        const r = canvas.getBoundingClientRect();
        const x = event.clientX - r.left;
        const y = event.clientY - r.top;
        if (Math.hypot(x - last.x, y - last.y) > 26) {
          bloom(x, y, 26 + Math.random() * 46, 0.05);
          last.x = x;
          last.y = y;
        }
      },
      { passive: true },
    );

    const frame = () => {
      ctx.clearRect(0, 0, w, h);

      // ambient drift — the page is never entirely still
      if (Math.random() < 0.012) {
        bloom(
          w * (0.1 + Math.random() * 0.8),
          h * (0.15 + Math.random() * 0.7),
          60 + Math.random() * 120,
          0.022,
        );
      }

      for (let i = blooms.length - 1; i >= 0; i--) {
        const b = blooms[i];
        b.life -= 0.0034;
        b.r += b.grow;
        if (b.life <= 0) {
          blooms.splice(i, 1);
          continue;
        }
        const alpha = b.a * b.life;
        const [r, g, bl] = ink;
        const grad = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r);
        grad.addColorStop(0, `rgba(${r},${g},${bl},${alpha})`);
        grad.addColorStop(0.55, `rgba(${r},${g},${bl},${alpha * 0.35})`);
        grad.addColorStop(1, `rgba(${r},${g},${bl},0)`);
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
        ctx.fill();
      }
      requestAnimationFrame(frame);
    };

    requestAnimationFrame(frame);
    setTimeout(() => canvas.setAttribute("data-on", "true"), 400);

    new MutationObserver(readInk).observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"],
    });
  })();
</script>
