/* visuals.jsx — animated SVGs for water + circuit
   Babel-in-browser. Defines components on window. */

const { useMemo } = React;

/* ─── Shared faucet/tap for pipe outlets ───────────────────────────
   (x, y) = pipe end-wall center; baseH = pipe half-height. Draws a sealed
   end wall, a tapering spout low on the wall (water exits at the bottom,
   where the pressure is), a rimmed mouth, and a T-handle valve stem.
   `s` scales the whole fitting; the spout tip is at faucetTip(...). */
function faucetTip({ x, y, baseH, s = 1 }) {
  return { x: x + 34 * s, y: y + baseH * 0.45 };
}
function renderFaucet({ x, y, baseH, on = false, s = 1 }) {
  const tip = faucetTip({ x, y, baseH, s });
  const tx = tip.x, ty = tip.y;
  const mouthR = Math.min(11 * s, baseH * 0.45);
  const rootR = Math.min(16 * s, baseH * 0.62);
  return (
    <g>
      {/* sealed end wall */}
      <line x1={x} y1={y - baseH - 1} x2={x} y2={y + baseH + 1}
            stroke="var(--ink)" strokeWidth={Math.min(4, 2.2 + s)} strokeLinecap="round" />
      {/* spout — tapering nose */}
      <path d={`M ${x} ${ty - rootR} L ${tx - 6 * s} ${ty - mouthR - 1}
                Q ${tx} ${ty - mouthR} ${tx} ${ty - mouthR + 2}
                L ${tx} ${ty + mouthR - 2}
                Q ${tx} ${ty + mouthR} ${tx - 6 * s} ${ty + mouthR + 1}
                L ${x} ${ty + rootR} Z`}
            fill="var(--bg-deeper)" stroke="var(--ink)" strokeWidth="2.5" strokeLinejoin="round" />
      {/* mouth rim — wet when flowing */}
      <ellipse cx={tx} cy={ty} rx={3.2 * s} ry={mouthR}
               fill={on ? "var(--water-deep)" : "var(--bg-deeper)"}
               stroke="var(--ink)" strokeWidth="1.8" />
      {/* valve stem + T-handle */}
      <rect x={x + 11 * s} y={ty - mouthR - 24 * s} width={9 * s} height={19 * s}
            fill="var(--bg-deeper)" stroke="var(--ink)" strokeWidth="2" />
      <rect x={x + 2 * s} y={ty - mouthR - 33 * s} width={30 * s} height={10 * s} rx={5 * s}
            fill="var(--bg-card)" stroke="var(--ink)" strokeWidth="2" />
    </g>
  );
}

// Drift speed tracks flow: lazy when pinched, rushing when wide open.
// Deliberately dramatic — current IS the speed of the water, and the eye
// should feel it. I=0.5 → ~11s/loop crawl; I=2 → 1.3s; I=6+ → 0.16s sprint.
function flowPeriod(current) {
  if (current <= 0.001) return "9999s";
  const seconds = Math.max(0.16, Math.min(11.0, 3.8 / Math.pow(current, 1.5)));
  return seconds.toFixed(3) + "s";
}

function fillRatio(v, max = 12) {
  return Math.max(0, Math.min(1, v / max));
}

// Resistance → pipe openness. R=0.5 wide open; R=10 nearly pinched shut.
function pipeOpenness(r, max = 10) {
  const t = Math.max(0, Math.min(1, (r - 0.5) / (max - 0.5)));
  return 1.0 - t * 0.94;
}

/* ─── Water-current texture ──────────────────────────────────────────────
   A drifting sine streak that reads as moving water (replaces the old
   marching dashes). The wavelength MUST match the translateX distance in
   styles.css → @keyframes water-drift, so each animation loop lands one
   full wavelength along and repeats seamlessly.
   ───────────────────────────────────────────────────────────────────────── */
const WATER_WAVELENGTH = 56;
function renderWaterCurrent({ x1, x2, y, amp = 4, period, flowing,
                              stroke = "var(--water-soft)", width = 2.5,
                              opacity = 0.85, phase = 0 }) {
  const lam = WATER_WAVELENGTH;
  // Start two wavelengths early so the rightward drift never exposes a gap
  // on the left edge (the path is clipped to the water anyway).
  const x0 = x1 - 2 * lam + (phase % lam);
  let d = `M ${x0} ${y}`;
  for (let x = x0; x < x2; x += lam) {
    d += ` q ${lam / 4} ${-amp * 2} ${lam / 2} 0 q ${lam / 4} ${amp * 2} ${lam / 2} 0`;
  }
  return (
    <path d={d} fill="none" stroke={stroke} strokeWidth={width}
          strokeLinecap="round" opacity={opacity}
          className={flowing ? "water-drift" : ""}
          style={{ "--flow-period": period }} />
  );
}

