'use strict';

/* ═══════════════════════════════════════════════════════════════════════
   SolutionsSim — «Растворы»
   4 sub-modes: calculator, dilution, mixing, solubility curves
   ═══════════════════════════════════════════════════════════════════════ */

/* ── helper: create element ── */
function _slEl(tag, props) {
  var el = document.createElement(tag);
  if (props) {
    Object.keys(props).forEach(function(k) {
      if (k === 'style') { el.style.cssText = props[k]; }
      else if (k === 'textContent') { el.textContent = props[k]; }
      else if (k === 'innerHTML')   { el.innerHTML   = props[k]; }
      else { el[k] = props[k]; }
    });
  }
  return el;
}

class SolutionsSim {

  /* ── Вещества-пресеты ── */
  static SUBSTANCES = [
    { label: 'NaCl',          M: 58.5,  color: '#bde0f7' },
    { label: 'NaOH',          M: 40.0,  color: '#a8f0a8' },
    { label: 'KOH',           M: 56.1,  color: '#b8f5b8' },
    { label: 'HCl (газ)',     M: 36.46, color: '#f5e0a0' },
    { label: 'H₂SO₄',        M: 98.08, color: '#ffa07a' },
    { label: 'HNO₃',         M: 63.01, color: '#f5d0a0' },
    { label: 'H₃PO₄',        M: 97.99, color: '#f0c8a0' },
    { label: 'CuSO₄·5H₂O',   M: 249.7, color: '#64b8f0' },
    { label: 'Глюкоза',       M: 180.2, color: '#f0d080' },
    { label: 'Сахароза',      M: 342.3, color: '#e8d0a0' },
    { label: 'Na₂SO₄',       M: 142.0, color: '#c8d8f0' },
    { label: 'KCl',           M: 74.55, color: '#d0f0d0' },
    { label: 'CaCl₂',        M: 111.0, color: '#f0d8b8' },
    { label: 'AlCl₃',        M: 133.3, color: '#e8d0c8' },
    { label: 'Другое',        M: 1.0,   color: '#aaaaaa' },
  ];

  /* ── Кривые растворимости (г/100г H₂O при 0,10,20,30,40,50,60,70,80,90,100 °C) ── */
  static SOLUBILITY = [
    { label: 'NaCl',        color: '#4CC9F0', data: [35.7,35.8,36.0,36.3,36.6,37.0,37.3,37.8,38.4,39.0,39.8] },
    { label: 'KNO₃',        color: '#FFD166', data: [13.3,20.9,31.6,45.8,63.9,85.5,110,138,169,202,247] },
    { label: 'KCl',         color: '#06D6E0', data: [27.6,31.0,34.0,37.0,40.0,42.6,45.5,48.3,51.1,54.0,56.7] },
    { label: 'Pb(NO₃)₂',   color: '#EF476F', data: [37.6,44.3,54.3,64.5,74.5,84.0,93.3,100,109,116,127] },
    { label: 'CuSO₄',      color: '#9B5DE5', data: [14.3,17.4,20.7,25.0,28.5,33.3,40.2,47.5,55.0,63.4,73.0] },
    { label: 'NH₄Cl',      color: '#F15BB5', data: [29.4,33.3,37.2,41.4,45.8,50.4,55.2,60.2,65.6,71.3,77.3] },
    { label: 'NH₃',        color: '#7BF5A4', data: [88.5,70.0,54.0,39.3,26.8,17.0,10.2,6.5,4.3,3.0,2.1] },
    { label: 'K₂Cr₂O₇',   color: '#FF9F1C', data: [4.7,8.3,12.5,19.0,26.3,35.0,45.6,58.2,73.0,87.0,100] },
  ];

  constructor(container) {
    this._container = container;
    this._mode = 'calc'; // 'calc' | 'dilution' | 'mixing' | 'solubility'
    this._solEnabled = new Set([0, 1, 2, 3, 4]); // enabled solubility curves
    this._solT = 20;

    // calc state
    this._calc = {
      m_solute: 58.5,   // г
      m_solution: 500,  // г
      rho: 1.08,        // г/мл
      M: 58.5,          // г/моль (NaCl)
      subIdx: 0,
      T: 20,
    };

    // dilution state
    this._dil = { m1: 200, omega1: 20, addWater: 100 };

    // mixing state
    this._mix = { m1: 200, omega1: 20, m2: 300, omega2: 10 };

    this._init();
    this._setMode('calc');
  }

  /* ── Build root DOM ── */
  _init() {
    var c = this._container;
    c.innerHTML = '';
    c.style.cssText = 'display:flex;flex-direction:column;height:100%;overflow:hidden;background:#0D0D1A;font-family:Manrope,sans-serif;';

    // Mode tabs
    var tabs = _slEl('div', {
      style: 'flex:0 0 auto;display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.02);',
    });
    var MODES = [
      { id: 'calc',       label: 'Калькулятор' },
      { id: 'dilution',   label: 'Разбавление' },
      { id: 'mixing',     label: 'Смешивание' },
      { id: 'solubility', label: 'Растворимость' },
    ];
    this._tabBtns = {};
    MODES.forEach(function(m) {
      var btn = _slEl('button', {
        style: 'flex:1;padding:10px 4px;background:none;border:none;border-bottom:2px solid transparent;color:rgba(255,255,255,0.5);font-size:.78rem;font-weight:700;font-family:Manrope,sans-serif;cursor:pointer;transition:color .2s,border-color .2s;',
        textContent: m.label,
      });
      btn.addEventListener('click', this._setMode.bind(this, m.id));
      this._tabBtns[m.id] = btn;
      tabs.appendChild(btn);
    }.bind(this));
    c.appendChild(tabs);

    // Content area
    this._content = _slEl('div', { style: 'flex:1 1 auto;display:flex;min-height:0;overflow:hidden;' });
    c.appendChild(this._content);

    // Canvas for visualisation (shared)
    this._canvas = document.createElement('canvas');
    this._ctx2d = this._canvas.getContext('2d');
    this._raf = null;

    if (window.ResizeObserver) {
      this._ro = new ResizeObserver(function() { this._fitCanvas(); this._drawViz(); }.bind(this));
    }
  }

  /* ── Switch mode ── */
  _setMode(mode) {
    this._mode = mode;
    Object.keys(this._tabBtns).forEach(function(id) {
      var btn = this._tabBtns[id];
      btn.style.color     = id === mode ? '#fff' : 'rgba(255,255,255,0.45)';
      btn.style.borderColor = id === mode ? 'var(--violet,#9B5DE5)' : 'transparent';
      btn.style.background  = id === mode ? 'rgba(155,93,229,0.08)' : 'none';
    }.bind(this));

    if (this._ro && this._canvas.parentElement) {
      this._ro.unobserve(this._canvas);
    }

    this._content.innerHTML = '';
    if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }

