Hero Isobar Contours

Heroloud

Hero background of drifting, breathing isobar-like contour lines whose density bunches tightly around the primary CTA and whose whole field leans toward the pointer, so the layout physically reads as a live pressure system centered on the one action that matters.

Install
npx shadcn add https://design.helpmarq.com/r/hero-isobar-contours.json
Source
registry/loud/hero-isobar-contours/component.tsx
"use client";

import { useEffect, useRef } from "react";

// ---------------------------------------------------------------------------
// PressureFront — a hero background of isobar-like closed contour lines that
// bunch tightly around the primary CTA, so the composition physically leans
// toward the one action that matters. Pure SVG: every ring is a <path> whose
// `d` is recomputed directly (no canvas, no interpolation) on a throttled
// rAF loop, so `stroke` can reference --border/--muted/--foreground natively
// and both themes repaint for free — no getComputedStyle color parsing at
// all. Line DENSITY (radial spacing that compresses near the CTA) is the
// hierarchy device, not color or a gradient fill.
//
// The field is genuinely alive at rest, not just on interaction: a
// continuous drift rotates the wobble, an independent faster breathing
// pulse swells/contracts the low-pressure centre, and it leans toward the
// pointer wherever it roams over the hero (not only when hovering the CTA).
// All three are additive perturbations on top of the same closed-form
// radius, never a point-attractor toward absolute coordinates — that keeps
// ~30 nested closed curves from ever converging into each other.
// ---------------------------------------------------------------------------

const RINGS = 30;
const ANGLES = 56; // vertices per ring — smooth at 1px stroke, cheap to redraw
const COMPRESSION = 2.15; // >1 bunches inner rings tightly, spreads outer ones
const ELLIPSE_X = 1.28; // rings are gently wide, matching a landscape hero
const PULL_PX = 8; // max inward pull on CTA hover/focus (deepening low)
const FRAME_INTERVAL = 1000 / 20; // throttled redraw rate — isobars drift slow, but smooth enough to read the pointer lean
const SPRING_K = 90;
const SPRING_ZETA = 0.8;
const BREATH_MS = 7000; // independent, faster period so the low visibly pulses instead of only slowly rotating
const BREATH_FRAC = 0.22; // +/- fraction minR itself swells by — a single global scalar, see drawRings for why
const POINTER_PULL_PX = 30; // max outward lean toward the pointer — stronger than the CTA's isotropic pull
const POINTER_GAP_CLAMP = 0.5; // never lean a ring past this fraction of its gap to the next ring; kept well under 1 because the existing wobble term already consumes part of that gap on its own

// Deterministic low-frequency harmonic sum standing in for 2D noise: it's
// exactly periodic in theta so every ring closes without a seam at 0/2π,
// which a sampled value-noise field can't guarantee without extra stitching.
function ringWobble(theta: number, phase: number, seed: number) {
  return (
    0.5 * Math.sin(3 * theta + phase + seed * 0.7) +
    0.3 * Math.sin(5 * theta - phase * 1.3 + seed * 1.3 + 1.7) +
    0.2 * Math.sin(8 * theta + phase * 0.6 + seed * 2.1 + 4.1)
  );
}

// Ring color steps toward the CTA — discrete steps, never a gradient. Colors
// are CSS custom properties resolved natively by the browser (no JS parsing).
function ringColor(i: number) {
  if (i < 2) return "color-mix(in srgb, var(--foreground) 25%, transparent)";
  if (i < 5) return "var(--muted)";
  return "var(--border)";
}

export interface PressureFrontCta {
  label: string;
  href?: string;
  onClick?: () => void;
}

export interface PressureFrontProps {
  eyebrow?: string;
  headline?: string | string[];
  subcopy?: string;
  primaryCta: PressureFrontCta;
  secondaryCta?: PressureFrontCta;
  /** number of contour rings. default 30 */
  rings?: number;
  /** ms for one full drift loop of the noise phase. default 20000 */
  driftMs?: number;
  className?: string;
}

