/**
 * CircuitSim — Enhanced Electric Circuits Simulation v2
 * MNA solver · L-shape wires · Drag · Undo/Redo · Tooltip
 * New: Capacitor · Diode · LED · AC source · Junction dots
 * Keyboard: W R B S L C D A V E  ·  Del  ·  Ctrl+Z/Y
 */
'use strict';

function distToSegment(px, py, x1, y1, x2, y2) {
  const dx = x2 - x1, dy = y2 - y1;
  const len2 = dx * dx + dy * dy;
  if (len2 < 1) return Math.hypot(px - x1, py - y1);
  const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / len2));
  return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
}

function _compLen(c) {
  return Math.max(1, Math.abs(c.x2 - c.x1) + Math.abs(c.y2 - c.y1));
}

class CircuitSim {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');

    // State
    this.components = [];
    this.addMode    = 'wire';
    this.R_value    = 10;
    this.U_value    = 9;
    this.C_value    = 100;   // µF (display label)
    this.L_value    = 10;    // mH for inductor
    this.acFreq     = 2;     // Hz for AC source
    this.ledColor   = '#7BF5A4';

    // Features
    this._heatmapOn = false; // power heatmap overlay toggle
    this._oscPanel  = null;  // oscilloscope canvas element (injected from outside)

    // Interaction
    this._drawing     = null;   // {x1, y1} while dragging new component
    this._ghostEnd    = null;   // {x2, y2} cursor snap
    this._hovered     = null;   // id of hovered component
    this._selected    = null;   // id of selected component
    this._dragIdx     = null;   // index of component being dragged
    this._dragStart   = null;   // {gx, gy} grid anchor at drag start
    this._dragOrigPos = null;   // original {x1,y1,x2,y2} before drag
    this._didDrag     = false;  // whether mouse actually moved during drag

    // Solver
    this._nextId    = 0;
    this._solution  = null;
    this._diodeR    = new Map(); // component id <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> effective R
    this._simTime   = 0;
    this._hasAC     = false;

    // Animation
    this._raf     = null;
    this._lastTs  = null;

    // Undo stack
    this._history    = [];
    this._historyIdx = -1;

    // Grid
    this.GW   = 22;
    this.GH   = 14;
    this.CELL = 40;
    this.ox   = 0;
    this.oy   = 0;
    this.W    = 0;
    this.H    = 0;

    this.onUpdate     = null;
    this.onModeChange = null; // called when keyboard changes addMode
    this._keyHandler  = null; // stored for destroy()

