'use strict';

/* ═══════════════════════════════════════════════════════════════════════
   StoichSim — «Стехиометрия»
   Single-page dashboard: селектор + реагенты + продукты + канва + итоги
   ═══════════════════════════════════════════════════════════════════════ */

class StoichSim {

  /* ── Рецепты реакций ─────────────────────────────────────────────── */
  static RECIPES = [
    {
      name: 'Zn + 2HCl → ZnCl₂ + H₂↑',
      label: 'Zn + HCl',
      reactants: [
        { sym: 'Zn',  coef: 1, M: 65.38,  phase: 's',  color: '#9BB8CC' },
        { sym: 'HCl', coef: 2, M: 36.46,  phase: 'aq', color: '#78D278' },
      ],
      products: [
        { sym: 'ZnCl₂', coef: 1, M: 136.28, phase: 'aq', color: '#4CC9F0' },
        { sym: 'H₂',    coef: 1, M: 2.016,  phase: 'g',  color: '#FFD166' },
      ],
    },
    {
      name: '2H₂ + O₂ → 2H₂O',
      label: 'H₂ + O₂',
      reactants: [
        { sym: 'H₂', coef: 2, M: 2.016,  phase: 'g', color: '#FFD166' },
        { sym: 'O₂', coef: 1, M: 31.998, phase: 'g', color: '#EF476F' },
      ],
      products: [
        { sym: 'H₂O', coef: 2, M: 18.015, phase: 'l', color: '#6EB4D7' },
      ],
    },
    {
      name: 'CH₄ + 2O₂ → CO₂ + 2H₂O',
      label: 'Горение метана',
      reactants: [
        { sym: 'CH₄', coef: 1, M: 16.043, phase: 'g', color: '#FFD166' },
        { sym: 'O₂',  coef: 2, M: 31.998, phase: 'g', color: '#EF476F' },
      ],
      products: [
        { sym: 'CO₂', coef: 1, M: 44.01,  phase: 'g', color: '#9B5DE5' },
        { sym: 'H₂O', coef: 2, M: 18.015, phase: 'g', color: '#6EB4D7' },
      ],
    },
    {
      name: 'N₂ + 3H₂ → 2NH₃',
      label: 'Синтез аммиака',
      reactants: [
        { sym: 'N₂', coef: 1, M: 28.014, phase: 'g', color: '#9B5DE5' },
        { sym: 'H₂', coef: 3, M: 2.016,  phase: 'g', color: '#FFD166' },
      ],
      products: [
        { sym: 'NH₃', coef: 2, M: 17.031, phase: 'g', color: '#06D6E0' },
      ],
    },
    {
      name: '2Al + 3CuSO₄ → Al₂(SO₄)₃ + 3Cu',
      label: 'Al + CuSO₄',
      reactants: [
        { sym: 'Al',    coef: 2, M: 26.982, phase: 's',  color: '#D6D6D6' },
        { sym: 'CuSO₄', coef: 3, M: 159.60, phase: 'aq', color: '#4CC9F0' },
      ],
      products: [
        { sym: 'Al₂(SO₄)₃', coef: 1, M: 342.15, phase: 'aq', color: '#B8D4F0' },
        { sym: 'Cu',         coef: 3, M: 63.546, phase: 's',  color: '#C87840' },
      ],
    },
    {
      name: '2Mg + O₂ → 2MgO',
      label: 'Горение магния',
      reactants: [
        { sym: 'Mg', coef: 2, M: 24.305, phase: 's', color: '#E8E8E8' },
        { sym: 'O₂', coef: 1, M: 31.998, phase: 'g', color: '#EF476F' },
      ],
      products: [
        { sym: 'MgO', coef: 2, M: 40.304, phase: 's', color: '#FFFFFF' },
      ],
    },
    {
      name: 'CaCO₃ → CaO + CO₂↑',
      label: 'Разложение мела',
      reactants: [
        { sym: 'CaCO₃', coef: 1, M: 100.086, phase: 's', color: '#F0F0F0' },
      ],
      products: [
        { sym: 'CaO', coef: 1, M: 56.077, phase: 's', color: '#D4C4A0' },
        { sym: 'CO₂', coef: 1, M: 44.01,  phase: 'g', color: '#9B5DE5' },
      ],
    },
    {
      name: 'HCl + NaOH → NaCl + H₂O',
      label: 'Нейтрализация',
      reactants: [
        { sym: 'HCl',  coef: 1, M: 36.46, phase: 'aq', color: '#78D278' },
        { sym: 'NaOH', coef: 1, M: 40.0,  phase: 'aq', color: '#7BF5A4' },
      ],
      products: [
        { sym: 'NaCl', coef: 1, M: 58.44,  phase: 'aq', color: '#FFFFFF' },
        { sym: 'H₂O',  coef: 1, M: 18.015, phase: 'l',  color: '#6EB4D7' },
      ],
    },
    {
      name: '2KMnO₄ → K₂MnO₄ + MnO₂ + O₂↑',
      label: 'Разложение KMnO₄',
      reactants: [
        { sym: 'KMnO₄', coef: 2, M: 158.034, phase: 's', color: '#9B59B6' },
      ],
      products: [
        { sym: 'K₂MnO₄', coef: 1, M: 197.132, phase: 's', color: '#27AE60' },
        { sym: 'MnO₂',   coef: 1, M: 86.937,  phase: 's', color: '#8899AA' },
        { sym: 'O₂',     coef: 1, M: 31.998,  phase: 'g', color: '#EF476F' },
      ],
    },
    {
      name: 'C₂H₅OH + 3O₂ → 2CO₂ + 3H₂O',
      label: 'Горение спирта',
      reactants: [
        { sym: 'C₂H₅OH', coef: 1, M: 46.068, phase: 'l', color: '#FFD166' },
        { sym: 'O₂',     coef: 3, M: 31.998, phase: 'g', color: '#EF476F' },
      ],
      products: [
        { sym: 'CO₂', coef: 2, M: 44.01,  phase: 'g', color: '#9B5DE5' },
        { sym: 'H₂O', coef: 3, M: 18.015, phase: 'g', color: '#6EB4D7' },
      ],
    },
  ];

