/* visuals2.jsx — Chapter 2: Series & Parallel
   SeriesWater, ParallelWater, SeriesCircuit, ParallelCircuit.
   Reuses flowPeriod, fillRatio, pipeOpenness from visuals.jsx (already on window). */

/* ─── Shared rendering helpers ──────────────────────────────────────────── */

// Build a half-pipe path (top or bottom outline) through any number of
// constrictions. `side` = -1 for top, +1 for bottom. A constriction with
// `gate: true` narrows from the TOP only (a sluice gate): its opening sits on
// the pipe floor, so the downstream water level lines up with it exactly.
function buildPipeSide(pipeStart, pipeEnd, pipeY, baseH, constrictions, side) {
  let path = `M ${pipeStart} ${pipeY + side * baseH}`;
  for (const c of constrictions) {
    const yN = c.gate
      ? (side < 0 ? pipeY + baseH - 2 * c.narrowH : pipeY + baseH)
      : pipeY + side * c.narrowH;
    path += ` L ${c.xa - 28} ${pipeY + side * baseH}`;
    path += ` C ${c.xa - 6} ${pipeY + side * baseH}, ${c.xa} ${yN}, ${c.xa + 8} ${yN}`;
    path += ` L ${c.xb - 8} ${yN}`;
    path += ` C ${c.xb} ${yN}, ${c.xb + 6} ${pipeY + side * baseH}, ${c.xb + 28} ${pipeY + side * baseH}`;
  }
  path += ` L ${pipeEnd} ${pipeY + side * baseH}`;
  return path;
}

// Closed interior path (top + reversed bottom).
function buildPipeInterior(pipeStart, pipeEnd, pipeY, baseH, constrictions) {
  const top = buildPipeSide(pipeStart, pipeEnd, pipeY, baseH, constrictions, -1);
  // Reverse the bottom-side path so the polygon closes cleanly. Easier:
  // build top, then descend at pipeEnd, then build bottom in reverse direction.
  let path = top;
  path += ` L ${pipeEnd} ${pipeY + baseH}`;
  // Bottom in reverse (right-to-left)
  const revCons = [...constrictions].reverse();
  for (const c of revCons) {
    const yN = c.gate ? pipeY + baseH : pipeY + c.narrowH;
    path += ` L ${c.xb + 28} ${pipeY + baseH}`;
    path += ` C ${c.xb + 6} ${pipeY + baseH}, ${c.xb} ${yN}, ${c.xb - 8} ${yN}`;
    path += ` L ${c.xa + 8} ${yN}`;
    path += ` C ${c.xa} ${yN}, ${c.xa - 6} ${pipeY + baseH}, ${c.xa - 28} ${pipeY + baseH}`;
  }
  path += ` L ${pipeStart} ${pipeY + baseH} Z`;
  return path;
}

// Barrel — reuse the same geometry as Chapter 1 but as a self-contained renderer.
function renderBarrel({ cx, top, bot, rimRx, midRx, fill, idSuffix, deepWater = false }) {
  const rimRy = 12;
  const waterTop = top + (1 - fill) * (bot - top);
  const halfWidthAt = (y) => {
    const t = Math.max(0, Math.min(1, (y - top) / (bot - top)));
    return rimRx + (midRx - rimRx) * Math.sin(t * Math.PI);
  };
  const silhouette = `
    M ${cx - rimRx} ${top}
    C ${cx - midRx - 4} ${top + 60}, ${cx - midRx - 4} ${bot - 60}, ${cx - rimRx} ${bot}
    L ${cx + rimRx} ${bot}
    C ${cx + midRx + 4} ${bot - 60}, ${cx + midRx + 4} ${top + 60}, ${cx + rimRx} ${top}
    Z
  `;
  const hoopYs = [top + 40, bot - 40];
  const staves = [-40, -20, 0, 20, 40];
  const clipId = `barrel-clip-${idSuffix}`;
  const woodId = `wood-${idSuffix}`;
  const waterId = `water-${idSuffix}`;

  return (
    <g>
      <defs>
        <linearGradient id={waterId} x1="0" y1="0" x2="0" y2="1">
          {deepWater ? (
            <>
              <stop offset="0%"  stopColor="var(--water)" />
              <stop offset="100%" stopColor="var(--water-deep)" />
            </>
          ) : (
            <>
              <stop offset="0%"  stopColor="var(--water-soft)" />
              <stop offset="100%" stopColor="var(--water)" />
            </>
          )}
        </linearGradient>
        <linearGradient id={woodId} 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>
        <clipPath id={clipId}><path d={silhouette} /></clipPath>
      </defs>
      <path d={silhouette} fill={`url(#${woodId})`} />
      <g clipPath={`url(#${clipId})`}>
        <rect x={cx - midRx - 6} y={waterTop}
              width={(midRx + 6) * 2} height={bot - waterTop}
              fill={`url(#${waterId})`} />
        <path d={`M ${cx - midRx} ${waterTop + 3} Q ${cx - midRx * 0.5} ${waterTop - 5}, ${cx} ${waterTop + 3} T ${cx + midRx} ${waterTop + 3}`}
              fill="none" stroke="var(--water-deep)" strokeWidth="2" opacity="0.55"/>
        {staves.map((dx) => (
          <line key={dx} x1={cx + dx} y1={top} x2={cx + dx} y2={bot}
                stroke="var(--ink)" strokeWidth="0.6" opacity="0.18" />
        ))}
      </g>
      {hoopYs.map((hy, i) => {
        const hw = halfWidthAt(hy);
        return (
          <ellipse key={i} cx={cx} cy={hy} rx={hw} ry={8}
                   fill="none" stroke="var(--ink)" strokeWidth="2.5" />
        );
      })}
      <ellipse cx={cx} cy={top} rx={rimRx} ry={rimRy} fill="var(--ink)" opacity="0.85" />
      <ellipse cx={cx} cy={top - 2} rx={rimRx - 2} ry={rimRy - 2} fill="var(--bg-deeper)" />
      <path d={silhouette} fill="none" stroke="var(--ink)" strokeWidth="2.5" />
    </g>
  );
}

