'use strict';
/* ====================================================================
   RedoxSim — Окислительно-восстановительные реакции
   ==================================================================== */

class RedoxSim {

  /* ── Данные реакций ──────────────────────────────────────────────── */

  static RXN = {
    fe_cu: {
      name: 'Fe + CuSO₄',
      reducer:  { f: 'Fe',    name: 'Железо',       color: '#A0856A', ox: 0  },
      oxidizer: { f: 'Cu²⁺',  name: 'Ион меди',     color: '#29B6F6', ox: 2  },
      prod_r:   { f: 'Fe²⁺',  color: '#66BB6A', ox: 2 },
      prod_o:   { f: 'Cu',    color: '#C87840', ox: 0, solid: true },
      e: 2,
      half_r:   'Fe⁰ – 2e⁻ <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> Fe²⁺       окисление',
      half_o:   'Cu²⁺ + 2e⁻ <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> Cu⁰       восстановление',
      eq_ion:   'Fe + Cu²⁺ <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> Fe²⁺ + Cu<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>',
      eq_mol:   'Fe + CuSO₄ <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> FeSO₄ + Cu<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>',
      sol_a: '#1565C040', sol_b: '#2E7D3230',
      precip: true, pcolor: '#C87840', pname: 'медь Cu',
    },
    zn_hcl: {
      name: 'Zn + HCl',
      reducer:  { f: 'Zn',   name: 'Цинк',      color: '#90A4AE', ox: 0 },
      oxidizer: { f: 'H⁺',   name: 'Ион H⁺',    color: '#EF5350', ox: 1 },
      prod_r:   { f: 'Zn²⁺', color: '#80CBC4',  ox: 2 },
      prod_o:   { f: 'H₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>',  color: '#EEEEEE',  ox: 0, gas: true },
      e: 2,
      half_r:   'Zn⁰ – 2e⁻ <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> Zn²⁺       окисление',
      half_o:   '2H⁺ + 2e⁻ <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> H₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>        восстановление',
      eq_ion:   'Zn + 2H⁺ <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> Zn²⁺ + H₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>',
      eq_mol:   'Zn + 2HCl <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> ZnCl₂ + H₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>',
      sol_a: '#EF525228', sol_b: '#E0F2F118',
      gas: true, gcolor: '#CFD8DC', gname: 'водород H₂',
    },
    cl2_ki: {
      name: 'Cl₂ + KI',
      reducer:  { f: 'I⁻',   name: 'Иодид-ион', color: '#CE93D8', ox: -1 },
      oxidizer: { f: 'Cl₂',  name: 'Хлор',      color: '#D4E157', ox:  0 },
      prod_r:   { f: 'I₂',   color: '#6A1B9A',  ox: 0, solid: true },
      prod_o:   { f: 'Cl⁻',  color: '#AED581',  ox: -1 },
      e: 1,
      half_r:   '2I⁻ – 2e⁻ <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> I₂          окисление',
      half_o:   'Cl₂ + 2e⁻ <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> 2Cl⁻        восстановление',
      eq_ion:   'Cl₂ + 2I⁻ <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> I₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg> + 2Cl⁻',
      eq_mol:   'Cl₂ + 2KI <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> I₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg> + 2KCl',
      sol_a: '#7B1FA230', sol_b: '#F9A82520',
      precip: true, pcolor: '#6A1B9A', pname: 'йод I₂',
    },
    kmno4: {
      name: 'KMnO₄ + FeSO₄',
      reducer:  { f: 'Fe²⁺',  name: 'Ион Fe²⁺',     color: '#66BB6A', ox: 2 },
      oxidizer: { f: 'MnO₄⁻', name: 'Перманганат',   color: '#AB47BC', ox: 7 },
      prod_r:   { f: 'Fe³⁺',  color: '#FFA726', ox: 3 },
      prod_o:   { f: 'Mn²⁺',  color: '#FFF9C4', ox: 2 },
      e: 5,
      half_r:   'Fe²⁺ – e⁻ <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> Fe³⁺  (×5)  окисление',
      half_o:   'MnO₄⁻+8H⁺+5e⁻<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>Mn²⁺+4H₂O  восстановление',
      eq_ion:   'MnO₄⁻ + 5Fe²⁺ + 8H⁺ <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> Mn²⁺ + 5Fe³⁺ + 4H₂O',
      eq_mol:   '2KMnO₄ + 10FeSO₄ + 8H₂SO₄ <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> 2MnSO₄ + 5Fe₂(SO₄)₃ + K₂SO₄ + 8H₂O',
      sol_a: '#7B1FA250', sol_b: '#F9A82515',
      colorChange: true, newSolColor: '#FFF9C428', newName: 'бесцветный MnSO₄',
    },
    cu_fecl3: {
      name: 'Cu + FeCl₃',
      reducer:  { f: 'Cu',    name: 'Медь',      color: '#C87840', ox: 0 },
      oxidizer: { f: 'Fe³⁺',  name: 'Ион Fe³⁺',  color: '#FFA726', ox: 3 },
      prod_r:   { f: 'Cu²⁺',  color: '#29B6F6',  ox: 2 },
      prod_o:   { f: 'Fe²⁺',  color: '#66BB6A',  ox: 2 },
      e: 1,
      half_r:   'Cu⁰ – 2e⁻ <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> Cu²⁺       окисление',
      half_o:   '2Fe³⁺ + 2e⁻ <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> 2Fe²⁺    восстановление',
      eq_ion:   'Cu + 2Fe³⁺ <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> Cu²⁺ + 2Fe²⁺',
      eq_mol:   'Cu + 2FeCl₃ <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> CuCl₂ + 2FeCl₂',
      sol_a: '#E6510018', sol_b: '#1565C018',
      colorChange: true, newSolColor: '#1565C030', newName: 'синий CuCl₂',
    },
  };

  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');
    this.rxnId  = 'fe_cu';
    this._raf   = null;
    this._last  = 0;
    this._t     = 0;
    this._phase = 'idle';   // idle | mixing | reacting | done
    this._prog  = 0;
    this._colorT = 0;
    this._stepIdx   = 0;
    this._stepTimer = 0;
    this._eParts = [];
    this._rParts = [];
    this._oParts = [];
    this._precip = [];
    this._gas    = [];
    /* edu-tooltip + product labels */
    this._eduTooltipAge   = -1;
    this._eduTooltipLines = [];
    this._prodLabelAge    = -1;
    this._prodLabelText   = '';
    this._prodLabelType   = 'precip';
    this.W = 0; this.H = 0;
    this.onUpdate = null;
    this.fit();
    this._initParts();
  }

  fit() {
    const dpr = window.devicePixelRatio || 1;
    const W = this.canvas.offsetWidth  || 600;
    const H = this.canvas.offsetHeight || 400;
    this.canvas.width  = Math.round(W * dpr);
    this.canvas.height = Math.round(H * dpr);
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    this.W = W; this.H = H;
    this._initParts();
  }

  setReaction(id) {
    if (!RedoxSim.RXN[id]) return;
    this.rxnId = id;
    this.reset();
  }

  reset() {
    this._phase = 'idle'; this._prog = 0; this._colorT = 0;
    this._stepIdx = 0; this._stepTimer = 0;
    this._eParts = []; this._precip = []; this._gas = [];
    this._initParts();
    this.draw();
  }

  _initParts() {
    const { W, H } = this;
    const N = 16;
    this._rParts = Array.from({ length: N }, () => ({
      x: W * 0.22 + (Math.random() - 0.5) * W * 0.22,
      y: H * 0.42 + (Math.random() - 0.5) * H * 0.34,
      vx: (Math.random() - 0.5) * 0.6, vy: (Math.random() - 0.5) * 0.6,
      r: 11 + Math.random() * 4,
      phase: Math.random() * Math.PI * 2,
      trans: false, flashT: 0,
    }));
    this._oParts = Array.from({ length: N }, () => ({
      x: W * 0.78 + (Math.random() - 0.5) * W * 0.22,
      y: H * 0.42 + (Math.random() - 0.5) * H * 0.34,
      vx: (Math.random() - 0.5) * 0.6, vy: (Math.random() - 0.5) * 0.6,
      r: 11 + Math.random() * 4,
      phase: Math.random() * Math.PI * 2,
      trans: false, flashT: 0,
    }));
  }

  start() {
    if (this._phase !== 'idle') this.reset();
    this._phase = 'mixing'; this._prog = 0;
    if (window.LabFX) LabFX.sound.play('whoosh', { pitch: 0.8 });
    if (this._raf) return;
    this._last = performance.now();
    const loop = t => { this._raf = requestAnimationFrame(loop); this._tick(t); };
    this._raf = requestAnimationFrame(loop);
  }

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

  /* ── Физика ─────────────────────────────────────────────────────── */

  _tick(t) {
    const dt = Math.min((t - this._last) / 1000, 0.05);
    this._last = t; this._t += dt;
    if (window.LabFX) LabFX.particles.update(dt);
    const { W, H } = this;
    const rxn = RedoxSim.RXN[this.rxnId];

    if (this._phase === 'mixing') {
      this._prog = Math.min(1, this._prog + dt * 0.38);
      const all = [...this._rParts, ...this._oParts];
      all.forEach(p => {
        const tx = W * 0.5 + (Math.random() - 0.5) * W * 0.52;
        const ty = H * 0.44 + (Math.random() - 0.5) * H * 0.34;
        p.vx += (tx - p.x) * 0.003 * this._prog;
        p.vy += (ty - p.y) * 0.003 * this._prog;
        p.vx += (Math.random() - 0.5) * 0.5;
        p.vy += (Math.random() - 0.5) * 0.5;
        p.vx *= 0.90; p.vy *= 0.90;
        p.x += p.vx; p.y += p.vy;
        p.phase += dt * 1.5;
        this._clamp(p);
      });
      if (this._prog >= 1) { this._phase = 'reacting'; this._prog = 0; }
    }

    if (this._phase === 'reacting') {
      this._prog = Math.min(1, this._prog + dt * 0.14);
      this._colorT = this._prog;
      this._stepTimer += dt;
      if (this._stepTimer > 1.6 && this._stepIdx < 3) { this._stepIdx++; this._stepTimer = 0; }

      const all = [...this._rParts, ...this._oParts];
      all.forEach(p => {
        p.vx += (Math.random() - 0.5) * 0.9;
        p.vy += (Math.random() - 0.5) * 0.9;
        p.vx *= 0.87; p.vy *= 0.87;
        p.x += p.vx; p.y += p.vy;
        p.phase += dt * 2;
        p.flashT = Math.max(0, p.flashT - dt * 3);
        this._clamp(p);
      });

      /* Transform particles proportional to progress */
      const rT = Math.floor(this._prog * this._rParts.length);
      const oT = Math.floor(this._prog * this._oParts.length);
      this._rParts.forEach((p, i) => {
        if (i < rT && !p.trans) { p.trans = true; p.flashT = 1; }
      });
      this._oParts.forEach((p, i) => {
        if (i < oT && !p.trans) {
          p.trans = true; p.flashT = 1;
          if (rxn.precip) this._precip.push({ x: p.x, y: p.y, vy: 0, r: 3 + Math.random() * 3, settled: false });
          if (rxn.gas)    this._gas.push({ x: p.x, y: p.y, vy: -(1.5 + Math.random()), vx: (Math.random() - 0.5) * 0.5, r: 2 + Math.random() * 3, alpha: 1 });
        }
      });

      if (Math.random() < 0.22 && this._prog > 0.05) this._spawnE();
      if (this._prog >= 1) { this._phase = 'done'; this._stepIdx = 3; }
    }

    if (this._phase === 'done') {
      const all = [...this._rParts, ...this._oParts];
      all.forEach(p => {
        p.vx += (Math.random() - 0.5) * 0.45;
        p.vy += (Math.random() - 0.5) * 0.45;
        p.vx *= 0.92; p.vy *= 0.92;
        p.x += p.vx; p.y += p.vy;
        p.phase += dt;
        this._clamp(p);
      });
      /* trigger product label + edu-tooltip once */
      if (window.ChemVisuals && this._prodLabelAge < 0) {
        const rxn = RedoxSim.RXN[this.rxnId];
        if (rxn.precip) {
          this._prodLabelText = (rxn.pname || '') + ' ';
          this._prodLabelType = 'precip';
          this._prodLabelAge  = 0;
        } else if (rxn.gas) {
          this._prodLabelText = (rxn.gname || '') + ' ';
          this._prodLabelType = 'gas';
          this._prodLabelAge  = 0;
        }
        if (this._eduTooltipAge < 0 && rxn.eq_mol) {
          const stripSVG = s => (s || '').replace(/<[^>]+>/g, '->');
          const eqClean = stripSVG(rxn.eq_ion || rxn.eq_mol).slice(0, 38);
          this._eduTooltipLines = [
            (rxn.name || '').slice(0, 34),
            'e⁻: от восстановителя к окислителю',
            eqClean,
          ].filter(Boolean).slice(0, 4);
          this._eduTooltipAge = 0;
        }
      }
    }

    /* advance ages */
    if (this._eduTooltipAge >= 0) {
      this._eduTooltipAge += dt / 4.0;
      if (this._eduTooltipAge >= 1.0) this._eduTooltipAge = -1;
    }
    if (this._prodLabelAge >= 0) {
      this._prodLabelAge += dt / 3.0;
      if (this._prodLabelAge >= 1.0) this._prodLabelAge = -1;
    }

    /* Electrons — quadratic bezier arc */
    this._eParts = this._eParts.filter(e => e.t < 1);
    this._eParts.forEach(e => {
      e.t = Math.min(1, e.t + dt * e.spd);
      const u = e.t;
      e.x = (1-u)*(1-u)*e.x0 + 2*(1-u)*u*e.mx + u*u*e.x1;
      e.y = (1-u)*(1-u)*e.y0 + 2*(1-u)*u*e.my + u*u*e.y1;
      e.alpha = u < 0.1 ? u * 10 : u > 0.85 ? (1 - u) / 0.15 : 1;
    });

    /* Precipitate */
    this._precip.forEach(p => {
      if (!p.settled) {
        p.vy = Math.min(p.vy + 0.15, 5);
        p.y += p.vy;
        if (p.y >= H * 0.78) { p.y = H * 0.78; p.vy = 0; p.settled = true; }
      }
    });

    /* Gas */
    this._gas.forEach(b => { b.y += b.vy; b.x += b.vx; b.vy -= 0.01; b.alpha -= 0.005; });
    this._gas = this._gas.filter(b => b.alpha > 0 && b.y > 10);

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

  _clamp(p) {
    const { W, H } = this;
    const bot = H * 0.78;
    if (p.x < p.r + 6)     { p.x = p.r + 6;     p.vx *= -0.5; }
    if (p.x > W - p.r - 6) { p.x = W - p.r - 6; p.vx *= -0.5; }
    if (p.y < p.r + 6)     { p.y = p.r + 6;     p.vy *= -0.5; }
    if (p.y > bot - p.r)   { p.y = bot - p.r;   p.vy *= -0.5; }
  }

  _spawnE() {
    const freeR = this._rParts.filter(p => !p.trans);
    const freeO = this._oParts.filter(p => !p.trans);
    if (!freeR.length || !freeO.length) return;
    const rp = freeR[Math.floor(Math.random() * freeR.length)];
    const op = freeO[Math.floor(Math.random() * freeO.length)];
    const mx = (rp.x + op.x) / 2;
    const my = Math.min(rp.y, op.y) - 45 - Math.random() * 40;
    this._eParts.push({
      x0: rp.x, y0: rp.y, x1: op.x, y1: op.y,
      mx, my, x: rp.x, y: rp.y,
      t: 0, spd: 0.65 + Math.random() * 0.45, alpha: 0,
    });
    // LabFX: electron transfer spark at the midpoint
    if (window.LabFX) {
      LabFX.particles.emit({ ctx: this.ctx, x: mx, y: my, count: 2,
        color: '#06D6E0', speed: 30, spread: 3.14, angle: 0,
        gravity: 0, life: 200, shape: 'spark', glow: true });
    }
  }

  /* ── Рендеринг ──────────────────────────────────────────────────── */

  draw() {
    const { ctx, W, H } = this;
    const rxn = RedoxSim.RXN[this.rxnId];

    /* Background */
    ctx.fillStyle = '#07071A';
    ctx.fillRect(0, 0, W, H);

    /* Dot grid */
    ctx.fillStyle = 'rgba(255,255,255,0.07)';
    for (let x = 0; x < W; x += 28) {
      for (let y = 0; y < H; y += 28) {
        ctx.beginPath(); ctx.arc(x, y, 0.8, 0, Math.PI * 2); ctx.fill();
      }
    }

    /* Solution tint */
    const bx = W * 0.04, bw = W * 0.92, bTop = H * 0.08, bBot = H * 0.80;
    if (this._phase === 'idle') {
      if (rxn.sol_a) { ctx.save(); ctx.fillStyle = rxn.sol_a; ctx.fillRect(bx, bTop, bw / 2, bBot - bTop); ctx.restore(); }
      if (rxn.sol_b) { ctx.save(); ctx.fillStyle = rxn.sol_b; ctx.fillRect(bx + bw / 2, bTop, bw / 2, bBot - bTop); ctx.restore(); }
      /* Dashed divider */
      ctx.save();
      ctx.setLineDash([6, 5]); ctx.strokeStyle = 'rgba(255,255,255,0.10)'; ctx.lineWidth = 1.5;
      ctx.beginPath(); ctx.moveTo(W / 2, bTop + 4); ctx.lineTo(W / 2, bBot - 4); ctx.stroke();
      ctx.setLineDash([]); ctx.restore();
      /* Zone labels */
      ctx.save();
      ctx.textAlign = 'center'; ctx.textBaseline = 'top';
      ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.font = 'bold 12px sans-serif';
      ctx.fillText(rxn.reducer.name, W * 0.22, bTop + 8);
      ctx.fillStyle = 'rgba(255,255,255,0.14)'; ctx.font = '10px sans-serif';
      ctx.fillText('восстановитель', W * 0.22, bTop + 26);
      ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.font = 'bold 12px sans-serif';
      ctx.fillText(rxn.oxidizer.name, W * 0.78, bTop + 8);
      ctx.fillStyle = 'rgba(255,255,255,0.14)'; ctx.font = '10px sans-serif';
      ctx.fillText('окислитель', W * 0.78, bTop + 26);
      ctx.restore();
    } else if (this._colorT > 0) {
      if (rxn.sol_a) {
        ctx.save(); ctx.globalAlpha = 1 - this._colorT * 0.7;
        ctx.fillStyle = rxn.sol_a; ctx.fillRect(bx, bTop, bw, bBot - bTop); ctx.restore();
      }
      if (rxn.colorChange && rxn.newSolColor) {
        ctx.save(); ctx.globalAlpha = this._colorT * 0.55;
        ctx.fillStyle = rxn.newSolColor; ctx.fillRect(bx, bTop, bw, bBot - bTop); ctx.restore();
      }
    }

    /* desk */
    if (window.ChemVisuals) {
      ChemVisuals.drawDeskBackground(ctx, W, H, H * 0.82);
      ChemVisuals.drawVesselShadow(ctx, W / 2, H * 0.82, W * 0.38);
    }

    this._drawBeaker(ctx, W, H);
    this._drawParticles(ctx, rxn);
    this._drawElectrons(ctx);
    if (rxn.precip) this._drawPrecip(ctx, rxn);
    if (rxn.gas)    this._drawGas(ctx, rxn);
    this._drawPanel(ctx, W, H, rxn);
    if (window.LabFX) LabFX.particles.draw(this.ctx);

    /* animated product label */
    if (window.ChemVisuals && this._prodLabelAge >= 0) {
      const labelY = this._prodLabelType === 'gas' ? H * 0.12 : H * 0.76;
      ChemVisuals.drawProductLabel(ctx, W / 2, labelY, this._prodLabelText, this._prodLabelType, this._prodLabelAge);
      if (this._prodLabelType === 'gas') {
        ChemVisuals.animateGasBubbles(ctx, W / 2, H * 0.16, rxn.gcolor || 'rgba(200,235,255,0.8)', this._t);
      } else {
        ChemVisuals.animatePrecipitateFall(ctx, W / 2, H * 0.72, rxn.pcolor || '#CCC', this._t);
      }
    }

    /* edu tooltip */
    if (window.ChemVisuals && this._eduTooltipAge >= 0 && this._eduTooltipLines.length > 0) {
      ChemVisuals.drawEduTooltip(ctx, W / 2, H * 0.10, 210, this._eduTooltipLines, this._eduTooltipAge);
    }
  }

  _drawBeaker(ctx, W, H) {
    const bx = W * 0.04, by = H * 0.08, bw = W * 0.92, bh = H * 0.73;
    ctx.save();
    ctx.strokeStyle = 'rgba(120,185,255,0.60)'; ctx.lineWidth = 2.5;
    ctx.beginPath();
    ctx.moveTo(bx, by); ctx.lineTo(bx, by + bh);
    ctx.lineTo(bx + bw, by + bh); ctx.lineTo(bx + bw, by);
    ctx.stroke();
    ctx.beginPath(); ctx.moveTo(bx - 5, by); ctx.lineTo(bx + bw + 5, by); ctx.stroke();
    /* Left highlight */
    const hlg = ctx.createLinearGradient(bx, by, bx + 18, by + bh);
    hlg.addColorStop(0, 'rgba(200,230,255,0.18)');
    hlg.addColorStop(1, 'rgba(200,230,255,0.02)');
    ctx.strokeStyle = hlg; ctx.lineWidth = 6;
    ctx.beginPath(); ctx.moveTo(bx + 8, by + 8); ctx.lineTo(bx + 8, by + bh - 8); ctx.stroke();
    ctx.restore();
  }

  _drawParticles(ctx, rxn) {
    const draw1 = (p, spec, prod) => {
      const s = p.trans ? prod : spec;
      ctx.save();
      ctx.shadowColor = p.flashT > 0 ? '#FFFFFF' : s.color;
      ctx.shadowBlur = p.flashT > 0 ? 28 * p.flashT : 8 + Math.sin(p.phase) * 3;
      ctx.globalAlpha = 0.88;
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
      ctx.fillStyle = p.flashT > 0 ? `rgba(255,255,255,${p.flashT * 0.9})` : s.color;
      ctx.fill();
      ctx.strokeStyle = 'rgba(255,255,255,0.22)'; ctx.lineWidth = 1; ctx.stroke();
      ctx.shadowBlur = 0; ctx.globalAlpha = 1;
      /* Oxidation state */
      const ox = p.trans ? prod.ox : spec.ox;
      const oxStr = ox > 0 ? `+${ox}` : ox < 0 ? `${ox}` : '0';
      ctx.fillStyle = p.trans ? '#FFD166' : 'rgba(255,255,255,0.88)';
      ctx.font = `bold ${Math.round(p.r * 0.78)}px monospace`;
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText(oxStr, p.x, p.y);
      ctx.restore();
    };
    this._rParts.forEach(p => draw1(p, rxn.reducer, rxn.prod_r));
    this._oParts.forEach(p => draw1(p, rxn.oxidizer, rxn.prod_o));
  }

  _drawElectrons(ctx) {
    this._eParts.forEach(e => {
      ctx.save();
      ctx.globalAlpha = e.alpha;
      ctx.shadowColor = '#4FC3F7'; ctx.shadowBlur = 16;
      ctx.beginPath(); ctx.arc(e.x, e.y, 5.5, 0, Math.PI * 2);
      ctx.fillStyle = '#4FC3F7'; ctx.fill();
      ctx.shadowBlur = 0;
      ctx.fillStyle = 'rgba(255,255,255,0.95)';
      ctx.font = 'bold 7px monospace';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('e⁻', e.x, e.y);
      ctx.restore();
    });
  }

  _drawPrecip(ctx, rxn) {
    if (!this._precip.length) return;
    ctx.save();
    this._precip.forEach(p => {
      ctx.shadowColor = rxn.pcolor; ctx.shadowBlur = 4;
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
      ctx.fillStyle = rxn.pcolor; ctx.fill();
    });
    ctx.restore();
    /* Label when settled */
    const settled = this._precip.filter(p => p.settled);
    if (settled.length > 3) {
      ctx.save();
      ctx.fillStyle = rxn.pcolor; ctx.font = 'bold 10px monospace';
      ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
      ctx.shadowColor = rxn.pcolor; ctx.shadowBlur = 6;
      ctx.fillText(`↓ ${rxn.pname}`, this.W / 2, this.H * 0.80 - 4);
      ctx.restore();
    }
  }

  _drawGas(ctx, rxn) {
    this._gas.forEach(b => {
      ctx.save(); ctx.globalAlpha = b.alpha * 0.75;
      ctx.shadowColor = rxn.gcolor; ctx.shadowBlur = 5;
      ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
      ctx.strokeStyle = rxn.gcolor; ctx.lineWidth = 1; ctx.stroke();
      ctx.restore();
    });
    const count = this._gas.length;
    if (count > 2) {
      ctx.save();
      ctx.fillStyle = rxn.gcolor; ctx.font = 'bold 10px monospace';
      ctx.textAlign = 'center'; ctx.shadowColor = rxn.gcolor; ctx.shadowBlur = 6;
      ctx.fillText(`↑ ${rxn.gname}`, this.W / 2, this.H * 0.12);
      ctx.restore();
    }
  }

  _drawPanel(ctx, W, H, rxn) {
    const py = H * 0.82;
    ctx.fillStyle = 'rgba(7,7,26,0.94)';
    ctx.fillRect(0, py, W, H - py);
    ctx.strokeStyle = 'rgba(100,165,255,0.25)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(W, py); ctx.stroke();

    if (this._phase === 'idle') {
      ctx.fillStyle = '#37474F'; ctx.font = '11px monospace';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('← Нажми «Начать» для запуска реакции →', W / 2, py + (H - py) / 2);
      return;
    }

    const steps = [
      { lbl: 'Молекулярное:', txt: rxn.eq_mol,  col: '#B0BEC5' },
      { lbl: 'Окисление:',    txt: rxn.half_r,  col: '#EF476F' },
      { lbl: 'Восстановление:', txt: rxn.half_o, col: '#4CC9F0' },
      { lbl: 'Ионное:',       txt: rxn.eq_ion,  col: '#FFD166' },
    ];

    const panH = H - py;
    const n = Math.min(this._stepIdx + 1, steps.length);
    for (let i = 0; i < n; i++) {
      const s = steps[i];
      const y = py + 11 + i * (panH * 0.22);
      ctx.save();
      if (i === this._stepIdx && this._phase !== 'done') {
        ctx.fillStyle = 'rgba(255,255,255,0.04)';
        ctx.fillRect(8, y - 9, W - 16, 20);
      }
      ctx.fillStyle = s.col; ctx.font = 'bold 9.5px monospace';
      ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
      ctx.fillText(s.lbl, 14, y);
      ctx.fillStyle = (i === this._stepIdx && this._phase !== 'done') ? '#FFF' : 'rgba(255,255,255,0.62)';
      ctx.font = '9.5px monospace';
      ctx.fillText(s.txt, 14 + ctx.measureText(s.lbl).width + 8, y);
      ctx.restore();
    }

    if (this._phase === 'done') {
      ctx.save();
      ctx.fillStyle = '#7BF5A4'; ctx.font = 'bold 10px monospace';
      ctx.textAlign = 'right'; ctx.textBaseline = 'top';
      ctx.shadowColor = '#7BF5A4'; ctx.shadowBlur = 8;
      ctx.fillText('✓ Реакция завершена', W - 14, py + 3);
      ctx.restore();
    }
  }

  info() {
    const rxn = RedoxSim.RXN[this.rxnId];
    return {
      rxn:  rxn.name,
      phase: this._phase,
      prog: Math.round((this._phase === 'reacting' ? this._prog : this._phase === 'done' ? 1 : 0) * 100),
      e: rxn.e,
    };
  }
}
