'use strict';
/* ════════════════════════════════════════════════════════════════
   FlaskSim v2 — «Химия в колбе»
   • Реалистичная вода: 3 слоя волн, каустики, мениск, SSS
   • Пар при нагреве, всплески пузырьков
   • Толстое стекло, пульсирующий glow реакции
   • Specular highlight металла, dissolution edge
   ════════════════════════════════════════════════════════════════ */

class FlaskSim {

  /* ── Реагенты ─────────────────────────────────────────────────── */

  static METALS = {
    Zn: { name: 'Цинк',     color: '#9BB8CC', k: 0.50, Ea: 0.9,  rho: 7.13, dH: 155,  h2: 1,   acids: ['HCl','H2SO4'] },
    Fe: { name: 'Железо',   color: '#A08060', k: 0.08, Ea: 1.4,  rho: 7.87, dH: 87,   h2: 1,   acids: ['HCl'], rust: true },
    Mg: { name: 'Магний',   color: '#D6D6D6', k: 1.50, Ea: 0.5,  rho: 1.74, dH: 467,  h2: 1,   acids: ['HCl','H2SO4','H2O'] },
    Cu: { name: 'Медь',     color: '#C87840', k: 0,    Ea: 99,   rho: 8.96, dH: 0,    h2: 0,   acids: [] },
    Na: { name: 'Натрий',   color: '#F5F0C8', k: 6.00, Ea: 0.05, rho: 0.97, dH: 883,  h2: 0.5, acids: ['HCl','H2SO4','H2O'], boom: true },
    Al: { name: 'Алюминий', color: '#C0C0C0', k: 0.60, Ea: 1.0,  rho: 2.70, dH: 300,  h2: 1.5, acids: ['HCl','H2SO4'] },
  };

  static ACIDS = {
    HCl:   { name: 'Соляная кислота HCl',  rgb: [120, 210, 120], pHf: 1.0, label: 'HCl' },
    H2SO4: { name: 'Серная кислота H₂SO₄', rgb: [210, 195, 120], pHf: 1.2, label: 'H₂SO₄' },
    H2O:   { name: 'Вода H₂O',             rgb: [110, 180, 215], pHf: 0.0, label: 'H₂O' },
  };

  static EQ = {
    Zn_HCl:   '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>',
    Zn_H2SO4: 'Zn + H₂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> ZnSO₄ + 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>',
    Fe_HCl:   'Fe + 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> FeCl₂ + 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>',
    Mg_HCl:   'Mg + 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> MgCl₂ + 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>',
    Mg_H2SO4: 'Mg + H₂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> MgSO₄ + 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>',
    Mg_H2O:   'Mg + 2H₂O <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> Mg(OH)₂<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> + 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>',
    Al_HCl:   '2Al + 6HCl <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> 2AlCl₃ + 3H₂<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>',
    Al_H2SO4: '2Al + 3H₂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> Al₂(SO₄)₃ + 3H₂<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>',
    Na_HCl:   '2Na + 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> 2NaCl + 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>',
    Na_H2SO4: '2Na + H₂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> Na₂SO₄ + 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>',
    Na_H2O:   '2Na + 2H₂O <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> 2NaOH + 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>',
    Cu_HCl:   'Cu + HCl — реакция не идёт',
    Cu_H2SO4: 'Cu + H₂SO₄(разб.) — реакция не идёт',
    Cu_H2O:   'Cu + H₂O — реакция не идёт',
    Fe_H2SO4: 'Fe + H₂SO₄(конц.) — пассивация!',
    Fe_H2O:   'Fe + H₂O — реакция не идёт при 20°C',
    Al_H2O:   'Al + H₂O — не реагирует (оксидная плёнка)',
    Zn_H2O:   'Zn + H₂O — реакция не идёт',
  };

  /* ── Конструктор ──────────────────────────────────────────────── */

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

    this.metalType = 'Zn';
    this.acidType  = 'HCl';
    this.concLevel = 0.35;
    this.envTemp   = 20;

    /* Частицы и волны */
    this._metal    = null;
    this._bubbles  = [];
    this._dusts    = [];
    this._sparks   = [];
    this._steam    = [];
    this._splashes = [];
    this._caustics = [];

    /* Фазы волн (3 независимые) */
    this._wave  = 0;
    this._wave2 = 0;
    this._wave3 = 0;

    /* Анимационные таймеры */
    this._glowPulse  = 0;
    this._causticTmr = 0;
    this._steamTmr   = 0;

    /* Физическое состояние */
    this._passiv   = false;
    this._ignited  = false;
    this._flameOn  = false;
    this._boomCD   = 0;
    this._conc     = this.concLevel;
    this._temp     = this.envTemp;
    this._pH       = 1.0;
    this._rxRate   = 0;
    this._h2       = 0;
    this._bubTmr   = 0;

    /* Анимация */
    this._raf    = null;
    this._last   = 0;
    this._paused = false;

    /* product label animation */
    this._prodLabelAge  = -1;
    this._prodLabelText = '';
    this._prodLabelType = 'gas';
    this._time = 0;

    this.onUpdate = null;
    this.W = 0; this.H = 0;
    this._g = {};