// 3D pipe renderer — interior + outlines + flow ripples + outlet end-cap.
// `levels` (optional): [{ fromX, toX, frac }] — renders the pipe as an EMPTY
// tube with water filled to `frac` of its height per segment (pressure-as-level).
// Without `levels`, the pipe renders full of water as before.
function renderPipe({ pipeStart, pipeEnd, pipeY, baseH, constrictions, current, period, idSuffix, showOutletCap = true, levels = null }) {
  const top = buildPipeSide(pipeStart, pipeEnd, pipeY, baseH, constrictions, -1);
  const bot = buildPipeSide(pipeStart, pipeEnd, pipeY, baseH, constrictions, +1);
  const interior = buildPipeInterior(pipeStart, pipeEnd, pipeY, baseH, constrictions);
  const centerline = `M ${pipeStart} ${pipeY} L ${pipeEnd} ${pipeY}`;
  const clipId = `pipe-clip-${idSuffix}`;
  const cylId = `pipe-cyl-${idSuffix}`;
  const specId = `pipe-spec-${idSuffix}`;
  const waterClipId = `pipe-water-clip-${idSuffix}`;
  const flowing = current > 0.05;

  // Stepped water polygon + surface line for level mode
  const bottomY = pipeY + baseH;
  const levelYOf = (f) => bottomY - Math.max(0, Math.min(1, f)) * (2 * baseH);
  let waterPath = null, waterSurface = null;
  if (levels && levels.length) {
    const pts = [];
    levels.forEach((s, i) => {
      const y = levelYOf(s.frac);
      pts.push(`${i === 0 ? "M" : "L"} ${s.fromX} ${y}`);
      pts.push(`L ${s.toX} ${y}`);
    });
    waterSurface = pts.join(" ");
    waterPath = `${waterSurface} L ${levels[levels.length - 1].toX} ${bottomY} L ${levels[0].fromX} ${bottomY} Z`;
  }

  return (
    <g>
      <defs>
        <linearGradient id={cylId} x1="0" y1="0" x2="0" y2="1">
          {levels ? (
            <>
              <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" />
            </>
          ) : (
            <>
              <stop offset="0%"   stopColor="var(--water-deep)" />
              <stop offset="22%"  stopColor="var(--water-soft)" />
              <stop offset="50%"  stopColor="var(--water)" />
              <stop offset="78%"  stopColor="var(--water)" />
              <stop offset="100%" stopColor="var(--water-deep)" />
            </>
          )}
        </linearGradient>
        <linearGradient id={`water-deep-${idSuffix}`} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"  stopColor="var(--water)" />
          <stop offset="100%" stopColor="var(--water-deep)" />
        </linearGradient>
        <linearGradient id={specId} 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.5)" />
          <stop offset="30%"  stopColor="rgba(255,255,255,0)" />
          <stop offset="100%" stopColor="rgba(255,255,255,0)" />
        </linearGradient>
        <clipPath id={clipId}><path d={interior} /></clipPath>
        {waterPath && <clipPath id={waterClipId}><path d={waterPath} /></clipPath>}
      </defs>

      <path d={interior} fill={`url(#${cylId})`} />
      {/* the water, filled to the pressure level (clipped to the pipe walls) */}
      {waterPath && (
        <g clipPath={`url(#${clipId})`}>
          <path d={waterPath} fill={`url(#water-deep-${idSuffix})`} />
          <path d={waterSurface} fill="none" stroke="var(--water-soft)" strokeWidth="2.5" opacity="0.8" />
        </g>
      )}
      <path d={interior} fill={`url(#${specId})`} pointerEvents="none" opacity={levels ? 0.55 : 1} />

      <path d={top} fill="none" stroke="var(--ink)" strokeWidth="2.5" />
      <path d={bot} fill="none" stroke="var(--ink)" strokeWidth="2.5" />

      {showOutletCap && (() => {
        const fs = Math.max(0.65, Math.min(1.05, baseH / 34));
        return renderFaucet({ x: pipeEnd, y: pipeY, baseH, on: flowing, s: fs });
      })()}

      <g clipPath={`url(#${clipId})`}>
        <g clipPath={waterPath ? `url(#${waterClipId})` : undefined}>
        {/* bright surface shimmer */}
        {renderWaterCurrent({ x1: pipeStart, x2: pipeEnd, y: pipeY - baseH * 0.35,
                              amp: Math.max(1.6, baseH * 0.07), period, flowing,
                              stroke: "rgba(255,255,255,0.55)", width: 1.5, opacity: 0.75 })}
        {/* main current */}
        {renderWaterCurrent({ x1: pipeStart, x2: pipeEnd, y: levels ? pipeY + baseH * 0.45 : pipeY,
                              amp: Math.max(2.4, baseH * 0.13), period, flowing,
                              stroke: "var(--water-soft)", width: 2.8, opacity: 0.85, phase: 19 })}
        {/* deep undertow */}
        {renderWaterCurrent({ x1: pipeStart, x2: pipeEnd, y: pipeY + baseH * (levels ? 0.72 : 0.4),
                              amp: Math.max(1.8, baseH * 0.09), period, flowing,
                              stroke: "var(--water-deep)", width: 1.2, opacity: 0.45, phase: 38 })}
        </g>
      </g>
    </g>
  );
}