export function PressureFront({
  eyebrow,
  headline = "Ship the thing",
  subcopy,
  primaryCta,
  secondaryCta,
  rings = RINGS,
  driftMs = 20000,
  className = "",
}: PressureFrontProps) {
  const rootRef = useRef<HTMLDivElement>(null);
  const svgRef = useRef<SVGSVGElement>(null);
  const ctaRef = useRef<HTMLElement | null>(null);
  const pathRefs = useRef<(SVGPathElement | null)[]>([]);
  const dimsRef = useRef({ w: 0, h: 0 });
  const ctaCenterRef = useRef({ x: 0, y: 0 });

  const headlineLines = Array.isArray(headline) ? headline : [headline];

  useEffect(() => {
    const root = rootRef.current;
    const svg = svgRef.current;
    const cta = ctaRef.current;
    if (!root || !svg || !cta) return;

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

    // -- measurement: CTA position re-derived on every resize --------------
    const measure = () => {
      const rootRect = root.getBoundingClientRect();
      const w = rootRect.width;
      const h = rootRect.height;
      dimsRef.current = { w, h };
      svg.setAttribute("viewBox", `0 0 ${Math.max(1, w)} ${Math.max(1, h)}`);
      const ctaRect = cta.getBoundingClientRect();
      ctaCenterRef.current = {
        x: ctaRect.left - rootRect.left + ctaRect.width / 2,
        y: ctaRect.top - rootRect.top + ctaRect.height / 2,
      };
    };
    measure();

    // -- geometry: recomputed and written directly to each <path d> --------
    // `breathPhase` runs on its own faster clock so the low-pressure centre
    // visibly pulses instead of only slowly rotating with the drift. Pointer
    // deformation is deliberately RADIAL and per-ring (never "toward a fixed
    // point in screen space"): each ring only ever swells along its own
    // center-relative radius, one-sided toward the cursor's bearing, clamped
    // to a fraction of its actual gap to the next ring. That keeps neighbor
    // rings moving in parallel — a point-attractor on ~30 nested closed
    // curves would make rings near the cursor's radius converge and cross.
    const drawRings = (
      phase: number,
      pullPx: number,
      breathPhase: number,
      pointerX: number,
      pointerY: number,
      pointerStrength: number
    ) => {
      const { w, h } = dimsRef.current;
      if (w < 1 || h < 1) return;
      const { x: ctaX, y: ctaY } = ctaCenterRef.current;
      const maxR = Math.max(w, h) * 0.92;
      const minR = Math.min(28, maxR * 0.05);

      // Breathing modulates minR as ONE global scalar, not a per-ring term —
      // deliberately, because the innermost rings are already packed within
      // a fraction of a px of each other (t^2.15 is nearly flat near t=0), so
      // any independent per-ring perturbation there risks flipping their
      // order. Folding the pulse into minR instead keeps every ring's radius
      // exactly (maxR - minRBreath) * t^COMPRESSION + minRBreath: the gap
      // between any two rings is (maxR - minRBreath) * (t_i^C - t_(i-1)^C),
      // always positive since maxR always exceeds minRBreath by construction
      // (BREATH_FRAC is a small fraction) — so the pulse can never cross
      // rings, only grow/shrink the whole low in lockstep, tapering to zero
      // at the outer edge for free via the same (1 - t^C) the compression
      // curve already has.
      const minRBreath = minR * (1 + BREATH_FRAC * Math.sin(breathPhase));

      // Pass 1: centers + base radii for every ring, so pass 2 can clamp the
      // pointer lean against each ring's *real* (post-breathing) neighbor gap.
      const centers: { x: number; y: number }[] = new Array(rings);
      const baseRs: number[] = new Array(rings);
      for (let i = 0; i < rings; i++) {
        const t = i / (rings - 1);
        const driftR = minR * 0.9;
        centers[i] = {
          x: ctaX + driftR * Math.cos(phase * 0.5 + i * 0.05),
          y: ctaY + driftR * 0.6 * Math.sin(phase * 0.5 + i * 0.05),
        };
        baseRs[i] = minRBreath + (maxR - minRBreath) * Math.pow(t, COMPRESSION);
      }

      for (let i = 0; i < rings; i++) {
        const el = pathRefs.current[i];
        if (!el) continue;
        const t = i / (rings - 1);
        const { x: cx, y: cy } = centers[i];
        const baseR = baseRs[i];
        const amp = (maxR - minR) * 0.045 * (0.35 + 0.65 * t);
        const seed = i * 0.53;
        const ringPhase = phase + i * 0.045;
        const pullHere = pullPx * (1 - t);

        // How much (and toward what bearing) this ring leans toward the
        // pointer: gaussian in RADIUS-space (how close the pointer's distance
        // from this ring's own center is to this ring's own radius), not
        // screen-space distance to a point — so the lean is inherently a
        // per-ring radial swell, not a convergent pull.
        let pointerAmp = 0;
        let pointerAngle = 0;
        if (pointerStrength > 0.001) {
          const dxp = pointerX - cx;
          const dyp = pointerY - cy;
          const distToCenter = Math.hypot(dxp, dyp);
          pointerAngle = Math.atan2(dyp, dxp);
          const distToRing = distToCenter - baseR;
          const sigma = Math.max(24, (maxR - minR) * 0.09);
          const ringFalloff = Math.exp(-(distToRing * distToRing) / (2 * sigma * sigma));
          const gapPrev = i > 0 ? Math.abs((baseRs[i] ?? baseR) - (baseRs[i - 1] ?? baseR)) : Math.abs((baseRs[1] ?? baseR) - (baseRs[0] ?? baseR));
          const gapNext = i < rings - 1 ? Math.abs((baseRs[i + 1] ?? baseR) - (baseRs[i] ?? baseR)) : gapPrev;
          const localGap = Math.min(gapPrev, gapNext);
          pointerAmp = Math.min(POINTER_PULL_PX, localGap * POINTER_GAP_CLAMP) * pointerStrength * ringFalloff;
        }

        let d = "";
        for (let k = 0; k < ANGLES; k++) {
          const theta = (k / ANGLES) * Math.PI * 2;
          const wob = amp * ringWobble(theta, ringPhase, seed);
          let r = baseR + wob - pullHere;
          if (pointerAmp > 0.01) {
            const align = Math.max(0, Math.cos(theta - pointerAngle));
            r += pointerAmp * align * align; // one-sided swell on the cursor-facing arc only
          }
          r = Math.max(4, r);
          const x = cx + r * Math.cos(theta) * ELLIPSE_X;
          const y = cy + r * Math.sin(theta);
          d += k === 0 ? `M${x.toFixed(1)},${y.toFixed(1)}` : `L${x.toFixed(1)},${y.toFixed(1)}`;
        }
        d += "Z";
        el.setAttribute("d", d);
      }
    };

    if (reduced) {
      // Static density gradient only: no phase drift, no continuous spring —
      // this is the strongest reduced-motion story in the set, since the
      // hierarchy is encoded structurally (ring spacing), not by motion.
      // No root-wide pointer tracking here on purpose — a redraw on every
      // pointermove is exactly the continuous motion prefers-reduced-motion
      // opts out of. Only the CTA's own enter/leave/focus/blur still toggles
      // a single static redraw, same as before.
      let staticPull = 0;
      drawRings(0, 0, 0, 0, 0, 0);
      const onEnter = () => {
        staticPull = PULL_PX;
        drawRings(0, staticPull, 0, 0, 0, 0);
      };
      const onLeave = () => {
        staticPull = 0;
        drawRings(0, staticPull, 0, 0, 0, 0);
      };
      cta.addEventListener("pointerenter", onEnter);
      cta.addEventListener("pointerleave", onLeave);
      cta.addEventListener("focus", onEnter);
      cta.addEventListener("blur", onLeave);
      const ro = new ResizeObserver(() => {
        measure();
        drawRings(0, staticPull, 0, 0, 0, 0);
      });
      ro.observe(root);
      const ctaRo = new ResizeObserver(() => {
        measure();
        drawRings(0, staticPull, 0, 0, 0, 0);
      });
      ctaRo.observe(cta);
      return () => {
        ro.disconnect();
        ctaRo.disconnect();
        cta.removeEventListener("pointerenter", onEnter);
        cta.removeEventListener("pointerleave", onLeave);
        cta.removeEventListener("focus", onEnter);
        cta.removeEventListener("blur", onLeave);
      };
    }

    // -- full loop: ambient drift + breathing + damped-spring hover pull +
    // pointer-follow lean -----------------------------------------------
    let raf = 0;
    let elVisible = true;
    let pageVisible = document.visibilityState === "visible";
    let visible = elVisible && pageVisible;
    let hoverOn = false;
    let startTime = 0;
    let lastTick = 0;
    let lastDraw = 0;
    let pull = 0;
    let pullVel = 0;

    // Pointer follow: raw target from the event, a smoothed trailing
    // position (framerate-normalized lerp, same idiom as chart-ridgeline-terrain),
    // and a spring-eased 0..1 strength so entering/leaving ramps rather than
    // snaps. Snaps straight to the raw position on the frame pointer
    // tracking (re)starts, so it doesn't sweep in from wherever it last was.
    let pointerRawX = 0;
    let pointerRawY = 0;
    let pointerX = 0;
    let pointerY = 0;
    let pointerActive = false;
    let pointerTracking = false;
    let pointerStrength = 0;
    let pointerStrengthVel = 0;

    const tick = (now: number) => {
      if (!startTime) startTime = now;
      const dt = lastTick ? Math.min(0.05, (now - lastTick) / 1000) : 1 / 60;
      lastTick = now;

      const target = hoverOn ? PULL_PX : 0;
      const c = 2 * SPRING_ZETA * Math.sqrt(SPRING_K);
      const accel = -SPRING_K * (pull - target) - c * pullVel;
      pullVel += accel * dt;
      pull += pullVel * dt;
      if (Math.abs(pull - target) < 0.02 && Math.abs(pullVel) < 0.02) {
        pull = target;
        pullVel = 0;
      }

      if (pointerActive) {
        if (!pointerTracking) {
          pointerX = pointerRawX;
          pointerY = pointerRawY;
          pointerTracking = true;
        } else {
          const k = 1 - Math.pow(0.88, dt * 60);
          pointerX += (pointerRawX - pointerX) * k;
          pointerY += (pointerRawY - pointerY) * k;
        }
      } else {
        pointerTracking = false;
      }
      const pointerTarget = pointerActive ? 1 : 0;
      const cp = 2 * SPRING_ZETA * Math.sqrt(SPRING_K);
      const accelP = -SPRING_K * (pointerStrength - pointerTarget) - cp * pointerStrengthVel;
      pointerStrengthVel += accelP * dt;
      pointerStrength += pointerStrengthVel * dt;
      if (Math.abs(pointerStrength - pointerTarget) < 0.002 && Math.abs(pointerStrengthVel) < 0.002) {
        pointerStrength = pointerTarget;
        pointerStrengthVel = 0;
      }

      if (now - lastDraw >= FRAME_INTERVAL) {
        lastDraw = now;
        const phase = ((now - startTime) / driftMs) * Math.PI * 2;
        const breathPhase = ((now - startTime) / BREATH_MS) * Math.PI * 2;
        drawRings(phase, pull, breathPhase, pointerX, pointerY, pointerStrength);
      }
      raf = visible ? requestAnimationFrame(tick) : 0;
    };
    const wake = () => {
      if (!raf && visible) raf = requestAnimationFrame(tick);
    };

    const onCtaOn = () => {
      hoverOn = true;
      wake();
    };
    const onCtaOff = () => {
      hoverOn = false;
      wake();
    };
    cta.addEventListener("pointerenter", onCtaOn);
    cta.addEventListener("pointerleave", onCtaOff);
    cta.addEventListener("focus", onCtaOn);
    cta.addEventListener("blur", onCtaOff);

    // Whole-hero pointer tracking — the field leans toward the cursor
    // wherever it is, not just when hovering the CTA. Mouse/pen only: touch
    // has no hover state and pointermove-during-scroll would read as jitter.
    // rect is re-measured on every move (matching chart-ridgeline-terrain's idiom)
    // rather than cached, so page scroll never throws the coordinates off.
    const onRootMove = (e: PointerEvent) => {
      if (e.pointerType === "touch") return;
      const rect = root.getBoundingClientRect();
      pointerRawX = e.clientX - rect.left;
      pointerRawY = e.clientY - rect.top;
      pointerActive = true;
      wake();
    };
    const onRootLeave = (e: PointerEvent) => {
      if (e.pointerType === "touch") return;
      pointerActive = false;
      wake();
    };
    root.addEventListener("pointermove", onRootMove);
    root.addEventListener("pointerleave", onRootLeave);

    const ro = new ResizeObserver(measure);
    ro.observe(root);
    const ctaRo = new ResizeObserver(measure);
    ctaRo.observe(cta);

    const io = new IntersectionObserver((entries) => {
      elVisible = entries[0]?.isIntersecting ?? true;
      visible = elVisible && pageVisible;
      wake();
    });
    io.observe(root);

    const onVisibility = () => {
      pageVisible = document.visibilityState === "visible";
      visible = elVisible && pageVisible;
      wake();
    };
    document.addEventListener("visibilitychange", onVisibility);

    wake();

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      ctaRo.disconnect();
      io.disconnect();
      document.removeEventListener("visibilitychange", onVisibility);
      cta.removeEventListener("pointerenter", onCtaOn);
      cta.removeEventListener("pointerleave", onCtaOff);
      cta.removeEventListener("focus", onCtaOn);
      cta.removeEventListener("blur", onCtaOff);
      root.removeEventListener("pointermove", onRootMove);
      root.removeEventListener("pointerleave", onRootLeave);
    };
  }, [rings, driftMs]);

  const ctaFocusRing =
    "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent";

  return (
    <section
      ref={rootRef}
      className={`relative isolate overflow-hidden bg-background ${className}`}
    >
      <svg
        ref={svgRef}
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 h-full w-full"
      >
        {Array.from({ length: rings }).map((_, i) => (
          <path
            key={i}
            ref={(el) => {
              pathRefs.current[i] = el;
            }}
            fill="none"
            style={{ stroke: ringColor(i), strokeWidth: 1 }}
          />
        ))}
      </svg>

      <div className="relative z-10 mx-auto flex w-full max-w-5xl flex-col items-center px-6 pb-16 pt-24 text-center sm:pb-24 sm:pt-32">
        {eyebrow ? (
          <p className="mb-6 font-mono text-[11px] tracking-widest text-muted">
            {eyebrow}
          </p>
        ) : null}
        <h1
          className="font-semibold text-foreground"
          style={{
            fontSize: "clamp(2.5rem, 6.5vw, 4.5rem)",
            lineHeight: 1.06,
            letterSpacing: "-0.03em",
          }}
        >
          {headlineLines.map((line, i) => (
            <span key={i} className="block">
              {line}
            </span>
          ))}
        </h1>
        {subcopy ? (
          <p className="mt-6 max-w-xl text-base leading-relaxed text-muted">
            {subcopy}
          </p>
        ) : null}
        <div className="mt-9 flex flex-wrap items-center justify-center gap-3">
          {primaryCta.href ? (
            <a
              ref={(el) => {
                ctaRef.current = el;
              }}
              href={primaryCta.href}
              data-cta="primary"
              onClick={primaryCta.onClick}
              className={`rounded-sm bg-foreground px-5 py-2.5 text-sm font-medium text-background transition-opacity duration-200 hover:opacity-90 ${ctaFocusRing}`}
            >
              {primaryCta.label}
            </a>
          ) : (
            <button
              ref={(el) => {
                ctaRef.current = el;
              }}
              type="button"
              data-cta="primary"
              onClick={primaryCta.onClick}
              className={`rounded-sm bg-foreground px-5 py-2.5 text-sm font-medium text-background transition-opacity duration-200 hover:opacity-90 ${ctaFocusRing}`}
            >
              {primaryCta.label}
            </button>
          )}
          {secondaryCta ? (
            secondaryCta.href ? (
              <a
                href={secondaryCta.href}
                onClick={secondaryCta.onClick}
                className={`rounded-sm border border-border px-5 py-2.5 text-sm font-medium text-muted transition-colors duration-200 hover:border-foreground/20 hover:text-foreground ${ctaFocusRing}`}
              >
                {secondaryCta.label}
              </a>
            ) : (
              <button
                type="button"
                onClick={secondaryCta.onClick}
                className={`rounded-sm border border-border px-5 py-2.5 text-sm font-medium text-muted transition-colors duration-200 hover:border-foreground/20 hover:text-foreground ${ctaFocusRing}`}
              >
                {secondaryCta.label}
              </button>
            )
          ) : null}
        </div>
      </div>
    </section>
  );
}
Use when