    this.fit();
  }

  /* ── Геометрия ────────────────────────────────────────────────── */

  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._calcGeom();
  }

  _calcGeom() {
    const { W, H } = this;
    const r    = Math.min(W * 0.195, H * 0.285);
    const cx   = W * 0.50;
    const cy   = H * 0.615;
    const nw   = r * 0.26;          // ширина горлышка
    const nh   = r * 1.05;          // высота горлышка
    const nt   = cy - r - nh;       // верх горлышка
    const nb   = cy - r * 0.80;     // точка начала плеч (где шея переходит в колбу)
    const liqTop = cy - r * 0.42;
    this._g = { r, cx, cy, nw, nh, nt, nb, liqTop };
  }

  _flaskPath(ctx) {
    const { r, cx, cy, nw, nt, nb } = this._g;
    ctx.beginPath();
    ctx.moveTo(cx - nw, nt);
    ctx.lineTo(cx - nw, nb);
    /* Левое плечо: плавная кривая Безье от шеи до экватора колбы */
    ctx.bezierCurveTo(
      cx - nw,       cy - r * 0.42,   // CP1: продолжаем вниз по шее
      cx - r * 0.85, cy - r * 0.10,   // CP2: выходим к экватору
      cx - r,        cy               // конец: левый экватор окружности
    );
    /* Нижняя дуга колбы: слева направо через дно (anticlockwise=true в canvas = через низ) */
    ctx.arc(cx, cy, r, Math.PI, 0, true);
    /* Правое плечо: симметрично */
    ctx.bezierCurveTo(
      cx + r * 0.85, cy - r * 0.10,
      cx + nw,       cy - r * 0.42,
      cx + nw,       nb
    );
    ctx.lineTo(cx + nw, nt);
    ctx.closePath();
  }

  /* ── Запуск / остановка ───────────────────────────────────────── */

  start() {
    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; }

  /* ── Публичный API ────────────────────────────────────────────── */

  dropMetal() {
    const { cx, nt } = this._g;
    const md   = FlaskSim.METALS[this.metalType];
    const mass = 5;
    this._metal = {
      type: this.metalType,
      mass, init: mass,
      x: cx + (Math.random() - 0.5) * 8,
      y: nt - 32,
      vx: (Math.random() - 0.5) * 22,
      vy: 0,
      r: this._m2r(mass),
      _v: Array.from({ length: 10 }, (_, i) => ({
        a: (i / 10) * Math.PI * 2,
        j: 0.68 + Math.random() * 0.32,
      })),
    };
    this._passiv  = false;
    this._ignited = false;
    this._h2      = 0;
    this._bubTmr  = 0;
    this._boomCD  = 0;

    if (window.LabFX) {
      LabFX.sound.play('bounce', { pitch: 0.6 });
      // Brief delay then fizz as metal hits acid
      setTimeout(() => { if (window.LabFX) LabFX.sound.play('fizz'); }, 350);
      LabFX.particles.emit({ ctx: this.ctx, x: cx, y: nt - 5, count: 6,
        color: '#FFFFFF', speed: 25, spread: 1.6, angle: -Math.PI / 2,
        gravity: -60, life: 1500, shape: 'ring' });
    }
  }

  reset() {
    this._metal    = null;
    this._bubbles  = [];
    this._dusts    = [];
    this._sparks   = [];
    this._steam    = [];
    this._splashes = [];
    this._caustics = [];
    this._passiv   = false;
    this._ignited  = false;
    this._flameOn  = false;
    this._h2       = 0;
    this._rxRate   = 0;
    this._boomCD   = 0;
    this._causticTmr = 0;
    this._steamTmr   = 0;
    this._conc     = this.concLevel;
    this._temp     = this.envTemp;
    this._pH       = this._startPH();
    if (this.onUpdate) this.onUpdate(this.info());
    this.draw();
  }

  togglePause() { this._paused  = !this._paused; if (window.LabFX) LabFX.sound.play('click'); }
  toggleFlame() { this._flameOn = !this._flameOn; }
  setMetal(t)   { this.metalType = t; }
  setAcid(t)    { this.acidType  = t; this.reset(); }
  setConc(v)    { this.concLevel = v; this._conc = v; if (!this._metal) this._pH = this._startPH(); }
  setEnvTemp(v) { this.envTemp = v; if (!this._metal) this._temp = v; }

  _startPH() {
    const a = FlaskSim.ACIDS[this.acidType];
    if (a.pHf === 0) return 7.0;
    return Math.max(0, -Math.log10(this.concLevel * 10 * a.pHf + 1e-10));
  }

  _m2r(mass) { return 8 + 24 * Math.cbrt(Math.max(0, mass) / 5); }

  /* ── Тик физики ───────────────────────────────────────────────── */

  _tick(now) {
    const dt = Math.min((now - this._last) / 1000, 0.05);
    this._last = now;
    this._time += dt;
    if (window.LabFX) LabFX.particles.update(dt);
    if (!this._paused) {
      this._wave      += dt * 1.7;
      this._wave2     += dt * 2.3;
      this._wave3     += dt * 0.88;
      this._glowPulse += dt * 3.2;
      this._stepMetal(dt);
      this._stepBubbles(dt);
      this._stepDusts(dt);
      this._stepSparks(dt);
      this._stepSteam(dt);
      this._stepSplashes(dt);
      this._stepCaustics(dt);
    }
    /* product label age */
    if (this._prodLabelAge >= 0) {
      this._prodLabelAge += dt / 3.0;
      if (this._prodLabelAge >= 1.0) this._prodLabelAge = -1;
    }
    this.draw();
    if (this.onUpdate) this.onUpdate(this.info());
  }

  /* ── Физика металла ───────────────────────────────────────────── */

  _stepMetal(dt) {
    const m = this._metal;
    if (!m || m.mass <= 0.01) { if (m) m.mass = 0; return; }

    const md = FlaskSim.METALS[m.type];
    const { cy, r, liqTop, cx } = this._g;

    const liqRho = 1.12;
    const inLiq  = m.y + m.r > liqTop;
    const grav   = 400;
    const buoy   = inLiq ? grav * (liqRho / md.rho) : 0;
    const drag   = inLiq ? 4.5 : 0.25;

    m.vy += (grav - buoy) * dt;
    m.vy -= drag * m.vy * dt;
    m.vx -= drag * m.vx * dt;
    m.y  += m.vy * dt;
    m.x  += m.vx * dt;

    const botY = cy + r - m.r;
    if (m.y > botY) { m.y = botY; m.vy *= -0.22; }
    const hw = Math.sqrt(Math.max(0, r * r - (m.y - cy) ** 2));
    m.x = Math.max(cx - hw + m.r, Math.min(cx + hw - m.r, m.x));

    if (md.rho < liqRho && inLiq) {
      const sfY = liqTop - m.r;
      if (m.y < sfY) { m.y = sfY; m.vy = Math.abs(m.vy) * 0.25; }
    }

    const reacts = md.acids.includes(this.acidType) && !this._passiv && this._conc > 4e-4;
    if (!reacts) { this._rxRate = 0; return; }

    const T_K    = this._temp + 273.15;
    const rate   = md.k * this._conc * Math.exp(-md.Ea * 3000 / (8.314 * T_K));
    this._rxRate = Math.min(1, rate * 4);

    const surf = (m.r / 26) ** 2;
    const dmdt = rate * surf * 0.95;

    m.mass = Math.max(0, m.mass - dmdt * dt);
    m.r    = this._m2r(m.mass);

    /* Слегка деформировать вершины при реакции */
    if (this._rxRate > 0.1 && Math.random() < dt * 6) {
      const vi = Math.floor(Math.random() * m._v.length);
      m._v[vi].j = Math.max(0.45, Math.min(1.0, m._v[vi].j + (Math.random() - 0.5) * 0.08));
    }

    const heatW = md.dH * dmdt * 0.055;
    const cool  = 0.30 * (this._temp - this.envTemp);
    this._temp  = Math.min(150, Math.max(this.envTemp, this._temp + (heatW - cool) * dt));

    this._conc  = Math.max(0, this._conc - dmdt * 0.07 * dt);

    const ad = FlaskSim.ACIDS[this.acidType];
    if (ad.pHf > 0) {
      this._pH = Math.min(7, Math.max(0, -Math.log10(this._conc * 10 * ad.pHf + 1e-10)));
    } else {
      this._pH = Math.min(14, 7 + Math.min(7, m.mass < 0.1 ? 7 : dmdt * 12));
    }

    this._h2 = Math.min(1, this._h2 + md.h2 * dmdt * 0.065 * dt);

    this._bubTmr += rate * 32 * dt;
    while (this._bubTmr > 1 && this._bubbles.length < 180) {
      this._spawnBubble(m.x, m.y - m.r * 0.6);
      this._bubTmr--;
    }

    /* trigger H2 product label when reaction first picks up */
    if (md.h2 > 0 && this._rxRate > 0.05 && this._prodLabelAge < 0 && window.ChemVisuals) {
      this._prodLabelText = 'H₂ ';
      this._prodLabelType = 'gas';
      this._prodLabelAge  = 0;
    }

    if (md.rust && Math.random() < rate * dt * 14) {
      this._spawnDust(m.x + (Math.random() - 0.5) * m.r, m.y + m.r * 0.3, '#8B3A0A', 0.65);
    }

    if (m.type === 'Fe' && this.acidType === 'H2SO4' && this.concLevel > 0.82) {
      this._passiv = true;
    }

    if (md.boom && this._boomCD <= 0 && rate > 0.28) {
      this._boom(m.x, m.y);
      this._boomCD = 0.45;
    }
    if (this._boomCD > 0) this._boomCD -= dt;

    if (this._flameOn && this._h2 > 0.22 && !this._ignited) {
      this._igniteH2();
    }
  }

  /* ── Частицы ─────────────────────────────────────────────────── */

  _spawnBubble(x, y) {
    this._bubbles.push({
      x: x + (Math.random() - 0.5) * 14,
      y,
      r:      1.4 + Math.random() * 3.8,
      vy:    -(16 + Math.random() * 38),
      vx:    (Math.random() - 0.5) * 9,
      wobble: Math.random() * Math.PI * 2,
      wFreq:  3.5 + Math.random() * 3,
      life:   1,
    });
  }

  _stepBubbles(dt) {
    const { liqTop } = this._g;
    for (const b of this._bubbles) {
      b.wobble += dt * b.wFreq;
      b.x  += b.vx * dt + Math.sin(b.wobble) * b.r * 0.35 * dt * 10;
      b.y  += b.vy * dt;
      b.vx += (Math.random() - 0.5) * 28 * dt;
      if (b.y - b.r < liqTop) {
        this._spawnSplash(b.x, liqTop, b.r);
        b.life = 0;
      } else {
        b.life -= dt * 0.14;
      }
    }
    this._bubbles = this._bubbles.filter(b => b.life > 0);
  }

  _spawnSplash(x, y, r) {
    if (r < 2) return;
    const n = Math.floor(2 + r);
    for (let i = 0; i < n; i++) {
      const a = -Math.PI * 0.5 + (Math.random() - 0.5) * Math.PI * 1.2;
      const s = 10 + r * 4 + Math.random() * 20;
      this._splashes.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 12, r: 0.8 + Math.random() * 1.4, life: 1 });
    }
  }

  _stepSplashes(dt) {
    for (const s of this._splashes) {
      s.x  += s.vx * dt;
      s.y  += s.vy * dt;
      s.vy += 55 * dt;
      s.life -= dt * 4.0;
    }
    this._splashes = this._splashes.filter(s => s.life > 0);
  }

  _spawnSteam(x, y) {
    this._steam.push({
      x: x + (Math.random() - 0.5) * 16,
      y,
      vx:   (Math.random() - 0.5) * 12,
      vy:  -(6 + Math.random() * 18),
      r:    2.5 + Math.random() * 6,
      life: 0.9 + Math.random() * 0.1,
    });
  }

  _stepSteam(dt) {
    if (this._temp > 70) {
      this._steamTmr += (this._temp - 70) / 30 * dt * 7;
      while (this._steamTmr > 1 && this._steam.length < 55) {
        const { liqTop, cx, nw } = this._g;
        this._spawnSteam(cx + (Math.random() - 0.5) * nw * 1.6, liqTop - 4);
        this._steamTmr--;
      }
    }
    for (const s of this._steam) {
      s.x  += s.vx * dt;
      s.y  += s.vy * dt;
      s.vx += (Math.random() - 0.5) * 12 * dt;
      s.r  += dt * 5;
      s.life -= dt * (0.7 + (1 - s.life) * 0.4);
    }
    this._steam = this._steam.filter(s => s.life > 0 && s.r < 50);
  }

  _spawnCaustic(x, y) {
    this._caustics.push({
      x, y,
      r:    7 + Math.random() * 20,
      vx:  (Math.random() - 0.5) * 16,
      vy:  (Math.random() - 0.5) * 7,
      life: 0.4 + Math.random() * 0.6,
      a:   0.05 + Math.random() * 0.09,
    });
  }

  _stepCaustics(dt) {
    this._causticTmr += dt * 3.5;
    while (this._causticTmr > 1 && this._caustics.length < 20) {
      const { cx, cy, r, liqTop } = this._g;
      const px = cx + (Math.random() - 0.5) * r * 1.5;
      const py = liqTop + 8 + Math.random() * (cy + r * 0.6 - liqTop - 16);
      this._spawnCaustic(px, py);
      this._causticTmr--;
    }
    for (const c of this._caustics) {
      c.x += c.vx * dt;
      c.y += c.vy * dt;
      c.r += dt * 4;
      c.life -= dt * 0.38;
    }
    this._caustics = this._caustics.filter(c => c.life > 0);
  }

  _spawnDust(x, y, col, a) {
    this._dusts.push({
      x, y,
      vx: (Math.random() - 0.5) * 14,
      vy: 4 + Math.random() * 20,
      r: 1.0 + Math.random() * 2.2,
      col, a, life: 1,
    });
  }

  _stepDusts(dt) {
    for (const d of this._dusts) {
      d.x  += d.vx * dt;
      d.y  += d.vy * dt;
      d.vy += 28 * dt;
      d.vx *= 1 - dt * 2.2;
      d.life -= dt * 0.22;
    }
    this._dusts = this._dusts.filter(d => d.life > 0);
    if (this._dusts.length > 300) this._dusts.splice(0, 60);
  }

  _stepSparks(dt) {
    for (const s of this._sparks) {
      s.x  += s.vx * dt;
      s.y  += s.vy * dt;
      s.vy += 210 * dt;
      s.vx *= 1 - dt * 0.8;
      s.life -= dt * 2.0;
    }
    this._sparks = this._sparks.filter(s => s.life > 0);
  }

  _boom(x, y) {
    for (let i = 0; i < 36; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = 90 + Math.random() * 240;
      this._sparks.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 90,
        r: 2 + Math.random() * 4, col: Math.random() < 0.55 ? '#FFD166' : '#EF476F', life: 1 });
    }
  }

  _igniteH2() {
    this._ignited = true;
    this._h2 = 0;
    const { cx, nt } = this._g;
    for (let i = 0; i < 60; i++) {
      const a = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI * 0.9;
      const s = 130 + Math.random() * 360;
      this._sparks.push({
        x: cx + (Math.random() - 0.5) * 18, y: nt - 10,
        vx: Math.cos(a) * s, vy: Math.sin(a) * s,
        r: 3 + Math.random() * 5, col: i < 30 ? '#FFD166' : '#FF6B35', life: 1,
      });
    }
    if (window.LabFX) {
      LabFX.sound.play('whoosh', { pitch: 1.5 });
      LabFX.particles.emit({ ctx: this.ctx, x: cx, y: nt - 10, count: 20,
        color: '#FFA500', speed: 80, spread: 2.0, angle: -Math.PI / 2,
        gravity: -100, life: 300, shape: 'spark', glow: true });
    }
  }

  /* ════════════════════════════════════════════════════════════════
     РЕНДЕРИНГ
  ════════════════════════════════════════════════════════════════ */

  draw() {
    const ctx = this.ctx;
    const { W, H, _g: g } = this;
    ctx.clearRect(0, 0, W, H);

    /* Фон */
    const bg = ctx.createRadialGradient(W * 0.5, H * 0.35, 0, W * 0.5, H * 0.5, W * 0.75);
    bg.addColorStop(0, '#0d1320');
    bg.addColorStop(1, '#05080f');
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, W, H);

    /* Сетка лаборатории */
    ctx.strokeStyle = 'rgba(255,255,255,0.018)'; ctx.lineWidth = 1;
    for (let x = 0; x < W; x += 28) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
    for (let y = 0; y < H; y += 28)  { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }

    /* Стол */
    const tableY = g.cy + g.r + 8;
    if (window.ChemVisuals) {
      ChemVisuals.drawDeskBackground(ctx, W, H, tableY);
      ChemVisuals.drawVesselShadow(ctx, g.cx, tableY + 2, g.r);
    } else {
      const tg = ctx.createLinearGradient(0, tableY, 0, tableY + 48);
      tg.addColorStop(0, '#19223a'); tg.addColorStop(1, '#0c101e');
      ctx.fillStyle = tg; ctx.fillRect(0, tableY, W, H - tableY);
      ctx.strokeStyle = 'rgba(90,120,200,0.20)'; ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(0, tableY); ctx.lineTo(W, tableY); ctx.stroke();
    }

    /* Spirit lamp under flask when flame is on */
    if (window.ChemVisuals) {
      const lampX = g.cx;
      const lampY = tableY + 18;
      ChemVisuals.drawSpiritLamp(ctx, lampX, lampY, this._flameOn, this._time);
    }

    this._drawFlaskShadow(ctx, tableY);
    this._drawLiquid(ctx);
    this._drawCaustics(ctx);
    this._drawDusts(ctx);
    this._drawBubbles(ctx);
    this._drawSplashes(ctx);
    this._drawMetal(ctx);
    this._drawFlaskGlass(ctx);
    this._drawSteam(ctx);
    this._drawSparks(ctx);
    this._drawThermometer(ctx);
    this._drawPHStrip(ctx);
    this._drawH2Bar(ctx);
    this._drawInfoPanel(ctx);
    if (!this._metal || this._metal.mass <= 0.01) this._drawHint(ctx);
    if (window.LabFX) LabFX.particles.draw(ctx);
    /* animated product labels */
    if (window.ChemVisuals && this._prodLabelAge >= 0) {
      ChemVisuals.drawProductLabel(ctx, g.cx, g.nt - 10, this._prodLabelText, this._prodLabelType, this._prodLabelAge);
      if (this._prodLabelType === 'gas') {
        ChemVisuals.animateGasBubbles(ctx, g.cx, g.nt - 8, 'rgba(200,235,255,0.8)', this._time);
      }
    }
  }

  /* ── Тень/отражение колбы на столе ── */
  _drawFlaskShadow(ctx, tableY) {
    const { _g: g } = this;
    ctx.save();
    ctx.scale(1, 0.26);
    const shadowGrad = ctx.createRadialGradient(g.cx, tableY / 0.26, 0, g.cx, tableY / 0.26, g.r * 1.15);
    shadowGrad.addColorStop(0, 'rgba(0,0,0,0.50)');
    shadowGrad.addColorStop(0.55, 'rgba(0,0,0,0.22)');
    shadowGrad.addColorStop(1,   'rgba(0,0,0,0)');
    ctx.fillStyle = shadowGrad;
    ctx.beginPath(); ctx.arc(g.cx, tableY / 0.26, g.r * 1.15, 0, Math.PI * 2); ctx.fill();
    ctx.restore();

    /* Реакционный glow на столе */
    if (this._rxRate > 0.05) {
      const ad = FlaskSim.ACIDS[this.acidType];
      const [ri, gi, bi] = ad.rgb;
      ctx.save();
      ctx.scale(1, 0.28);
      const gg = ctx.createRadialGradient(g.cx, tableY / 0.28, 0, g.cx, tableY / 0.28, g.r * 0.9);
      gg.addColorStop(0, `rgba(${ri},${gi},${bi},${this._rxRate * 0.18})`);
      gg.addColorStop(1, 'rgba(0,0,0,0)');
      ctx.fillStyle = gg;
      ctx.beginPath(); ctx.arc(g.cx, tableY / 0.28, g.r * 0.9, 0, Math.PI * 2); ctx.fill();
      ctx.restore();
    }
  }

  /* ── Жидкость: 3 волны + SSS + каустики + мениск ── */
  _drawLiquid(ctx) {
    const { _g: g } = this;
    const ad = FlaskSim.ACIDS[this.acidType];
    const [ri, gi, bi] = ad.rgb;
    const heat = Math.min(1, (this._temp - 20) / 80);
    const lr   = Math.min(255, ri + heat * 80);
    const lg   = Math.max(0,   gi - heat * 58);
    const lb   = Math.max(0,   bi - heat * 58);
    const al   = 0.20 + this._conc * 0.40;

    const amp  = 1.8 + this._rxRate * 8;

    /* Функция волновой поверхности */
    const waveY = (x) => {
      const wx = x - g.cx;
      return g.liqTop
        + Math.sin(wx * 0.065 + this._wave)              * amp
        + Math.sin(wx * 0.130 - this._wave2 * 1.38)      * amp * 0.36
        + Math.sin(wx * 0.046 + this._wave3 * 0.75)      * amp * 0.20;
    };

    const step = 2;
    const x0 = g.cx - g.r - 2, x1 = g.cx + g.r + 2;

    ctx.save();
    this._flaskPath(ctx);
    ctx.clip();

    /* ── Слой 1: основное тело жидкости ── */
    ctx.beginPath();
    ctx.moveTo(x0, waveY(x0));
    for (let x = x0; x <= x1; x += step) ctx.lineTo(x, waveY(x));
    ctx.lineTo(x1, g.cy + g.r + 12);
    ctx.lineTo(x0, g.cy + g.r + 12);
    ctx.closePath();

    const depthGrad = ctx.createLinearGradient(0, g.liqTop, 0, g.cy + g.r);
    depthGrad.addColorStop(0,    `rgba(${lr},${lg},${lb},${al * 0.40})`);
    depthGrad.addColorStop(0.30, `rgba(${lr},${lg},${lb},${al * 0.65})`);
    depthGrad.addColorStop(1,    `rgba(${lr},${lg},${lb},${al})`);
    ctx.fillStyle = depthGrad; ctx.fill();

    /* ── Слой 2: subsurface scattering (22px полоса под поверхностью) ── */
    ctx.beginPath();
    ctx.moveTo(x0, waveY(x0));
    for (let x = x0; x <= x1; x += step) ctx.lineTo(x, waveY(x));
    for (let x = x1; x >= x0; x -= step) ctx.lineTo(x, waveY(x) + 22);
    ctx.closePath();
    const sssGrad = ctx.createLinearGradient(0, g.liqTop, 0, g.liqTop + 22);
    sssGrad.addColorStop(0, `rgba(${Math.min(255,lr+90)},${Math.min(255,lg+80)},${Math.min(255,lb+80)},0.22)`);
    sssGrad.addColorStop(1, `rgba(${lr},${lg},${lb},0)`);
    ctx.fillStyle = sssGrad; ctx.fill();

    /* ── Слой 3: радиальный тинт дна (имитация рассеяния) ── */
    const botGrad = ctx.createRadialGradient(g.cx, g.cy + g.r * 0.55, 0, g.cx, g.cy + g.r * 0.55, g.r * 0.80);
    botGrad.addColorStop(0, `rgba(${Math.min(255,lr+30)},${Math.min(255,lg+30)},${Math.min(255,lb+40)},0.14)`);
    botGrad.addColorStop(1, 'rgba(0,0,0,0)');
    ctx.fillStyle = botGrad;
    ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, 0, Math.PI * 2); ctx.fill();

    /* ── Основной блик поверхности ── */
    ctx.beginPath();
    ctx.moveTo(x0, waveY(x0));
    for (let x = x0; x <= x1; x += step) ctx.lineTo(x, waveY(x));
    ctx.strokeStyle = `rgba(${Math.min(255,lr+100)},${Math.min(255,lg+95)},${Math.min(255,lb+95)},0.50)`;
    ctx.lineWidth = 1.6; ctx.stroke();

    /* ── Вторая, более тонкая волна-блик (чуть ниже) ── */
    ctx.beginPath();
    for (let x = x0; x <= x1; x += step) {
      const wy = waveY(x) + 3
        + Math.sin((x - g.cx) * 0.11 - this._wave2 * 1.1) * amp * 0.55;
      if (x === x0) ctx.moveTo(x, wy); else ctx.lineTo(x, wy);
    }
    ctx.strokeStyle = `rgba(${Math.min(255,lr+55)},${Math.min(255,lg+55)},${Math.min(255,lb+55)},0.18)`;
    ctx.lineWidth = 1; ctx.stroke();

    /* ── Мениск у стенок колбы ── */
    const mY = waveY(g.cx);
    ctx.beginPath();
    ctx.moveTo(g.cx - g.r + 2, waveY(g.cx - g.r) + 6);
    ctx.quadraticCurveTo(g.cx - g.r + 14, mY - 4, g.cx - g.r * 0.38, mY);
    ctx.strokeStyle = `rgba(${Math.min(255,lr+70)},${Math.min(255,lg+70)},${Math.min(255,lb+70)},0.32)`;
    ctx.lineWidth = 2.2; ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(g.cx + g.r - 2, waveY(g.cx + g.r) + 6);
    ctx.quadraticCurveTo(g.cx + g.r - 14, mY - 4, g.cx + g.r * 0.38, mY);
    ctx.stroke();

    ctx.restore();
  }

  /* ── Каустики (световые пятна в толще жидкости) ── */
  _drawCaustics(ctx) {
    if (this._caustics.length === 0) return;
    ctx.save();
    this._flaskPath(ctx); ctx.clip();
    for (const c of this._caustics) {
      const alpha = c.a * c.life;
      const cg = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, c.r);
      cg.addColorStop(0,   `rgba(255,255,255,${alpha * 0.9})`);
      cg.addColorStop(0.45,`rgba(210,235,255,${alpha * 0.4})`);
      cg.addColorStop(1,   'rgba(0,0,0,0)');
      ctx.fillStyle = cg;
      ctx.beginPath(); ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2); ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.restore();
  }

  /* ── Пузырьки с wobble, specular, вторичным бликом ── */
  _drawBubbles(ctx) {
    ctx.save();
    this._flaskPath(ctx); ctx.clip();
    for (const b of this._bubbles) {
      const a = Math.min(1, b.life * 2.5);

      /* Тело пузырька — градиент */
      const bg = ctx.createRadialGradient(
        b.x - b.r * 0.30, b.y - b.r * 0.30, 0,
        b.x, b.y, b.r
      );
      bg.addColorStop(0,   `rgba(220,240,255,${a * 0.18})`);
      bg.addColorStop(0.65,`rgba(180,215,255,${a * 0.09})`);
      bg.addColorStop(1,   `rgba(130,185,255,${a * 0.04})`);
      ctx.fillStyle = bg;
      ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill();

      /* Контур */
      ctx.strokeStyle = `rgba(200,230,255,${a * 0.70})`;
      ctx.lineWidth = 0.85;
      ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.stroke();

      /* Specular highlight (верхний левый) */
      const hg = ctx.createRadialGradient(
        b.x - b.r * 0.30, b.y - b.r * 0.32, 0,
        b.x - b.r * 0.30, b.y - b.r * 0.32, b.r * 0.30
      );
      hg.addColorStop(0, `rgba(255,255,255,${a * 0.88})`);
      hg.addColorStop(1, 'rgba(255,255,255,0)');
      ctx.fillStyle = hg;
      ctx.beginPath(); ctx.arc(b.x - b.r * 0.30, b.y - b.r * 0.32, b.r * 0.30, 0, Math.PI * 2); ctx.fill();

      /* Малый блик (нижний правый) */
      ctx.fillStyle = `rgba(200,225,255,${a * 0.28})`;
      ctx.beginPath(); ctx.arc(b.x + b.r * 0.24, b.y + b.r * 0.30, b.r * 0.12, 0, Math.PI * 2); ctx.fill();
    }
    ctx.restore();
  }

  /* ── Всплески на поверхности ── */
  _drawSplashes(ctx) {
    if (this._splashes.length === 0) return;
    ctx.save();
    this._flaskPath(ctx); ctx.clip();
    const ad = FlaskSim.ACIDS[this.acidType];
    const [ri, gi, bi] = ad.rgb;
    for (const s of this._splashes) {
      ctx.globalAlpha = s.life * 0.75;
      ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fillStyle = `rgb(${Math.min(255,ri+70)},${Math.min(255,gi+70)},${Math.min(255,bi+70)})`;
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.restore();
  }

  /* ── Пар ── */
  _drawSteam(ctx) {
    if (this._steam.length === 0) return;
    const { _g: g } = this;
    for (const s of this._steam) {
      /* Пар выходит только выше горлышка или через нагрев (внутри колбы у шейки) */
      const inNeck = s.x > g.cx - g.nw - 6 && s.x < g.cx + g.nw + 6;
      if (!inNeck && s.y > g.nt - 4) continue;
      ctx.save();
      ctx.globalAlpha = s.life * 0.38 * Math.min(1, s.r / 8);
      const sg = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r);
      sg.addColorStop(0, 'rgba(200,215,255,0.9)');
      sg.addColorStop(0.6,'rgba(180,200,240,0.4)');
      sg.addColorStop(1, 'rgba(160,190,230,0)');
      ctx.fillStyle = sg;
      ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill();
      ctx.restore();
    }
  }

  /* ── Колба: толстое стекло, glow реакции, highlights ── */
  _drawFlaskGlass(ctx) {
    const { _g: g } = this;
    const { r, cx, cy, nw, nt, nb } = g;
    const heat = Math.min(1, (this._temp - 20) / 80);
    const ad   = FlaskSim.ACIDS[this.acidType];
    const [ri, gi, bi] = ad.rgb;

    /* ── Reaction glow (пульсирующий) ── */
    if (this._rxRate > 0.04) {
      ctx.save();
      const pulse = 0.5 + 0.5 * Math.sin(this._glowPulse);
      ctx.shadowColor = `rgb(${ri},${gi},${bi})`;
      ctx.shadowBlur  = 10 + this._rxRate * 30 + pulse * 10;
      this._flaskPath(ctx);
      ctx.strokeStyle = `rgba(${ri},${gi},${bi},${this._rxRate * 0.50 + pulse * 0.12})`;
      ctx.lineWidth = 1.2;
      ctx.stroke();
      ctx.shadowBlur = 0;
      ctx.restore();
    }

    /* ── Внешний контур ── */
    this._flaskPath(ctx);
    ctx.strokeStyle = 'rgba(100,165,255,0.68)';
    ctx.lineWidth = 3.0;
    ctx.stroke();

    /* ── Внутренний контур (толщина стекла) ── */
    const tk   = 4;
    const r_i  = r  - tk;
    const nw_i = nw - tk * 0.70;
    const nb_i = cy - r_i * 0.80;
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(cx - nw_i, nt + tk * 0.9);
    ctx.lineTo(cx - nw_i, nb_i);
    ctx.bezierCurveTo(
      cx - nw_i,        cy - r_i * 0.42,
      cx - r_i * 0.85,  cy - r_i * 0.10,
      cx - r_i,         cy
    );
    ctx.arc(cx, cy, r_i, Math.PI, 0, true);
    ctx.bezierCurveTo(
      cx + r_i * 0.85,  cy - r_i * 0.10,
      cx + nw_i,        cy - r_i * 0.42,
      cx + nw_i,        nb_i
    );
    ctx.lineTo(cx + nw_i, nt + tk * 0.9);
    ctx.strokeStyle = 'rgba(75,125,215,0.16)';
    ctx.lineWidth = 1;
    ctx.stroke();
    ctx.restore();

    /* ── Большой левый блик (gradient arc) ── */
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(cx - nw * 0.50, nt + 10);
    ctx.lineTo(cx - nw * 0.50, nb);
    ctx.bezierCurveTo(
      cx - nw * 0.50, nb + r * 0.18,
      cx - r * 0.72,  cy - r * 0.42,
      cx - r * 0.74,  cy - r * 0.05
    );
    const hlGrad = ctx.createLinearGradient(cx - r * 0.73, nt, cx - r * 0.73, cy);
    hlGrad.addColorStop(0,    'rgba(225,242,255,0.40)');
    hlGrad.addColorStop(0.40, 'rgba(215,235,255,0.22)');
    hlGrad.addColorStop(0.75, 'rgba(200,225,255,0.10)');
    hlGrad.addColorStop(1,    'rgba(200,225,255,0.02)');
    ctx.strokeStyle = hlGrad;
    ctx.lineWidth = 5;
    ctx.stroke();
    ctx.restore();

    /* ── Правый мягкий блик ── */
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(cx + r * 0.62, cy - r * 0.56);
    ctx.quadraticCurveTo(cx + r * 0.83, cy - r * 0.18, cx + r * 0.78, cy + r * 0.20);
    ctx.strokeStyle = 'rgba(200,225,255,0.11)';
    ctx.lineWidth = 3;
    ctx.stroke();
    ctx.restore();

    /* ── Горлышко — ободок ── */
    ctx.beginPath();
    ctx.moveTo(cx - nw - 5, nt);
    ctx.lineTo(cx + nw + 5, nt);
    ctx.strokeStyle = 'rgba(100,165,255,0.68)';
    ctx.lineWidth = 3.2;
    ctx.stroke();

    /* ── Блик горлышка ── */
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(cx - nw * 0.42, nt + 4);
    ctx.lineTo(cx - nw * 0.42, nb - 4);
    ctx.strokeStyle = 'rgba(220,240,255,0.26)';
    ctx.lineWidth = 2.2;
    ctx.stroke();
    ctx.restore();

    /* ── Тепловой тинт (стекло краснеет при нагреве) ── */
    if (heat > 0.15) {
      ctx.save();
      this._flaskPath(ctx);
      ctx.fillStyle = `rgba(255,${Math.round(155 - heat * 110)},40,${heat * 0.072})`;
      ctx.fill();
      ctx.restore();
    }
  }

  /* ── Металл: specular, dissolution edge ── */
  _drawMetal(ctx) {
    const m = this._metal;
    if (!m || m.mass <= 0.01) return;
    const md = FlaskSim.METALS[m.type];

    ctx.save();

    /* Dissolution edge glow */
    if (this._rxRate > 0.08) {
      ctx.shadowColor = '#FFD166';
      ctx.shadowBlur  = 6 + this._rxRate * 22;
    }

    /* Тело */
    ctx.beginPath();
    for (let i = 0; i < m._v.length; i++) {
      const v  = m._v[i];
      const px = m.x + Math.cos(v.a) * m.r * v.j;
      const py = m.y + Math.sin(v.a) * m.r * v.j;
      if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
    }
    ctx.closePath();

    const mg = ctx.createRadialGradient(m.x - m.r * 0.32, m.y - m.r * 0.30, 0, m.x, m.y, m.r);
    mg.addColorStop(0,   this._tint(md.color, 80));
    mg.addColorStop(0.28,this._tint(md.color, 48));
    mg.addColorStop(0.68,md.color);
    mg.addColorStop(1,   this._tint(md.color, -62));
    ctx.fillStyle = mg; ctx.fill();
    ctx.strokeStyle = this._tint(md.color, 32); ctx.lineWidth = 1.5; ctx.stroke();
    ctx.shadowBlur = 0;

    /* Specular dot */
    ctx.save();
    ctx.globalAlpha = 0.68;
    const sg = ctx.createRadialGradient(
      m.x - m.r * 0.28, m.y - m.r * 0.30, 0,
      m.x - m.r * 0.28, m.y - m.r * 0.30, m.r * 0.40
    );
    sg.addColorStop(0, 'rgba(255,255,255,0.95)');
    sg.addColorStop(0.5,'rgba(255,255,255,0.35)');
    sg.addColorStop(1, 'rgba(255,255,255,0)');
    ctx.fillStyle = sg;
    ctx.beginPath(); ctx.arc(m.x - m.r * 0.28, m.y - m.r * 0.30, m.r * 0.40, 0, Math.PI * 2); ctx.fill();
    ctx.restore();

    /* Dissolution edge */
    if (this._rxRate > 0.12) {
      ctx.save();
      ctx.globalAlpha = this._rxRate * 0.55;
      ctx.beginPath();
      for (let i = 0; i < m._v.length; i++) {
        const v = m._v[i];
        const px = m.x + Math.cos(v.a) * m.r * v.j;
        const py = m.y + Math.sin(v.a) * m.r * v.j;
        if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
      }
      ctx.closePath();
      ctx.strokeStyle = '#FFD166';
      ctx.lineWidth = 1.5 + this._rxRate * 3.5;
      ctx.shadowColor = '#FFD166'; ctx.shadowBlur = 10;
      ctx.stroke();
      ctx.restore();
    }

    /* Пассивирующая плёнка */
    if (this._passiv) {
      ctx.beginPath();
      for (let i = 0; i < m._v.length; i++) {
        const v = m._v[i];
        if (i === 0) ctx.moveTo(m.x + Math.cos(v.a) * m.r * v.j, m.y + Math.sin(v.a) * m.r * v.j);
        else          ctx.lineTo(m.x + Math.cos(v.a) * m.r * v.j, m.y + Math.sin(v.a) * m.r * v.j);
      }
      ctx.closePath();
      ctx.fillStyle = 'rgba(55,40,25,0.65)'; ctx.fill();
    }

    ctx.restore();
  }

  _drawDusts(ctx) {
    ctx.save();
    this._flaskPath(ctx); ctx.clip();
    for (const d of this._dusts) {
      ctx.globalAlpha = d.a * d.life;
      ctx.beginPath(); ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
      ctx.fillStyle = d.col; ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.restore();
  }

  _drawSparks(ctx) {
    for (const s of this._sparks) {
      ctx.save();
      ctx.globalAlpha = s.life;
      ctx.shadowColor = s.col;
      ctx.shadowBlur  = 12;
      ctx.beginPath(); ctx.arc(s.x, s.y, s.r * s.life, 0, Math.PI * 2);
      ctx.fillStyle = s.col; ctx.fill();
      ctx.restore();
    }
    if (this._flameOn) {
      const { cx, nt, nw } = this._g;
      ctx.font = '22px serif';
      ctx.fillText('*', cx + nw + 8, nt + 8);
    }
  }

  /* ── Термометр ── */
  _drawThermometer(ctx) {
    const { _g: g } = this;
    const tx = g.cx + g.r + 44;
    const ty = g.nt + 8;
    const th = g.cy + g.r - ty - 16;
    const tw = 11;
    const frac = Math.min(1, Math.max(0, (this._temp - 10) / 140));
    const fillH = th * frac;
    const col = `hsl(${Math.round(55 - frac * 55)},92%,56%)`;

    _flask_rrect(ctx, tx - tw / 2, ty, tw, th, tw / 2);
    ctx.fillStyle = 'rgba(255,255,255,0.07)'; ctx.fill();
    ctx.strokeStyle = 'rgba(120,175,255,0.38)'; ctx.lineWidth = 1.5; ctx.stroke();

    if (fillH > 0) {
      _flask_rrect(ctx, tx - tw / 2 + 2, ty + th - fillH, tw - 4, fillH, (tw - 4) / 2);
      ctx.fillStyle = col; ctx.fill();
    }

    ctx.beginPath(); ctx.arc(tx, ty + th + tw * 0.68, tw * 0.74, 0, Math.PI * 2);
    ctx.fillStyle = col; ctx.shadowColor = col; ctx.shadowBlur = 10; ctx.fill(); ctx.shadowBlur = 0;

    ctx.strokeStyle = 'rgba(150,185,255,0.3)'; ctx.lineWidth = 1;
    for (let deg = 20; deg <= 150; deg += 20) {
      const fy = ty + th - th * (deg - 10) / 140;
      ctx.beginPath(); ctx.moveTo(tx + tw / 2, fy); ctx.lineTo(tx + tw / 2 + 5, fy); ctx.stroke();
    }

    ctx.font = 'bold 10.5px monospace'; ctx.fillStyle = 'rgba(195,215,255,0.82)';
    ctx.textAlign = 'center';
    ctx.fillText(Math.round(this._temp) + '°C', tx, ty + th + tw * 2 + 17);
    ctx.fillText('T', tx, ty - 5);
    ctx.textAlign = 'left';
  }

  /* ── pH-полоска ── */
  _drawPHStrip(ctx) {
    const { _g: g } = this;
    const px = g.cx - g.r - 44;
    const py = g.liqTop - 6;
    const pw = 14; const ph = 88;
    const hue = Math.round(this._pH / 14 * 270);
    const col = `hsl(${hue},80%,52%)`;

    _flask_rrect(ctx, px - pw / 2, py, pw, ph, 3);
    ctx.fillStyle = col; ctx.shadowColor = col; ctx.shadowBlur = 8; ctx.fill();
    ctx.shadowBlur = 0;
    ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 1; ctx.stroke();

    ctx.strokeStyle = 'rgba(0,0,0,0.25)'; ctx.lineWidth = 0.8;
    for (let i = 0; i <= 14; i += 2) {
      const ry = py + ph * (1 - i / 14);
      ctx.beginPath(); ctx.moveTo(px - pw / 2 + 2, ry); ctx.lineTo(px + pw / 2 - 2, ry); ctx.stroke();
    }

    ctx.font = 'bold 10.5px monospace'; ctx.fillStyle = 'rgba(195,215,255,0.82)';
    ctx.textAlign = 'center';
    ctx.fillText('pH', px, py - 5);
    ctx.fillText(this._pH.toFixed(1), px, py + ph + 15);
    ctx.textAlign = 'left';
  }

  /* ── Бар H₂ ── */
  _drawH2Bar(ctx) {
    const { _g: g } = this;
    const bx = g.cx - 46, by = g.nt - 30;
    const bw = 92, bh = 10;

    _flask_rrect(ctx, bx, by, bw, bh, 4);
    ctx.fillStyle = 'rgba(255,255,255,0.07)'; ctx.fill();

    if (this._h2 > 0) {
      const col = this._ignited ? '#EF476F' : '#4CC9F0';
      ctx.shadowColor = col; ctx.shadowBlur = this._h2 > 0.45 ? 10 : 4;
      _flask_rrect(ctx, bx, by, bw * this._h2, bh, 4);
      ctx.fillStyle = col; ctx.fill(); ctx.shadowBlur = 0;
    }

    ctx.font = '10px monospace'; ctx.fillStyle = 'rgba(190,215,255,0.72)';
    ctx.textAlign = 'center'; ctx.fillText('H₂', g.cx, by - 5); ctx.textAlign = 'left';

    if (this._h2 > 0.65 && !this._ignited) {
      ctx.font = 'bold 10px sans-serif'; ctx.fillStyle = '#FFD166';
      ctx.textAlign = 'center';
      ctx.fillText('Поднести огонь!', g.cx, by - 16);
      ctx.textAlign = 'left';
    }
    if (this._ignited) {
      ctx.font = 'bold 10px sans-serif'; ctx.fillStyle = '#EF476F';
      ctx.textAlign = 'center'; ctx.fillText('H₂ воспламенился!', g.cx, by - 16); ctx.textAlign = 'left';
    }
  }

  /* ── Информационная панель ── */
  _drawInfoPanel(ctx) {
    const { _g: g, W } = this;
    const eq  = FlaskSim.EQ[`${this.metalType}_${this.acidType}`] || '—';
    const eqY = g.cy + g.r + 26;

    ctx.font = '12.5px monospace'; ctx.fillStyle = 'rgba(185,215,255,0.78)';
    ctx.textAlign = 'center'; ctx.fillText(eq, W * 0.44, eqY); ctx.textAlign = 'left';

    if (this._passiv) {
      ctx.font = 'bold 11px sans-serif'; ctx.fillStyle = '#FFD166';
      ctx.textAlign = 'center';
      ctx.fillText('Пассивация: Fe покрыт оксидной плёнкой — реакция прекратилась', W * 0.44, eqY + 19);
      ctx.textAlign = 'left';
    }

    if (this._metal && this._metal.mass > 0.1 && this._rxRate > 0) {
      const bx = g.cx - g.r, by = g.cy + g.r - 6;
      const bw = g.r * 2;
      _flask_rrect(ctx, bx, by, bw, 5, 2);
      ctx.fillStyle = 'rgba(255,255,255,0.07)'; ctx.fill();
      const col = this._rxRate > 0.6 ? '#EF476F' : this._rxRate > 0.3 ? '#FFD166' : '#7BF5A4';
      _flask_rrect(ctx, bx, by, bw * this._rxRate, 5, 2);
      ctx.fillStyle = col; ctx.shadowColor = col; ctx.shadowBlur = 4; ctx.fill(); ctx.shadowBlur = 0;
    }
  }

  _drawHint(ctx) {
    const { _g: g } = this;
    ctx.font = '13px sans-serif'; ctx.fillStyle = 'rgba(185,210,255,0.32)';
    ctx.textAlign = 'center';
    ctx.fillText('Нажмите «Бросить металл» для начала реакции', g.cx, g.cy + 14);
    ctx.textAlign = 'left';
  }

  /* ── Вспомогательные ──────────────────────────────────────────── */

  _tint(hex, d) {
    const n = parseInt(hex.slice(1), 16);
    const c = v => Math.max(0, Math.min(255, v));
    return `rgb(${c((n >> 16) + d)},${c(((n >> 8) & 255) + d)},${c((n & 255) + d)})`;
  }

  info() {
    const m  = this._metal;
    const md = m ? FlaskSim.METALS[m.type] : null;
    return {
      metal:  md?.name ?? '—',
      mass:   m ? m.mass.toFixed(2) : '0',
      temp:   this._temp.toFixed(1),
      pH:     this._pH.toFixed(2),
      h2pct:  (this._h2 * 100).toFixed(0),
      rate:   (this._rxRate * 100).toFixed(0),
      reacts: md ? md.acids.includes(this.acidType) : false,
    };
  }
}

/* ── Util: скруглённый прямоугольник ─────────────────────────── */
function _flask_rrect(ctx, x, y, w, h, r) {
  if (w <= 0 || h <= 0) return;
  r = Math.min(r, w / 2, h / 2);
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y,     x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x,     y + h, r);
  ctx.arcTo(x,     y + h, x,     y,     r);
  ctx.arcTo(x,     y,     x + w, y,     r);
  ctx.closePath();
}