function renderConstrictionLabels({ constrictions, pipeY, baseH, labels }) {
  return (
    <g>
      {constrictions.map((c, i) => {
        const mid = (c.xa + c.xb) / 2;
        const yTip = c.gate ? pipeY + baseH - 2 * c.narrowH - 6 : pipeY - c.narrowH - 6;
        return (
          <g key={i}>
            <line x1={mid} y1={pipeY - baseH - 30} x2={mid} y2={yTip}
                  stroke="var(--ink-faint)" strokeWidth="1" strokeDasharray="3 3" />
            <text x={mid} y={pipeY - baseH - 38}
                  fontFamily="IBM Plex Mono, monospace" fontSize="21.5"
                  fill="var(--ink-faint)" textAnchor="middle" letterSpacing="0.12em">
              {labels[i]}
            </text>
          </g>
        );
      })}
    </g>
  );
}

function renderOutletDroplets({ pipeEnd, pipeY, current, period, count = 6, baseH = 30 }) {
  if (current <= 0.1) return null;
  // Jet leaves from the faucet's spout mouth (drawn by renderPipe).
  const fs = Math.max(0.65, Math.min(1.05, baseH / 34));
  const tip = faucetTip({ x: pipeEnd, y: pipeY, baseH, s: fs });
  const reach = 22 + Math.min(140, current * 22);
  const landX = tip.x + reach, landY = pipeY + 110;
  const arc = `M ${tip.x} ${tip.y} Q ${tip.x + reach * 0.52} ${tip.y + 2}, ${landX} ${landY}`;
  const teardrop = "M 6 0 C 2.6 4, -4.5 3, -7 0 C -4.5 -3, 2.6 -4, 6 0 Z";
  const n = Math.round(count + Math.min(5, current));
  const pd = parseFloat(period);
  return (
    <g>
      {current > 1.4 && (
        <path d={arc} fill="none" stroke="var(--water)" strokeWidth={Math.min(10, 3 + current * 1.3)}
              strokeLinecap="round" opacity="0.3" />
      )}
      {Array.from({ length: n }, (_, i) => {
        const sc = 0.78 + (i % 3) * 0.2;
        const begin = `-${(i * pd) / n}s`;
        return (
          <path key={i} d={teardrop} fill="var(--water)"
                transform={`translate(${tip.x} ${tip.y}) scale(${sc})`}>
            <animateMotion path={arc} dur={period} begin={begin} repeatCount="indefinite" rotate="auto" />
            <animate attributeName="opacity" values="0;0.95;0.95;0" keyTimes="0;0.12;0.78;1"
                     dur={period} begin={begin} repeatCount="indefinite" />
          </path>
        );
      })}
      <ellipse cx={landX} cy={landY + 4} rx={Math.min(24, 9 + current * 2)} ry="3.5"
               fill="var(--water)" opacity="0.26">
        <animate attributeName="opacity" values="0.1;0.32;0.1" dur="0.9s" repeatCount="indefinite" />
      </ellipse>
    </g>
  );
}

/* ─── SeriesWater ───────────────────────────────────────────────────────── */