    this.fit();
    this._bindEvents();
  }

  /* ─── Geometry ─────────────────────────────────────────────────────────── */

  _nodePixel(gx, gy) {
    return { x: this.ox + gx * this.CELL, y: this.oy + gy * this.CELL };
  }

  _snapGrid(px, py) {
    return {
      gx: Math.max(0, Math.min(this.GW, Math.round((px - this.ox) / this.CELL))),
      gy: Math.max(0, Math.min(this.GH, Math.round((py - this.oy) / this.CELL)))
    };
  }

  /* ─── Undo / Redo ──────────────────────────────────────────────────────── */

  _pushHistory() {
    this._history.splice(this._historyIdx + 1);
    this._history.push(JSON.stringify(this.components.map(c => ({
      id: c.id, type: c.type,
      x1: c.x1, y1: c.y1, x2: c.x2, y2: c.y2,
      value: c.value, L_value: c.L_value, open: c.open,
      ledColor: c.ledColor, acFreq: c.acFreq
    }))));
    if (this._history.length > 20) this._history.shift();
    this._historyIdx = this._history.length - 1;
  }

  undo() {
    if (this._historyIdx <= 0) return;
    this._historyIdx--;
    this._applyHistory();
  }

  redo() {
    if (this._historyIdx >= this._history.length - 1) return;
    this._historyIdx++;
    this._applyHistory();
  }

  _applyHistory() {
    const snap = JSON.parse(this._history[this._historyIdx]);
    this._diodeR.clear();
    this.components = snap.map(s => {
      if (s.type === 'diode' || s.type === 'led') this._diodeR.set(s.id, 1e9);
      return { ...s, _I: 0, _v1: 0, _v2: 0, _t: Math.random() };
    });
    this._nextId = Math.max(0, ...this.components.map(c => c.id + 1));
    this._selected = null;
    this._solve();
    this.draw();
    if (this.onUpdate) this.onUpdate(this.info());
  }

  /* ─── Component resistance ─────────────────────────────────────────────── */

  _compR(c) {
    switch (c.type) {
      case 'wire':      return 0.001;
      case 'ammeter':   return 0.001;
      case 'voltmeter': return 1e7;
      case 'resistor':  return Math.max(0.001, c.value || 10);
      case 'lamp':      return 20;
      case 'capacitor': return 1e7;   // open circuit in DC
      case 'inductor':  return 0.001; // short circuit in DC; AC handled in _buildMatrix
      case 'switch':    return c.open ? 1e9 : 0.001;
      case 'diode':
      case 'led':       return this._diodeR.get(c.id) ?? 1e9;
      default:          return 1;
    }
  }

  /* ─── MNA Solver ───────────────────────────────────────────────────────── */

  _buildNodes() {
    const allKeys = new Set();
    for (const c of this.components) {
      allKeys.add(`${c.x1},${c.y1}`);
      allKeys.add(`${c.x2},${c.y2}`);
    }
    const parent = new Map();
    for (const k of allKeys) parent.set(k, k);

    const find = k => {
      while (parent.get(k) !== k) {
        parent.set(k, parent.get(parent.get(k)));
        k = parent.get(k);
      }
      return k;
    };
    const union = (a, b) => parent.set(find(a), find(b));

    for (const c of this.components) {
      if (c.type === 'wire' || (c.type === 'switch' && !c.open)) {
        union(`${c.x1},${c.y1}`, `${c.x2},${c.y2}`);
      }
    }

    const roots = new Set();
    for (const k of allKeys) roots.add(find(k));
    const nodeId = new Map();
    let n = 0;
    for (const r of roots) nodeId.set(r, n++);

    return { nNodes: n, nodeOf: key => nodeId.get(find(key)) };
  }

  _buildMatrix(nodeOf, nNodes) {
    const vsrcs = this.components.filter(c => c.type === 'battery' || c.type === 'ac');
    const nb = vsrcs.length;
    if (nb === 0) return null;
    const size = nNodes - 1 + nb;
    if (size <= 0) return null;

    const G = Array.from({ length: size }, () => new Float64Array(size));
    const I = new Float64Array(size);

    const stamp = (r, c, v) => {
      if (r >= 0 && c >= 0 && r < size && c < size) G[r][c] += v;
    };

    for (const comp of this.components) {
      if (comp.type === 'battery' || comp.type === 'ac') continue;
      const n1 = nodeOf(`${comp.x1},${comp.y1}`);
      const n2 = nodeOf(`${comp.x2},${comp.y2}`);
      let R = this._compR(comp);
      // AC overrides: inductor → |jωL|, capacitor → 1/|jωC|
      if (this._hasAC && comp.type === 'inductor') {
        const freq = this.acFreq || 2;
        const L_H  = (comp.L_value || this.L_value || 10) * 1e-3; // mH → H
        const Xl   = 2 * Math.PI * freq * L_H;
        R = Math.max(0.001, Xl);
      } else if (this._hasAC && comp.type === 'capacitor') {
        const freq = this.acFreq || 2;
        const C_F  = (comp.value || this.C_value || 100) * 1e-6;  // µF → F
        const Xc   = 1 / (2 * Math.PI * freq * C_F);
        R = Math.max(0.001, Xc);
      }
      if (R >= 1e7) continue;
      const g = 1 / R;
      stamp(n1 - 1, n1 - 1, g);
      stamp(n2 - 1, n2 - 1, g);
      stamp(n1 - 1, n2 - 1, -g);
      stamp(n2 - 1, n1 - 1, -g);
    }

    for (let b = 0; b < nb; b++) {
      const vs = vsrcs[b];
      const n1 = nodeOf(`${vs.x1},${vs.y1}`); // negative
      const n2 = nodeOf(`${vs.x2},${vs.y2}`); // positive
      const row = nNodes - 1 + b;
      const U = vs.type === 'ac'
        ? (vs.value || 9) * Math.sin(2 * Math.PI * (vs.acFreq || this.acFreq) * this._simTime)
        : (vs.value || 9);
      if (n2 > 0) { stamp(row, n2 - 1, 1);  stamp(n2 - 1, row, 1); }
      if (n1 > 0) { stamp(row, n1 - 1, -1); stamp(n1 - 1, row, -1); }
      I[row] = U;
    }

    return { G, I, nNodes, nodeOf, vsrcs, nb, size };
  }

  _gaussElim(G, I) {
    const n = I.length;
    for (let col = 0; col < n; col++) {
      let maxRow = col;
      for (let r = col + 1; r < n; r++) {
        if (Math.abs(G[r][col]) > Math.abs(G[maxRow][col])) maxRow = r;
      }
      [G[col], G[maxRow]] = [G[maxRow], G[col]];
      [I[col], I[maxRow]] = [I[maxRow], I[col]];
      if (Math.abs(G[col][col]) < 1e-12) continue;
      for (let r = col + 1; r < n; r++) {
        const f = G[r][col] / G[col][col];
        for (let c = col; c < n; c++) G[r][c] -= f * G[col][c];
        I[r] -= f * I[col];
      }
    }
    const x = new Float64Array(n);
    for (let r = n - 1; r >= 0; r--) {
      if (Math.abs(G[r][r]) < 1e-12) continue;
      x[r] = I[r];
      for (let c = r + 1; c < n; c++) x[r] -= G[r][c] * x[c];
      x[r] /= G[r][r];
    }
    return x;
  }

  _solveOnce() {
    if (this.components.length === 0) { this._solution = null; return; }
    const { nNodes, nodeOf } = this._buildNodes();
    if (nNodes < 2) { this._solution = null; return; }
    const mat = this._buildMatrix(nodeOf, nNodes);
    if (!mat) { this._solution = null; return; }
    const { G, I, vsrcs, nb } = mat;
    let x;
    try { x = this._gaussElim(G, I); } catch { this._solution = null; return; }

    const voltages = new Map();
    voltages.set(0, 0);
    for (let i = 1; i < nNodes; i++) voltages.set(i, x[i - 1] || 0);

    for (const c of this.components) {
      const n1 = nodeOf(`${c.x1},${c.y1}`);
      const n2 = nodeOf(`${c.x2},${c.y2}`);
      c._v1 = voltages.get(n1) ?? 0;
      c._v2 = voltages.get(n2) ?? 0;
      if (c.type === 'battery' || c.type === 'ac') {
        const bi = vsrcs.indexOf(c);
        c._I = bi >= 0 ? (x[nNodes - 1 + bi] || 0) : 0;
      } else {
        const R = this._compR(c);
        c._I = R < 1e7 ? (c._v1 - c._v2) / R : 0;
      }
    }
    this._solution = { solved: true, voltages };
  }

  _solve() {
    // Detect AC presence first so _buildMatrix uses correct impedances
    this._hasAC = this.components.some(c => c.type === 'ac');
    // Init diode R
    for (const c of this.components) {
      if ((c.type === 'diode' || c.type === 'led') && !this._diodeR.has(c.id)) {
        this._diodeR.set(c.id, 1e9);
      }
    }
    // Iterative diode solve (Newton-style)
    for (let iter = 0; iter < 6; iter++) {
      this._solveOnce();
      if (!this._solution?.solved) break;
      let changed = false;
      for (const c of this.components) {
        if (c.type !== 'diode' && c.type !== 'led') continue;
        const vd = (c._v1 ?? 0) - (c._v2 ?? 0);
        const oldR = this._diodeR.get(c.id) ?? 1e9;
        const newR = vd > 0.5 ? 0.5 : 1e9;
        if (newR !== oldR) { this._diodeR.set(c.id, newR); changed = true; }
      }
      if (!changed) break;
    }
    if (this.onUpdate) this.onUpdate(this.info());
  }

  /* ─── Animation ────────────────────────────────────────────────────────── */

  _tick(ts) {
    if (!this._raf) return;
    const dt = Math.min((ts - (this._lastTs || ts)) / 1000, 0.05);
    this._lastTs = ts;
    this._simTime += dt;

    if (this._hasAC) this._solveOnce(); // re-solve each frame for AC

    for (const c of this.components) {
      if (!c._I || Math.abs(c._I) < 0.001) continue;
      const speed = Math.min(Math.abs(c._I) * 0.6, 8) / (this.CELL * _compLen(c));
      c._t = ((c._t || 0) + speed * dt * 60) % 1;
    }

    if (window.LabFX) LabFX.particles.update(dt);

    // heat shimmer: emit smoke above hottest resistor every 5 frames
    if (window.LabFX && this._heatmapOn && this._solution?.solved) {
      if (!this._smokeFrame) this._smokeFrame = 0;
      this._smokeFrame++;
      if (this._smokeFrame % 5 === 0) {
        const dissipators = this.components.filter(c => c.type === 'resistor' || c.type === 'lamp');
        if (dissipators.length) {
          let hottest = dissipators[0], hotP = 0;
          for (const c of dissipators) {
            const P = Math.abs((c._I ?? 0) ** 2 * this._compR(c));
            if (P > hotP) { hotP = P; hottest = c; }
          }
          if (hotP > 0.1) {
            const p1 = this._nodePixel(hottest.x1, hottest.y1), p2 = this._nodePixel(hottest.x2, hottest.y2);
            const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
            LabFX.particles.emit({ ctx: this.ctx, x: mx, y: my - 10, count: 1, color: 'rgba(255,180,100,0.15)', speed: 8, spread: 0.5, angle: -Math.PI / 2, gravity: -50, life: 1500, shape: 'smoke', size: 8, fade: true });
          }
        }
      }
    }

    this.draw();
    if (this._oscPanel && this._oscPanel.offsetParent !== null) {
      this.drawOscilloscope(this._oscPanel);
    }
    this._raf = requestAnimationFrame(ts => this._tick(ts));
  }

  start() {
    if (this._raf) return;
    this._lastTs = null;
    this._raf = requestAnimationFrame(ts => this._tick(ts));
  }

  stop() {
    cancelAnimationFrame(this._raf);
    this._raf = null;
  }

  destroy() {
    this.stop();
    if (this._keyHandler) {
      document.removeEventListener('keydown', this._keyHandler);
      this._keyHandler = null;
    }
  }

  /* ─── Color helpers ────────────────────────────────────────────────────── */

  _voltColor(v, alpha = 1) {
    if (!this._solution?.solved) return `rgba(180,180,200,${alpha})`;
    const t = Math.tanh(v / 6);
    if (t >= 0) {
      return `rgba(${Math.round(239+t*16)},${Math.round(71-t*30)},${Math.round(111-t*80)},${alpha})`;
    } else {
      const s = -t;
      return `rgba(${Math.round(76-s*40)},${Math.round(201+s*20)},${Math.round(240-s*20)},${alpha})`;
    }
  }

  /* ─── Grid ─────────────────────────────────────────────────────────────── */

  _drawGrid(ctx) {
    for (let gx = 0; gx <= this.GW; gx++) {
      for (let gy = 0; gy <= this.GH; gy++) {
        const { x, y } = this._nodePixel(gx, gy);
        ctx.beginPath();
        ctx.arc(x, y, 1.5, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(255,255,255,0.12)';
        ctx.fill();
      }
    }
    if (this._ghostEnd) {
      const { x, y } = this._nodePixel(this._ghostEnd.x2, this._ghostEnd.y2);
      ctx.beginPath();
      ctx.arc(x, y, 4.5, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(255,255,255,0.55)';
      ctx.shadowBlur = 8; ctx.shadowColor = '#fff';
      ctx.fill(); ctx.shadowBlur = 0;
    }
  }

  /* ─── Junction dots ────────────────────────────────────────────────────── */

  _drawJunctions(ctx) {
    const count = new Map();
    for (const c of this.components) {
      const k1 = `${c.x1},${c.y1}`, k2 = `${c.x2},${c.y2}`;
      count.set(k1, (count.get(k1) || 0) + 1);
      count.set(k2, (count.get(k2) || 0) + 1);
    }
    ctx.shadowBlur = 6;
    ctx.shadowColor = '#fff';
    for (const [key, cnt] of count) {
      if (cnt < 3) continue;
      const [gx, gy] = key.split(',').map(Number);
      const { x, y } = this._nodePixel(gx, gy);
      ctx.beginPath();
      ctx.arc(x, y, 4, 0, Math.PI * 2);
      ctx.fillStyle = '#fff';
      ctx.fill();
    }
    ctx.shadowBlur = 0;
  }

  /* ─── Node voltage labels ──────────────────────────────────────────────── */

  _drawNodeLabels(ctx) {
    if (!this._solution?.solved) return;
    const shown = new Set();
    ctx.font = 'bold 8px Manrope, sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'bottom';
    for (const c of this.components) {
      for (const [gx, gy, v] of [[c.x1, c.y1, c._v1], [c.x2, c.y2, c._v2]]) {
        const key = `${gx},${gy}`;
        if (shown.has(key) || Math.abs(v) < 0.05) continue;
        shown.add(key);
        const { x, y } = this._nodePixel(gx, gy);
        ctx.fillStyle = v >= 0 ? 'rgba(239,140,140,0.75)' : 'rgba(100,190,240,0.75)';
        ctx.fillText(v.toFixed(1) + 'V', x, y - 7);
      }
    }
    ctx.textBaseline = 'alphabetic';
  }

  /* ─── Wire line ─────────────────────────────────────────────────────────── */

  _drawWireLine(ctx, p1, p2, v1, v2, lineWidth, glow) {
    if (Math.hypot(p2.x - p1.x, p2.y - p1.y) < 0.5) return;
    ctx.save();
    ctx.lineWidth = lineWidth || 3;
    ctx.lineCap = 'round';
    if (glow) { ctx.shadowBlur = 10; ctx.shadowColor = this._voltColor((v1 + v2) / 2, 1); }
    try {
      const grad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
      grad.addColorStop(0, this._voltColor(v1, 1));
      grad.addColorStop(1, this._voltColor(v2, 1));
      ctx.strokeStyle = grad;
    } catch { ctx.strokeStyle = this._voltColor((v1 + v2) / 2, 1); }
    ctx.beginPath();
    ctx.moveTo(p1.x, p1.y);
    ctx.lineTo(p2.x, p2.y);
    ctx.stroke();
    ctx.shadowBlur = 0;
    ctx.restore();
  }

  /* ─── Component draw methods ───────────────────────────────────────────── */

  _drawWire(ctx, c, p1, p2, hasI) {
    if (window.LabFX && hasI && Math.abs(c._I) > 0) {
      const intensity = Math.min(20, 6 + Math.abs(c._I) * 2);
      LabFX.glow.drawGlow(ctx, () => this._drawWireLine(ctx, p1, p2, c._v1, c._v2, 3, hasI), { color: '#06D6E0', intensity });
    } else {
      this._drawWireLine(ctx, p1, p2, c._v1, c._v2, 3, hasI);
    }
  }

  _drawResistor(ctx, c, p1, p2, mx, my, hasI) {
    const dx = p2.x - p1.x, dy = p2.y - p1.y;
    const len = Math.hypot(dx, dy);
    const ux = dx / len, uy = dy / len;
    const half = len * 0.2;
    const sP1 = { x: mx - ux*half, y: my - uy*half };
    const sP2 = { x: mx + ux*half, y: my + uy*half };
    this._drawWireLine(ctx, p1, sP1, c._v1, c._v1, 3, hasI);
    this._drawWireLine(ctx, sP2, p2, c._v2, c._v2, 3, hasI);

    // Power heat color
    const power = Math.abs((c._I ?? 0) ** 2 * this._compR(c));
    const heat  = Math.min(1, power / 5);

    ctx.save();
    ctx.translate(mx, my);
    ctx.rotate(Math.atan2(dy, dx));
    const rw = half * 2, rh = 12;
    ctx.beginPath();
    ctx.roundRect(-rw/2, -rh/2, rw, rh, 2);
    ctx.fillStyle = `rgb(${Math.round(13 + heat*200)},${Math.round(13 + heat*60)},13)`;
    ctx.fill();
    ctx.strokeStyle = this._voltColor((c._v1+c._v2)/2, 0.9);
    ctx.lineWidth = 1.5; ctx.stroke();

    // Zigzag
    ctx.beginPath();
    const zs = 6, zStep = rw / zs;
    ctx.moveTo(-rw/2, 0);
    for (let i = 0; i < zs; i++) ctx.lineTo(-rw/2+(i+0.5)*zStep, i%2===0?-4:4);
    ctx.lineTo(rw/2, 0);
    ctx.strokeStyle = this._voltColor((c._v1+c._v2)/2, 0.7);
    ctx.lineWidth = 1; ctx.stroke();
    ctx.restore();

    ctx.font = '9px Manrope,sans-serif';
    ctx.fillStyle = heat > 0.3 ? `rgba(255,${Math.round(200-heat*150)},80,0.85)` : 'rgba(255,255,255,0.55)';
    ctx.textAlign = 'center'; ctx.textBaseline = 'top';
    ctx.fillText(`R=${c.value}\u202fΩ`, mx - uy*14, my + ux*14 - 4);
    ctx.textBaseline = 'alphabetic';
  }

  _drawBattery(ctx, c, p1, p2, mx, my, hasI) {
    const dx = p2.x - p1.x, dy = p2.y - p1.y;
    const len = Math.hypot(dx, dy);
    const ux = len>0?dx/len:1, uy = len>0?dy/len:0;
    const half = Math.min(this.CELL*0.45, len*0.38);
    const sP1 = {x:mx-ux*half, y:my-uy*half};
    const sP2 = {x:mx+ux*half, y:my+uy*half};
    this._drawWireLine(ctx, p1, sP1, c._v1, c._v1, 3, hasI);
    this._drawWireLine(ctx, sP2, p2, c._v2, c._v2, 3, hasI);

    ctx.save();
    ctx.translate(mx, my);
    ctx.rotate(Math.atan2(dy, dx));
    // Two cell pairs
    for (let i = 0; i < 2; i++) {
      const ox = (i - 0.5) * 12;
      ctx.beginPath(); ctx.moveTo(ox+5,-11); ctx.lineTo(ox+5,11);
      ctx.strokeStyle='#EF476F'; ctx.lineWidth=1.5; ctx.lineCap='round'; ctx.stroke();
      ctx.beginPath(); ctx.moveTo(ox-5,-7); ctx.lineTo(ox-5,7);
      ctx.strokeStyle='#4CC9F0'; ctx.lineWidth=4; ctx.stroke();
    }
    ctx.restore();

    ctx.font = 'bold 10px Manrope,sans-serif'; ctx.textAlign='center';
    ctx.fillStyle='#EF476F';
    ctx.fillText('+', p2.x + uy*14 - ux*8, p2.y - ux*14 - uy*8);
    ctx.fillStyle='#4CC9F0';
    ctx.fillText('\u2212', p1.x + uy*14 + ux*8, p1.y - ux*14 + uy*8);
    ctx.font = '9px Manrope,sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.65)';
    ctx.textBaseline = 'bottom';
    ctx.fillText(`${c.value}\u202fV`, mx - uy*18, my + ux*18);
    ctx.textBaseline = 'alphabetic';
  }

  _drawAC(ctx, c, p1, p2, mx, my, hasI) {
    const dx = p2.x-p1.x, dy = p2.y-p1.y;
    const len = Math.hypot(dx,dy);
    const ux = dx/len, uy = dy/len;
    const r = 15;
    this._drawWireLine(ctx, p1, {x:mx-ux*r,y:my-uy*r}, c._v1,c._v1,3,hasI);
    this._drawWireLine(ctx, {x:mx+ux*r,y:my+uy*r}, p2, c._v2,c._v2,3,hasI);

    ctx.save();
    ctx.translate(mx,my); ctx.rotate(Math.atan2(dy,dx));
    ctx.beginPath(); ctx.arc(0,0,r,0,Math.PI*2);
    ctx.fillStyle='#0d0d2b'; ctx.fill();
    ctx.strokeStyle='#FFD166'; ctx.lineWidth=1.5; ctx.stroke();
    ctx.beginPath(); ctx.strokeStyle='#FFD166'; ctx.lineWidth=1.2;
    for (let i=0;i<=24;i++) {
      const t=(i/24)*2*Math.PI;
      const sx=(i/24-0.5)*r*1.5, sy=-Math.sin(t)*5;
      i===0?ctx.moveTo(sx,sy):ctx.lineTo(sx,sy);
    }
    ctx.stroke();
    ctx.restore();

    ctx.font='9px Manrope,sans-serif'; ctx.fillStyle='rgba(255,213,102,0.75)';
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.fillText(`${c.value}V ${c.acFreq||this.acFreq}Hz`, mx-uy*20, my+ux*20-4);
    ctx.textBaseline='alphabetic';
  }

  _drawSwitch(ctx, c, p1, p2, mx, my, hasI) {
    if (!c.open) {
      this._drawWireLine(ctx, p1, p2, c._v1, c._v2, 3, hasI);
    } else {
      const dx=p2.x-p1.x, dy=p2.y-p1.y;
      const len=Math.hypot(dx,dy);
      const ux=dx/len, uy=dy/len;
      const gap=this.CELL*0.35;
      const gapP1={x:mx-ux*gap,y:my-uy*gap};
      this._drawWireLine(ctx,p1,gapP1,c._v1,c._v1,3,false);
      this._drawWireLine(ctx,{x:mx+ux*gap,y:my+uy*gap},p2,c._v2,c._v2,3,false);
      // Open arm
      const armAngle=Math.PI/5, armLen=gap*2;
      const nx=-uy, ny=ux;
      ctx.save();
      ctx.strokeStyle=this._voltColor(c._v1,0.9);
      ctx.lineWidth=3; ctx.lineCap='round';
      ctx.beginPath();
      ctx.moveTo(gapP1.x, gapP1.y);
      ctx.lineTo(gapP1.x+ux*armLen*Math.cos(armAngle)-nx*armLen*Math.sin(armAngle),
                 gapP1.y+uy*armLen*Math.cos(armAngle)-ny*armLen*Math.sin(armAngle));
      ctx.stroke();
      ctx.restore();
    }
    const dx=p2.x-p1.x, dy=p2.y-p1.y;
    const len=Math.hypot(dx,dy);
    const ux=len>0?dx/len:1, uy=len>0?dy/len:0;
    ctx.font='9px Manrope,sans-serif';
    ctx.fillStyle=c.open?'rgba(239,71,111,0.7)':'rgba(76,201,240,0.7)';
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.fillText(c.open?'OPEN':'SW', mx-uy*14, my+ux*14-4);
    ctx.textBaseline='alphabetic';
  }

  _drawLamp(ctx, c, p1, p2, mx, my, hasI) {
    const dx=p2.x-p1.x, dy=p2.y-p1.y;
    const len=Math.hypot(dx,dy);
    const ux=len>0?dx/len:1, uy=len>0?dy/len:0;
    const r=10;
    this._drawWireLine(ctx,p1,{x:mx-ux*r,y:my-uy*r},c._v1,c._v1,3,hasI);
    this._drawWireLine(ctx,{x:mx+ux*r,y:my+uy*r},p2,c._v2,c._v2,3,hasI);

    const power=Math.abs((c._I||0)**2*this._compR(c));
    const bright=Math.min(1, power/3);
    ctx.save();
    if (bright>0.05) {
      const grd=ctx.createRadialGradient(mx,my,r,mx,my,r+35*bright);
      grd.addColorStop(0,`rgba(255,220,100,${bright*0.6})`);
      grd.addColorStop(1,'rgba(255,200,50,0)');
      ctx.fillStyle=grd;
      ctx.beginPath(); ctx.arc(mx,my,r+35*bright,0,Math.PI*2); ctx.fill();
    }
    ctx.beginPath(); ctx.arc(mx,my,r,0,Math.PI*2);
    if (bright>0.05) {
      const ig=ctx.createRadialGradient(mx,my,0,mx,my,r);
      ig.addColorStop(0,`rgba(255,240,180,${bright})`);
      ig.addColorStop(1,'rgba(20,20,50,0.9)');
      ctx.fillStyle=ig;
      ctx.shadowBlur=20*bright; ctx.shadowColor='#FFD166';
    } else { ctx.fillStyle='#0d0d2b'; }
    ctx.fill();
    ctx.strokeStyle=this._voltColor((c._v1+c._v2)/2,0.9);
    ctx.lineWidth=1.5; ctx.shadowBlur=0; ctx.stroke();
    const cr=r*0.6;
    ctx.strokeStyle=bright>0.05?`rgba(255,240,180,${0.4+bright*0.6})`:'rgba(255,255,255,0.4)';
    ctx.lineWidth=1.5;
    ctx.beginPath();
    ctx.moveTo(mx-cr,my-cr); ctx.lineTo(mx+cr,my+cr);
    ctx.moveTo(mx+cr,my-cr); ctx.lineTo(mx-cr,my+cr);
    ctx.stroke();
    ctx.restore();
    ctx.font='9px Manrope,sans-serif'; ctx.fillStyle='rgba(255,255,255,0.5)';
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.fillText('L', mx-uy*14, my+ux*14-4);
    if (this._solution?.solved && Math.abs(c._I)>0.001) {
      ctx.fillStyle='rgba(255,209,102,0.8)';
      ctx.fillText(`${power.toFixed(2)}W`, mx-uy*14, my+ux*14+6);
    }
    ctx.textBaseline='alphabetic';
  }

  _drawCapacitor(ctx, c, p1, p2, mx, my) {
    const dx=p2.x-p1.x, dy=p2.y-p1.y;
    const len=Math.hypot(dx,dy);
    const ux=dx/len, uy=dy/len;
    const nx=-uy, ny=ux;
    const pg=6, ph=12; // plate gap half, plate half-length
    const sP1={x:mx-ux*pg,y:my-uy*pg};
    const sP2={x:mx+ux*pg,y:my+uy*pg};
    this._drawWireLine(ctx,p1,sP1,c._v1,c._v1,3,false);
    this._drawWireLine(ctx,sP2,p2,c._v2,c._v2,3,false);

    // Plates
    ctx.save();
    ctx.lineWidth=2.5; ctx.lineCap='round';
    ctx.strokeStyle=this._voltColor(c._v1,0.9);
    ctx.beginPath(); ctx.moveTo(sP1.x-nx*ph,sP1.y-ny*ph); ctx.lineTo(sP1.x+nx*ph,sP1.y+ny*ph); ctx.stroke();
    ctx.strokeStyle=this._voltColor(c._v2,0.9);
    ctx.beginPath(); ctx.moveTo(sP2.x-nx*ph,sP2.y-ny*ph); ctx.lineTo(sP2.x+nx*ph,sP2.y+ny*ph); ctx.stroke();

    // Charge fill
    const vd=Math.abs((c._v2||0)-(c._v1||0));
    if (vd>0.1 && this._solution?.solved) {
      const alpha=Math.min(0.35, vd/12);
      const cg=ctx.createLinearGradient(sP1.x,sP1.y,sP2.x,sP2.y);
      cg.addColorStop(0,this._voltColor(c._v1,alpha));
      cg.addColorStop(1,this._voltColor(c._v2,alpha));
      ctx.fillStyle=cg;
      ctx.beginPath();
      ctx.moveTo(sP1.x-nx*ph,sP1.y-ny*ph); ctx.lineTo(sP1.x+nx*ph,sP1.y+ny*ph);
      ctx.lineTo(sP2.x+nx*ph,sP2.y+ny*ph); ctx.lineTo(sP2.x-nx*ph,sP2.y-ny*ph);
      ctx.closePath(); ctx.fill();
    }
    ctx.restore();

    ctx.font='9px Manrope,sans-serif'; ctx.fillStyle='rgba(255,255,255,0.55)';
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.fillText(`C=${c.value}µF`, mx-uy*16, my+ux*16-4);
    ctx.textBaseline='alphabetic';
  }

  _drawInductor(ctx, c, p1, p2, mx, my, hasI) {
    const dx=p2.x-p1.x, dy=p2.y-p1.y;
    const len=Math.hypot(dx,dy);
    const ux=dx/len, uy=dy/len;
    const nx=-uy, ny=ux;
    const hw=18; // half-width of coil region
    const sP1={x:mx-ux*hw, y:my-uy*hw};
    const sP2={x:mx+ux*hw, y:my+uy*hw};
    this._drawWireLine(ctx, p1, sP1, c._v1, c._v1, 3, hasI);
    this._drawWireLine(ctx, sP2, p2, c._v2, c._v2, 3, hasI);

    // 3 half-circle loops
    ctx.save();
    ctx.translate(mx, my);
    ctx.rotate(Math.atan2(dy, dx));
    const loopR=6, loopN=3;
    const totalW=loopN*loopR*2;
    ctx.strokeStyle=this._voltColor((c._v1+c._v2)/2, 0.9);
    ctx.lineWidth=2; ctx.lineCap='round';
    for (let i=0;i<loopN;i++) {
      const cx=-totalW/2+loopR+i*loopR*2;
      ctx.beginPath();
      ctx.arc(cx, 0, loopR, Math.PI, 0, false);
      ctx.stroke();
    }
    ctx.restore();

    const L_mH = c.L_value ?? this.L_value ?? 10;
    ctx.font='9px Manrope,sans-serif'; ctx.fillStyle='rgba(200,160,255,0.75)';
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.fillText(`L=${L_mH} мГн`, mx-uy*16, my+ux*16-4);
    ctx.textBaseline='alphabetic';
  }

  _drawDiode(ctx, c, p1, p2, mx, my, hasI) {
    const dx=p2.x-p1.x, dy=p2.y-p1.y;
    const len=Math.hypot(dx,dy);
    const ux=dx/len, uy=dy/len;
    const tw=10, th=11;
    this._drawWireLine(ctx,p1,{x:mx-ux*tw,y:my-uy*tw},c._v1,c._v1,3,hasI);
    this._drawWireLine(ctx,{x:mx+ux*tw,y:my+uy*tw},p2,c._v2,c._v2,3,hasI);

    const on=(this._diodeR.get(c.id)??1e9)<1;
    const col=on?'#7BF5A4':'rgba(255,255,255,0.65)';

    ctx.save();
    ctx.translate(mx,my); ctx.rotate(Math.atan2(dy,dx));
    ctx.beginPath();
    ctx.moveTo(-tw,-th); ctx.lineTo(-tw,th); ctx.lineTo(tw,0); ctx.closePath();
    ctx.fillStyle=on?'rgba(123,245,164,0.25)':'rgba(255,255,255,0.06)';
    ctx.fill();
    ctx.strokeStyle=col; ctx.lineWidth=1.6; ctx.stroke();
    ctx.beginPath(); ctx.moveTo(tw,-th); ctx.lineTo(tw,th); ctx.stroke();
    ctx.restore();

    ctx.font='9px Manrope,sans-serif';
    ctx.fillStyle=on?'rgba(123,245,164,0.8)':'rgba(255,255,255,0.4)';
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.fillText(on?'ON':'OFF', mx-uy*16, my+ux*16-4);
    ctx.textBaseline='alphabetic';
  }

  _drawLED(ctx, c, p1, p2, mx, my, hasI) {
    const dx=p2.x-p1.x, dy=p2.y-p1.y;
    const len=Math.hypot(dx,dy);
    const ux=dx/len, uy=dy/len;
    const tw=9, th=10;
    this._drawWireLine(ctx,p1,{x:mx-ux*tw,y:my-uy*tw},c._v1,c._v1,3,hasI);
    this._drawWireLine(ctx,{x:mx+ux*tw,y:my+uy*tw},p2,c._v2,c._v2,3,hasI);

    const on=(this._diodeR.get(c.id)??1e9)<1;
    const col=c.ledColor||this.ledColor;

    const drawLEDBody = () => {
      if (on) {
        const grd=ctx.createRadialGradient(mx,my,0,mx,my,38);
        grd.addColorStop(0,col+'50'); grd.addColorStop(1,col+'00');
        ctx.fillStyle=grd;
        ctx.beginPath(); ctx.arc(mx,my,38,0,Math.PI*2); ctx.fill();
      }
      ctx.save();
      ctx.translate(mx,my); ctx.rotate(Math.atan2(dy,dx));
      ctx.beginPath();
      ctx.moveTo(-tw,-th); ctx.lineTo(-tw,th); ctx.lineTo(tw,0); ctx.closePath();
      ctx.fillStyle=on?col+'55':col+'18'; ctx.fill();
      ctx.strokeStyle=on?col:col+'90'; ctx.lineWidth=1.5; ctx.stroke();
      ctx.beginPath(); ctx.moveTo(tw,-th); ctx.lineTo(tw,th); ctx.stroke();
      if (on) {
        ctx.strokeStyle=col; ctx.lineWidth=1.2;
        for (let s=-1;s<=1;s+=2) {
          ctx.beginPath();
          ctx.moveTo(tw+3, s*5);
          ctx.lineTo(tw+11, s*5-s*6);
          ctx.stroke();
          ctx.beginPath();
          ctx.moveTo(tw+11,s*5-s*6); ctx.lineTo(tw+8,s*5-s*6);
          ctx.moveTo(tw+11,s*5-s*6); ctx.lineTo(tw+11,s*5-s*3);
          ctx.stroke();
        }
      }
      ctx.restore();
    };

    if (window.LabFX && on) {
      LabFX.glow.drawGlow(ctx, drawLEDBody, { color: col, intensity: 18, layers: 2 });
    } else {
      drawLEDBody();
    }

    ctx.font='9px Manrope,sans-serif'; ctx.fillStyle=col+'cc';
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.fillText('LED', mx-uy*16, my+ux*16-4);
    ctx.textBaseline='alphabetic';
  }

  _drawAmmeter(ctx, c, p1, p2, mx, my) {
    const dx=p2.x-p1.x, dy=p2.y-p1.y, len=Math.hypot(dx,dy);
    const ux=len>0?dx/len:1, uy=len>0?dy/len:0, r=9;
    this._drawWireLine(ctx,p1,{x:mx-ux*r,y:my-uy*r},c._v1,c._v1,3,false);
    this._drawWireLine(ctx,{x:mx+ux*r,y:my+uy*r},p2,c._v2,c._v2,3,false);
    ctx.beginPath(); ctx.arc(mx,my,r,0,Math.PI*2);
    ctx.fillStyle='#0d0d2b'; ctx.fill();
    ctx.strokeStyle='rgba(76,201,240,0.9)'; ctx.lineWidth=1.5; ctx.stroke();
    ctx.font='bold 9px Manrope,sans-serif'; ctx.fillStyle='#4CC9F0';
    ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('A',mx,my);
    if (this._solution?.solved) {
      ctx.font='8px Manrope,sans-serif'; ctx.fillStyle='rgba(255,255,255,0.55)';
      ctx.textBaseline='top'; ctx.fillText(`${(c._I||0).toFixed(3)}A`,mx,my+r+3);
    }
    ctx.textBaseline='alphabetic';
  }

  _drawVoltmeter(ctx, c, p1, p2, mx, my) {
    const dx=p2.x-p1.x, dy=p2.y-p1.y, len=Math.hypot(dx,dy);
    const ux=len>0?dx/len:1, uy=len>0?dy/len:0, r=9;
    this._drawWireLine(ctx,p1,{x:mx-ux*r,y:my-uy*r},c._v1,c._v1,2,false);
    this._drawWireLine(ctx,{x:mx+ux*r,y:my+uy*r},p2,c._v2,c._v2,2,false);
    ctx.beginPath(); ctx.arc(mx,my,r,0,Math.PI*2);
    ctx.fillStyle='#0d0d2b'; ctx.fill();
    ctx.strokeStyle='rgba(239,71,111,0.9)'; ctx.lineWidth=1.5; ctx.stroke();
    ctx.font='bold 9px Manrope,sans-serif'; ctx.fillStyle='#EF476F';
    ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('V',mx,my);
    if (this._solution?.solved) {
      ctx.font='8px Manrope,sans-serif'; ctx.fillStyle='rgba(255,255,255,0.55)';
      ctx.textBaseline='top'; ctx.fillText(`${((c._v1||0)-(c._v2||0)).toFixed(2)}V`,mx,my+r+3);
    }
    ctx.textBaseline='alphabetic';
  }

  /* ─── Animated current (comet trail) ──────────────────────────────────── */

  _drawAnimDots(ctx) {
    if (!this._solution?.solved) return;
    for (const c of this.components) {
      if (!c._I || Math.abs(c._I) < 0.05) continue;
      const p1=this._nodePixel(c.x1,c.y1), p2=this._nodePixel(c.x2,c.y2);
      const N=Math.max(1,Math.min(5,Math.ceil(Math.abs(c._I))));
      const dir=c._I>0?1:-1;
      for (let i=0;i<N;i++) {
        const phase=((c._t||0)+i/N)%1;
        const tt=dir>0?phase:1-phase;
        // Comet trail: 5 fading dots
        for (let tr=0;tr<5;tr++) {
          const trailT=dir>0?Math.max(0,tt-tr*0.035):Math.min(1,tt+tr*0.035);
          const tx=p1.x+(p2.x-p1.x)*trailT;
          const ty=p1.y+(p2.y-p1.y)*trailT;
          const alpha=(1-tr/5)*(tr===0?1:0.5);
          const sz=Math.max(0, tr===0?3:2.5-tr*0.4);
          ctx.beginPath(); ctx.arc(tx,ty,sz,0,Math.PI*2);
          if (tr===0) {
            ctx.fillStyle='#FFD166';
            ctx.shadowColor='#FFD166'; ctx.shadowBlur=8;
          } else {
            ctx.shadowBlur=0;
            ctx.fillStyle=`rgba(255,209,102,${alpha})`;
          }
          ctx.fill();
        }
        ctx.shadowBlur=0;
      }
    }
  }

  /* ─── Components dispatch ──────────────────────────────────────────────── */

  _drawComponents(ctx) {
    for (const c of this.components) {
      const p1=this._nodePixel(c.x1,c.y1), p2=this._nodePixel(c.x2,c.y2);
      const mx=(p1.x+p2.x)/2, my=(p1.y+p2.y)/2;
      const isHov=this._hovered===c.id, isSel=this._selected===c.id;
      const hasI=!!(this._solution?.solved && Math.abs(c._I)>0.001);

      // Selection / hover highlight
      if (isSel || isHov) {
        ctx.save();
        ctx.globalAlpha=isSel?0.28:0.15;
        ctx.fillStyle=isSel?'#FFD166':'#fff';
        const bx=Math.min(p1.x,p2.x)-10, by=Math.min(p1.y,p2.y)-10;
        const bw=Math.abs(p2.x-p1.x)+20, bh=Math.abs(p2.y-p1.y)+20;
        ctx.beginPath(); ctx.roundRect(bx,by,Math.max(bw,20),Math.max(bh,20),5); ctx.fill();
        ctx.restore();
      }

      switch (c.type) {
        case 'wire':      this._drawWire(ctx,c,p1,p2,hasI);              break;
        case 'resistor':  this._drawResistor(ctx,c,p1,p2,mx,my,hasI);   break;
        case 'battery':   this._drawBattery(ctx,c,p1,p2,mx,my,hasI);    break;
        case 'ac':        this._drawAC(ctx,c,p1,p2,mx,my,hasI);         break;
        case 'switch':    this._drawSwitch(ctx,c,p1,p2,mx,my,hasI);     break;
        case 'lamp':      this._drawLamp(ctx,c,p1,p2,mx,my,hasI);       break;
        case 'capacitor': this._drawCapacitor(ctx,c,p1,p2,mx,my);       break;
        case 'inductor':  this._drawInductor(ctx,c,p1,p2,mx,my,hasI);  break;
        case 'diode':     this._drawDiode(ctx,c,p1,p2,mx,my,hasI);      break;
        case 'led':       this._drawLED(ctx,c,p1,p2,mx,my,hasI);        break;
        case 'ammeter':   this._drawAmmeter(ctx,c,p1,p2,mx,my);         break;
        case 'voltmeter': this._drawVoltmeter(ctx,c,p1,p2,mx,my);       break;
      }
    }
  }

  /* ─── Ghost while drawing ──────────────────────────────────────────────── */

  _drawGhost(ctx) {
    if (!this._drawing || !this._ghostEnd) return;
    const p1=this._nodePixel(this._drawing.x1,this._drawing.y1);
    const g2=this._ghostEnd;
    ctx.save();
    ctx.globalAlpha=0.45; ctx.strokeStyle='#FFD166';
    ctx.lineWidth=3; ctx.lineCap='round'; ctx.setLineDash([6,4]);

    if (this.addMode==='wire' && this._drawing.x1!==g2.x2 && this._drawing.y1!==g2.y2) {
      const corner=this._nodePixel(g2.x2, this._drawing.y1);
      const p2=this._nodePixel(g2.x2,g2.y2);
      ctx.beginPath(); ctx.moveTo(p1.x,p1.y); ctx.lineTo(corner.x,corner.y); ctx.lineTo(p2.x,p2.y); ctx.stroke();
    } else {
      const p2=this._nodePixel(g2.x2,g2.y2);
      ctx.beginPath(); ctx.moveTo(p1.x,p1.y); ctx.lineTo(p2.x,p2.y); ctx.stroke();
    }
    ctx.setLineDash([]);
    ctx.beginPath(); ctx.arc(p1.x,p1.y,5,0,Math.PI*2);
    ctx.fillStyle='#FFD166'; ctx.globalAlpha=0.75; ctx.fill();
    ctx.restore();
  }

  /* ─── Tooltip ──────────────────────────────────────────────────────────── */

  _drawTooltip(ctx) {
    if (!this._hovered || !this._solution?.solved) return;
    const c=this.components.find(x=>x.id===this._hovered);
    if (!c) return;
    const p1=this._nodePixel(c.x1,c.y1), p2=this._nodePixel(c.x2,c.y2);
    const mx=(p1.x+p2.x)/2, my=(p1.y+p2.y)/2;
    const R=this._compR(c), I=c._I??0;
    const U=Math.abs((c._v1??0)-(c._v2??0));
    const P=Math.abs(I*I*Math.min(R,1e6));
    const lines=[];
    if (c.type==='resistor'||c.type==='lamp'||c.type==='wire')
      lines.push(`R = ${R<1?R.toFixed(4):R.toFixed(1)} Ω`);
    if (Math.abs(I)>0.0001) lines.push(`I = ${I.toFixed(4)} А`);
    if (U>0.001) lines.push(`U = ${U.toFixed(3)} В`);
    if (P>0.0001&&P<1e5) lines.push(`P = ${P.toFixed(4)} Вт`);
    if (!lines.length) return;

    const lh=14, pad=7, tw=96, th=lines.length*lh+pad*2;
    let tx=mx+18, ty=my-th/2;
    if (tx+tw>this.W-4) tx=mx-tw-18;
    if (ty<4) ty=4;
    if (ty+th>this.H-4) ty=this.H-th-4;

    ctx.fillStyle='rgba(6,6,22,0.93)';
    ctx.strokeStyle='rgba(255,255,255,0.14)';
    ctx.lineWidth=1;
    ctx.beginPath(); ctx.roundRect(tx,ty,tw,th,5); ctx.fill(); ctx.stroke();
    ctx.font='10px Manrope,sans-serif'; ctx.textAlign='left'; ctx.textBaseline='top';
    lines.forEach((l,i)=>{
      ctx.fillStyle='rgba(255,255,255,0.78)';
      ctx.fillText(l, tx+pad, ty+pad+i*lh);
    });
    ctx.textBaseline='alphabetic';
  }

  /* ─── Short circuit warning ────────────────────────────────────────────── */

  _drawShortCircuitWarning(ctx) {
    if (!this._solution?.solved) return;
    const batt=this.components.find(c=>c.type==='battery'||c.type==='ac');
    if (!batt||Math.abs(batt._I)<50) return;
    const a=0.12+0.08*Math.sin(this._simTime*12);
    ctx.fillStyle=`rgba(239,71,111,${a})`;
    ctx.fillRect(0,0,this.W,this.H);
    ctx.fillStyle='rgba(239,71,111,0.92)';
    ctx.font='bold 18px Manrope,sans-serif';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText('Короткое замыкание!', this.W/2, this.H/2);
    ctx.textBaseline='alphabetic';
    if (window.LabFX) {
      const now4 = performance.now();
      if (!this._shortFXt || now4 - this._shortFXt > 600) {
        this._shortFXt = now4;
        const p1=this._nodePixel(batt.x1,batt.y1), p2=this._nodePixel(batt.x2,batt.y2);
        const sx=(p1.x+p2.x)/2, sy=(p1.y+p2.y)/2;
        LabFX.particles.emit({ ctx, x: sx, y: sy, count: 12, color: '#EF476F', speed: 80, spread: Math.PI*2, life: 300, shape: 'spark', size: 3, glow: true });
        LabFX.sound.play('spark');
        LabFX.shake(this.canvas, { intensity: 5, durMs: 200 });
      }
    }
  }

  /* ─── Hint ─────────────────────────────────────────────────────────────── */

  _drawHint(ctx) {
    ctx.font='15px Manrope,sans-serif'; ctx.fillStyle='rgba(255,255,255,0.18)';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText('Выберите инструмент и нарисуйте схему', this.W/2, this.H/2-14);
    ctx.font='11px Manrope,sans-serif'; ctx.fillStyle='rgba(255,255,255,0.09)';
    ctx.fillText('ПКМ — удалить · Dbl‑клик на ключе — переключить · Del — удалить выбранный', this.W/2, this.H/2+12);
    ctx.font='10px Manrope,sans-serif'; ctx.fillStyle='rgba(255,255,255,0.07)';
    ctx.fillText('Ctrl+Z / Ctrl+Y — Undo/Redo  ·  W R B S L C I D A V E — горячие клавиши', this.W/2, this.H/2+30);
    ctx.textBaseline='alphabetic';
  }

  /* ─── Power heatmap overlay ─────────────────────────────────────────────── */

  _drawHeatmap(ctx) {
    if (!this._heatmapOn || !this._solution?.solved) return;
    // Compute power for each dissipating component
    const dissipators = this.components.filter(c =>
      c.type === 'resistor' || c.type === 'lamp' || c.type === 'led' ||
      c.type === 'diode' || c.type === 'inductor'
    );
    if (!dissipators.length) return;
    const powers = dissipators.map(c => {
      const R = this._compR(c);
      return Math.abs((c._I ?? 0) ** 2 * Math.min(R, 1e6));
    });
    const Pmax = Math.max(...powers, 1e-9);

    for (let i = 0; i < dissipators.length; i++) {
      const c = dissipators[i];
      const p1 = this._nodePixel(c.x1, c.y1), p2 = this._nodePixel(c.x2, c.y2);
      const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
      const t = powers[i] / Pmax; // 0..1
      if (t < 0.01) continue;
      // Color: cool blue (t=0) → orange (t=0.5) → red (t=1)
      const r = Math.round(30 + t * 225);
      const g = Math.round(100 - t * 100);
      const b = Math.round(220 - t * 220);
      const radius = 28 + t * 24;
      const grd = ctx.createRadialGradient(mx, my, 0, mx, my, radius);
      grd.addColorStop(0, `rgba(${r},${g},${b},${(0.18 + t * 0.32).toFixed(2)})`);
      grd.addColorStop(1, `rgba(${r},${g},${b},0)`);
      ctx.beginPath();
      ctx.arc(mx, my, radius, 0, Math.PI * 2);
      ctx.fillStyle = grd;
      ctx.fill();
    }
  }

  /* ─── Oscilloscope render ────────────────────────────────────────────────── */

  drawOscilloscope(oscCanvas) {
    if (!oscCanvas) return;
    const W = oscCanvas.width  || 300;
    const H = oscCanvas.height || 180;
    const ctx = oscCanvas.getContext('2d');
    ctx.clearRect(0, 0, W, H);
    ctx.fillStyle = '#06060e';
    ctx.fillRect(0, 0, W, H);

    // Grid lines
    ctx.strokeStyle = 'rgba(255,255,255,0.07)';
    ctx.lineWidth = 1;
    for (let x = 0; x <= W; x += W / 6) {
      ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
    }
    for (let y = 0; y <= H; y += H / 4) {
      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
    }
    // Axis labels
    ctx.font = '8px Manrope,sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.3)';
    ctx.textAlign = 'left'; ctx.fillText('U (В)', 4, 10);
    ctx.textAlign = 'right'; ctx.fillText('I (А)', W - 4, 10);

    const sel = this._selected !== null ? this.components.find(c => c.id === this._selected) : null;
    if (!sel || !this._solution?.solved) {
      ctx.font = '11px Manrope,sans-serif';
      ctx.fillStyle = 'rgba(255,255,255,0.28)';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('Выбери компонент', W / 2, H / 2);
      ctx.textBaseline = 'alphabetic';
      return;
    }

    const N = 100;
    const Uvals = new Float64Array(N);
    const Ivals = new Float64Array(N);

    if (this._hasAC) {
      // Compute 2 periods of AC signal
      const freq = this.acFreq || 2;
      const T = 1 / freq;
      const tSpan = 2 * T;
      const savedTime = this._simTime;
      for (let k = 0; k < N; k++) {
        this._simTime = savedTime + (k / (N - 1)) * tSpan - tSpan / 2;
        this._solveOnce();
        const c = this.components.find(cc => cc.id === sel.id);
        if (c) {
          Uvals[k] = (c._v1 ?? 0) - (c._v2 ?? 0);
          Ivals[k] = c._I ?? 0;
        }
      }
      this._simTime = savedTime;
      this._solveOnce(); // restore
    } else {
      // DC: horizontal lines
      const U0 = (sel._v1 ?? 0) - (sel._v2 ?? 0);
      const I0 = sel._I ?? 0;
      Uvals.fill(U0); Ivals.fill(I0);
    }

    const Umax = Math.max(...Uvals.map(Math.abs), 1e-9);
    const Imax = Math.max(...Ivals.map(Math.abs), 1e-9);
    const padY = 18, innerH = H - padY * 2;

    const drawLine = (vals, maxVal, color) => {
      ctx.beginPath();
      ctx.strokeStyle = color;
      ctx.lineWidth = 1.5;
      for (let k = 0; k < N; k++) {
        const px = (k / (N - 1)) * W;
        const py = padY + innerH / 2 - (vals[k] / maxVal) * (innerH / 2 - 2);
        k === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
      }
      ctx.stroke();
    };

    drawLine(Uvals, Umax, '#a78bfa'); // violet for U
    drawLine(Ivals, Imax, '#22d3ee'); // cyan for I

    // Scale labels
    ctx.font = '8px Manrope,sans-serif'; ctx.textBaseline = 'middle';
    ctx.fillStyle = '#a78bfa'; ctx.textAlign = 'left';
    ctx.fillText(Umax.toFixed(2) + 'В', 4, padY);
    ctx.fillStyle = '#22d3ee'; ctx.textAlign = 'right';
    ctx.fillText(Imax.toFixed(3) + 'А', W - 4, padY);
    ctx.textBaseline = 'alphabetic';
  }

  /* ─── Main draw ────────────────────────────────────────────────────────── */

  draw() {
    const ctx=this.ctx, W=this.W, H=this.H;
    if (!W||!H) return;
    ctx.clearRect(0,0,W,H);
    ctx.fillStyle='#080818'; ctx.fillRect(0,0,W,H);
    this._drawGrid(ctx);
    this._drawHeatmap(ctx);
    this._drawComponents(ctx);
    this._drawJunctions(ctx);
    this._drawNodeLabels(ctx);
    this._drawAnimDots(ctx);
    this._drawShortCircuitWarning(ctx);
    if (this._drawing&&this._ghostEnd) this._drawGhost(ctx);
    this._drawTooltip(ctx);
    if (this.components.length===0) this._drawHint(ctx);
    if (window.LabFX) LabFX.particles.draw(ctx);
  }

  /* ─── Events ───────────────────────────────────────────────────────────── */

  _bindEvents() {
    const cvs=this.canvas;

    const pos = e => {
      const r=cvs.getBoundingClientRect(), s=e.touches?e.touches[0]:e;
      return { x:s.clientX-r.left, y:s.clientY-r.top };
    };
    const snap = p => this._snapGrid(p.x,p.y);
    const hitComp = p => {
      for (let i=this.components.length-1;i>=0;i--) {
        const c=this.components[i];
        const q1=this._nodePixel(c.x1,c.y1), q2=this._nodePixel(c.x2,c.y2);
        if (distToSegment(p.x,p.y,q1.x,q1.y,q2.x,q2.y)<13) return i;
      }
      return -1;
    };

    // ── Keyboard shortcuts ──
    this._keyHandler = e => {
      const simEl=document.getElementById('sim-circuit');
      if (!simEl||simEl.style.display==='none') return;
      if (e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return;

      if ((e.ctrlKey||e.metaKey)&&e.key==='z') { e.preventDefault(); this.undo(); return; }
      if ((e.ctrlKey||e.metaKey)&&(e.key==='y'||(e.shiftKey&&e.key==='z'))) { e.preventDefault(); this.redo(); return; }

      const modeMap={w:'wire',r:'resistor',b:'battery',s:'switch',l:'lamp',c:'capacitor',i:'inductor',d:'diode',a:'ammeter',v:'voltmeter',e:'erase'};
      const newMode=modeMap[e.key.toLowerCase()];
      if (newMode) { this.addMode=newMode; if (this.onModeChange) this.onModeChange(newMode); }

      if ((e.key==='Delete'||e.key==='Backspace')&&this._selected!==null) {
        const idx=this.components.findIndex(c=>c.id===this._selected);
        if (idx>=0) { this._pushHistory(); this.components.splice(idx,1); this._selected=null; this._solve(); this.draw(); }
      }
      if (e.key==='Escape') {
        this._selected=null; this._drawing=null; this._ghostEnd=null; this._dragIdx=null;
        if (!this._raf) this.draw();
      }
    };
    document.addEventListener('keydown', this._keyHandler);

    // ── Mouse ──
    cvs.addEventListener('mousedown', e=>{
      if (e.button!==0) return;
      const p=pos(e), g=snap(p), hi=hitComp(p);

      if (this.addMode==='erase') {
        if (hi>=0) { this._pushHistory(); this.components.splice(hi,1); this._solve(); this.draw(); if (window.LabFX) LabFX.sound.play('fizz', { volume: 0.3 }); }
        return;
      }
      if (hi>=0) {
        // Start potential drag / select
        this._selected=this.components[hi].id;
        this._dragIdx=hi;
        this._dragStart=g;
        this._dragOrigPos={...this.components[hi]};
        this._didDrag=false;
        this.draw(); return;
      }
      this._selected=null;
      this._drawing={x1:g.gx,y1:g.gy};
      this._ghostEnd={x2:g.gx,y2:g.gy};
    });

    cvs.addEventListener('mousemove', e=>{
      const p=pos(e), g=snap(p);
      this._ghostEnd={x2:g.gx,y2:g.gy};
      const hi=hitComp(p);
      this._hovered=hi>=0?this.components[hi].id:null;

      if (this._dragIdx!==null&&this._dragStart) {
        const dx=g.gx-this._dragStart.gx, dy=g.gy-this._dragStart.gy;
        if (dx!==0||dy!==0) {
          this._didDrag=true;
          const c=this.components[this._dragIdx], o=this._dragOrigPos;
          c.x1=Math.max(0,Math.min(this.GW,o.x1+dx));
          c.y1=Math.max(0,Math.min(this.GH,o.y1+dy));
          c.x2=Math.max(0,Math.min(this.GW,o.x2+dx));
          c.y2=Math.max(0,Math.min(this.GH,o.y2+dy));
          this._solve();
        }
      }
      if (!this._raf) this.draw();
    });

    cvs.addEventListener('mouseup', e=>{
      if (e.button!==0) return;

      if (this._dragIdx!==null) {
        if (this._didDrag) this._pushHistory();
        this._dragIdx=null; this._dragStart=null; this._dragOrigPos=null; this._didDrag=false;
        if (!this._raf) this.draw(); return;
      }
      if (!this._drawing) return;
      const p=pos(e), g=snap(p);
      const {x1,y1}=this._drawing;
      const x2=g.gx, y2=g.gy;
      this._drawing=null; this._ghostEnd=null;

      if (x1===x2&&y1===y2) { if (!this._raf) this.draw(); return; }
      this._pushHistory();

      // L-shape wires: split diagonal into two orthogonal segments
      if (this.addMode==='wire'&&x1!==x2&&y1!==y2) {
        this._add('wire',x1,y1,x2,y1);
        this._add('wire',x2,y1,x2,y2);
      } else {
        this.addComponent(this.addMode,x1,y1,x2,y2);
        return;
      }
      this._solve(); this.draw();
      if (this.onUpdate) this.onUpdate(this.info());
    });

    cvs.addEventListener('contextmenu', e=>{
      e.preventDefault();
      const p=pos(e), i=hitComp(p);
      if (i>=0) { this._pushHistory(); this.components.splice(i,1); this._selected=null; this._solve(); this.draw(); if (window.LabFX) LabFX.sound.play('fizz', { volume: 0.3 }); }
    });

    cvs.addEventListener('dblclick', e=>{
      const p=pos(e), i=hitComp(p);
      if (i>=0&&this.components[i].type==='switch') {
        this._pushHistory(); this.components[i].open=!this.components[i].open; this._solve(); this.draw();
        if (window.LabFX) LabFX.sound.play('click', { pitch: 1.5 });
      }
    });

    cvs.addEventListener('mouseleave', ()=>{
      this._ghostEnd=null; this._drawing=null; this._hovered=null;
      if (this._dragIdx!==null) { this._dragIdx=null; this._dragStart=null; this._dragOrigPos=null; this._didDrag=false; }
      if (!this._raf) this.draw();
    });

    // Touch
    cvs.addEventListener('touchstart', e=>{
      e.preventDefault();
      const p=pos(e), g=snap(p);
      if (this.addMode==='erase') {
        const i=this._hitCompFromSnap(p);
        if (i>=0) { this._pushHistory(); this.components.splice(i,1); this._solve(); this.draw(); }
        return;
      }
      this._drawing={x1:g.gx,y1:g.gy}; this._ghostEnd={x2:g.gx,y2:g.gy};
    },{passive:false});

    cvs.addEventListener('touchmove', e=>{
      e.preventDefault();
      const p=pos(e), g=snap(p);
      this._ghostEnd={x2:g.gx,y2:g.gy};
      if (!this._raf) this.draw();
    },{passive:false});

    cvs.addEventListener('touchend', e=>{
      e.preventDefault();
      if (!this._drawing) return;
      const {x1,y1}=this._drawing;
      const x2=this._ghostEnd?.x2??x1, y2=this._ghostEnd?.y2??y1;
      this._drawing=null; this._ghostEnd=null;
      if (x1===x2&&y1===y2) { if (!this._raf) this.draw(); return; }
      this._pushHistory();
      if (this.addMode==='wire'&&x1!==x2&&y1!==y2) {
        this._add('wire',x1,y1,x2,y1); this._add('wire',x2,y1,x2,y2);
        this._solve(); this.draw(); if (this.onUpdate) this.onUpdate(this.info());
      } else {
        this.addComponent(this.addMode,x1,y1,x2,y2);
      }
    },{passive:false});
  }

  /* ─── CRUD ─────────────────────────────────────────────────────────────── */

  addComponent(type, x1, y1, x2, y2) {
    const value = type==='resistor'?this.R_value
      : type==='battery'||type==='ac'?this.U_value
      : type==='capacitor'?this.C_value
      : undefined;
    const L_value = type==='inductor' ? this.L_value : undefined;
    this._add(type,x1,y1,x2,y2,value,L_value);
    if (window.LabFX) LabFX.sound.play('click', { pitch: 0.9 });
    this._solve(); this.draw();
    if (this.onUpdate) this.onUpdate(this.info());
  }

  _add(type, x1, y1, x2, y2, value, L_value) {
    const id=this._nextId++;
    if (type==='diode'||type==='led') this._diodeR.set(id,1e9);
    this.components.push({
      id, type, x1, y1, x2, y2,
      value: value??undefined,
      L_value: L_value??undefined,
      open: false,
      ledColor: type==='led'?(this.ledColor||'#7BF5A4'):undefined,
      acFreq:   type==='ac'?this.acFreq:undefined,
      _I:0, _v1:0, _v2:0, _t:Math.random()
    });
  }

  /* ─── Presets ──────────────────────────────────────────────────────────── */

  preset(name) {
    this.components=[]; this._nextId=0; this._diodeR.clear(); this._selected=null;

    switch (name) {
      case 'serial':
        this._add('battery',1,7,1,4,9);
        this._add('wire',1,4,8,4);
        this._add('resistor',8,4,13,4,10);
        this._add('resistor',13,4,19,4,20);
        this._add('wire',19,4,19,7);
        this._add('wire',19,7,1,7);
        break;

      case 'parallel':
        this._add('battery',1,7,1,4,12);
        this._add('wire',1,4,8,4);
        this._add('wire',8,4,8,3);
        this._add('resistor',8,3,16,3,10);
        this._add('wire',16,3,16,4);
        this._add('wire',8,4,8,6);
        this._add('resistor',8,6,16,6,20);
        this._add('wire',16,6,16,4);
        this._add('wire',16,4,19,4);
        this._add('wire',19,4,19,7);
        this._add('wire',19,7,1,7);
        break;

      case 'lamp':
        this._add('battery',2,8,2,4,9);
        this._add('wire',2,4,8,4);
        this._add('switch',8,4,13,4);
        this._add('lamp',13,4,18,4);
        this._add('wire',18,4,20,4);
        this._add('wire',20,4,20,8);
        this._add('wire',20,8,2,8);
        break;

      case 'divider':
        this._add('battery',2,7,2,4,12);
        this._add('wire',2,4,9,4);
        this._add('resistor',9,4,14,4,10);
        this._add('resistor',14,4,19,4,10);
        this._add('wire',19,4,19,7);
        this._add('wire',19,7,2,7);
        this._add('voltmeter',14,4,14,7);
        this._add('wire',14,7,19,7);
        break;

      case 'bridge':
        this._add('battery',1,7,1,4,12);
        this._add('wire',1,4,5,4);
        this._add('resistor',5,4,10,2,10);
        this._add('resistor',5,4,10,6,20);
        this._add('resistor',10,2,16,4,10);
        this._add('resistor',10,6,16,4,30);
        this._add('ammeter',10,2,10,6);
        this._add('wire',16,4,19,4);
        this._add('wire',19,4,19,7);
        this._add('wire',19,7,1,7);
        break;

      case 'diode':
        this._add('battery',2,7,2,4,9);
        this._add('wire',2,4,7,4);
        this._add('diode',7,4,13,4);
        this._add('resistor',13,4,19,4,100);
        this._add('ammeter',19,4,19,7);
        this._add('wire',19,7,2,7);
        break;

      case 'led':
        this._add('battery',2,7,2,4,9);
        this._add('wire',2,4,7,4);
        this._add('led',7,4,13,4);
        this._add('resistor',13,4,19,4,47);
        this._add('wire',19,4,19,7);
        this._add('wire',19,7,2,7);
        break;

      case 'rc':
        this._add('battery',2,7,2,4,9);
        this._add('wire',2,4,6,4);
        this._add('switch',6,4,10,4);
        this._add('resistor',10,4,15,4,100);
        this._add('capacitor',15,4,19,4,100);
        this._add('wire',19,4,19,7);
        this._add('wire',19,7,2,7);
        break;

      case 'ac':
        this._add('ac',2,7,2,4,9);
        this._add('wire',2,4,9,4);
        this._add('resistor',9,4,15,4,10);
        this._add('wire',15,4,19,4);
        this._add('wire',19,4,19,7);
        this._add('wire',19,7,2,7);
        break;

      case 'rlc': {
        // RLC series: AC → R → L → C
        this._add('ac',2,7,2,4,9);
        this._add('wire',2,4,6,4);
        this._add('resistor',6,4,10,4,10);
        this._add('inductor',10,4,14,4,undefined,10);
        this._add('capacitor',14,4,19,4,100);
        this._add('wire',19,4,19,7);
        this._add('wire',19,7,2,7);
        break;
      }

      default: break;
    }

    this._pushHistory();
    this._solve(); this.draw();
    if (this.onUpdate) this.onUpdate(this.info());
  }

  /* ─── Fit ──────────────────────────────────────────────────────────────── */

  fit() {
    this.W=this.canvas.offsetWidth||800;
    this.H=this.canvas.offsetHeight||500;
    const dpr=window.devicePixelRatio||1;
    this.canvas.width=this.W*dpr;
    this.canvas.height=this.H*dpr;
    this.ctx.setTransform(1,0,0,1,0,0);
    this.ctx.scale(dpr,dpr);
    this.CELL=Math.max(24,Math.floor(Math.min((this.W-40)/this.GW,(this.H-40)/this.GH)));
    this.ox=Math.round((this.W-this.CELL*this.GW)/2);
    this.oy=Math.round((this.H-this.CELL*this.GH)/2);
    this._solve(); this.draw();
  }

  /* ─── Info ─────────────────────────────────────────────────────────────── */

  info() {
    const solved=this._solution?.solved??false;
    const batt=this.components.find(c=>c.type==='battery'||c.type==='ac');
    return {
      components: this.components.length,
      solved,
      voltage:  batt?batt.value:0,
      current:  batt?Math.abs(batt._I).toFixed(3):'—',
      power:    batt?(Math.abs(batt._I)*(batt.value||0)).toFixed(2):'—',
      totalR:   (batt&&batt._I&&Math.abs(batt._I)>0.0001)?((batt.value||0)/Math.abs(batt._I)).toFixed(1):'—',
      addMode:  this.addMode
    };
  }

  /* ─── Clear ────────────────────────────────────────────────────────────── */

  clear() {
    this._pushHistory();
    this.components=[]; this._nextId=0; this._diodeR.clear(); this._selected=null;
    this._solve(); this.draw();
    if (this.onUpdate) this.onUpdate(this.info());
  }
}

/* ─── lab UI init ─────────────────────────────────── */
  var cirSim = null;
  var reacSim = null;
  var flaskSim = null;

  function _openCircuit() {
    document.getElementById('sim-topbar-title').textContent = 'Электрические цепи';
    _simShow('sim-circuit');
    _simShow('ctrl-circuit');
    requestAnimationFrame(() => requestAnimationFrame(() => {
      const canvas = document.getElementById('circuit-canvas');
      if (!cirSim) {
        cirSim = new CircuitSim(canvas);
        cirSim.onUpdate = _circUpdateUI;
        cirSim.onModeChange = (mode) => {
          document.querySelectorAll('.circ-tool-btn').forEach(b => {
            b.classList.toggle('active', b.dataset.tool === mode);
          });
          document.querySelectorAll('.circ-top-btn').forEach(b => {
            b.classList.toggle('active', b.id === 'ctool-' + mode);
          });
        };
      } else {
        cirSim.stop();
      }
      cirSim.fit();
      if (cirSim.components.length === 0) cirSim.preset('serial');
      cirSim.start();
      _circUpdateUI(cirSim.info());
    }));
  }

  function circTool(tool, el) {
    if (cirSim) cirSim.addMode = tool;
    document.querySelectorAll('.circ-tool-btn').forEach(b => b.classList.toggle('active', b.dataset.tool === tool));
    document.querySelectorAll('.circ-top-btn').forEach(b => b.classList.toggle('active', b.id === 'ctool-' + tool));
  }

  function circPreset(name) {
    if (!cirSim) return;
    cirSim.preset(name);
  }

  function circRChange() {
    const v = +document.getElementById('sl-circR').value;
    document.getElementById('circ-R-val').textContent = v + ' Ω';
    if (cirSim) cirSim.R_value = v;
  }

  function circUChange() {
    const v = +document.getElementById('sl-circU').value;
    document.getElementById('circ-U-val').textContent = v + ' В';
    if (cirSim) cirSim.U_value = v;
  }

  function circCChange() {
    const v = +document.getElementById('sl-circC').value;
    document.getElementById('circ-C-val').textContent = v + ' µF';
    if (cirSim) cirSim.C_value = v;
  }

  function circFChange() {
    const v = +document.getElementById('sl-circF').value;
    document.getElementById('circ-F-val').textContent = v + ' Гц';
    if (cirSim) cirSim.acFreq = v;
  }

  function circLChange() {
    const v = +document.getElementById('sl-circL').value;
    document.getElementById('circ-L-val').textContent = v + ' мГн';
    if (cirSim) cirSim.L_value = v;
  }

  function circToggleHeat() {
    if (!cirSim) return;
    cirSim._heatmapOn = !cirSim._heatmapOn;
    const btn = document.getElementById('ctool-heat');
    if (btn) btn.classList.toggle('active', cirSim._heatmapOn);
    if (!cirSim._raf) cirSim.draw();
  }

  function circToggleOsc() {
    const panel = document.getElementById('osc-panel');
    if (!panel) return;
    const visible = panel.style.display !== 'none';
    panel.style.display = visible ? 'none' : 'block';
    const btn = document.getElementById('btn-osc-toggle');
    if (btn) btn.classList.toggle('active', !visible);
    if (cirSim) {
      cirSim._oscPanel = visible ? null : document.getElementById('osc-canvas');
      if (!visible && cirSim._oscPanel) cirSim.drawOscilloscope(cirSim._oscPanel);
    }
  }

  function _circUpdateUI(info) {
    if (!info) return;
    document.getElementById('cirbar-comps').textContent  = info.components;
    document.getElementById('cirbar-U').textContent      = info.voltage ? info.voltage + ' В' : '—';
    document.getElementById('cirbar-I').textContent      = info.current ? info.current + ' А' : '—';
    document.getElementById('cirbar-P').textContent      = info.power   ? info.power   + ' Вт' : '—';
    const st = document.getElementById('cirbar-status');
    st.textContent  = info.solved ? 'Замкнута' : 'Разомкнута';
    st.style.color  = info.solved ? '#7BF5A4' : '#EF476F';
  }

  /* ════════════════════════════════
     ХИМИЯ (unified: кинетика + колба + ОВР + ионный обмен)
  ════════════════════════════════ */

  let _chemMode = 'kinetics'; // 'kinetics' | 'flask' | 'redox' | 'ionex'