  /* ── Конструктор ─────────────────────────────────────────────────── */
  constructor(container) {
    this._container = container;
    this._recipeIdx = 0;
    this._amounts   = [];
    this._inputMode = [];
    this._computed  = null;
    this._animState = 'idle';
    this._animT     = 0;
    this._raf       = null;
    this._idleRaf   = null;
    this._idleT     = 0;          // continuous time for wobble/bubbles
    this._lastIdleTs = 0;
    this._sliderPulse = 0;        // 0..1, decays after slider change
    this._canvas    = null;
    this._ctx       = null;
    this._ro        = null;
    this._W         = 0;
    this._H         = 0;

    this._initAmounts();
    this._build();
    this._startIdleLoop();
  }

  /* ── Непрерывный анимационный цикл (волны, пузырьки) ───────────── */
  _startIdleLoop() {
    if (this._idleRaf) return;
    this._lastIdleTs = performance.now();
    const loop = (now) => {
      const dt = Math.min((now - this._lastIdleTs) / 1000, 0.05);
      this._lastIdleTs = now;
      this._idleT += dt;
      if (this._sliderPulse > 0) this._sliderPulse = Math.max(0, this._sliderPulse - dt * 2);
      /* skip redraw if reaction animation owns RAF */
      if (this._animState !== 'reacting' && this._ctx) {
        this._draw();
      }
      this._idleRaf = requestAnimationFrame(loop);
    };
    this._idleRaf = requestAnimationFrame(loop);
  }

  /* ── Инициализация начальных количеств ───────────────────────────── */
  _initAmounts() {
    const r = StoichSim.RECIPES[this._recipeIdx];
    this._amounts   = r.reactants.map(re => re.M);
    this._inputMode = r.reactants.map(() => 'mass');
    this._compute();
  }

  /* ── Построение всего интерфейса ─────────────────────────────────── */
  _build() {
    const c = this._container;
    c.innerHTML = '';
    c.style.cssText = [
      'display:flex',
      'flex-direction:column',
      'height:100%',
      'overflow:hidden',
      'background:#0D0D1A',
      'font-family:Manrope,sans-serif',
    ].join(';');

    /* 1. Topbar */
    this._topbar = _stEl('div', {
      style: [
        'flex:0 0 auto',
        'display:flex',
        'align-items:center',
        'gap:10px',
        'padding:0 14px',
        'height:50px',
        'background:rgba(255,255,255,0.04)',
        'border-bottom:1px solid rgba(255,255,255,0.08)',
        'min-width:0',
      ].join(';'),
    });
    c.appendChild(this._topbar);

    /* 2. Основная двухколоночная область */
    this._mainArea = _stEl('div', {
      style: [
        'display:flex',
        'flex:1 1 auto',
        'overflow:hidden',
        'min-height:0',
      ].join(';'),
    });
    c.appendChild(this._mainArea);

    /* 3. Канва */
    this._canvasWrap = _stEl('div', {
      style: [
        'flex:0 0 180px',
        'position:relative',
        'width:100%',
        'background:#0D0D1A',
        'border-top:1px solid rgba(255,255,255,0.08)',
      ].join(';'),
    });
    c.appendChild(this._canvasWrap);

    /* 4. Нижняя панель итогов */
    this._summaryBar = _stEl('div', {
      style: [
        'flex:0 0 auto',
        'display:flex',
        'align-items:center',
        'flex-wrap:wrap',
        'gap:6px',
        'padding:8px 14px',
        'background:rgba(255,255,255,0.03)',
        'border-top:1px solid rgba(255,255,255,0.08)',
      ].join(';'),
    });
    c.appendChild(this._summaryBar);

    this._renderTopbar();
    this._renderMainArea();
    this._renderCanvas();
    this._renderSummary();
  }

  /* ── Topbar ─────────────────────────────────────────────────────── */
  _renderTopbar() {
    const bar = this._topbar;
    bar.innerHTML = '';

    /* Селектор реакции */
    const sel = document.createElement('select');
    sel.style.cssText = [
      'padding:6px 12px',
      'border-radius:7px',
      'background:rgba(255,255,255,0.08)',
      'color:rgba(255,255,255,0.92)',
      'border:1px solid rgba(255,255,255,0.15)',
      'font-size:0.92rem',
      'font-family:Manrope,sans-serif',
      'cursor:pointer',
      'flex:0 0 auto',
      'max-width:180px',
    ].join(';');
    StoichSim.RECIPES.forEach((rc, i) => {
      const opt = document.createElement('option');
      opt.value = String(i);
      opt.textContent = rc.label;
      if (i === this._recipeIdx) opt.selected = true;
      sel.appendChild(opt);
    });
    sel.addEventListener('change', () => {
      this._recipeIdx = parseInt(sel.value, 10);
      this._animState = 'idle';
      this._animT     = 0;
      this._initAmounts();
      this._rebuildAll();
      if (window.LabFX) LabFX.sound.play('click', { pitch: 1.3 });
    });
    bar.appendChild(sel);

    /* Уравнение реакции */
    this._eqLabel = _stEl('div', {
      style: [
        'flex:1 1 0',
        'font-size:1.1rem',
        'color:rgba(255,255,255,0.95)',
        'font-weight:700',
        'white-space:nowrap',
        'overflow:hidden',
        'text-overflow:ellipsis',
        'min-width:0',
        'padding:0 6px',
      ].join(';'),
      textContent: StoichSim.RECIPES[this._recipeIdx].name,
    });
    bar.appendChild(this._eqLabel);

    /* Кнопка сброса */
    const resetBtn = _stEl('button', {
      style: [
        'flex:0 0 auto',
        'padding:5px 12px',
        'border-radius:6px',
        'background:rgba(255,255,255,0.07)',
        'color:rgba(255,255,255,0.75)',
        'border:1px solid rgba(255,255,255,0.14)',
        'font-size:0.85rem',
        'font-family:Manrope,sans-serif',
        'cursor:pointer',
        'white-space:nowrap',
      ].join(';'),
      textContent: 'Сброс',
    });
    resetBtn.addEventListener('click', () => {
      this._animState = 'idle';
      this._animT     = 0;
      this._initAmounts();
      this._rebuildAll();
      if (window.LabFX) LabFX.sound.play('click');
    });
    bar.appendChild(resetBtn);
  }

