← Commit history
SKILL.mdadded+55
@@ -0,0 +1,55 @@+---+name: circuit-trace-generators+description: Generate the Adom Nitrogen circuit-trace background as seeded SVG — either a seamless 900x450 tile you can repeat as a CSS background-image, or one continuous non-repeating field sized to a mount. Use when a page needs the PCB-trace backdrop, when asked to "generate a circuit background", "make a PCB trace pattern", "seamless circuit tile", "nitrogen hero background", or when tuning trace density, bus width, swirl, or the routed-light animation.+user-invocable: true+---++# Circuit trace backgrounds++Two engines. Pick by whether the output must repeat.++## Need a repeatable texture → tiling router++Emits a `900×450` tile routed on a torus, so it repeats seamlessly in both axes+and no traces cross the seam.++```bash+node src/generate-tile.mjs --seed traces --out tile.svg+```++Use it as `background-image` with `background-repeat: repeat`. Tuning flags are+kebab-case versions of the twelve generation knobs — `--turn-penalty` (higher =+longer, calmer runs), `--bus-width`, `--formations`, `--density`, `--weave`,+`--cramp`, `--horizontal-bias`, `--net-reach`, `--jog-bias`, `--stub-chance`,+`--inline-via-chance`, `--combs`. `--transparent` drops the board fill.++## Need a bespoke backdrop → field router++One continuous field at the mount's real size, packed with a flow field and+seeded vortices. Mount it in a page:++```js+import { createCircuitBackground } from "./src/circuit-background-v5.js";++const circuit = createCircuitBackground(document.querySelector("#hero"), {+  seed: "product-launch",+  animationSystem: "handshake",  // "none" | "conductor" | "handshake"+  motion: true,+});+```++`animationSystem: "handshake"` is the one to reach for on a dense board — it caps+concurrent transactions at 3, so per-frame cost does not grow with geometry.+`"none"` mounts a static SVG with no animation layer at all.++For geometry without a DOM, call `generateCircuitField(options)` and render+`pathData(trace.points)` yourself; each trace carries `points`, `length`,+`viaStart`, `viaEnd`.++## Both++Seeded — the same seed and dimensions give identical geometry. Dependency-free ES+modules. Board `#0d1117`, traces `#151b25`, routed light+`#00e6dc`/`#64abff`/`#c5b3ff`. Both honour `prefers-reduced-motion`.++See the page README for the full parameter reference and the two live demos.
docs/field-router.pngadded
⋯ 1 unchanged line ⋯
docs/hero.pngadded
⋯ 1 unchanged line ⋯
docs/tiling-router.pngadded
⋯ 1 unchanged line ⋯
install.shadded+10
@@ -0,0 +1,10 @@+#!/bin/sh+set -e++# The engines are dependency-free ES modules — there is nothing to compile or+# link. Installing just puts them on disk; point your page's import at src/,+# or run the tile CLI straight out of the module directory.++echo "installed: circuit-trace-generators"+echo "  engines:   $PWD/src/circuit-background.js (tiling), $PWD/src/circuit-background-v5.js (field)"+echo "  tile CLI:  node $PWD/src/generate-tile.mjs --seed traces --out tile.svg"
package.jsonadded+43
@@ -0,0 +1,43 @@+{+  "slug": "circuit-trace-generators",+  "title": "Circuit Trace Generators",+  "type": "app",+  "version": "0.1.0",+  "description": "Two dependency-free, seeded SVG generators for the Adom Nitrogen hero background: a tiling router that emits a seamless 900x450 PCB-trace tile from a torus occupancy grid, and a field router that packs one continuous non-repeating trace field using a flow field and seeded vortices.",+  "tags": [+    "svg",+    "generative",+    "background",+    "pcb",+    "brand"+  ],+  "dependencies": {},+  "discovery_triggers": [+    "generate a circuit background",+    "make a PCB trace background",+    "nitrogen hero background",+    "seamless circuit tile",+    "procedural trace pattern"+  ],+  "discovery_pitch": "Reach for this when a page needs the Adom circuit-trace backdrop — either a seamless tile you can repeat as a CSS background-image, or one continuous generated field sized to the mount.",+  "sample_prompts": [+    {+      "label": "Seamless tile",+      "prompt": "generate a seamless circuit-trace tile I can use as a repeating CSS background"+    },+    {+      "label": "Hero backdrop",+      "prompt": "put the Nitrogen circuit background behind this hero section"+    }+  ],+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "hero": {+    "headline": "Circuit Trace Generators",+    "subhead": "Two seeded SVG routers for the Nitrogen hero background — one tiles a torus, one packs a continuous field.",+    "screenshot": "docs/hero.png",+    "accent": "#00e6dc"+  }+}
src/circuit-animation-conductor.jsadded+429
@@ -0,0 +1,429 @@+const SVG_NS = "http://www.w3.org/2000/svg";++function svgElement(name, attributes = {}) {+  const element = document.createElementNS(SVG_NS, name);+  for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, String(value));+  return element;+}++function pathData(points) {+  return points.map(([x, y], index) => `${index ? "L" : "M"}${x.toFixed(2)},${y.toFixed(2)}`).join("");+}++function integer(random, min, max) {+  return Math.floor(random() * (max - min + 1)) + min;+}++function weighted(random, entries) {+  let total = 0;+  for (const [, weight] of entries) total += weight;+  let roll = random() * total;+  for (const [value, weight] of entries) {+    roll -= weight;+    if (roll <= 0) return value;+  }+  return entries.at(-1)?.[0];+}++function shuffle(random, values) {+  for (let index = values.length - 1; index > 0; index -= 1) {+    const swap = integer(random, 0, index);+    [values[index], values[swap]] = [values[swap], values[index]];+  }+  return values;+}++export function createConductorAnimation(svg, field, config, random) {+  const ambientLayer = svgElement("g", {+    "data-animation": "conductor-ambient", fill: "none", "pointer-events": "none",+  });+  const signalLayer = svgElement("g", {+    "data-animation": "conductor-signals", fill: "none", "pointer-events": "none",+  });+  svg.append(ambientLayer, signalLayer);++  let timer;+  let waveTimer;+  let activeSignals = 0;+  let ambientCurrentCount = 0;+  let ambientNodeCount = 0;+  let wavesTriggered = 0;+  let pointer = null;+  let lastPointerWake = 0;+  const scheduled = new Set();+  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;++  function life() {+    return Math.max(0, Math.min(1.5, config.liveliness));+  }++  function featureScale() {+    return Math.max(0.25, config.featureScale);+  }++  function trackTimeout(callback, delay) {+    const timeout = window.setTimeout(() => {+      scheduled.delete(timeout);+      callback();+    }, Math.max(0, delay));+    scheduled.add(timeout);+    return timeout;+  }++  function clearScheduled() {+    for (const timeout of scheduled) window.clearTimeout(timeout);+    scheduled.clear();+  }++  function nearestTrace(origin = pointer) {+    if (!origin || !field.traces.length) return null;+    let result = null;+    let best = Infinity;+    const attempts = Math.min(72, field.traces.length);+    for (let attempt = 0; attempt < attempts; attempt += 1) {+      const trace = field.traces[integer(random, 0, field.traces.length - 1)];+      const point = trace.points[Math.floor(trace.points.length / 2)];+      const distance = Math.hypot(point[0] - origin.x, point[1] - origin.y);+      if (distance < best) {+        best = distance;+        result = trace;+      }+    }+    return result;+  }++  function flashNode(point, color, delay = 0, intensity = 1) {+    if (delay > 0) {+      trackTimeout(() => flashNode(point, color, 0, intensity), delay);+      return;+    }+    if (!signalLayer.isConnected || reducedMotion || !config.motion || !config.nodeFlashes) return;+    const scale = featureScale();+    const radius = config.viaOuterRadius * scale;+    const ring = svgElement("circle", {+      cx: point[0], cy: point[1], r: radius * 0.7,+      fill: "none", stroke: color,+      "stroke-width": Math.max(0.8, config.traceWidth * scale * 0.85),+      opacity: 0,+    });+    ring.style.filter = `drop-shadow(0 0 ${3.5 * scale}px ${color})`;+    signalLayer.appendChild(ring);+    const duration = 480 + intensity * 220;+    ring.animate(+      [+        { opacity: 0, transform: "scale(.45)" },+        { offset: 0.18, opacity: Math.min(1, 0.55 + intensity * 0.25), transform: "scale(.9)" },+        { opacity: 0, transform: `scale(${2.2 + intensity * 0.7})` },+      ],+      { duration, easing: "cubic-bezier(.16,.8,.3,1)", fill: "forwards" },+    );+    ring.style.transformBox = "fill-box";+    ring.style.transformOrigin = "center";+    trackTimeout(() => ring.remove(), duration + 40);+  }++  function spawnSignal({+    nearPointer = false,+    trace = null,+    delay = 0,+    reverse,+    color,+    intensity = 1,+    lengthScale = 1,+    speedScale = 1,+    endpointFlash = false,+  } = {}) {+    if (delay > 0) {+      trackTimeout(() => spawnSignal({+        nearPointer, trace, reverse, color, intensity, lengthScale, speedScale, endpointFlash,+      }), delay);+      return true;+    }+    if (!config.motion || reducedMotion || activeSignals >= config.maxSignals || !field.traces.length) return false;+    const selected = trace || (nearPointer && nearestTrace()) || field.traces[integer(random, 0, field.traces.length - 1)];+    const reversed = reverse ?? random() < 0.5;+    const points = reversed ? [...selected.points].reverse() : selected.points;+    const selectedColor = color || weighted(random, config.signalColors);+    const scale = featureScale();+    const requestedLength = config.signalLength * scale * lengthScale;+    const signalLength = Math.max(12 * scale, Math.min(requestedLength, selected.length * 0.72));+    const signalSpeed = config.signalSpeed * scale * (0.78 + life() * 0.27) * speedScale;+    const duration = Math.max(460, ((selected.length + signalLength) / signalSpeed) * 1000);+    const dashGap = selected.length + signalLength;+    const group = svgElement("g", { "data-signal": "" });+    const halo = svgElement("path", {+      d: pathData(points), stroke: selectedColor,+      "stroke-width": (5.2 + intensity * 2.2) * scale,+      "stroke-linecap": "round",+      "stroke-dasharray": `${signalLength} ${dashGap}`,+      "stroke-dashoffset": signalLength,+      opacity: 0,+    });+    const core = svgElement("path", {+      d: pathData(points), stroke: selectedColor,+      "stroke-width": (1.25 + intensity * 1.25) * scale,+      "stroke-linecap": "round",+      "stroke-dasharray": `${signalLength} ${dashGap}`,+      "stroke-dashoffset": signalLength,+      opacity: 0,+    });+    halo.style.filter = `blur(${1.6 * scale}px) drop-shadow(0 0 ${4.5 * scale}px ${selectedColor})`;+    core.style.filter = `drop-shadow(0 0 ${2.8 * scale}px ${selectedColor})`;+    if (config.signalHalos) group.appendChild(halo);+    group.appendChild(core);+    signalLayer.appendChild(group);+    activeSignals += 1;+    const movement = [+      { strokeDashoffset: signalLength, opacity: 0 },+      { offset: 0.08, opacity: 0.92 },+      { offset: 0.84, opacity: 0.72 },+      { strokeDashoffset: -selected.length, opacity: 0 },+    ];+    if (config.signalHalos) {+      halo.animate(movement.map((frame) => ({ ...frame, opacity: (frame.opacity || 0) * 0.22 * intensity })), {+        duration, easing: "linear", fill: "forwards",+      });+    }+    core.animate(movement, { duration, easing: "linear", fill: "forwards" });+    if (endpointFlash) {+      flashNode(points[0], selectedColor, 0, intensity);+      flashNode(points.at(-1), selectedColor, duration * 0.87, intensity * 0.8);+    }+    trackTimeout(() => {+      group.remove();+      activeSignals = Math.max(0, activeSignals - 1);+    }, duration + 100);+    return true;+  }++  function spawnConvoy({ nearPointer = false, trace = null } = {}) {+    if (!config.motion || !config.signalConvoys || reducedMotion || !field.traces.length) return;+    const selected = trace || (nearPointer && nearestTrace()) || field.traces[integer(random, 0, field.traces.length - 1)];+    const count = Math.max(2, Math.min(4, Math.round(2 + life() + random())));+    const selectedColor = weighted(random, config.signalColors);+    const reverse = random() < 0.5;+    for (let index = 0; index < count; index += 1) {+      spawnSignal({+        trace: selected,+        reverse,+        color: selectedColor,+        delay: index * (78 + random() * 54),+        intensity: 0.62 + index * 0.08,+        lengthScale: 0.28 + random() * 0.22,+        speedScale: 0.92 + random() * 0.18,+        endpointFlash: index === 0,+      });+    }+  }++  function ripple(origin, color) {+    if (!signalLayer.isConnected) return;+    const scale = featureScale();+    const startRadius = config.pitch * scale * 0.8;+    const ring = svgElement("circle", {+      cx: origin.x, cy: origin.y, r: startRadius,+      fill: "none", stroke: color,+      "stroke-width": Math.max(0.7, config.traceWidth * scale * 0.72),+      opacity: 0.72,+    });+    ring.style.filter = `drop-shadow(0 0 ${4 * scale}px ${color})`;+    signalLayer.appendChild(ring);+    ring.animate(+      [+        { opacity: 0.72, transform: "scale(.35)" },+        { offset: 0.3, opacity: 0.34 },+        { opacity: 0, transform: "scale(6)" },+      ],+      { duration: 980, easing: "cubic-bezier(.12,.72,.18,1)", fill: "forwards" },+    );+    ring.style.transformBox = "fill-box";+    ring.style.transformOrigin = "center";+    trackTimeout(() => ring.remove(), 1040);+  }++  function spawnWave(origin = null) {+    if (!config.motion || !config.signalWaves || reducedMotion || !field.traces.length) return false;+    const source = origin || {+      x: field.width * (0.14 + random() * 0.72),+      y: field.height * (0.14 + random() * 0.72),+    };+    const selectedColor = weighted(random, config.signalColors);+    ripple(source, selectedColor);+    const ranked = field.traces.map((trace) => {+      const midpoint = trace.points[Math.floor(trace.points.length / 2)];+      return { trace, distance: Math.hypot(midpoint[0] - source.x, midpoint[1] - source.y) };+    }).sort((a, b) => a.distance - b.distance);+    const count = Math.min(ranked.length, Math.max(6, Math.round(6 + life() * 8)));+    const outerDistance = ranked[count - 1]?.distance || 1;+    for (let index = 0; index < count; index += 1) {+      const { trace, distance } = ranked[index];+      const start = trace.points[0];+      const end = trace.points.at(-1);+      const reverse = Math.hypot(end[0] - source.x, end[1] - source.y)+        < Math.hypot(start[0] - source.x, start[1] - source.y);+      spawnSignal({+        trace,+        reverse,+        color: selectedColor,+        delay: 90 + distance / outerDistance * 560 + index * 14,+        intensity: 0.64 + (1 - distance / outerDistance) * 0.34,+        lengthScale: 0.44 + random() * 0.2,+        speedScale: 1.02 + random() * 0.18,+        endpointFlash: index % 4 === 0,+      });+    }+    wavesTriggered += 1;+    return true;+  }++  function seedAmbientLife() {+    ambientCurrentCount = 0;+    ambientNodeCount = 0;+    if (!config.motion || reducedMotion || life() <= 0.02 || !field.traces.length) return;+    const scale = featureScale();+    const eligible = field.traces.filter((trace) => trace.length > 28 * scale);+    const currents = config.ambientCurrents+      ? Math.min(eligible.length, Math.max(1, Math.round(config.ambientCurrentCount * life())))+      : 0;+    shuffle(random, eligible);+    for (const trace of eligible.slice(0, currents)) {+      const color = weighted(random, config.signalColors);+      const dash = Math.max(12 * scale, trace.length * (0.07 + random() * 0.1));+      const current = svgElement("path", {+        d: pathData(random() < 0.5 ? [...trace.points].reverse() : trace.points),+        stroke: color,+        "stroke-width": config.traceWidth * scale * (0.9 + random() * 0.55),+        "stroke-linecap": "round",+        "stroke-dasharray": `${dash} ${trace.length + dash}`,+        "stroke-dashoffset": dash,+        opacity: 0,+      });+      current.style.filter = `drop-shadow(0 0 ${2.2 * scale}px ${color})`;+      ambientLayer.appendChild(current);+      const duration = (6400 + random() * 9000) / (0.72 + life() * 0.35);+      current.animate(+        [+          { strokeDashoffset: dash, opacity: 0 },+          { offset: 0.14, opacity: 0.08 + life() * 0.055 },+          { offset: 0.78, opacity: 0.05 + life() * 0.04 },+          { strokeDashoffset: -trace.length, opacity: 0 },+        ],+        { duration, delay: -random() * duration, iterations: Infinity, easing: "linear" },+      );+      ambientCurrentCount += 1;+    }++    const nodes = config.ambientNodes+      ? Math.min(field.traces.length, Math.max(1, Math.round(config.ambientNodeCount * life())))+      : 0;+    for (let index = 0; index < nodes; index += 1) {+      const trace = field.traces[integer(random, 0, field.traces.length - 1)];+      const point = random() < 0.5 ? trace.points[0] : trace.points.at(-1);+      const color = weighted(random, config.signalColors);+      const node = svgElement("circle", {+        cx: point[0], cy: point[1],+        r: config.viaOuterRadius * scale * (0.34 + random() * 0.18),+        fill: color, opacity: 0,+      });+      node.style.filter = `drop-shadow(0 0 ${3.2 * scale}px ${color})`;+      node.style.transformBox = "fill-box";+      node.style.transformOrigin = "center";+      ambientLayer.appendChild(node);+      const duration = 2600 + random() * 4300;+      node.animate(+        [+          { opacity: 0, transform: "scale(.65)" },+          { offset: 0.48, opacity: 0.16 + life() * 0.11, transform: "scale(1.22)" },+          { opacity: 0, transform: "scale(.72)" },+        ],+        { duration, delay: -random() * duration, iterations: Infinity, easing: "ease-in-out" },+      );+      ambientNodeCount += 1;+    }+  }++  function pointerMoved(origin) {+    pointer = origin;+    if (!pointer) return;+    const wakeInterval = config.pointerWakeInterval / Math.max(0.55, 0.55 + life() * 0.5);+    if (config.motion && config.pointerWake && !reducedMotion && pointer.at - lastPointerWake > wakeInterval) {+      lastPointerWake = pointer.at;+      spawnSignal({+        nearPointer: true,+        intensity: 0.5 + life() * 0.16,+        lengthScale: 0.34,+        speedScale: 1.3,+      });+    }+  }++  function clicked(origin) {+    pointer = origin;+    spawnWave(pointer);+    if (config.signalConvoys) trackTimeout(() => spawnConvoy({ nearPointer: true }), 160);+  }++  function stop() {+    window.clearInterval(timer);+    window.clearInterval(waveTimer);+    timer = undefined;+    waveTimer = undefined;+    clearScheduled();+  }++  function start() {+    stop();+    if (!config.motion || reducedMotion || life() <= 0.02) return;+    seedAmbientLife();+    const interval = Math.max(130, config.signalInterval / (0.55 + life() * 0.78));+    timer = window.setInterval(() => {+      if (document.hidden) return;+      const nearPointer = pointer && performance.now() - pointer.at < 3200;+      if (config.signalConvoys && random() < 0.12 + life() * 0.13) spawnConvoy({ nearPointer });+      else spawnSignal({+        nearPointer,+        intensity: 0.62 + random() * 0.34,+        lengthScale: 0.52 + random() * 0.58,+        speedScale: 0.86 + random() * 0.34,+        endpointFlash: random() < 0.12,+      });+    }, interval);+    if (config.signalWaves) {+      const intervalForWaves = Math.max(2800, config.waveInterval / (0.66 + life() * 0.48));+      waveTimer = window.setInterval(() => {+        if (!document.hidden) spawnWave();+      }, intervalForWaves);+    }+    if (config.signalConvoys) trackTimeout(() => spawnConvoy(), 360);+    if (config.signalWaves) trackTimeout(() => spawnWave(), 1450);+  }++  function destroy() {+    stop();+    ambientLayer.remove();+    signalLayer.remove();+  }++  return {+    start,+    stop,+    destroy,+    pointerMoved,+    clicked,+    trigger: spawnWave,+    get stats() {+      return {+        system: "original-conductor",+        activeSignals,+        ambientCurrentCount,+        ambientNodeCount,+        wavesTriggered,+        reducedMotion,+        signalHalos: config.signalHalos,+        signalConvoys: config.signalConvoys,+        signalWaves: config.signalWaves,+        pointerWake: config.pointerWake,+      };+    },+  };+}
src/circuit-animation-handshake.jsadded+428
@@ -0,0 +1,428 @@+const TAU = Math.PI * 2;++function clamp(value, minimum = 0, maximum = 1) {+  return Math.max(minimum, Math.min(maximum, value));+}++function smoothstep(value) {+  const amount = clamp(value);+  return amount * amount * (3 - 2 * amount);+}++function cumulativeLengths(points) {+  const cumulative = [0];+  for (let index = 1; index < points.length; index += 1) {+    cumulative.push(cumulative[index - 1] + Math.hypot(+      points[index][0] - points[index - 1][0],+      points[index][1] - points[index - 1][1],+    ));+  }+  return cumulative;+}++function orientedRoute(points) {+  const cumulative = cumulativeLengths(points);+  return { points, cumulative, length: cumulative.at(-1) || 0 };+}++function pointAlong(route, distance) {+  const target = clamp(distance, 0, route.length);+  let upper = 1;+  while (upper < route.cumulative.length && route.cumulative[upper] < target) upper += 1;+  if (upper >= route.points.length) return route.points.at(-1);+  const lower = upper - 1;+  const span = Math.max(0.0001, route.cumulative[upper] - route.cumulative[lower]);+  const progress = (target - route.cumulative[lower]) / span;+  return [+    route.points[lower][0] + (route.points[upper][0] - route.points[lower][0]) * progress,+    route.points[lower][1] + (route.points[upper][1] - route.points[lower][1]) * progress,+  ];+}++function strokeInterval(context, route, startDistance, endDistance) {+  const start = clamp(startDistance, 0, route.length);+  const end = clamp(endDistance, 0, route.length);+  if (end - start < 0.05) return false;++  context.beginPath();+  const first = pointAlong(route, start);+  context.moveTo(first[0], first[1]);+  for (let index = 1; index < route.points.length - 1; index += 1) {+    const distance = route.cumulative[index];+    if (distance > start && distance < end) {+      context.lineTo(route.points[index][0], route.points[index][1]);+    }+  }+  const last = pointAlong(route, end);+  context.lineTo(last[0], last[1]);+  context.stroke();+  return true;+}++function segmentTouchesRect(start, end, rect) {+  return Math.max(start[0], end[0]) >= rect.left+    && Math.min(start[0], end[0]) <= rect.right+    && Math.max(start[1], end[1]) >= rect.top+    && Math.min(start[1], end[1]) <= rect.bottom;+}++function routeTouchesRect(points, rect) {+  for (let index = 1; index < points.length; index += 1) {+    if (segmentTouchesRect(points[index - 1], points[index], rect)) return true;+  }+  return false;+}++function brandColor(x, width) {+  const position = x / Math.max(1, width);+  if (position < 0.42) return "#00e6dc";+  if (position < 0.74) return "#49a9ff";+  return "#a58cff";+}++export function createHandshakeAnimation(mount, field, config, random, { readingZone = null } = {}) {+  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;+  const pixelWidth = Math.max(1, mount.clientWidth);+  const pixelHeight = Math.max(1, mount.clientHeight);+  const pixelBudgetRatio = Math.sqrt(1_450_000 / Math.max(1, pixelWidth * pixelHeight));+  const pixelRatio = Math.max(1, Math.min(window.devicePixelRatio || 1, 1.5, pixelBudgetRatio));+  const canvas = document.createElement("canvas");+  canvas.setAttribute("aria-hidden", "true");+  canvas.setAttribute("data-animation", "via-handshake-canvas");+  Object.assign(canvas.style, {+    position: "absolute",+    inset: "0",+    width: "100%",+    height: "100%",+    display: "block",+    pointerEvents: "none",+    zIndex: "1",+    contain: "strict",+  });+  canvas.width = Math.max(1, Math.round(pixelWidth * pixelRatio));+  canvas.height = Math.max(1, Math.round(pixelHeight * pixelRatio));+  mount.appendChild(canvas);++  const context = canvas.getContext("2d", { alpha: true, desynchronized: true })+    || canvas.getContext("2d");+  const targetFrameRate = canvas.width * canvas.height > 1_200_000 ? 30 : 45;+  const frameInterval = 1000 / targetFrameRate;+  const scale = Math.max(0.25, config.featureScale);+  const maxTransactions = Math.max(1, Math.min(4, Math.round(config.maxHandshakes || 3)));+  const routes = field.traces.map((trace, index) => {+    const forward = orientedRoute(trace.points);+    const reverse = orientedRoute([...trace.points].reverse());+    const midpoint = trace.points[Math.floor(trace.points.length / 2)];+    return { index, trace, forward, reverse, midpoint, length: forward.length };+  });++  function readingRect() {+    const mountRect = mount.getBoundingClientRect();+    const zoneRect = readingZone?.getBoundingClientRect?.();+    if (zoneRect && mountRect.width > 0 && mountRect.height > 0 && zoneRect.width > 0 && zoneRect.height > 0) {+      const scaleX = field.width / mountRect.width;+      const scaleY = field.height / mountRect.height;+      const paddingX = Math.max(38, zoneRect.width * 0.08) * scaleX;+      const paddingY = Math.max(28, zoneRect.height * 0.1) * scaleY;+      return {+        left: (zoneRect.left - mountRect.left) * scaleX - paddingX,+        right: (zoneRect.right - mountRect.left) * scaleX + paddingX,+        top: (zoneRect.top - mountRect.top) * scaleY - paddingY,+        bottom: (zoneRect.bottom - mountRect.top) * scaleY + paddingY,+      };+    }+    return {+      left: field.width * 0.13,+      right: field.width * 0.87,+      top: field.height * 0.26,+      bottom: field.height * 0.74,+    };+  }++  const quietZone = readingRect();+  const minimumLength = Math.max(28 * scale, Math.hypot(field.width, field.height) * 0.035);+  const maximumLength = Math.hypot(field.width, field.height) * 0.64;+  const inset = config.viaOuterRadius * scale * 1.4;+  const insetRoutes = routes.filter(({ trace, length }) => {+    const start = trace.points[0];+    const end = trace.points.at(-1);+    return trace.viaStart && trace.viaEnd+      && length >= minimumLength+      && length <= maximumLength+      && start[0] > inset && start[0] < field.width - inset+      && start[1] > inset && start[1] < field.height - inset+      && end[0] > inset && end[0] < field.width - inset+      && end[1] > inset && end[1] < field.height - inset;+  });+  const outsideCopy = insetRoutes.filter(({ trace }) => !routeTouchesRect(trace.points, quietZone));+  const outsideMidpoint = insetRoutes.filter(({ midpoint }) => !(+    midpoint[0] >= quietZone.left && midpoint[0] <= quietZone.right+    && midpoint[1] >= quietZone.top && midpoint[1] <= quietZone.bottom+  ));+  const eligibleRoutes = outsideCopy.length >= 12+    ? outsideCopy+    : outsideMidpoint.length >= 12 ? outsideMidpoint : insetRoutes.length ? insetRoutes : routes;++  let frameRequest;+  let running = false;+  let frameCount = 0;+  let transactionsCompleted = 0;+  let transactionsTriggered = 0;+  let lastPaintTime = 0;+  let nextSpawnAt = 0;+  let pointer = null;+  const transactions = [];+  const recentRoutes = [];++  function life() {+    return clamp(config.liveliness, 0, 1.5);+  }++  function activeRoute(index) {+    return transactions.some((transaction) => transaction.route.index === index);+  }++  function selectRoute(origin = null) {+    const available = eligibleRoutes.filter(({ index }) => !activeRoute(index));+    if (!available.length) return null;+    let selected = available[Math.floor(random() * available.length)];+    let bestScore = -Infinity;+    const attempts = Math.min(36, available.length);+    const source = origin || (pointer && performance.now() - pointer.at < 2400 ? pointer : null);+    for (let attempt = 0; attempt < attempts; attempt += 1) {+      const candidate = available[Math.floor(random() * available.length)];+      const targetLength = Math.hypot(field.width, field.height) * 0.16;+      const lengthScore = Math.exp(-Math.abs(candidate.length - targetLength) / Math.max(1, targetLength));+      const recentPenalty = recentRoutes.includes(candidate.index) ? 1.2 : 0;+      let proximityScore = 0;+      if (source) {+        const first = candidate.trace.points[0];+        const last = candidate.trace.points.at(-1);+        const distance = Math.min(+          Math.hypot(first[0] - source.x, first[1] - source.y),+          Math.hypot(last[0] - source.x, last[1] - source.y),+        );+        proximityScore = 1 - distance / Math.max(field.width, field.height);+      }+      const score = random() * 0.7 + lengthScore * 0.8 + proximityScore * 1.35 - recentPenalty;+      if (score > bestScore) {+        bestScore = score;+        selected = candidate;+      }+    }+    return selected;+  }++  function spawnTransaction(origin = null, timestamp = performance.now(), triggered = false) {+    if (!running || reducedMotion || !config.motion || transactions.length >= maxTransactions) return false;+    const route = selectRoute(origin);+    if (!route || route.length < 1) return false;+    const first = route.trace.points[0];+    const last = route.trace.points.at(-1);+    const reverse = origin+      ? Math.hypot(last[0] - origin.x, last[1] - origin.y) < Math.hypot(first[0] - origin.x, first[1] - origin.y)+      : random() < 0.5;+    const outbound = reverse ? route.reverse : route.forward;+    const acknowledgement = reverse ? route.forward : route.reverse;+    const source = outbound.points[0];+    const destination = outbound.points.at(-1);+    const packetLength = clamp(+      (config.handshakeLength || 48) * scale,+      route.length * 0.12,+      route.length * 0.42,+    );+    const speed = (config.handshakeSpeed || 230) * scale * (0.84 + life() * 0.2);+    const outboundDuration = clamp((route.length + packetLength) / speed * 1000, 940, 2350);+    transactions.push({+      route,+      outbound,+      acknowledgement,+      source,+      destination,+      packetLength,+      sourceColor: brandColor(source[0], field.width),+      destinationColor: brandColor(destination[0], field.width),+      startedAt: timestamp,+      chargeDuration: 320,+      outboundDuration,+      holdDuration: 190,+      acknowledgementDuration: Math.max(820, outboundDuration * 0.82),+      settleDuration: 360,+      triggered,+    });+    recentRoutes.push(route.index);+    if (recentRoutes.length > 7) recentRoutes.shift();+    if (triggered) transactionsTriggered += 1;+    return true;+  }++  function scheduleNext(timestamp, initial = false) {+    const interval = (config.handshakeInterval || 1650) / (0.82 + life() * 0.18);+    nextSpawnAt = timestamp + (initial ? 320 : interval * (0.72 + random() * 0.62));+  }++  function drawPacket(route, packetLength, progress, color, opacity) {+    const travel = progress * (route.length + packetLength);+    const head = clamp(travel, 0, route.length);+    const tail = clamp(travel - packetLength, 0, route.length);+    if (head - tail < 0.05) return false;+    context.strokeStyle = color;+    context.lineCap = "round";+    context.lineJoin = "round";+    context.lineWidth = config.traceWidth * scale * 4.4;+    context.globalAlpha = opacity * 0.1;+    strokeInterval(context, route, tail, head);+    context.lineWidth = config.traceWidth * scale * 1.3;+    context.globalAlpha = opacity * 0.82;+    strokeInterval(context, route, tail, head);+    return true;+  }++  function drawVia(point, color, energy) {+    if (energy <= 0.002) return;+    const radius = (config.viaOuterRadius + config.viaInnerRadius) * scale * 0.5;+    const ringWidth = Math.max(0.7 * scale, (config.viaOuterRadius - config.viaInnerRadius) * scale * 0.72);+    context.strokeStyle = color;+    context.lineCap = "round";+    context.lineWidth = ringWidth * 3.1;+    context.globalAlpha = energy * 0.08;+    context.beginPath();+    context.arc(point[0], point[1], radius, 0, TAU);+    context.stroke();+    context.lineWidth = ringWidth;+    context.globalAlpha = energy * 0.72;+    context.beginPath();+    context.arc(point[0], point[1], radius, 0, TAU);+    context.stroke();+  }++  function drawTransaction(transaction, timestamp) {+    const elapsed = timestamp - transaction.startedAt;+    const chargeEnd = transaction.chargeDuration;+    const outboundEnd = chargeEnd + transaction.outboundDuration;+    const holdEnd = outboundEnd + transaction.holdDuration;+    const acknowledgementEnd = holdEnd + transaction.acknowledgementDuration;+    const end = acknowledgementEnd + transaction.settleDuration;+    let sourceEnergy = 0;+    let destinationEnergy = 0;++    if (elapsed < chargeEnd) {+      sourceEnergy = smoothstep(elapsed / transaction.chargeDuration);+    } else if (elapsed < outboundEnd) {+      const progress = (elapsed - chargeEnd) / transaction.outboundDuration;+      drawPacket(transaction.outbound, transaction.packetLength, progress, transaction.sourceColor, 0.92);+      sourceEnergy = 1 - progress * 0.76;+      destinationEnergy = smoothstep((progress - 0.72) / 0.28) * 0.92;+    } else if (elapsed < holdEnd) {+      sourceEnergy = 0.18;+      destinationEnergy = 0.92;+    } else if (elapsed < acknowledgementEnd) {+      const progress = (elapsed - holdEnd) / transaction.acknowledgementDuration;+      drawPacket(transaction.acknowledgement, transaction.packetLength * 0.72, progress, transaction.destinationColor, 0.5);+      destinationEnergy = 0.92 - progress * 0.7;+      sourceEnergy = 0.18 + smoothstep((progress - 0.72) / 0.28) * 0.58;+    } else if (elapsed < end) {+      const progress = smoothstep((elapsed - acknowledgementEnd) / transaction.settleDuration);+      sourceEnergy = 0.76 * (1 - progress);+      destinationEnergy = 0.22 * (1 - progress);+    } else {+      return false;+    }++    drawVia(transaction.source, transaction.sourceColor, sourceEnergy);+    drawVia(transaction.destination, transaction.destinationColor, destinationEnergy);+    return true;+  }++  function renderFrame(timestamp) {+    if (!running || !canvas.isConnected || !context) return;+    frameRequest = window.requestAnimationFrame(renderFrame);+    if (document.hidden) return;+    const sincePaint = lastPaintTime ? timestamp - lastPaintTime : frameInterval;+    if (sincePaint < frameInterval) return;+    lastPaintTime = lastPaintTime ? timestamp - (sincePaint % frameInterval) : timestamp;+    frameCount += 1;++    if (timestamp >= nextSpawnAt) {+      spawnTransaction(null, timestamp);+      scheduleNext(timestamp);+    }++    context.setTransform(1, 0, 0, 1, 0, 0);+    context.clearRect(0, 0, canvas.width, canvas.height);+    context.setTransform(canvas.width / field.width, 0, 0, canvas.height / field.height, 0, 0);+    context.globalCompositeOperation = "source-over";++    for (let index = transactions.length - 1; index >= 0; index -= 1) {+      if (!drawTransaction(transactions[index], timestamp)) {+        transactions.splice(index, 1);+        transactionsCompleted += 1;+      }+    }+    context.globalAlpha = 1;+  }++  function pointerMoved(point) {+    pointer = point ? { ...point, at: performance.now() } : null;+  }++  function pointerLeft() {+    if (pointer) pointer.at = performance.now() - 3000;+  }++  function trigger(origin = null) {+    return spawnTransaction(origin, performance.now(), true);+  }++  function start() {+    if (running || reducedMotion || !config.motion || !context || config.liveliness <= 0.02) return;+    running = true;+    frameCount = 0;+    lastPaintTime = 0;+    const now = performance.now();+    scheduleNext(now, true);+    frameRequest = window.requestAnimationFrame(renderFrame);+  }++  function stop() {+    running = false;+    window.cancelAnimationFrame(frameRequest);+    frameRequest = undefined;+    lastPaintTime = 0;+  }++  function destroy() {+    stop();+    transactions.length = 0;+    canvas.remove();+  }++  return {+    start,+    stop,+    destroy,+    pointerMoved,+    pointerLeft,+    clicked: trigger,+    trigger,+    get stats() {+      return {+        system: "adom-handshake",+        renderer: "canvas-range-strokes",+        animationNodes: canvas.isConnected ? 1 : 0,+        activeTransactions: transactions.length,+        maxTransactions,+        eligibleRoutes: eligibleRoutes.length,+        totalRoutes: routes.length,+        transactionsCompleted,+        transactionsTriggered,+        frameCount,+        targetFrameRate,+        rendererPixelRatio: pixelRatio,+        quietZoneProtected: Boolean(readingZone),+        perFrameTraceBudget: maxTransactions,+        reducedMotion,+      };+    },+  };+}
src/circuit-background-v2.jsadded+445
@@ -0,0 +1,445 @@+const SVG_NS = "http://www.w3.org/2000/svg";++const DEFAULTS = Object.freeze({+  seed: "nitrogen",+  tileWidth: 900,+  tileHeight: 450,+  pitch: 7.895,+  scale: 1000 / 900,+  density: 1,+  traceColor: "#151b25",+  traceWidth: 1.58,+  viaOuterRadius: 3.34,+  viaInnerRadius: 1.76,+  signals: true,+  interactive: true,+  signalColors: [+    ["#00e6dc", 0.6],+    ["#64abff", 0.25],+    ["#c5b3ff", 0.15],+  ],+  signalInterval: 520,+  signalSpeed: 800,+  signalLength: 150,+  maxSignals: 8,+});++let instanceCount = 0;++function hashSeed(value) {+  let hash = 2166136261;+  for (const char of String(value)) {+    hash ^= char.charCodeAt(0);+    hash = Math.imul(hash, 16777619);+  }+  return hash >>> 0;+}++function mulberry32(seed) {+  return function random() {+    let value = (seed += 0x6d2b79f5);+    value = Math.imul(value ^ (value >>> 15), value | 1);+    value ^= value + Math.imul(value ^ (value >>> 7), value | 61);+    return ((value ^ (value >>> 14)) >>> 0) / 4294967296;+  };+}++function integer(random, min, max) {+  return Math.floor(random() * (max - min + 1)) + min;+}++function choose(random, values) {+  return values[Math.floor(random() * values.length)];+}++function weighted(random, entries) {+  let roll = random();+  for (const [value, weight] of entries) {+    roll -= weight;+    if (roll <= 0) return value;+  }+  return entries[0][0];+}++function shuffle(random, values) {+  const result = [...values];+  for (let index = result.length - 1; index > 0; index -= 1) {+    const swap = integer(random, 0, index);+    [result[index], result[swap]] = [result[swap], result[index]];+  }+  return result;+}++function partition(random, total, count, minimum) {+  const values = Array(count).fill(Math.floor(total / count));+  for (let index = 0; index < total % count; index += 1) values[index] += 1;+  for (let turn = 0; turn < 10; turn += 1) {+    const from = integer(random, 0, count - 1);+    let to = integer(random, 0, count - 1);+    if (to === from) to = (to + 1) % count;+    if (values[from] > minimum) {+      values[from] -= 1;+      values[to] += 1;+    }+  }+  return values;+}++function lengthOf(points) {+  return points.slice(1).reduce((total, point, index) => {+    const previous = points[index];+    return total + Math.hypot(point[0] - previous[0], point[1] - previous[1]);+  }, 0);+}++function compact(points) {+  const clean = [];+  for (const point of points) {+    const previous = clean.at(-1);+    if (!previous || point[0] !== previous[0] || point[1] !== previous[1]) clean.push(point);+  }+  return clean;+}++function addTrace(target, points, viaStart = true, viaEnd = true) {+  const clean = compact(points);+  target.push({ points: clean, length: lengthOf(clean), viaStart, viaEnd });+}++function horizontalBundle(target, box, count, random) {+  const firstBend = box.x + Math.max(3, Math.floor(box.w * 0.25));+  const secondBend = box.x + Math.min(box.w - 3, Math.floor(box.w * 0.75));+  const maximumDiagonal = Math.max(1, Math.floor((secondBend - firstBend - 1) / 2));+  const diagonal = Math.min(integer(random, 2, 3), maximumDiagonal);+  const shift = diagonal * (random() < 0.5 ? -1 : 1);+  const available = box.h - 4 - diagonal;+  const lanes = Math.max(3, Math.min(count, available));+  const yStart = shift > 0 ? box.y + 2 : box.y + box.h - 2 - lanes;++  for (let lane = 0; lane < lanes; lane += 1) {+    const y = yStart + lane;+    addTrace(target, [+      [box.x, y],+      [firstBend, y],+      [firstBend + diagonal, y + shift],+      [secondBend - diagonal, y + shift],+      [secondBend, y],+      [box.x + box.w, y],+    ], random() < 0.72, random() < 0.72);+  }+}++function verticalBundle(target, box, count, random) {+  const swapped = [];+  horizontalBundle(swapped, { x: box.y, y: box.x, w: box.h, h: box.w }, count, random);+  for (const trace of swapped) {+    addTrace(target, trace.points.map(([x, y]) => [y, x]), trace.viaStart, trace.viaEnd);+  }+}++function cornerBundle(target, box, count, random) {+  const lanes = Math.max(4, Math.min(count, box.w - 5, box.h - 5));+  const orientation = integer(random, 0, 3);+  for (let lane = 0; lane < lanes; lane += 1) {+    let points;+    if (orientation === 0) {+      const x = box.x + 2 + lane;+      const y = box.y + box.h - 2 - lane;+      points = [[x, box.y], [x, y - 1], [x + 1, y], [box.x + box.w, y]];+    } else if (orientation === 1) {+      const x = box.x + box.w - 2 - lane;+      const y = box.y + box.h - 2 - lane;+      points = [[x, box.y], [x, y - 1], [x - 1, y], [box.x, y]];+    } else if (orientation === 2) {+      const x = box.x + 2 + lane;+      const y = box.y + 2 + lane;+      points = [[x, box.y + box.h], [x, y + 1], [x + 1, y], [box.x + box.w, y]];+    } else {+      const x = box.x + box.w - 2 - lane;+      const y = box.y + 2 + lane;+      points = [[x, box.y + box.h], [x, y + 1], [x - 1, y], [box.x, y]];+    }+    addTrace(target, points, random() < 0.76, random() < 0.76);+  }+}++function hairpinBundle(target, box, count, random) {+  const loops = Math.max(3, Math.min(Math.floor(count * 0.62), Math.floor((box.h - 3) / 2), box.w - 6));+  const opensLeft = random() < 0.5;+  for (let lane = 0; lane < loops; lane += 1) {+    const top = box.y + 1 + lane;+    const bottom = box.y + box.h - 1 - lane;+    if (opensLeft) {+      const end = box.x + box.w - 1 - lane;+      addTrace(target, [+        [box.x, top],+        [end - 1, top],+        [end, top + 1],+        [end, bottom - 1],+        [end - 1, bottom],+        [box.x, bottom],+      ]);+    } else {+      const end = box.x + 1 + lane;+      addTrace(target, [+        [box.x + box.w, top],+        [end + 1, top],+        [end, top + 1],+        [end, bottom - 1],+        [end + 1, bottom],+        [box.x + box.w, bottom],+      ]);+    }+  }+}++function scaleTrace(trace, pitch) {+  const points = trace.points.map(([x, y]) => [x * pitch, y * pitch]);+  return { ...trace, points, length: lengthOf(points) };+}++export function generateCircuitTile(options = {}) {+  const config = { ...DEFAULTS, ...options };+  const random = mulberry32(hashSeed(config.seed));+  const columns = Math.round(config.tileWidth / config.pitch);+  const rows = Math.round(config.tileHeight / config.pitch);+  const rowHeights = partition(random, rows, 4, 12);+  const patterns = shuffle(random, [+    "horizontal", "corner", "hairpin", "vertical", "corner",+    "vertical", "horizontal", "corner", "hairpin", "horizontal",+    "corner", "vertical", "horizontal", "corner", "hairpin",+    "horizontal", "corner", "vertical", "hairpin", "horizontal",+  ]);+  const traces = [];+  const cells = [];+  let y = 0;+  let patternIndex = 0;++  for (const rowHeight of rowHeights) {+    const widths = partition(random, columns, 5, 18);+    let x = 0;+    for (const width of widths) {+      const box = { x: x + 1, y: y + 1, w: width - 2, h: rowHeight - 2 };+      const pattern = patterns[patternIndex++];+      const baseLanes = integer(random, 5, 8);+      const laneCount = Math.max(3, Math.round(baseLanes * config.density));+      if (pattern === "horizontal") horizontalBundle(traces, box, laneCount, random);+      if (pattern === "vertical") verticalBundle(traces, box, laneCount, random);+      if (pattern === "corner") cornerBundle(traces, box, laneCount, random);+      if (pattern === "hairpin") hairpinBundle(traces, box, laneCount, random);+      cells.push({ ...box, pattern });+      x += width;+    }+    y += rowHeight;+  }++  const scaled = traces.map((trace) => scaleTrace(trace, config.pitch));+  return {+    width: columns * config.pitch,+    height: rows * config.pitch,+    pitch: config.pitch,+    traces: scaled,+    cells,+  };+}++export function pathData(points, offsetX = 0, offsetY = 0) {+  return points+    .map(([x, y], index) => `${index ? "L" : "M"}${(x + offsetX).toFixed(2)},${(y + offsetY).toFixed(2)}`)+    .join("");+}++function svgElement(name, attributes = {}) {+  const element = document.createElementNS(SVG_NS, name);+  for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, String(value));+  return element;+}++export function createCircuitBackground(mount, options = {}) {+  if (!(mount instanceof Element)) throw new TypeError("mount must be a DOM element");++  const id = `circuit-v2-${++instanceCount}`;+  let config = { ...DEFAULTS, ...options };+  let tile;+  let svg;+  let uses;+  let signals;+  let resizeObserver;+  let timer;+  let activeSignals = 0;+  let tilesAcross = 1;+  let tilesDown = 1;+  let pointer = null;+  let signalRandom = mulberry32(hashSeed(`${config.seed}:signals`));+  const interactionTarget = mount.parentElement || mount;+  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;+  const originalPosition = mount.style.position;+  const computedPosition = getComputedStyle(mount).position;+  if (computedPosition === "static") mount.style.position = "relative";+  mount.style.overflow = "hidden";++  function via(parent, point) {+    parent.append(+      svgElement("circle", { cx: point[0], cy: point[1], r: config.viaOuterRadius, fill: config.traceColor }),+      svgElement("circle", { cx: point[0], cy: point[1], r: config.viaInnerRadius, fill: "var(--circuit-hole, #0d1117)" }),+    );+  }++  function layout() {+    const width = Math.max(1, mount.clientWidth);+    const height = Math.max(1, mount.clientHeight);+    tilesAcross = Math.max(1, Math.ceil(width / (tile.width * config.scale)));+    tilesDown = Math.max(2, Math.ceil(height / (tile.height * config.scale)) + 1);+    const viewWidth = tilesAcross * tile.width;+    const viewHeight = tilesDown * tile.height;+    svg.setAttribute("width", viewWidth * config.scale);+    svg.setAttribute("height", viewHeight * config.scale);+    svg.setAttribute("viewBox", `0 ${tile.height / 2} ${viewWidth} ${viewHeight}`);+    uses.replaceChildren();+    for (let row = 0; row < tilesDown; row += 1) {+      for (let column = 0; column < tilesAcross; column += 1) {+        uses.appendChild(svgElement("use", {+          href: `#${id}-tile`,+          transform: `translate(${column * tile.width}, ${row * tile.height})`,+        }));+      }+    }+  }++  function build() {+    svg?.remove();+    tile = generateCircuitTile(config);+    signalRandom = mulberry32(hashSeed(`${config.seed}:signals`));+    svg = svgElement("svg", { "aria-hidden": true, focusable: false, xmlns: SVG_NS });+    Object.assign(svg.style, {+      position: "absolute", left: "0", top: "0", display: "block",+      maxWidth: "none", pointerEvents: "none", overflow: "visible",+    });+    const defs = svgElement("defs");+    const tileGroup = svgElement("g", { id: `${id}-tile` });+    for (const trace of tile.traces) {+      tileGroup.appendChild(svgElement("path", {+        d: pathData(trace.points), fill: "none", stroke: config.traceColor,+        "stroke-width": config.traceWidth, "stroke-linecap": "butt", "stroke-linejoin": "miter",+      }));+      if (trace.viaStart) via(tileGroup, trace.points[0]);+      if (trace.viaEnd) via(tileGroup, trace.points.at(-1));+    }+    defs.appendChild(tileGroup);+    svg.appendChild(defs);+    uses = svgElement("g");+    signals = svgElement("g", { "data-signals": "", fill: "none" });+    svg.append(uses, signals);+    mount.prepend(svg);+    layout();+  }++  function nearestTrace() {+    if (!pointer) return null;+    let result = null;+    let best = Infinity;+    for (let attempt = 0; attempt < 40; attempt += 1) {+      const trace = tile.traces[integer(signalRandom, 0, tile.traces.length - 1)];+      const point = trace.points[Math.floor(trace.points.length / 2)];+      const column = Math.floor(pointer.x / tile.width);+      const row = Math.floor(pointer.y / tile.height);+      const distance = Math.hypot(point[0] + column * tile.width - pointer.x, point[1] + row * tile.height - pointer.y);+      if (distance < best) {+        best = distance;+        result = { trace, column, row };+      }+    }+    return result;+  }++  function spawnSignal({ nearPointer = false } = {}) {+    if (!config.signals || reducedMotion || activeSignals >= config.maxSignals || !tile.traces.length) return;+    const selected = nearPointer ? nearestTrace() : null;+    const trace = selected?.trace || tile.traces[integer(signalRandom, 0, tile.traces.length - 1)];+    const column = selected?.column ?? integer(signalRandom, 0, tilesAcross - 1);+    const row = selected?.row ?? integer(signalRandom, 0, tilesDown - 1);+    if (column < 0 || column >= tilesAcross || row < 0 || row >= tilesDown) return;+    const points = signalRandom() < 0.5 ? [...trace.points].reverse() : trace.points;+    const d = pathData(points, column * tile.width, row * tile.height);+    const color = weighted(signalRandom, config.signalColors);+    const duration = ((trace.length + config.signalLength) / config.signalSpeed) * 1000;+    const group = svgElement("g");+    const pulse = svgElement("path", {+      d, stroke: color, "stroke-width": 2.4, "stroke-linecap": "round",+      "stroke-dasharray": `${config.signalLength} ${trace.length + config.signalLength}`,+      "stroke-dashoffset": config.signalLength, "stroke-opacity": 0.62,+    });+    pulse.style.filter = `drop-shadow(0 0 3px ${color})`;+    group.appendChild(pulse);+    signals.appendChild(group);+    activeSignals += 1;+    pulse.animate(+      [{ strokeDashoffset: config.signalLength }, { strokeDashoffset: -trace.length }],+      { duration, easing: "linear", fill: "forwards" },+    );+    window.setTimeout(() => {+      group.remove();+      activeSignals = Math.max(0, activeSignals - 1);+    }, duration + 150);+  }++  function onPointerMove(event) {+    const rect = mount.getBoundingClientRect();+    pointer = {+      x: (event.clientX - rect.left) / config.scale,+      y: (event.clientY - rect.top) / config.scale + tile.height / 2,+      at: performance.now(),+    };+  }++  function onClick() {+    for (let index = 0; index < 5; index += 1) {+      window.setTimeout(() => spawnSignal({ nearPointer: true }), index * 55);+    }+  }++  function startSignals() {+    window.clearInterval(timer);+    if (!config.signals || reducedMotion) return;+    timer = window.setInterval(() => {+      if (!document.hidden) spawnSignal({ nearPointer: pointer && performance.now() - pointer.at < 4000 });+    }, config.signalInterval);+  }++  function regenerate(nextOptions = {}) {+    config = { ...config, ...nextOptions };+    activeSignals = 0;+    build();+    startSignals();+    return api;+  }++  function exportSvg() {+    const clone = svg.cloneNode(true);+    clone.querySelector("[data-signals]")?.replaceChildren();+    clone.removeAttribute("style");+    return `<?xml version="1.0" encoding="UTF-8"?>\n${clone.outerHTML}`;+  }++  function destroy() {+    window.clearInterval(timer);+    resizeObserver?.disconnect();+    interactionTarget.removeEventListener("pointermove", onPointerMove);+    interactionTarget.removeEventListener("click", onClick);+    svg?.remove();+    if (computedPosition === "static") mount.style.position = originalPosition;+  }++  const api = { regenerate, spawnSignal, exportSvg, destroy, get tile() { return tile; } };+  build();+  resizeObserver = new ResizeObserver(layout);+  resizeObserver.observe(mount);+  if (config.interactive) {+    interactionTarget.addEventListener("pointermove", onPointerMove, { passive: true });+    interactionTarget.addEventListener("click", onClick);+  }+  startSignals();+  return api;+}++export { DEFAULTS };
src/circuit-background-v3.jsadded+560
@@ -0,0 +1,560 @@+const SVG_NS = "http://www.w3.org/2000/svg";++const DEFAULTS = Object.freeze({+  seed: "nitrogen",+  pitch: 7.895,+  scale: 1000 / 900,+  density: 1,+  traceColor: "#151b25",+  traceWidth: 1.58,+  viaOuterRadius: 3.34,+  viaInnerRadius: 1.76,+  signals: true,+  interactive: true,+  signalColors: [+    ["#00e6dc", 0.6],+    ["#64abff", 0.25],+    ["#c5b3ff", 0.15],+  ],+  signalInterval: 480,+  signalSpeed: 800,+  signalLength: 150,+  maxSignals: 9,+});++let instanceCount = 0;++function hashSeed(value) {+  let hash = 2166136261;+  for (const char of String(value)) {+    hash ^= char.charCodeAt(0);+    hash = Math.imul(hash, 16777619);+  }+  return hash >>> 0;+}++function mulberry32(seed) {+  return function random() {+    let value = (seed += 0x6d2b79f5);+    value = Math.imul(value ^ (value >>> 15), value | 1);+    value ^= value + Math.imul(value ^ (value >>> 7), value | 61);+    return ((value ^ (value >>> 14)) >>> 0) / 4294967296;+  };+}++function integer(random, min, max) {+  return Math.floor(random() * (max - min + 1)) + min;+}++function weighted(random, entries) {+  let roll = random();+  for (const [value, weight] of entries) {+    roll -= weight;+    if (roll <= 0) return value;+  }+  return entries[0][0];+}++class MinHeap {+  constructor() {+    this.values = [];+  }++  push(value) {+    const values = this.values;+    values.push(value);+    let index = values.length - 1;+    while (index > 0) {+      const parent = Math.floor((index - 1) / 2);+      if (values[parent].score <= value.score) break;+      values[index] = values[parent];+      index = parent;+    }+    values[index] = value;+  }++  pop() {+    const values = this.values;+    if (!values.length) return null;+    const root = values[0];+    const last = values.pop();+    if (!values.length) return root;+    let index = 0;+    while (true) {+      const left = index * 2 + 1;+      const right = left + 1;+      if (left >= values.length) break;+      let child = left;+      if (right < values.length && values[right].score < values[left].score) child = right;+      if (values[child].score >= last.score) break;+      values[index] = values[child];+      index = child;+    }+    values[index] = last;+    return root;+  }++  get size() {+    return this.values.length;+  }+}++const DIRECTIONS = [+  [1, 0], [0, 1], [-1, 0], [0, -1],+];++function key(x, y) {+  return (y + 32768) * 65536 + x + 32768;+}++function diagonalKey(x, y, nx, ny) {+  return `${Math.min(x, nx)}:${Math.min(y, ny)}:${(nx - x) * (ny - y) > 0 ? 1 : -1}`;+}++function distanceHeuristic(x, y, gx, gy) {+  return Math.abs(gx - x) + Math.abs(gy - y);+}++function turnCost(previous, next) {+  if (previous === 4 || previous === next) return 0;+  const difference = Math.min((next - previous + 4) % 4, (previous - next + 4) % 4);+  if (difference === 1) return 0.9;+  return 5;+}++function route(start, goal, context, noiseSeed) {+  const { columns, rows, occupied, reserved, diagonalEdges } = context;+  const stateCount = columns * rows * 5;+  const costs = new Float64Array(stateCount);+  costs.fill(Infinity);+  const parents = new Int32Array(stateCount);+  parents.fill(-1);+  const heap = new MinHeap();+  const startCell = start[1] * columns + start[0];+  const startState = startCell * 5 + 4;+  costs[startState] = 0;+  heap.push({ state: startState, score: distanceHeuristic(start[0], start[1], goal[0], goal[1]) });+  const span = Math.hypot(goal[0] - start[0], goal[1] - start[1]);+  const padding = 8 + Math.floor(span * 0.1);+  const minimumX = Math.max(0, Math.min(start[0], goal[0]) - padding);+  const maximumX = Math.min(columns - 1, Math.max(start[0], goal[0]) + padding);+  const minimumY = Math.max(0, Math.min(start[1], goal[1]) - padding);+  const maximumY = Math.min(rows - 1, Math.max(start[1], goal[1]) + padding);+  let goalState = -1;+  let expansions = 0;++  while (heap.size && expansions < 9000) {+    const current = heap.pop();+    const state = current.state;+    const cell = Math.floor(state / 5);+    const previousDirection = state % 5;+    const x = cell % columns;+    const y = Math.floor(cell / columns);+    const knownScore = costs[state] + distanceHeuristic(x, y, goal[0], goal[1]);+    if (current.score > knownScore + 1e-8) continue;+    if (x === goal[0] && y === goal[1]) {+      goalState = state;+      break;+    }+    expansions += 1;++    for (let direction = 0; direction < DIRECTIONS.length; direction += 1) {+      const [dx, dy] = DIRECTIONS[direction];+      const nx = x + dx;+      const ny = y + dy;+      if (nx < minimumX || nx > maximumX || ny < minimumY || ny > maximumY) continue;+      const nextKey = key(nx, ny);+      const isGoal = nx === goal[0] && ny === goal[1];+      if (!isGoal && (occupied.has(nextKey) || reserved.has(nextKey))) continue;++      const diagonal = dx !== 0 && dy !== 0;+      if (diagonal && previousDirection < 4 && DIRECTIONS[previousDirection][0] !== 0 && DIRECTIONS[previousDirection][1] !== 0) continue;+      if (diagonal) {+        const edge = diagonalKey(x, y, nx, ny);+        const pieces = edge.split(":");+        const opposite = `${pieces[0]}:${pieces[1]}:${-Number(pieces[2])}`;+        if (diagonalEdges.has(opposite)) continue;+      }++      let nearby = 0;+      for (const [ox, oy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {+        if (occupied.has(key(nx + ox, ny + oy))) nearby += 1;+      }+      const step = diagonal ? 1.7 : 1;+      const attraction = Math.max(0, 0.28 - nearby * 0.08);+      const noise = (((nx * 73856093) ^ (ny * 19349663) ^ noiseSeed) >>> 0) % 97 / 9700;+      const nextCost = costs[state] + step + turnCost(previousDirection, direction) + attraction + noise;+      const nextCell = ny * columns + nx;+      const nextState = nextCell * 5 + direction;+      if (nextCost >= costs[nextState]) continue;+      costs[nextState] = nextCost;+      parents[nextState] = state;+      heap.push({+        state: nextState,+        score: nextCost + distanceHeuristic(nx, ny, goal[0], goal[1]),+      });+    }+  }++  if (goalState < 0) return null;+  const cells = [];+  let state = goalState;+  while (state >= 0) {+    const cell = Math.floor(state / 5);+    cells.push([cell % columns, Math.floor(cell / columns)]);+    state = parents[state];+  }+  cells.reverse();+  return cells;+}++function chamfer(path) {+  if (path.length < 3) return path;+  const result = [path[0]];+  for (let index = 1; index < path.length - 1; index += 1) {+    const previous = path[index - 1];+    const current = path[index];+    const next = path[index + 1];+    const ax = current[0] - previous[0];+    const ay = current[1] - previous[1];+    const bx = next[0] - current[0];+    const by = next[1] - current[1];+    const firstCardinal = (ax === 0) !== (ay === 0);+    const secondCardinal = (bx === 0) !== (by === 0);+    if (firstCardinal && secondCardinal && ax * bx + ay * by === 0) continue;+    result.push(current);+  }+  result.push(path.at(-1));+  return result;+}++function simplify(path) {+  if (path.length < 3) return path;+  const result = [path[0]];+  for (let index = 1; index < path.length - 1; index += 1) {+    const previous = result.at(-1);+    const current = path[index];+    const next = path[index + 1];+    const ax = Math.sign(current[0] - previous[0]);+    const ay = Math.sign(current[1] - previous[1]);+    const bx = Math.sign(next[0] - current[0]);+    const by = Math.sign(next[1] - current[1]);+    if (ax !== bx || ay !== by) result.push(current);+  }+  result.push(path.at(-1));+  return result;+}++function randomBank(random, count, columns, rows, occupied, reserved) {+  for (let attempt = 0; attempt < 80; attempt += 1) {+    const boundary = random() < 0.34;+    let points;+    let side = "interior";+    if (boundary) {+      side = ["left", "right", "top", "bottom"][integer(random, 0, 3)];+      if (side === "left" || side === "right") {+        const x = side === "left" ? 0 : columns - 1;+        const y = integer(random, 2, rows - count - 2);+        points = Array.from({ length: count }, (_, index) => [x, y + index]);+      } else {+        const x = integer(random, 2, columns - count - 2);+        const y = side === "top" ? 0 : rows - 1;+        points = Array.from({ length: count }, (_, index) => [x + index, y]);+      }+    } else {+      const vertical = random() < 0.5;+      if (vertical) {+        const x = integer(random, 4, columns - 5);+        const y = integer(random, 4, rows - count - 5);+        points = Array.from({ length: count }, (_, index) => [x, y + index]);+      } else {+        const x = integer(random, 4, columns - count - 5);+        const y = integer(random, 4, rows - 5);+        points = Array.from({ length: count }, (_, index) => [x + index, y]);+      }+    }+    if (points.every(([x, y]) => !occupied.has(key(x, y)) && !reserved.has(key(x, y)))) {+      return { points, side };+    }+  }+  return null;+}++function bankCenter(bank) {+  const first = bank.points[0];+  const last = bank.points.at(-1);+  return [(first[0] + last[0]) / 2, (first[1] + last[1]) / 2];+}++function pathLength(points) {+  return points.slice(1).reduce((total, point, index) => {+    const previous = points[index];+    return total + Math.hypot(point[0] - previous[0], point[1] - previous[1]);+  }, 0);+}++export function generateCircuitField(options = {}) {+  const config = { ...DEFAULTS, ...options };+  const width = Math.max(240, config.width || 900);+  const height = Math.max(180, config.height || 450);+  const columns = Math.max(30, Math.floor(width / config.pitch));+  const rows = Math.max(22, Math.floor(height / config.pitch));+  const random = mulberry32(hashSeed(`${config.seed}:${columns}x${rows}`));+  let occupied = new Set();+  let reserved = new Set();+  let diagonalEdges = new Set();+  const traces = [];+  const target = Math.round((columns * rows) / 220 * config.density);+  let attempts = 0;++  while (traces.length < target && attempts < 55) {+    attempts += 1;+    const remaining = target - traces.length;+    const count = Math.min(remaining, integer(random, 4, 7));+    const startBank = randomBank(random, count, columns, rows, occupied, reserved);+    if (!startBank) continue;+    for (const point of startBank.points) reserved.add(key(...point));+    let endBank = null;+    for (let pairAttempt = 0; pairAttempt < 45; pairAttempt += 1) {+      const candidate = randomBank(random, count, columns, rows, occupied, reserved);+      if (!candidate) continue;+      const a = bankCenter(startBank);+      const b = bankCenter(candidate);+      if (Math.hypot(a[0] - b[0], a[1] - b[1]) < Math.min(columns, rows) * 0.26) continue;+      endBank = candidate;+      break;+    }+    if (!endBank) {+      for (const point of startBank.points) reserved.delete(key(...point));+      continue;+    }+    for (const point of endBank.points) reserved.add(key(...point));++    const occupiedBefore = occupied;+    const edgesBefore = diagonalEdges;+    occupied = new Set(occupied);+    diagonalEdges = new Set(diagonalEdges);+    const bundle = [];+    const routeOrder = Array.from({ length: count }, (_, index) => index);+    if (random() < 0.5) routeOrder.reverse();++    for (const lane of routeOrder) {+      const start = startBank.points[lane];+      const goal = endBank.points[lane];+      const cells = route(start, goal, { columns, rows, occupied, reserved, diagonalEdges }, hashSeed(`${config.seed}:${attempts}:${lane}`));+      if (!cells || cells.length < 8) continue;+      const shaped = simplify(chamfer(cells));+      for (const point of cells) occupied.add(key(...point));+      for (let index = 1; index < shaped.length; index += 1) {+        const previous = shaped[index - 1];+        const current = shaped[index];+        if (previous[0] !== current[0] && previous[1] !== current[1]) {+          diagonalEdges.add(diagonalKey(previous[0], previous[1], current[0], current[1]));+        }+      }+      const points = shaped.map(([x, y]) => [x * config.pitch, y * config.pitch]);+      bundle.push({+        points,+        length: pathLength(points),+        viaStart: startBank.side === "interior",+        viaEnd: endBank.side === "interior",+      });+    }++    for (const point of startBank.points) reserved.delete(key(...point));+    for (const point of endBank.points) reserved.delete(key(...point));+    if (bundle.length < 3) {+      occupied = occupiedBefore;+      diagonalEdges = edgesBefore;+      continue;+    }+    traces.push(...bundle);+  }++  return {+    width: (columns - 1) * config.pitch,+    height: (rows - 1) * config.pitch,+    columns,+    rows,+    traces: traces.slice(0, target),+  };+}++export function pathData(points) {+  return points.map(([x, y], index) => `${index ? "L" : "M"}${x.toFixed(2)},${y.toFixed(2)}`).join("");+}++function svgElement(name, attributes = {}) {+  const element = document.createElementNS(SVG_NS, name);+  for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, String(value));+  return element;+}++export function createCircuitBackground(mount, options = {}) {+  if (!(mount instanceof Element)) throw new TypeError("mount must be a DOM element");+  const id = `circuit-field-${++instanceCount}`;+  let config = { ...DEFAULTS, ...options };+  let field;+  let svg;+  let signalLayer;+  let timer;+  let resizeObserver;+  let resizeTimer;+  let activeSignals = 0;+  let pointer = null;+  let signalRandom;+  let lastSize = "";+  const interactionTarget = mount.parentElement || mount;+  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;+  const originalPosition = mount.style.position;+  if (getComputedStyle(mount).position === "static") mount.style.position = "relative";+  mount.style.overflow = "hidden";++  function addVia(group, point) {+    group.append(+      svgElement("circle", { cx: point[0], cy: point[1], r: config.viaOuterRadius, fill: config.traceColor }),+      svgElement("circle", { cx: point[0], cy: point[1], r: config.viaInnerRadius, fill: "var(--circuit-hole, #0d1117)" }),+    );+  }++  function build(force = false) {+    const pixelWidth = Math.max(1, mount.clientWidth);+    const pixelHeight = Math.max(1, mount.clientHeight);+    const worldWidth = pixelWidth / config.scale;+    const worldHeight = pixelHeight / config.scale;+    const size = `${Math.floor(worldWidth / config.pitch)}x${Math.floor(worldHeight / config.pitch)}`;+    if (!force && size === lastSize) return;+    lastSize = size;+    svg?.remove();+    field = generateCircuitField({ ...config, width: worldWidth, height: worldHeight });+    signalRandom = mulberry32(hashSeed(`${config.seed}:${size}:signals`));+    svg = svgElement("svg", {+      id,+      "aria-hidden": true,+      focusable: false,+      viewBox: `0 0 ${field.width} ${field.height}`,+      preserveAspectRatio: "none",+    });+    Object.assign(svg.style, {+      position: "absolute", inset: "0", width: "100%", height: "100%",+      display: "block", pointerEvents: "none",+    });+    const traces = svgElement("g");+    for (const trace of field.traces) {+      traces.appendChild(svgElement("path", {+        d: pathData(trace.points), fill: "none", stroke: config.traceColor,+        "stroke-width": config.traceWidth, "stroke-linecap": "butt", "stroke-linejoin": "miter",+      }));+      if (trace.viaStart) addVia(traces, trace.points[0]);+      if (trace.viaEnd) addVia(traces, trace.points.at(-1));+    }+    signalLayer = svgElement("g", { "data-signals": "", fill: "none" });+    svg.append(traces, signalLayer);+    mount.prepend(svg);+  }++  function nearestTrace() {+    if (!pointer || !field?.traces.length) return null;+    let result = null;+    let best = Infinity;+    for (let attempt = 0; attempt < 42; attempt += 1) {+      const trace = field.traces[integer(signalRandom, 0, field.traces.length - 1)];+      const point = trace.points[Math.floor(trace.points.length / 2)];+      const distance = Math.hypot(point[0] - pointer.x, point[1] - pointer.y);+      if (distance < best) {+        best = distance;+        result = trace;+      }+    }+    return result;+  }++  function spawnSignal({ nearPointer = false } = {}) {+    if (!config.signals || reducedMotion || activeSignals >= config.maxSignals || !field?.traces.length) return;+    const trace = (nearPointer && nearestTrace()) || field.traces[integer(signalRandom, 0, field.traces.length - 1)];+    const points = signalRandom() < 0.5 ? [...trace.points].reverse() : trace.points;+    const color = weighted(signalRandom, config.signalColors);+    const duration = ((trace.length + config.signalLength) / config.signalSpeed) * 1000;+    const pulse = svgElement("path", {+      d: pathData(points), stroke: color, "stroke-width": 2.4, "stroke-linecap": "round",+      "stroke-dasharray": `${config.signalLength} ${trace.length + config.signalLength}`,+      "stroke-dashoffset": config.signalLength, "stroke-opacity": 0.62,+    });+    pulse.style.filter = `drop-shadow(0 0 3px ${color})`;+    signalLayer.appendChild(pulse);+    activeSignals += 1;+    pulse.animate(+      [{ strokeDashoffset: config.signalLength }, { strokeDashoffset: -trace.length }],+      { duration, easing: "linear", fill: "forwards" },+    );+    window.setTimeout(() => {+      pulse.remove();+      activeSignals = Math.max(0, activeSignals - 1);+    }, duration + 150);+  }++  function onPointerMove(event) {+    const rect = mount.getBoundingClientRect();+    pointer = {+      x: (event.clientX - rect.left) / rect.width * field.width,+      y: (event.clientY - rect.top) / rect.height * field.height,+      at: performance.now(),+    };+  }++  function onClick() {+    for (let index = 0; index < 6; index += 1) {+      window.setTimeout(() => spawnSignal({ nearPointer: true }), index * 50);+    }+  }++  function startSignals() {+    window.clearInterval(timer);+    if (!config.signals || reducedMotion) return;+    timer = window.setInterval(() => {+      if (!document.hidden) spawnSignal({ nearPointer: pointer && performance.now() - pointer.at < 4000 });+    }, config.signalInterval);+  }++  function regenerate(nextOptions = {}) {+    config = { ...config, ...nextOptions };+    activeSignals = 0;+    lastSize = "";+    build(true);+    startSignals();+    return api;+  }++  function exportSvg() {+    const clone = svg.cloneNode(true);+    clone.querySelector("[data-signals]")?.replaceChildren();+    clone.removeAttribute("style");+    return `<?xml version="1.0" encoding="UTF-8"?>\n${clone.outerHTML}`;+  }++  function destroy() {+    window.clearInterval(timer);+    window.clearTimeout(resizeTimer);+    resizeObserver?.disconnect();+    interactionTarget.removeEventListener("pointermove", onPointerMove);+    interactionTarget.removeEventListener("click", onClick);+    svg?.remove();+    mount.style.position = originalPosition;+  }++  const api = { regenerate, spawnSignal, exportSvg, destroy, get field() { return field; } };+  build(true);+  resizeObserver = new ResizeObserver(() => {+    window.clearTimeout(resizeTimer);+    resizeTimer = window.setTimeout(() => build(), 220);+  });+  resizeObserver.observe(mount);+  if (config.interactive) {+    interactionTarget.addEventListener("pointermove", onPointerMove, { passive: true });+    interactionTarget.addEventListener("click", onClick);+  }+  startSignals();+  return api;+}++export { DEFAULTS };
src/circuit-background-v4.jsadded+1062
@@ -0,0 +1,1062 @@+import { createConductorAnimation } from "./circuit-animation-conductor.js?v=1";++const SVG_NS = "http://www.w3.org/2000/svg";++const DEFAULTS = Object.freeze({+  seed: "nitrogen",+  pitch: 7.895,+  scale: 1000 / 900,+  density: 1,+  fill: 0.668,+  routeLength: 0.78,+  horizontalFlow: 0.82,+  flowAngle: 0,+  swirl: 0.28,+  swirlScale: 0.8,+  diagonalFlow: 0.58,+  jogLength: 0.72,+  channelAttraction: 0.78,+  longFlow: 0.18,+  edgeDensity: 0.22,+  traceColor: "#151b25",+  traceWidth: 1.58,+  featureScale: 1,+  viaOuterRadius: 3.34,+  viaInnerRadius: 1.76,+  motion: true,+  interactive: true,+  animationSystem: "conductor",+  liveliness: 0.82,+  signalHalos: true,+  signalConvoys: true,+  signalWaves: true,+  pointerWake: true,+  nodeFlashes: true,+  ambientCurrents: true,+  ambientNodes: true,+  signalColors: [+    ["#00e6dc", 0.6],+    ["#64abff", 0.25],+    ["#c5b3ff", 0.15],+  ],+  signalInterval: 420,+  signalSpeed: 800,+  signalLength: 110,+  maxSignals: 28,+  ambientCurrentCount: 18,+  ambientNodeCount: 22,+  waveInterval: 6200,+  pointerWakeInterval: 170,+  sweepDuration: 15200,+  sweepAngle: 18,+  sweepWidth: 0.075,+  bloomInterval: 7600,+  bloomDuration: 4800,+  pointerRadius: 0.19,+  atmosphereStrength: 1,+});++const ANIMATION_PRESETS = Object.freeze({+  none: Object.freeze({+    animationSystem: "none",+    motion: false,+    liveliness: 0,+    atmosphereStrength: 0,+  }),+  conductor: Object.freeze({+    animationSystem: "conductor",+    motion: true,+    liveliness: 0.82,+    signalHalos: true,+    signalConvoys: true,+    signalWaves: true,+    pointerWake: true,+    nodeFlashes: true,+    ambientCurrents: true,+    ambientNodes: true,+    signalColors: [+      ["#00e6dc", 0.6],+      ["#64abff", 0.25],+      ["#c5b3ff", 0.15],+    ],+    signalInterval: 420,+    signalSpeed: 800,+    signalLength: 110,+    maxSignals: 28,+    ambientCurrentCount: 18,+    ambientNodeCount: 22,+    waveInterval: 6200,+    pointerWakeInterval: 170,+    atmosphereStrength: 1,+  }),+  adom: Object.freeze({+    animationSystem: "adom",+    motion: true,+    liveliness: 0.88,+    sweepDuration: 15200,+    sweepAngle: 18,+    sweepWidth: 0.075,+    bloomInterval: 7600,+    bloomDuration: 4800,+    pointerRadius: 0.19,+    atmosphereStrength: 1,+  }),+});++const DIRECTIONS = [[1, 0], [0, 1], [-1, 0], [0, -1]];+let instanceCount = 0;++function hashSeed(value) {+  let hash = 2166136261;+  for (const char of String(value)) {+    hash ^= char.charCodeAt(0);+    hash = Math.imul(hash, 16777619);+  }+  return hash >>> 0;+}++function mulberry32(seed) {+  return function random() {+    let value = (seed += 0x6d2b79f5);+    value = Math.imul(value ^ (value >>> 15), value | 1);+    value ^= value + Math.imul(value ^ (value >>> 7), value | 61);+    return ((value ^ (value >>> 14)) >>> 0) / 4294967296;+  };+}++function integer(random, min, max) {+  return Math.floor(random() * (max - min + 1)) + min;+}++function weighted(random, entries) {+  let total = 0;+  for (const [, weight] of entries) total += weight;+  let roll = random() * total;+  for (const [value, weight] of entries) {+    roll -= weight;+    if (roll <= 0) return value;+  }+  return entries.at(-1)?.[0];+}++function shuffle(random, values) {+  for (let index = values.length - 1; index > 0; index -= 1) {+    const swap = integer(random, 0, index);+    [values[index], values[swap]] = [values[swap], values[index]];+  }+  return values;+}++function cellIndex(x, y, columns) {+  return y * columns + x;+}++function runLength(random, perpendicular, config, vector = [1, 0]) {+  const localDiagonalness = diagonalness(vector);+  let result;+  if (perpendicular) {+    result = random() < 0.22 ? integer(random, 4, 7) : integer(random, 1, 4);+    const scale = config.jogLength + (1 - config.jogLength) * localDiagonalness;+    return Math.max(1, Math.round(result * scale));+  }+  const roll = random();+  if (roll < 0.14) result = integer(random, 16, 28);+  else if (roll < 0.52) result = integer(random, 9, 16);+  else result = integer(random, 4, 10);+  const scale = 1 - localDiagonalness * config.diagonalFlow * 0.9;+  return Math.max(1, Math.round(result * scale));+}++function traceBudget(random, columns, rows, config) {+  const ceiling = Math.max(12, Math.min(86, Math.floor(Math.hypot(columns, rows) * 0.52)));+  const roll = random();+  let result;+  if (roll < 0.2) result = integer(random, Math.min(42, ceiling), ceiling);+  else if (roll < 0.65) result = integer(random, 24, Math.min(50, ceiling));+  else result = integer(random, 12, Math.min(28, ceiling));+  return Math.max(6, Math.round(result * config.routeLength));+}++function createFlowField(config, columns, rows) {+  const random = mulberry32(hashSeed(`${config.seed}:${columns}x${rows}:flow-field`));+  const minimumDimension = Math.min(columns, rows);+  const count = Math.max(2, Math.min(5, Math.round(columns * rows / 5200)));+  const vortices = [];+  for (let index = 0; index < count; index += 1) {+    const radius = minimumDimension * config.swirlScale * (0.55 + random() * 0.45);+    vortices.push({+      x: columns * (0.08 + random() * 0.84),+      y: rows * (0.08 + random() * 0.84),+      radius: Math.max(8, radius),+      spin: random() < 0.5 ? -1 : 1,+      strength: 0.72 + random() * 0.56,+    });+  }+  return { vortices };+}++function flowVector(x, y, angleDegrees, field, config) {+  const radians = angleDegrees * Math.PI / 180;+  let vx = Math.cos(radians);+  let vy = Math.sin(radians);+  for (const vortex of field.vortices) {+    const dx = x - vortex.x;+    const dy = y - vortex.y;+    const distance = Math.hypot(dx, dy);+    if (distance < 0.0001) continue;+    const falloff = Math.exp(-(distance * distance) / (2 * vortex.radius * vortex.radius));+    const influence = config.swirl * vortex.strength * falloff;+    vx += -dy / distance * vortex.spin * influence;+    vy += dx / distance * vortex.spin * influence;+  }+  const length = Math.hypot(vx, vy) || 1;+  return [vx / length, vy / length];+}++function closestDirection(vector) {+  let bestDirection = 0;+  let bestScore = -Infinity;+  for (let direction = 0; direction < DIRECTIONS.length; direction += 1) {+    const score = DIRECTIONS[direction][0] * vector[0] + DIRECTIONS[direction][1] * vector[1];+    if (score > bestScore) {+      bestScore = score;+      bestDirection = direction;+    }+  }+  return bestDirection;+}++function diagonalness(vector) {+  return 1 - Math.abs(Math.abs(vector[0]) - Math.abs(vector[1]));+}++function lookAhead(x, y, direction, blocked, local, columns, rows) {+  const [dx, dy] = DIRECTIONS[direction];+  let open = 0;+  for (let step = 1; step <= 5; step += 1) {+    const nx = x + dx * step;+    const ny = y + dy * step;+    if (nx < 0 || ny < 0 || nx >= columns || ny >= rows) break;+    const index = cellIndex(nx, ny, columns);+    if (blocked[index] || local.has(index)) break;+    open += 1;+  }+  return open;+}++function growOne(start, initialDirection, limit, blocked, local, columns, rows, random, config, field, angleDegrees, travelSign = 1) {+  const points = [start];+  let [x, y] = start;+  let direction = initialDirection;+  const initialVector = flowVector(x, y, angleDegrees, field, config);+  const initialTarget = [initialVector[0] * travelSign, initialVector[1] * travelSign];+  let remaining = runLength(random, false, config, initialTarget);++  while (points.length <= limit) {+    const candidates = [];+    const directions = [direction, (direction + 1) % 4, (direction + 3) % 4];+    const sampledVector = flowVector(x, y, angleDegrees, field, config);+    const targetVector = [sampledVector[0] * travelSign, sampledVector[1] * travelSign];+    const primaryDirection = closestDirection(targetVector);+    for (const candidate of directions) {+      const [dx, dy] = DIRECTIONS[candidate];+      const nx = x + dx;+      const ny = y + dy;+      // Keep every endpoint (and therefore every via) fully inside the canvas.+      if (nx < 1 || ny < 1 || nx >= columns - 1 || ny >= rows - 1) continue;+      const index = cellIndex(nx, ny, columns);+      if (blocked[index] || local.has(index)) continue;+      const open = lookAhead(x, y, candidate, blocked, local, columns, rows);+      if (!open) continue;+      const straight = candidate === direction;+      let weight = straight ? (remaining > 0 ? 15 : 1.6) : (remaining > 0 ? 0.12 : 4.2);+      const alignment = dx * targetVector[0] + dy * targetVector[1];+      if (alignment < -0.16) continue;+      weight *= 0.45 + open * 0.34;+      let neighbors = 0;+      if (candidate % 2 === 0) {+      weight *= 0.45 + Math.max(0, alignment) * 2.2;+        if (ny > 0 && blocked[cellIndex(nx, ny - 1, columns)]) neighbors += 1;+        if (ny < rows - 1 && blocked[cellIndex(nx, ny + 1, columns)]) neighbors += 1;+      } else {+        if (nx > 0 && blocked[cellIndex(nx - 1, ny, columns)]) neighbors += 1;+        if (nx < columns - 1 && blocked[cellIndex(nx + 1, ny, columns)]) neighbors += 1;+      }+      weight *= 1 + neighbors * config.channelAttraction;+      candidates.push([candidate, weight]);+    }+    if (!candidates.length) break;+    const nextDirection = weighted(random, candidates);+    if (nextDirection !== direction) {+      direction = nextDirection;+      remaining = runLength(random, direction % 2 !== primaryDirection % 2, config, targetVector);+    } else {+      remaining -= 1;+    }+    x += DIRECTIONS[direction][0];+    y += DIRECTIONS[direction][1];+    const index = cellIndex(x, y, columns);+    local.add(index);+    points.push([x, y]);+  }+  return points;+}++function chamfer(path) {+  if (path.length < 3) return path;+  const result = [path[0]];+  for (let index = 1; index < path.length - 1; index += 1) {+    const previous = path[index - 1];+    const current = path[index];+    const next = path[index + 1];+    const ax = current[0] - previous[0];+    const ay = current[1] - previous[1];+    const bx = next[0] - current[0];+    const by = next[1] - current[1];+    if (ax * bx + ay * by === 0 && (ax !== bx || ay !== by)) continue;+    result.push(current);+  }+  result.push(path.at(-1));+  return result;+}++function simplify(path) {+  if (path.length < 3) return path;+  const result = [path[0]];+  for (let index = 1; index < path.length - 1; index += 1) {+    const previous = result.at(-1);+    const current = path[index];+    const next = path[index + 1];+    const ax = Math.sign(current[0] - previous[0]);+    const ay = Math.sign(current[1] - previous[1]);+    const bx = Math.sign(next[0] - current[0]);+    const by = Math.sign(next[1] - current[1]);+    if (ax !== bx || ay !== by) result.push(current);+  }+  result.push(path.at(-1));+  return result;+}++function pathLength(points) {+  let length = 0;+  for (let index = 1; index < points.length; index += 1) {+    length += Math.hypot(points[index][0] - points[index - 1][0], points[index][1] - points[index - 1][1]);+  }+  return length;+}++function commitTrace(cells, state) {+  if (cells.length < 6) return false;+  const { blocked, columns, rows, pitch, traces } = state;+  for (const [x, y] of cells) blocked[cellIndex(x, y, columns)] = 1;+  state.occupied += cells.length;+  const shaped = simplify(chamfer(cells));+  const points = shaped.map(([x, y]) => [x * pitch, y * pitch]);+  traces.push({+    points,+    length: pathLength(points),+    viaStart: true,+    viaEnd: true,+  });+  return true;+}++function makeInteriorTrace(start, state, random, config) {+  const { blocked, columns, rows } = state;+  const startIndex = cellIndex(start[0], start[1], columns);+  if (blocked[startIndex]) return false;+  const followsPrimary = random() < config.horizontalFlow;+  const angleDegrees = config.flowAngle + (followsPrimary ? 0 : 90);+  const startVector = flowVector(start[0], start[1], angleDegrees, state.flowField, config);+  const direction = closestDirection(startVector);+  const budget = traceBudget(random, columns, rows, config);+  const forwardBudget = Math.max(3, Math.floor(budget * (0.42 + random() * 0.16)));+  const local = new Set([startIndex]);+  const forward = growOne(start, direction, forwardBudget, blocked, local, columns, rows, random, config, state.flowField, angleDegrees, 1);+  const backward = growOne(start, (direction + 2) % 4, budget - forwardBudget, blocked, local, columns, rows, random, config, state.flowField, angleDegrees, -1);+  const cells = backward.slice(1).reverse().concat(forward);+  return commitTrace(cells, state);+}++function makeEdgeTrace(start, direction, state, random, config, options = {}) {+  const { blocked, columns, rows } = state;+  const startIndex = cellIndex(start[0], start[1], columns);+  if (blocked[startIndex]) return false;+  const local = new Set([startIndex]);+  const baseBudget = integer(random, 10, Math.max(14, Math.min(48, Math.floor(Math.hypot(columns, rows) * 0.3))));+  const budget = options.budget ?? Math.max(6, Math.round(baseBudget * config.routeLength));+  const angleDegrees = options.angleDegrees ?? config.flowAngle + (random() < config.horizontalFlow ? 0 : 90);+  const sampledVector = flowVector(start[0], start[1], angleDegrees, state.flowField, config);+  const inward = DIRECTIONS[direction][0] * sampledVector[0] + DIRECTIONS[direction][1] * sampledVector[1];+  const cells = growOne(start, direction, budget, blocked, local, columns, rows, random, config, state.flowField, angleDegrees, inward >= 0 ? 1 : -1);+  if (cells.length < (options.minimumLength ?? 6)) return false;+  return commitTrace(cells, state);+}++export function generateCircuitField(options = {}) {+  const config = { ...DEFAULTS, ...options };+  const width = Math.max(240, config.width || 900);+  const height = Math.max(180, config.height || 450);+  const featureScale = Math.max(0.25, config.featureScale);+  const pitch = config.pitch * featureScale;+  const columns = Math.max(30, Math.floor(width / pitch));+  const rows = Math.max(22, Math.floor(height / pitch));+  const random = mulberry32(hashSeed(`${config.seed}:${columns}x${rows}`));+  const state = {+    blocked: new Uint8Array(columns * rows), columns, rows,+    pitch, traces: [], occupied: 0,+    flowField: createFlowField(config, columns, rows),+  };++  const flowRadians = config.flowAngle * Math.PI / 180;+  const absoluteX = Math.abs(Math.cos(flowRadians));+  const absoluteY = Math.abs(Math.sin(flowRadians));+  const primaryHorizontalWeight = absoluteX / (absoluteX + absoluteY || 1);+  const horizontalWeight = config.horizontalFlow * primaryHorizontalWeight + (1 - config.horizontalFlow) * (1 - primaryHorizontalWeight);+  const verticalWeight = 1 - horizontalWeight;+  const horizontalCarriers = [];+  for (let y = 1; y < rows - 1; y += 1) {+    horizontalCarriers.push([[1, y], 0], [[columns - 2, y], 2]);+  }+  shuffle(random, horizontalCarriers);+  const horizontalTarget = Math.round(rows * config.longFlow * horizontalWeight);+  let horizontalCarrierCount = 0;+  for (const [start, direction] of horizontalCarriers) {+    if (horizontalCarrierCount >= horizontalTarget) break;+    const minimumBudget = Math.max(12, Math.round(columns * 0.58 * config.routeLength));+    const maximumBudget = Math.max(minimumBudget, Math.round(columns * 1.02 * config.routeLength));+    const budget = integer(random, minimumBudget, maximumBudget);+    if (makeEdgeTrace(start, direction, state, random, config, {+      budget,+      angleDegrees: config.flowAngle,+      minimumLength: Math.round(columns * 0.3),+    })) horizontalCarrierCount += 1;+  }++  const verticalCarriers = [];+  for (let x = 1; x < columns - 1; x += 1) {+    verticalCarriers.push([[x, 1], 1], [[x, rows - 2], 3]);+  }+  shuffle(random, verticalCarriers);+  const verticalTarget = Math.round(columns * config.longFlow * verticalWeight * 0.34);+  let verticalCarrierCount = 0;+  for (const [start, direction] of verticalCarriers) {+    if (verticalCarrierCount >= verticalTarget) break;+    const minimumBudget = Math.max(12, Math.round(rows * 0.58 * config.routeLength));+    const maximumBudget = Math.max(minimumBudget, Math.round(rows * 1.02 * config.routeLength));+    const budget = integer(random, minimumBudget, maximumBudget);+    if (makeEdgeTrace(start, direction, state, random, config, {+      angleDegrees: config.flowAngle,+      budget,+      minimumLength: Math.round(rows * 0.3),+    })) verticalCarrierCount += 1;+  }++  const edgeSeeds = [];+  for (let x = 1; x < columns - 1; x += 1) {+    edgeSeeds.push([[x, 1], 1], [[x, rows - 2], 3]);+  }+  for (let y = 1; y < rows - 1; y += 1) {+    edgeSeeds.push([[1, y], 0], [[columns - 2, y], 2]);+  }+  shuffle(random, edgeSeeds);+  const edgeTarget = Math.round((columns + rows) * config.edgeDensity);+  let edgeCount = 0;+  for (const [start, direction] of edgeSeeds) {+    if (edgeCount >= edgeTarget) break;+    if (makeEdgeTrace(start, direction, state, random, config)) edgeCount += 1;+  }++  const interiorColumns = columns - 2;+  const interiorRows = rows - 2;+  const seeds = shuffle(random, Array.from(+    { length: interiorColumns * interiorRows },+    (_, index) => (Math.floor(index / interiorColumns) + 1) * columns + index % interiorColumns + 1,+  ));+  // Density controls occupied routing cells only. Feature scale changes the+  // physical size of the whole motif without secretly changing this target.+  const targetFill = Math.min(0.82, Math.max(0.18, config.fill * config.density));+  for (const index of seeds) {+    if (state.occupied / (columns * rows) >= targetFill) break;+    if (state.blocked[index]) continue;+    makeInteriorTrace([index % columns, Math.floor(index / columns)], state, random, config);+  }++  return {+    width: (columns - 1) * pitch,+    height: (rows - 1) * pitch,+    columns,+    rows,+    fill: state.occupied / (columns * rows),+    traces: state.traces,+  };+}++export function pathData(points) {+  return points.map(([x, y], index) => `${index ? "L" : "M"}${x.toFixed(2)},${y.toFixed(2)}`).join("");+}++function svgElement(name, attributes = {}) {+  const element = document.createElementNS(SVG_NS, name);+  for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, String(value));+  return element;+}++export function createCircuitBackground(mount, options = {}) {+  if (!(mount instanceof Element)) throw new TypeError("mount must be a DOM element");+  const id = `circuit-field-${++instanceCount}`;+  let config = { ...DEFAULTS, ...options };+  let field;+  let svg;+  let motionCanvas;+  let motionContext;+  let brandGradient;+  let conductor;+  let resizeObserver;+  let resizeTimer;+  let frameRequest;+  let frameCount = 0;+  let activeTraces = 0;+  let bloomsTriggered = 0;+  let visuals = [];+  let blooms = [];+  let pointer = null;+  let motionRandom;+  let lastSize = "";+  let sceneStart = 0;+  let lastFrameTime = 0;+  let lastPaintTime = 0;+  let nextBloomAt = 0;+  let bloomCursor = 0;+  let focus = { x: 0, y: 0 };+  let rendererPixelRatio = 1;+  let targetFrameRate = 60;+  let frameInterval = 1000 / targetFrameRate;+  let energyBudget = 0;+  let sparkBudget = 0;+  let nodeBudget = 0;+  let drawnEnergyTraces = 0;+  let drawnSparks = 0;+  let drawnNodes = 0;+  const energyCandidates = [];+  const sparkCandidates = [];+  const nodeCandidates = [];+  const interactionTarget = mount.parentElement || mount;+  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;+  const originalPosition = mount.style.position;+  if (getComputedStyle(mount).position === "static") mount.style.position = "relative";+  mount.style.overflow = "hidden";++  function life() {+    return Math.max(0, Math.min(1.5, config.liveliness));+  }++  function featureScale() {+    return Math.max(0.25, config.featureScale);+  }++  function clamp(value, minimum = 0, maximum = 1) {+    return Math.max(minimum, Math.min(maximum, value));+  }++  function colorAt(position) {+    if (position < 0.46) return "#00e6dc";+    if (position < 0.76) return "#39b8ff";+    return "#8c6bf7";+  }++  function addVia(group, point) {+    const scale = featureScale();+    group.append(+      svgElement("circle", { cx: point[0], cy: point[1], r: config.viaOuterRadius * scale, fill: config.traceColor }),+      svgElement("circle", { cx: point[0], cy: point[1], r: config.viaInnerRadius * scale, fill: "var(--circuit-hole, #0d1117)" }),+    );+  }++  function build(force = false) {+    const pixelWidth = Math.max(1, mount.clientWidth);+    const pixelHeight = Math.max(1, mount.clientHeight);+    const worldWidth = pixelWidth / config.scale;+    const worldHeight = pixelHeight / config.scale;+    const pitch = config.pitch * featureScale();+    const size = `${Math.floor(worldWidth / pitch)}x${Math.floor(worldHeight / pitch)}`;+    if (!force && size === lastSize) return false;+    lastSize = size;+    activeTraces = 0;+    visuals = [];+    blooms = [];+    conductor?.destroy();+    conductor = undefined;+    motionCanvas?.remove();+    motionCanvas = undefined;+    motionContext = undefined;+    brandGradient = undefined;+    svg?.remove();+    field = generateCircuitField({ ...config, width: worldWidth, height: worldHeight });+    const motionSeed = config.animationSystem === "conductor"+      ? `${config.seed}:${size}:signals`+      : `${config.seed}:${size}:adom-motion`;+    motionRandom = mulberry32(hashSeed(motionSeed));+    svg = svgElement("svg", {+      id, "aria-hidden": true, focusable: false,+      viewBox: `0 0 ${field.width} ${field.height}`,+      preserveAspectRatio: "none",+    });+    Object.assign(svg.style, {+      position: "absolute", inset: "0", width: "100%", height: "100%",+      display: "block", pointerEvents: "none",+    });++    const traceLayer = svgElement("g", { "data-static-traces": "" });+    for (const trace of field.traces) {+      traceLayer.appendChild(svgElement("path", {+        d: pathData(trace.points), fill: "none", stroke: config.traceColor,+        "stroke-width": config.traceWidth * config.featureScale,+        "stroke-linecap": "butt", "stroke-linejoin": "miter",+      }));+      if (trace.viaStart) addVia(traceLayer, trace.points[0]);+      if (trace.viaEnd) addVia(traceLayer, trace.points.at(-1));+    }++    if (config.animationSystem === "none") {+      svg.append(traceLayer);+      mount.prepend(svg);+      return true;+    }++    if (config.animationSystem === "conductor") {+      svg.append(traceLayer);+      mount.prepend(svg);+      conductor = createConductorAnimation(svg, field, config, motionRandom);+      return true;+    }++    const angle = config.sweepAngle * Math.PI / 180;+    const axisX = Math.cos(angle);+    const axisY = Math.sin(angle);+    let projectionMin = Infinity;+    let projectionMax = -Infinity;+    const prepared = field.traces.map((trace, index) => {+      const midpoint = trace.points[Math.floor(trace.points.length / 2)];+      const projection = midpoint[0] * axisX + midpoint[1] * axisY;+      projectionMin = Math.min(projectionMin, projection);+      projectionMax = Math.max(projectionMax, projection);+      return { trace, index, midpoint, projection };+    });+    const projectionSpan = Math.max(1, projectionMax - projectionMin);+    const scale = featureScale();++    for (const preparedTrace of prepared) {+      const { trace, index, midpoint } = preparedTrace;+      const normalizedProjection = (preparedTrace.projection - projectionMin) / projectionSpan;+      const directionBias = trace.points[0][0] <= trace.points.at(-1)[0] ? 1 : -1;+      const sparkLength = clamp(trace.length * (0.1 + motionRandom() * 0.06), 10 * scale, 58 * scale);+      const endpoint = trace.points[(index + Math.round(normalizedProjection * 5)) % 2 ? 0 : trace.points.length - 1];+      let canvasPath = null;+      if (typeof Path2D === "function") {+        canvasPath = new Path2D();+        trace.points.forEach(([x, y], pointIndex) => {+          if (pointIndex) canvasPath.lineTo(x, y);+          else canvasPath.moveTo(x, y);+        });+      }+      visuals.push({+        trace, midpoint, projection: normalizedProjection,+        canvasPath, endpoint, nodeColor: colorAt(normalizedProjection),+        sparkLength, directionBias,+        charge: 0, phase: motionRandom(), sparkEligible: motionRandom() > 0.34,+        energyOpacity: 0, sparkOpacity: 0, nodeOpacity: 0,+      });+    }++    svg.append(traceLayer);+    mount.prepend(svg);++    const traceCount = visuals.length;+    targetFrameRate = traceCount > 1500 ? 24 : traceCount > 900 ? 30 : traceCount > 420 ? 45 : 60;+    frameInterval = 1000 / targetFrameRate;+    energyBudget = traceCount > 1500 ? 420 : traceCount > 900 ? 500 : traceCount > 420 ? 620 : traceCount;+    sparkBudget = traceCount > 1500 ? 28 : traceCount > 900 ? 36 : traceCount > 420 ? 46 : 58;+    nodeBudget = traceCount > 1500 ? 48 : traceCount > 900 ? 64 : traceCount > 420 ? 82 : 104;+    const deviceRatio = Math.max(1, window.devicePixelRatio || 1);+    rendererPixelRatio = Math.min(deviceRatio, traceCount > 1200 ? 1 : traceCount > 600 ? 1.25 : 1.5);++    motionCanvas = document.createElement("canvas");+    motionCanvas.setAttribute("aria-hidden", "true");+    motionCanvas.setAttribute("data-animation", "routed-field-canvas");+    Object.assign(motionCanvas.style, {+      position: "absolute", inset: "0", width: "100%", height: "100%",+      display: "block", pointerEvents: "none", mixBlendMode: "screen", zIndex: "1",+    });+    motionCanvas.width = Math.max(1, Math.round(pixelWidth * rendererPixelRatio));+    motionCanvas.height = Math.max(1, Math.round(pixelHeight * rendererPixelRatio));+    mount.appendChild(motionCanvas);+    motionContext = motionCanvas.getContext("2d", { alpha: true, desynchronized: true })+      || motionCanvas.getContext("2d");+    if (motionContext) {+      motionContext.setTransform(+        motionCanvas.width / field.width, 0,+        0, motionCanvas.height / field.height, 0, 0,+      );+      brandGradient = motionContext.createLinearGradient(+        0, field.height * 0.15, field.width, field.height * 0.85,+      );+      brandGradient.addColorStop(0, "#00e6dc");+      brandGradient.addColorStop(0.6, "#39b8ff");+      brandGradient.addColorStop(1, "#8c6bf7");+    }+    focus = { x: field.width * 0.5, y: field.height * 0.5 };+    return true;+  }++  function spawnBloom(origin = null, intensity = 1) {+    if (config.animationSystem !== "adom" || !config.motion || reducedMotion || !field?.traces.length) return false;+    const source = origin || { x: field.width * 0.24, y: field.height * 0.62 };+    const angle = Math.atan2(source.y - field.height * 0.5, source.x - field.width * 0.5);+    blooms.push({+      x: clamp(source.x, 0, field.width),+      y: clamp(source.y, 0, field.height),+      startedAt: performance.now(),+      duration: config.bloomDuration / clamp(life(), 0.55, 1.35),+      intensity: clamp(intensity, 0.35, 1.4),+      spin: angle / (Math.PI * 2) + motionRandom() * 0.18,+    });+    if (blooms.length > 3) blooms.shift();+    bloomsTriggered += 1;+    return true;+  }++  function spawnAutomaticBloom() {+    if (!visuals.length) return;+    const stride = Math.max(1, Math.floor(visuals.length * 0.381966));+    bloomCursor = (bloomCursor + stride) % visuals.length;+    const anchor = visuals[bloomCursor].midpoint;+    spawnBloom({ x: anchor[0], y: anchor[1] }, 0.78 + motionRandom() * 0.18);+  }++  function energyFromBloom(visual, bloom, timestamp, maximumDimension) {+    const progress = (timestamp - bloom.startedAt) / bloom.duration;+    if (progress < 0 || progress > 1) return 0;+    const dx = visual.midpoint[0] - bloom.x;+    const dy = visual.midpoint[1] - bloom.y;+    const distance = Math.hypot(dx, dy) / maximumDimension;+    const angle = (Math.atan2(dy, dx) / (Math.PI * 2) + 1 + bloom.spin) % 1;+    const arrival = distance * 1.08 + angle * 0.105;+    const distanceFromFront = progress - arrival;+    const crest = Math.exp(-(distanceFromFront * distanceFromFront) / (2 * 0.052 * 0.052));+    const afterglow = distanceFromFront > 0 && distanceFromFront < 0.24+      ? (1 - distanceFromFront / 0.24) ** 2 * 0.23+      : 0;+    return (crest + afterglow) * bloom.intensity;+  }++  function onPointerMove(event) {+    if (!field) return;+    const rect = mount.getBoundingClientRect();+    pointer = {+      x: (event.clientX - rect.left) / rect.width * field.width,+      y: (event.clientY - rect.top) / rect.height * field.height,+      at: performance.now(),+    };+    conductor?.pointerMoved(pointer);+  }++  function onPointerLeave() {+    if (pointer) pointer.at = performance.now() - 1800;+  }++  function onClick(event) {+    onPointerMove(event);+    if (config.animationSystem === "conductor") conductor?.clicked(pointer);+    else spawnBloom(pointer, 1.18);+  }++  function strokeCanvasPath(context, visual) {+    if (visual.canvasPath) {+      context.stroke(visual.canvasPath);+      return;+    }+    context.beginPath();+    visual.trace.points.forEach(([x, y], index) => {+      if (index) context.lineTo(x, y);+      else context.moveTo(x, y);+    });+    context.stroke();+  }++  function renderFrame(timestamp) {+    if (!motionCanvas?.isConnected || !motionContext || !visuals.length) return;+    frameRequest = window.requestAnimationFrame(renderFrame);+    if (document.hidden) return;+    const sincePaint = lastPaintTime ? timestamp - lastPaintTime : frameInterval;+    if (sincePaint < frameInterval) return;+    lastPaintTime = lastPaintTime+      ? timestamp - (sincePaint % frameInterval)+      : timestamp;+    frameCount += 1;+    const frameDelta = lastFrameTime ? clamp(timestamp - lastFrameTime, 8, 50) : 16;+    lastFrameTime = timestamp;++    const strength = life();+    const maximumDimension = Math.max(field.width, field.height);+    const elapsed = timestamp - sceneStart;+    const sweepPhase = ((elapsed % config.sweepDuration) + config.sweepDuration) % config.sweepDuration / config.sweepDuration;+    const sweepActive = sweepPhase < 0.73;+    const sweepFront = sweepActive ? -0.14 + sweepPhase / 0.73 * 1.28 : 2;+    const sweepWidth = config.sweepWidth / clamp(strength, 0.65, 1.25);++    if (timestamp >= nextBloomAt) {+      spawnAutomaticBloom();+      nextBloomAt = timestamp + config.bloomInterval / clamp(0.72 + strength * 0.28, 0.72, 1.12);+    }+    blooms = blooms.filter((bloom) => timestamp - bloom.startedAt <= bloom.duration);++    const pointerAge = pointer ? timestamp - pointer.at : Infinity;+    const pointerPresence = clamp(1 - pointerAge / 1450);+    const automaticFocus = {+      x: field.width * (0.5 + Math.sin(elapsed * 0.000095) * 0.27),+      y: field.height * (0.48 + Math.sin(elapsed * 0.000071 + 1.4) * 0.21),+    };+    const focusTarget = pointerPresence > 0.02 ? pointer : automaticFocus;+    const focusEase = pointerPresence > 0.02 ? 0.105 : 0.018;+    focus.x += (focusTarget.x - focus.x) * focusEase;+    focus.y += (focusTarget.y - focus.y) * focusEase;++    energyCandidates.length = 0;+    sparkCandidates.length = 0;+    nodeCandidates.length = 0;+    let active = 0;+    for (const visual of visuals) {+      const delta = sweepFront - visual.projection;+      const crest = sweepActive+        ? Math.exp(-(delta * delta) / (2 * sweepWidth * sweepWidth))+        : 0;+      const trail = sweepActive && delta > 0 && delta < 0.28+        ? (1 - delta / 0.28) ** 2 * 0.24+        : 0;+      let bloomEnergy = 0;+      for (const bloom of blooms) {+        bloomEnergy = Math.max(bloomEnergy, energyFromBloom(visual, bloom, timestamp, maximumDimension));+      }+      let pointerEnergy = 0;+      if (pointerPresence > 0.001) {+        const distance = Math.hypot(visual.midpoint[0] - pointer.x, visual.midpoint[1] - pointer.y);+        const radius = maximumDimension * config.pointerRadius;+        pointerEnergy = Math.exp(-(distance * distance) / (2 * radius * radius)) * pointerPresence * 0.42;+      }+      const ambient = 0.018 + (Math.sin(timestamp * 0.0005 + visual.phase * Math.PI * 2) + 1) * 0.006;+      const target = clamp((crest * 0.96 + trail + bloomEnergy + pointerEnergy + ambient) * strength, 0, 1.18);+      const response = target > visual.charge ? 1 - Math.exp(-frameDelta / 72) : 1 - Math.exp(-frameDelta / 610);+      visual.charge += (target - visual.charge) * response;++      const energyOpacity = clamp((visual.charge - 0.018) * 0.72, 0, 0.74);+      const sparkOpacity = visual.sparkEligible+        ? clamp((visual.charge - 0.25) * 1.48, 0, 0.92)+        : 0;+      const nodePulse = 0.82 + Math.sin(timestamp * 0.0031 + visual.phase * Math.PI * 2) * 0.18;+      const nodeOpacity = clamp((visual.charge - 0.34) * 1.65 * nodePulse, 0, 0.88);++      visual.energyOpacity = energyOpacity;+      visual.sparkOpacity = sparkOpacity;+      visual.nodeOpacity = nodeOpacity;+      if (energyOpacity > 0.008) energyCandidates.push(visual);+      if (sparkOpacity > 0.01) sparkCandidates.push(visual);+      if (nodeOpacity > 0.01) nodeCandidates.push(visual);+      if (visual.charge > 0.13) active += 1;+    }+    activeTraces = active;++    energyCandidates.sort((a, b) => b.energyOpacity - a.energyOpacity);+    sparkCandidates.sort((a, b) => b.sparkOpacity - a.sparkOpacity);+    nodeCandidates.sort((a, b) => b.nodeOpacity - a.nodeOpacity);+    drawnEnergyTraces = Math.min(energyBudget, energyCandidates.length);+    drawnSparks = Math.min(sparkBudget, sparkCandidates.length);+    drawnNodes = Math.min(nodeBudget, nodeCandidates.length);++    const context = motionContext;+    context.setTransform(1, 0, 0, 1, 0, 0);+    context.clearRect(0, 0, motionCanvas.width, motionCanvas.height);+    context.setTransform(+      motionCanvas.width / field.width, 0,+      0, motionCanvas.height / field.height, 0, 0,+    );++    const focusWash = context.createRadialGradient(+      focus.x, focus.y, 0,+      focus.x, focus.y, maximumDimension * 0.54,+    );+    focusWash.addColorStop(0, "rgba(0,230,220,.16)");+    focusWash.addColorStop(0.35, "rgba(57,184,255,.075)");+    focusWash.addColorStop(0.72, "rgba(140,107,247,.025)");+    focusWash.addColorStop(1, "rgba(140,107,247,0)");+    context.globalCompositeOperation = "source-over";+    context.globalAlpha = (0.58 + pointerPresence * 0.24) * clamp(strength, 0, 1.2);+    context.fillStyle = focusWash;+    context.fillRect(0, 0, field.width, field.height);++    context.globalCompositeOperation = "lighter";+    context.strokeStyle = brandGradient;+    context.lineWidth = config.traceWidth * featureScale() * 1.18;+    context.lineCap = "round";+    context.lineJoin = "miter";+    context.setLineDash([]);+    for (let index = 0; index < drawnEnergyTraces; index += 1) {+      const visual = energyCandidates[index];+      context.globalAlpha = visual.energyOpacity;+      strokeCanvasPath(context, visual);+    }++    context.lineCap = "round";+    context.lineJoin = "round";+    for (let index = 0; index < drawnSparks; index += 1) {+      const visual = sparkCandidates[index];+      const distance = visual.trace.length + visual.sparkLength;+      const travel = (timestamp * (0.052 + strength * 0.026) + visual.phase * distance) % distance;+      context.setLineDash([visual.sparkLength, distance]);+      context.lineDashOffset = -travel * visual.directionBias;+      context.lineWidth = config.traceWidth * featureScale() * 3.15;+      context.globalAlpha = visual.sparkOpacity * 0.16;+      strokeCanvasPath(context, visual);+      context.lineWidth = config.traceWidth * featureScale() * 1.42;+      context.globalAlpha = visual.sparkOpacity;+      strokeCanvasPath(context, visual);+    }+    context.setLineDash([]);+    context.lineDashOffset = 0;++    const nodeRadius = config.viaInnerRadius * featureScale() * 0.78;+    for (let index = 0; index < drawnNodes; index += 1) {+      const visual = nodeCandidates[index];+      context.fillStyle = visual.nodeColor;+      context.globalAlpha = visual.nodeOpacity * 0.14;+      context.beginPath();+      context.arc(visual.endpoint[0], visual.endpoint[1], nodeRadius * 2.65, 0, Math.PI * 2);+      context.fill();+      context.globalAlpha = visual.nodeOpacity;+      context.beginPath();+      context.arc(visual.endpoint[0], visual.endpoint[1], nodeRadius, 0, Math.PI * 2);+      context.fill();+    }+    context.globalAlpha = 1;+    context.globalCompositeOperation = "source-over";+  }++  function stopMotion() {+    window.cancelAnimationFrame(frameRequest);+    conductor?.stop();+    frameRequest = undefined;+    activeTraces = 0;+    lastFrameTime = 0;+    lastPaintTime = 0;+  }++  function startMotion() {+    stopMotion();+    if (config.animationSystem === "none" || !config.motion || reducedMotion || life() <= 0.02) return;+    if (config.animationSystem === "conductor") {+      conductor?.start();+      return;+    }+    const now = performance.now();+    sceneStart = now - 760;+    nextBloomAt = now + 1180;+    frameCount = 0;+    spawnBloom({ x: field.width * 0.2, y: field.height * 0.64 }, 0.88);+    frameRequest = window.requestAnimationFrame(renderFrame);+  }++  function regenerate(nextOptions = {}) {+    stopMotion();+    config = { ...config, ...nextOptions };+    lastSize = "";+    build(true);+    startMotion();+    return api;+  }++  function exportSvg() {+    const clone = svg.cloneNode(true);+    clone.querySelectorAll("[data-animation]").forEach((layer) => layer.remove());+    clone.removeAttribute("style");+    return `<?xml version="1.0" encoding="UTF-8"?>\n${clone.outerHTML}`;+  }++  function destroy() {+    stopMotion();+    conductor?.destroy();+    conductor = undefined;+    window.clearTimeout(resizeTimer);+    resizeObserver?.disconnect();+    interactionTarget.removeEventListener("pointermove", onPointerMove);+    interactionTarget.removeEventListener("pointerleave", onPointerLeave);+    interactionTarget.removeEventListener("click", onClick);+    motionCanvas?.remove();+    motionCanvas = undefined;+    motionContext = undefined;+    svg?.remove();+    mount.style.position = originalPosition;+  }++  const api = {+    regenerate,+    triggerBloom: spawnBloom,+    triggerAnimation(origin = null) {+      return config.animationSystem === "conductor"+        ? conductor?.trigger(origin)+        : spawnBloom(origin, 1.12);+    },+    exportSvg,+    destroy,+    get field() { return field; },+    get animationStats() {+      if (config.animationSystem === "none") {+        return {+          system: "none",+          animationNodes: 0,+          frameCount: 0,+          reducedMotion,+        };+      }+      if (config.animationSystem === "conductor") return conductor?.stats;+      return {+        system: "adom-routed-field",+        activeTraces,+        activeBlooms: blooms.length,+        bloomsTriggered,+        frameCount,+        renderer: "canvas",+        animationNodes: motionCanvas ? 1 : 0,+        traceCount: visuals.length,+        targetFrameRate,+        rendererPixelRatio,+        energyBudget,+        sparkBudget,+        nodeBudget,+        drawnEnergyTraces,+        drawnSparks,+        drawnNodes,+        reducedMotion,+      };+    },+  };+  build(true);+  resizeObserver = new ResizeObserver(() => {+    window.clearTimeout(resizeTimer);+    resizeTimer = window.setTimeout(() => {+      if (build()) {+        stopMotion();+        startMotion();+      }+    }, 220);+  });+  resizeObserver.observe(mount);+  if (config.interactive) {+    interactionTarget.addEventListener("pointermove", onPointerMove, { passive: true });+    interactionTarget.addEventListener("pointerleave", onPointerLeave, { passive: true });+    interactionTarget.addEventListener("click", onClick);+  }+  startMotion();+  return api;+}++export { DEFAULTS, ANIMATION_PRESETS };
src/circuit-background-v5.jsadded+334
@@ -0,0 +1,334 @@+import { createConductorAnimation } from "./circuit-animation-conductor.js?v=1";+import { createHandshakeAnimation } from "./circuit-animation-handshake.js?v=2";+import {+  createCircuitBackground as createLegacyCircuitBackground,+  DEFAULTS as GEOMETRY_DEFAULTS,+  ANIMATION_PRESETS as LEGACY_ANIMATION_PRESETS,+  generateCircuitField,+  pathData,+} from "./circuit-background-v4.js?geometry=1";++const SVG_NS = "http://www.w3.org/2000/svg";++const DEFAULTS = Object.freeze({+  ...GEOMETRY_DEFAULTS,+  animationSystem: "none",+  motion: false,+  liveliness: 0,+  atmosphereStrength: 0,+  handshakeInterval: 1650,+  handshakeSpeed: 230,+  handshakeLength: 48,+  maxHandshakes: 3,+});++const ANIMATION_PRESETS = Object.freeze({+  none: LEGACY_ANIMATION_PRESETS.none,+  conductor: LEGACY_ANIMATION_PRESETS.conductor,+  routed: LEGACY_ANIMATION_PRESETS.adom,+  handshake: Object.freeze({+    animationSystem: "handshake",+    motion: true,+    liveliness: 0.72,+    handshakeInterval: 1650,+    handshakeSpeed: 230,+    handshakeLength: 48,+    maxHandshakes: 3,+    atmosphereStrength: 0,+  }),+});++let instanceCount = 0;++function hashSeed(value) {+  let hash = 2166136261;+  for (const char of String(value)) {+    hash ^= char.charCodeAt(0);+    hash = Math.imul(hash, 16777619);+  }+  return hash >>> 0;+}++function mulberry32(seed) {+  return function random() {+    let value = (seed += 0x6d2b79f5);+    value = Math.imul(value ^ (value >>> 15), value | 1);+    value ^= value + Math.imul(value ^ (value >>> 7), value | 61);+    return ((value ^ (value >>> 14)) >>> 0) / 4294967296;+  };+}++function svgElement(name, attributes = {}) {+  const element = document.createElementNS(SVG_NS, name);+  for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, String(value));+  return element;+}++function createModernCircuitBackground(mount, options = {}) {+  if (!(mount instanceof Element)) throw new TypeError("mount must be a DOM element");+  const id = `circuit-field-${++instanceCount}`;+  let config = { ...DEFAULTS, ...options };+  let field;+  let svg;+  let conductor;+  let handshake;+  let resizeObserver;+  let resizeTimer;+  let pointer = null;+  let lastSize = "";+  const interactionTarget = mount.parentElement || mount;+  const readingZone = interactionTarget.querySelector?.("[data-circuit-reading-zone]") || null;+  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;+  const originalPosition = mount.style.position;+  const originalOverflow = mount.style.overflow;+  if (getComputedStyle(mount).position === "static") mount.style.position = "relative";+  mount.style.overflow = "hidden";++  function life() {+    return Math.max(0, Math.min(1.5, config.liveliness));+  }++  function featureScale() {+    return Math.max(0.25, config.featureScale);+  }++  function addVia(group, point) {+    const scale = featureScale();+    group.append(+      svgElement("circle", {+        cx: point[0], cy: point[1], r: config.viaOuterRadius * scale, fill: config.traceColor,+      }),+      svgElement("circle", {+        cx: point[0], cy: point[1], r: config.viaInnerRadius * scale,+        fill: "var(--circuit-hole, #0d1117)",+      }),+    );+  }++  function destroyAnimationSystems() {+    conductor?.destroy();+    handshake?.destroy();+    conductor = undefined;+    handshake = undefined;+  }++  function build(force = false) {+    const pixelWidth = Math.max(1, mount.clientWidth);+    const pixelHeight = Math.max(1, mount.clientHeight);+    const worldWidth = pixelWidth / config.scale;+    const worldHeight = pixelHeight / config.scale;+    const pitch = config.pitch * featureScale();+    const size = `${Math.floor(worldWidth / pitch)}x${Math.floor(worldHeight / pitch)}`;+    if (!force && size === lastSize) return false;+    lastSize = size;++    destroyAnimationSystems();+    svg?.remove();+    field = generateCircuitField({ ...config, width: worldWidth, height: worldHeight });+    const motionNamespace = config.animationSystem === "conductor" ? "signals" : "handshakes";+    const motionRandom = mulberry32(hashSeed(`${config.seed}:${size}:${motionNamespace}`));++    svg = svgElement("svg", {+      id,+      "aria-hidden": true,+      focusable: false,+      viewBox: `0 0 ${field.width} ${field.height}`,+      preserveAspectRatio: "none",+    });+    Object.assign(svg.style, {+      position: "absolute",+      inset: "0",+      width: "100%",+      height: "100%",+      display: "block",+      pointerEvents: "none",+    });++    const traceLayer = svgElement("g", { "data-static-traces": "" });+    for (const trace of field.traces) {+      traceLayer.appendChild(svgElement("path", {+        d: pathData(trace.points),+        fill: "none",+        stroke: config.traceColor,+        "stroke-width": config.traceWidth * config.featureScale,+        "stroke-linecap": "butt",+        "stroke-linejoin": "miter",+      }));+      if (trace.viaStart) addVia(traceLayer, trace.points[0]);+      if (trace.viaEnd) addVia(traceLayer, trace.points.at(-1));+    }+    svg.append(traceLayer);+    mount.prepend(svg);++    if (config.animationSystem === "conductor") {+      conductor = createConductorAnimation(svg, field, config, motionRandom);+    } else if (+      config.animationSystem === "handshake"+      && config.motion+      && !reducedMotion+      && life() > 0.02+    ) {+      handshake = createHandshakeAnimation(mount, field, config, motionRandom, { readingZone });+    }+    return true;+  }++  function onPointerMove(event) {+    if (!field) return;+    const rect = mount.getBoundingClientRect();+    pointer = {+      x: (event.clientX - rect.left) / rect.width * field.width,+      y: (event.clientY - rect.top) / rect.height * field.height,+      at: performance.now(),+    };+    conductor?.pointerMoved(pointer);+    handshake?.pointerMoved(pointer);+  }++  function onPointerLeave() {+    if (pointer) pointer.at = performance.now() - 3000;+    handshake?.pointerLeft();+  }++  function onClick(event) {+    onPointerMove(event);+    if (config.animationSystem === "conductor") conductor?.clicked(pointer);+    else if (config.animationSystem === "handshake") handshake?.clicked(pointer);+  }++  function stopMotion() {+    conductor?.stop();+    handshake?.stop();+  }++  function startMotion() {+    stopMotion();+    if (config.animationSystem === "none" || !config.motion || reducedMotion || life() <= 0.02) return;+    if (config.animationSystem === "conductor") conductor?.start();+    else if (config.animationSystem === "handshake") handshake?.start();+  }++  function regenerate(nextOptions = {}) {+    stopMotion();+    config = { ...config, ...nextOptions };+    lastSize = "";+    build(true);+    startMotion();+    return api;+  }++  function triggerAnimation(origin = null) {+    if (config.animationSystem === "conductor") return conductor?.trigger(origin) || false;+    if (config.animationSystem === "handshake") return handshake?.trigger(origin) || false;+    return false;+  }++  function exportSvg() {+    const clone = svg.cloneNode(true);+    clone.querySelectorAll("[data-animation]").forEach((layer) => layer.remove());+    clone.removeAttribute("style");+    return `<?xml version="1.0" encoding="UTF-8"?>\n${clone.outerHTML}`;+  }++  function destroy() {+    stopMotion();+    destroyAnimationSystems();+    window.clearTimeout(resizeTimer);+    resizeObserver?.disconnect();+    interactionTarget.removeEventListener("pointermove", onPointerMove);+    interactionTarget.removeEventListener("pointerleave", onPointerLeave);+    interactionTarget.removeEventListener("click", onClick);+    svg?.remove();+    mount.style.position = originalPosition;+    mount.style.overflow = originalOverflow;+  }++  const api = {+    regenerate,+    triggerAnimation,+    triggerBloom: triggerAnimation,+    exportSvg,+    destroy,+    get field() { return field; },+    get animationStats() {+      if (config.animationSystem === "none") {+        return { system: "none", animationNodes: 0, frameCount: 0, reducedMotion };+      }+      if (config.animationSystem === "conductor") return conductor?.stats;+      if (config.animationSystem === "handshake") {+        return handshake?.stats || {+          system: "adom-handshake",+          animationNodes: 0,+          activeTransactions: 0,+          frameCount: 0,+          reducedMotion,+        };+      }+      return { system: "unknown", animationNodes: 0, frameCount: 0, reducedMotion };+    },+  };++  build(true);+  resizeObserver = new ResizeObserver(() => {+    window.clearTimeout(resizeTimer);+    resizeTimer = window.setTimeout(() => {+      if (build()) {+        stopMotion();+        startMotion();+      }+    }, 220);+  });+  resizeObserver.observe(mount);+  if (config.interactive) {+    interactionTarget.addEventListener("pointermove", onPointerMove, { passive: true });+    interactionTarget.addEventListener("pointerleave", onPointerLeave, { passive: true });+    interactionTarget.addEventListener("click", onClick);+  }+  startMotion();+  return api;+}++export function createCircuitBackground(mount, options = {}) {+  let config = { ...DEFAULTS, ...options };+  let destroyed = false;+  const usesRoutedField = (value) => value.animationSystem === "adom";+  const createEngine = () => usesRoutedField(config)+    ? createLegacyCircuitBackground(mount, config)+    : createModernCircuitBackground(mount, config);+  let engine = createEngine();++  const api = {+    regenerate(nextOptions = {}) {+      if (destroyed) throw new Error("Cannot regenerate a destroyed circuit background");+      const wasRouted = usesRoutedField(config);+      config = { ...config, ...nextOptions };+      if (wasRouted !== usesRoutedField(config)) {+        engine.destroy();+        engine = createEngine();+      } else {+        engine.regenerate(nextOptions);+      }+      return api;+    },+    triggerAnimation(origin = null) {+      return engine.triggerAnimation?.(origin) || false;+    },+    triggerBloom(origin = null) {+      return engine.triggerBloom?.(origin) || engine.triggerAnimation?.(origin) || false;+    },+    exportSvg() {+      return engine.exportSvg();+    },+    destroy() {+      if (destroyed) return;+      destroyed = true;+      engine.destroy();+    },+    get field() { return engine.field; },+    get animationStats() { return engine.animationStats; },+  };++  return api;+}++export { DEFAULTS, ANIMATION_PRESETS, generateCircuitField, pathData };
src/circuit-background.jsadded+1830
@@ -0,0 +1,1830 @@+const SVG_NS = "http://www.w3.org/2000/svg";++// Geometry constants measured from the reference artwork (adom.inc hero):+// a 114 x 57 cell routing grid with a 150/19 ~= 7.895 unit pitch, 1.58 unit+// traces, and 3.34 / 1.76 annular vias sitting exactly on grid nodes.+const DEFAULTS = Object.freeze({+  seed: "traces",+  tileWidth: 900,+  tileHeight: 450,+  pitch: 150 / 19,+  scale: 1000 / 900,+  density: 1.05,+  traceColor: "#151b25",+  traceWidth: 1.58,+  viaOuterRadius: 3.34,+  viaInnerRadius: 1.76,+  // --- generation tuning -----------------------------------------------+  formations: 96, // terminal groups (via ladders/rows) per tile+  busWidth: 1, // scales pins per formation => bundle width+  turnPenalty: 0.55, // higher = longer, calmer runs+  weave: 0.22, // preference for 2-lane spacing between neighbors+  cramp: 0.4, // aversion to 1-lane spacing outside bundles+  horizontalBias: 0.6, // 0.5 = isotropic, higher = more horizontal flow+  netReach: 42, // max cells a net may span+  jogBias: 5, // preferred endpoint misalignment => staircase jogs+  stubChance: 0, // leftover pins that get short escape stubs+  inlineViaChance: 0.03, // per-step chance of a mid-trace via+  combs: 8, // max tap-comb motifs per tile+  signalColors: [+    ["#00e6dc", 0.6],+    ["#64abff", 0.25],+    ["#c5b3ff", 0.15],+  ],+  signals: true,+  interactive: true,+  signalInterval: 240, // passive spawn cadence — a fresh signal appears near the cursor this often+  maxSignals: 12, // fill target for the ambient fountain (maxBurstSignals is the hard ceiling)+  spawnRadius: 110, // spawn scatter around the pointer (soft jitter, not a hard cutoff)+  signalSpeed: 420,+  signalLength: 120,+  burstBoost: 7, // click: how many extra signals join at once+  hopSurvival: 0.4, // per-hop survival for PASSIVE signals; low = short-lived, high = long chains+  burstSurvival: 0.72, // per-hop survival for CLICKED signals — tuned independently of hopSurvival+  hopRadius: 440, // a surviving signal hops to a via within this range+  colorDrift: 0.18, // chance per hop that a chain crossfades to a new color+  maxBurstSignals: 25,+});++// 8 directions, clockwise from east; even indices are cardinal.+const DIR8 = [+  [1, 0],+  [1, 1],+  [0, 1],+  [-1, 1],+  [-1, 0],+  [-1, -1],+  [0, -1],+  [1, -1],+];++// Walker start-direction weights derived from horizontalBias (0.5 = even).+function startDirectionWeights(horizontalBias) {+  const ew = 0.48 * horizontalBias;+  const ns = 0.24 * (1 - horizontalBias);+  const diagonal = Math.max(0.005, (1 - 2 * ew - 2 * ns) / 4);+  return [+    [0, ew],+    [4, ew],+    [2, ns],+    [6, ns],+    [1, diagonal],+    [3, diagonal],+    [5, diagonal],+    [7, diagonal],+  ];+}++let instanceCount = 0;++function svgEl(name, attrs = {}) {+  const node = document.createElementNS(SVG_NS, name);+  for (const [key, value] of Object.entries(attrs)) {+    node.setAttribute(key, String(value));+  }+  return node;+}++function hashSeed(input) {+  let hash = 2166136261;+  for (const char of String(input)) {+    hash ^= char.charCodeAt(0);+    hash = Math.imul(hash, 16777619);+  }+  return hash >>> 0;+}++function mulberry32(seed) {+  return function random() {+    let value = (seed += 0x6d2b79f5);+    value = Math.imul(value ^ (value >>> 15), value | 1);+    value ^= value + Math.imul(value ^ (value >>> 7), value | 61);+    return ((value ^ (value >>> 14)) >>> 0) / 4294967296;+  };+}++function weightedChoice(random, entries) {+  let roll = random();+  for (const [value, weight] of entries) {+    roll -= weight;+    if (roll <= 0) return value;+  }+  return entries[0][0];+}++// --- routing engine ---------------------------------------------------------+//+// Occupancy lives on a doubled ("half step") grid wrapped on a torus: every+// cell step claims its end node and the midpoint it crosses, which blocks+// same-lane overlap and any crossing (two diagonals share a midpoint, a+// diagonal crosses a cardinal only through a node). The torus wrap is what+// makes the finished tile seamless.++function createRouter(random, columns, rows) {+  const wide = columns * 2;+  const tall = rows * 2;+  // Occupancy as a flat typed array — this sits in the A* hot path.+  const occupied = new Uint8Array(wide * tall);+  const viaNodes = new Uint8Array(wide * tall);++  const halfKey = (hx, hy) => (((hy % tall) + tall) % tall) * wide + (((hx % wide) + wide) % wide);+  const nodeKey = (x, y) => halfKey(x * 2, y * 2);++  function claim(trace, key) {+    occupied[key] = 1;+    trace.claims.push(key);+    trace.ownKeys.add(key);+  }++  return {+    columns,+    rows,+    nodeKey,+    nodeFree: (x, y) => !occupied[nodeKey(x, y)],+    crowding(x, y) {+      let count = 0;+      for (let dy = -4; dy <= 4; dy += 2) {+        for (let dx = -4; dx <= 4; dx += 2) {+          if (occupied[nodeKey(x + dx, y + dy)]) count += 1;+        }+      }+      return count;+    },+    stepFree(x, y, direction) {+      const [dx, dy] = DIR8[direction];+      return (+        !occupied[halfKey(x * 2 + dx, y * 2 + dy)] &&+        !occupied[halfKey(x * 2 + dx * 2, y * 2 + dy * 2)]+      );+    },+    // Like stepFree, but the destination node may be the (already-claimed)+    // goal pin the route is allowed to land on.+    routeStepFree(x, y, direction, goalKey) {+      const [dx, dy] = DIR8[direction];+      if (occupied[halfKey(x * 2 + dx, y * 2 + dy)]) return false;+      const node = halfKey(x * 2 + dx * 2, y * 2 + dy * 2);+      return node === goalKey || !occupied[node];+    },+    // How close the nearest parallel neighbor sits, perpendicular to travel:+    // 1 = one lane away (cramped), 2 = two lanes away (the reference's even+    // weave), 0 = open field. The A* cost prefers 2 and avoids 1.+    laneNeighbors(x, y, direction) {+      const [dx, dy] = DIR8[direction];+      if (occupied[nodeKey(x - dy, y + dx)] || occupied[nodeKey(x + dy, y - dx)]) return 1;+      if (occupied[nodeKey(x - dy * 2, y + dx * 2)] || occupied[nodeKey(x + dy * 2, y - dx * 2)]) return 2;+      return 0;+    },+    claimNode(trace, x, y) {+      claim(trace, nodeKey(x, y));+    },+    claimStep(trace, x, y, direction) {+      const [dx, dy] = DIR8[direction];+      claim(trace, halfKey(x * 2 + dx, y * 2 + dy));+      let count = 1;+      const node = halfKey(x * 2 + dx * 2, y * 2 + dy * 2);+      if (!occupied[node]) {+        claim(trace, node);+        count = 2;+      }+      trace.stepClaims.push(count);+    },+    // Remove the record's most recent step and exactly the claims it made.+    // Only valid while nothing later than that step has claimed (sealEnd+    // never trims past the record's own last via).+    unclaimLastStep(record) {+      const count = record.stepClaims.pop() || 0;+      for (let index = 0; index < count; index += 1) {+        const key = record.claims.pop();+        occupied[key] = 0;+        record.ownKeys.delete(key);+      }+      const step = record.steps.pop();+      record.end = [record.end[0] - DIR8[step][0], record.end[1] - DIR8[step][1]];+    },+    // A via may not sit on or cardinally beside another via (their annular+    // rings would overlap at one-pitch spacing), and the four diagonal+    // midpoints around it must not carry someone else's trace.+    viaAllowed(trace, x, y) {+      for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) {+        if (viaNodes[nodeKey(x + dx, y + dy)]) return false;+      }+      for (const [dx, dy] of [[1, 1], [1, -1], [-1, 1], [-1, -1]]) {+        const key = halfKey(x * 2 + dx, y * 2 + dy);+        if (occupied[key] && !trace.ownKeys.has(key)) return false;+      }+      return true;+    },+    placeVia(trace, x, y) {+      viaNodes[nodeKey(x, y)] = 1;+      trace.viaCells.push([x, y]);+      trace.viaSteps.add(trace.steps.length);+      for (const [dx, dy] of [[1, 1], [1, -1], [-1, 1], [-1, -1]]) {+        const key = halfKey(x * 2 + dx, y * 2 + dy);+        if (!occupied[key]) claim(trace, key);+      }+    },+    rollback(trace) {+      for (const key of trace.claims) occupied[key] = 0;+      for (const [x, y] of trace.viaCells) viaNodes[nodeKey(x, y)] = 0;+    },+  };+}++function newTraceRecord(x, y) {+  return {+    start: [x, y],+    end: [x, y],+    steps: [],+    claims: [],+    stepClaims: [], // claims added per step, so ends can be trimmed precisely+    ownKeys: new Set(),+    viaCells: [],+    viaSteps: new Set(),+  };+}++// Every trace must end on a via. If the end position has no clearance, trim+// the trace back one step at a time (never past its own last via) until a+// via fits. Returns false when no compliant ending exists.+function sealEnd(router, record, minSteps) {+  for (;;) {+    if (record.viaSteps.has(record.steps.length)) return true;+    const [x, y] = record.end;+    if (router.viaAllowed(record, x, y)) {+      router.placeVia(record, x, y);+      return true;+    }+    const floor = Math.max(minSteps, record.viaSteps.size ? Math.max(...record.viaSteps) : 0);+    if (record.steps.length <= floor) return false;+    router.unclaimLastStep(record);+  }+}++// --- A* net router ----------------------------------------------------------+//+// Routes run between terminal formations the way a human lays out a board:+// they escape a pin, join a corridor beside existing traces (hug bonus), pay+// for every 45 degree turn (long calm runs), pay a small surcharge for+// sustained diagonals (the reference uses short 45 degree jogs), and finish+// into the far pin. States are (node, incoming direction) so turn costs are+// exact. Everything wraps on the torus.++class MinHeap {+  constructor() {+    this.costs = [];+    this.values = [];+  }++  get size() {+    return this.costs.length;+  }++  push(cost, value) {+    const { costs, values } = this;+    let index = costs.length;+    costs.push(cost);+    values.push(value);+    while (index > 0) {+      const parent = (index - 1) >> 1;+      if (costs[parent] <= costs[index]) break;+      [costs[parent], costs[index]] = [costs[index], costs[parent]];+      [values[parent], values[index]] = [values[index], values[parent]];+      index = parent;+    }+  }++  pop() {+    const { costs, values } = this;+    const top = values[0];+    const lastCost = costs.pop();+    const lastValue = values.pop();+    if (costs.length) {+      costs[0] = lastCost;+      values[0] = lastValue;+      let index = 0;+      for (;;) {+        const left = index * 2 + 1;+        const right = left + 1;+        let smallest = index;+        if (left < costs.length && costs[left] < costs[smallest]) smallest = left;+        if (right < costs.length && costs[right] < costs[smallest]) smallest = right;+        if (smallest === index) break;+        [costs[smallest], costs[index]] = [costs[index], costs[smallest]];+        [values[smallest], values[index]] = [values[index], values[smallest]];+        index = smallest;+      }+    }+    return top;+  }+}++function createPathfinder(router, columns, rows, config) {+  const turnPenalty = config.turnPenalty;+  const crampPenalty = config.cramp;+  const weaveBonus = config.weave;+  const stateCount = columns * rows * 8;+  const bestCost = new Float64Array(stateCount);+  const seen = new Int32Array(stateCount);+  const closed = new Int32Array(stateCount);+  const parent = new Int32Array(stateCount);+  let generation = 0;++  const wrapX = (value) => ((value % columns) + columns) % columns;+  const wrapY = (value) => ((value % rows) + rows) % rows;++  // start: {x, y, direction}; goal: {x, y, arrivals: Set<direction>}+  // bundleMode flips the lane preference: followers rejoining their bundle+  // WANT to ride one lane off their neighbor.+  return function route(random, start, goal, why = {}, bundleMode = false) {+    generation += 1;+    const heap = new MinHeap();+    const goalX = wrapX(goal.x);+    const goalY = wrapY(goal.y);+    const goalKey = router.nodeKey(goalX, goalY);++    const heuristic = (x, y) => {+      let dx = Math.abs(x - goalX);+      dx = Math.min(dx, columns - dx);+      let dy = Math.abs(y - goalY);+      dy = Math.min(dy, rows - dy);+      return Math.max(dx, dy) + 0.42 * Math.min(dx, dy);+    };++    const startX = wrapX(start.x);+    const startY = wrapY(start.y);+    // Seed the facing direction free, and the two perpendiculars at a small+    // cost — a pin whose lane is congested can still hook out sideways.+    const h0 = heuristic(startX, startY);+    for (const [turn, cost] of [[0, 0], [2, 0.9], [6, 0.9]]) {+      const startState = (startY * columns + startX) * 8 + ((start.direction + turn) % 8);+      seen[startState] = generation;+      bestCost[startState] = cost;+      parent[startState] = -1;+      heap.push(cost + h0, startState);+    }++    const straightestFirst = heuristic(startX, startY);+    let goalState = -1;+    let expansions = 0;+    const TURNS = [0, 1, 7];++    // Weighted A* (heuristic scaled below): meaningfully faster, and the mild+    // suboptimality reads as organic hand-routing rather than error.+    while (heap.size && expansions < 9000) {+      expansions += 1;+      const state = heap.pop();+      if (closed[state] === generation) continue;+      closed[state] = generation;+      const direction = state % 8;+      const nodeIndex = (state - direction) / 8;+      const x = nodeIndex % columns;+      const y = (nodeIndex - x) / columns;++      if (x === goalX && y === goalY) {+        if (goal.arrivals.has(direction)) {+          goalState = state;+          break;+        }+        continue;+      }++      const cost = bestCost[state];+      for (const turn of TURNS) {+        const next = (direction + turn) % 8;+        if (!router.routeStepFree(x, y, next, goalKey)) continue;+        const nx = wrapX(x + DIR8[next][0]);+        const ny = wrapY(y + DIR8[next][1]);+        let stepCost = next % 2 === 0 ? 1 : 1.414;+        if (turn !== 0) stepCost += turnPenalty;+        const lane = router.laneNeighbors(nx, ny, next);+        if (bundleMode) {+          if (lane === 1) stepCost -= 0.22;+        } else if (lane === 1) stepCost += crampPenalty;+        else if (lane === 2) stepCost -= weaveBonus;+        stepCost += random() * 0.05;+        const nextState = (ny * columns + nx) * 8 + next;+        const total = cost + stepCost;+        if (seen[nextState] === generation && bestCost[nextState] <= total) continue;+        seen[nextState] = generation;+        bestCost[nextState] = total;+        parent[nextState] = state;+        heap.push(total + heuristic(nx, ny) * 1.2, nextState);+      }+    }++    if (goalState < 0) {+      why.reason = heap.size ? "expansions" : "exhausted";+      return null;+    }+    const directions = [];+    let cursor = goalState;+    while (parent[cursor] >= 0) {+      directions.push(cursor % 8);+      cursor = parent[cursor];+    }+    directions.reverse();+    // Reject absurd detours (torus wrap can produce them when hemmed in).+    if (directions.length > straightestFirst * 2.4 + 12) {+      why.reason = "detour";+      return null;+    }+    return directions;+  };+}++// --- terminal formations ----------------------------------------------------+//+// The reference pattern's vias are not sprinkled at random: they sit in small+// deliberate formations — diagonal ladders (staggered bus fanouts), rows, and+// singles — and traces escape them on a consistent side. Formations are placed+// with best-candidate sampling so the tile keeps even coverage and breathing+// room between groups.++function placeFormations(random, router, columns, rows, count, config) {+  const formations = [];+  const centers = [];+  const busWidth = config.busWidth;+  const horizontalEscape = Math.min(0.95, config.horizontalBias + 0.02);+  const rowSplit = 0.52 + 0.26 * config.horizontalBias;+  const wrapDist = (a, b, m) => {+    const d = Math.abs(a - b) % m;+    return Math.min(d, m - d);+  };++  for (let index = 0; index < count; index += 1) {+    let best = null;+    let bestClearance = -1;+    for (let candidate = 0; candidate < 8; candidate += 1) {+      const x = Math.floor(random() * columns);+      const y = Math.floor(random() * rows);+      let clearance = Infinity;+      for (const [cx, cy] of centers) {+        clearance = Math.min(+          clearance,+          Math.hypot(wrapDist(x, cx, columns), wrapDist(y, cy, rows)),+        );+      }+      if (clearance > bestClearance) {+        bestClearance = clearance;+        best = [x, y];+      }+    }++    const [x, y] = best;+    const roll = random();+    const pins = [];+    let signature;+    if (roll < 0.52) {+      // Diagonal via ladder — the staggered bus-fanout motif.+      const length = Math.max(2, Math.min(8, Math.round((3 + random() * 4) * busWidth)));+      const slope = random() < 0.5 ? -1 : 1;+      const escape = random() < horizontalEscape ? 0 : 4;+      signature = `ladder${slope}`;+      for (let k = 0; k < length; k += 1) pins.push({ x: x + k, y: y + k * slope, escape });+    } else if (roll < rowSplit) {+      // Vertical via row (2-cell spacing), horizontal escapes.+      const length = Math.max(2, Math.min(6, Math.round((2 + random() * 2) * busWidth)));+      const escape = random() < 0.5 ? 0 : 4;+      signature = "vrow";+      for (let k = 0; k < length; k += 1) pins.push({ x, y: y + k * 2, escape });+    } else if (roll < 0.78) {+      // Horizontal via row, vertical escapes.+      const length = Math.max(2, Math.min(6, Math.round((2 + random() * 2) * busWidth)));+      const escape = random() < 0.5 ? 2 : 6;+      signature = "hrow";+      for (let k = 0; k < length; k += 1) pins.push({ x: x + k * 2, y, escape });+    } else {+      signature = "solo";+      pins.push({ x, y, escape: [0, 4, 2, 6][Math.floor(random() * 4)] });+    }++    // Reserve the pin nodes now; the via graphics are only placed when a net+    // or stub actually connects, so a formation that stays unconnected leaves+    // no orphaned via row behind.+    const record = newTraceRecord(pins[0].x, pins[0].y);+    let placeable = true;+    for (const pin of pins) {+      if (!router.nodeFree(pin.x, pin.y)) {+        placeable = false;+        break;+      }+      router.claimNode(record, pin.x, pin.y);+    }+    if (!placeable) {+      router.rollback(record);+      continue;+    }+    centers.push([x, y]);+    formations.push({ pins: pins.map((pin) => ({ ...pin, used: false })), record, signature });+  }+  return formations;+}++// Start a trace record at a pin with its mandatory via already in place.+// Returns null when the pin has no via clearance (the pin is then skipped —+// every trace must begin on a via).+function startAtPin(router, pin) {+  const record = newTraceRecord(pin.x, pin.y);+  if (!router.viaAllowed(record, pin.x, pin.y)) return null;+  router.placeVia(record, pin.x, pin.y);+  return record;+}++function signedWrap(delta, modulus) {+  let d = ((delta % modulus) + modulus) % modulus;+  if (d > modulus / 2) d -= modulus;+  return d;+}++// A route may enter a pin from the facing side or hook in from up to 90+// degrees off — only a straight-through backside entry is forbidden.+const ARRIVALS = DIR8.map((_, escape) => {+  const reverse = (escape + 4) % 8;+  return new Set([+    (reverse + 6) % 8,+    (reverse + 7) % 8,+    reverse,+    (reverse + 1) % 8,+    (reverse + 2) % 8,+  ]);+});++// Route bundles of nets between compatible formations. Only the FIRST pin+// pair is routed with A*; the remaining pins get pure translated copies of+// the leader. Translation is what the reference does: neighbors that bend at+// the same places, producing nested chevrons and group staircases. Followers+// that hit an obstruction stop early and drop their via there — a natural+// fanout.+function routeNets(random, router, route, formations, addRecord, stats, config) {+  const majorFor = (escape) => (escape === 0 || escape === 4 ? 0 : 1);+  const sameNode = (x1, y1, x2, y2) =>+    router.nodeKey(x1, y1) === router.nodeKey(x2, y2);++  for (const formation of formations) {+    const minePins = formation.pins.filter((pin) => !pin.used);+    if (!minePins.length) continue;++    const escape = formation.pins[0].escape;+    const axis = majorFor(escape);+    // Find a partner formation whose pins face back toward this one; a+    // partner with the same shape nests perfectly under translation.+    let partner = null;+    let partnerScore = Infinity;+    for (const other of formations) {+      if (other === formation) continue;+      const otherFree = other.pins.filter((pin) => !pin.used);+      if (!otherFree.length) continue;+      if (other.pins[0].escape !== (escape + 4) % 8) continue;+      const dx = signedWrap(other.pins[0].x - formation.pins[0].x, router.columns);+      const dy = signedWrap(other.pins[0].y - formation.pins[0].y, router.rows);+      const major = axis === 0 ? dx : dy;+      const minor = axis === 0 ? dy : dx;+      const forward = escape === 0 || escape === 2 ? major : -major;+      if (forward < 8 || forward > config.netReach) continue;+      if (Math.abs(minor) > 12) continue;+      // Prefer a few cells of misalignment: it forces the staircase jogs the+      // reference is full of. Perfect alignment yields sterile laser lines.+      let score =+        Math.abs(Math.abs(minor) - config.jogBias) * 0.8 ++        Math.abs(forward - 22) * 0.3 ++        random() * 5;+      if (other.signature === formation.signature) score -= 4;+      if (score < partnerScore) {+        partnerScore = score;+        partner = other;+      }+    }+    if (!partner) continue;++    const minorOf = (pin) => (axis === 0 ? pin.y : pin.x);+    const mine = minePins.sort((a, b) => minorOf(a) - minorOf(b));+    const theirs = partner.pins+      .filter((pin) => !pin.used)+      .sort((a, b) => minorOf(a) - minorOf(b));+    const pairCount = Math.min(mine.length, theirs.length);+    if (!pairCount) continue;++    // Leader. Both pin vias go in before routing — every trace begins and+    // ends on a via, so a net is only viable if both terminals have+    // clearance.+    const leadFrom = mine[0];+    const leadTo = theirs[0];+    const leadRecord = startAtPin(router, leadFrom);+    if (!leadRecord) continue;+    if (!router.viaAllowed(leadRecord, leadTo.x, leadTo.y)) {+      router.rollback(leadRecord);+      continue;+    }+    router.placeVia(leadRecord, leadTo.x, leadTo.y);+    const why = {};+    const directions = route(+      random,+      { x: leadFrom.x, y: leadFrom.y, direction: leadFrom.escape },+      { x: leadTo.x, y: leadTo.y, arrivals: ARRIVALS[leadTo.escape] },+      why,+    );+    if (!directions) {+      router.rollback(leadRecord);+      stats.leaderFails += 1;+      stats[why.reason] = (stats[why.reason] || 0) + 1;+      continue;+    }+    stats.leaders += 1;+    stats.followerTries += pairCount - 1;+    {+      let x = leadFrom.x;+      let y = leadFrom.y;+      for (const step of directions) {+        router.claimStep(leadRecord, x, y, step);+        x += DIR8[step][0];+        y += DIR8[step][1];+        leadRecord.steps.push(step);+      }+      leadRecord.end = [x, y];+      leadFrom.used = true;+      leadTo.used = true;+      addRecord(leadRecord);+    }++    // Followers: translated copies pin-by-pin. A follower whose translated+    // corridor is blocked early gets rerouted with A* in bundle mode so it+    // wraps the obstacle and rejoins the group. Every follower starts on its+    // pin via and seals its far end with a via (trimming back if needed).+    for (let index = 1; index < pairCount; index += 1) {+      const from = mine[index];+      const to = theirs[index];+      const goalKey = router.nodeKey(to.x, to.y);++      const record = startAtPin(router, from);+      if (!record) {+        stats.followerFails += 1;+        continue;+      }++      const clearSteps = () => {+        while (record.steps.length) router.unclaimLastStep(record);+      };+      const layTranslation = () => {+        let x = from.x;+        let y = from.y;+        for (const step of directions) {+          if (!router.routeStepFree(x, y, step, goalKey)) break;+          router.claimStep(record, x, y, step);+          x += DIR8[step][0];+          y += DIR8[step][1];+          record.steps.push(step);+          if (sameNode(x, y, to.x, to.y)) break;+        }+        record.end = [x, y];+      };++      layTranslation();+      let reached = sameNode(record.end[0], record.end[1], to.x, to.y);+      if (!reached && record.steps.length < Math.max(6, Math.floor(directions.length * 0.6))) {+        clearSteps();+        const alt = route(+          random,+          { x: from.x, y: from.y, direction: from.escape },+          { x: to.x, y: to.y, arrivals: ARRIVALS[to.escape] },+          {},+          true,+        );+        if (alt) {+          let x = from.x;+          let y = from.y;+          for (const step of alt) {+            router.claimStep(record, x, y, step);+            x += DIR8[step][0];+            y += DIR8[step][1];+            record.steps.push(step);+          }+          record.end = [x, y];+          reached = true;+          stats.followersRerouted += 1;+        } else {+          layTranslation();+          reached = sameNode(record.end[0], record.end[1], to.x, to.y);+        }+      }++      if (record.steps.length < 4 || !sealEnd(router, record, 4)) {+        router.rollback(record);+        stats.followerFails += 1;+        continue;+      }+      from.used = true;+      if (sameNode(record.end[0], record.end[1], to.x, to.y)) {+        to.used = true;+        stats.followersFull += 1;+      } else {+        stats.followersPartial += 1;+      }+      addRecord(record);+    }+  }+}++// Tap combs: short perpendicular stubs dropping off a long run at a regular+// pitch, each ending in a via — the power-rail tap motif that punctuates the+// reference artwork.+function tapCombs(random, router, records, addRecord, config) {+  const runs = [];+  for (const record of records) {+    let x = record.start[0];+    let y = record.start[1];+    let runX = x;+    let runY = y;+    let runDir = -1;+    let runLen = 0;+    const flush = () => {+      if (runDir >= 0 && runDir % 2 === 0 && runLen >= 8) {+        runs.push({ x: runX, y: runY, dir: runDir, len: runLen });+      }+    };+    for (const step of record.steps) {+      if (step === runDir) runLen += 1;+      else {+        flush();+        runDir = step;+        runX = x;+        runY = y;+        runLen = 1;+      }+      x += DIR8[step][0];+      y += DIR8[step][1];+    }+    flush();+  }++  let combs = 0;+  for (const run of runs) {+    if (combs >= config.combs || random() >= 0.28) continue;+    const [dx, dy] = DIR8[run.dir];+    const side = random() < 0.5 ? 1 : -1;+    const px = -dy * side;+    const py = dx * side;+    const tapDir = DIR8.findIndex(([tx, ty]) => tx === px && ty === py);+    const teeth = 2 + Math.floor(random() * 3);+    // Depth 2-3: the junction via and tip via need a two-cell gap to clear+    // each other's annular rings.+    const depth = 2 + (random() < 0.35 ? 1 : 0);+    const span = teeth * 2;+    if (run.len < span + 2) continue;+    const offset = 1 + Math.floor(random() * (run.len - span - 1));+    const made = [];+    for (let tooth = 0; tooth < teeth; tooth += 1) {+      const bx = run.x + dx * (offset + tooth * 2);+      const by = run.y + dy * (offset + tooth * 2);+      // The tap starts ON the parent trace (a T junction, marked with its+      // own via) and steps outward to a tip via.+      const record = newTraceRecord(bx, by);+      if (!router.viaAllowed(record, bx, by)) continue;+      router.placeVia(record, bx, by);+      let cx = bx;+      let cy = by;+      for (let d = 0; d < depth; d += 1) {+        if (!router.stepFree(cx, cy, tapDir)) break;+        router.claimStep(record, cx, cy, tapDir);+        cx += px;+        cy += py;+        record.steps.push(tapDir);+      }+      record.end = [cx, cy];+      if (record.steps.length < 2 || !sealEnd(router, record, 2)) {+        router.rollback(record);+        continue;+      }+      made.push(record);+    }+    if (made.length >= 2) {+      for (const record of made) addRecord(record);+      combs += 1;+    } else {+      for (const record of made) router.rollback(record);+    }+  }+}++// Short bare-ended escape stubs for pins that never got a net — the reference+// is full of these little launches.+function routeStubs(random, router, formations, addRecord, config) {+  for (const formation of formations) {+    for (const pin of formation.pins) {+      if (pin.used || random() >= config.stubChance) continue;+      const record = startAtPin(router, pin);+      if (!record) continue;+      let x = pin.x;+      let y = pin.y;+      let direction = pin.escape;+      const length = 2 + Math.floor(random() * 5);+      for (let step = 0; step < length; step += 1) {+        if (step > 0 && random() < 0.2) {+          const jog = (direction + (random() < 0.5 ? 1 : 7)) % 8;+          if (router.stepFree(x, y, jog)) direction = jog;+        }+        if (!router.stepFree(x, y, direction)) break;+        router.claimStep(record, x, y, direction);+        x += DIR8[direction][0];+        y += DIR8[direction][1];+        record.steps.push(direction);+      }+      record.end = [x, y];+      if (record.steps.length < 2 || !sealEnd(router, record, 2)) {+        router.rollback(record);+        continue;+      }+      pin.used = true;+      addRecord(record);+    }+  }+}++// Translated companions for a filler walk — same idea as the net followers:+// staggered trims and via ladders at the ends, bending wherever the base+// bends. In the reference almost nothing travels alone.+function replicateWalk(random, router, base, addRecord) {+  if (random() >= 0.55 || base.steps.length < 8) return;+  let horizontalSteps = 0;+  let verticalSteps = 0;+  for (const step of base.steps) {+    if (step === 0 || step === 4) horizontalSteps += 1;+    else if (step === 2 || step === 6) verticalSteps += 1;+  }+  const horizontal = horizontalSteps >= verticalSteps;+  const spacing = random() < 0.7 ? 1 : 2;+  const side = random() < 0.5 ? 1 : -1;+  const shift = horizontal ? [0, spacing * side] : [spacing * side, 0];+  const copies = 1 + Math.floor(random() * 3);+  const trimStartUnit = random() < 0.5 ? 1 : 0;+  const trimEndUnit = random() < 0.8 ? 1 : 2;++  for (let copy = 1; copy <= copies; copy += 1) {+    const trimStart = copy * trimStartUnit;+    const trimEnd = copy * trimEndUnit;+    if (base.steps.length - trimStart - trimEnd < 5) break;+    let x = base.start[0] + shift[0] * copy;+    let y = base.start[1] + shift[1] * copy;+    for (let index = 0; index < trimStart; index += 1) {+      x += DIR8[base.steps[index]][0];+      y += DIR8[base.steps[index]][1];+    }+    if (!router.nodeFree(x, y) || !router.viaAllowed(SCRATCH_RECORD, x, y)) continue;+    const record = newTraceRecord(x, y);+    router.claimNode(record, x, y);+    router.placeVia(record, x, y); // every trace starts on a via+    for (let index = trimStart; index < base.steps.length - trimEnd; index += 1) {+      const step = base.steps[index];+      if (!router.stepFree(x, y, step)) break;+      router.claimStep(record, x, y, step);+      x += DIR8[step][0];+      y += DIR8[step][1];+      record.steps.push(step);+    }+    record.end = [x, y];+    if (record.steps.length < 5 || !sealEnd(router, record, 5)) {+      router.rollback(record);+      continue;+    }+    addRecord(record);+  }+}++// --- filler walker ----------------------------------------------------------+//+// A small amount of free-roaming texture between the routed bundles, using the+// same collision rules. Kept from the earlier engine but used sparingly.++const SCRATCH_RECORD = { ownKeys: new Set() };++function walkTrace(random, router, config, startWeights) {+  let sx = -1;+  let sy = -1;+  let bestCrowding = Infinity;+  for (let attempt = 0, found = 0; attempt < 24 && found < 3; attempt += 1) {+    const x = Math.floor(random() * router.columns);+    const y = Math.floor(random() * router.rows);+    if (!router.nodeFree(x, y) || !router.viaAllowed(SCRATCH_RECORD, x, y)) continue;+    found += 1;+    const crowding = router.crowding(x, y);+    if (crowding < bestCrowding) {+      bestCrowding = crowding;+      sx = x;+      sy = y;+    }+  }+  if (sx < 0) return null;++  const trace = newTraceRecord(sx, sy);+  router.claimNode(trace, sx, sy);+  router.placeVia(trace, sx, sy); // every trace starts on a via++  let direction = weightedChoice(random, startWeights);+  let lastCardinal = direction % 2 === 0 ? direction : (direction + (random() < 0.5 ? 1 : 7)) % 8;+  const budget = 14 + Math.floor(random() * 22);+  let x = sx;+  let y = sy;+  let sinceVia = 4;+  let turnStreak = 0; // consecutive same-side turns; capped to avoid closed loops++  while (trace.steps.length < budget) {+    const cardinal = direction % 2 === 0;+    const runLength = cardinal+      ? 3 + Math.floor(random() * random() * 9)+      : 1 + Math.floor(random() * 3);++    for (let step = 0; step < runLength && trace.steps.length < budget; step += 1) {+      if (!router.stepFree(x, y, direction)) break;+      router.claimStep(trace, x, y, direction);+      x += DIR8[direction][0];+      y += DIR8[direction][1];+      trace.steps.push(direction);+      sinceVia += 1;+      if (+        cardinal &&+        sinceVia > 4 &&+        trace.steps.length < budget - 2 &&+        random() < config.inlineViaChance &&+        router.viaAllowed(trace, x, y)+      ) {+        router.placeVia(trace, x, y);+        sinceVia = 0;+      }+    }++    if (trace.steps.length >= budget) break;++    let next;+    if (cardinal) {+      // Curling the same way repeatedly closes octagon loops; alternate.+      const side = Math.abs(turnStreak) >= 3 ? -Math.sign(turnStreak) : random() < 0.5 ? 1 : -1;+      next = (direction + (side > 0 ? 1 : 7)) % 8;+    } else {+      const optionA = (direction + 1) % 8;+      const optionB = (direction + 7) % 8;+      const preferPrevious = (optionA === lastCardinal || optionB === lastCardinal) && random() < 0.7;+      next = preferPrevious ? lastCardinal : random() < 0.5 ? optionA : optionB;+    }+    if (!router.stepFree(x, y, next)) {+      const mirrored = (direction + (8 - ((next - direction + 8) % 8))) % 8;+      if (!router.stepFree(x, y, mirrored)) break;+      next = mirrored;+    }+    const turnSide = ((next - direction + 8) % 8) === 1 ? 1 : -1;+    turnStreak = Math.sign(turnStreak) === turnSide ? turnStreak + turnSide : turnSide;+    direction = next;+    if (direction % 2 === 0) lastCardinal = direction;+  }++  trace.end = [x, y];+  if (trace.steps.length < 7 || !sealEnd(router, trace, 7)) {+    router.rollback(trace);+    return null;+  }+  return trace;+}++function toTileTrace(record, pitch) {+  const points = [[record.start[0], record.start[1]]];+  let previous = -1;+  let [x, y] = record.start;+  for (const step of record.steps) {+    x += DIR8[step][0];+    y += DIR8[step][1];+    if (step === previous) points[points.length - 1] = [x, y];+    else points.push([x, y]);+    previous = step;+  }+  const scaled = points.map(([px, py]) => [px * pitch, py * pitch]);+  let length = 0;+  for (let index = 1; index < scaled.length; index += 1) {+    length += Math.hypot(+      scaled[index][0] - scaled[index - 1][0],+      scaled[index][1] - scaled[index - 1][1],+    );+  }+  return {+    points: scaled,+    length,+    vias: record.viaCells.map(([vx, vy]) => [vx * pitch, vy * pitch]),+  };+}++export function generateCircuitTile(options = {}) {+  const config = { ...DEFAULTS, ...options };+  const random = mulberry32(hashSeed(config.seed));+  const columns = Math.round(config.tileWidth / config.pitch);+  const rows = Math.round(config.tileHeight / config.pitch);+  const router = createRouter(random, columns, rows);+  const route = createPathfinder(router, columns, rows, config);+  const startWeights = startDirectionWeights(config.horizontalBias);++  const records = [];+  let routedSteps = 0;+  const addRecord = (record) => {+    records.push(record);+    routedSteps += record.steps.length;+  };++  // 1. Terminal formations: via ladders, rows, singles.+  const formations = placeFormations(+    random,+    router,+    columns,+    rows,+    Math.round(config.formations * config.density),+    config,+  );+  for (const formation of formations) records.push(formation.record);++  // 2. Nets between formations — the flow. Two passes so pins left over from+  // the first pairing find another partner.+  const stats = {+    formations: formations.length,+    leaders: 0,+    leaderFails: 0,+    followerTries: 0,+    followersFull: 0,+    followersPartial: 0,+    followersRerouted: 0,+    followerFails: 0,+  };+  routeNets(random, router, route, formations, addRecord, stats, config);+  routeNets(random, router, route, formations, addRecord, stats, config);++  // 3. Escape stubs for leftover pins.+  routeStubs(random, router, formations, addRecord, config);++  // 4. A sprinkle of free-roaming filler until the reference ink density.+  const targetSteps = Math.round(4800 * config.density);+  let attempts = 0;+  while (routedSteps < targetSteps && attempts < 3200) {+    attempts += 1;+    const trace = walkTrace(random, router, config, startWeights);+    if (trace) {+      addRecord(trace);+      replicateWalk(random, router, trace, addRecord);+    }+  }++  // 5. Tap combs hanging off the long runs (nets and filler alike).+  if (config.combs > 0) tapCombs(random, router, [...records], addRecord, config);++  return {+    width: columns * config.pitch,+    height: rows * config.pitch,+    pitch: config.pitch,+    stats,+    traces: records.map((record) => toTileTrace(record, config.pitch)),+  };+}++export function pathData(points, offsetX = 0, offsetY = 0) {+  return points+    .map(([x, y], index) => `${index ? "L" : "M"}${(x + offsetX).toFixed(2)},${(y + offsetY).toFixed(2)}`)+    .join("");+}++// Standalone seamless tile: the geometry is drawn 3 x 3 around the viewBox and+// clipped, so traces that wrap the torus edge reappear on the opposite side.+export function tileSvg(tile, options = {}) {+  const config = { ...DEFAULTS, ...options };+  const { width, height } = tile;+  const body = [];+  for (const trace of tile.traces) {+    if (trace.points.length < 2) continue;+    body.push(+      `<path d="${pathData(trace.points)}" fill="none" stroke="${config.traceColor}" ` ++        `stroke-width="${config.traceWidth}" stroke-linecap="butt" stroke-linejoin="miter"/>`,+    );+  }+  const vias = [];+  for (const trace of tile.traces) {+    for (const [cx, cy] of trace.vias) {+      vias.push(+        `<circle cx="${cx.toFixed(2)}" cy="${cy.toFixed(2)}" r="${config.viaOuterRadius}" fill="${config.traceColor}"/>` ++          `<circle cx="${cx.toFixed(2)}" cy="${cy.toFixed(2)}" r="${config.viaInnerRadius}" fill="${config.holeColor || config.background || "#0d1117"}"/>`,+      );+    }+  }+  const uses = [];+  for (let row = -1; row <= 1; row += 1) {+    for (let column = -1; column <= 1; column += 1) {+      uses.push(`<use href="#tile" transform="translate(${column * width}, ${row * height})"/>`);+    }+  }+  const backgroundRect = config.background+    ? `<rect width="${width}" height="${height}" fill="${config.background}"/>`+    : "";+  return (+    `<?xml version="1.0" encoding="UTF-8"?>\n` ++    `<svg xmlns="${SVG_NS}" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">` ++    `<defs><g id="tile">${body.join("")}${vias.join("")}</g>` ++    `<clipPath id="edge"><rect width="${width}" height="${height}"/></clipPath></defs>` ++    `${backgroundRect}<g clip-path="url(#edge)">${uses.join("")}</g></svg>`+  );+}++// --- signal routing ---------------------------------------------------------+//+// Signals ride the real centerlines. Each trace is resampled to ~3.5 unit+// spacing and indexed in a spatial hash; when a pulse reaches the end of a+// trace it looks for a nearby, direction-aligned point on another trace (in+// this tile or a neighbor) and hops there, exactly like the reference site.++const SIGNAL_SAMPLE = 3.5;+const HOP_RADIUS = 3.5;+const HASH_CELL = 8;+const signalIndexCache = new WeakMap();++function ensureSignalIndex(tile) {+  let index = signalIndexCache.get(tile);+  if (index) return index;+  const dense = tile.traces.map((trace) => {+    if (trace.points.length < 2) return [];+    const points = [];+    for (let segment = 1; segment < trace.points.length; segment += 1) {+      const [ax, ay] = trace.points[segment - 1];+      const [bx, by] = trace.points[segment];+      const steps = Math.max(1, Math.round(Math.hypot(bx - ax, by - ay) / SIGNAL_SAMPLE));+      for (let step = segment === 1 ? 0 : 1; step <= steps; step += 1) {+        points.push([ax + ((bx - ax) * step) / steps, ay + ((by - ay) * step) / steps]);+      }+    }+    return points;+  });+  const buckets = new Map();+  dense.forEach((points, traceIndex) => {+    points.forEach((point, pointIndex) => {+      const key = `${Math.floor(point[0] / HASH_CELL)}:${Math.floor(point[1] / HASH_CELL)}`;+      let bucket = buckets.get(key);+      if (!bucket) buckets.set(key, (bucket = []));+      bucket.push([traceIndex, pointIndex]);+    });+  });+  // Trace endpoints (all mandated vias) — the launch pads for via hopping.+  const endpoints = [];+  dense.forEach((points, traceIndex) => {+    if (points.length < 6) return;+    endpoints.push({ trace: traceIndex, point: 0, direction: 1, x: points[0][0], y: points[0][1] });+    const last = points.length - 1;+    endpoints.push({ trace: traceIndex, point: last, direction: -1, x: points[last][0], y: points[last][1] });+  });+  index = { dense, buckets, endpoints };+  signalIndexCache.set(tile, index);+  return index;+}++function findHop(tile, absX, absY, dirX, dirY, visited) {+  const { dense, buckets } = ensureSignalIndex(tile);+  const baseTileX = Math.floor(absX / tile.width);+  const baseTileY = Math.floor(absY / tile.height);+  let best = null;++  for (let tileY = baseTileY - 2; tileY <= baseTileY + 2; tileY += 1) {+    for (let tileX = baseTileX - 2; tileX <= baseTileX + 2; tileX += 1) {+      const localX = absX - tileX * tile.width;+      const localY = absY - tileY * tile.height;+      const minCellX = Math.floor((localX - HOP_RADIUS) / HASH_CELL);+      const maxCellX = Math.floor((localX + HOP_RADIUS) / HASH_CELL);+      const minCellY = Math.floor((localY - HOP_RADIUS) / HASH_CELL);+      const maxCellY = Math.floor((localY + HOP_RADIUS) / HASH_CELL);+      for (let cellY = minCellY; cellY <= maxCellY; cellY += 1) {+        for (let cellX = minCellX; cellX <= maxCellX; cellX += 1) {+          const bucket = buckets.get(`${cellX}:${cellY}`);+          if (!bucket) continue;+          for (const [traceIndex, pointIndex] of bucket) {+            if (visited.has(`${traceIndex}:${tileX}:${tileY}`)) continue;+            const point = dense[traceIndex][pointIndex];+            const distance = Math.hypot(point[0] - localX, point[1] - localY);+            if (distance >= HOP_RADIUS) continue;+            if (distance >= 1.2) {+              const toward =+                ((point[0] - localX) * dirX + (point[1] - localY) * dirY) / (distance || 1);+              if (toward < 0.35) continue;+            }+            for (const hopDirection of [1, -1]) {+              const ahead = pointIndex + hopDirection * 3;+              if (ahead < 0 || ahead >= dense[traceIndex].length) continue;+              const segX = dense[traceIndex][ahead][0] - point[0];+              const segY = dense[traceIndex][ahead][1] - point[1];+              const segLength = Math.hypot(segX, segY) || 1;+              const score = (segX * dirX + segY * dirY) / segLength - distance * 0.05;+              if (!best || score > best.score) {+                best = {+                  trace: traceIndex,+                  tileX,+                  tileY,+                  point: pointIndex,+                  direction: hopDirection,+                  score,+                };+              }+            }+          }+        }+      }+    }+  }+  return best && best.score > 0.1 ? best : null;+}++function buildSignalPath(tile, start, budget, minLength) {+  const { dense } = ensureSignalIndex(tile);+  const visited = new Set();+  const points = [];+  let travelled = 0;+  let cursor = { ...start };++  // The walk always finishes at a trace end (a mandated via) — the budget+  // only limits further mid-route hops, never cuts a pulse off mid-trace.+  for (;;) {+    const tracePoints = dense[cursor.trace];+    visited.add(`${cursor.trace}:${cursor.tileX}:${cursor.tileY}`);+    const offsetX = cursor.tileX * tile.width;+    const offsetY = cursor.tileY * tile.height;+    for (+      let index = cursor.point;+      index >= 0 && index < tracePoints.length;+      index += cursor.direction+    ) {+      const x = tracePoints[index][0] + offsetX;+      const y = tracePoints[index][1] + offsetY;+      if (points.length) {+        const last = points[points.length - 1];+        travelled += Math.hypot(x - last[0], y - last[1]);+      }+      points.push([x, y]);+    }+    if (points.length < 2 || travelled >= budget) break;++    const last = points[points.length - 1];+    const before = points[points.length - 2];+    const stepLength = Math.hypot(last[0] - before[0], last[1] - before[1]) || 1;+    const hop = findHop(+      tile,+      last[0],+      last[1],+      (last[0] - before[0]) / stepLength,+      (last[1] - before[1]) / stepLength,+      visited,+    );+    if (!hop) break;+    cursor = hop;+  }++  return travelled >= minLength ? { points, length: travelled } : null;+}++// --- DOM mount ---------------------------------------------------------------++export function createCircuitBackground(mount, options = {}) {+  if (!(mount instanceof Element)) throw new TypeError("mount must be a DOM element");++  const id = `circuit-${++instanceCount}`;+  let config = { ...DEFAULTS, ...options };+  let random = mulberry32(hashSeed(`${config.seed}:signals`));+  let tile = generateCircuitTile(config);+  let svg;+  let usesGroup;+  let signalGroup;+  let resizeObserver;+  let activeSignals = 0;+  let signalTimer;+  let epoch = 0; // invalidates in-flight signal timeouts across regenerations+  let tilesAcross = 1;+  let tilesDown = 1;+  let topOffset = -config.tileHeight / 2;+  let pointer = null;+  const interactionTarget = mount.parentElement || mount;+  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;++  const originalPosition = mount.style.position;+  const computedPosition = getComputedStyle(mount).position;+  if (computedPosition === "static") mount.style.position = "relative";+  mount.style.overflow = "hidden";++  function buildSvg() {+    svg?.remove();+    epoch += 1;+    activeSignals = 0;+    tile = generateCircuitTile(config);+    random = mulberry32(hashSeed(`${config.seed}:signals`));+    svg = svgEl("svg", {+      "aria-hidden": "true",+      focusable: "false",+      xmlns: SVG_NS,+    });+    svg.dataset.circuitBackground = "";+    Object.assign(svg.style, {+      position: "absolute",+      left: "0",+      top: "0",+      display: "block",+      maxWidth: "none",+      pointerEvents: "none",+      overflow: "visible",+    });++    const defs = svgEl("defs");+    const tileGroup = svgEl("g", { id: `${id}-tile` });+    const traceGroup = svgEl("g", {+      fill: "none",+      stroke: config.traceColor,+      "stroke-width": config.traceWidth,+      "stroke-linecap": "butt",+      "stroke-linejoin": "miter",+    });+    const viaGroup = svgEl("g");+    for (const trace of tile.traces) {+      if (trace.points.length >= 2) {+        traceGroup.appendChild(svgEl("path", { d: pathData(trace.points) }));+      }+      for (const [cx, cy] of trace.vias) {+        viaGroup.appendChild(+          svgEl("circle", { cx, cy, r: config.viaOuterRadius, fill: config.traceColor }),+        );+        viaGroup.appendChild(+          svgEl("circle", {+            cx,+            cy,+            r: config.viaInnerRadius,+            fill: "var(--circuit-hole, #0d1117)",+          }),+        );+      }+    }+    tileGroup.append(traceGroup, viaGroup);+    defs.appendChild(tileGroup);+    svg.appendChild(defs);++    usesGroup = svgEl("g");+    signalGroup = svgEl("g", { fill: "none" });+    svg.append(usesGroup, signalGroup);+    mount.prepend(svg);+    layout();+  }++  function layout() {+    const width = Math.max(1, mount.clientWidth);+    const height = Math.max(1, mount.clientHeight);+    const tilePixelWidth = tile.width * config.scale;+    const tilePixelHeight = tile.height * config.scale;+    tilesAcross = Math.max(1, Math.ceil(width / tilePixelWidth));+    tilesDown = Math.max(2, Math.ceil(height / tilePixelHeight) + 1);+    topOffset = -tile.height / 2;+    const viewWidth = tilesAcross * tile.width;+    const viewHeight = tilesDown * tile.height;++    svg.setAttribute("width", `${viewWidth * config.scale}`);+    svg.setAttribute("height", `${viewHeight * config.scale}`);+    svg.setAttribute("viewBox", `0 ${-topOffset} ${viewWidth} ${viewHeight}`);+    usesGroup.replaceChildren();+    // The extra ring of tiles paints the traces that wrap in from neighbors.+    for (let row = -1; row <= tilesDown; row += 1) {+      for (let column = -1; column <= tilesAcross; column += 1) {+        usesGroup.appendChild(+          svgEl("use", {+            href: `#${id}-tile`,+            transform: `translate(${column * tile.width}, ${row * tile.height})`,+          }),+        );+      }+    }+  }++  // Pointer-biased launch pad: candidates are scored by the distance from+  // the ACTUAL launch point (a trace end, not the trace middle) to the+  // cursor, plus spawnRadius worth of jitter — so spawns cluster within+  // roughly spawnRadius of the pointer without a hard cutoff.+  function pickStart() {+    const { dense } = ensureSignalIndex(tile);+    const nearPointer = pointer && performance.now() - pointer.at < 4000;+    let choice = null;+    let bestScore = Infinity;+    const tries = nearPointer ? 40 : 4;+    for (let attempt = 0; attempt < tries; attempt += 1) {+      const trace = Math.floor(random() * dense.length);+      const tileX = Math.floor(random() * tilesAcross);+      const tileY = Math.floor(random() * tilesDown);+      if (dense[trace].length < 6) continue;+      const fromStart = random() < 0.5;+      const candidate = {+        trace,+        tileX,+        tileY,+        point: fromStart ? 0 : dense[trace].length - 1,+        direction: fromStart ? 1 : -1,+      };+      if (!nearPointer) return candidate;+      const [padX, padY] = dense[trace][candidate.point];+      const score =+        Math.hypot(padX + tileX * tile.width - pointer.x, padY + tileY * tile.height - pointer.y) ++        config.spawnRadius * random();+      if (score < bestScore) {+        bestScore = score;+        choice = candidate;+      }+    }+    return choice;+  }++  // Launch pads (trace-end vias) near an ended signal, closest first, so the+  // replacement reads as the signal hopping vias onto a neighboring trace.+  function nearbyEndpointStarts(absX, absY, excludeTrace) {+    const { endpoints } = ensureSignalIndex(tile);+    const baseTileX = Math.floor(absX / tile.width);+    const baseTileY = Math.floor(absY / tile.height);+    const candidates = [];+    for (let tileY = baseTileY - 1; tileY <= baseTileY + 1; tileY += 1) {+      for (let tileX = baseTileX - 1; tileX <= baseTileX + 1; tileX += 1) {+        const localX = absX - tileX * tile.width;+        const localY = absY - tileY * tile.height;+        for (const endpoint of endpoints) {+          if (endpoint.trace === excludeTrace) continue;+          const distance = Math.hypot(endpoint.x - localX, endpoint.y - localY);+          if (distance > 2 && distance < config.hopRadius) {+            candidates.push({ endpoint, tileX, tileY, distance: distance + random() * 20 });+          }+        }+      }+    }+    candidates.sort((a, b) => a.distance - b.distance);+    return candidates.slice(0, 6).map((pick) => ({+      trace: pick.endpoint.trace,+      tileX: pick.tileX,+      tileY: pick.tileY,+      point: pick.endpoint.point,+      direction: pick.endpoint.direction,+    }));+  }++  // phase (0..1) fast-forwards the animation so boot signals are already+  // mid-journey. A chain keeps its speedFactor, color AND survival rate across+  // hops — the signal that entered blue exits blue, and a click-born chain+  // keeps decaying at burstSurvival while ambient chains use hopSurvival.+  // Occasionally (colorDrift) a leg crossfades to a new color over its whole+  // run, so palettes shift gradually, never pop.+  function launchSignal(path, phase = 0, speedFactor, chainColor, survival) {+    const factor = speedFactor ?? 0.85 + random() * 0.3;+    const surviveChance = survival ?? config.hopSurvival;+    const colorFrom = chainColor ?? weightedChoice(random, config.signalColors);+    const colorTo =+      random() < config.colorDrift ? weightedChoice(random, config.signalColors) : colorFrom;+    const d = pathData(path.points);+    const speed = config.signalSpeed * factor;+    const head = config.signalLength;+    const tail = head * 2.2;+    const length = path.length;+    const total = length + tail;+    const duration = (total / speed) * 1000;+    const group = svgEl("g");+    // Faint persistent wake along the whole route.+    const wake = svgEl("path", { d, stroke: colorFrom, "stroke-width": 2, opacity: 0 });+    // Soft glow under the head — a wide translucent stroke is far cheaper+    // than a drop-shadow filter re-rasterizing every frame.+    const glow = svgEl("path", {+      d,+      stroke: colorFrom,+      "stroke-width": 7,+      "stroke-linecap": "round",+      "stroke-opacity": 0.1,+      "stroke-dasharray": `${head} ${total}`,+      "stroke-dashoffset": head,+    });+    // Long dim comet tail behind the head.+    const comet = svgEl("path", {+      d,+      stroke: colorFrom,+      "stroke-width": 2,+      "stroke-linecap": "round",+      "stroke-opacity": 0.16,+      "stroke-dasharray": `${tail} ${total}`,+      "stroke-dashoffset": tail,+    });+    // Bright head.+    const pulse = svgEl("path", {+      d,+      stroke: colorFrom,+      "stroke-width": 2.4,+      "stroke-linecap": "round",+      "stroke-dasharray": `${head} ${total}`,+      "stroke-dashoffset": head,+      "stroke-opacity": 0.72,+    });+    group.append(wake, glow, comet, pulse);+    signalGroup.appendChild(group);+    activeSignals += 1;++    // All layers share the same leading edge; each runs from its own dash+    // length down to -(total - itsDash) so they exit together. Color rides+    // along in the same keyframes when this leg is a crossfade.+    const timing = { duration, easing: "linear", fill: "forwards" };+    const animations = [+      pulse.animate(+        [+          { strokeDashoffset: head, stroke: colorFrom },+          { strokeDashoffset: head - total, stroke: colorTo },+        ],+        timing,+      ),+      glow.animate(+        [+          { strokeDashoffset: head, stroke: colorFrom },+          { strokeDashoffset: head - total, stroke: colorTo },+        ],+        timing,+      ),+      comet.animate(+        [+          { strokeDashoffset: tail, stroke: colorFrom },+          { strokeDashoffset: tail - total, stroke: colorTo },+        ],+        timing,+      ),+      wake.animate(+        [+          { opacity: 0, stroke: colorFrom },+          { opacity: 0.06 },+          { opacity: 0.06 },+          { opacity: 0, stroke: colorTo },+        ],+        { duration, fill: "forwards" },+      ),+    ];+    const headArrival = (duration * length) / total;+    const skip = Math.min(phase, 0.95) * headArrival;+    if (skip > 0) for (const animation of animations) animation.currentTime = skip;++    // The signal is logically done when its head reaches the final via (the+    // tail after that is just residue): blip, release its population slot,+    // and — in the same beat — launch its successor from a nearby via. The+    // handoff is synchronous so motion never pauses.+    const [endX, endY] = path.points[path.points.length - 1];+    const myEpoch = epoch;+    window.setTimeout(() => {+      if (myEpoch !== epoch) return;+      activeSignals -= 1;+      const blip = svgEl("circle", {+        cx: endX,+        cy: endY,+        r: 2,+        fill: "none",+        stroke: colorTo,+        "stroke-width": 1.5,+        "stroke-opacity": 0.6,+      });+      group.appendChild(blip);+      blip.animate(+        [+          { r: 2, strokeOpacity: 0.6 },+          { r: 8, strokeOpacity: 0 },+        ],+        { duration: 500, easing: "ease-out", fill: "forwards" },+      );+      // The successor inherits the color the chain ARRIVED with.+      continueChain(endX, endY, path.startTrace, factor, colorTo, surviveChance);+    }, headArrival - skip);++    window.setTimeout(() => {+      group.remove();+    }, duration + 620 - skip);+  }++  // Every signal is mortal: on each via hop it rolls against its chain's+  // survival rate (hopSurvival for ambient chains, burstSurvival for+  // click-born ones — passed in so a chain keeps its own decay for life).+  // Survivors continue synchronously from a nearby via, inheriting speed and+  // color (arrive blue → depart blue); the rest just die with their arrival+  // blip. Replenishment is NOT done here — the ambient spawn timer refills+  // the population near the cursor, so a death simply lowers the census and+  // the next tick fountains a fresh signal in. Low survival => most signals+  // die after one leg => the fountain stays busy near the pointer.+  function continueChain(endX, endY, excludeTrace, speedFactor, chainColor, survival) {+    if (!config.signals || reducedMotion || document.hidden) return;+    if (random() >= (survival ?? config.hopSurvival)) return;+    if (activeSignals >= config.maxBurstSignals) return;+    spawnSignal(nearbyEndpointStarts(endX, endY, excludeTrace), 0, speedFactor, chainColor, survival);+  }++  // preferred: array of nearby launch pads tried in order (short legs are+  // fine for chain continuations); falls back to random picks elsewhere.+  function spawnSignal(preferred, phase = 0, speedFactor, chainColor, survival) {+    if (!config.signals || reducedMotion) return false;+    if (activeSignals >= config.maxBurstSignals) return false;+    const preferredCount = preferred ? preferred.length : 0;+    const candidates = preferred ? [...preferred] : [];+    for (let attempt = 0; attempt < 8; attempt += 1) {+      const start = pickStart();+      if (start) candidates.push(start);+    }+    for (let index = 0; index < candidates.length; index += 1) {+      const minLength = index < preferredCount ? 60 : 140;+      const path = buildSignalPath(tile, candidates[index], 450 + random() * 900, minLength);+      if (path) {+        path.startTrace = candidates[index].trace;+        launchSignal(path, phase, speedFactor, chainColor, survival);+        return true;+      }+    }+    return false;+  }++  // Fill the population up to maxSignals with fast-forwarded signals, so a+  // fresh board (or a tab returning to view after its chains drained) is+  // already mid-journey instead of visibly cold-starting.+  function prewarm() {+    if (!config.signals || reducedMotion || document.hidden) return;+    let guard = 0;+    while (activeSignals < config.maxSignals && guard < config.maxSignals + 2) {+      guard += 1;+      if (!spawnSignal(null, random() * 0.85)) break;+    }+  }++  // The passive fountain, like the reference site: a steady cadence of new+  // signals near the cursor (pickStart biases toward the pointer when it moved+  // recently, spreads across the board when idle), refilling toward+  // maxSignals. Because signals are mortal, the census keeps dipping below the+  // target and the timer keeps topping it up — so fresh signals keep appearing+  // at the pointer. The balance the user wants is fast spawn + short life:+  // a small signalInterval with a low hopSurvival gives a lively cursor+  // fountain; long life lets the population sit at the cap and spawn rarely.+  function startSignals() {+    window.clearInterval(signalTimer);+    if (!config.signals || reducedMotion) return;+    prewarm();+    signalTimer = window.setInterval(() => {+      if (!document.hidden && activeSignals < config.maxSignals) spawnSignal(null);+    }, config.signalInterval);+  }++  function burstAt(x, y) {+    const { dense } = ensureSignalIndex(tile);+    const candidates = [];+    const baseTileX = Math.floor(x / tile.width);+    const baseTileY = Math.floor(y / tile.height);+    for (let tileY = baseTileY - 1; tileY <= baseTileY + 1; tileY += 1) {+      for (let tileX = baseTileX - 1; tileX <= baseTileX + 1; tileX += 1) {+        if (tileX < -1 || tileX > tilesAcross || tileY < -1 || tileY > tilesDown) continue;+        const localX = x - tileX * tile.width;+        const localY = y - tileY * tile.height;+        for (let trace = 0; trace < dense.length; trace += 1) {+          let bestPoint = -1;+          let bestDistance = 40;+          for (let point = 0; point < dense[trace].length; point += 1) {+            const distance = Math.hypot(dense[trace][point][0] - localX, dense[trace][point][1] - localY);+            if (distance < bestDistance) {+              bestDistance = distance;+              bestPoint = point;+            }+          }+          if (bestPoint >= 0) candidates.push({ trace, tileX, tileY, point: bestPoint, distance: bestDistance });+        }+      }+    }+    candidates.sort((a, b) => a.distance - b.distance);+    let spawned = 0;+    for (const candidate of candidates.slice(0, Math.ceil(config.burstBoost / 2) + 1)) {+      for (const direction of [1, -1]) {+        if (activeSignals >= config.maxBurstSignals || spawned >= config.burstBoost) return;+        const path = buildSignalPath(+          tile,+          { trace: candidate.trace, tileX: candidate.tileX, tileY: candidate.tileY, point: candidate.point, direction },+          260 + random() * 380,+          50,+        );+        if (path) {+          path.startTrace = candidate.trace;+          // Clicked chains decay at burstSurvival, independent of the ambient+          // hopSurvival, so a click can ripple long even when passive life is short.+          launchSignal(path, 0, undefined, undefined, config.burstSurvival);+          spawned += 1;+        }+      }+    }+  }++  function onPointerMove(event) {+    const rect = mount.getBoundingClientRect();+    pointer = {+      x: (event.clientX - rect.left) / config.scale,+      y: (event.clientY - rect.top) / config.scale - topOffset,+      at: performance.now(),+    };+  }++  function onClick(event) {+    if (!config.interactive || !config.signals || reducedMotion) return;+    const rect = mount.getBoundingClientRect();+    burstAt(+      (event.clientX - rect.left) / config.scale,+      (event.clientY - rect.top) / config.scale - topOffset,+    );+  }++  function onVisibility() {+    if (!document.hidden) prewarm();+  }++  function regenerate(nextOptions = {}) {+    config = { ...config, ...nextOptions };+    buildSvg();+    startSignals();+    return api;+  }++  function exportSvg() {+    const clone = svg.cloneNode(true);+    clone.querySelectorAll("g[fill='none'] > g").forEach((node) => node.remove());+    clone.removeAttribute("style");+    clone.setAttribute("width", String(tilesAcross * tile.width * config.scale));+    clone.setAttribute("height", String(tilesDown * tile.height * config.scale));+    return `<?xml version="1.0" encoding="UTF-8"?>\n${clone.outerHTML}`;+  }++  function exportTileSvg(tileOptions = {}) {+    return tileSvg(tile, { ...config, ...tileOptions });+  }++  function destroy() {+    epoch += 1;+    window.clearInterval(signalTimer);+    document.removeEventListener("visibilitychange", onVisibility);+    resizeObserver?.disconnect();+    interactionTarget.removeEventListener("pointermove", onPointerMove);+    interactionTarget.removeEventListener("click", onClick);+    svg?.remove();+    if (computedPosition === "static") mount.style.position = originalPosition;+  }++  const api = {+    regenerate,+    spawnSignal,+    exportSvg,+    exportTileSvg,+    destroy,+    get tile() {+      return tile;+    },+  };+  buildSvg();+  resizeObserver = new ResizeObserver(layout);+  resizeObserver.observe(mount);+  if (config.interactive) {+    interactionTarget.addEventListener("pointermove", onPointerMove, { passive: true });+    interactionTarget.addEventListener("click", onClick);+  }+  // Hidden tabs let the census drain (the timer no-ops); refill on return.+  document.addEventListener("visibilitychange", onVisibility);+  startSignals();+  return api;+}++// Named animation presets. Each is a partial config merged over the current+// settings; only signal/animation knobs are touched so generation geometry is+// left alone. `signals: false` gives a fully static board.+const ANIMATION_PRESETS = Object.freeze({+  "None": { signals: false },+  "Fireflies": {+    // A few long-lived signals drifting/bouncing across the whole screen.+    signals: true,+    signalInterval: 850,+    maxSignals: 5,+    spawnRadius: 200,+    hopSurvival: 0.92,+    burstSurvival: 0.9,+    colorDrift: 0.12,+  },+  "Swarm": {+    // Same persistent drift, but a dense population.+    signals: true,+    signalInterval: 360,+    maxSignals: 18,+    spawnRadius: 200,+    hopSurvival: 0.9,+    burstSurvival: 0.9,+    colorDrift: 0.18,+  },+  "Cursor sparks": {+    // Really short life + very fast spawning tight around the cursor.+    signals: true,+    signalInterval: 90,+    maxSignals: 14,+    spawnRadius: 35,+    hopSurvival: 0.1,+    burstSurvival: 0.6,+    colorDrift: 0.15,+  },+  "Slow streams": {+    // Longer-lived flowing chains at a relaxed cadence.+    signals: true,+    signalInterval: 560,+    maxSignals: 8,+    spawnRadius: 130,+    hopSurvival: 0.72,+    burstSurvival: 0.82,+    colorDrift: 0.2,+  },+});++export { DEFAULTS, ANIMATION_PRESETS };
src/generate-tile.mjsadded+67
@@ -0,0 +1,67 @@+#!/usr/bin/env node+// Generate a seamless circuit-pattern tile SVG from the command line.+//+//   node generate-tile.mjs --seed nitrogen --density 1 --out tile.svg+//   node generate-tile.mjs --seed foo --transparent > tile.svg+//+// The output tiles seamlessly in both axes (the router runs on a torus), so+// it can be used directly as a repeating CSS background-image.++import { writeFileSync } from "node:fs";+import { generateCircuitTile, tileSvg } from "./circuit-background.js";++const args = process.argv.slice(2);+const flag = (name) => {+  const index = args.indexOf(`--${name}`);+  return index >= 0 ? args[index + 1] : undefined;+};++// Tunable generation parameters, exposed as kebab-case flags+// (e.g. --turn-penalty 0.8 --bus-width 1.4 --combs 0).+const TUNABLES = [+  "density",+  "formations",+  "busWidth",+  "turnPenalty",+  "weave",+  "cramp",+  "horizontalBias",+  "netReach",+  "jogBias",+  "stubChance",+  "inlineViaChance",+  "combs",+];+const kebab = (key) => key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);++if (args.includes("--help") || args.includes("-h")) {+  console.log(+    "Usage: node generate-tile.mjs [--seed <string>] [--background <css-color>] " ++      "[--transparent] [--out <file>]\n\nTuning flags (numbers):\n" ++      TUNABLES.map((key) => `  --${kebab(key)}`).join("\n"),+  );+  process.exit(0);+}++const seed = flag("seed") ?? "nitrogen";+const background = args.includes("--transparent") ? "" : flag("background") ?? "#0d1117";+const out = flag("out");++const options = { seed };+for (const key of TUNABLES) {+  const value = flag(kebab(key));+  if (value !== undefined) options[key] = Number(value);+}++const tile = generateCircuitTile(options);+const svg = tileSvg(tile, { background, holeColor: flag("background") ?? "#0d1117" });++if (out) {+  writeFileSync(out, svg);+  const vias = tile.traces.reduce((total, trace) => total + trace.vias.length, 0);+  console.error(`OK: wrote ${out} (seed=${seed} traces=${tile.traces.length} vias=${vias})`);+  console.error("Hint: the tile repeats seamlessly; use it as background-image with background-repeat: repeat.");+  console.error("Hint: run with --help to see the tuning flags (bus width, turn penalty, weave, ...).");+} else {+  process.stdout.write(svg);+}
src/index-classic.htmladded+433
@@ -0,0 +1,433 @@+<!doctype html>+<html lang="en">+  <head>+    <meta charset="UTF-8" />+    <meta name="viewport" content="width=device-width, initial-scale=1" />+    <title>Procedural Circuit Background</title>+    <style>+      :root {+        color-scheme: dark;+        --bg: #0d1117;+        --surface: #161b22;+        --border: #30363d;+        --text: #e6edf3;+        --muted: #8b949e;+        --accent: #00b8b0;+        --accent-bright: #00e6dc;+      }++      * { box-sizing: border-box; }+      html, body { min-height: 100%; margin: 0; }+      body {+        background: var(--bg);+        color: var(--text);+        font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;+      }++      .hero {+        --circuit-hole: var(--bg);+        position: relative;+        min-height: 100svh;+        overflow: hidden;+        display: grid;+        place-items: center;+        isolation: isolate;+      }++      .circuit-layer {+        position: absolute;+        inset: 0;+        z-index: -2;+      }++      .glow {+        position: absolute;+        inset: 0;+        z-index: -1;+        pointer-events: none;+        background:+          radial-gradient(circle at 52% 44%, rgba(0, 230, 220, .06), transparent 34%),+          linear-gradient(to top, var(--bg) 0, transparent 12rem);+      }++      main {+        width: min(760px, calc(100% - 40px));+        text-align: center;+        padding: 80px 0 180px;+      }++      .eyebrow {+        margin: 0 0 20px;+        color: var(--accent-bright);+        font: 600 12px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;+        letter-spacing: .18em;+        text-transform: uppercase;+      }++      h1 {+        margin: 0;+        font-size: clamp(2.6rem, 8vw, 5.6rem);+        line-height: .9;+        letter-spacing: -.055em;+        color: transparent;+        background: linear-gradient(110deg, #e9ffff, #7bc9c9 62%, var(--accent-bright));+        background-clip: text;+        filter: drop-shadow(0 0 .4em rgba(255,255,255,.18));+      }++      .lede {+        max-width: 570px;+        margin: 28px auto 0;+        color: #bac6d3;+        font-size: clamp(1rem, 2vw, 1.25rem);+        line-height: 1.6;+      }++      .hint {+        margin-top: 24px;+        color: var(--muted);+        font: 500 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;+      }++      .controls {+        position: fixed;+        left: 50%;+        bottom: 12px;+        z-index: 5;+        width: min(940px, calc(100% - 24px));+        transform: translateX(-50%);+        display: grid;+        grid-template-columns: minmax(104px, .6fr) minmax(0, 3fr) auto auto auto;+        align-items: end;+        gap: 8px;+        padding: 10px 12px;+        border: 1px solid rgba(48, 54, 61, .9);+        border-radius: 12px;+        background: rgba(22, 27, 34, .88);+        box-shadow: 0 18px 70px rgba(0,0,0,.38);+        backdrop-filter: blur(18px);+      }++      label { display: grid; gap: 5px; color: var(--muted); font-size: 12px; }+      input[type="text"] {+        width: 100%;+        height: 32px;+        padding: 0 10px;+        color: var(--text);+        border: 1px solid var(--border);+        border-radius: 7px;+        outline: none;+        background: #0d1117;+        font: 500 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;+      }+      input[type="text"]:focus { border-color: var(--accent); }+      input[type="range"] { width: 100%; accent-color: var(--accent); }++      button {+        height: 32px;+        padding: 0 12px;+        color: var(--text);+        border: 1px solid var(--border);+        border-radius: 7px;+        background: #21262d;+        font: 700 13px system-ui, sans-serif;+        cursor: pointer;+      }+      button:hover { border-color: #59636e; background: #282e36; }+      button.primary { color: #062d2b; border-color: var(--accent); background: var(--accent); }+      button.primary:hover { background: var(--accent-bright); }++      /* Sits in the top row where Density + Live signals used to be, and is+         labelled like the other fields ("Seed" above its input). */+      .presets-field { display: grid; gap: 5px; min-width: 0; }+      .presets-label { color: var(--muted); font-size: 12px; }+      .presets {+        display: flex;+        flex-wrap: wrap;+        align-items: center;+        gap: 5px;+      }+      .presets button {+        height: 32px;+        padding: 0 9px;+        font: 600 12px system-ui, sans-serif;+      }+      .presets button.active {+        color: #062d2b;+        border-color: var(--accent);+        background: var(--accent);+      }++      .tuning {+        grid-column: 1 / -1;+        border-top: 1px solid rgba(48, 54, 61, .7);+        padding-top: 8px;+      }+      .tuning summary {+        cursor: pointer;+        color: var(--muted);+        font: 600 12px system-ui, sans-serif;+        user-select: none;+      }+      .tuning summary:hover { color: var(--text); }+      .tuning .knobs {+        display: grid;+        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));+        gap: 7px 14px;+        padding-top: 9px;+      }+      .knob span {+        display: flex;+        justify-content: space-between;+        gap: 8px;+      }+      .knob output { color: var(--accent-bright); font-variant-numeric: tabular-nums; }+      .tuning .reset-row { margin-top: 9px; display: flex; justify-content: flex-end; }+      .tuning button { height: 27px; font-size: 12px; }++      @media (max-width: 760px) {+        .controls { grid-template-columns: 1fr 1fr; max-height: 60svh; overflow-y: auto; }+        /* Direct children only — preset chips and panel resets keep their size. */+        .controls > button { width: 100%; }+        .presets-field { grid-column: 1 / -1; }+        main { padding-bottom: 270px; }+      }+    </style>+  </head>+  <body>+    <section class="hero" id="hero">+      <div class="circuit-layer" id="circuit"></div>+      <div class="glow" aria-hidden="true"></div>+      <main>+        <p class="eyebrow">Seeded SVG generator · occupancy-grid engine</p>+        <h1>Live circuitry</h1>+        <p class="lede">+          Procedural generation of Nitrogen's trace pattern: collision-free routed traces,+          45° chamfers, vias, and glowing signals that hop across the generated topology.+        </p>+        <p class="hint">Move the pointer to attract signals · click anywhere for a burst</p>+      </main>+    </section>++    <form class="controls" id="controls">+      <label>+        Seed+        <input id="seed" type="text" value="nitrogen" autocomplete="off" spellcheck="false" />+      </label>+      <div class="presets-field">+        <span class="presets-label">Animation</span>+        <div class="presets" id="presets"></div>+      </div>+      <button class="primary" type="submit">Generate</button>+      <button id="download" type="button">Download SVG</button>+      <button id="downloadTile" type="button">Download Tile</button>++      <details class="tuning">+        <summary>Generation tuning</summary>+        <div class="knobs">+          <label class="knob">+            <span>Density <output>1.05</output></span>+            <input type="range" data-key="density" min="0.35" max="1.45" step="0.05" value="1.05" />+          </label>+          <label class="knob">+            <span>Formations <output>96</output></span>+            <input type="range" data-key="formations" min="24" max="96" step="4" value="96" />+          </label>+          <label class="knob">+            <span>Bus width <output>1</output></span>+            <input type="range" data-key="busWidth" min="0.5" max="2" step="0.1" value="1" />+          </label>+          <label class="knob">+            <span>Turn penalty <output>0.55</output></span>+            <input type="range" data-key="turnPenalty" min="0.1" max="1.2" step="0.05" value="0.55" />+          </label>+          <label class="knob">+            <span>Weave spacing <output>0.22</output></span>+            <input type="range" data-key="weave" min="0" max="0.45" step="0.01" value="0.22" />+          </label>+          <label class="knob">+            <span>Cramp aversion <output>0.4</output></span>+            <input type="range" data-key="cramp" min="0" max="0.8" step="0.05" value="0.4" />+          </label>+          <label class="knob">+            <span>Horizontal bias <output>0.6</output></span>+            <input type="range" data-key="horizontalBias" min="0.35" max="0.9" step="0.05" value="0.6" />+          </label>+          <label class="knob">+            <span>Net reach <output>42</output></span>+            <input type="range" data-key="netReach" min="16" max="60" step="2" value="42" />+          </label>+          <label class="knob">+            <span>Jog bias <output>5</output></span>+            <input type="range" data-key="jogBias" min="0" max="10" step="1" value="5" />+          </label>+          <label class="knob">+            <span>Stub chance <output>0</output></span>+            <input type="range" data-key="stubChance" min="0" max="1" step="0.05" value="0" />+          </label>+          <label class="knob">+            <span>Inline vias <output>0.03</output></span>+            <input type="range" data-key="inlineViaChance" min="0" max="0.12" step="0.005" value="0.03" />+          </label>+          <label class="knob">+            <span>Tap combs <output>8</output></span>+            <input type="range" data-key="combs" min="0" max="16" step="1" value="8" />+          </label>+        </div>+        <div class="reset-row">+          <button class="reset-tuning" type="button">Reset generation</button>+        </div>+      </details>++      <details class="tuning" data-panel="animation">+        <summary>Animation tuning</summary>+        <div class="knobs">+          <label class="knob">+            <span>Signal speed <output>420</output></span>+            <input type="range" data-key="signalSpeed" min="100" max="1200" step="20" value="420" />+          </label>+          <label class="knob">+            <span>Spawn rate (ms) <output>240</output></span>+            <input type="range" data-key="signalInterval" min="80" max="900" step="20" value="240" />+          </label>+          <label class="knob">+            <span>Cursor range <output>110</output></span>+            <input type="range" data-key="spawnRadius" min="25" max="200" step="5" value="110" />+          </label>+          <label class="knob">+            <span>Max signals <output>12</output></span>+            <input type="range" data-key="maxSignals" min="2" max="20" step="1" value="12" />+          </label>+          <label class="knob">+            <span>Passive survival <output>0.4</output></span>+            <input type="range" data-key="hopSurvival" min="0" max="0.95" step="0.05" value="0.4" />+          </label>+          <label class="knob">+            <span>Click survival <output>0.72</output></span>+            <input type="range" data-key="burstSurvival" min="0" max="0.95" step="0.05" value="0.72" />+          </label>+          <label class="knob">+            <span>Click boost <output>7</output></span>+            <input type="range" data-key="burstBoost" min="0" max="11" step="1" value="7" />+          </label>+        </div>+        <div class="reset-row">+          <button class="reset-tuning" type="button">Reset animation</button>+        </div>+      </details>+    </form>++    <script type="module">+      import { createCircuitBackground, ANIMATION_PRESETS } from "./circuit-background.js";++      const mount = document.querySelector("#circuit");+      const controls = document.querySelector("#controls");+      const seed = document.querySelector("#seed");+      const download = document.querySelector("#download");+      const downloadTile = document.querySelector("#downloadTile");+      const presetBar = document.querySelector("#presets");++      // Animation on/off now lives entirely in the presets ("None" = off);+      // the page opens on None, so the board starts static.+      let signalsOn = false;++      const circuit = createCircuitBackground(mount, { seed: seed.value, signals: signalsOn });++      // Every slider (density included) is a data-key knob inside a tuning+      // panel, so `tuning()` collects them all straight into the config.+      const knobs = [...document.querySelectorAll(".tuning input[type=range]")];+      const knobDefaults = new Map(knobs.map((knob) => [knob, knob.value]));+      const knobByKey = new Map(knobs.map((knob) => [knob.dataset.key, knob]));+      const tuning = () =>+        Object.fromEntries(knobs.map((knob) => [knob.dataset.key, Number(knob.value)]));+      const settings = () => ({+        seed: seed.value.trim() || "nitrogen",+        signals: signalsOn,+        ...tuning(),+      });++      // --- animation presets --------------------------------------------------+      const presetButtons = new Map();+      const clearActivePreset = () => {+        for (const button of presetButtons.values()) button.classList.remove("active");+      };+      const syncKnob = (knob) => {+        knob.parentElement.querySelector("output").value = knob.value;+      };+      const applyPreset = (name) => {+        const preset = ANIMATION_PRESETS[name];+        for (const [key, value] of Object.entries(preset)) {+          if (key === "signals") { signalsOn = value; continue; }+          const knob = knobByKey.get(key);+          if (knob) { knob.value = value; syncKnob(knob); }+        }+        clearActivePreset();+        presetButtons.get(name).classList.add("active");+        circuit.regenerate(settings());+      };+      for (const name of Object.keys(ANIMATION_PRESETS)) {+        const button = document.createElement("button");+        button.type = "button";+        button.textContent = name;+        button.addEventListener("click", () => applyPreset(name));+        presetBar.appendChild(button);+        presetButtons.set(name, button);+      }+      // Reflect the initial "None" state in the preset bar.+      presetButtons.get("None").classList.add("active");++      // Only animation knobs invalidate the highlighted preset — retuning+      // generation (density, formations, ...) leaves the animation untouched.+      const isAnimationPanel = (node) => node.closest("details")?.dataset.panel === "animation";++      for (const knob of knobs) {+        const out = knob.parentElement.querySelector("output");+        const show = () => { out.value = knob.value; };+        show();+        knob.addEventListener("input", show);+        knob.addEventListener("change", () => {+          if (isAnimationPanel(knob)) clearActivePreset();+          circuit.regenerate(settings());+        });+      }++      // Each panel's reset button restores only that panel's own knobs.+      for (const resetButton of document.querySelectorAll(".reset-tuning")) {+        resetButton.addEventListener("click", () => {+          const panelKnobs = resetButton.closest("details").querySelectorAll("input[type=range]");+          for (const knob of panelKnobs) {+            knob.value = knobDefaults.get(knob);+            knob.parentElement.querySelector("output").value = knob.value;+          }+          if (isAnimationPanel(resetButton)) clearActivePreset();+          circuit.regenerate(settings());+        });+      }++      controls.addEventListener("submit", (event) => {+        event.preventDefault();+        circuit.regenerate(settings());+      });++      function saveBlob(text, name) {+        const blob = new Blob([text], { type: "image/svg+xml" });+        const url = URL.createObjectURL(blob);+        const link = document.createElement("a");+        link.href = url;+        link.download = name;+        link.click();+        URL.revokeObjectURL(url);+      }++      const slug = () => (seed.value || "seed").replace(/[^a-z0-9_-]+/gi, "-");++      download.addEventListener("click", () => {+        saveBlob(circuit.exportSvg(), `circuit-${slug()}.svg`);+      });++      downloadTile.addEventListener("click", () => {+        saveBlob(circuit.exportTileSvg(), `circuit-tile-${slug()}.svg`);+      });+    </script>+  </body>+</html>
src/index.htmladded+570
@@ -0,0 +1,570 @@+<!doctype html>+<html lang="en">+  <head>+    <meta charset="UTF-8" />+    <meta name="viewport" content="width=device-width, initial-scale=1" />+    <title>Procedural Circuit Background</title>+    <style>+      :root {+        color-scheme: dark;+        --bg: #0d1117;+        --surface: #161b22;+        --border: #30363d;+        --text: #e6edf3;+        --muted: #8b949e;+        --accent: #00b8b0;+        --accent-bright: #00e6dc;+      }++      * { box-sizing: border-box; }+      html, body { min-height: 100%; margin: 0; }+      body {+        background: var(--bg);+        color: var(--text);+        font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;+      }++      .hero {+        --circuit-hole: var(--bg);+        position: relative;+        min-height: 100svh;+        overflow: hidden;+        display: grid;+        place-items: center;+        isolation: isolate;+      }++      .circuit-layer {+        position: absolute;+        inset: 0;+        z-index: 0;+      }++      .glow {+        --atmosphere-strength: 1;+        position: absolute;+        inset: 0;+        z-index: 1;+        pointer-events: none;+        background:+          radial-gradient(ellipse 46% 34% at 50% 45%, rgba(13, 17, 23, .64), rgba(13, 17, 23, .34) 48%, transparent 100%),+          linear-gradient(to top, var(--bg) 0, transparent 13rem);+      }++      .glow::before {+        content: "";+        position: absolute;+        inset: -12%;+        background:+          radial-gradient(ellipse 28% 38% at 22% 48%, rgba(0, 230, 220, .06), transparent 72%),+          radial-gradient(ellipse 34% 42% at 58% 46%, rgba(57, 184, 255, .045), transparent 72%),+          radial-gradient(ellipse 30% 38% at 84% 52%, rgba(140, 107, 247, .045), transparent 72%);+        mix-blend-mode: screen;+        opacity: calc(var(--atmosphere-strength) * .72);+        filter: blur(34px);+      }++      .hero[data-motion="on"][data-animation-system="conductor"] .glow {+        background:+          radial-gradient(circle at 52% 44%, rgba(0, 230, 220, .06), transparent 34%),+          linear-gradient(to top, var(--bg) 0, transparent 12rem);+        animation: atmosphere-breathe 8s ease-in-out infinite alternate;+        will-change: opacity, filter;+      }++      .hero[data-motion="on"][data-animation-system="conductor"] .glow::before {+        inset: -18%;+        background:+          radial-gradient(circle at 28% 38%, rgba(0, 230, 220, .075), transparent 20%),+          radial-gradient(circle at 72% 58%, rgba(100, 171, 255, .055), transparent 24%),+          radial-gradient(circle at 52% 82%, rgba(197, 179, 255, .04), transparent 18%);+        opacity: 1;+        filter: none;+        animation: atmosphere-drift 19s cubic-bezier(.45,.05,.55,.95) infinite alternate;+        will-change: transform;+      }++      @keyframes atmosphere-breathe {+        from { opacity: var(--atmosphere-low, .66); filter: saturate(.82); }+        to { opacity: var(--atmosphere-high, 1); filter: saturate(1.18); }+      }++      @keyframes atmosphere-drift {+        0% { transform: translate3d(-3%, -2%, 0) scale(1); }+        45% { transform: translate3d(4%, 1%, 0) scale(1.08); }+        100% { transform: translate3d(-1%, 5%, 0) scale(1.03); }+      }++      main {+        position: relative;+        z-index: 2;+        width: min(760px, calc(100% - 40px));+        text-align: center;+        padding: 80px 0 180px;+      }++      .hero-copy { position: relative; }++      .eyebrow {+        margin: 0 0 20px;+        color: var(--accent-bright);+        font: 600 12px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;+        letter-spacing: .18em;+        text-transform: uppercase;+      }++      h1 {+        margin: 0;+        font-size: clamp(3.2rem, 10vw, 7.5rem);+        line-height: .9;+        letter-spacing: -.055em;+        color: transparent;+        background: linear-gradient(100deg, #e6edf3 4%, #00e6dc 38%, #39b8ff 66%, #8c6bf7 96%);+        background-clip: text;+        filter: drop-shadow(0 0 .4em rgba(255,255,255,.18));+      }++      .lede {+        max-width: 570px;+        margin: 28px auto 0;+        color: #bac6d3;+        font-size: clamp(1rem, 2vw, 1.25rem);+        line-height: 1.6;+      }++      .hint {+        margin-top: 24px;+        color: var(--muted);+        font: 500 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;+      }++      .controls {+        position: fixed;+        left: 50%;+        bottom: 12px;+        z-index: 5;+        width: min(1180px, calc(100% - 24px));+        transform: translateX(-50%);+        display: grid;+        grid-template-columns: repeat(12, minmax(0, 1fr));+        align-items: end;+        gap: 8px;+        padding: 10px;+        border: 1px solid rgba(48, 54, 61, .9);+        border-radius: 12px;+        background: rgba(22, 27, 34, .88);+        box-shadow: 0 18px 70px rgba(0,0,0,.38);+        backdrop-filter: blur(18px);+      }++      label { min-width: 0; display: grid; gap: 4px; color: var(--muted); font-size: 10px; }+      label > span { display: flex; justify-content: space-between; gap: 3px; white-space: nowrap; }+      input[type="text"] {+        width: 100%;+        height: 32px;+        padding: 0 8px;+        color: var(--text);+        border: 1px solid var(--border);+        border-radius: 8px;+        outline: none;+        background: #0d1117;+        font: 500 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;+      }+      input[type="text"]:focus { border-color: var(--accent); }+      input[type="range"] { width: 100%; accent-color: var(--accent); }++      button {+        height: 32px;+        padding: 0 11px;+        color: var(--text);+        border: 1px solid var(--border);+        border-radius: 8px;+        background: #21262d;+        font: 700 12px system-ui, sans-serif;+        cursor: pointer;+      }+      button:hover { border-color: #59636e; background: #282e36; }+      button.primary { color: #062d2b; border-color: var(--accent); background: var(--accent); }+      button.primary:hover { background: var(--accent-bright); }++      .preset-bar {+        grid-column: 1 / -1;+        display: flex;+        align-items: center;+        gap: 6px;+        min-width: 0;+      }+      .preset-title {+        margin-right: 3px;+        color: var(--muted);+        font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;+        letter-spacing: .12em;+        text-transform: uppercase;+      }+      .preset-bar button {+        height: 28px;+        padding: 0 10px;+        color: #aab5c1;+        font-size: 11px;+      }+      .preset-bar button[aria-pressed="true"] {+        color: #082f2d;+        border-color: var(--accent);+        background: var(--accent);+      }+      .parameter-panel {+        grid-column: 1 / -1;+        display: grid;+        grid-template-columns: repeat(12, minmax(0, 1fr));+        align-items: end;+        gap: 8px;+      }+      .parameter-panel[hidden] { display: none; }+      /* A quiet text toggle, not a button: it opens a drawer, it does not pick a preset. */+      .preset-bar .panel-toggle {+        display: inline-flex;+        align-items: center;+        gap: 4px;+        width: auto;+        height: 28px;+        padding: 0 2px;+        margin-left: 4px;+        color: var(--muted);+        border: 0;+        border-radius: 0;+        background: none;+        font: 600 11px system-ui, sans-serif;+      }+      .preset-bar .panel-toggle:hover { color: var(--text); border: 0; background: none; }+      .preset-bar .panel-toggle:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; }+      .preset-bar .panel-toggle[aria-expanded="true"] { color: var(--accent-bright); }+      .panel-toggle .chevron { display: inline-block; font-size: 9px; line-height: 1; transition: transform .15s ease; }+      .panel-toggle[aria-expanded="true"] .chevron { transform: rotate(180deg); }++      .animation-bar {+        padding-top: 8px;+        border-top: 1px solid rgba(48, 54, 61, .72);+      }+      .movement-power {+        flex: 1 1 150px;+        max-width: 180px;+        margin-left: 6px;+      }+      .movement-power input { margin: 0; }+      .trace-count {+        margin-left: auto;+        color: var(--accent-bright);+        font: 600 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;+        white-space: nowrap;+      }++      @media (max-width: 1180px) {+        .controls {+          grid-template-columns: repeat(6, minmax(0, 1fr));+          max-height: calc(100svh - 24px);+          overflow-y: auto;+        }+        .parameter-panel { grid-template-columns: repeat(6, minmax(0, 1fr)); }+        .preset-bar { flex-wrap: wrap; }+      }++      @media (max-width: 760px) {+        .controls { grid-template-columns: 1fr 1fr; bottom: 12px; }+        .parameter-panel { grid-template-columns: 1fr 1fr; }+        .controls button { width: 100%; }+        .preset-title { width: 100%; }+        .trace-count { margin-left: 0; }+        main { padding-bottom: 360px; }+      }++      @media (prefers-reduced-motion: reduce) {+        .glow, .glow::before { animation: none; }+        .panel-toggle .chevron { transition: none; }+      }+    </style>+  </head>+  <body>+    <section class="hero" id="hero">+      <div class="circuit-layer" id="circuit"></div>+      <div class="glow" aria-hidden="true"></div>+      <main>+        <div class="hero-copy" data-circuit-reading-zone>+          <p class="eyebrow">Seeded SVG generator</p>+          <h1>Live circuitry</h1>+          <p class="lede">+            A procedural version of the Nitrogen hero pattern: dense routed traces, 45° chamfers,+            annular vias, continuous trace packing, and three complete animation systems for the generated topology.+          </p>+          <p class="hint" id="motionHint">Static circuit · no animation layers</p>+        </div>+      </main>+    </section>++    <form class="controls" id="controls">+      <div class="preset-bar" aria-label="Generator presets">+        <span class="preset-title">Generation</span>+        <button type="button" data-preset="original" aria-pressed="true">Original match</button>+        <button type="button" data-preset="dense" aria-pressed="false">Dense</button>+        <button type="button" data-preset="diagonal" aria-pressed="false">Diagonal sweep</button>+        <button type="button" data-preset="swirl" aria-pressed="false">Broad swirl</button>+        <button type="button" data-preset="sparse" aria-pressed="false">Sparse</button>+        <button type="button" class="panel-toggle" id="parameterToggle" aria-expanded="false" aria-controls="parameterPanel">+          Parameters<span class="chevron" aria-hidden="true">▾</span>+        </button>+        <output class="trace-count" id="traceCount">235 traces</output>+      </div>+      <div class="parameter-panel" id="parameterPanel" hidden>+      <label>+        Seed+        <input id="seed" type="text" value="nitrogen" autocomplete="off" spellcheck="false" />+      </label>+      <label>+        <span>Density <output id="fillValue">74.4%</output></span>+        <input id="fill" type="range" min="0.35" max="0.76" step="0.001" value="0.744" />+      </label>+      <label>+        <span>Avg length <output id="routeLengthValue">1.14×</output></span>+        <input id="routeLength" type="range" min="0.55" max="2.25" step="0.01" value="1.14" />+      </label>+      <label>+        <span>Primary flow <output id="horizontalFlowValue">90%</output></span>+        <input id="horizontalFlow" type="range" min="0.3" max="0.98" step="0.01" value="0.9" />+      </label>+      <label>+        <span>Angle <output id="flowAngleValue">153°</output></span>+        <input id="flowAngle" type="range" min="0" max="180" step="1" value="153" />+      </label>+      <label>+        <span>Diagonal <output id="diagonalFlowValue">100%</output></span>+        <input id="diagonalFlow" type="range" min="0" max="1" step="0.01" value="1" />+      </label>+      <label>+        <span>Long flow <output id="longFlowValue">31%</output></span>+        <input id="longFlow" type="range" min="0" max="0.36" step="0.01" value="0.31" />+      </label>+      <label>+        <span>Swirl <output id="swirlValue">82%</output></span>+        <input id="swirl" type="range" min="0" max="1.4" step="0.01" value="0.82" />+      </label>+      <label>+        <span>Swirl scale <output id="swirlScaleValue">0.72×</output></span>+        <input id="swirlScale" type="range" min="0.25" max="1.4" step="0.01" value="0.72" />+      </label>+      <label>+        <span>Jog size <output id="jogLengthValue">0.42×</output></span>+        <input id="jogLength" type="range" min="0.3" max="1.8" step="0.01" value="0.42" />+      </label>+      <label>+        <span>Channel pull <output id="channelAttractionValue">150%</output></span>+        <input id="channelAttraction" type="range" min="0" max="1.5" step="0.01" value="1.5" />+      </label>+      <label>+        <span>Scale <output id="featureScaleValue">1.37×</output></span>+        <input id="featureScale" type="range" min="0.55" max="2" step="0.01" value="1.37" />+      </label>+      </div>+      <div class="preset-bar animation-bar" aria-label="Animation presets">+        <span class="preset-title">Animation</span>+        <button type="button" data-animation-preset="none" aria-pressed="true">None</button>+        <button type="button" data-animation-preset="conductor" aria-pressed="false">Conductor</button>+        <button type="button" data-animation-preset="routed" aria-pressed="false">Routed field</button>+        <button type="button" data-animation-preset="handshake" aria-pressed="false">Handshake</button>+        <label class="movement-power">+          <span>Movement power <output id="livelinessValue">0%</output></span>+          <input id="liveliness" type="range" min="0" max="1.5" step="0.01" value="0" />+        </label>+        <button class="primary" type="submit">Generate</button>+        <button id="download" type="button">Export SVG</button>+      </div>+    </form>++    <script type="module">+      import { ANIMATION_PRESETS, createCircuitBackground } from "./circuit-background-v5.js?v=3";++      const mount = document.querySelector("#circuit");+      const controls = document.querySelector("#controls");+      const generationPresetButtons = [...document.querySelectorAll("[data-preset]")];+      const animationPresetButtons = [...document.querySelectorAll("[data-animation-preset]")];+      const traceCount = document.querySelector("#traceCount");+      const hero = document.querySelector("#hero");+      const glow = document.querySelector(".glow");+      const seed = document.querySelector("#seed");+      const parameterToggle = document.querySelector("#parameterToggle");+      const parameterPanel = document.querySelector("#parameterPanel");+      const motionHint = document.querySelector("#motionHint");+      const download = document.querySelector("#download");++      const geometryParameterIds = ["fill", "routeLength", "horizontalFlow", "flowAngle", "diagonalFlow", "longFlow", "swirl", "swirlScale", "jogLength", "channelAttraction", "featureScale"];+      const animationParameterIds = ["liveliness"];+      const parameterIds = [...geometryParameterIds, ...animationParameterIds];+      const parameters = Object.fromEntries(parameterIds.map((id) => [id, document.querySelector(`#${id}`)]));+      const presets = {+        original: { fill: 0.744, routeLength: 1.14, horizontalFlow: 0.9, flowAngle: 153, diagonalFlow: 1, longFlow: 0.31, swirl: 0.82, swirlScale: 0.72, jogLength: 0.42, channelAttraction: 1.5, featureScale: 1.37 },+        dense: { fill: 0.76, routeLength: 1.29, horizontalFlow: 0.57, flowAngle: 138, diagonalFlow: 0.26, longFlow: 0.3, swirl: 0.43, swirlScale: 0.52, jogLength: 0.85, channelAttraction: 0.69, featureScale: 0.65 },+        diagonal: { fill: 0.591, routeLength: 1.51, horizontalFlow: 0.98, flowAngle: 45, diagonalFlow: 1, longFlow: 0, swirl: 0, swirlScale: 0.75, jogLength: 0.65, channelAttraction: 0.86, featureScale: 1.28 },+        swirl: { fill: 0.58, routeLength: 1.1, horizontalFlow: 0.82, flowAngle: 15, diagonalFlow: 0.72, longFlow: 0.2, swirl: 0.9, swirlScale: 0.9, jogLength: 0.75, channelAttraction: 0.8, featureScale: 1 },+        sparse: { fill: 0.35, routeLength: 1.9, horizontalFlow: 0.45, flowAngle: 0, diagonalFlow: 0.3, longFlow: 0.32, swirl: 0.08, swirlScale: 0.93, jogLength: 0.57, channelAttraction: 0.6, featureScale: 0.88 },+      };+      const animationHints = {+        none: "Static circuit · no animation layers",+        conductor: "Move to shape the current · click anywhere to activate the network",+        adom: "Move to steer the routed field · click to launch a local bloom",+        handshake: "Move to bias the next handshake · click to request a local exchange",+      };+      const formats = {+        fill: (value) => `${Number((value * 100).toFixed(1))}%`,+        routeLength: (value) => `${value.toFixed(2)}×`,+        horizontalFlow: (value) => `${Math.round(value * 100)}%`,+        flowAngle: (value) => `${Math.round(value)}°`,+        diagonalFlow: (value) => `${Math.round(value * 100)}%`,+        longFlow: (value) => `${Math.round(value * 100)}%`,+        swirl: (value) => `${Math.round(value * 100)}%`,+        swirlScale: (value) => `${value.toFixed(2)}×`,+        jogLength: (value) => `${value.toFixed(2)}×`,+        channelAttraction: (value) => `${Math.round(value * 100)}%`,+        featureScale: (value) => `${value.toFixed(2)}×`,+        liveliness: (value) => `${Math.round(value * 100)}%`,+      };++      const query = new URLSearchParams(location.search);+      const requestedAnimationPreset = query.get("animationPreset");+      const initialAnimationPreset = ANIMATION_PRESETS[requestedAnimationPreset]+        ? requestedAnimationPreset+        : "none";+      let animationOptions = { ...ANIMATION_PRESETS[initialAnimationPreset] };+      if (query.has("seed")) seed.value = query.get("seed").slice(0, 80);+      for (const id of parameterIds) {+        if (!query.has(id)) continue;+        const value = Number(query.get(id));+        if (Number.isFinite(value)) parameters[id].value = String(value);+      }+      // A deep link that tunes parameters should show them, not hide the reason the render differs.+      if (geometryParameterIds.some((id) => query.has(id))) {+        parameterToggle.setAttribute("aria-expanded", "true");+        parameterPanel.hidden = false;+      }+      if (!query.has("liveliness")) parameters.liveliness.value = String(animationOptions.liveliness);+      if (query.has("motion")) {+        animationOptions.motion = !["0", "false", "off"].includes(query.get("motion").toLowerCase());+      }++      function setActiveGenerationPreset(name) {+        for (const button of generationPresetButtons) {+          button.setAttribute("aria-pressed", String(button.dataset.preset === name));+        }+      }++      function setActiveAnimationPreset(name) {+        for (const button of animationPresetButtons) {+          button.setAttribute("aria-pressed", String(button.dataset.animationPreset === name));+        }+      }++      function applyAtmosphere(options) {+        const amount = Math.max(0, Math.min(1.25, options.atmosphereStrength));+        hero.dataset.animationSystem = options.animationSystem;+        hero.dataset.motion = options.motion ? "on" : "off";+        glow.style.setProperty("--atmosphere-strength", String(amount));+        glow.style.setProperty("--atmosphere-low", String(Math.min(1, amount * 0.66)));+        glow.style.setProperty("--atmosphere-high", String(Math.min(1, amount)));+        motionHint.textContent = animationHints[options.animationSystem] || animationHints.none;+      }++      if (geometryParameterIds.some((id) => query.has(id))) setActiveGenerationPreset(null);+      const animationIsCustom = animationParameterIds.some((id) => query.has(id)) || query.has("motion");+      setActiveAnimationPreset(animationIsCustom ? null : initialAnimationPreset);+      applyAtmosphere(animationOptions);++      function optionsFromControls() {+        const options = {+          ...animationOptions,+          seed: seed.value.trim() || "nitrogen",+          traceColor: "#1d2938",+        };+        for (const id of parameterIds) options[id] = Number(parameters[id].value);+        return options;+      }++      function updateOutput(id) {+        const value = Number(parameters[id].value);+        document.querySelector(`#${id}Value`).value = formats[id](value);+      }+      parameterIds.forEach(updateOutput);++      const circuit = createCircuitBackground(mount, optionsFromControls());+      let regenerationTimer;++      function updateTraceCount() {+        traceCount.value = `${circuit.field.traces.length} traces`;+      }+      updateTraceCount();++      function regenerate() {+        window.clearTimeout(regenerationTimer);+        circuit.regenerate(optionsFromControls());+        updateTraceCount();+      }++      for (const id of parameterIds) {+        parameters[id].addEventListener("input", () => {+          if (geometryParameterIds.includes(id)) setActiveGenerationPreset(null);+          else setActiveAnimationPreset(null);+          updateOutput(id);+          window.clearTimeout(regenerationTimer);+          regenerationTimer = window.setTimeout(regenerate, 140);+        });+      }+      for (const button of generationPresetButtons) {+        button.addEventListener("click", () => {+          const name = button.dataset.preset;+          const values = presets[name];+          for (const [id, value] of Object.entries(values)) {+            parameters[id].value = String(value);+            updateOutput(id);+          }+          setActiveGenerationPreset(name);+          regenerate();+        });+      }+      parameterToggle.addEventListener("click", () => {+        const expanded = parameterToggle.getAttribute("aria-expanded") === "true";+        parameterToggle.setAttribute("aria-expanded", String(!expanded));+        parameterPanel.hidden = expanded;+      });+      for (const button of animationPresetButtons) {+        button.addEventListener("click", () => {+          const name = button.dataset.animationPreset;+          animationOptions = { ...ANIMATION_PRESETS[name] };+          parameters.liveliness.value = String(animationOptions.liveliness);+          updateOutput("liveliness");+          applyAtmosphere(animationOptions);+          setActiveAnimationPreset(name);+          regenerate();+        });+      }+      seed.addEventListener("change", regenerate);++      controls.addEventListener("submit", (event) => {+        event.preventDefault();+        regenerate();+      });++      function saveBlob(text, name) {+        const blob = new Blob([text], { type: "image/svg+xml" });+        const url = URL.createObjectURL(blob);+        const link = document.createElement("a");+        link.href = url;+        link.download = name;+        link.click();+        URL.revokeObjectURL(url);+      }++      const slug = () => (seed.value || "seed").replace(/[^a-z0-9_-]+/gi, "-");++      download.addEventListener("click", () => {+        saveBlob(circuit.exportSvg(), `circuit-${slug()}.svg`);+      });++    </script>+  </body>+</html>
src/serve-classic.mjsadded+61
@@ -0,0 +1,61 @@+// Static server for the occupancy-grid ("classic") engine demo.+// Separate from server.js so the two demo deployments can't interfere.+//+//   node serve-classic.mjs            # port 8930+//   CLASSIC_PORT=9000 node serve-classic.mjs++import { createReadStream, statSync } from "node:fs";+import { createServer } from "node:http";+import { dirname, join } from "node:path";+import { fileURLToPath } from "node:url";++const directory = dirname(fileURLToPath(import.meta.url));+const port = Number.parseInt(process.env.CLASSIC_PORT || "8930", 10);+const files = new Map([+  ["/", ["index-classic.html", "text/html; charset=utf-8"]],+  ["/index.html", ["index-classic.html", "text/html; charset=utf-8"]],+  ["/circuit-background.js", ["circuit-background.js", "text/javascript; charset=utf-8"]],+]);++function headers(contentType) {+  return {+    "Content-Type": contentType,+    "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",+    Pragma: "no-cache",+    "Surrogate-Control": "no-store",+    "X-Content-Type-Options": "nosniff",+  };+}++const server = createServer((request, response) => {+  const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);++  if (url.pathname === "/health") {+    response.writeHead(200, headers("application/json; charset=utf-8"));+    response.end(+      JSON.stringify({ ok: true, service: "circuit-generator-classic", uptime: process.uptime() }),+    );+    return;+  }++  const match = files.get(url.pathname);+  if (!match) {+    response.writeHead(404, headers("text/plain; charset=utf-8"));+    response.end("Not found\n");+    return;+  }++  const [filename, contentType] = match;+  const path = join(directory, filename);+  const size = statSync(path).size;+  response.writeHead(200, { ...headers(contentType), "Content-Length": size });+  if (request.method === "HEAD") {+    response.end();+    return;+  }+  createReadStream(path).pipe(response);+});++server.listen(port, "0.0.0.0", () => {+  console.log(`[circuit-generator-classic] listening on http://0.0.0.0:${port}`);+});
src/serve.shadded+26
@@ -0,0 +1,26 @@+#!/bin/bash+set -u++SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"+PORT="${CIRCUIT_PORT:-8124}"+LOG_FILE="/tmp/circuit-generator-serve.log"++if curl -fsS --max-time 2 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then+  echo "[circuit-generator] already running on port ${PORT}"+  exit 0+fi++cd "$SCRIPT_DIR"+nohup /usr/bin/node server.js >>"$LOG_FILE" 2>&1 &+SERVER_PID=$!++for _ in 1 2 3 4 5; do+  if curl -fsS --max-time 2 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then+    echo "[circuit-generator] started PID ${SERVER_PID} on port ${PORT}"+    exit 0+  fi+  sleep 1+done++echo "[circuit-generator] failed to start; see ${LOG_FILE}" >&2+exit 1
src/server.jsadded+59
@@ -0,0 +1,59 @@+import { createReadStream, statSync } from "node:fs";+import { createServer } from "node:http";+import { dirname, join } from "node:path";+import { fileURLToPath } from "node:url";++const directory = dirname(fileURLToPath(import.meta.url));+const port = Number.parseInt(process.env.CIRCUIT_PORT || "8124", 10);+const files = new Map([+  ["/", ["index.html", "text/html; charset=utf-8"]],+  ["/index.html", ["index.html", "text/html; charset=utf-8"]],+  ["/circuit-animation-conductor.js", ["circuit-animation-conductor.js", "text/javascript; charset=utf-8"]],+  ["/circuit-animation-handshake.js", ["circuit-animation-handshake.js", "text/javascript; charset=utf-8"]],+  ["/circuit-background.js", ["circuit-background.js", "text/javascript; charset=utf-8"]],+  ["/circuit-background-v4.js", ["circuit-background-v4.js", "text/javascript; charset=utf-8"]],+  ["/circuit-background-v5.js", ["circuit-background-v5.js", "text/javascript; charset=utf-8"]],+  ["/circuit-background-v3.js", ["circuit-background-v3.js", "text/javascript; charset=utf-8"]],+  ["/circuit-background-v2.js", ["circuit-background-v2.js", "text/javascript; charset=utf-8"]],+]);++function headers(contentType) {+  return {+    "Content-Type": contentType,+    "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",+    Pragma: "no-cache",+    "Surrogate-Control": "no-store",+    "X-Content-Type-Options": "nosniff",+  };+}++const server = createServer((request, response) => {+  const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);++  if (url.pathname === "/health") {+    response.writeHead(200, headers("application/json; charset=utf-8"));+    response.end(JSON.stringify({ ok: true, service: "circuit-generator", uptime: process.uptime() }));+    return;+  }++  const match = files.get(url.pathname);+  if (!match) {+    response.writeHead(404, headers("text/plain; charset=utf-8"));+    response.end("Not found\n");+    return;+  }++  const [filename, contentType] = match;+  const path = join(directory, filename);+  const size = statSync(path).size;+  response.writeHead(200, { ...headers(contentType), "Content-Length": size });+  if (request.method === "HEAD") {+    response.end();+    return;+  }+  createReadStream(path).pipe(response);+});++server.listen(port, "0.0.0.0", () => {+  console.log(`[circuit-generator] listening on http://0.0.0.0:${port}`);+});
uninstall.shadded+5
@@ -0,0 +1,5 @@+#!/bin/sh+set -e++# Undo what install.sh did (remove the symlinks you created).+echo "uninstalled"