    switch (mode) {
      case 'calc':      this._buildCalc();      break;
      case 'dilution':  this._buildDilution();  break;
      case 'mixing':    this._buildMixing();     break;
      case 'solubility':this._buildSolubility(); break;
    }
  }

  /* ════════════════════════════════════════════════════════
     MODE 1 — КАЛЬКУЛЯТОР
  ════════════════════════════════════════════════════════ */
  _buildCalc() {
    var self = this;
    var c = this._content;
    c.style.flexDirection = '';

    // Left panel
    var left = _slEl('div', {
      style: 'flex:0 0 270px;display:flex;flex-direction:column;gap:0;overflow-y:auto;padding:12px 14px;border-right:1px solid rgba(255,255,255,0.07);',
    });

    // Section header
    function sHead(txt) {
      return _slEl('div', { style: 'font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:rgba(155,93,229,0.8);margin:10px 0 6px;', textContent: txt });
    }

    // Substance selector
    left.appendChild(sHead('Вещество (M, г/моль)'));
    var subWrap = _slEl('div', { style: 'display:flex;gap:6px;align-items:center;margin-bottom:8px;' });
    var subSel = document.createElement('select');
    subSel.style.cssText = 'flex:1;background:#1a1a2e;color:#fff;border:1px solid rgba(255,255,255,0.15);border-radius:6px;padding:5px 8px;font-size:.78rem;font-family:Manrope,sans-serif;cursor:pointer;';
    SolutionsSim.SUBSTANCES.forEach(function(s, i) {
      var opt = document.createElement('option');
      opt.value = i;
      opt.textContent = s.label + ' = ' + s.M;
      if (i === self._calc.subIdx) opt.selected = true;
      subSel.appendChild(opt);
    });
    subSel.addEventListener('change', function() {
      self._calc.subIdx = +subSel.value;
      var sub = SolutionsSim.SUBSTANCES[self._calc.subIdx];
      self._calc.M = sub.M;
      if (mInput) mInput.value = sub.M;
      if (window.LabFX) LabFX.sound.play('click', { pitch: 1.1 });
      self._recalcCalc('M');
    });
    subWrap.appendChild(subSel);
    left.appendChild(subWrap);

    // Slider row
    function sliderRow(label, id, min, max, step, val, unit, color, onInput) {
      var row = _slEl('div', { style: 'margin-bottom:10px;' });
      var labelRow = _slEl('div', { style: 'display:flex;justify-content:space-between;margin-bottom:3px;' });
      labelRow.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.7);', innerHTML: label }));
      var valSpan = _slEl('span', { id: id + '-val', style: 'font-size:.78rem;font-weight:700;color:' + color + ';', textContent: val + ' ' + unit });
      labelRow.appendChild(valSpan);
      row.appendChild(labelRow);

      var sl = document.createElement('input');
      sl.type = 'range'; sl.id = id + '-sl';
      sl.min = min; sl.max = max; sl.step = step; sl.value = val;
      sl.style.cssText = 'width:100%;accent-color:' + color + ';cursor:pointer;';
      sl.addEventListener('input', function() {
        if (window.LabFX) LabFX.sound.play('click', { pitch: 1.0, volume: 0.25 });
        onInput(+sl.value);
        valSpan.textContent = (+sl.value).toFixed(step < 1 ? 2 : 0) + ' ' + unit;
      });
      row.appendChild(sl);

      // number input
      var ni = document.createElement('input');
      ni.type = 'number'; ni.min = min; ni.max = max; ni.step = step; ni.value = val;
      ni.style.cssText = 'width:100%;background:#111122;color:#fff;border:1px solid rgba(255,255,255,0.12);border-radius:5px;padding:4px 8px;font-size:.78rem;margin-top:2px;box-sizing:border-box;';
      ni.addEventListener('change', function() {
        var v = Math.min(max, Math.max(min, +ni.value || min));
        sl.value = v; ni.value = v;
        valSpan.textContent = v.toFixed(step < 1 ? 2 : 0) + ' ' + unit;
        onInput(v);
      });
      row.appendChild(ni);
      return { row: row, sl: sl, ni: ni, valSpan: valSpan };
    }

    left.appendChild(sHead('Входные параметры'));

    var mSoluteCtrl = sliderRow('m<sub>в</sub> — масса растворённого', 'sl-calc-ms', 0, 500, 1, self._calc.m_solute, 'г', '#4CC9F0', function(v) {
      self._calc.m_solute = v;
      self._recalcCalc('m_solute');
    });
    left.appendChild(mSoluteCtrl.row);

    var mSolCtrl = sliderRow('m<sub>р-ра</sub> — масса раствора', 'sl-calc-mr', 1, 1000, 1, self._calc.m_solution, 'г', '#FFD166', function(v) {
      self._calc.m_solution = Math.max(v, self._calc.m_solute + 0.01);
      self._recalcCalc('m_solution');
    });
    left.appendChild(mSolCtrl.row);

    var rhoCtrl = sliderRow('ρ — плотность раствора', 'sl-calc-rho', 0.8, 2.0, 0.01, self._calc.rho, 'г/мл', '#9B5DE5', function(v) {
      self._calc.rho = v;
      self._recalcCalc('rho');
    });
    left.appendChild(rhoCtrl.row);

    left.appendChild(sHead('Молярная масса'));
    var mWrap = _slEl('div', { style: 'display:flex;gap:6px;align-items:center;margin-bottom:10px;' });
    mWrap.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.7);', textContent: 'M =' }));
    var mInput = document.createElement('input');
    mInput.type = 'number'; mInput.min = 1; mInput.max = 500; mInput.step = 0.1;
    mInput.value = self._calc.M;
    mInput.style.cssText = 'flex:1;background:#111122;color:#fff;border:1px solid rgba(255,255,255,0.12);border-radius:5px;padding:4px 8px;font-size:.78rem;';
    mInput.addEventListener('change', function() {
      self._calc.M = Math.max(1, Math.min(500, +mInput.value || 1));
      mInput.value = self._calc.M;
      self._recalcCalc('M');
    });
    mWrap.appendChild(mInput);
    mWrap.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.5);', textContent: 'г/моль' }));
    left.appendChild(mWrap);

    left.appendChild(sHead('Температура'));
    var tCtrl = sliderRow('T — температура', 'sl-calc-t', 0, 100, 1, self._calc.T, '°C', '#F15BB5', function(v) {
      self._calc.T = v;
      self._recalcCalc('T');
    });
    left.appendChild(tCtrl.row);

    c.appendChild(left);

    // Center — canvas visualization
    var centerWrap = _slEl('div', { style: 'flex:1 1 auto;display:flex;flex-direction:column;align-items:stretch;min-width:0;position:relative;' });
    this._canvas.style.cssText = 'flex:1 1 auto;width:100%;height:100%;display:block;';
    centerWrap.appendChild(this._canvas);
    c.appendChild(centerWrap);

    // Right panel — results
    var right = _slEl('div', {
      style: 'flex:0 0 220px;display:flex;flex-direction:column;gap:0;overflow-y:auto;padding:12px 14px;border-left:1px solid rgba(255,255,255,0.07);',
    });
    right.appendChild(sHead('Вычисленные значения'));
    this._calcResults = right;
    c.appendChild(right);

    // attach ResizeObserver
    if (this._ro) { this._ro.observe(this._canvas); }
    requestAnimationFrame(function() {
      self._fitCanvas();
      self._recalcCalc('init');
    });
  }

  _recalcCalc(changed) {
    var s = this._calc;
    if (s.m_solute > s.m_solution) s.m_solution = s.m_solute + 0.01;

    var omega     = s.m_solution > 0 ? (s.m_solute / s.m_solution) * 100 : 0;
    var m_water   = s.m_solution - s.m_solute;
    var V_liters  = (s.m_solution / s.rho) / 1000; // л
    var V_ml      = s.m_solution / s.rho;           // мл
    var nu        = s.M > 0 ? s.m_solute / s.M : 0; // моль
    var cM        = V_liters > 0 ? nu / V_liters : 0; // моль/л
    // норм. концентрация — упрощённо, как молярная для однозарядных
    var cN        = cM;

    this._calcState = { omega, m_water, V_ml, V_liters, nu, cM, cN };

    var r = this._calcResults;
    if (!r) return;

    function resLine(label, value, unit, color, formula) {
      var row = _slEl('div', { style: 'margin-bottom:10px;padding:8px;background:rgba(255,255,255,0.04);border-radius:8px;border-left:3px solid ' + color + ';' });
      row.appendChild(_slEl('div', { style: 'font-size:.72rem;color:rgba(255,255,255,0.5);margin-bottom:2px;', innerHTML: label }));
      row.appendChild(_slEl('div', { style: 'font-size:1.1rem;font-weight:800;color:' + color + ';', textContent: value + ' ' + unit }));
      if (formula) {
        var fd = _slEl('div', { style: 'margin-top:4px;font-size:.7rem;color:rgba(255,255,255,0.35);' });
        fd.setAttribute('data-formula', formula);
        row.appendChild(fd);
      }
      return row;
    }

    r.innerHTML = '';
    r.appendChild(_slEl('div', { style: 'font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:rgba(155,93,229,0.8);margin:10px 0 6px;', textContent: 'Вычисленные значения' }));

    r.appendChild(resLine('ω — массовая доля', omega.toFixed(2), '%',     '#4CC9F0',  '\\omega = \\frac{m_в}{m_{р-ра}} \\times 100\\%'));
    r.appendChild(resLine('m<sub>воды</sub> — масса воды', m_water.toFixed(1), 'г', '#06D6E0',  ''));
    r.appendChild(resLine('V — объём раствора', V_ml.toFixed(1), 'мл',    '#FFD166',  'V = \\frac{m_{р-ра}}{\\rho}'));
    r.appendChild(resLine('ν — количество вещества', nu.toFixed(4), 'моль', '#9B5DE5', '\\nu = \\frac{m_в}{M}'));
    r.appendChild(resLine('C<sub>М</sub> — молярная', cM.toFixed(4), 'моль/л', '#F15BB5', 'C_M = \\frac{\\nu}{V}'));
    r.appendChild(resLine('C<sub>Н</sub> — нормальность', cN.toFixed(4), 'моль-экв/л', '#EF476F', ''));

    // Render KaTeX formulas
    r.querySelectorAll('[data-formula]').forEach(function(el) {
      if (window.katex && el.getAttribute('data-formula')) {
        try {
          katex.render(el.getAttribute('data-formula'), el, { throwOnError: false, displayMode: false });
        } catch(e) { /* ignore */ }
      }
    });

    if (window.LabFX && changed !== 'init') {
      LabFX.sound.play('chime', { pitch: 1.4, volume: 0.3 });
    }

    this._drawViz();
  }

  /* ════════════════════════════════════════════════════════
     MODE 2 — РАЗБАВЛЕНИЕ
  ════════════════════════════════════════════════════════ */
  _buildDilution() {
    var self = this;
    var c = this._content;
    c.style.flexDirection = '';

    // Left controls
    var left = _slEl('div', {
      style: 'flex:0 0 260px;display:flex;flex-direction:column;overflow-y:auto;padding:14px;border-right:1px solid rgba(255,255,255,0.07);',
    });

    function sh(t) { return _slEl('div', { style: 'font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:rgba(155,93,229,0.8);margin:8px 0 6px;', textContent: t }); }
    function numRow(label, val, min, max, step, unit, color, cb) {
      var row = _slEl('div', { style: 'margin-bottom:10px;' });
      var lrow = _slEl('div', { style: 'display:flex;justify-content:space-between;margin-bottom:3px;' });
      lrow.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.7);', innerHTML: label }));
      var vs = _slEl('span', { style: 'font-size:.78rem;font-weight:700;color:' + color + ';', textContent: val + ' ' + unit });
      lrow.appendChild(vs);
      row.appendChild(lrow);
      var sl = document.createElement('input');
      sl.type = 'range'; sl.min = min; sl.max = max; sl.step = step; sl.value = val;
      sl.style.cssText = 'width:100%;accent-color:' + color + ';cursor:pointer;';
      sl.addEventListener('input', function() {
        vs.textContent = (+sl.value).toFixed(step < 1 ? 1 : 0) + ' ' + unit;
        if (window.LabFX) LabFX.sound.play('click', { volume: 0.2 });
        cb(+sl.value);
      });
      row.appendChild(sl);
      return { row, sl, vs };
    }

    left.appendChild(sh('Исходный раствор'));
    numRow('m₁ — масса раствора', self._dil.m1, 50, 500, 1, 'г', '#FFD166', function(v) { self._dil.m1 = v; self._recalcDil(); }).row;
    left.appendChild(left.lastChild);
    var dil_m1 = left.lastChild;

    numRow('ω₁ — концентрация', self._dil.omega1, 1, 80, 0.5, '%', '#4CC9F0', function(v) { self._dil.omega1 = v; self._recalcDil(); }).row;
    left.appendChild(left.lastChild);

    left.appendChild(sh('Добавляем воду'));
    numRow('V<sub>воды</sub> — объём воды', self._dil.addWater, 0, 1000, 1, 'мл', '#06D6E0', function(v) {
      self._dil.addWater = v;
      if (window.LabFX && v > 0) LabFX.sound.play('pour', { volume: 0.4 });
      self._recalcDil();
    }).row;
    left.appendChild(left.lastChild);

    left.appendChild(sh('Результат'));
    this._dilResultsEl = _slEl('div', {});
    left.appendChild(this._dilResultsEl);

    c.appendChild(left);

    // Center — canvas
    var centerWrap = _slEl('div', { style: 'flex:1 1 auto;display:flex;flex-direction:column;min-width:0;' });
    this._canvas.style.cssText = 'flex:1 1 auto;width:100%;display:block;';
    centerWrap.appendChild(this._canvas);
    c.appendChild(centerWrap);

    if (this._ro) { this._ro.observe(this._canvas); }
    requestAnimationFrame(function() { self._fitCanvas(); self._recalcDil(); });
  }

  _recalcDil() {
    var d = this._dil;
    var m_solute = d.m1 * d.omega1 / 100;
    var m_new    = d.m1 + d.addWater;   // плотность воды = 1 г/мл
    var omega2   = m_new > 0 ? (m_solute / m_new) * 100 : 0;

    this._dilState = { m_solute, m_new, omega2 };

    var r = this._dilResultsEl;
    if (!r) return;
    r.innerHTML = '';

    function valLine(label, val, unit, color) {
      var el = _slEl('div', { style: 'display:flex;justify-content:space-between;padding:6px 10px;background:rgba(255,255,255,0.04);border-radius:7px;margin-bottom:6px;' });
      el.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.6);', innerHTML: label }));
      el.appendChild(_slEl('span', { style: 'font-size:.82rem;font-weight:800;color:' + color + ';', textContent: val + ' ' + unit }));
      return el;
    }
    r.appendChild(valLine('m<sub>в</sub> = const', m_solute.toFixed(2), 'г', '#4CC9F0'));
    r.appendChild(valLine('m<sub>р-ра новая</sub>', m_new.toFixed(1), 'г', '#FFD166'));
    r.appendChild(valLine('ω₂ (новая)', omega2.toFixed(3), '%', '#7BF5A4'));

    if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.2, volume: 0.25 });
    this._drawViz();
  }

  /* ════════════════════════════════════════════════════════
     MODE 3 — СМЕШИВАНИЕ
  ════════════════════════════════════════════════════════ */
  _buildMixing() {
    var self = this;
    var c = this._content;
    c.style.flexDirection = '';

    var left = _slEl('div', {
      style: 'flex:0 0 260px;display:flex;flex-direction:column;overflow-y:auto;padding:14px;border-right:1px solid rgba(255,255,255,0.07);',
    });

    function sh(t) { return _slEl('div', { style: 'font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:rgba(155,93,229,0.8);margin:8px 0 6px;', textContent: t }); }
    function addSl(label, val, min, max, step, unit, color, cb) {
      var row = _slEl('div', { style: 'margin-bottom:10px;' });
      var lrow = _slEl('div', { style: 'display:flex;justify-content:space-between;margin-bottom:3px;' });
      lrow.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.7);', innerHTML: label }));
      var vs = _slEl('span', { style: 'font-size:.78rem;font-weight:700;color:' + color + ';', textContent: val + ' ' + unit });
      lrow.appendChild(vs);
      row.appendChild(lrow);
      var sl = document.createElement('input');
      sl.type = 'range'; sl.min = min; sl.max = max; sl.step = step; sl.value = val;
      sl.style.cssText = 'width:100%;accent-color:' + color + ';cursor:pointer;';
      sl.addEventListener('input', function() {
        vs.textContent = (+sl.value).toFixed(step < 1 ? 1 : 0) + ' ' + unit;
        if (window.LabFX) LabFX.sound.play('click', { volume: 0.2 });
        cb(+sl.value);
      });
      row.appendChild(sl);
      left.appendChild(row);
    }

    left.appendChild(sh('Раствор 1'));
    addSl('m₁ — масса', self._mix.m1, 10, 500, 1, 'г', '#4CC9F0', function(v) { self._mix.m1 = v; self._recalcMix(); });
    addSl('ω₁ — концентрация', self._mix.omega1, 0, 90, 0.5, '%', '#4CC9F0', function(v) { self._mix.omega1 = v; self._recalcMix(); });

    left.appendChild(sh('Раствор 2'));
    addSl('m₂ — масса', self._mix.m2, 10, 500, 1, 'г', '#FFD166', function(v) { self._mix.m2 = v; self._recalcMix(); });
    addSl('ω₂ — концентрация', self._mix.omega2, 0, 90, 0.5, '%', '#FFD166', function(v) { self._mix.omega2 = v; self._recalcMix(); });

    left.appendChild(sh('Результат'));
    this._mixResultsEl = _slEl('div', {});
    left.appendChild(this._mixResultsEl);

    c.appendChild(left);

    var centerWrap = _slEl('div', { style: 'flex:1 1 auto;display:flex;flex-direction:column;min-width:0;' });
    this._canvas.style.cssText = 'flex:1 1 auto;width:100%;display:block;';
    centerWrap.appendChild(this._canvas);
    c.appendChild(centerWrap);

    if (this._ro) { this._ro.observe(this._canvas); }
    requestAnimationFrame(function() { self._fitCanvas(); self._recalcMix(); });
  }

  _recalcMix() {
    var x = this._mix;
    var m3    = x.m1 + x.m2;
    var omega3 = m3 > 0 ? (x.m1 * x.omega1 + x.m2 * x.omega2) / m3 : 0;
    this._mixState = { m3, omega3 };

    var r = this._mixResultsEl;
    if (!r) return;
    r.innerHTML = '';

    function valLine(label, val, unit, color) {
      var el = _slEl('div', { style: 'display:flex;justify-content:space-between;padding:6px 10px;background:rgba(255,255,255,0.04);border-radius:7px;margin-bottom:6px;' });
      el.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.6);', innerHTML: label }));
      el.appendChild(_slEl('span', { style: 'font-size:.82rem;font-weight:800;color:' + color + ';', textContent: val + ' ' + unit }));
      return el;
    }
    r.appendChild(valLine('m₃ = m₁ + m₂', m3.toFixed(1), 'г', '#7BF5A4'));
    r.appendChild(valLine('ω₃ — итоговая', omega3.toFixed(3), '%', '#F15BB5'));

    // правило рычага
    var fd = _slEl('div', { style: 'margin-top:10px;padding:8px;background:rgba(155,93,229,0.08);border-radius:8px;font-size:.75rem;color:rgba(255,255,255,0.5);' });
    fd.appendChild(_slEl('div', { style: 'margin-bottom:4px;color:rgba(155,93,229,0.9);font-weight:700;font-size:.72rem;', textContent: 'Правило рычага' }));
    var formulaEl = _slEl('div', {});
    formulaEl.setAttribute('data-formula', 'm_1 \\cdot \\omega_1 + m_2 \\cdot \\omega_2 = m_3 \\cdot \\omega_3');
    fd.appendChild(formulaEl);
    r.appendChild(fd);

    if (window.katex) {
      r.querySelectorAll('[data-formula]').forEach(function(el) {
        try { katex.render(el.getAttribute('data-formula'), el, { throwOnError: false, displayMode: false }); } catch(e) {}
      });
    }

    if (window.LabFX) LabFX.sound.play('pour', { volume: 0.5 });
    this._drawViz();
  }

  /* ════════════════════════════════════════════════════════
     MODE 4 — КРИВЫЕ РАСТВОРИМОСТИ
  ════════════════════════════════════════════════════════ */
  _buildSolubility() {
    var self = this;
    var c = this._content;
    c.style.flexDirection = '';

    // Left panel
    var left = _slEl('div', {
      style: 'flex:0 0 220px;display:flex;flex-direction:column;overflow-y:auto;padding:12px 14px;border-right:1px solid rgba(255,255,255,0.07);',
    });

    function sh(t) { return _slEl('div', { style: 'font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:rgba(155,93,229,0.8);margin:10px 0 6px;', textContent: t }); }

    left.appendChild(sh('Вещества'));
    SolutionsSim.SOLUBILITY.forEach(function(s, idx) {
      var row = _slEl('div', { style: 'display:flex;align-items:center;gap:8px;margin-bottom:7px;cursor:pointer;' });
      var cb = document.createElement('input');
      cb.type = 'checkbox';
      cb.checked = self._solEnabled.has(idx);
      cb.style.cssText = 'accent-color:' + s.color + ';width:14px;height:14px;cursor:pointer;';
      cb.addEventListener('change', function() {
        if (cb.checked) { self._solEnabled.add(idx); } else { self._solEnabled.delete(idx); }
        if (window.LabFX) LabFX.sound.play('click', { pitch: 0.9 });
        self._drawViz();
      });
      var colorDot = _slEl('span', { style: 'width:10px;height:10px;border-radius:50%;background:' + s.color + ';flex-shrink:0;display:inline-block;' });
      row.appendChild(cb);
      row.appendChild(colorDot);
      row.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.8);', textContent: s.label }));
      left.appendChild(row);
    });

    left.appendChild(sh('Температура'));
    var tRow = _slEl('div', { style: 'margin-bottom:10px;' });
    var tRowL = _slEl('div', { style: 'display:flex;justify-content:space-between;margin-bottom:3px;' });
    tRowL.appendChild(_slEl('span', { style: 'font-size:.78rem;color:rgba(255,255,255,0.7);', textContent: 'T =' }));
    var tValSpan = _slEl('span', { style: 'font-size:.78rem;font-weight:700;color:#F15BB5;', textContent: self._solT + ' °C' });
    tRowL.appendChild(tValSpan);
    tRow.appendChild(tRowL);
    var tSl = document.createElement('input');
    tSl.type = 'range'; tSl.min = 0; tSl.max = 100; tSl.step = 1; tSl.value = self._solT;
    tSl.style.cssText = 'width:100%;accent-color:#F15BB5;cursor:pointer;';
    tSl.addEventListener('input', function() {
      self._solT = +tSl.value;
      tValSpan.textContent = self._solT + ' °C';
      if (window.LabFX) LabFX.sound.play('click', { volume: 0.2 });
      self._drawViz();
    });
    tRow.appendChild(tSl);
    left.appendChild(tRow);

    // Перекристаллизация задача
    left.appendChild(sh('Задача: перекристаллизация'));
    var kno3Hint = _slEl('div', { style: 'font-size:.72rem;color:rgba(255,255,255,0.55);margin-bottom:8px;line-height:1.5;', textContent: 'Насыщенный раствор KNO₃ при 80°C охладили до 20°C.' });
    left.appendChild(kno3Hint);

    var crystRow = _slEl('div', { style: 'display:flex;gap:6px;align-items:center;margin-bottom:8px;' });
    crystRow.appendChild(_slEl('span', { style: 'font-size:.75rem;color:rgba(255,255,255,0.6);', textContent: 'Масса р-ра (г):' }));
    var crystInput = document.createElement('input');
    crystInput.type = 'number'; crystInput.min = 10; crystInput.max = 10000; crystInput.step = 10; crystInput.value = 200;
    crystInput.style.cssText = 'flex:1;background:#111122;color:#fff;border:1px solid rgba(255,255,255,0.12);border-radius:5px;padding:4px 6px;font-size:.75rem;';
    crystRow.appendChild(crystInput);
    left.appendChild(crystRow);

    var crystBtn = _slEl('button', {
      style: 'width:100%;padding:8px;border-radius:7px;border:none;background:linear-gradient(135deg,#9B5DE5,#4CC9F0);color:#fff;font-size:.78rem;font-weight:700;cursor:pointer;',
      textContent: 'Рассчитать',
    });
    this._crystAnswer = _slEl('div', { style: 'margin-top:8px;font-size:.75rem;line-height:1.5;color:#7BF5A4;', textContent: '' });
    crystBtn.addEventListener('click', function() {
      var mSol = +crystInput.value || 200;
      // S at 80°C = 169 g/100g H2O, at 20°C = 31.6 g/100g H2O
      var S80  = 169, S20 = 31.6;
      var kno3Index = 1; // KNO₃ is index 1 in SOLUBILITY
      // Насыщенный раствор при 80°C: x г KNO3 на 100 г воды
      // m_r = m_KNO3 + m_H2O => m_KNO3 = mSol * S80 / (100 + S80)
      var m_kno3 = mSol * S80 / (100 + S80);
      var m_h2o  = mSol - m_kno3;
      // при 20°C в m_h2o г воды растворится не более S20 * m_h2o / 100
      var dissolved20 = S20 * m_h2o / 100;
      var precipitated = m_kno3 - dissolved20;
      precipitated = Math.max(0, precipitated);
      self._crystAnswer.textContent =
        'KNO₃ в р-ре: ' + m_kno3.toFixed(1) + ' г\n' +
        'H₂O: ' + m_h2o.toFixed(1) + ' г\n' +
        'Выпало осадка: ' + precipitated.toFixed(1) + ' г';
      if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.0, volume: 0.5 });
    });
    left.appendChild(crystBtn);
    left.appendChild(this._crystAnswer);

    c.appendChild(left);

    // Center — canvas
    var centerWrap = _slEl('div', { style: 'flex:1 1 auto;display:flex;flex-direction:column;min-width:0;' });
    this._canvas.style.cssText = 'flex:1 1 auto;width:100%;display:block;';
    centerWrap.appendChild(this._canvas);
    c.appendChild(centerWrap);

    if (this._ro) { this._ro.observe(this._canvas); }
    requestAnimationFrame(function() { self._fitCanvas(); self._drawViz(); });
  }

  /* ════════════════════════════════════════════════════════
     CANVAS — fitCanvas + drawViz dispatcher
  ════════════════════════════════════════════════════════ */
  _fitCanvas() {
    var cv = this._canvas;
    if (!cv.parentElement) return;
    var r = cv.parentElement.getBoundingClientRect();
    if (r.width < 1 || r.height < 1) return;
    cv.width  = r.width  * (window.devicePixelRatio || 1);
    cv.height = r.height * (window.devicePixelRatio || 1);
    cv.style.width  = r.width  + 'px';
    cv.style.height = r.height + 'px';
    this._ctx2d.setTransform(window.devicePixelRatio || 1, 0, 0, window.devicePixelRatio || 1, 0, 0);
  }

  _drawViz() {
    switch (this._mode) {
      case 'calc':       this._drawCalcViz();    break;
      case 'dilution':   this._drawDilViz();     break;
      case 'mixing':     this._drawMixViz();     break;
      case 'solubility': this._drawSolubility(); break;
    }
  }

  /* ── Draw helper: gradient beaker ── */
  _drawBeaker(ctx, x, y, w, h, fillH, color, label) {
    var bw = w * 0.55;   // beaker width
    var bh = h * 0.72;   // beaker body height
    var bx = x + (w - bw) / 2;
    var by = y + h * 0.1;

    // beaker body (trapezoid-ish, bottom wider)
    ctx.strokeStyle = 'rgba(255,255,255,0.35)';
    ctx.lineWidth   = 2;
    ctx.lineJoin    = 'round';
    ctx.beginPath();
    ctx.moveTo(bx + bw * 0.1, by);
    ctx.lineTo(bx + bw * 0.9, by);
    ctx.lineTo(bx + bw, by + bh);
    ctx.lineTo(bx, by + bh);
    ctx.closePath();
    ctx.stroke();

    // liquid fill
    var fillRatio = Math.min(1, Math.max(0, fillH));
    var fillY    = by + bh * (1 - fillRatio);
    ctx.save();
    ctx.beginPath();
    ctx.rect(bx - 2, fillY, bw + 4, bh - (fillY - by));
    ctx.clip();
    var grad = ctx.createLinearGradient(bx, fillY, bx + bw, fillY + bh);
    grad.addColorStop(0, color.replace(')', ',0.85)').replace('rgb', 'rgba'));
    grad.addColorStop(1, color.replace(')', ',0.5)').replace('rgb', 'rgba'));
    ctx.fillStyle = color + 'cc';
    ctx.fill();
    ctx.restore();

    // label
    ctx.fillStyle = 'rgba(255,255,255,0.85)';
    ctx.font = 'bold 13px Manrope,sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(label, x + w / 2, y + h + 16);
  }

  /* ── Calc visualization: beaker + percentage ── */
  _drawCalcViz() {
    var cv = this._canvas;
    var ctx = this._ctx2d;
    var W = cv.width / (window.devicePixelRatio || 1);
    var H = cv.height / (window.devicePixelRatio || 1);
    ctx.clearRect(0, 0, W, H);
    if (!this._calcState) return;

    var st = this._calcState;
    var omega = st.omega;
    var sub   = SolutionsSim.SUBSTANCES[this._calc.subIdx];

    // draw background gradient
    var bg = ctx.createRadialGradient(W/2, H/2, 10, W/2, H/2, Math.max(W, H) * 0.7);
    bg.addColorStop(0, '#14142a');
    bg.addColorStop(1, '#0D0D1A');
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, W, H);

    // Beaker
    var bW = Math.min(W * 0.45, 220);
    var bH = Math.min(H * 0.7, 320);
    var bX = (W - bW) / 2;
    var bY = H * 0.08;

    var bw = bW * 0.55;
    var bh = bH * 0.72;
    var bx = bX + (bW - bw) / 2;
    var by = bY + bH * 0.1;

    // outer glass
    ctx.strokeStyle = 'rgba(255,255,255,0.3)';
    ctx.lineWidth   = 2.5;
    ctx.lineJoin    = 'round';
    ctx.beginPath();
    ctx.moveTo(bx + bw * 0.08, by);
    ctx.lineTo(bx + bw * 0.92, by);
    ctx.lineTo(bx + bw, by + bh);
    ctx.lineTo(bx, by + bh);
    ctx.closePath();
    ctx.stroke();

    // graduation marks
    ctx.strokeStyle = 'rgba(255,255,255,0.15)';
    ctx.lineWidth = 1;
    for (var i = 1; i <= 4; i++) {
      var gy = by + bh * (i / 5);
      ctx.beginPath(); ctx.moveTo(bx + 4, gy); ctx.lineTo(bx + 12, gy); ctx.stroke();
    }

    // liquid fill
    var fillRatio = Math.min(1, Math.max(0.05, (this._calc.m_solution / 1000) * 0.85));
    var liquidTop = by + bh * (1 - fillRatio);

    ctx.save();
    ctx.beginPath();
    ctx.moveTo(bx + bw * 0.08, by);
    ctx.lineTo(bx + bw * 0.92, by);
    ctx.lineTo(bx + bw, by + bh);
    ctx.lineTo(bx, by + bh);
    ctx.closePath();
    ctx.clip();

    var lgrad = ctx.createLinearGradient(bx, liquidTop, bx + bw, by + bh);
    var baseColor = sub.color || '#4CC9F0';
    lgrad.addColorStop(0, baseColor + 'bb');
    lgrad.addColorStop(1, baseColor + '77');
    ctx.fillStyle = lgrad;
    ctx.beginPath();
    ctx.rect(bx - 5, liquidTop, bw + 10, bh - (liquidTop - by) + 5);
    ctx.fill();

    // concentration ripple effect on surface
    ctx.strokeStyle = 'rgba(255,255,255,0.2)';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(bx + 4, liquidTop);
    ctx.bezierCurveTo(bx + bw * 0.25, liquidTop - 4, bx + bw * 0.75, liquidTop + 4, bx + bw - 4, liquidTop);
    ctx.stroke();

    ctx.restore();

    // % label inside beaker
    ctx.fillStyle = 'rgba(255,255,255,0.9)';
    ctx.font = 'bold 28px Manrope,sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(omega.toFixed(1) + '%', bx + bw / 2, liquidTop + (by + bh - liquidTop) * 0.45);

    // substance name below
    ctx.font = '12px Manrope,sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.fillText(sub.label, bx + bw / 2, by + bh + 22);

    // beaker label top
    ctx.font = 'bold 11px Manrope,sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.35)';
    ctx.fillText('Мерный стакан', bx + bw / 2, by - 10);

    ctx.textBaseline = 'alphabetic';

    // formula annotation
    var fY = by + bh + 48;
    ctx.font = '11px Manrope,sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.3)';
    ctx.textAlign = 'center';
    ctx.fillText('ω = ' + omega.toFixed(2) + '% · ν = ' + this._calcState.nu.toFixed(3) + ' моль · Cм = ' + this._calcState.cM.toFixed(3) + ' моль/л', W / 2, fY);
  }

  /* ── Dilution visualization ── */
  _drawDilViz() {
    var cv = this._canvas;
    var ctx = this._ctx2d;
    var W = cv.width / (window.devicePixelRatio || 1);
    var H = cv.height / (window.devicePixelRatio || 1);
    ctx.clearRect(0, 0, W, H);
    if (!this._dilState) return;

    var bg = ctx.createRadialGradient(W/2, H/2, 10, W/2, H/2, Math.max(W,H)*0.7);
    bg.addColorStop(0, '#14142a'); bg.addColorStop(1, '#0D0D1A');
    ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);

    var d = this._dil;
    var st = this._dilState;

    var bH = Math.min(H * 0.65, 280);
    var bW = 100;
    var gap = 60;
    var totalW = bW * 2 + gap + 80;
    var startX = (W - totalW) / 2;
    var bY = H * 0.12;

    function drawSimpleBeaker(cx, cy, bw, bh, fill, color, label, omegaLabel) {
      // glass
      ctx.strokeStyle = 'rgba(255,255,255,0.3)';
      ctx.lineWidth = 2;
      ctx.lineJoin = 'round';
      ctx.beginPath();
      ctx.moveTo(cx, cy); ctx.lineTo(cx + bw, cy);
      ctx.lineTo(cx + bw, cy + bh); ctx.lineTo(cx, cy + bh);
      ctx.closePath(); ctx.stroke();

      // liquid
      var lh = Math.max(4, bh * fill);
      var ly = cy + bh - lh;
      ctx.save();
      ctx.beginPath(); ctx.rect(cx + 1, cy + 1, bw - 2, bh - 2); ctx.clip();
      var g = ctx.createLinearGradient(cx, ly, cx + bw, cy + bh);
      g.addColorStop(0, color + 'cc'); g.addColorStop(1, color + '77');
      ctx.fillStyle = g;
      ctx.fillRect(cx + 1, ly, bw - 2, lh); ctx.restore();

      // omega text
      ctx.fillStyle = 'rgba(255,255,255,0.9)';
      ctx.font = 'bold 14px Manrope,sans-serif';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText(omegaLabel, cx + bw / 2, ly + lh * 0.45);

      // label
      ctx.font = '12px Manrope,sans-serif';
      ctx.fillStyle = 'rgba(255,255,255,0.55)';
      ctx.textBaseline = 'alphabetic';
      ctx.fillText(label, cx + bw / 2, cy + bh + 20);
    }

    // Original beaker
    var fill1 = Math.min(0.9, Math.max(0.1, d.m1 / 500));
    drawSimpleBeaker(startX, bY, bW, bH, fill1, '#4CC9F0', 'Исходный', d.omega1.toFixed(1) + '%');

    // Arrow + "добавили воды"
    var arrowX = startX + bW + 10;
    var arrowY = bY + bH / 2;
    ctx.strokeStyle = 'rgba(6,214,224,0.7)';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(arrowX, arrowY); ctx.lineTo(arrowX + gap - 10, arrowY); ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(arrowX + gap - 10, arrowY - 6);
    ctx.lineTo(arrowX + gap, arrowY);
    ctx.lineTo(arrowX + gap - 10, arrowY + 6); ctx.stroke();
    ctx.fillStyle = 'rgba(255,255,255,0.45)';
    ctx.font = '10px Manrope,sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic';
    ctx.fillText('+' + d.addWater + ' мл', arrowX + gap / 2, arrowY - 10);
    ctx.fillText('H₂O', arrowX + gap / 2, arrowY + 20);

    // New beaker (lighter color)
    var colorIntensity = Math.max(0.15, st.omega2 / Math.max(1, d.omega1));
    var r2 = Math.round(76 + (1 - colorIntensity) * 179);
    var g2 = Math.round(201 + (1 - colorIntensity) * 54);
    var b2 = Math.round(240 + 0);
    var c2 = 'rgb(' + r2 + ',' + Math.min(255, g2) + ',' + Math.min(255, b2) + ')';
    var fill2 = Math.min(0.9, Math.max(0.1, st.m_new / 600));
    drawSimpleBeaker(startX + bW + gap, bY, bW, bH, fill2, c2, 'Разбавленный', st.omega2.toFixed(2) + '%');

    // note
    ctx.fillStyle = 'rgba(255,255,255,0.3)';
    ctx.font = '11px Manrope,sans-serif'; ctx.textAlign = 'center';
    ctx.fillText('m_в = const = ' + (d.m1 * d.omega1 / 100).toFixed(1) + ' г', W / 2, bY + bH + 50);
  }

  /* ── Mixing visualization ── */
  _drawMixViz() {
    var cv = this._canvas;
    var ctx = this._ctx2d;
    var W = cv.width / (window.devicePixelRatio || 1);
    var H = cv.height / (window.devicePixelRatio || 1);
    ctx.clearRect(0, 0, W, H);
    if (!this._mixState) return;

    var bg = ctx.createRadialGradient(W/2, H/2, 10, W/2, H/2, Math.max(W,H)*0.7);
    bg.addColorStop(0, '#14142a'); bg.addColorStop(1, '#0D0D1A');
    ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);

    var x = this._mix;
    var st = this._mixState;

    var bH = Math.min(H * 0.55, 240);
    var bW = 90;
    var bY = H * 0.1;
    var totalW3 = bW * 3 + 140;
    var sx = (W - totalW3) / 2;

    function drawBk(bx, by, bw, bh, fill, color, lbl, pct) {
      ctx.strokeStyle = 'rgba(255,255,255,0.3)';
      ctx.lineWidth = 2; ctx.lineJoin = 'round';
      ctx.beginPath();
      ctx.moveTo(bx, by); ctx.lineTo(bx + bw, by);
      ctx.lineTo(bx + bw, by + bh); ctx.lineTo(bx, by + bh);
      ctx.closePath(); ctx.stroke();
      var lh = Math.max(4, bh * fill);
      var ly = by + bh - lh;
      ctx.save(); ctx.beginPath(); ctx.rect(bx+1, by+1, bw-2, bh-2); ctx.clip();
      var g = ctx.createLinearGradient(bx, ly, bx + bw, by + bh);
      g.addColorStop(0, color + 'cc'); g.addColorStop(1, color + '66');
      ctx.fillStyle = g; ctx.fillRect(bx+1, ly, bw-2, lh); ctx.restore();
      ctx.fillStyle = 'rgba(255,255,255,0.9)';
      ctx.font = 'bold 13px Manrope,sans-serif';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText(pct, bx + bw/2, ly + lh * 0.45);
      ctx.font = '11px Manrope,sans-serif';
      ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.textBaseline = 'alphabetic';
      ctx.fillText(lbl, bx + bw/2, by + bh + 18);
    }

    var fill1 = Math.min(0.88, Math.max(0.08, x.m1 / 500));
    var fill2 = Math.min(0.88, Math.max(0.08, x.m2 / 500));
    var fill3 = Math.min(0.88, Math.max(0.08, st.m3 / 700));
    drawBk(sx, bY, bW, bH, fill1, '#4CC9F0', 'Р-р 1', x.omega1.toFixed(1) + '%');

    // + sign
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.font = 'bold 24px Manrope,sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('+', sx + bW + 28, bY + bH / 2);

    drawBk(sx + bW + 55, bY, bW, bH, fill2, '#FFD166', 'Р-р 2', x.omega2.toFixed(1) + '%');

    // = sign
    ctx.font = 'bold 24px Manrope,sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('=', sx + bW * 2 + 55 + 30, bY + bH / 2);

    // mixed color
    var r0 = 76, g0 = 201, b0 = 240; // #4CC9F0
    var r1x = 255, g1x = 209, b1x = 102; // #FFD166
    var t = x.m1 / Math.max(1, x.m1 + x.m2);
    var mixR = Math.round(r0 * t + r1x * (1 - t));
    var mixG = Math.round(g0 * t + g1x * (1 - t));
    var mixB = Math.round(b0 * t + b1x * (1 - t));
    var mixColor = 'rgb(' + mixR + ',' + mixG + ',' + mixB + ')';
    drawBk(sx + bW * 2 + 55 + 55, bY, bW, bH, fill3, mixColor, 'Смесь', st.omega3.toFixed(2) + '%');

    ctx.fillStyle = 'rgba(255,255,255,0.3)';
    ctx.font = '11px Manrope,sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic';
    ctx.fillText('m₃ = ' + st.m3.toFixed(0) + ' г · ω₃ = ' + st.omega3.toFixed(3) + '%', W / 2, bY + bH + 46);
  }

  /* ── Solubility curve chart ── */
  _drawSolubility() {
    var cv = this._canvas;
    var ctx = this._ctx2d;
    var W = cv.width / (window.devicePixelRatio || 1);
    var H = cv.height / (window.devicePixelRatio || 1);
    ctx.clearRect(0, 0, W, H);

    var bg = ctx.createRadialGradient(W/2, H/2, 10, W/2, H/2, Math.max(W,H)*0.7);
    bg.addColorStop(0, '#14142a'); bg.addColorStop(1, '#0D0D1A');
    ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);

    var pad = { l: 56, r: 24, t: 20, b: 46 };
    var cW = W - pad.l - pad.r;
    var cH = H - pad.t - pad.b;
    var temps = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
    var maxS  = 0;
    SolutionsSim.SOLUBILITY.forEach(function(s, i) {
      if (this._solEnabled.has(i)) {
        s.data.forEach(function(v) { if (v > maxS) maxS = v; });
      }
    }.bind(this));
    maxS = maxS > 0 ? Math.ceil(maxS / 50) * 50 : 250;

    function toX(T) { return pad.l + (T / 100) * cW; }
    function toY(S) { return pad.t + cH - (S / maxS) * cH; }

    // Grid
    ctx.strokeStyle = 'rgba(255,255,255,0.06)';
    ctx.lineWidth = 1;
    for (var gs = 0; gs <= maxS; gs += 50) {
      ctx.beginPath(); ctx.moveTo(pad.l, toY(gs)); ctx.lineTo(pad.l + cW, toY(gs)); ctx.stroke();
    }
    for (var gt = 0; gt <= 100; gt += 20) {
      ctx.beginPath(); ctx.moveTo(toX(gt), pad.t); ctx.lineTo(toX(gt), pad.t + cH); ctx.stroke();
    }

    // Axes
    ctx.strokeStyle = 'rgba(255,255,255,0.35)';
    ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.moveTo(pad.l, pad.t); ctx.lineTo(pad.l, pad.t + cH); ctx.lineTo(pad.l + cW, pad.t + cH); ctx.stroke();

    // Axis labels
    ctx.fillStyle = 'rgba(255,255,255,0.4)';
    ctx.font = '10px Manrope,sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'top';
    temps.forEach(function(t) {
      if (t % 20 === 0) ctx.fillText(t + '°', toX(t), pad.t + cH + 6);
    });
    ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
    for (var ys = 0; ys <= maxS; ys += 50) {
      ctx.fillText(ys, pad.l - 6, toY(ys));
    }

    ctx.save();
    ctx.translate(pad.l - 40, pad.t + cH / 2);
    ctx.rotate(-Math.PI / 2);
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillStyle = 'rgba(255,255,255,0.35)';
    ctx.font = '11px Manrope,sans-serif';
    ctx.fillText('S, г / 100г H₂O', 0, 0);
    ctx.restore();
    ctx.fillStyle = 'rgba(255,255,255,0.35)';
    ctx.font = '11px Manrope,sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic';
    ctx.fillText('T, °C', pad.l + cW / 2, H - 4);

    // Curves
    SolutionsSim.SOLUBILITY.forEach(function(s, idx) {
      if (!this._solEnabled.has(idx)) return;
      ctx.strokeStyle = s.color;
      ctx.lineWidth = 2.2;
      ctx.lineJoin = 'round';
      ctx.beginPath();
      s.data.forEach(function(val, i) {
        var px = toX(temps[i]);
        var py = toY(val);
        if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
      });
      ctx.stroke();

      // End label
      var last = s.data[s.data.length - 1];
      var lx = toX(100) + 4;
      var ly = toY(last);
      ctx.fillStyle = s.color;
      ctx.font = 'bold 10px Manrope,sans-serif';
      ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
      ctx.fillText(s.label, lx, ly);
    }.bind(this));

    // Current temperature line + dots
    var Tcur = this._solT;
    ctx.strokeStyle = '#F15BB5';
    ctx.lineWidth = 1.5;
    ctx.setLineDash([5, 3]);
    ctx.beginPath(); ctx.moveTo(toX(Tcur), pad.t); ctx.lineTo(toX(Tcur), pad.t + cH); ctx.stroke();
    ctx.setLineDash([]);

    // Intersection dots + values
    SolutionsSim.SOLUBILITY.forEach(function(s, idx) {
      if (!this._solEnabled.has(idx)) return;
      // interpolate S at Tcur
      var ti = Tcur / 10;
      var lo = Math.floor(ti);
      var hi = Math.min(lo + 1, 10);
      var frac = ti - lo;
      var Sval = s.data[lo] * (1 - frac) + s.data[hi] * frac;

      var dotX = toX(Tcur);
      var dotY = toY(Sval);
      ctx.beginPath(); ctx.arc(dotX, dotY, 5, 0, Math.PI * 2);
      ctx.fillStyle = s.color; ctx.fill();
      ctx.strokeStyle = '#0D0D1A'; ctx.lineWidth = 1.5;
      ctx.stroke();

      // value popup
      ctx.fillStyle = s.color;
      ctx.font = 'bold 10px Manrope,sans-serif';
      ctx.textAlign = dotX > W * 0.6 ? 'right' : 'left';
      ctx.textBaseline = 'middle';
      var offset = dotX > W * 0.6 ? -8 : 8;
      ctx.fillText(Sval.toFixed(1), dotX + offset, dotY - 10);
    }.bind(this));

    ctx.textAlign = 'left';
  }

  /* ── stop for closeSim() ── */
  stop() {
    if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
    if (this._ro && this._canvas.parentElement) { this._ro.unobserve(this._canvas); }
  }

  /* ── fit ── */
  fit() {
    this._fitCanvas();
    this._drawViz();
  }
}

if (typeof module !== 'undefined') module.exports = SolutionsSim;

/* ─── lab UI init ─────────────────────────────────── */
var _solutionsSim = null;

function _openSolutions() {
  document.getElementById('sim-topbar-title').textContent = 'Растворы';
  _simShow('sim-solutions');

  requestAnimationFrame(function() {
    requestAnimationFrame(function() {
      var container = document.getElementById('solutions-wrap');
      if (!_solutionsSim) {
        _solutionsSim = new SolutionsSim(container);
      } else {
        _solutionsSim.fit();
      }
    });
  });
}