  /* ── Перестройка всего при смене реакции ─────────────────────────── */
  _rebuildAll() {
    const r = StoichSim.RECIPES[this._recipeIdx];
    this._eqLabel.textContent = r.name;
    this._renderMainArea();
    this._renderSummary();
    this._draw();
  }

  /* ── Двухколоночная область: реагенты + продукты ─────────────────── */
  _renderMainArea() {
    const area = this._mainArea;
    area.innerHTML = '';

    const colStyle = [
      'flex:1 1 0',
      'overflow-y:auto',
      'overflow-x:hidden',
      'padding:14px 12px',
      'display:flex',
      'flex-direction:column',
      'gap:12px',
      'min-width:0',
    ].join(';');

    /* Левая колонка: реагенты */
    const leftCol = _stEl('div', { style: colStyle });
    leftCol.appendChild(this._sectionHeader('РЕАГЕНТЫ'));
    this._reactantCards = [];
    StoichSim.RECIPES[this._recipeIdx].reactants.forEach((re, i) => {
      const card = this._buildReactantCard(re, i);
      this._reactantCards.push(card);
      leftCol.appendChild(card.el);
    });

    /* Разделитель */
    const divider = _stEl('div', {
      style: [
        'flex:0 0 1px',
        'width:1px',
        'background:rgba(255,255,255,0.08)',
        'align-self:stretch',
      ].join(';'),
    });

    /* Правая колонка: продукты */
    const rightCol = _stEl('div', { style: colStyle });
    rightCol.appendChild(this._sectionHeader('ПРОДУКТЫ'));
    this._productCardEls = [];
    StoichSim.RECIPES[this._recipeIdx].products.forEach((pr, i) => {
      const el = this._buildProductCard(pr, i);
      this._productCardEls.push(el);
      rightCol.appendChild(el);
    });

    area.appendChild(leftCol);
    area.appendChild(divider);
    area.appendChild(rightCol);
  }

  /* ── Заголовок секции ─────────────────────────────────────────────── */
  _sectionHeader(text) {
    return _stEl('div', {
      style: [
        'font-size:0.78rem',
        'font-weight:700',
        'color:rgba(255,255,255,0.6)',
        'text-transform:uppercase',
        'letter-spacing:0.07em',
        'padding-bottom:4px',
        'border-bottom:1px solid rgba(255,255,255,0.07)',
      ].join(';'),
      textContent: text,
    });
  }

  /* ── Карточка реагента ───────────────────────────────────────────── */
  _buildReactantCard(re, i) {
    const card = _stEl('div', {
      style: [
        'padding:12px',
        'border-radius:8px',
        'background:rgba(255,255,255,0.05)',
        'border:1px solid rgba(255,255,255,0.1)',
        'display:flex',
        'flex-direction:column',
        'gap:8px',
      ].join(';'),
    });

    /* Строка 1: символ + переключатель единиц */
    const row1 = _stEl('div', {
      style: 'display:flex;align-items:center;justify-content:space-between;gap:8px;',
    });
    const symEl = _stEl('div', {
      style: `font-size:1.15rem;font-weight:700;color:${re.color};`,
      textContent: re.sym,
    });
    row1.appendChild(symEl);

    /* Pill-переключатель единиц */
    const pill = _stEl('div', {
      style: [
        'display:flex',
        'border-radius:6px',
        'overflow:hidden',
        'border:1px solid rgba(255,255,255,0.12)',
        'flex:0 0 auto',
      ].join(';'),
    });

    const modes = [['mass', 'г'], ['mol', 'моль'], ...(re.phase === 'g' ? [['vol', 'л']] : [])];
    const modeButtons = {};

    const updateMode = (newMode) => {
      this._inputMode[i] = newMode;
      modes.forEach(([m]) => {
        const btn = modeButtons[m];
        if (!btn) return;
        const active = m === newMode;
        btn.style.cssText = this._pillBtnStyle(active);
      });
      this._syncSlider(i, re, sliderEl, valEl);
      this._updateAfterSlider();
    };

    modes.forEach(([m, label]) => {
      const btn = _stEl('button', {
        style: this._pillBtnStyle(this._inputMode[i] === m),
        textContent: label,
      });
      btn.addEventListener('click', () => updateMode(m));
      modeButtons[m] = btn;
      pill.appendChild(btn);
    });
    row1.appendChild(pill);
    card.appendChild(row1);

    /* Строка 2: ползунок + значение */
    const row2 = _stEl('div', {
      style: 'display:flex;align-items:center;gap:8px;',
    });

    const slParams = this._sliderParams(i, re);
    const sliderEl = document.createElement('input');
    sliderEl.type  = 'range';
    sliderEl.min   = String(slParams.min);
    sliderEl.max   = String(slParams.max);
    sliderEl.step  = String(slParams.step);
    sliderEl.value = String(slParams.val);
    sliderEl.style.cssText = 'flex:1 1 0;accent-color:#9B5DE5;cursor:pointer;height:6px;min-width:0;';

    const valEl = _stEl('div', {
      style: [
        'font-size:1.0rem',
        'font-weight:700',
        'color:#FFD166',
        'min-width:80px',
        'text-align:right',
        'flex:0 0 auto',
        'white-space:nowrap',
      ].join(';'),
      textContent: this._fmtSliderVal(i, re),
    });

    sliderEl.addEventListener('input', () => {
      const v = parseFloat(sliderEl.value);
      const mode = this._inputMode[i];
      if (mode === 'mass')     this._amounts[i] = v;
      else if (mode === 'mol') this._amounts[i] = v * re.M;
      else                     this._amounts[i] = (v / 22.4) * re.M;
      valEl.textContent = this._fmtSliderVal(i, re);
      this._updateAfterSlider();
    });

    row2.appendChild(sliderEl);
    row2.appendChild(valEl);
    card.appendChild(row2);

    /* Бейдж лимита (скрытый изначально) */
    const limitBadge = _stEl('div', {
      style: [
        'display:none',
        'padding:2px 6px',
        'border-radius:4px',
        'background:rgba(239,71,111,0.2)',
        'color:#EF476F',
        'font-size:0.72rem',
        'font-weight:700',
        'width:fit-content',
      ].join(';'),
      textContent: 'ЛИМИТ',
    });
    card.appendChild(limitBadge);

    return { el: card, limitBadge, sliderEl, valEl, re, i };
  }