/* ─── WaterScene ──────────────────────────────────────────────────────────
   Wooden water barrel on the left (the "pressure source"), with a
   horizontal pipe running off the bottom-right, narrowing in the middle
   (the resistance). Water level in the barrel follows voltage.
   ───────────────────────────────────────────────────────────────────────── */
function WaterScene({ voltage = 6, resistance = 3, current = 2, showLabels = true, height = 380 }) {
  const W = 820, H = 460;
  const fill = fillRatio(voltage);
  const open = pipeOpenness(resistance);
  const period = flowPeriod(current);

  // Barrel geometry — sits on the left half
  const barrelCx = 150;
  const barrelTop = 70;
  const barrelBot = 380;
  const rimRx = 82;          // half-width at the rim
  const midRx = 102;         // half-width at the bulge
  const rimRy = 14;          // ellipse for top rim (perspective)

  // Pipe geometry — exits the BOTTOM of the barrel's side (pressure is
  // highest at the bottom, and the barrel can drain fully).
  const pipeBaseHalfHeight = 38;
  const pipeY = barrelBot - pipeBaseHalfHeight;   // pipe floor flush with the barrel floor
  const pipeStart = barrelCx + 78;  // overlaps the barrel a little
  const pipeEnd = W - 200;          // leaves room for the faucet + jet arc INSIDE the viewBox

  // Constriction (40% – 60% of pipe length) — drawn as a GATE coming down
  // from the top wall, so its opening sits on the pipe floor and lines up
  // exactly with the downstream water level. R=0 → gate fully raised → no pinch.
  const cX1 = pipeStart + (pipeEnd - pipeStart) * 0.40;
  const cX2 = pipeStart + (pipeEnd - pipeStart) * 0.60;
  const narrowHalfHeight = pipeBaseHalfHeight * (0.10 + 0.90 * open);
  const yThroatTop = pipeY + pipeBaseHalfHeight - 2 * narrowHalfHeight;   // top of the gate opening

  // Pressure shown as WATER LEVEL: full before the pinch, and after it the
  // level sits EXACTLY at the gate's opening — the pinch IS the new level.
  // Zero resistance → gate raised → no drop. Flow dashes stay uniform.
  const pinchCx = (cX1 + cX2) / 2;
  const bottomY = pipeY + pipeBaseHalfHeight;
  const topY = pipeY - pipeBaseHalfHeight;
  const fracAfter = narrowHalfHeight / pipeBaseHalfHeight;            // opening ÷ full height
  const pinchFrac = (pinchCx - pipeStart) / (pipeEnd - pipeStart);    // where the pinch sits along the pipe
  const washMax = Math.min(0.82, (1 - fracAfter) * 0.85);            // how much the water pales after the pinch
  const waterPath = `M ${pipeStart} ${topY} L ${pinchCx} ${topY} L ${pinchCx} ${yThroatTop} L ${pipeEnd} ${yThroatTop} L ${pipeEnd} ${bottomY} L ${pipeStart} ${bottomY} Z`;
  const waterSurface = `M ${pipeStart} ${topY} L ${pinchCx} ${topY} L ${pinchCx} ${yThroatTop} L ${pipeEnd} ${yThroatTop}`;
  const outletY = Math.max(pipeY, (yThroatTop + bottomY) / 2);        // spurt exits at the low level

  // Water level inside the barrel
  const waterTop = barrelTop + (1 - fill) * (barrelBot - barrelTop);

  // half-width of barrel at any y (bulged sinusoidally)
  const halfWidthAt = (y) => {
    const t = Math.max(0, Math.min(1, (y - barrelTop) / (barrelBot - barrelTop)));
    return rimRx + (midRx - rimRx) * Math.sin(t * Math.PI);
  };

  // Barrel silhouette path — bottom edge bows down (we look slightly down
  // on a cylinder, so the base reads as the front of an ellipse)
  const barrelSilhouette = `
    M ${barrelCx - rimRx} ${barrelTop}
    C ${barrelCx - midRx - 4} ${barrelTop + 70}, ${barrelCx - midRx - 4} ${barrelBot - 70}, ${barrelCx - rimRx} ${barrelBot}
    A ${rimRx} 13 0 0 0 ${barrelCx + rimRx} ${barrelBot}
    C ${barrelCx + midRx + 4} ${barrelBot - 70}, ${barrelCx + midRx + 4} ${barrelTop + 70}, ${barrelCx + rimRx} ${barrelTop}
    Z
  `;

  // Pipe paths — flat floor; only the TOP wall dips down at the gate
  const topPath = `
    M ${pipeStart} ${pipeY - pipeBaseHalfHeight}
    L ${cX1 - 28} ${pipeY - pipeBaseHalfHeight}
    C ${cX1 - 6} ${pipeY - pipeBaseHalfHeight}, ${cX1} ${yThroatTop}, ${cX1 + 8} ${yThroatTop}
    L ${cX2 - 8} ${yThroatTop}
    C ${cX2} ${yThroatTop}, ${cX2 + 6} ${pipeY - pipeBaseHalfHeight}, ${cX2 + 28} ${pipeY - pipeBaseHalfHeight}
    L ${pipeEnd} ${pipeY - pipeBaseHalfHeight}
  `;
  const botPath = `
    M ${pipeStart} ${pipeY + pipeBaseHalfHeight}
    L ${pipeEnd} ${pipeY + pipeBaseHalfHeight}
  `;
  const interiorPath = `
    M ${pipeStart} ${pipeY - pipeBaseHalfHeight}
    L ${cX1 - 28} ${pipeY - pipeBaseHalfHeight}
    C ${cX1 - 6} ${pipeY - pipeBaseHalfHeight}, ${cX1} ${yThroatTop}, ${cX1 + 8} ${yThroatTop}
    L ${cX2 - 8} ${yThroatTop}
    C ${cX2} ${yThroatTop}, ${cX2 + 6} ${pipeY - pipeBaseHalfHeight}, ${cX2 + 28} ${pipeY - pipeBaseHalfHeight}
    L ${pipeEnd} ${pipeY - pipeBaseHalfHeight}
    L ${pipeEnd} ${pipeY + pipeBaseHalfHeight}
    L ${pipeStart} ${pipeY + pipeBaseHalfHeight}
    Z
  `;
  const centerline = `M ${pipeStart} ${pipeY} L ${pipeEnd} ${pipeY}`;

  // Stave x positions (vertical wood planks)
  const stavesX = [-50, -25, 0, 25, 50];

  // ── Faucet at the pipe's end ───────────────────────────────
  // The pipe ends in a barrel tap. Physics of the jet:
  //   · exit SPEED  ← pressure left after the pinch (reach + droplet pace)
  //   · thickness   ← current (how much water is actually moving)
  const FAUCET_S = 1.55;                           // chunky, readable tap
  const tip = faucetTip({ x: pipeEnd, y: pipeY, baseH: pipeBaseHalfHeight, s: FAUCET_S });
  const tipX = tip.x, tipY = tip.y;
  const pressureLeft = voltage * fracAfter;        // 0..12 — the push at the outlet
  const pNorm = Math.max(0, Math.min(1, pressureLeft / 12));
  const spurt = current > 0.1;
  const spurtReach = 12 + pNorm * 100;             // speed → distance before gravity wins (kept inside viewBox)
  const landX = tipX + spurtReach;
  const landY = 436;
  const spurtPath = `M ${tipX} ${tipY} Q ${tipX + spurtReach * 0.55} ${tipY + 4}, ${landX} ${landY}`;
  // droplet travel time ← exit speed (pressure), NOT flow volume
  const jetSecs = Math.max(0.45, 2.4 - pNorm * 1.8);
  const jetPeriod = jetSecs.toFixed(2) + "s";
  const droplets = spurt ? Array.from({ length: Math.round(5 + Math.min(6, current)) }, (_, i) => i) : [];
  // Stream thickness ← current: a thin trickle at low flow, a fat jet at high.
  const jetWidth = 2.5 + Math.min(15, current * 2.6);
  const dropScale = 0.55 + Math.min(1.15, current * 0.22);
  // teardrop pointing in +x (bulb leads, tail trails) — rotate="auto" aligns it to the arc
  const teardrop = "M 7 0 C 3 4.5, -5 3.5, -8 0 C -5 -3.5, 3 -4.5, 7 0 Z";

  // Hoops (horizontal metal bands) — drawn as ellipses sized to the barrel's
  // half-width at that y. The bottom half of each ellipse is hidden by the
  // barrel; only the top arc reads as a wrapping hoop. We also draw a short
  // flat band underneath to suggest the hoop's thickness.
  const hoopYs = [110, 340];

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={height}
         preserveAspectRatio="xMidYMid meet"
         style={{ display: "block", maxHeight: height }}>
      <defs>
        <linearGradient id="water-grad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"  stopColor="var(--water-soft)" />
          <stop offset="100%" stopColor="var(--water)" />
        </linearGradient>
        <linearGradient id="barrel-deep" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"  stopColor="var(--water)" />
          <stop offset="100%" stopColor="var(--water-deep)" />
        </linearGradient>
        <linearGradient id="pinch-wash" gradientUnits="userSpaceOnUse"
                        x1={pipeStart} y1="0" x2={pipeEnd} y2="0">
          <stop offset="0" stopColor="var(--bg)" stopOpacity="0" />
          <stop offset={Math.max(0, pinchFrac - 0.02)} stopColor="var(--bg)" stopOpacity="0" />
          <stop offset={Math.min(1, pinchFrac + 0.10)} stopColor="var(--bg)" stopOpacity={washMax * 0.7} />
          <stop offset="1" stopColor="var(--bg)" stopOpacity={washMax} />
        </linearGradient>
        <linearGradient id="wood-grad" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%"  stopColor="var(--bg-card)" />
          <stop offset="50%" stopColor="var(--bg)" />
          <stop offset="100%" stopColor="var(--bg-deeper)" />
        </linearGradient>
        <linearGradient id="pipe-3d" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"   stopColor="var(--ink)" stopOpacity="0.22" />
          <stop offset="20%"  stopColor="var(--bg)" />
          <stop offset="60%"  stopColor="var(--bg-card)" />
          <stop offset="100%" stopColor="var(--ink)" stopOpacity="0.28" />
        </linearGradient>
        <linearGradient id="pipe-spec" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"   stopColor="rgba(255,255,255,0)" />
          <stop offset="12%"  stopColor="rgba(255,255,255,0.55)" />
          <stop offset="30%"  stopColor="rgba(255,255,255,0)" />
          <stop offset="100%" stopColor="rgba(255,255,255,0)" />
        </linearGradient>
        <linearGradient id="barrel-shade" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%"   stopColor="var(--ink)" stopOpacity="0.20" />
          <stop offset="20%"  stopColor="#ffffff" stopOpacity="0.18" />
          <stop offset="42%"  stopColor="#ffffff" stopOpacity="0" />
          <stop offset="78%"  stopColor="var(--ink)" stopOpacity="0.10" />
          <stop offset="100%" stopColor="var(--ink)" stopOpacity="0.26" />
        </linearGradient>
        <clipPath id="barrel-clip">
          <path d={barrelSilhouette} />
        </clipPath>
        <clipPath id="pipe-clip">
          <path d={interiorPath} />
        </clipPath>
        <clipPath id="pipe-water-clip">
          <path d={waterPath} />
        </clipPath>
      </defs>

      {/* ── Barrel ─────────────────────────────────────────────────── */}
      {/* ground shadow */}
      <ellipse cx={barrelCx} cy={barrelBot + 10} rx={rimRx + 16} ry="9"
               fill="var(--ink)" opacity="0.08" />
      {/* wood-grain fill */}
      <path d={barrelSilhouette} fill="url(#wood-grad)" />

      {/* water inside (clipped to barrel) */}
      <g clipPath="url(#barrel-clip)">
        <rect x={barrelCx - midRx - 6} y={waterTop}
              width={(midRx + 6) * 2} height={barrelBot - waterTop + 16}
              fill="url(#barrel-deep)" />
        {/* wave on the water surface */}
        <path
          d={`M ${barrelCx - midRx} ${waterTop + 3}
              Q ${barrelCx - midRx * 0.5} ${waterTop - 5}, ${barrelCx} ${waterTop + 3}
              T ${barrelCx + midRx} ${waterTop + 3}`}
          fill="none" stroke="var(--water-soft)" strokeWidth="2.5" opacity="0.55"
        />
        {/* subtle vertical staves */}
        {stavesX.map((dx) => (
          <line key={dx}
                x1={barrelCx + dx} y1={barrelTop}
                x2={barrelCx + dx} y2={barrelBot + 14}
                stroke="var(--ink)" strokeWidth="0.6" opacity="0.18" />
        ))}
        {/* cylindrical shading — highlight left-of-center, shadow at the edges */}
        <path d={barrelSilhouette} fill="url(#barrel-shade)" />
      </g>

      {/* hoops — drawn over the barrel, only top half shows */}
      {hoopYs.map((hy) => {
        const hw = halfWidthAt(hy);
        return (
          <g key={hy}>
            <ellipse cx={barrelCx} cy={hy} rx={hw} ry={9}
                     fill="none" stroke="var(--ink)" strokeWidth="3" />
            {/* highlight on the upper rim of the hoop */}
            <path d={`M ${barrelCx - hw + 6} ${hy - 2}
                       A ${hw - 6} 5 0 0 1 ${barrelCx + hw - 6} ${hy - 2}`}
                  fill="none" stroke="var(--bg-card)" strokeWidth="1" opacity="0.6" />
          </g>
        );
      })}

      {/* top rim (open mouth, looking slightly down) */}
      <ellipse cx={barrelCx} cy={barrelTop} rx={rimRx} ry={rimRy}
               fill="var(--ink)" opacity="0.85" />
      <ellipse cx={barrelCx} cy={barrelTop - 2} rx={rimRx - 2} ry={rimRy - 2}
               fill="var(--bg-deeper)" />
      {/* if water level is ABOVE the rim (impossible) or AT it, show wavy surface inside rim */}
      {fill > 0.95 && (
        <ellipse cx={barrelCx} cy={barrelTop - 1} rx={rimRx - 4} ry={rimRy - 4}
                 fill="var(--water-soft)" />
      )}

      {/* barrel outline (drawn last so it's crisp on top) */}
      <path d={barrelSilhouette} fill="none" stroke="var(--ink)" strokeWidth="2.5" />

      {/* fill markers on the side */}
      {[0.25, 0.5, 0.75].map(t => {
        const y = barrelTop + t * (barrelBot - barrelTop);
        const hw = halfWidthAt(y);
        return (
          <g key={t}>
            <line x1={barrelCx + hw + 4} y1={y}
                  x2={barrelCx + hw + 14} y2={y}
                  stroke="var(--ink-faint)" strokeWidth="1.5" />
          </g>
        );
      })}
      <text x={barrelCx + midRx + 22} y={barrelTop + 12}
            fontFamily="IBM Plex Mono, monospace" fontSize="21.5"
            fill="var(--ink-faint)" letterSpacing="0.12em">HIGH</text>
      <text x={barrelCx} y={barrelBot + 38} textAnchor="middle"
            fontFamily="IBM Plex Mono, monospace" fontSize="21.5"
            fill="var(--ink-faint)" letterSpacing="0.12em">LOW</text>

      {/* ── Pipe + flow (3D cylindrical) ───────────────────────────── */}
      {/* 3D interior fill — empty tube */}
      <path d={interiorPath} fill="url(#pipe-3d)" />
      {/* the water itself — same deep gradient as the barrel; level = push left */}
      <g clipPath="url(#pipe-clip)">
        <path d={waterPath} fill="url(#barrel-deep)" />
        {/* pressure tint: deep/saturated before the pinch, drained after — the
           downstream water fades toward the page BACKGROUND by how much push is
           spent. Washing to --bg (not white) reads in every theme: it pales on
           the light themes and darkens toward navy on blueprint, so the
           pre/post-pinch contrast is always visible. */}
        <path d={waterPath} fill="url(#pinch-wash)" />
        <path d={waterSurface} fill="none" stroke="var(--water-soft)" strokeWidth="2.5" opacity="0.8" />
      </g>
      {/* specular highlight band near the top */}
      <path d={interiorPath} fill="url(#pipe-spec)" pointerEvents="none" opacity="0.55" />

      {/* pipe outlines */}
      <path d={topPath} fill="none" stroke="var(--ink)" strokeWidth="2.5" />
      <path d={botPath} fill="none" stroke="var(--ink)" strokeWidth="2.5" />

      {/* flange where the pipe leaves the barrel — reads as a real fitting */}
      <ellipse cx={pipeStart + 16} cy={pipeY} rx="6" ry={pipeBaseHalfHeight + 7}
               fill="var(--bg-deeper)" stroke="var(--ink)" strokeWidth="2.5" />
      <ellipse cx={pipeStart + 16} cy={pipeY} rx="2.5" ry={pipeBaseHalfHeight + 2}
               fill="none" stroke="var(--ink)" strokeWidth="1" opacity="0.4" />

      {/* ── faucet at the outlet: sealed end wall + barrel tap ── */}
      {renderFaucet({ x: pipeEnd, y: pipeY, baseH: pipeBaseHalfHeight, on: spurt, s: FAUCET_S })}

      {/* animated water-current streaks — clipped to the WATER so they ride inside it */}
      <g clipPath="url(#pipe-water-clip)">
        {/* bright surface shimmer (visible in the full section) */}
        {renderWaterCurrent({ x1: pipeStart, x2: pipeEnd, y: pipeY - pipeBaseHalfHeight * 0.35,
                              amp: 2.2, period, flowing: current > 0.05,
                              stroke: "rgba(255,255,255,0.55)", width: 1.6, opacity: 0.75 })}
        {/* main current — low in the pipe so it survives the level drop */}
        {renderWaterCurrent({ x1: pipeStart, x2: pipeEnd, y: pipeY + pipeBaseHalfHeight * 0.45,
                              amp: 4.5, period, flowing: current > 0.05,
                              stroke: "var(--water-soft)", width: 2.8, opacity: 0.9, phase: 19 })}
        {/* deep undertow */}
        {renderWaterCurrent({ x1: pipeStart, x2: pipeEnd, y: pipeY + pipeBaseHalfHeight * 0.72,
                              amp: 3, period, flowing: current > 0.05,
                              stroke: "var(--water-deep)", width: 1.4, opacity: 0.5, phase: 38 })}
      </g>

      {/* constriction callout */}
      {showLabels && (
        <g>
          <line x1={(cX1+cX2)/2} y1={pipeY - pipeBaseHalfHeight - 32}
                x2={(cX1+cX2)/2} y2={yThroatTop - 6}
                stroke="var(--ink-faint)" strokeWidth="1.2" strokeDasharray="3 3" />
          <text x={(cX1+cX2)/2} y={pipeY - pipeBaseHalfHeight - 42}
                fontFamily="IBM Plex Mono, monospace" fontSize="24.5"
                fill="var(--ink-faint)" textAnchor="middle" letterSpacing="0.14em">
            RESISTANCE · R
          </text>
        </g>
      )}

      {/* pressure before / after the pinch */}
      {showLabels && (
        <g fontFamily="IBM Plex Mono, monospace" textAnchor="middle" letterSpacing="0.1em">
          <text x={pipeStart + (pipeEnd - pipeStart) * 0.20} y={pipeY + pipeBaseHalfHeight + 30}
                fontSize="16" fill="var(--water)">FULL PUSH</text>
          <text x={pipeStart + (pipeEnd - pipeStart) * 0.80} y={pipeY + pipeBaseHalfHeight + 30}
                fontSize="16" fill="var(--ink-faint)">PUSH LEFT · {Math.round(fracAfter * 100)}%</text>
        </g>
      )}

      {/* outlet droplets */}
      {/* outlet water spurt — droplets arc out and fall under gravity */}
      {spurt && (
        <g>
          {/* continuous jet — thickness grows with current, so more flow reads as a fatter stream */}
          <path d={spurtPath} fill="none" stroke="url(#water-grad)"
                strokeWidth={jetWidth} strokeLinecap="round"
                opacity={0.22 + Math.min(0.34, current * 0.12)} />
          {/* teardrops travelling the arc — pace set by PRESSURE, not flow */}
          {droplets.map(i => {
            const sc = dropScale * (0.82 + (i % 3) * 0.16);
            return (
              <g key={i}>
                <path d={teardrop} fill="var(--water)" transform={`translate(${tipX} ${tipY}) scale(${sc})`}>
                  <animateMotion path={spurtPath} dur={jetPeriod}
                                 begin={`-${(i * jetSecs) / droplets.length}s`}
                                 repeatCount="indefinite" rotate="auto" />
                  <animate attributeName="opacity" values="0;0.95;0.95;0"
                           keyTimes="0;0.12;0.78;1" dur={jetPeriod}
                           begin={`-${(i * jetSecs) / droplets.length}s`}
                           repeatCount="indefinite" />
                </path>
              </g>
            );
          })}
          {/* splash where the stream lands */}
          <ellipse cx={landX} cy={landY + 4} rx={Math.min(26, 10 + current * 2)} ry="4"
                   fill="var(--water)" opacity="0.28">
            <animate attributeName="opacity" values="0.1;0.34;0.1" dur="0.9s" repeatCount="indefinite" />
          </ellipse>
          {[-1, 1].map((d, i) => (
            <circle key={i} r="2.6" fill="var(--water)" cx={landX} cy={landY}>
              <animate attributeName="cx" values={`${landX};${landX + d * (14 + current * 2)}`} dur="0.7s" begin={`-${i * 0.2}s`} repeatCount="indefinite" />
              <animate attributeName="cy" values={`${landY};${landY - 14};${landY + 2}`} keyTimes="0;0.5;1" dur="0.7s" begin={`-${i * 0.2}s`} repeatCount="indefinite" />
              <animate attributeName="opacity" values="0.9;0" dur="0.7s" begin={`-${i * 0.2}s`} repeatCount="indefinite" />
            </circle>
          ))}
        </g>
      )}

      {/* main labels */}
      {showLabels && (
        <g fontFamily="IBM Plex Mono, monospace" fill="var(--ink-faint)" letterSpacing="0.14em">
          <text x={barrelCx - midRx} y={barrelTop - 28} fontSize="27" fontWeight="500"
                fill="var(--water)">
            PRESSURE · V
          </text>
          <text x={pipeEnd - 110} y={pipeY - pipeBaseHalfHeight - 18} fontSize="24.5"
                fill="var(--current)">
            FLOW · I →
          </text>
        </g>
      )}
    </svg>
  );
}

