# Argon — Animated Text Effect for React

> SVG gas-discharge tube lettering — ignition travels along each glyph's tube with a seeded strike flicker, then holds with a faint mains hum; the letter under the pointer destabilises. Neon, argon and mercury presets; fully lit and steady under reduced motion.

- Page: https://crucibleui.com/components/text/argon
- Category: [text](https://crucibleui.com/components/text)
- Tier: free (full source)
- Engine: css

## Install

```bash
npx shadcn@latest add @crucible/argon
```

Registry JSON: https://crucibleui.com/r/argon.json

Import from `@/components/crucible/argon`.

## Props (`ArgonProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text?` | `string` | `"Open late"` | The lettering. Use "\n" for a stacked multi-line sign. |
| `gas?` | `ArgonGas` | `"argon"` | Gas fill: "neon" (red-orange), "argon" (ice blue), "mercury" (cold white), or any CSS color. |
| `as?` | `"h1" \| "h2" \| "h3" \| "p"` | `"h2"` | Element the (visually hidden) real text is rendered as, so it stays a heading. |
| `fontSize?` | `number` | `96` | Glyph size in px at natural size; the sign scales down to fit its container. |
| `strokeWidth?` | `number` | `2.4` | Lit tube stroke width in px at natural size. |
| `clips?` | `boolean` | `true` | Render mounting clips beneath each glyph and a back-wire behind every line. |
| `igniteOnView?` | `boolean` | `true` | Wait until the sign scrolls into view before igniting (otherwise ignite on mount). |
| `intensity?` | `number` | `1` | Glow strength: bloom size/opacity and interior gas fill. 0–2. |
| `idle?` | `boolean` | `true` | When no pointer has been present for ~1.5s, glyphs occasionally destabilise on their own. |
| `paused?` | `boolean` | `false` | Hold the sign fully lit and steady — no intro, hum, or flicker. |
| `ref?` | `React.Ref<ArgonHandle>` | — | Imperative handle: `replay()` and `flicker(index?)`. |

Also accepts: `Omit<React.HTMLAttributes<HTMLElement>, "children">`.

## Usage example

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { Argon, type ArgonHandle } from "@/components/crucible/argon";

const GASES = [
  { id: "argon", label: "Argon", tint: "124,196,255" },
  { id: "neon", label: "Neon", tint: "255,90,60" },
  { id: "mercury", label: "Mercury", tint: "232,241,255" },
] as const;

type GasId = (typeof GASES)[number]["id"];

export default function ArgonDemo() {
  const [gas, setGas] = React.useState<GasId>("argon");
  const signRef = React.useRef<ArgonHandle>(null);
  const tint = GASES.find((g) => g.id === gas)!.tint;

  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950">
      {/* dark brick wall */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0"
        style={{
          backgroundColor: "#141012",
          backgroundImage: [
            "repeating-linear-gradient(0deg, rgba(0,0,0,0.55) 0 2px, transparent 2px 28px)",
            "repeating-linear-gradient(90deg, rgba(0,0,0,0.45) 0 2px, transparent 2px 64px)",
            "linear-gradient(180deg, #1b1618 0%, #110e10 100%)",
          ].join(","),
          backgroundSize: "100% 28px, 64px 56px, 100% 100%",
        }}
      />
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0"
        style={{
          backgroundImage:
            "repeating-linear-gradient(90deg, rgba(0,0,0,0.45) 0 2px, transparent 2px 64px)",
          backgroundSize: "64px 56px",
          backgroundPosition: "32px 28px",
          maskImage: "repeating-linear-gradient(0deg, transparent 0 28px, #000 28px 56px)",
          WebkitMaskImage: "repeating-linear-gradient(0deg, transparent 0 28px, #000 28px 56px)",
        }}
      />
      {/* light spill from the tubes onto the wall */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 transition-[background] duration-500"
        style={{
          background: `radial-gradient(ellipse 55% 45% at 50% 46%, rgba(${tint},0.16), transparent 70%), radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.75) 100%)`,
        }}
      />

      <div className="relative z-10 flex w-full flex-col items-center gap-6 px-6 text-center">
        <Argon ref={signRef} text="Open late" gas={gas} fontSize={104} className="text-white" />

        <div className="flex flex-wrap items-center justify-center gap-3">
          <div
            role="radiogroup"
            aria-label="Gas"
            className="inline-flex rounded-full border border-white/10 bg-black/40 p-1 backdrop-blur"
          >
            {GASES.map((g, i) => (
              <button
                key={g.id}
                type="button"
                role="radio"
                aria-checked={gas === g.id}
                tabIndex={gas === g.id ? 0 : -1}
                onClick={() => setGas(g.id)}
                onKeyDown={(e) => {
                  if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
                  e.preventDefault();
                  const next = GASES[(i + (e.key === "ArrowRight" ? 1 : GASES.length - 1)) % GASES.length]!;
                  setGas(next.id);
                  const group = e.currentTarget.parentElement;
                  requestAnimationFrame(() =>
                    group?.querySelector<HTMLButtonElement>('[aria-checked="true"]')?.focus()
                  );
                }}
                className={cn(
                  "flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-white/40",
                  gas === g.id ? "bg-white/10 text-white" : "text-neutral-400 hover:text-neutral-200"
                )}
              >
                <span
                  aria-hidden
                  className="size-1.5 rounded-full"
                  style={{ background: `rgb(${g.tint})`, boxShadow: `0 0 6px rgb(${g.tint})` }}
                />
                {g.label}
              </button>
            ))}
          </div>
          <button
            type="button"
            onClick={() => signRef.current?.replay()}
            className="rounded-full border border-white/10 bg-black/40 px-3 py-1.5 text-xs font-medium text-neutral-300 outline-none backdrop-blur transition-colors hover:text-white focus-visible:ring-2 focus-visible:ring-white/40"
          >
            Replay
          </button>
        </div>

        <p className="text-sm text-neutral-500">Hover a letter — it flickers.</p>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.

## Source (`argon.tsx`)

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { useReducedMotion } from "@/hooks/use-reduced-motion";

/** Built-in gas presets, or any CSS color string. */
export type ArgonGas = "neon" | "argon" | "mercury" | (string & {});

export interface ArgonHandle {
  /** Switch every tube off and run the ignition intro again. */
  replay(): void;
  /** Destabilise one glyph (by index among visible glyphs), or a seeded random one. */
  flicker(index?: number): void;
}

export interface ArgonProps extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
  /** The lettering. Use "\n" for a stacked multi-line sign. @default "Open late" */
  text?: string;
  /** Gas fill: "neon" (red-orange), "argon" (ice blue), "mercury" (cold white), or any CSS color. @default "argon" */
  gas?: ArgonGas;
  /** Element the (visually hidden) real text is rendered as, so it stays a heading. @default "h2" */
  as?: "h1" | "h2" | "h3" | "p";
  /** Glyph size in px at natural size; the sign scales down to fit its container. @default 96 */
  fontSize?: number;
  /** Lit tube stroke width in px at natural size. @default 2.4 */
  strokeWidth?: number;
  /** Render mounting clips beneath each glyph and a back-wire behind every line. @default true */
  clips?: boolean;
  /** Wait until the sign scrolls into view before igniting (otherwise ignite on mount). @default true */
  igniteOnView?: boolean;
  /** Glow strength: bloom size/opacity and interior gas fill. 0–2. @default 1 */
  intensity?: number;
  /** When no pointer has been present for ~1.5s, glyphs occasionally destabilise on their own. @default true */
  idle?: boolean;
  /** Hold the sign fully lit and steady — no intro, hum, or flicker. @default false */
  paused?: boolean;
  /** Imperative handle: `replay()` and `flicker(index?)`. */
  ref?: React.Ref<ArgonHandle>;
}

const GAS_PRESETS: Record<string, string> = {
  neon: "#ff5a3c",
  argon: "#7cc4ff",
  mercury: "#e8f1ff",
};

/** Stagger between glyph ignitions (ms). */
const STAGGER = 70;
/** Time for the ignition to travel the dash window (ms). */
const DRAW_MS = 1400;
/** Dash window, in em — longer than any glyph's outline. */
const DASH_EM = 10;
/** Photosensitivity budget: max opacity dips from hover/idle flicker in any 1s window. */
const MAX_DIPS_PER_SECOND = 3;
const FLICKER_DIPS = 2;

interface Glyph {
  char: string;
  line: number;
  /** Start x relative to the line's left edge. */
  x: number;
  /** Advance width. */
  w: number;
}

interface Layout {
  glyphs: Glyph[];
  lineWidths: number[];
  measured: boolean;
}

function mulberry32(seed: number) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function hashString(s: string): number {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

/** Deterministic pre-measure layout (SSR + first client render; hidden until measured). */
function estimateLayout(lines: string[], fontSize: number): Layout {
  const glyphs: Glyph[] = [];
  const lineWidths: number[] = [];
  const adv = fontSize * 0.6;
  lines.forEach((line, li) => {
    const chars = Array.from(line);
    chars.forEach((char, ci) => {
      if (!/\s/.test(char)) glyphs.push({ char, line: li, x: ci * adv, w: adv });
    });
    lineWidths.push(chars.length * adv);
  });
  return { glyphs, lineWidths, measured: false };
}

/** Seeded strike: 2–3 uneven partial dips inside the first ~200ms of a glyph lighting. */
function strikeKeyframes(rng: () => number): Keyframe[] {
  const dips = rng() < 0.5 ? 2 : 3;
  const total = 240;
  const frames: Keyframe[] = [{ opacity: 0, offset: 0 }];
  let t = 18 + rng() * 14;
  frames.push({ opacity: 0.85 + rng() * 0.15, offset: t / total });
  for (let d = 0; d < dips; d++) {
    t += 22 + rng() * 22;
    frames.push({ opacity: 0.2 + rng() * 0.25, offset: Math.min(t, 200) / total });
    t += 18 + rng() * 22;
    frames.push({ opacity: 0.8 + rng() * 0.2, offset: Math.min(t, 210) / total });
  }
  frames.push({ opacity: 1, offset: 1 });
  return frames;
}

/** Seeded ~400ms destabilise: exactly two partial dips, then a wobble back to steady. */
function flickerKeyframes(rng: () => number): Keyframe[] {
  return [
    { opacity: 1, offset: 0 },
    { opacity: 0.28 + rng() * 0.14, offset: 0.08 + rng() * 0.04 },
    { opacity: 0.88 + rng() * 0.1, offset: 0.2 },
    { opacity: 0.72 + rng() * 0.1, offset: 0.34 },
    { opacity: 0.38 + rng() * 0.14, offset: 0.46 + rng() * 0.04 },
    { opacity: 1, offset: 0.62 },
    { opacity: 0.9 + rng() * 0.06, offset: 0.8 },
    { opacity: 1, offset: 1 },
  ];
}

/**
 * Argon — a headline rendered as bent gas-discharge glass tubes. Each glyph
 * is an SVG text outline in three layers: a dim unlit tube, a bright lit core
 * with a white-hot center, and a blurred bloom. On view, ignition travels
 * along each tube glyph-by-glyph with an uneven strike flicker, then holds
 * with a near-imperceptible mains hum. The glyph nearest the pointer
 * destabilises briefly. Flicker is photosensitivity-budgeted (≤3 dips/s);
 * reduced motion renders the sign fully lit and steady.
 */
export function Argon({
  text = "Open late",
  gas = "argon",
  as = "h2",
  fontSize = 96,
  strokeWidth = 2.4,
  clips = true,
  igniteOnView = true,
  intensity = 1,
  idle = true,
  paused = false,
  ref,
  className,
  style,
  ...props
}: ArgonProps) {
  const reducedMotion = useReducedMotion();
  const staticMode = reducedMotion || paused;
  const Tag = as;
  const uid = React.useId().replace(/:/g, "");
  const filterId = `argon-bloom-${uid}`;
  const maskId = `argon-mask-${uid}`;

  const lines = React.useMemo(() => text.split("\n"), [text]);
  const seed = React.useMemo(() => hashString(text), [text]);
  const color = GAS_PRESETS[gas] ?? gas;
  const hot = `color-mix(in srgb, ${color} 42%, #ffffff)`;
  const k = Math.max(0, Math.min(2, intensity));

  const [layout, setLayout] = React.useState<Layout>(() => estimateLayout(lines, fontSize));
  const [igniteKey, setIgniteKey] = React.useState(0);
  const [lit, setLit] = React.useState(false);
  const [inView, setInView] = React.useState(false);
  const [docVisible, setDocVisible] = React.useState(true);

  const svgRef = React.useRef<SVGSVGElement>(null);
  const measureRefs = React.useRef<(SVGTextElement | null)[]>([]);
  const dashRefs = React.useRef<(SVGTextElement | null)[][]>([]);
  const glowRefs = React.useRef<(SVGGElement | null)[]>([]);
  const bloomRefs = React.useRef<(SVGTextElement | null)[]>([]);
  const fillRefs = React.useRef<(SVGTextElement | null)[]>([]);
  const playedKeyRef = React.useRef(0);
  const activeFlickers = React.useRef(new Map<number, Animation>());
  const dipTimes = React.useRef<number[]>([]);
  const flickerCount = React.useRef(0);
  const lastPointer = React.useRef(-Infinity);
  const hoverIndex = React.useRef(-1);

  // ---- geometry -----------------------------------------------------------
  const pad = Math.max(strokeWidth * 6, fontSize * 0.2);
  const lineHeight = fontSize * 1.18;
  const ascent = fontSize * 0.8;
  const clipSpace = clips ? fontSize * 0.2 : 0;
  const maxLineWidth = Math.max(1, ...layout.lineWidths);
  const vbW = maxLineWidth + pad * 2;
  const vbH = pad * 2 + ascent + (lines.length - 1) * lineHeight + fontSize * 0.22 + clipSpace;
  const baselineOf = (line: number) => pad + ascent + line * lineHeight;
  const lineOffset = (line: number) => pad + (maxLineWidth - (layout.lineWidths[line] ?? 0)) / 2;
  const dashLen = fontSize * DASH_EM;

  // ---- measure real glyph advances (kerning + letter-spacing included) -----
  const measure = React.useCallback(() => {
    const glyphs: Glyph[] = [];
    const lineWidths: number[] = [];
    lines.forEach((line, li) => {
      const el = measureRefs.current[li];
      if (!el) {
        lineWidths.push(0);
        return;
      }
      let width = 0;
      try {
        width = el.getComputedTextLength();
      } catch {
        width = 0;
      }
      lineWidths.push(width);
      let unit = 0;
      for (const char of Array.from(line)) {
        if (!/\s/.test(char)) {
          try {
            const start = el.getStartPositionOfChar(unit).x;
            const end = el.getEndPositionOfChar(unit + char.length - 1).x;
            glyphs.push({ char, line: li, x: start, w: Math.max(0, end - start) });
          } catch {
            /* unaddressable character — skip */
          }
        }
        unit += char.length;
      }
    });
    setLayout({ glyphs, lineWidths, measured: true });
  }, [lines]);

  React.useLayoutEffect(() => {
    measure();
    const fonts = typeof document !== "undefined" ? document.fonts : undefined;
    if (!fonts) return;
    let alive = true;
    const onLoad = () => alive && measure();
    fonts.ready.then(onLoad).catch(() => {});
    fonts.addEventListener?.("loadingdone", onLoad);
    return () => {
      alive = false;
      fonts.removeEventListener?.("loadingdone", onLoad);
    };
  }, [measure, fontSize]);

  // ---- visibility (in view + tab visible) ---------------------------------
  React.useEffect(() => {
    const node = svgRef.current;
    if (!node) return;
    if (typeof IntersectionObserver === "undefined") {
      setInView(true);
      return;
    }
    const io = new IntersectionObserver(([entry]) => setInView(!!entry?.isIntersecting), {
      threshold: 0.35,
    });
    io.observe(node);
    return () => io.disconnect();
  }, []);

  React.useEffect(() => {
    const onVis = () => setDocVisible(document.visibilityState !== "hidden");
    onVis();
    document.addEventListener("visibilitychange", onVis);
    return () => document.removeEventListener("visibilitychange", onVis);
  }, []);

  // ---- ignition trigger ---------------------------------------------------
  React.useEffect(() => {
    if (igniteKey !== 0) return;
    if (!igniteOnView || inView) setIgniteKey(1);
  }, [igniteOnView, inView, igniteKey]);

  // Static mode (reduced motion / paused): everything lit, nothing animating.
  React.useEffect(() => {
    if (!staticMode) return;
    playedKeyRef.current = Math.max(playedKeyRef.current, igniteKey);
    activeFlickers.current.forEach((a) => a.cancel());
    activeFlickers.current.clear();
    setLit(true);
  }, [staticMode, igniteKey]);

  const glyphCount = layout.glyphs.length;
  const measured = layout.measured;

  React.useLayoutEffect(() => {
    if (staticMode || !measured || igniteKey === 0 || playedKeyRef.current === igniteKey) return;
    playedKeyRef.current = igniteKey;
    setLit(false);
    activeFlickers.current.forEach((a) => a.cancel());
    activeFlickers.current.clear();

    const anims: Animation[] = [];
    const len = fontSize * DASH_EM;
    for (let i = 0; i < glyphCount; i++) {
      const delay = i * STAGGER;
      const rng = mulberry32(seed ^ Math.imul(i + 1, 0x9e3779b1) ^ Math.imul(igniteKey, 0x85ebca6b));
      for (const el of dashRefs.current[i] ?? []) {
        if (!el?.animate) continue;
        anims.push(
          el.animate([{ strokeDashoffset: len }, { strokeDashoffset: 0 }], {
            duration: DRAW_MS,
            delay,
            easing: "cubic-bezier(0.3, 0.15, 0.55, 1)",
            fill: "both",
          })
        );
      }
      const strike = strikeKeyframes(rng);
      const g = glowRefs.current[i];
      const b = bloomRefs.current[i];
      if (g?.animate) anims.push(g.animate(strike, { duration: 240, delay, fill: "both" }));
      if (b?.animate) anims.push(b.animate(strike, { duration: 240, delay, fill: "both" }));
      const f = fillRefs.current[i];
      if (f?.animate) {
        anims.push(
          f.animate([{ opacity: 0 }, { opacity: 1 }], {
            duration: 520,
            delay: delay + DRAW_MS * 0.3,
            easing: "ease-out",
            fill: "both",
          })
        );
      }
    }
    let cancelled = false;
    Promise.all(anims.map((a) => a.finished))
      .then(() => {
        if (cancelled) return;
        setLit(true);
        // Committed state now matches the end frames — release the animations.
        anims.forEach((a) => a.cancel());
      })
      .catch(() => {});
    return () => {
      cancelled = true;
      anims.forEach((a) => a.cancel());
    };
  }, [igniteKey, staticMode, measured, glyphCount, seed, fontSize]);

  // ---- flicker (hover / idle / handle) ------------------------------------
  const flicker = React.useCallback(
    (index?: number) => {
      if (staticMode || !lit || glyphCount === 0) return;
      const n = flickerCount.current++;
      const rng = mulberry32(seed ^ Math.imul(n + 17, 0x27d4eb2f));
      const i =
        index === undefined ? Math.floor(rng() * glyphCount) : Math.max(0, Math.min(glyphCount - 1, index));
      if (activeFlickers.current.has(i)) return;
      const now = performance.now();
      dipTimes.current = dipTimes.current.filter((t) => now - t < 1000);
      if (dipTimes.current.length + FLICKER_DIPS > MAX_DIPS_PER_SECOND) return;
      const frames = flickerKeyframes(rng);
      const opts: KeyframeAnimationOptions = { duration: 400, easing: "linear" };
      const g = glowRefs.current[i];
      const b = bloomRefs.current[i];
      const f = fillRefs.current[i];
      if (!g?.animate) return;
      dipTimes.current.push(now, now + 180);
      const a = g.animate(frames, opts);
      b?.animate?.(frames, opts);
      f?.animate?.(frames, opts);
      activeFlickers.current.set(i, a);
      const done = () => {
        if (activeFlickers.current.get(i) === a) activeFlickers.current.delete(i);
      };
      a.finished.then(done, done);
    },
    [staticMode, lit, glyphCount, seed]
  );

  React.useImperativeHandle(
    ref,
    () => ({
      replay: () => {
        if (staticMode) return;
        setIgniteKey((key) => key + 1);
      },
      flicker,
    }),
    [flicker, staticMode]
  );

  // Idle ghost: the sign occasionally destabilises on its own when nobody's pointing.
  const running = lit && !staticMode && inView && docVisible;
  React.useEffect(() => {
    if (!running || !idle) return;
    const rng = mulberry32(seed ^ 0x51ed27);
    let timer: ReturnType<typeof setTimeout>;
    const schedule = () => {
      timer = setTimeout(
        () => {
          if (performance.now() - lastPointer.current > 1500) flicker();
          schedule();
        },
        2600 + rng() * 3200
      );
    };
    schedule();
    return () => clearTimeout(timer);
  }, [running, idle, seed, flicker]);

  React.useEffect(() => {
    const flickers = activeFlickers.current;
    return () => {
      flickers.forEach((a) => a.cancel());
      flickers.clear();
    };
  }, []);

  const nearestGlyph = (clientX: number, clientY: number) => {
    const svg = svgRef.current;
    const ctm = svg?.getScreenCTM();
    if (!svg || !ctm) return -1;
    const p = new DOMPoint(clientX, clientY).matrixTransform(ctm.inverse());
    let best = -1;
    let bestD = fontSize * 0.9;
    layout.glyphs.forEach((g, i) => {
      const cx = lineOffset(g.line) + g.x + g.w / 2;
      const cy = baselineOf(g.line) - fontSize * 0.36;
      const d = Math.hypot((p.x - cx) * 1.15, p.y - cy);
      if (d < bestD) {
        bestD = d;
        best = i;
      }
    });
    return best;
  };

  const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {
    lastPointer.current = performance.now();
    const i = nearestGlyph(e.clientX, e.clientY);
    if (i !== hoverIndex.current) {
      hoverIndex.current = i;
      if (i >= 0) flicker(i);
    }
  };
  const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {
    lastPointer.current = performance.now();
    const i = nearestGlyph(e.clientX, e.clientY);
    if (i >= 0) flicker(i);
  };
  const onPointerLeave = () => {
    lastPointer.current = performance.now();
    hoverIndex.current = -1;
  };

  // ---- render -------------------------------------------------------------
  const restOffset = staticMode || lit ? 0 : dashLen;
  const restFill = staticMode || lit ? 1 : 0;
  const humming = lit && !staticMode;
  const bloomBlur = strokeWidth * (0.9 + 1.4 * k);
  const tubeTransition = "stroke 450ms ease, fill 450ms ease";

  const textProps = (g: Glyph) => ({
    x: lineOffset(g.line) + g.x,
    y: baselineOf(g.line),
  });

  return (
    <Tag
      data-crucible="argon"
      className={cn("relative inline-block max-w-full font-semibold", className)}
      style={style}
      {...props}
    >
      <span className="sr-only">{text}</span>
      <style>{`
        @keyframes crucible-argon-hum {
          0%, 100% { opacity: 1; }
          23% { opacity: 0.997; }
          41% { opacity: 1; }
          67% { opacity: 0.9975; }
          84% { opacity: 0.999; }
        }
        @keyframes crucible-argon-hum-bloom {
          0%, 100% { opacity: 1; }
          31% { opacity: 0.99; }
          58% { opacity: 1; }
          79% { opacity: 0.992; }
        }
        [data-crucible="argon"] [data-hum] {
          animation: crucible-argon-hum 2.3s ease-in-out infinite;
          animation-play-state: var(--argon-play, running);
        }
        [data-crucible="argon"] [data-hum="bloom"] {
          animation-name: crucible-argon-hum-bloom;
          animation-duration: 3.1s;
        }
        @media (prefers-reduced-motion: reduce) {
          [data-crucible="argon"] [data-hum] { animation: none; }
        }
      `}</style>
      <svg
        ref={svgRef}
        aria-hidden
        focusable="false"
        width={vbW}
        viewBox={`0 0 ${vbW} ${vbH}`}
        className="block h-auto max-w-full select-none overflow-visible"
        style={
          {
            visibility: layout.measured ? "visible" : "hidden",
            "--argon-play": running ? "running" : "paused",
          } as React.CSSProperties
        }
        onPointerMove={onPointerMove}
        onPointerDown={onPointerDown}
        onPointerLeave={onPointerLeave}
      >
        <defs>
          <filter id={filterId} x="-30%" y="-40%" width="160%" height="180%" colorInterpolationFilters="sRGB">
            <feGaussianBlur in="SourceGraphic" stdDeviation={bloomBlur} result="near" />
            <feGaussianBlur in="SourceGraphic" stdDeviation={bloomBlur * 3.4} result="far" />
            <feMerge>
              <feMergeNode in="far" />
              <feMergeNode in="near" />
            </feMerge>
          </filter>
          {/* Hides the inner half of every outline stroke — tubes read as clean
              rims, and overlapping contours in variable fonts never show. */}
          <mask id={maskId} maskUnits="userSpaceOnUse" x={0} y={0} width={vbW} height={vbH}>
            <rect x={0} y={0} width={vbW} height={vbH} fill="#fff" />
            <g style={{ fontSize }} fontFamily="inherit" fontWeight="inherit" fill="#000" stroke="none">
              {layout.glyphs.map((g, i) => (
                <text key={i} {...textProps(g)}>
                  {g.char}
                </text>
              ))}
            </g>
          </mask>
        </defs>

        <g style={{ fontSize }} fontFamily="inherit" fontWeight="inherit" strokeLinejoin="round" strokeLinecap="round">
          {/* measuring copies — never painted */}
          <g visibility="hidden">
            {lines.map((line, li) => (
              <text
                key={li}
                ref={(el) => {
                  measureRefs.current[li] = el;
                }}
                x={0}
                y={baselineOf(li)}
                style={{ whiteSpace: "pre" }}
              >
                {line}
              </text>
            ))}
          </g>

          {/* back-wire + mounting clips */}
          {clips && (
            <g>
              {lines.map((_, li) => {
                const lineGlyphs = layout.glyphs.filter((g) => g.line === li);
                if (lineGlyphs.length === 0) return null;
                const first = lineGlyphs[0]!;
                const last = lineGlyphs[lineGlyphs.length - 1]!;
                const off = lineOffset(li);
                const wy = baselineOf(li) + fontSize * 0.15;
                return (
                  <line
                    key={li}
                    x1={off + first.x - fontSize * 0.08}
                    x2={off + last.x + last.w + fontSize * 0.08}
                    y1={wy}
                    y2={wy}
                    stroke="rgba(255,255,255,0.09)"
                    strokeWidth={Math.max(1, fontSize * 0.012)}
                  />
                );
              })}
              {layout.glyphs.map((g, i) => {
                const cx = lineOffset(g.line) + g.x + g.w / 2;
                const by = baselineOf(g.line);
                const cw = fontSize * 0.085;
                const ch = fontSize * 0.06;
                return (
                  <g key={i}>
                    <line
                      x1={cx}
                      x2={cx}
                      y1={by + ch * 0.6}
                      y2={by + fontSize * 0.15}
                      stroke="rgba(255,255,255,0.12)"
                      strokeWidth={Math.max(1, fontSize * 0.01)}
                    />
                    <rect
                      x={cx - cw / 2}
                      y={by - ch * 0.35}
                      width={cw}
                      height={ch}
                      rx={ch * 0.3}
                      style={{
                        fill: `color-mix(in srgb, ${color} ${restFill ? 14 : 4}%, #2b2b30)`,
                        stroke: "rgba(255,255,255,0.2)",
                        transition: "fill 600ms ease",
                      }}
                      strokeWidth={Math.max(0.75, fontSize * 0.006)}
                    />
                  </g>
                );
              })}
            </g>
          )}

          {/* unlit glass tubes */}
          {layout.glyphs.map((g, i) => (
            <text key={i} {...textProps(g)} fill="rgba(255,255,255,0.03)" stroke="none">
              {g.char}
            </text>
          ))}
          <g mask={`url(#${maskId})`}>
            {layout.glyphs.map((g, i) => (
              <text
                key={i}
                {...textProps(g)}
                fill="none"
                stroke="rgba(226,232,240,0.2)"
                strokeWidth={strokeWidth * 1.5}
              >
                {g.char}
              </text>
            ))}
          </g>

          {/* bloom */}
          <g filter={`url(#${filterId})`} opacity={Math.min(1, 0.6 * k)} data-hum={humming ? "bloom" : undefined}>
            {layout.glyphs.map((g, i) => (
              <text
                key={i}
                ref={(el) => {
                  bloomRefs.current[i] = el;
                  (dashRefs.current[i] ??= [])[0] = el;
                }}
                {...textProps(g)}
                fill="none"
                strokeWidth={strokeWidth * (2.2 + k)}
                strokeDasharray={`${dashLen} ${dashLen}`}
                style={{ stroke: color, strokeDashoffset: restOffset, transition: tubeTransition }}
              >
                {g.char}
              </text>
            ))}
          </g>

          {/* gas glowing inside the glass */}
          <g data-hum={humming ? "core" : undefined}>
            {layout.glyphs.map((g, i) => (
              <text
                key={i}
                ref={(el) => {
                  fillRefs.current[i] = el;
                }}
                {...textProps(g)}
                stroke="none"
                style={{
                  fill: color,
                  fillOpacity: 0.08 * k,
                  opacity: restFill,
                  transition: tubeTransition,
                }}
              >
                {g.char}
              </text>
            ))}
          </g>

          {/* lit tubes: core rim + white-hot inner edge (outer half only, via mask) */}
          <g data-hum={humming ? "core" : undefined} mask={`url(#${maskId})`}>
            {layout.glyphs.map((g, i) => (
              <g
                key={i}
                ref={(el) => {
                  glowRefs.current[i] = el;
                }}
              >
                <text
                  ref={(el) => {
                    (dashRefs.current[i] ??= [])[1] = el;
                  }}
                  {...textProps(g)}
                  fill="none"
                  strokeWidth={strokeWidth * 2}
                  strokeDasharray={`${dashLen} ${dashLen}`}
                  style={{
                    stroke: color,
                    strokeOpacity: Math.min(1, 0.65 + 0.35 * k),
                    strokeDashoffset: restOffset,
                    transition: tubeTransition,
                  }}
                >
                  {g.char}
                </text>
                <text
                  ref={(el) => {
                    (dashRefs.current[i] ??= [])[2] = el;
                  }}
                  {...textProps(g)}
                  fill="none"
                  strokeWidth={strokeWidth * 0.9}
                  strokeDasharray={`${dashLen} ${dashLen}`}
                  style={{ stroke: hot, strokeDashoffset: restOffset, transition: tubeTransition }}
                >
                  {g.char}
                </text>
              </g>
            ))}
          </g>
        </g>
      </svg>
    </Tag>
  );
}
```