  /* ── Pill-кнопка стиль ───────────────────────────────────────────── */
  _pillBtnStyle(active) {
    return [
      'padding:3px 8px',
      'font-size:0.78rem',
      'font-weight:600',
      'cursor:pointer',
      'border:none',
      'font-family:Manrope,sans-serif',
      'transition:background .12s',
      active
        ? 'background:rgba(155,93,229,0.55);color:#fff'
        : 'background:rgba(255,255,255,0.05);color:rgba(255,255,255,0.65)',
    ].join(';');
  }

  /* ── Карточка продукта ───────────────────────────────────────────── */
  _buildProductCard(pr, i) {
    const comp = this._computed;
    const q = comp ? comp.productQ[i] : { n: 0, m: 0, v: 0 };

    const card = _stEl('div', {
      style: [
        'padding:12px',
        'border-radius:8px',
        'background:rgba(255,255,255,0.05)',
        'border:1px solid rgba(255,255,255,0.1)',
        'display:flex',
        'flex-direction:column',
        'gap:6px',
      ].join(';'),
    });
    card.setAttribute('data-prod-idx', i);

    /* Строка 1: символ + бейдж (газ) */
    const row1 = _stEl('div', {
      style: 'display:flex;align-items:center;gap:8px;',
    });
    row1.appendChild(_stEl('div', {
      style: `font-size:1.15rem;font-weight:700;color:${pr.color};`,
      textContent: pr.sym,
    }));
    if (pr.phase === 'g') {
      row1.appendChild(_stEl('div', {
        style: [
          'padding:2px 7px',
          'border-radius:4px',
          'background:rgba(155,93,229,0.15)',
          'color:#9B5DE5',
          'font-size:0.78rem',
          'font-weight:600',
        ].join(';'),
        textContent: 'газ',
      }));
    }
    card.appendChild(row1);

    /* Строки данных */
    card.appendChild(this._dataRow('n =', q.n.toFixed(3) + ' моль'));
    card.appendChild(this._dataRow('m =', q.m.toFixed(3) + ' г'));
    if (pr.phase === 'g') {
      card.appendChild(this._dataRow('V =', q.v.toFixed(3) + ' л (н.у.)'));
    }

    return card;
  }

  /* ── Строка данных label: value ───────────────────────────────────── */
  _dataRow(label, value) {
    const row = _stEl('div', {
      style: 'display:flex;align-items:baseline;gap:6px;',
    });
    row.appendChild(_stEl('span', {
      style: 'font-size:0.85rem;color:rgba(255,255,255,0.65);min-width:28px;',
      textContent: label,
    }));
    const valEl = _stEl('span', {
      style: 'font-size:0.95rem;color:rgba(255,255,255,0.92);font-weight:600;',
      textContent: value,
    });
    row.appendChild(valEl);
    return row;
  }

  /* ── Обновление после изменения ползунка ─────────────────────────── */
  _updateAfterSlider() {
    this._compute();
    this._updateProductCards();
    this._updateLimitBadges();
    this._renderSummary();
    this._draw();
  }