/* ─── CircuitScene ────────────────────────────────────────────────────────
   Battery on left, resistor on top, lamp on right. Electrons travel
   around a closed loop using SMIL animateMotion — clearer & slower
   than dashed-line marching.
   ───────────────────────────────────────────────────────────────────────── */
function CircuitScene({ voltage = 6, resistance = 3, current = 2, showLabels = true, height = 380 }) {
  const W = 820, H = 460;
  const period = flowPeriod(current);
  const flowing = current > 0.05;
  const periodSec = parseFloat(period);

  // Loop corners
  const L = 140, R = 680, T = 130, B = 360;

  // Battery position (vertical, on left wire)
  const batCx = L, batCy = (T + B) / 2;
  const batHalfH = 30;   // half the vertical extent of the schematic battery

  // Resistor on top wire
  const resCx = (L + R) / 2, resCy = T;
  const resW = 130, resH = 38;

  // Bulb on right wire
  const bulbCx = R, bulbCy = (T + B) / 2;
  const bulbR = 30;

  // Visible wire segments (drawn with breaks at battery / bulb)
  const wirePath = `
    M ${L} ${batCy - batHalfH}
    L ${L} ${T}
    L ${resCx - resW/2} ${T}
    M ${resCx + resW/2} ${T}
    L ${R} ${T}
    L ${R} ${bulbCy - bulbR}
    M ${R} ${bulbCy + bulbR}
    L ${R} ${B}
    L ${L} ${B}
    L ${L} ${batCy + batHalfH}
  `;

  // Continuous closed loop for animateMotion (electrons travel through everything,
  // including the battery & lamp interior). Conventional current direction:
  // from + terminal (top of battery) → top-left → right along top → through R →
  // continue right → down → through bulb → bottom-right → left along bottom →
  // up to − terminal → back through battery to +.
  const electronLoop = `
    M ${L} ${batCy - batHalfH}
    L ${L} ${T}
    L ${R} ${T}
    L ${R} ${B}
    L ${L} ${B}
    L ${L} ${batCy + batHalfH}
    L ${L} ${batCy - batHalfH}
    Z
  `;

  // Direction arrowheads at fixed positions along the loop
  const arrows = [
    { x: (L + R) / 2 - 80, y: T,         angle: 0 },
    { x: (L + R) / 2 + 80, y: T,         angle: 0 },
    { x: R,                y: (T + B)/2 + 60, angle: 90 },
    { x: (L + R) / 2,      y: B,         angle: 180 },
    { x: L,                y: (T + B)/2 - 60, angle: 270 },
  ];

  // Power → bulb glow & subtle scale
  const power = voltage * current;
  const glowOpacity = Math.max(0, Math.min(1, power / 14));
  const bulbScale = 1 + Math.min(0.08, power / 180);

  // Electrons — 7 evenly spaced around the loop
  const electronCount = 7;
  const electrons = flowing
    ? Array.from({ length: electronCount }, (_, i) => i)
    : [];

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={height}
         preserveAspectRatio="xMidYMid meet"
         style={{ display: "block", maxHeight: height }}>
      <defs>
        <radialGradient id="bulb-glow" cx="50%" cy="50%" r="50%">
          <stop offset="0%"  stopColor="var(--current-deep)" stopOpacity="0.95"/>
          <stop offset="55%" stopColor="var(--current)" stopOpacity="0.45"/>
          <stop offset="100%" stopColor="var(--current)" stopOpacity="0"/>
        </radialGradient>
        <filter id="electron-soft" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="0.4"/>
        </filter>
      </defs>

      {/* underglow on wires when current flows */}
      <path d={wirePath} fill="none" stroke="var(--current-faint)"
            strokeWidth="9" strokeLinecap="round"
            opacity={flowing ? 0.55 : 0}
            style={{ transition: "opacity 200ms ease" }} />

      {/* main wires */}
      <path d={wirePath} fill="none" stroke="var(--ink)" strokeWidth="2.5" strokeLinecap="round" />

      {/* direction arrowheads */}
      {showLabels && arrows.map((a, i) => (
        <g key={i} transform={`translate(${a.x} ${a.y}) rotate(${a.angle})`}
           opacity={flowing ? 0.85 : 0.35}
           style={{ transition: "opacity 200ms ease" }}>
          <path d="M -8 -6 L 4 0 L -8 6 L -5 0 Z"
                fill={flowing ? "var(--current)" : "var(--ink-faint)"}
                stroke="none" />
        </g>
      ))}

      {/* electrons traveling the loop (JS-driven so they stay evenly spaced) */}
      {flowing && (
        <Electrons path={electronLoop} period={period} count={electronCount}
                   flowing={flowing} r={6} color="var(--current)" filter="url(#electron-soft)" />
      )}

      {/* ── Battery (schematic, enlarged with dynamic voltage label) ── */}
      <g>
        <line x1={batCx - 34} y1={batCy - batHalfH}     x2={batCx + 34} y2={batCy - batHalfH}     stroke="var(--ink)" strokeWidth="3.5" />
        <line x1={batCx - 20} y1={batCy - batHalfH + 14} x2={batCx + 20} y2={batCy - batHalfH + 14} stroke="var(--ink)" strokeWidth="3.5" />
        <line x1={batCx - 34} y1={batCy + batHalfH - 14} x2={batCx + 34} y2={batCy + batHalfH - 14} stroke="var(--ink)" strokeWidth="3.5" />
        <line x1={batCx - 20} y1={batCy + batHalfH}     x2={batCx + 20} y2={batCy + batHalfH}     stroke="var(--ink)" strokeWidth="3.5" />
        <text x={batCx + 46} y={batCy - batHalfH + 8}
              fontFamily="IBM Plex Mono, monospace" fontSize="29.5" fontWeight="600"
              fill="var(--water)">+</text>
        <text x={batCx + 46} y={batCy + batHalfH + 6}
              fontFamily="IBM Plex Mono, monospace" fontSize="29.5" fontWeight="600"
              fill="var(--ink-faint)">−</text>
        {showLabels && (
          <text x={batCx - 24} y={batCy + 6}
                fontFamily="IBM Plex Mono, monospace" fontSize="24.5"
                fill="var(--water)" textAnchor="end" letterSpacing="0.14em">
            {voltage.toFixed(1)}V
          </text>
        )}
      </g>

      {/* ── Resistor ─────────────────────────────────────────────── */}
      <g>
        <rect x={resCx - resW/2} y={resCy - resH/2} width={resW} height={resH}
              fill="var(--bg-card)" stroke="var(--ink)" strokeWidth="2.5" rx="2" />
        {Array.from({ length: Math.min(10, Math.max(2, Math.round(resistance + 1))) }).map((_, i, arr) => {
          const x = resCx - resW/2 + (resW / (arr.length + 1)) * (i + 1);
          return <line key={i} x1={x} y1={resCy - resH/2 + 6} x2={x} y2={resCy + resH/2 - 6}
                       stroke="var(--ink)" strokeWidth="1.6" opacity="0.65" />;
        })}
        {showLabels && (
          <text x={resCx} y={resCy - resH/2 - 16}
                fontFamily="IBM Plex Mono, monospace" fontSize="24.5"
                fill="var(--ink-faint)" textAnchor="middle" letterSpacing="0.14em">
            RESISTOR · {resistance.toFixed(1)}Ω
          </text>
        )}
      </g>

      {/* ── Bulb ─────────────────────────────────────────────────── */}
      <g style={{ transformOrigin: `${bulbCx}px ${bulbCy}px`,
                  transform: `scale(${bulbScale})`,
                  transition: "transform 200ms ease" }}>
        {/* warm halo (scales with power) */}
        <circle cx={bulbCx} cy={bulbCy - 4} r="70" fill="url(#bulb-glow)"
                opacity={glowOpacity} style={{ transition: "opacity 200ms ease" }} />

        {/* screw base / cap at the bottom */}
        <rect x={bulbCx - 13} y={bulbCy + 14} width="26" height="20" rx="3"
              fill="var(--bg-deeper)" stroke="var(--ink)" strokeWidth="2" />
        {[19, 24, 29].map((dy, i) => (
          <line key={i} x1={bulbCx - 13} y1={bulbCy + dy} x2={bulbCx + 13} y2={bulbCy + dy}
                stroke="var(--ink)" strokeWidth="1" opacity="0.55" />
        ))}

        {/* glass envelope (pear-ish) */}
        <path d={`M ${bulbCx - 11} ${bulbCy + 15}
                  C ${bulbCx - 30} ${bulbCy + 4} ${bulbCx - 28} ${bulbCy - 34} ${bulbCx} ${bulbCy - 34}
                  C ${bulbCx + 28} ${bulbCy - 34} ${bulbCx + 30} ${bulbCy + 4} ${bulbCx + 11} ${bulbCy + 15} Z`}
              fill={flowing ? "color-mix(in oklch, var(--current) 22%, var(--bg-card))" : "var(--bg-card)"}
              stroke="var(--ink)" strokeWidth="2.5"
              style={{ transition: "fill 220ms ease" }} />

        {/* filament — two posts + a glowing zigzag */}
        <g stroke={flowing ? "var(--current-deep)" : "var(--ink-soft)"}
           strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round"
           style={{ transition: "stroke 220ms ease",
                    filter: flowing ? "drop-shadow(0 0 5px var(--current))" : "none" }}>
          <line x1={bulbCx - 7} y1={bulbCy + 14} x2={bulbCx - 7} y2={bulbCy - 6} />
          <line x1={bulbCx + 7} y1={bulbCy + 14} x2={bulbCx + 7} y2={bulbCy - 6} />
          <path d={`M ${bulbCx - 7} ${bulbCy - 6}
                    L ${bulbCx - 3.5} ${bulbCy - 13} L ${bulbCx} ${bulbCy - 6}
                    L ${bulbCx + 3.5} ${bulbCy - 13} L ${bulbCx + 7} ${bulbCy - 6}`} />
        </g>

        {showLabels && (
          <text x={bulbCx + 52} y={bulbCy + 2}
                fontFamily="IBM Plex Mono, monospace" fontSize="24.5"
                fill="var(--ink-faint)" letterSpacing="0.14em">
            LAMP
          </text>
        )}
      </g>

      {/* current direction caption */}
      {showLabels && (
        <text x={(L + R) / 2} y={B + 36}
              fontFamily="IBM Plex Mono, monospace" fontSize="24.5"
              fill={flowing ? "var(--current)" : "var(--ink-faint)"}
              letterSpacing="0.16em" textAnchor="middle"
              style={{ transition: "fill 200ms ease" }}>
          CURRENT · I = {current.toFixed(2)} A
        </text>
      )}
    </svg>
  );
}

Object.assign(window, { WaterScene, CircuitScene, flowPeriod, fillRatio, pipeOpenness, renderWaterCurrent, renderFaucet, faucetTip });