a landing hero whose visual hierarchy is encoded structurally — line density converging on the CTA, reinforced by a live pressure field that breathes and leans toward the cursor — rather than hero-dipole-field, where cursor proximity is the whole mechanism, or chart-ridgeline-terrain, a canvas terrain sampling live data; pick this one when the persuasion has to survive prefers-reduced-motion as a still frame, since the density gradient alone still does the full hierarchy job with the drift, breathing and pointer-lean all frozen off.

Build spec

A hero whose background is ~30 closed isobar-like contour <path> rings, each one a plain SVG element whose stroke reads --border/--muted/--foreground (and color-mix(in srgb, var(--foreground) 25%, transparent) for the innermost two) directly as CSS custom properties on the `style` attribute — no getComputedStyle, no canvas, no color parsing at all, so both themes repaint for free. Ring radius follows radius(t) = minR + (maxR - minR) * t^2.15 for t in [0,1] across the ring index, a convex power curve that packs the inner rings tightly and spreads the outer ones — this radial compression toward the CTA's measured center is the entire hierarchy device; color only steps twice (border to muted to foreground@25%) as secondary reinforcement, never a continuous gradient. Each ring is additionally perturbed by a deterministic low-frequency harmonic sum (three sine terms at ascending integer frequencies with descending amplitude, a closed-form stand-in for 2D noise) sampled directly in theta, which is exactly periodic so every ring closes without a stitching seam; each ring also carries a small per-index phase offset and a slightly orbiting center (a few px, cosine/sine of the drift phase) so the rings read as a layered weather-map field rather than N identical concentric circles. The CTA's on-screen center is measured via getBoundingClientRect relative to the hero's own bounding rect and re-measured on a ResizeObserver watching both the hero container and the CTA element itself, so layout reflow (font load, viewport resize, content wrap) never leaves the field anchored to a stale position. Geometry is recomputed directly every throttled tick (~20fps via a rAF accumulator, no interpolation between cached keyframes — the recompute itself is cheap closed-form trig, on the order of a couple thousand sin/cos calls, so no Web Worker is warranted) and the phase advances (elapsed / 20s) * 2*pi in a continuous loop that never settles, giving the field constant drift (tightened from an original 40s once that read as too subtle to register as alive at rest); the loop pauses on IntersectionObserver (offscreen) and document visibilitychange (backgrounded tab). A second, independent phase clock (7s period) drives a breathing pulse by modulating minR itself as a single global +/-22% scalar (never per-ring), so the low-pressure centre visibly swells and contracts on its own faster rhythm instead of the field reading as a slow uniform rotation; folding the pulse into minR rather than perturbing each ring's radius independently keeps every ring's gap to its neighbor at (maxR - minR_breathing) * (t_i^2.15 - t_(i-1)^2.15), always positive, so the pulse cannot invert ring order even where the innermost rings are packed within a fraction of a pixel of each other, and it tapers to zero at the outer edge for free via the same (1 - t^2.15) the compression curve already has. Hovering or focusing the primary CTA raises a target inward pull of 8px, integrated every animation frame (not just the throttled draw ticks) through a damped spring (k=90, zeta=0.8) and applied per ring scaled by (1 - t), so inner rings pull hardest and the effect reads as the low visibly deepening; releasing focus/hover springs it back. Separately, the whole hero tracks the pointer (mouse/pen, not touch) and every ring leans a bounded outward swell toward the cursor's bearing — one-sided via max(0, cos(theta - angleToPointer))^2, gaussian-weighted by how close the pointer's distance from that ring's own center is to that ring's own radius, and clamped to 50% of its real base gap to the neighboring ring, deliberately well under 100% because the pre-existing wobble term already consumes part of that same gap on its own — so the pointer term alone is always a parallel, non-convergent lean (never a point-attractor that would pull rings across each other), and in practice adjacent contours stay clearly separated on the cursor-facing arc even directly over the densely-packed centre; a spring-eased 0..1 strength ramps the effect in/out on pointer enter/leave rather than snapping. The SVG layer is aria-hidden and pointer-events-none, so it never interferes with the real interactive CTA underneath — pointer tracking is bound to the hero section itself, not the SVG. The breathing pulse and pointer-lean are both full-motion-only: under reduced motion neither the faster breathing clock nor root-wide pointer tracking ever starts (a continuous redraw on every pointermove is exactly the motion that preference opts out of), leaving only the CTA's existing single-toggle hover/focus pull. The primary CTA is a solid bg-foreground/text-background button — deliberately neutral, since --accent is reserved for exactly one appearance in this whole component: the CTA's focus-visible outline, the same outline-2/outline-offset-2/outline-accent pattern used registry-wide. The secondary CTA is a bordered ghost button with the same focus treatment (in --border/--foreground, not accent, keeping accent scarce). Under prefers-reduced-motion the phase is frozen at a single fixed instant and the continuous spring/rAF loop never starts at all — the static ring spacing alone still does the complete hierarchy job, and hover/focus on the CTA instantly toggles between the resting and pulled-in ring layout (a single redraw, not an animation) so the interaction stays legible without motion. Zero dependencies.

Tags
svgherocontourisobarctahierarchyambientdriftpointer-reactivesignup