  /* ── Обновление карточек продуктов ───────────────────────────────── */
  _updateProductCards() {
    const r    = StoichSim.RECIPES[this._recipeIdx];
    const comp = this._computed;
    if (!comp) return;

    this._productCardEls.forEach((card, i) => {
      const pr = r.products[i];
      const q  = comp.productQ[i];
      const rows = card.querySelectorAll('[data-data-row]');
      /* Пересоздаём строки с новыми значениями */
      const existingRows = Array.from(card.querySelectorAll('[data-data-row]'));
      existingRows.forEach(el => el.remove());

      const newRows = [];
      newRows.push(this._dataRow('n =', q.n.toFixed(3) + ' моль'));
      newRows.push(this._dataRow('m =', q.m.toFixed(3) + ' г'));
      if (pr.phase === 'g') {
        newRows.push(this._dataRow('V =', q.v.toFixed(3) + ' л (н.у.)'));
      }
      newRows.forEach(r => {
        r.setAttribute('data-data-row', '1');
        card.appendChild(r);
      });
    });

    /* Повторный проход — удалить старые, добавить свежие */
    /* (карточки пересобираются полностью) */
    const area   = this._mainArea;
    const cols   = area.children;
    if (cols.length < 3) return;
    const rightCol = cols[2];

    /* Убираем всё кроме заголовка */
    while (rightCol.children.length > 1) {
      rightCol.removeChild(rightCol.lastChild);
    }

    StoichSim.RECIPES[this._recipeIdx].products.forEach((pr, i) => {
      const el = this._buildProductCard(pr, i);
      this._productCardEls[i] = el;
      rightCol.appendChild(el);
    });
  }

  /* ── Обновление бейджей лимита ───────────────────────────────────── */
  _updateLimitBadges() {
    if (!this._computed || !this._reactantCards) return;
    this._reactantCards.forEach((card, i) => {
      card.limitBadge.style.display = (i === this._computed.limitIdx) ? 'block' : 'none';
      /* Подсветка карточки лимитирующего реагента */
      card.el.style.borderColor = (i === this._computed.limitIdx)
        ? 'rgba(239,71,111,0.5)'
        : 'rgba(255,255,255,0.1)';
    });
  }

  /* ── Canvas-область ─────────────────────────────────────────────── */
  _renderCanvas() {
    const wrap = this._canvasWrap;
    wrap.innerHTML = '';

    this._canvas = document.createElement('canvas');
    this._canvas.style.cssText = 'display:block;width:100%;height:180px;';
    wrap.appendChild(this._canvas);

    /* Кнопка запуска анимации */
    this._animBtn = _stEl('button', {
      style: [
        'position:absolute',
        'right:14px',
        'bottom:10px',
        'padding:7px 18px',
        'border-radius:8px',
        'background:linear-gradient(135deg,#9B5DE5,#4CC9F0)',
        'color:#fff',
        'font-size:0.9rem',
        'font-weight:700',
        'border:none',
        'cursor:pointer',
        'font-family:Manrope,sans-serif',
        'z-index:1',
      ].join(';'),
    });
    this._animBtn.textContent = 'Запустить реакцию';
    this._animBtn.addEventListener('click', () => {
      this._startAnim();
    });
    wrap.appendChild(this._animBtn);

    requestAnimationFrame(() => {
      this._ctx = this._canvas.getContext('2d');
      if (window.ResizeObserver) {
        if (this._ro) this._ro.disconnect();
        this._ro = new ResizeObserver(() => { this._fitCanvas(); this._draw(); });
        this._ro.observe(this._canvas);
      }
      this._fitCanvas();
      this._draw();
    });
  }

  /* ── Нижняя панель итогов ─────────────────────────────────────────── */
  _renderSummary() {
    const bar = this._summaryBar;
    bar.innerHTML = '';

    const r    = StoichSim.RECIPES[this._recipeIdx];
    const comp = this._computed;
    if (!comp) return;

    const limRe = r.reactants[comp.limitIdx];
    const chips = [];

    /* Лимит */
    chips.push({ label: 'Лимит:', value: limRe.sym, color: '#EF476F' });

    /* Избытки */
    r.reactants.forEach((re, i) => {
      if (i !== comp.limitIdx) {
        const excessM = (comp.reactantQ[i].nExcess * re.M).toFixed(2);
        chips.push({ label: 'Избыток ' + re.sym + ':', value: excessM + ' г', color: '#FFD166' });
      }
    });

    /* Теоретический выход */
    const totalProdM = comp.productQ.reduce((s, q) => s + q.m, 0);
    chips.push({ label: 'Выход теор.:', value: totalProdM.toFixed(3) + ' г', color: '#06D6E0' });

    /* Суммарный объём газов */
    const totalGasV = r.products.reduce((s, pr, i) =>
      s + (pr.phase === 'g' ? comp.productQ[i].v : 0), 0
    );
    if (totalGasV > 0.0001) {
      chips.push({ label: 'Газов:', value: totalGasV.toFixed(3) + ' л', color: '#9B5DE5' });
    }

    chips.forEach((chip, idx) => {
      if (idx > 0) {
        bar.appendChild(_stEl('span', {
          style: 'color:rgba(255,255,255,0.3);font-size:0.9rem;',
          textContent: '|',
        }));
      }
      const chipEl = _stEl('span', {
        style: 'display:inline-flex;align-items:center;gap:4px;font-size:0.9rem;',
      });
      chipEl.appendChild(_stEl('span', {
        style: 'color:rgba(255,255,255,0.65);',
        textContent: chip.label,
      }));
      chipEl.appendChild(_stEl('span', {
        style: `color:${chip.color};font-weight:700;`,
        textContent: chip.value,
      }));
      bar.appendChild(chipEl);
    });
  }

  /* ── Вспомогательные методы слайдера ─────────────────────────────── */
  _sliderParams(i, re) {
    const mode = this._inputMode[i];
    if (mode === 'mol') {
      return { min: 0.01, max: 10, step: 0.01, val: +(this._amounts[i] / re.M).toFixed(4) };
    } else if (mode === 'vol') {
      return { min: 0.1, max: 100, step: 0.1, val: +(this._amounts[i] / re.M * 22.4).toFixed(3) };
    }
    return {
      min:  +(re.M * 0.1).toFixed(2),
      max:  +(re.M * 10).toFixed(0),
      step: +(re.M * 0.01).toFixed(2),
      val:  +this._amounts[i].toFixed(4),
    };
  }