function SeriesWater({ voltage = 6, r1 = 3, r2 = 3, height = 380, showLabels = true }) {
  const W = 820, H = 460;
  const I = voltage / (r1 + r2);
  const period = flowPeriod(I);
  const fill = fillRatio(voltage);

  const barrelCx = 110, barrelTop = 60, barrelBot = 370;
  const rimRx = 62, midRx = 80;

  const pipeY = 280;
  const pipeStart = barrelCx + 60;
  const pipeEnd = W - 70;
  const baseH = 32;

  const span = pipeEnd - pipeStart;
  const c1xa = pipeStart + span * 0.22, c1xb = pipeStart + span * 0.38;
  const c2xa = pipeStart + span * 0.55, c2xb = pipeStart + span * 0.71;
  const nh1 = baseH * (0.12 + 0.88 * pipeOpenness(r1));
  const nh2 = baseH * (0.12 + 0.88 * pipeOpenness(r2));
  const constrictions = [
    { xa: c1xa, xb: c1xb, narrowH: nh1, gate: true },
    { xa: c2xa, xb: c2xb, narrowH: nh2, gate: true },
  ];

  // Pressure shown as WATER LEVEL. Each gate's opening sits on the pipe floor,
  // and the level downstream lines up with it EXACTLY: zero resistance, zero
  // pinch, no drop. A looser second gate can't raise the level back up — the
  // level only ever steps DOWN (to the tightest gate so far).
  const frac1 = nh1 / baseH;                          // opening of gate 1
  const fracMid = Math.min(1, frac1);
  const fracEnd = Math.min(fracMid, nh2 / baseH);     // tighter gate wins
  const m1 = (c1xa + c1xb) / 2, m2 = (c2xa + c2xb) / 2;
  const levels = [
    { fromX: pipeStart, toX: m1, frac: 1 },
    { fromX: m1, toX: m2, frac: fracMid },
    { fromX: m2, toX: pipeEnd, frac: fracEnd },
  ];

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={height}
         preserveAspectRatio="xMidYMid meet" style={{ display: "block", maxHeight: height }}>
      {renderBarrel({ cx: barrelCx, top: barrelTop, bot: barrelBot, rimRx, midRx, fill, idSuffix: "sw", deepWater: true })}
      {renderPipe({ pipeStart, pipeEnd, pipeY, baseH, constrictions, current: I, period, idSuffix: "sw", levels })}
      {renderOutletDroplets({ pipeEnd, pipeY, baseH, current: I, period })}
      {showLabels && renderConstrictionLabels({ constrictions, pipeY, baseH, labels: [`R₁ · ${r1.toFixed(1)}Ω`, `R₂ · ${r2.toFixed(1)}Ω`] })}
      {showLabels && (
        <g fontFamily="IBM Plex Mono, monospace" textAnchor="middle" letterSpacing="0.06em">
          <text x={pipeStart + span * 0.10} y={pipeY + baseH + 30} fontSize="17" fill="var(--water)">100%</text>
          <text x={pipeStart + span * 0.46} y={pipeY + baseH + 30} fontSize="17" fill="var(--water)">{Math.round(fracMid * 100)}%</text>
          <text x={pipeStart + span * 0.85} y={pipeY + baseH + 30} fontSize="17" fill="var(--ink-faint)">{Math.round(fracEnd * 100)}%</text>
          <text x={pipeStart + span * 0.46} y={pipeY + baseH + 50} fontSize="12" fill="var(--ink-faint)" letterSpacing="0.12em">WATER LEVEL = PUSH LEFT</text>
        </g>
      )}
      {showLabels && (
        <g fontFamily="IBM Plex Mono, monospace" fill="var(--ink-faint)" letterSpacing="0.14em">
          <text x={barrelCx - midRx + 6} y={barrelTop - 22} fontSize="24.5" fill="var(--water)" fontWeight="500">
            PRESSURE · {voltage.toFixed(1)}V
          </text>
          <text x={pipeEnd - 130} y={pipeY - baseH - 14} fontSize="21.5" fill="var(--current)">
            FLOW · {I.toFixed(2)}A
          </text>
        </g>
      )}
    </svg>
  );
}

/* ─── ParallelWater ─────────────────────────────────────────────────────── */
/* Water splits into two horizontal branches, each with its own constriction. */