  _syncSlider(i, re, sliderEl, valEl) {
    const p = this._sliderParams(i, re);
    sliderEl.min   = String(p.min);
    sliderEl.max   = String(p.max);
    sliderEl.step  = String(p.step);
    sliderEl.value = String(p.val);
    valEl.textContent = this._fmtSliderVal(i, re);
  }

  _fmtSliderVal(i, re) {
    const mode = this._inputMode[i];
    if (mode === 'mol')     return (this._amounts[i] / re.M).toFixed(3) + ' моль';
    else if (mode === 'vol') return (this._amounts[i] / re.M * 22.4).toFixed(3) + ' л';
    return this._amounts[i].toFixed(2) + ' г';
  }

  /* ── Расчёт стехиометрии ─────────────────────────────────────────── */
  _compute() {
    const r = StoichSim.RECIPES[this._recipeIdx];

    const ratios   = r.reactants.map((re, i) => (this._amounts[i] / re.M) / re.coef);
    const limitVal = Math.min(...ratios);
    const limitIdx = ratios.indexOf(limitVal);

    const reactantQ = r.reactants.map((re, i) => {
      const nConsumed = limitVal * re.coef;
      const nActual   = this._amounts[i] / re.M;
      return {
        n:       nConsumed,
        m:       nConsumed * re.M,
        v:       nConsumed * 22.4,
        nExcess: nActual - nConsumed,
        mExcess: (nActual - nConsumed) * re.M,
        vExcess: (nActual - nConsumed) * 22.4,
      };
    });

    const productQ = r.products.map(pr => {
      const nProd = limitVal * pr.coef;
      return { n: nProd, m: nProd * pr.M, v: nProd * 22.4 };
    });

    const prevLimitIdx = this._computed ? this._computed.limitIdx : -1;
    this._computed = { limitIdx, limitVal, ratios, reactantQ, productQ };

    if (window.LabFX && prevLimitIdx !== -1 && prevLimitIdx !== limitIdx) {
      LabFX.haptic(20);
      LabFX.sound.play('tick', { pitch: 0.8, volume: 0.3 });
    }
  }

  /* ── Canvas ─────────────────────────────────────────────────────── */
  _fitCanvas() {
    const cv = this._canvas;
    if (!cv) return;
    const dpr = window.devicePixelRatio || 1;
    const w = cv.clientWidth;
    const h = cv.clientHeight;
    if (!w || !h) return;
    if (cv.width !== Math.round(w * dpr) || cv.height !== Math.round(h * dpr)) {
      cv.width  = Math.round(w * dpr);
      cv.height = Math.round(h * dpr);
      this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    this._W = w;
    this._H = h;
  }

  _draw() {
    const ctx = this._ctx;
    if (!ctx) return;
    const W = this._W || (this._canvas ? this._canvas.clientWidth : 0);
    const H = this._H || (this._canvas ? this._canvas.clientHeight : 0);
    if (!W || !H) return;

    ctx.clearRect(0, 0, W, H);
    ctx.fillStyle = '#0D0D1A';
    ctx.fillRect(0, 0, W, H);

    const r    = StoichSim.RECIPES[this._recipeIdx];
    const comp = this._computed;
    if (!comp) return;

    const allSubs = [
      ...r.reactants.map((s, i) => ({ s, i, isReactant: true,  q: comp.reactantQ[i] })),
      ...r.products.map((s, i)  => ({ s, i, isReactant: false, q: comp.productQ[i]  })),
    ];

    const N      = allSubs.length;
    const gap    = 10;
    const boxW   = Math.min(Math.floor((W - (N + 1) * gap) / N), 140);
    const boxH   = Math.min(H - 28, 150);
    const totalW = N * boxW + (N - 1) * gap;
    const startX = (W - totalW) / 2;
    const topY   = (H - boxH) / 2 - 6;

    const sepIdx = r.reactants.length;
    const animT  = this._animState === 'reacting' ? this._animT
      : (this._animState === 'done' ? 1 : 0);

    allSubs.forEach(({ s, i, isReactant, q }, k) => {
      const x = startX + k * (boxW + gap);

      /* Стрелка между реагентами и продуктами */
      if (k === sepIdx) {
        const arrowX = x - gap * 0.5;
        const midY   = topY + boxH / 2;
        const t = this._idleT || 0;
        const reacting = this._animState === 'reacting';
        ctx.save();
        if (reacting) {
          ctx.shadowColor = '#9B5DE5';
          ctx.shadowBlur = 12 + Math.sin(t * 12) * 6;
        }
        const alpha = reacting ? 0.85 + 0.15 * Math.sin(t * 12) : 0.35 + animT * 0.5;
        ctx.strokeStyle = `rgba(255,255,255,${alpha})`;
        ctx.lineWidth = reacting ? 3.2 : 2.5;
        ctx.beginPath();
        ctx.moveTo(arrowX - 18, midY);
        ctx.lineTo(arrowX + 2,  midY);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(arrowX - 7, midY - 7);
        ctx.lineTo(arrowX + 2, midY);
        ctx.lineTo(arrowX - 7, midY + 7);
        ctx.stroke();
        ctx.restore();
        /* sparks travelling along the arrow during reaction */
        if (reacting) {
          for (let s = 0; s < 3; s++) {
            const sp = ((t * 1.5 + s * 0.33) % 1);
            const sx = arrowX - 18 + sp * 20;
            ctx.save();
            ctx.fillStyle = `rgba(255,209,102,${1 - sp})`;
            ctx.beginPath();
            ctx.arc(sx, midY, 2.4, 0, Math.PI * 2);
            ctx.fill();
            ctx.restore();
          }
        }
      }

      const isLimit = isReactant && i === comp.limitIdx;
      this._drawBeaker(ctx, x, topY, boxW, boxH, s, q, isReactant, isLimit, animT);
    });
  }

  _drawBeaker(ctx, x, y, bw, bh, sub, q, isReactant, isLimit, animT) {
    ctx.save();

    const t = this._idleT || 0;

    /* Pulsing border for limit */
    const pulse = isLimit ? (0.5 + 0.5 * Math.sin(t * 3.2)) : 0;
    const borderColor = isLimit
      ? `rgba(239,71,111,${0.55 + 0.35 * pulse + animT * 0.3})`
      : 'rgba(255,255,255,0.14)';
    ctx.strokeStyle = borderColor;
    ctx.lineWidth   = isLimit ? 2 + pulse * 0.6 : 1;
    ctx.beginPath();
    _stRoundRect(ctx, x, y, bw, bh, 7);
    ctx.stroke();

    /* Glow for limit */
    if (isLimit) {
      ctx.save();
      ctx.shadowColor = '#EF476F';
      ctx.shadowBlur = 14 + pulse * 8;
      ctx.strokeStyle = 'rgba(239,71,111,0)';
      ctx.beginPath();
      _stRoundRect(ctx, x, y, bw, bh, 7);
      ctx.stroke();
      ctx.restore();
    }

    ctx.fillStyle = 'rgba(255,255,255,0.03)';
    ctx.fill();

    /* Header symbol */
    ctx.fillStyle = sub.color;
    ctx.font = 'bold 13px Manrope,sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(sub.sym, x + bw / 2, y + 18);

    const areaX = x + 8;
    const areaY = y + 26;
    const areaW = bw - 16;
    const areaH = bh - 46;

    /* Liquid level: scales with amount (reactants: m; products: n) */
    let fillFrac;
    if (isReactant) {
      fillFrac = Math.min(1, q.n / 0.25 + 0.18);
      fillFrac *= Math.max(0.15, 1 - animT);
    } else {
      fillFrac = Math.min(1, animT * 1.2) * Math.min(1, q.n / 0.25 + 0.18);
    }
    fillFrac = Math.max(0, Math.min(1, fillFrac));

    const isGas = sub.phase === 'g';
    const liquidH   = areaH * fillFrac;
    const liquidTop = areaY + areaH - liquidH;

    if (liquidH > 1) {
      /* wavy surface */
      ctx.save();
      const grad = ctx.createLinearGradient(0, liquidTop, 0, areaY + areaH);
      grad.addColorStop(0, sub.color + 'aa');
      grad.addColorStop(1, sub.color + '55');
      ctx.fillStyle = grad;

      ctx.beginPath();
      ctx.moveTo(areaX, liquidTop);
      const waveSeed = sub.sym.charCodeAt(0);
      for (let xi = 0; xi <= areaW; xi += 2) {
        const wave = Math.sin((xi / areaW) * Math.PI * 3 + t * 2 + waveSeed) * 1.4
                   + Math.sin((xi / areaW) * Math.PI * 5 + t * 1.3) * 0.8;
        ctx.lineTo(areaX + xi, liquidTop + wave);
      }
      ctx.lineTo(areaX + areaW, areaY + areaH);
      ctx.lineTo(areaX, areaY + areaH);
      ctx.closePath();
      ctx.fill();

      /* highlight on surface */
      ctx.strokeStyle = sub.color + 'cc';
      ctx.lineWidth = 1;
      ctx.beginPath();
      for (let xi = 0; xi <= areaW; xi += 2) {
        const wave = Math.sin((xi / areaW) * Math.PI * 3 + t * 2 + waveSeed) * 1.4
                   + Math.sin((xi / areaW) * Math.PI * 5 + t * 1.3) * 0.8;
        if (xi === 0) ctx.moveTo(areaX + xi, liquidTop + wave);
        else ctx.lineTo(areaX + xi, liquidTop + wave);
      }
      ctx.stroke();
      ctx.restore();

      /* bubbles for gas products / aq solutions */
      if (isGas || sub.phase === 'aq') {
        const seed2 = sub.sym.split('').reduce((a, ch) => a + ch.charCodeAt(0), 0);
        const nBub = isGas ? 7 : 3;
        ctx.save();
        ctx.fillStyle = 'rgba(255,255,255,0.45)';
        for (let b = 0; b < nBub; b++) {
          const phase = (t * (isGas ? 0.9 : 0.4) + b * 0.7 + seed2 * 0.13) % 1;
          const bx = areaX + areaW * (0.15 + 0.7 * _stLcg(seed2 + b * 11));
          const by = areaY + areaH - phase * liquidH;
          const br = isGas ? (1.4 + (1 - phase) * 1.6) : (1.0 + (1 - phase) * 0.8);
          ctx.beginPath();
          ctx.arc(bx, by, br, 0, Math.PI * 2);
          ctx.fill();
        }
        ctx.restore();
      }
    }

    /* particles flowing on reaction (kept from original logic, simplified) */
    if (animT > 0 && animT < 1) {
      const maxParticles = 16;
      const nParticles = Math.max(1, Math.round(Math.min(q.n / 0.2, 1) * maxParticles));
      const seedP = sub.sym.split('').reduce((a, ch) => a + ch.charCodeAt(0), 0);
      const alpha = isReactant ? Math.max(0, 1 - animT * 1.5) : Math.min(1, animT * 1.8);
      ctx.globalAlpha = alpha;
      ctx.fillStyle = sub.color;
      for (let p = 0; p < nParticles; p++) {
        const px = areaX + _stLcg(seedP + p * 7) * areaW;
        const py = areaY + _stLcg(seedP + p * 7 + 3) * areaH;
        const jx = isReactant ? (x + bw / 2 - px) * animT       : 0;
        const jy = isReactant ? (y + bh / 2 - py) * animT * 0.5 : 0;
        ctx.beginPath();
        ctx.arc(px + jx, py + jy, 3, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalAlpha = 1;
    }

    /* Phase label */
    const phaseText = sub.phase === 'g' ? '(г)' : sub.phase === 'aq' ? '(р-р)' : sub.phase === 'l' ? '(ж)' : '(тв)';
    ctx.fillStyle = 'rgba(255,255,255,0.55)';
    ctx.font = '9px Manrope,sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(phaseText, x + bw / 2, y + bh - 18);

    /* Mass label */
    ctx.fillStyle = 'rgba(255,214,102,0.95)';
    ctx.font = 'bold 9px Manrope,sans-serif';
    ctx.textAlign = 'right';
    ctx.fillText(q.m.toFixed(2) + 'г', x + bw - 4, y + bh - 6);

    /* LIMIT label */
    if (isLimit) {
      ctx.fillStyle = `rgba(239,71,111,${0.7 + 0.3 * pulse})`;
      ctx.font = 'bold 8.5px Manrope,sans-serif';
      ctx.textAlign = 'left';
      ctx.fillText('ЛИМИТ', x + 4, y + bh - 6);
    }

    ctx.restore();
  }

  /* ── Анимация реакции ───────────────────────────────────────────── */
  _startAnim() {
    if (this._animState === 'reacting') return;
    this._animState = 'reacting';
    this._animT     = 0;

    if (this._animBtn) {
      this._animBtn.disabled = true;
      this._animBtn.style.opacity = '0.5';
    }

    const dur   = 1200;
    const start = performance.now();
    let lastTs  = start;

    if (window.LabFX && this._ctx) {
      LabFX.sound.play('fizz');
      const r = StoichSim.RECIPES[this._recipeIdx];
      const W = this._W || 300;
      const H = this._H || 180;
      r.reactants.forEach((re, i) => {
        const x = (W / (r.reactants.length + 1)) * (i + 1);
        LabFX.particles.emit({
          ctx: this._ctx, x, y: H * 0.4, count: 8,
          color: re.color || '#FFFFFF', speed: 35, spread: 2.5,
          angle: -Math.PI / 2, gravity: -50, life: 800, shape: 'ring',
        });
      });
    }

    const tick = (now) => {
      const dt = (now - lastTs) / 1000;
      lastTs = now;
      if (window.LabFX) LabFX.particles.update(dt);
      this._animT = Math.min(1, (now - start) / dur);
      this._draw();
      if (window.LabFX && this._ctx) LabFX.particles.draw(this._ctx);
      if (this._animT < 1) {
        this._raf = requestAnimationFrame(tick);
      } else {
        this._animState = 'done';
        this._draw();
        if (this._animBtn) {
          this._animBtn.disabled = false;
          this._animBtn.style.opacity = '1';
          this._animBtn.textContent = 'Сбросить анимацию';
          this._animBtn.onclick = () => {
            this._animState = 'idle';
            this._animT     = 0;
            this._animBtn.textContent = 'Запустить реакцию';
            this._animBtn.onclick = () => this._startAnim();
            this._draw();
          };
        }
      }
    };
    this._raf = requestAnimationFrame(tick);
  }

  /* ── Public API ─────────────────────────────────────────────────── */
  fit() {
    this._fitCanvas();
    this._draw();
  }

  destroy() {
    if (this._raf) cancelAnimationFrame(this._raf);
    if (this._idleRaf) cancelAnimationFrame(this._idleRaf);
    if (this._ro)  this._ro.disconnect();
    this._canvas = null;
    this._ctx    = null;
    this._idleRaf = null;
    this._raf = null;
  }
}

/* ── Helpers (prefixed _st to avoid collisions) ─────────────────── */
function _stEl(tag, props) {
  const el = document.createElement(tag);
  Object.entries(props || {}).forEach(([k, v]) => {
    if (k === 'textContent') el.textContent = v;
    else if (k === 'style')  el.style.cssText = v;
    else el[k] = v;
  });
  return el;
}

function _stRoundRect(ctx, x, y, w, h, r) {
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.arcTo(x + w, y,     x + w, y + r,     r);
  ctx.lineTo(x + w, y + h - r);
  ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
  ctx.lineTo(x + r, y + h);
  ctx.arcTo(x, y + h,     x, y + h - r,     r);
  ctx.lineTo(x, y + r);
  ctx.arcTo(x, y,         x + r, y,         r);
  ctx.closePath();
}

function _stLcg(seed) {
  const a = 1664525, c = 1013904223, m = 2 ** 32;
  return ((a * seed + c) % m) / m;
}

/* ═══════════════════════════════════════════════════════════════════
   Lab UI init
   ═══════════════════════════════════════════════════════════════════ */
var _stoichSim = null;

function _openStoich() {
  document.getElementById('sim-topbar-title').textContent = 'Стехиометрия';
  _simShow('sim-stoichiometry');

  requestAnimationFrame(() => requestAnimationFrame(() => {
    const container = document.getElementById('stoichiometry-wrap');
    if (!_stoichSim) {
      _stoichSim = new StoichSim(container);
    } else {
      _stoichSim.fit();
    }
  }));
}