function ParallelWater({ voltage = 6, r1 = 3, r2 = 3, height = 380, showLabels = true }) {
  const W = 820, H = 460;
  const I1 = voltage / r1;
  const I2 = voltage / r2;
  const Itot = I1 + I2;
  const period1 = flowPeriod(I1);
  const period2 = flowPeriod(I2);
  const periodMain = flowPeriod(Itot);
  const fill = fillRatio(voltage);

  const barrelCx = 110, barrelTop = 60, barrelBot = 370;
  const rimRx = 62, midRx = 80;

  // Layout
  const baseH = 22;             // branches thinner than main
  const baseHMain = 30;
  const trunkY = 230;
  const topY = 130;
  const botY = 330;
  const trunkStart = barrelCx + 60;
  const splitX = trunkStart + 70;
  const branchStartX = splitX + 12;
  const branchEndX = W - 200;
  const recombineX = branchEndX + 12;
  const trunkEnd = recombineX + 70;
  const outletX = W - 60;

  const branchSpan = branchEndX - branchStartX;
  // Constrictions in each branch
  const cTop = { xa: branchStartX + branchSpan * 0.28, xb: branchStartX + branchSpan * 0.66,
                 narrowH: baseH * (0.14 + 0.86 * pipeOpenness(r1)) };
  const cBot = { xa: branchStartX + branchSpan * 0.28, xb: branchStartX + branchSpan * 0.66,
                 narrowH: baseH * (0.14 + 0.86 * pipeOpenness(r2)) };

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={height}
         preserveAspectRatio="xMidYMid meet" style={{ display: "block", maxHeight: height }}>
      {renderBarrel({ cx: barrelCx, top: barrelTop, bot: barrelBot, rimRx, midRx, fill, idSuffix: "pw" })}

      {/* Trunk in (barrel → split) */}
      {renderPipe({ pipeStart: trunkStart, pipeEnd: splitX + 10, pipeY: trunkY,
                    baseH: baseHMain, constrictions: [], current: Itot, period: periodMain,
                    idSuffix: "pw-tin", showOutletCap: false })}

      {/* Y-junctions — two diagonal connectors at the split */}
      <path d={`M ${splitX - 4} ${trunkY - baseHMain}
                Q ${splitX + 10} ${trunkY - baseHMain * 0.5}, ${branchStartX} ${topY + baseH}
                L ${branchStartX} ${topY - baseH}
                Q ${splitX + 10} ${trunkY - baseHMain * 0.5}, ${splitX - 4} ${trunkY - baseHMain * 0.2}
                Z`}
            fill="var(--water)" opacity="0.0" stroke="none" />

      {/* Top branch wires (connectors) */}
      <path d={`M ${splitX} ${trunkY - baseHMain} L ${branchStartX} ${topY + baseH}`}
            stroke="var(--ink)" strokeWidth="2.5" fill="none" strokeLinecap="round" />
      <path d={`M ${splitX} ${trunkY - baseHMain + 6} L ${branchStartX - 6} ${topY + baseH + 4}`}
            stroke="var(--ink)" strokeWidth="2.5" fill="none" strokeLinecap="round" />

      {/* Bottom branch connectors */}
      <path d={`M ${splitX} ${trunkY + baseHMain} L ${branchStartX} ${botY - baseH}`}
            stroke="var(--ink)" strokeWidth="2.5" fill="none" strokeLinecap="round" />
      <path d={`M ${splitX} ${trunkY + baseHMain - 6} L ${branchStartX - 6} ${botY - baseH - 4}`}
            stroke="var(--ink)" strokeWidth="2.5" fill="none" strokeLinecap="round" />

      {/* Top branch pipe */}
      {renderPipe({ pipeStart: branchStartX, pipeEnd: branchEndX, pipeY: topY,
                    baseH, constrictions: [cTop], current: I1, period: period1,
                    idSuffix: "pw-top", showOutletCap: false })}

      {/* Bottom branch pipe */}
      {renderPipe({ pipeStart: branchStartX, pipeEnd: branchEndX, pipeY: botY,
                    baseH, constrictions: [cBot], current: I2, period: period2,
                    idSuffix: "pw-bot", showOutletCap: false })}

      {/* Right recombine connectors */}
      <path d={`M ${branchEndX} ${topY + baseH} L ${recombineX} ${trunkY - baseHMain}`}
            stroke="var(--ink)" strokeWidth="2.5" fill="none" strokeLinecap="round" />
      <path d={`M ${branchEndX - 6} ${topY + baseH + 4} L ${recombineX} ${trunkY - baseHMain + 6}`}
            stroke="var(--ink)" strokeWidth="2.5" fill="none" strokeLinecap="round" />
      <path d={`M ${branchEndX} ${botY - baseH} L ${recombineX} ${trunkY + baseHMain}`}
            stroke="var(--ink)" strokeWidth="2.5" fill="none" strokeLinecap="round" />
      <path d={`M ${branchEndX - 6} ${botY - baseH - 4} L ${recombineX} ${trunkY + baseHMain - 6}`}
            stroke="var(--ink)" strokeWidth="2.5" fill="none" strokeLinecap="round" />

      {/* Trunk out (recombine → outlet) */}
      {renderPipe({ pipeStart: recombineX - 10, pipeEnd: outletX, pipeY: trunkY,
                    baseH: baseHMain, constrictions: [], current: Itot, period: periodMain,
                    idSuffix: "pw-tout" })}

      {renderOutletDroplets({ pipeEnd: outletX, pipeY: trunkY, baseH: baseHMain, current: Itot, period: periodMain })}

      {showLabels && (
        <g>
          <text x={(branchStartX + branchEndX)/2} y={topY - baseH - 22}
                fontFamily="IBM Plex Mono, monospace" fontSize="20.5"
                fill="var(--ink-faint)" textAnchor="middle" letterSpacing="0.12em">
            R₁ · {r1.toFixed(1)}Ω · I₁ = {I1.toFixed(2)}A
          </text>
          <text x={(branchStartX + branchEndX)/2} y={botY + baseH + 32}
                fontFamily="IBM Plex Mono, monospace" fontSize="20.5"
                fill="var(--ink-faint)" textAnchor="middle" letterSpacing="0.12em">
            R₂ · {r2.toFixed(1)}Ω · I₂ = {I2.toFixed(2)}A
          </text>
          <text x={barrelCx - midRx + 6} y={barrelTop - 22}
                fontFamily="IBM Plex Mono, monospace" fontSize="24.5"
                fill="var(--water)" letterSpacing="0.14em">
            PRESSURE · {voltage.toFixed(1)}V
          </text>
          <text x={outletX - 130} y={trunkY - baseHMain - 14}
                fontFamily="IBM Plex Mono, monospace" fontSize="21.5"
                fill="var(--current)" letterSpacing="0.14em">
            TOTAL · {Itot.toFixed(2)}A
          </text>
        </g>
      )}
    </svg>
  );
}

/* ─── Circuit drawing helpers ───────────────────────────────────────────── */

function renderBatterySymbol({ cx, cy, halfH, voltage, showLabel }) {
  return (
    <g>
      <line x1={cx - 32} y1={cy - halfH}     x2={cx + 32} y2={cy - halfH}     stroke="var(--ink)" strokeWidth="3.5" />
      <line x1={cx - 18} y1={cy - halfH + 12} x2={cx + 18} y2={cy - halfH + 12} stroke="var(--ink)" strokeWidth="3.5" />
      <line x1={cx - 32} y1={cy + halfH - 12} x2={cx + 32} y2={cy + halfH - 12} stroke="var(--ink)" strokeWidth="3.5" />
      <line x1={cx - 18} y1={cy + halfH}     x2={cx + 18} y2={cy + halfH}     stroke="var(--ink)" strokeWidth="3.5" />
      <text x={cx + 42} y={cy - halfH + 8} fontFamily="IBM Plex Mono, monospace"
            fontSize="27" fontWeight="600" fill="var(--water)">+</text>
      <text x={cx + 42} y={cy + halfH + 4} fontFamily="IBM Plex Mono, monospace"
            fontSize="27" fontWeight="600" fill="var(--ink-faint)">−</text>
      {showLabel && (
        <text x={cx - 22} y={cy + 6} fontFamily="IBM Plex Mono, monospace"
              fontSize="21.5" fill="var(--water)" textAnchor="end" letterSpacing="0.14em">
          {voltage.toFixed(1)}V
        </text>
      )}
    </g>
  );
}

function renderResistorSymbol({ cx, cy, w = 100, h = 30, r = 3, label, vertical = false }) {
  if (vertical) {
    // Rotate 90° via transforming
    return (
      <g transform={`translate(${cx} ${cy}) rotate(90)`}>
        <rect x={-w/2} y={-h/2} width={w} height={h}
              fill="var(--bg-card)" stroke="var(--ink)" strokeWidth="2.5" rx="2" />
        {Array.from({ length: Math.min(8, Math.max(2, Math.round(r + 1))) }).map((_, i, arr) => {
          const x = -w/2 + (w / (arr.length + 1)) * (i + 1);
          return <line key={i} x1={x} y1={-h/2 + 5} x2={x} y2={h/2 - 5}
                       stroke="var(--ink)" strokeWidth="1.5" opacity="0.65" />;
        })}
        {label && (
          <text x="0" y={-h/2 - 12} fontFamily="IBM Plex Mono, monospace"
                fontSize="19" fill="var(--ink-faint)" textAnchor="middle"
                letterSpacing="0.12em" transform="rotate(-90)">
            {label}
          </text>
        )}
      </g>
    );
  }
  return (
    <g>
      <rect x={cx - w/2} y={cy - h/2} width={w} height={h}
            fill="var(--bg-card)" stroke="var(--ink)" strokeWidth="2.5" rx="2" />
      {Array.from({ length: Math.min(8, Math.max(2, Math.round(r + 1))) }).map((_, i, arr) => {
        const x = cx - w/2 + (w / (arr.length + 1)) * (i + 1);
        return <line key={i} x1={x} y1={cy - h/2 + 5} x2={x} y2={cy + h/2 - 5}
                     stroke="var(--ink)" strokeWidth="1.5" opacity="0.65" />;
      })}
      {label && (
        <text x={cx} y={cy - h/2 - 12} fontFamily="IBM Plex Mono, monospace"
              fontSize="19" fill="var(--ink-faint)" textAnchor="middle" letterSpacing="0.12em">
          {label}
        </text>
      )}
    </g>
  );
}

function renderBulbSymbol({ cx, cy, r = 26, glowOpacity, flowing, label }) {
  const idGlow = `bulb-glow-${cx}-${cy}`;
  const s = r / 26;  // scale factor relative to the design size
  return (
    <g>
      <defs>
        <radialGradient id={idGlow} 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>
      </defs>
      <circle cx={cx} cy={cy - 3 * s} r={r * 2.4} fill={`url(#${idGlow})`}
              opacity={glowOpacity} style={{ transition: "opacity 200ms ease" }} />

      {/* screw base / cap at the bottom */}
      <rect x={cx - 11 * s} y={cy + 11 * s} width={22 * s} height={16 * s} rx={2.5 * s}
            fill="var(--bg-deeper)" stroke="var(--ink)" strokeWidth="2" />
      {[15, 19, 23].map((dy, i) => (
        <line key={i} x1={cx - 11 * s} y1={cy + dy * s} x2={cx + 11 * s} y2={cy + dy * s}
              stroke="var(--ink)" strokeWidth="0.9" opacity="0.5" />
      ))}

      {/* glass envelope (pear-ish) */}
      <path d={`M ${cx - 9 * s} ${cy + 12 * s}
                C ${cx - 25 * s} ${cy + 3 * s} ${cx - 23 * s} ${cy - 28 * s} ${cx} ${cy - 28 * s}
                C ${cx + 23 * s} ${cy - 28 * s} ${cx + 25 * s} ${cy + 3 * s} ${cx + 9 * s} ${cy + 12 * s} 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={cx - 6 * s} y1={cy + 11 * s} x2={cx - 6 * s} y2={cy - 5 * s} />
        <line x1={cx + 6 * s} y1={cy + 11 * s} x2={cx + 6 * s} y2={cy - 5 * s} />
        <path d={`M ${cx - 6 * s} ${cy - 5 * s}
                  L ${cx - 3 * s} ${cy - 11 * s} L ${cx} ${cy - 5 * s}
                  L ${cx + 3 * s} ${cy - 11 * s} L ${cx + 6 * s} ${cy - 5 * s}`} />
      </g>

      {label && (
        <text x={cx + r + 16} y={cy + 5}
              fontFamily="IBM Plex Mono, monospace" fontSize="19"
              fill="var(--ink-faint)" letterSpacing="0.12em">
          {label}
        </text>
      )}
    </g>
  );
}

// Returns electrons animating along a closed path (now JS-driven via the
// shared <Electrons> component so they never clump when speed changes).
function renderElectrons({ path, period, count = 6, flowing, color = "var(--current)" }) {
  if (!flowing) return null;
  return <Electrons path={path} period={period} count={count} flowing={flowing} color={color} />;
}

/* ─── SeriesCircuit ────────────────────────────────────────────────────── */

function SeriesCircuit({ voltage = 6, r1 = 3, r2 = 3, height = 380, showLabels = true }) {
  const W = 820, H = 460;
  const I = voltage / (r1 + r2);
  const period = flowPeriod(I);
  const flowing = I > 0.05;
  const periodSec = parseFloat(period);

  const L = 130, R = 690, T = 120, B = 360;
  const batCx = L, batCy = (T + B) / 2, batHalfH = 32;
  const res1Cx = L + (R - L) * 0.32, res2Cx = L + (R - L) * 0.62;
  const resCy = T;
  const resW = 90, resH = 32;
  const bulbCx = R, bulbCy = (T + B) / 2, bulbR = 28;

  const wirePath = `
    M ${L} ${batCy - batHalfH}
    L ${L} ${T}
    L ${res1Cx - resW/2} ${T}
    M ${res1Cx + resW/2} ${T}
    L ${res2Cx - resW/2} ${T}
    M ${res2Cx + resW/2} ${T}
    L ${R} ${T}
    L ${R} ${bulbCy - bulbR}
    M ${R} ${bulbCy + bulbR}
    L ${R} ${B}
    L ${L} ${B}
    L ${L} ${batCy + batHalfH}
  `;
  const loopPath = `
    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
  `;

  const power = voltage * I;
  const glow = Math.max(0, Math.min(1, power / 14));

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={height}
         preserveAspectRatio="xMidYMid meet" style={{ display: "block", maxHeight: height }}>
      <path d={wirePath} fill="none" stroke="var(--current-faint)"
            strokeWidth="9" strokeLinecap="round"
            opacity={flowing ? 0.55 : 0} style={{ transition: "opacity 200ms ease" }} />
      <path d={wirePath} fill="none" stroke="var(--ink)" strokeWidth="2.5" strokeLinecap="round" />

      {renderElectrons({ path: loopPath, period, flowing, count: 6 })}

      {renderBatterySymbol({ cx: batCx, cy: batCy, halfH: batHalfH, voltage, showLabel: showLabels })}
      {renderResistorSymbol({ cx: res1Cx, cy: resCy, w: resW, h: resH, r: r1, label: showLabels && `R₁ · ${r1.toFixed(1)}Ω` })}
      {renderResistorSymbol({ cx: res2Cx, cy: resCy, w: resW, h: resH, r: r2, label: showLabels && `R₂ · ${r2.toFixed(1)}Ω` })}
      {renderBulbSymbol({ cx: bulbCx, cy: bulbCy, r: bulbR, glowOpacity: glow, flowing, label: showLabels && "LAMP" })}

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

/* ─── ParallelCircuit ──────────────────────────────────────────────────── */
/* Battery on left, two parallel branches each w/ resistor + bulb, between
   top & bottom rails. */

function ParallelCircuit({ voltage = 6, r1 = 3, r2 = 3, height = 380, showLabels = true }) {
  const W = 820, H = 460;
  const I1 = voltage / r1;
  const I2 = voltage / r2;
  const Itot = I1 + I2;
  const period1 = flowPeriod(I1);
  const period2 = flowPeriod(I2);

  const T = 110, B = 380;
  const Lrail = 110, Rrail = 680;
  const batCx = Lrail, batCy = (T + B) / 2, batHalfH = 36;
  const b1X = 330, b2X = 540;
  const resCy = T + 70, bulbCy = B - 70;
  const bulbR = 24;

  // Visible wire path
  const wirePath = `
    M ${Lrail} ${batCy - batHalfH}
    L ${Lrail} ${T}
    L ${Rrail} ${T}
    M ${Lrail} ${B}
    L ${Rrail} ${B}
    M ${Lrail} ${batCy + batHalfH}
    L ${Lrail} ${B}
    M ${b1X} ${T}
    L ${b1X} ${resCy - 16}
    M ${b1X} ${resCy + 16}
    L ${b1X} ${bulbCy - bulbR}
    M ${b1X} ${bulbCy + bulbR}
    L ${b1X} ${B}
    M ${b2X} ${T}
    L ${b2X} ${resCy - 16}
    M ${b2X} ${resCy + 16}
    L ${b2X} ${bulbCy - bulbR}
    M ${b2X} ${bulbCy + bulbR}
    L ${b2X} ${B}
  `;

  // Loops for electron animation — one per branch
  const loop1 = `
    M ${Lrail} ${batCy - batHalfH}
    L ${Lrail} ${T} L ${b1X} ${T}
    L ${b1X} ${B} L ${Lrail} ${B}
    L ${Lrail} ${batCy + batHalfH}
    L ${Lrail} ${batCy - batHalfH} Z
  `;
  const loop2 = `
    M ${Lrail} ${batCy - batHalfH}
    L ${Lrail} ${T} L ${b2X} ${T}
    L ${b2X} ${B} L ${Lrail} ${B}
    L ${Lrail} ${batCy + batHalfH}
    L ${Lrail} ${batCy - batHalfH} Z
  `;

  const flowing1 = I1 > 0.05;
  const flowing2 = I2 > 0.05;
  const flowing = flowing1 || flowing2;
  const glow1 = Math.max(0, Math.min(1, (voltage * I1) / 14));
  const glow2 = Math.max(0, Math.min(1, (voltage * I2) / 14));

  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={height}
         preserveAspectRatio="xMidYMid meet" style={{ display: "block", maxHeight: height }}>
      <path d={wirePath} fill="none" stroke="var(--current-faint)"
            strokeWidth="9" strokeLinecap="round"
            opacity={flowing ? 0.45 : 0} style={{ transition: "opacity 200ms ease" }} />
      <path d={wirePath} fill="none" stroke="var(--ink)" strokeWidth="2.5" strokeLinecap="round" />

      {/* junction dots at branch points */}
      {[b1X, b2X].map(x => (
        <g key={x}>
          <circle cx={x} cy={T} r="4" fill="var(--ink)" />
          <circle cx={x} cy={B} r="4" fill="var(--ink)" />
        </g>
      ))}

      {renderElectrons({ path: loop1, period: period1, flowing: flowing1, count: 5 })}
      {renderElectrons({ path: loop2, period: period2, flowing: flowing2, count: 5 })}

      {renderBatterySymbol({ cx: batCx, cy: batCy, halfH: batHalfH, voltage, showLabel: showLabels })}
      {renderResistorSymbol({ cx: b1X, cy: resCy, w: 32, h: 70, r: r1, vertical: true,
                              label: showLabels && `R₁·${r1.toFixed(1)}Ω` })}
      {renderResistorSymbol({ cx: b2X, cy: resCy, w: 32, h: 70, r: r2, vertical: true,
                              label: showLabels && `R₂·${r2.toFixed(1)}Ω` })}
      {renderBulbSymbol({ cx: b1X, cy: bulbCy, r: bulbR, glowOpacity: glow1, flowing: flowing1,
                          label: showLabels && `I₁·${I1.toFixed(2)}A` })}
      {renderBulbSymbol({ cx: b2X, cy: bulbCy, r: bulbR, glowOpacity: glow2, flowing: flowing2,
                          label: showLabels && `I₂·${I2.toFixed(2)}A` })}

      {showLabels && (
        <g fontFamily="IBM Plex Mono, monospace" letterSpacing="0.14em">
          <text x={(Lrail + Rrail) / 2} y={T - 18} fontSize="19" fill="var(--water)" textAnchor="middle">
            SAME VOLTAGE · {voltage.toFixed(1)}V ACROSS EACH
          </text>
          <text x={(Lrail + Rrail) / 2} y={B + 30} fontSize="19" fill="var(--current)" textAnchor="middle">
            TOTAL I = I₁ + I₂ = {Itot.toFixed(2)}A
          </text>
        </g>
      )}
    </svg>
  );
}

Object.assign(window, { SeriesWater, ParallelWater, SeriesCircuit, ParallelCircuit, renderPipe, renderOutletDroplets });
