'use strict';

/* ══════════════════════════════════════════════════════════════════
   OrganicSim — «Органическая химия»
   3 sub-modes:
     1. «Конструктор молекул»  — canvas 2D drag-drop builder, valence check,
        auto-class detection, IUPAC naming for alkanes
     2. «Гомологические ряды» — preset series with property table
     3. «Качественные реакции» — drag-drop reagent into test-tube, animations
   ══════════════════════════════════════════════════════════════════ */

class OrganicSim {

  /* ── Atom valences ─────────────────────────────────────────────── */
  static VALENCE = { C: 4, H: 1, O: 2, N: 3, Cl: 1, S: 2 };

  /* ── Atom display colors ───────────────────────────────────────── */
  static ATOM_COLOR = {
    C:  '#9B5DE5',
    H:  '#E0E0E0',
    O:  '#EF476F',
    N:  '#4CC9F0',
    Cl: '#34d399',
    S:  '#FFD166',
  };

  /* ── Atom radii on canvas ──────────────────────────────────────── */
  static ATOM_R = { C: 20, H: 13, O: 17, N: 16, Cl: 17, S: 17 };

  /* ── Homologous series data ────────────────────────────────────── */
  static HOMOLOG_SERIES = {
    alkanes: {
      name: 'Алканы', formula: (n) => `C${n}H${2*n+2}`, minC: 1,
      tboil: [-161.5,-88.6,-42.1,-0.5,36.1,68.7,98.4,125.6,150.8,174.1],
      tmelt: [-182.5,-183.3,-187.7,-138.3,-129.7,-95.3,-90.6,-56.8,-53.5,-29.7],
      state: (n) => n<=4 ? 'Газ' : n<=17 ? 'Жидкость' : 'Твёрдое',
      M:     (n) => 12*n + (2*n+2),
      notable: {
        1: { name: 'Метан', info: 'Природный газ, топливо, 87% в составе природного газа.' },
        2: { name: 'Этан',  info: 'Компонент природного газа, сырьё для производства этилена.' },
        6: { name: 'Гексан', info: 'Растворитель для экстракции масел.' },
      }
    },
    alkenes: {
      name: 'Алкены', formula: (n) => `C${n}H${2*n}`, minC: 2,
      tboil: [null,-103.7,-47.6,-6.3,30,63.5,93.6,121.3,146.9,170.5],
      tmelt: [null,-169.1,-185.2,-185.3,-138.9,-119.7,-101.7,-101.7,-86.9,-75.6],
      state: (n) => n<=4 ? 'Газ' : 'Жидкость',
      M:     (n) => 12*n + 2*n,
      notable: {
        2: { name: 'Этилен', info: 'Фитогормон, сырьё для полиэтилена. Участвует в созревании плодов.' },
        3: { name: 'Пропилен', info: 'Мономер полипропилена. Производство пластмасс и каучука.' },
      }
    },
    alkynes: {
      name: 'Алкины', formula: (n) => `C${n}H${2*n-2}`, minC: 2,
      tboil: [null,-84,23.2,8.1,26.1,71.4,99.7,125.2,150.8,174],
      tmelt: [null,-80.8,-101.5,-123.1,-130,-90,-81,-79.3,-65,-36],
      state: (n) => n<=4 ? 'Газ' : 'Жидкость',
      M:     (n) => 12*n + (2*n-2),
      notable: {
        2: { name: 'Ацетилен', info: 'Сварка металлов, синтез уксусной кислоты, ПВХ.' },
      }
    },
    alcohols: {
      name: 'Спирты', formula: (n) => `C${n}H${2*n+1}OH`, minC: 1,
      tboil: [64.7,78.4,97.2,117.7,137.8,157.9,176,194,213,231],
      tmelt: [-97.8,-114.1,-126,-89.5,-79,-51.6,-34.5,-17,21,6],
      state: (n) => n<=11 ? 'Жидкость' : 'Твёрдое',
      M:     (n) => 12*n + (2*n+1) + 17,
      notable: {
        1: { name: 'Метанол', info: 'Метиловый спирт. Топливо, растворитель. Ядовит!' },
        2: { name: 'Этанол',  info: 'Этиловый спирт. Напитки, медицина, растворитель, биотопливо.' },
        3: { name: 'Пропанол', info: 'Растворитель, дезинфектант.' },
      }
    },
    aldehydes: {
      name: 'Альдегиды', formula: (n) => `C${n}H${2*n}O`, minC: 1,
      tboil: [-21,20.2,48.8,74.8,103,128,152,173,191,208],
      tmelt: [-92,-123,-81,-99,-91.5,-66,-45,-26,-12,4],
      state: (n) => n<=4 ? 'Газ' : 'Жидкость',
      M:     (n) => 12*n + 2*n + 16,
      notable: {
        1: { name: 'Формальдегид', info: 'Консервант, производство смол. Ядовит.' },
        2: { name: 'Уксусный альдегид', info: 'Сырьё для уксусной кислоты.' },
      }
    },
    acids: {
      name: 'Карб. кислоты', formula: (n) => `C${n}H${2*n}O₂`, minC: 1,
      tboil: [100.7,118.1,141.2,163.7,186.3,205.3,223.1,239.3,253.9,268.5],
      tmelt: [8.3,16.6,-20.5,-7.9,-33.8,-8,16.5,12,15,31.5],
      state: (n) => n<=3 ? 'Жидкость (смеш)' : 'Жидкость',
      M:     (n) => 12*n + 2*n + 32,
      notable: {
        1: { name: 'Муравьиная', info: 'Укусы муравьёв, пчёл. Кожное средство.' },
        2: { name: 'Уксусная',   info: '3–9% в столовом уксусе. Консервация, растворитель, синтез.' },
        3: { name: 'Пропионовая', info: 'Консервант (E280). Производство гербицидов.' },
      }
    },
    amines: {
      name: 'Амины', formula: (n) => `C${n}H${2*n+3}N`, minC: 1,
      tboil: [-6.3,16.6,48.7,77.8,104.4,130.5,154,177,199,220],
      tmelt: [-93.5,-81,-83,-50,-55,-23,-12,0,17,30],
      state: (n) => n<=2 ? 'Газ' : 'Жидкость',
      M:     (n) => 12*n + (2*n+3) + 14,
      notable: {
        1: { name: 'Метиламин', info: 'Рыбный запах, синтез красителей, фармацевтика.' },
        2: { name: 'Этиламин',  info: 'Растворитель, производство каучука.' },
      }
    },
  };

  /* ── Qualitative reactions ─────────────────────────────────────── */
  static QUAL_REACTIONS = [
    {
      id: 'bromine',
      reagent: 'Br₂(водн)',
      reagentColor: '#A0520020',
      reagentLiquid: '#B85C00',
      desc: 'Бромная вода (Br₂(aq))',
      compounds: [
        { name: 'Алкен (C=C)', result: 'Обесцвечивание', equation: 'R-CH=CH-R + Br₂ → R-CHBr-CHBr-R', color: '#ffffff10', resultColor: '#F5E8CC20', symbol: '+' },
        { name: 'Алкан',      result: 'Нет реакции',    equation: 'Алканы не реагируют с Br₂(водн) при н.у.',color: '#B85C0080', resultColor: '#B85C0080', symbol: '−' },
        { name: 'Фенол',      result: 'Белый осадок',   equation: 'C₆H₅OH + 3Br₂ → C₆H₂Br₃OH↓ + 3HBr', color: '#ffffff20', resultColor: '#ffffff80', symbol: '+' },
        { name: 'Алкин',      result: 'Обесцвечивание', equation: 'HC≡CH + 2Br₂ → CHBr₂-CHBr₂', color: '#B85C0080', resultColor: '#ffffff10', symbol: '+' },
      ],
    },
    {
      id: 'kmno4',
      reagent: 'KMnO₄',
      reagentColor: '#7B2FBE80',
      reagentLiquid: '#9B3FDE',
      desc: 'Перманганат калия KMnO₄',
      compounds: [
        { name: 'Алкен',      result: 'Обесцвечивание', equation: '3R-CH=CH₂ + 2KMnO₄ + 4H₂O → 3R-CH(OH)-CH₂OH + 2MnO₂↓ + 2KOH', color: '#9B3FDE80', resultColor: '#A0700020', symbol: '+' },
        { name: 'Альдегид',   result: 'Обесцвечивание', equation: 'R-CHO + [O] → R-COOH', color: '#9B3FDE80', resultColor: '#A0700020', symbol: '+' },
        { name: 'Алкан',      result: 'Нет реакции',    equation: 'Алканы устойчивы к KMnO₄ при н.у.',  color: '#9B3FDE80', resultColor: '#9B3FDE80', symbol: '−' },
        { name: 'Алкин',      result: 'Обесцвечивание', equation: 'HC≡CH + 2KMnO₄ → 2CO₂ + 2KOH + 2MnO₂↓', color: '#9B3FDE80', resultColor: '#A0700020', symbol: '+' },
      ],
    },
    {
      id: 'silver',
      reagent: 'Ag₂O/NH₃',
      reagentColor: '#C0C0C020',
      reagentLiquid: '#C8C8C8',
      desc: 'Реакция серебряного зеркала',
      compounds: [
        { name: 'Альдегид',   result: 'Серебристый налёт', equation: 'R-CHO + Ag₂O → R-COOH + 2Ag↓', color: '#C8C8C820', resultColor: '#E8E8E880', symbol: '+', mirror: true },
        { name: 'Кетон',      result: 'Нет реакции',       equation: 'Кетоны не реагируют с реактивом Толленса',  color: '#C8C8C820', resultColor: '#C8C8C820', symbol: '−' },
        { name: 'Сахар (альд)', result: 'Серебристый налёт', equation: 'Глюкоза (альдегид) → серебристый налёт', color: '#C8C8C820', resultColor: '#E8E8E880', symbol: '+', mirror: true },
      ],
    },
    {
      id: 'cuoh2',
      reagent: 'Cu(OH)₂',
      reagentColor: '#4CC9F030',
      reagentLiquid: '#3aaad0',
      desc: 'Гидроксид меди(II)',
      compounds: [
        { name: 'Многоатом. спирт', result: 'Ярко-синий р-р',  equation: 'Глицерин + Cu(OH)₂ → ярко-синий раствор', color: '#3aaad080', resultColor: '#1E90FF80', symbol: '+', heat: false },
        { name: 'Альдегид (нагрев)', result: 'Красный Cu₂O↓', equation: 'R-CHO + 2Cu(OH)₂ →(нагрев) R-COOH + Cu₂O↓(красный) + 2H₂O', color: '#3aaad080', resultColor: '#CC440080', symbol: '+', heat: true },
        { name: 'Кетон',            result: 'Нет реакции',     equation: 'Кетоны не восстанавливают Cu(OH)₂', color: '#3aaad080', resultColor: '#3aaad080', symbol: '−' },
      ],
    },
    {
      id: 'fecl3',
      reagent: 'FeCl₃',
      reagentColor: '#D4A04040',
      reagentLiquid: '#C88020',
      desc: 'Хлорид железа(III)',
      compounds: [
        { name: 'Фенол',   result: 'Фиолетовый цвет', equation: 'C₆H₅OH + FeCl₃ → [Fe(OC₆H₅)₃]Cl₃ (фиолетовый)', color: '#C8802060', resultColor: '#8000FF80', symbol: '+' },
        { name: 'Спирт',   result: 'Нет реакции',     equation: 'Спирты не дают фиолетовой окраски с FeCl₃', color: '#C8802060', resultColor: '#C8802060', symbol: '−' },
      ],
    },
    {
      id: 'sodium',
      reagent: 'Na (металл)',
      reagentColor: '#F5F0C820',
      reagentLiquid: '#F0EAB0',
      desc: 'Натрий металлический',
      compounds: [
        { name: 'Спирт',  result: 'H₂↑ (пузырьки)', equation: '2R-OH + 2Na → 2R-ONa + H₂↑', color: '#F0EAB060', resultColor: '#6EB4D780', symbol: '+', gas: true },
        { name: 'Алкан',  result: 'Нет реакции',    equation: 'Алканы не реагируют с натрием', color: '#F0EAB060', resultColor: '#F0EAB060', symbol: '−' },
      ],
    },
  ];

  /* ── Constructor ───────────────────────────────────────────────── */
  constructor(wrap) {
    this._wrap = wrap;
    this._mode = 'constructor'; // 'constructor' | 'homologs' | 'qualitative'
    this._raf  = null;
    this._dirty = false;

    // constructor state
    this._atoms = [];
    this._bonds = [];
    this._drag  = null;
    this._pendingBond = null;  // first atom of bond being drawn
    this._toolAtom = 'C';
    this._toolBond = 1; // 1,2,3

    // homologs state
    this._homSeries = 'alkanes';
    this._homN = 1;

    // qualitative state
    this._qualReaction = OrganicSim.QUAL_REACTIONS[0];
    this._qualCompound = null;
    this._qualAnim = null;

    this._initDOM();
  }

  /* ── DOM bootstrap ─────────────────────────────────────────────── */
  _initDOM() {
    this._wrap.innerHTML = '';
    this._wrap.style.display = 'flex';
    this._wrap.style.flexDirection = 'column';
    this._wrap.style.height = '100%';
    this._wrap.style.background = '#0D0D1A';
    this._wrap.style.fontFamily = "'Manrope', sans-serif";
    this._wrap.style.color = '#e0e0e0';
    this._wrap.style.overflow = 'hidden';

    // ── top mode bar
    const modeBar = document.createElement('div');
    modeBar.style.cssText = 'display:flex;gap:8px;padding:12px 16px 0;flex-shrink:0';
    [
      ['constructor', 'Конструктор молекул'],
      ['homologs',    'Гомологические ряды'],
      ['qualitative', 'Качественные реакции'],
    ].forEach(([id, label]) => {
      const btn = document.createElement('button');
      btn.textContent = label;
      btn.dataset.mode = id;
      btn.style.cssText = 'padding:6px 14px;border-radius:8px;border:1px solid rgba(255,255,255,0.12);' +
        'background:rgba(255,255,255,0.04);color:#c0c0c0;cursor:pointer;font-size:.78rem;font-family:inherit;' +
        'transition:all .15s';
      btn.addEventListener('click', () => this._setMode(id));
      modeBar.appendChild(btn);
    });
    this._modeBar = modeBar;
    this._wrap.appendChild(modeBar);

    // ── content area
    this._content = document.createElement('div');
    this._content.style.cssText = 'flex:1;display:flex;overflow:hidden;min-height:0';
    this._wrap.appendChild(this._content);

    this._buildConstructor();
    this._setMode('constructor');
  }

  /* ── Mode switch ───────────────────────────────────────────────── */
  _setMode(mode) {
    this._mode = mode;
    // update button styles
    this._modeBar.querySelectorAll('button').forEach(b => {
      const active = b.dataset.mode === mode;
      b.style.background    = active ? 'rgba(155,93,229,0.25)' : 'rgba(255,255,255,0.04)';
      b.style.borderColor   = active ? '#9B5DE5' : 'rgba(255,255,255,0.12)';
      b.style.color         = active ? '#C9A0FF' : '#c0c0c0';
      b.style.fontWeight    = active ? '700' : '400';
    });
    // show correct panel
    if (this._consPanel)  this._consPanel.style.display  = mode === 'constructor'  ? 'flex' : 'none';
    if (this._homoPanel)  this._homoPanel.style.display  = mode === 'homologs'     ? 'flex' : 'none';
    if (this._qualPanel)  this._qualPanel.style.display  = mode === 'qualitative'  ? 'flex' : 'none';

    if (mode === 'constructor') { this._drawMolecule(); }
    if (mode === 'homologs')    { this._drawHomologs(); }
    if (mode === 'qualitative') { this._drawQual(); }
  }

  /* ══════════════════════════════════════════════════════════════
     MODE 1 — CONSTRUCTOR
  ══════════════════════════════════════════════════════════════ */
  _buildConstructor() {
    const panel = document.createElement('div');
    panel.style.cssText = 'display:flex;width:100%;height:100%';
    this._consPanel = panel;
    this._content.appendChild(panel);

    // left toolbar
    const left = document.createElement('div');
    left.style.cssText = 'width:160px;flex-shrink:0;padding:12px 10px;display:flex;flex-direction:column;gap:6px;' +
      'border-right:1px solid rgba(255,255,255,0.07);overflow-y:auto';
    panel.appendChild(left);

    // atom palette
    const atomTitle = document.createElement('div');
    atomTitle.style.cssText = 'font-size:.65rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;' +
      'color:rgba(255,255,255,0.4);margin-bottom:2px';
    atomTitle.textContent = 'Атомы';
    left.appendChild(atomTitle);

    this._atomBtns = {};
    const atomWrap = document.createElement('div');
    atomWrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px';
    ['C','H','O','N','Cl','S'].forEach(sym => {
      const btn = document.createElement('button');
      btn.textContent = sym;
      const col = OrganicSim.ATOM_COLOR[sym];
      btn.style.cssText = `width:38px;height:32px;border-radius:6px;border:1.5px solid ${col}44;` +
        `background:${col}1A;color:${col};cursor:pointer;font-size:.8rem;font-weight:700;font-family:inherit;transition:all .12s`;
      btn.addEventListener('click', () => this._selectAtom(sym));
      atomWrap.appendChild(btn);
      this._atomBtns[sym] = btn;
    });
    left.appendChild(atomWrap);

    // bond order
    const bondTitle = document.createElement('div');
    bondTitle.style.cssText = 'font-size:.65rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;' +
      'color:rgba(255,255,255,0.4);margin-bottom:2px';
    bondTitle.textContent = 'Связь';
    left.appendChild(bondTitle);

    this._bondBtns = {};
    const bondWrap = document.createElement('div');
    bondWrap.style.cssText = 'display:flex;gap:4px;margin-bottom:8px';
    [[1,'─'],[2,'═'],[3,'≡']].forEach(([n, sym]) => {
      const btn = document.createElement('button');
      btn.textContent = sym;
      btn.style.cssText = 'width:36px;height:28px;border-radius:6px;border:1.5px solid rgba(255,255,255,0.15);' +
        'background:rgba(255,255,255,0.05);color:#e0e0e0;cursor:pointer;font-size:.9rem;font-family:inherit;transition:all .12s';
      btn.addEventListener('click', () => this._selectBond(n));
      bondWrap.appendChild(btn);
      this._bondBtns[n] = btn;
    });
    left.appendChild(bondWrap);

    // separator
    const sep = document.createElement('div');
    sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.07);margin:2px 0';
    left.appendChild(sep);

    // actions
    const clearBtn = document.createElement('button');
    clearBtn.textContent = 'Очистить';
    clearBtn.style.cssText = 'padding:6px;border-radius:6px;border:1px solid rgba(239,71,111,0.3);' +
      'background:rgba(239,71,111,0.08);color:#EF476F;cursor:pointer;font-size:.75rem;font-family:inherit';
    clearBtn.addEventListener('click', () => { this._atoms = []; this._bonds = []; this._pendingBond = null; this._drawMolecule(); this._updateFormula(); });
    left.appendChild(clearBtn);

    // hint
    const hint = document.createElement('div');
    hint.style.cssText = 'font-size:.62rem;color:rgba(255,255,255,0.3);line-height:1.4;margin-top:4px';
    hint.textContent = 'Клик на холст — добавить атом. Клик на 2 атома — нарисовать связь. ПКМ — удалить.';
    left.appendChild(hint);

    // center canvas
    const canvasWrap = document.createElement('div');
    canvasWrap.style.cssText = 'flex:1;position:relative;overflow:hidden;background:#080810';
    panel.appendChild(canvasWrap);

    const canvas = document.createElement('canvas');
    canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%';
    this._molCanvas = canvas;
    canvasWrap.appendChild(canvas);

    // right info panel
    const right = document.createElement('div');
    right.style.cssText = 'width:220px;flex-shrink:0;padding:12px;display:flex;flex-direction:column;gap:8px;' +
      'border-left:1px solid rgba(255,255,255,0.07);overflow-y:auto';
    panel.appendChild(right);

    const infoTitle = document.createElement('div');
    infoTitle.style.cssText = 'font-size:.65rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;' +
      'color:rgba(255,255,255,0.4)';
    infoTitle.textContent = 'Анализ молекулы';
    right.appendChild(infoTitle);

    this._formulaEl   = this._infoBox(right, 'Молекулярная формула', '—');
    this._structEl    = this._infoBox(right, 'Структурная формула',  '—');
    this._classEl     = this._infoBox(right, 'Класс соединения',     '—');
    this._iupacEl     = this._infoBox(right, 'Название (ИЮПАК)',     '—');
    this._valenceEl   = this._infoBox(right, 'Валентность',          '—');

    // canvas events
    canvas.addEventListener('click',       e => this._molClick(e));
    canvas.addEventListener('mousedown',   e => this._molMouseDown(e));
    canvas.addEventListener('mousemove',   e => this._molMouseMove(e));
    canvas.addEventListener('mouseup',     e => this._molMouseUp(e));
    canvas.addEventListener('contextmenu', e => { e.preventDefault(); this._molRightClick(e); });

    this._selectAtom('C');
    this._selectBond(1);
    this._updateFormula();
  }

  _infoBox(parent, label, value) {
    const wrap = document.createElement('div');
    wrap.style.cssText = 'padding:8px 10px;border-radius:8px;background:rgba(255,255,255,0.04);' +
      'border:1px solid rgba(255,255,255,0.07)';
    const lbl = document.createElement('div');
    lbl.style.cssText = 'font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;' +
      'color:rgba(255,255,255,0.35);margin-bottom:3px';
    lbl.textContent = label;
    const val = document.createElement('div');
    val.style.cssText = 'font-size:.9rem;font-weight:700;color:#e8e8e8;word-break:break-all';
    val.textContent = value;
    wrap.appendChild(lbl);
    wrap.appendChild(val);
    parent.appendChild(wrap);
    return val;
  }

  _selectAtom(sym) {
    this._toolAtom = sym;
    Object.entries(this._atomBtns).forEach(([s, btn]) => {
      const col = OrganicSim.ATOM_COLOR[s];
      const active = s === sym;
      btn.style.background  = active ? `${col}40` : `${col}1A`;
      btn.style.borderColor = active ? col : `${col}44`;
      btn.style.boxShadow   = active ? `0 0 8px ${col}60` : 'none';
    });
  }

  _selectBond(n) {
    this._toolBond = n;
    Object.entries(this._bondBtns).forEach(([k, btn]) => {
      const active = parseInt(k) === n;
      btn.style.background  = active ? 'rgba(155,93,229,0.25)' : 'rgba(255,255,255,0.05)';
      btn.style.borderColor = active ? '#9B5DE5' : 'rgba(255,255,255,0.15)';
      btn.style.color       = active ? '#C9A0FF' : '#e0e0e0';
    });
  }

  /* ── Canvas size sync ─────────────────────────────────────────── */
  _fitMolCanvas() {
    const c = this._molCanvas;
    if (!c) return;
    const rect = c.getBoundingClientRect();
    if (rect.width === 0) return;
    c.width  = Math.round(rect.width  * devicePixelRatio);
    c.height = Math.round(rect.height * devicePixelRatio);
    c.getContext('2d').setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
  }

  /* ── Canvas pointer helpers ─────────────────────────────────────── */
  _molXY(e) {
    const r = this._molCanvas.getBoundingClientRect();
    return { x: e.clientX - r.left, y: e.clientY - r.top };
  }

  _atomAt(x, y) {
    return this._atoms.find(a => Math.hypot(a.x - x, a.y - y) <= OrganicSim.ATOM_R[a.sym] + 4);
  }

  /* ── Mouse/click handlers ─────────────────────────────────────── */
  _molClick(e) {
    const { x, y } = this._molXY(e);
    const hit = this._atomAt(x, y);
    if (hit) {
      // bond mode: select second atom
      if (this._pendingBond) {
        if (this._pendingBond !== hit) {
          const existing = this._bonds.find(b =>
            (b.a === this._pendingBond && b.b === hit) ||
            (b.a === hit && b.b === this._pendingBond));
          if (existing) {
            existing.order = this._toolBond;
          } else {
            this._bonds.push({ a: this._pendingBond, b: hit, order: this._toolBond });
          }
        }
        this._pendingBond = null;
      } else {
        this._pendingBond = hit;
      }
    } else {
      this._pendingBond = null;
      // place new atom
      const atom = { id: Date.now() + Math.random(), sym: this._toolAtom, x, y };
      this._atoms.push(atom);
    }
    this._drawMolecule();
    this._updateFormula();
  }

  _molMouseDown(e) {
    if (e.button !== 0) return;
    const { x, y } = this._molXY(e);
    const hit = this._atomAt(x, y);
    if (hit && !this._pendingBond) {
      this._drag = { atom: hit, ox: hit.x - x, oy: hit.y - y };
    }
  }

  _molMouseMove(e) {
    const { x, y } = this._molXY(e);
    if (this._drag) {
      this._drag.atom.x = x + this._drag.ox;
      this._drag.atom.y = y + this._drag.oy;
      this._drawMolecule();
    }
    this._molCanvas.style.cursor = this._atomAt(x, y) ? 'grab' : 'crosshair';
  }

  _molMouseUp(e) {
    if (this._drag) { this._drag = null; this._updateFormula(); }
  }

  _molRightClick(e) {
    const { x, y } = this._molXY(e);
    const hit = this._atomAt(x, y);
    if (hit) {
      this._bonds = this._bonds.filter(b => b.a !== hit && b.b !== hit);
      this._atoms = this._atoms.filter(a => a !== hit);
      if (this._pendingBond === hit) this._pendingBond = null;
      this._drawMolecule();
      this._updateFormula();
    }
  }

  /* ── Draw molecule ─────────────────────────────────────────────── */
  _drawMolecule() {
    const c = this._molCanvas;
    if (!c) return;
    this._fitMolCanvas();
    const ctx = c.getContext('2d');
    const W = c.getBoundingClientRect().width;
    const H = c.getBoundingClientRect().height;
    ctx.clearRect(0, 0, W, H);

    // subtle grid
    ctx.strokeStyle = 'rgba(255,255,255,0.03)';
    ctx.lineWidth = 1;
    for (let gx = 0; gx < W; gx += 30) {
      ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, H); ctx.stroke();
    }
    for (let gy = 0; gy < H; gy += 30) {
      ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(W, gy); ctx.stroke();
    }

    // bad valence set
    const badAtoms = this._getBadValenceAtoms();

    // bonds
    this._bonds.forEach(b => {
      const x1 = b.a.x, y1 = b.a.y, x2 = b.b.x, y2 = b.b.y;
      const dx = x2 - x1, dy = y2 - y1;
      const len = Math.hypot(dx, dy) || 1;
      const px = -dy / len, py = dx / len;
      ctx.strokeStyle = 'rgba(255,255,255,0.55)';
      ctx.lineWidth = 2;
      const offsets = b.order === 1 ? [0] : b.order === 2 ? [-3, 3] : [-5, 0, 5];
      offsets.forEach(o => {
        ctx.beginPath();
        ctx.moveTo(x1 + px * o, y1 + py * o);
        ctx.lineTo(x2 + px * o, y2 + py * o);
        ctx.stroke();
      });
    });

    // pending bond line
    if (this._pendingBond) {
      const pa = this._pendingBond;
      ctx.strokeStyle = 'rgba(155,93,229,0.4)';
      ctx.lineWidth = 1.5;
      ctx.setLineDash([4, 3]);
      ctx.beginPath();
      ctx.arc(pa.x, pa.y, OrganicSim.ATOM_R[pa.sym] + 6, 0, Math.PI * 2);
      ctx.stroke();
      ctx.setLineDash([]);
    }

    // atoms
    this._atoms.forEach(a => {
      const r = OrganicSim.ATOM_R[a.sym];
      const col = OrganicSim.ATOM_COLOR[a.sym];
      const bad = badAtoms.has(a);
      const sel = this._pendingBond === a;

      // glow
      if (sel) {
        ctx.shadowColor = '#9B5DE5';
        ctx.shadowBlur = 16;
      } else if (bad) {
        ctx.shadowColor = '#EF476F';
        ctx.shadowBlur = 12;
      }

      ctx.beginPath();
      ctx.arc(a.x, a.y, r, 0, Math.PI * 2);
      ctx.fillStyle = bad ? '#EF476F30' : (sel ? 'rgba(155,93,229,0.35)' : `${col}28`);
      ctx.fill();

      ctx.strokeStyle = bad ? '#EF476F' : col;
      ctx.lineWidth = bad ? 2 : 1.8;
      ctx.stroke();

      ctx.shadowBlur = 0;
      ctx.shadowColor = 'transparent';

      ctx.fillStyle = bad ? '#EF476F' : col;
      ctx.font = `700 ${a.sym.length > 1 ? '11' : '13'}px Manrope, sans-serif`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(a.sym, a.x, a.y);
    });
  }

  /* ── Valence check ─────────────────────────────────────────────── */
  _getBondCount(atom) {
    let count = 0;
    this._bonds.forEach(b => {
      if (b.a === atom || b.b === atom) count += b.order;
    });
    return count;
  }

  _getBadValenceAtoms() {
    const bad = new Set();
    this._atoms.forEach(a => {
      const v = OrganicSim.VALENCE[a.sym] || 1;
      const used = this._getBondCount(a);
      if (used > v) bad.add(a);
    });
    return bad;
  }

  /* ── Formula + class detection ─────────────────────────────────── */
  _updateFormula() {
    if (!this._formulaEl) return;
    const counts = {};
    this._atoms.forEach(a => { counts[a.sym] = (counts[a.sym] || 0) + 1; });

    // hill order: C first, H second, rest alphabetically
    let formula = '';
    if (counts['C']) { formula += 'C'; if (counts['C'] > 1) formula += this._sub(counts['C']); }
    if (counts['H']) { formula += 'H'; if (counts['H'] > 1) formula += this._sub(counts['H']); }
    ['N','O','S','Cl'].forEach(s => {
      if (counts[s]) { formula += s; if (counts[s] > 1) formula += this._sub(counts[s]); }
    });

    const klass = this._detectClass();
    const struct = this._buildStructural(counts);
    const iupac  = this._iupacName(counts, klass);
    const bad    = this._getBadValenceAtoms();
    const valMsg = bad.size > 0
      ? Array.from(bad).map(a => `${a.sym}(${this._getBondCount(a)}/${OrganicSim.VALENCE[a.sym]})`).join(', ') + ' — превышена валентность'
      : (this._atoms.length > 0 ? 'OK' : '—');

    this._formulaEl.textContent = formula || '—';
    this._structEl.textContent  = struct  || '—';
    this._classEl.textContent   = klass   || '—';
    this._iupacEl.textContent   = iupac   || '—';
    this._valenceEl.textContent = valMsg;
    this._valenceEl.style.color = bad.size > 0 ? '#EF476F' : '#34d399';
  }

  _sub(n) {
    const map = '₀₁₂₃₄₅₆₇₈₉';
    return String(n).split('').map(d => map[d]).join('');
  }

  _buildStructural(counts) {
    if (!counts['C'] && !counts['H']) return '';
    const parts = [];
    if (counts['C']) parts.push(`C${counts['C'] > 1 ? counts['C'] : ''}`);
    if (counts['H']) parts.push(`H${counts['H'] > 1 ? counts['H'] : ''}`);
    if (counts['O']) parts.push(`O${counts['O'] > 1 ? counts['O'] : ''}`);
    if (counts['N']) parts.push(`N${counts['N'] > 1 ? counts['N'] : ''}`);
    if (counts['Cl']) parts.push(`Cl${counts['Cl'] > 1 ? counts['Cl'] : ''}`);
    if (counts['S']) parts.push(`S${counts['S'] > 1 ? counts['S'] : ''}`);
    return parts.join('-');
  }

  /* ── Class detection ───────────────────────────────────────────── */
  _detectClass() {
    if (this._atoms.length === 0) return '—';

    const hasDoubleCO = this._bonds.some(b =>
      b.order === 2 && ((b.a.sym === 'C' && b.b.sym === 'O') || (b.a.sym === 'O' && b.b.sym === 'C')));
    const hasDoubleCC = this._bonds.some(b =>
      b.order === 2 && b.a.sym === 'C' && b.b.sym === 'C');
    const hasTripleCC = this._bonds.some(b =>
      b.order === 3 && b.a.sym === 'C' && b.b.sym === 'C');

    // detect -OH group: O connected to C (sp3) and H
    const hasOH = this._atoms.some(a => {
      if (a.sym !== 'O') return false;
      const neighbors = this._getNeighbors(a);
      return neighbors.some(n => n.sym === 'H') && neighbors.some(n => n.sym === 'C');
    });

    // detect -CHO: C with double bond to O and single bond to H
    const hasCHO = this._atoms.some(a => {
      if (a.sym !== 'C') return false;
      const nb = this._getNeighbors(a);
      const hasHNeighbor = nb.some(n => n.sym === 'H');
      const hasDoubleOBond = this._bonds.some(b =>
        b.order === 2 &&
        ((b.a === a && b.b.sym === 'O') || (b.b === a && b.a.sym === 'O')));
      return hasHNeighbor && hasDoubleOBond;
    });

    // detect -COOH: C with double O and single O (which has H)
    const hasCOOH = this._atoms.some(a => {
      if (a.sym !== 'C') return false;
      const dblO = this._bonds.filter(b =>
        b.order === 2 &&
        ((b.a === a && b.b.sym === 'O') || (b.b === a && b.a.sym === 'O'))).length;
      const sngO = this._bonds.filter(b =>
        b.order === 1 &&
        ((b.a === a && b.b.sym === 'O') || (b.b === a && b.a.sym === 'O'))).length;
      return dblO >= 1 && sngO >= 1;
    });

    // ketone: C with two double-bond O neighbors via C-C bonds with no H on the carbonyl C
    const hasKetone = hasCOOH ? false : (this._atoms.some(a => {
      if (a.sym !== 'C') return false;
      const nb = this._getNeighbors(a);
      const hasHNeighbor = nb.some(n => n.sym === 'H');
      const hasDoubleOBond = this._bonds.some(b =>
        b.order === 2 &&
        ((b.a === a && b.b.sym === 'O') || (b.b === a && b.a.sym === 'O')));
      const hasCCBond = nb.filter(n => n.sym === 'C').length >= 2;
      return hasDoubleOBond && hasCCBond && !hasHNeighbor;
    }));

    // ester: -COO- (C with dbl O and O linked to C)
    const hasEster = this._atoms.some(a => {
      if (a.sym !== 'C') return false;
      const dblOBond = this._bonds.find(b =>
        b.order === 2 && ((b.a === a && b.b.sym === 'O') || (b.b === a && b.a.sym === 'O')));
      if (!dblOBond) return false;
      const sngOs = this._bonds.filter(b =>
        b.order === 1 &&
        ((b.a === a && b.b.sym === 'O') || (b.b === a && b.a.sym === 'O')));
      return sngOs.some(b => {
        const oAtom = b.a === a ? b.b : b.a;
        const oNb = this._getNeighbors(oAtom);
        return oNb.some(n => n.sym === 'C' && n !== a);
      });
    });

    // ether: O linked to two C (no H on O)
    const hasEther = this._atoms.some(a => {
      if (a.sym !== 'O') return false;
      const nb = this._getNeighbors(a);
      return nb.filter(n => n.sym === 'C').length >= 2 && !nb.some(n => n.sym === 'H');
    });

    // amine: N with H or N between carbons
    const hasAmine = this._atoms.some(a => a.sym === 'N');

    // halide
    const hasCl = this._atoms.some(a => a.sym === 'Cl');
    const hasS  = this._atoms.some(a => a.sym === 'S');

    // only C & H present
    const onlyCH = this._atoms.every(a => a.sym === 'C' || a.sym === 'H');

    // benzene ring detection (6 C in ring with alternating bonds)
    const hasBenzene = this._detectBenzene();

    if (hasCOOH)          return 'Карбоновая кислота (-COOH)';
    if (hasEster)         return 'Сложный эфир (-COO-)';
    if (hasCHO)           return 'Альдегид (-CHO)';
    if (hasKetone)        return 'Кетон (C=O между C)';
    if (hasOH)            return 'Спирт (-OH)';
    if (hasAmine)         return 'Амин (-NHₓ)';
    if (hasBenzene)       return 'Ароматическое (бензольное кольцо)';
    if (hasTripleCC)      return 'Алкин (C≡C)';
    if (hasDoubleCC)      return 'Алкен (C=C)';
    if (hasCl)            return 'Галогеналкан (-Cl)';
    if (hasS)             return 'Серосодержащее';
    if (hasDoubleCO)      return 'Карбонильное соединение (C=O)';
    if (onlyCH)           return this._detectCHClass();
    return 'Органическое соединение';
  }

  _detectCHClass() {
    // check if cycle present
    if (this._detectCycle()) return 'Циклоалкан';
    return 'Алкан (только C-C + H)';
  }

  _detectCycle() {
    if (this._atoms.length < 3) return false;
    // simple DFS cycle detection
    const visited = new Set();
    const adj = new Map();
    this._atoms.forEach(a => adj.set(a, []));
    this._bonds.forEach(b => {
      adj.get(b.a).push(b.b);
      adj.get(b.b).push(b.a);
    });
    let hasCycle = false;
    const dfs = (node, parent) => {
      if (hasCycle) return;
      visited.add(node);
      for (const nb of adj.get(node)) {
        if (nb === parent) continue;
        if (visited.has(nb)) { hasCycle = true; return; }
        dfs(nb, node);
      }
    };
    if (this._atoms.length > 0) dfs(this._atoms[0], null);
    return hasCycle;
  }

  _detectBenzene() {
    // look for 6 C-atoms forming a ring with alternating single/double bonds
    const cAtoms = this._atoms.filter(a => a.sym === 'C');
    if (cAtoms.length < 6) return false;
    // build adjacency among C atoms
    const adj = new Map();
    cAtoms.forEach(a => adj.set(a, []));
    this._bonds.forEach(b => {
      if (b.a.sym === 'C' && b.b.sym === 'C') {
        adj.get(b.a).push(b.b);
        adj.get(b.b).push(b.a);
      }
    });
    // find if any C is in a 6-membered ring
    for (const start of cAtoms) {
      const path = [start];
      const found = this._findRing(start, start, path, adj, 6);
      if (found) return true;
    }
    return false;
  }

  _findRing(start, cur, path, adj, targetLen) {
    if (path.length === targetLen) {
      return adj.get(cur).includes(start);
    }
    for (const nb of adj.get(cur)) {
      if (path.length > 1 && nb === path[path.length - 2]) continue;
      if (path.includes(nb)) continue;
      path.push(nb);
      if (this._findRing(start, nb, path, adj, targetLen)) return true;
      path.pop();
    }
    return false;
  }

  _getNeighbors(atom) {
    const nb = [];
    this._bonds.forEach(b => {
      if (b.a === atom) nb.push(b.b);
      if (b.b === atom) nb.push(b.a);
    });
    return nb;
  }

  /* ── IUPAC name for simple alkanes ─────────────────────────────── */
  _iupacName(counts, klass) {
    if (!klass || !klass.includes('Алкан') || klass.includes('Цикло')) {
      if (!klass || !counts['C']) return '—';
    }
    const prefixes = ['','мет','эт','проп','бут','пент','гекс','гепт','окт','нон','дек'];
    const n = counts['C'] || 0;
    if (n < 1 || n > 10) return n > 10 ? `алкан C${n}` : '—';
    if (klass.includes('Циклоалкан')) return `цикло${prefixes[n]}ан`;
    if (klass.includes('Алкен'))     return `${prefixes[n]}ен`;
    if (klass.includes('Алкин'))     return `${prefixes[n]}ин`;
    if (klass.includes('Алкан'))     return `${prefixes[n]}ан`;
    if (klass.includes('Спирт'))     return `${prefixes[n]}анол-1`;
    if (klass.includes('Альдегид'))  return `${prefixes[n]}аналь`;
    if (klass.includes('Кислота'))   return `${prefixes[n]}ановая кислота`;
    if (klass.includes('Амин'))      return `${prefixes[n]}иламин`;
    return '—';
  }

  /* ══════════════════════════════════════════════════════════════
     MODE 2 — HOMOLOGS
  ══════════════════════════════════════════════════════════════ */
  _buildHomologs() {
    const panel = document.createElement('div');
    panel.style.cssText = 'display:flex;width:100%;height:100%;gap:0';
    this._homoPanel = panel;
    this._content.appendChild(panel);

    // left controls
    const left = document.createElement('div');
    left.style.cssText = 'width:200px;flex-shrink:0;padding:12px 10px;display:flex;flex-direction:column;gap:8px;' +
      'border-right:1px solid rgba(255,255,255,0.07);overflow-y:auto';
    panel.appendChild(left);

    const serLabel = document.createElement('div');
    serLabel.style.cssText = 'font-size:.65rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:rgba(255,255,255,0.4)';
    serLabel.textContent = 'Гомологический ряд';
    left.appendChild(serLabel);

    this._serBtns = {};
    Object.entries(OrganicSim.HOMOLOG_SERIES).forEach(([key, s]) => {
      const btn = document.createElement('button');
      btn.textContent = s.name;
      btn.style.cssText = 'padding:6px 10px;border-radius:6px;border:1px solid rgba(255,255,255,0.1);' +
        'background:rgba(255,255,255,0.04);color:#c0c0c0;cursor:pointer;font-size:.75rem;font-family:inherit;text-align:left';
      btn.addEventListener('click', () => this._selectSeries(key));
      left.appendChild(btn);
      this._serBtns[key] = btn;
    });

    const sep2 = document.createElement('div');
    sep2.style.cssText = 'height:1px;background:rgba(255,255,255,0.07);margin:2px 0';
    left.appendChild(sep2);

    const nLabel = document.createElement('div');
    nLabel.style.cssText = 'font-size:.65rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:rgba(255,255,255,0.4)';
    nLabel.textContent = 'Число атомов C';
    left.appendChild(nLabel);

    const sliderRow = document.createElement('div');
    sliderRow.style.cssText = 'display:flex;align-items:center;gap:8px';
    const slider = document.createElement('input');
    slider.type = 'range';
    slider.min = 1; slider.max = 10; slider.value = 1;
    slider.style.cssText = 'flex:1;accent-color:#9B5DE5';
    const nVal = document.createElement('span');
    nVal.style.cssText = 'font-size:1rem;font-weight:700;color:#C9A0FF;min-width:18px;text-align:center';
    nVal.textContent = '1';
    slider.addEventListener('input', () => {
      this._homN = parseInt(slider.value);
      nVal.textContent = this._homN;
      this._drawHomologs();
    });
    sliderRow.appendChild(slider);
    sliderRow.appendChild(nVal);
    left.appendChild(sliderRow);
    this._homSlider = slider;
    this._homNVal   = nVal;

    // center + right
    const center = document.createElement('div');
    center.style.cssText = 'flex:1;display:flex;flex-direction:column;overflow:hidden';
    panel.appendChild(center);

    // molecule sketch area
    const sketch = document.createElement('canvas');
    sketch.style.cssText = 'width:100%;height:260px;flex-shrink:0;display:block';
    this._homCanvas = sketch;
    center.appendChild(sketch);

    // properties table
    const tableWrap = document.createElement('div');
    tableWrap.style.cssText = 'flex:1;overflow-y:auto;padding:12px 16px';
    this._homTableWrap = tableWrap;
    center.appendChild(tableWrap);

    this._selectSeries('alkanes');
  }

  _selectSeries(key) {
    this._homSeries = key;
    const s = OrganicSim.HOMOLOG_SERIES[key];
    // adjust slider
    this._homSlider.min = s.minC;
    if (this._homN < s.minC) { this._homN = s.minC; this._homSlider.value = s.minC; this._homNVal.textContent = s.minC; }

    Object.entries(this._serBtns).forEach(([k, btn]) => {
      const active = k === key;
      btn.style.background  = active ? 'rgba(155,93,229,0.2)' : 'rgba(255,255,255,0.04)';
      btn.style.borderColor = active ? '#9B5DE5' : 'rgba(255,255,255,0.1)';
      btn.style.color       = active ? '#C9A0FF' : '#c0c0c0';
      btn.style.fontWeight  = active ? '700' : '400';
    });
    this._drawHomologs();
  }

  _drawHomologs() {
    if (!this._homCanvas) return;
    const s   = OrganicSim.HOMOLOG_SERIES[this._homSeries];
    const n   = this._homN;
    const idx = n - 1;

    // fit canvas
    const c = this._homCanvas;
    const rect = c.getBoundingClientRect();
    if (!rect.width) return;
    c.width  = Math.round(rect.width  * devicePixelRatio);
    c.height = Math.round(rect.height * devicePixelRatio);
    const ctx = c.getContext('2d');
    ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
    const W = rect.width, H = rect.height;

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

    // draw 2D skeletal formula
    this._drawSkeletal(ctx, W, H, s, n);

    // properties table
    const tboil = s.tboil[idx];
    const tmelt = s.tmelt[idx];
    const state = s.state(n);
    const M     = s.M(n);
    const formula = s.formula(n);
    const notable = s.notable && s.notable[n];

    let html = `
      <div style="font-size:1rem;font-weight:700;color:#C9A0FF;margin-bottom:10px">${s.name} — ${formula}</div>
      <table style="width:100%;border-collapse:collapse;font-size:.8rem">
        <thead>
          <tr style="color:rgba(255,255,255,0.4);font-size:.65rem;text-transform:uppercase;letter-spacing:.05em">
            <th style="text-align:left;padding:4px 8px;border-bottom:1px solid rgba(255,255,255,0.07)">Параметр</th>
            <th style="text-align:right;padding:4px 8px;border-bottom:1px solid rgba(255,255,255,0.07)">Значение</th>
          </tr>
        </thead>
        <tbody>
          ${this._propRow('Молярная масса', `${M} г/моль`)}
          ${this._propRow('Т. кипения', tboil != null ? `${tboil} °C` : '—')}
          ${this._propRow('Т. плавления', tmelt != null ? `${tmelt} °C` : '—')}
          ${this._propRow('Агрегатное состояние (20°C)', state)}
        </tbody>
      </table>`;

    if (notable) {
      html += `<div style="margin-top:12px;padding:10px 12px;border-radius:8px;background:rgba(155,93,229,0.1);border:1px solid rgba(155,93,229,0.25)">
        <div style="font-size:.75rem;font-weight:700;color:#C9A0FF;margin-bottom:4px">${notable.name}</div>
        <div style="font-size:.75rem;color:rgba(255,255,255,0.6);line-height:1.4">${notable.info}</div>
      </div>`;
    }

    this._homTableWrap.innerHTML = html;
  }

  _propRow(label, value) {
    return `<tr>
      <td style="padding:5px 8px;color:rgba(255,255,255,0.55);border-bottom:1px solid rgba(255,255,255,0.05)">${label}</td>
      <td style="padding:5px 8px;text-align:right;font-weight:700;color:#e8e8e8;border-bottom:1px solid rgba(255,255,255,0.05)">${value}</td>
    </tr>`;
  }

  /* ── Skeletal formula 2D auto-drawing ─────────────────────────── */
  _drawSkeletal(ctx, W, H, s, n) {
    const key = this._homSeries;
    const CX = W / 2, CY = H / 2;
    const bond = Math.min(55, (W - 80) / Math.max(n, 1));
    const R = 13;

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

    // subtle grid
    ctx.strokeStyle = 'rgba(255,255,255,0.03)';
    ctx.lineWidth = 1;
    for (let gx = 0; gx < W; gx += 30) { ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,H); ctx.stroke(); }
    for (let gy = 0; gy < H; gy += 30) { ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(W,gy); ctx.stroke(); }

    // positions: zigzag for chain
    const positions = [];
    const totalW = bond * (n - 1);
    let startX = CX - totalW / 2;
    for (let i = 0; i < n; i++) {
      const y = CY + (i % 2 === 0 ? -12 : 12);
      positions.push({ x: startX + i * bond, y });
    }

    // determine bond order for main chain
    let mainBondOrder = 1;
    if (key === 'alkenes') mainBondOrder = 2;
    if (key === 'alkynes') mainBondOrder = 3;

    const drawBond = (x1, y1, x2, y2, order) => {
      const dx = x2 - x1, dy = y2 - y1;
      const len = Math.hypot(dx, dy) || 1;
      const px = -dy/len, py = dx/len;
      ctx.strokeStyle = 'rgba(255,255,255,0.6)';
      ctx.lineWidth = 1.8;
      const offs = order === 1 ? [0] : order === 2 ? [-2.5,2.5] : [-4,0,4];
      offs.forEach(o => {
        ctx.beginPath();
        ctx.moveTo(x1 + px*o, y1 + py*o);
        ctx.lineTo(x2 + px*o, y2 + py*o);
        ctx.stroke();
      });
    };

    // main chain bonds
    for (let i = 0; i < n - 1; i++) {
      const p1 = positions[i], p2 = positions[i+1];
      const bo = (key === 'alkenes' && i === 0) ? 2
               : (key === 'alkynes' && i === 0) ? 3
               : 1;
      drawBond(p1.x, p1.y, p2.x, p2.y, bo);
    }

    // side groups: draw H or functional group atoms
    const colC  = OrganicSim.ATOM_COLOR['C'];
    const colH  = OrganicSim.ATOM_COLOR['H'];
    const colO  = OrganicSim.ATOM_COLOR['O'];
    const colN  = OrganicSim.ATOM_COLOR['N'];

    positions.forEach((p, i) => {
      let hs = 0;
      if (key === 'alkanes')   hs = (i === 0 || i === n-1) ? 3 : 2;
      if (key === 'alkenes')   hs = (i === 0) ? 2 : (i === n-1) ? 3 : 2;
      if (key === 'alkynes')   hs = (i === 0 || i === 1) ? 1 : (i === n-1) ? 3 : 2;
      if (key === 'alcohols')  {
        hs = (i === 0 || i === n-1) ? 3 : 2;
        if (i === n-1) {
          // draw -OH
          drawBond(p.x, p.y, p.x + 30, p.y - 20, 1);
          ctx.beginPath(); ctx.arc(p.x+30, p.y-20, R, 0, Math.PI*2);
          ctx.fillStyle = OrganicSim.ATOM_COLOR['O']+'30'; ctx.fill();
          ctx.strokeStyle = OrganicSim.ATOM_COLOR['O']; ctx.lineWidth=1.5; ctx.stroke();
          ctx.fillStyle = OrganicSim.ATOM_COLOR['O'];
          ctx.font='bold 11px Manrope,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
          ctx.fillText('O', p.x+30, p.y-20);
          // H on O
          drawBond(p.x+30, p.y-20, p.x+44, p.y-30, 1);
          ctx.beginPath(); ctx.arc(p.x+44, p.y-30, R-4, 0, Math.PI*2);
          ctx.fillStyle = colH+'30'; ctx.fill();
          ctx.strokeStyle = colH; ctx.lineWidth=1.2; ctx.stroke();
          ctx.fillStyle = colH; ctx.font='bold 10px Manrope,sans-serif'; ctx.fillText('H', p.x+44, p.y-30);
          hs = 2;
        }
      }
      if (key === 'aldehydes') {
        hs = i === 0 ? 1 : (i === n-1) ? 3 : 2;
        if (i === 0) {
          // draw =O
          drawBond(p.x, p.y, p.x - 28, p.y - 22, 2);
          ctx.beginPath(); ctx.arc(p.x-28, p.y-22, R, 0, Math.PI*2);
          ctx.fillStyle = colO+'30'; ctx.fill();
          ctx.strokeStyle = colO; ctx.lineWidth=1.5; ctx.stroke();
          ctx.fillStyle = colO; ctx.font='bold 11px Manrope,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
          ctx.fillText('O', p.x-28, p.y-22);
          // H on C-1
          drawBond(p.x, p.y, p.x - 28, p.y + 22, 1);
          ctx.beginPath(); ctx.arc(p.x-28, p.y+22, R-4, 0, Math.PI*2);
          ctx.fillStyle = colH+'30'; ctx.fill();
          ctx.strokeStyle = colH; ctx.lineWidth=1.2; ctx.stroke();
          ctx.fillStyle = colH; ctx.font='bold 10px Manrope,sans-serif'; ctx.fillText('H', p.x-28, p.y+22);
        }
      }
      if (key === 'acids') {
        hs = i === 0 ? 0 : (i === n-1) ? 3 : 2;
        if (i === 0) {
          // draw -COOH
          drawBond(p.x, p.y, p.x-28, p.y-22, 2);
          ctx.beginPath(); ctx.arc(p.x-28, p.y-22, R, 0, Math.PI*2);
          ctx.fillStyle = colO+'30'; ctx.fill(); ctx.strokeStyle=colO; ctx.lineWidth=1.5; ctx.stroke();
          ctx.fillStyle=colO; ctx.font='bold 11px Manrope,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
          ctx.fillText('O', p.x-28, p.y-22);
          drawBond(p.x, p.y, p.x-28, p.y+22, 1);
          ctx.beginPath(); ctx.arc(p.x-28, p.y+22, R, 0, Math.PI*2);
          ctx.fillStyle=colO+'30'; ctx.fill(); ctx.strokeStyle=colO; ctx.lineWidth=1.5; ctx.stroke();
          ctx.fillStyle=colO; ctx.fillText('O', p.x-28, p.y+22);
          // H on single O
          drawBond(p.x-28, p.y+22, p.x-42, p.y+32, 1);
          ctx.beginPath(); ctx.arc(p.x-42, p.y+32, R-4, 0, Math.PI*2);
          ctx.fillStyle=colH+'30'; ctx.fill(); ctx.strokeStyle=colH; ctx.lineWidth=1.2; ctx.stroke();
          ctx.fillStyle=colH; ctx.font='bold 10px Manrope,sans-serif'; ctx.fillText('H', p.x-42, p.y+32);
        }
      }
      if (key === 'amines') {
        hs = i === n-1 ? 2 : (i === 0 ? 1 : 2);
        if (i === n-1) {
          // draw -NH₂
          drawBond(p.x, p.y, p.x+28, p.y-22, 1);
          ctx.beginPath(); ctx.arc(p.x+28, p.y-22, R, 0, Math.PI*2);
          ctx.fillStyle=colN+'30'; ctx.fill(); ctx.strokeStyle=colN; ctx.lineWidth=1.5; ctx.stroke();
          ctx.fillStyle=colN; ctx.font='bold 11px Manrope,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
          ctx.fillText('N', p.x+28, p.y-22);
          // 2 H on N
          [[p.x+42,p.y-32],[p.x+42,p.y-12]].forEach(([hx,hy]) => {
            drawBond(p.x+28, p.y-22, hx, hy, 1);
            ctx.beginPath(); ctx.arc(hx,hy,R-4,0,Math.PI*2);
            ctx.fillStyle=colH+'30'; ctx.fill(); ctx.strokeStyle=colH; ctx.lineWidth=1.2; ctx.stroke();
            ctx.fillStyle=colH; ctx.font='bold 10px Manrope,sans-serif'; ctx.fillText('H',hx,hy);
          });
        }
      }

      // draw H on main C
      const hPositions = [[0,-1],[0,1],[-1,0]].slice(0, hs);
      hPositions.forEach(([hx0, hy0], hi) => {
        const angle = (hi / Math.max(hs,1)) * Math.PI + (i%2===0 ? Math.PI*1.1 : Math.PI*0.1);
        const hx = p.x + Math.cos(angle) * (bond*0.55);
        const hy = p.y + Math.sin(angle) * (bond*0.55);
        drawBond(p.x, p.y, hx, hy, 1);
        ctx.beginPath(); ctx.arc(hx, hy, R-3, 0, Math.PI*2);
        ctx.fillStyle = colH+'25'; ctx.fill();
        ctx.strokeStyle = colH; ctx.lineWidth = 1.2; ctx.stroke();
        ctx.fillStyle = colH;
        ctx.font = 'bold 10px Manrope,sans-serif';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        ctx.fillText('H', hx, hy);
      });

      // main C atom
      ctx.beginPath(); ctx.arc(p.x, p.y, R, 0, Math.PI*2);
      ctx.fillStyle = colC+'30'; ctx.fill();
      ctx.strokeStyle = colC; ctx.lineWidth = 1.8; ctx.stroke();
      ctx.fillStyle = colC;
      ctx.font = 'bold 12px Manrope,sans-serif';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('C', p.x, p.y);
    });

    // formula label
    const formula = s.formula(n);
    ctx.fillStyle = 'rgba(255,255,255,0.4)';
    ctx.font = '12px Manrope,sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText(formula, 12, H - 12);
  }

  /* ══════════════════════════════════════════════════════════════
     MODE 3 — QUALITATIVE REACTIONS
  ══════════════════════════════════════════════════════════════ */
  _buildQualitative() {
    const panel = document.createElement('div');
    panel.style.cssText = 'display:flex;width:100%;height:100%;gap:0';
    this._qualPanel = panel;
    this._content.appendChild(panel);

    // left reagent selector
    const left = document.createElement('div');
    left.style.cssText = 'width:200px;flex-shrink:0;padding:12px 10px;display:flex;flex-direction:column;gap:6px;' +
      'border-right:1px solid rgba(255,255,255,0.07);overflow-y:auto';
    panel.appendChild(left);

    const rl = document.createElement('div');
    rl.style.cssText = 'font-size:.65rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:rgba(255,255,255,0.4);margin-bottom:2px';
    rl.textContent = 'Реагент в пробирке';
    left.appendChild(rl);

    this._qualBtns = {};
    OrganicSim.QUAL_REACTIONS.forEach(rxn => {
      const btn = document.createElement('button');
      btn.textContent = rxn.desc;
      btn.style.cssText = 'padding:6px 8px;border-radius:6px;border:1px solid rgba(255,255,255,0.1);' +
        'background:rgba(255,255,255,0.04);color:#c0c0c0;cursor:pointer;font-size:.72rem;font-family:inherit;text-align:left;line-height:1.3';
      btn.addEventListener('click', () => this._selectQual(rxn));
      left.appendChild(btn);
      this._qualBtns[rxn.id] = btn;
    });

    const sep3 = document.createElement('div');
    sep3.style.cssText = 'height:1px;background:rgba(255,255,255,0.07);margin:2px 0';
    left.appendChild(sep3);

    const hint3 = document.createElement('div');
    hint3.style.cssText = 'font-size:.62rem;color:rgba(255,255,255,0.3);line-height:1.4';
    hint3.textContent = 'Выберите реагент → затем кликните на вещество для реакции';
    left.appendChild(hint3);

    // center: test tube canvas
    const center = document.createElement('div');
    center.style.cssText = 'flex:1;display:flex;flex-direction:column;overflow:hidden;position:relative';
    panel.appendChild(center);

    const qualCanvas = document.createElement('canvas');
    qualCanvas.style.cssText = 'width:100%;flex:1;display:block';
    this._qualCanvas = qualCanvas;
    center.appendChild(qualCanvas);

    // compounds area
    const compArea = document.createElement('div');
    compArea.style.cssText = 'padding:8px 12px;background:rgba(0,0,0,0.3);border-top:1px solid rgba(255,255,255,0.07);' +
      'display:flex;flex-wrap:wrap;gap:6px;flex-shrink:0';
    this._compArea = compArea;
    center.appendChild(compArea);

    // right: result panel
    const right = document.createElement('div');
    right.style.cssText = 'width:220px;flex-shrink:0;padding:12px;display:flex;flex-direction:column;gap:8px;' +
      'border-left:1px solid rgba(255,255,255,0.07);overflow-y:auto';
    panel.appendChild(right);

    const rt = document.createElement('div');
    rt.style.cssText = 'font-size:.65rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:rgba(255,255,255,0.4)';
    rt.textContent = 'Результат';
    right.appendChild(rt);

    this._qualResultEl = this._infoBox(right, 'Наблюдение', '—');
    this._qualEqEl     = this._infoBox(right, 'Уравнение реакции', '—');
    this._qualEqEl.style.fontSize = '.72rem';
    this._qualEqEl.style.lineHeight = '1.4';

    const resetBtn = document.createElement('button');
    resetBtn.textContent = 'Сбросить';
    resetBtn.style.cssText = 'padding:6px 10px;border-radius:6px;border:1px solid rgba(239,71,111,0.3);' +
      'background:rgba(239,71,111,0.08);color:#EF476F;cursor:pointer;font-size:.75rem;font-family:inherit;margin-top:auto';
    resetBtn.addEventListener('click', () => {
      this._qualCompound = null;
      this._qualAnim = null;
      this._qualResultEl.textContent = '—';
      this._qualEqEl.textContent = '—';
      this._drawQual();
    });
    right.appendChild(resetBtn);

    this._selectQual(OrganicSim.QUAL_REACTIONS[0]);
  }

  _selectQual(rxn) {
    this._qualReaction = rxn;
    this._qualCompound = null;
    this._qualAnim = null;
    this._qualResultEl.textContent = '—';
    this._qualEqEl.textContent = '—';

    Object.entries(this._qualBtns).forEach(([id, btn]) => {
      const active = id === rxn.id;
      btn.style.background  = active ? 'rgba(155,93,229,0.2)' : 'rgba(255,255,255,0.04)';
      btn.style.borderColor = active ? '#9B5DE5' : 'rgba(255,255,255,0.1)';
      btn.style.color       = active ? '#C9A0FF' : '#c0c0c0';
      btn.style.fontWeight  = active ? '700' : '400';
    });

    // build compound buttons
    this._compArea.innerHTML = '';
    rxn.compounds.forEach(comp => {
      const btn = document.createElement('button');
      btn.textContent = comp.name;
      btn.style.cssText = 'padding:5px 10px;border-radius:6px;border:1px solid rgba(255,255,255,0.12);' +
        'background:rgba(255,255,255,0.06);color:#e0e0e0;cursor:pointer;font-size:.78rem;font-family:inherit;' +
        'transition:all .15s';
      btn.addEventListener('click', () => this._runQualReaction(comp));
      btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(155,93,229,0.2)'; btn.style.borderColor = '#9B5DE5'; });
      btn.addEventListener('mouseleave', () => { btn.style.background = 'rgba(255,255,255,0.06)'; btn.style.borderColor = 'rgba(255,255,255,0.12)'; });
      this._compArea.appendChild(btn);
    });

    this._drawQual();
  }

  _runQualReaction(comp) {
    this._qualCompound = comp;
    this._qualResultEl.textContent = comp.result;
    this._qualEqEl.textContent = comp.equation;
    this._qualResultEl.style.color = comp.symbol === '+' ? '#34d399' : '#EF476F';

    // start animation
    this._qualAnim = {
      compound: comp,
      t: 0,
      maxT: 120,
    };
    this._animQual();
  }

  _animQual() {
    if (!this._qualAnim) return;
    this._qualAnim.t++;
    this._drawQual();
    if (this._qualAnim.t < this._qualAnim.maxT) {
      this._raf = requestAnimationFrame(() => this._animQual());
    }
  }

  _drawQual() {
    const c = this._qualCanvas;
    if (!c) return;
    const rect = c.getBoundingClientRect();
    if (!rect.width) return;
    c.width  = Math.round(rect.width  * devicePixelRatio);
    c.height = Math.round(rect.height * devicePixelRatio);
    const ctx = c.getContext('2d');
    ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
    const W = rect.width, H = rect.height;

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

    // subtle grid
    ctx.strokeStyle = 'rgba(255,255,255,0.03)';
    ctx.lineWidth = 1;
    for (let gx = 0; gx < W; gx += 30) { ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,H); ctx.stroke(); }
    for (let gy = 0; gy < H; gy += 30) { ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(W,gy); ctx.stroke(); }

    const rxn = this._qualReaction;
    const comp = this._qualCompound;
    const anim = this._qualAnim;

    // draw multiple test tubes
    const tubes = rxn.compounds;
    const tubeW = 56, tubeH = 150, gap = 20;
    const totalW = tubes.length * (tubeW + gap) - gap;
    let startX = (W - totalW) / 2;

    tubes.forEach((tube, i) => {
      const tx = startX + i * (tubeW + gap);
      const ty = (H - tubeH) / 2 - 10;
      const isActive = comp && comp === tube;
      const progress = (isActive && anim) ? Math.min(anim.t / anim.maxT, 1) : 0;

      this._drawTestTube(ctx, tx, ty, tubeW, tubeH, rxn, tube, progress, isActive);

      // label
      ctx.fillStyle = isActive ? '#C9A0FF' : 'rgba(255,255,255,0.5)';
      ctx.font = `${isActive ? '700' : '400'} 10px Manrope,sans-serif`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'top';
      const label = tube.name.length > 12 ? tube.name.substring(0,11)+'…' : tube.name;
      ctx.fillText(label, tx + tubeW/2, ty + tubeH + 8);
    });

    // reagent label
    ctx.fillStyle = 'rgba(255,255,255,0.35)';
    ctx.font = '11px Manrope,sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'bottom';
    ctx.fillText(`Реагент: ${rxn.reagent}`, W/2, H - 4);
  }

  _drawTestTube(ctx, x, y, w, h, rxn, comp, progress, isActive) {
    const liqH = h * 0.55;
    const liqY = y + h - liqH;

    // glass tube outline
    ctx.save();
    ctx.strokeStyle = isActive ? '#9B5DE5' : 'rgba(255,255,255,0.25)';
    ctx.lineWidth   = isActive ? 2 : 1.5;

    // tube shape: rect top + rounded bottom
    ctx.beginPath();
    ctx.moveTo(x + 4, y);
    ctx.lineTo(x + 4, y + h - w/2 + 4);
    ctx.arcTo(x + 4, y + h, x + w/2, y + h, w/2 - 4);
    ctx.arcTo(x + w - 4, y + h, x + w - 4, y + h - w/2 + 4, w/2 - 4);
    ctx.lineTo(x + w - 4, y);
    ctx.stroke();

    // clip to tube for liquid
    ctx.beginPath();
    ctx.rect(x + 4, liqY, w - 8, liqH - 8);
    ctx.arc(x + w/2, y + h - (w/2 - 4), w/2 - 4, 0, Math.PI);
    ctx.clip();

    // base liquid (reagent color)
    const baseColor = rxn.reagentLiquid;
    ctx.fillStyle = baseColor + 'A0';
    ctx.fillRect(x + 4, liqY, w - 8, liqH);

    // result color overlay animated
    if (progress > 0) {
      const resColor = comp.resultColor;
      ctx.globalAlpha = progress;
      ctx.fillStyle = resColor;
      ctx.fillRect(x + 4, liqY, w - 8, liqH);
      ctx.globalAlpha = 1;

      // precipitate settling
      if (comp.symbol === '+' && progress > 0.4) {
        const precH = (progress - 0.4) / 0.6 * 20;
        ctx.fillStyle = 'rgba(255,255,255,0.5)';
        ctx.fillRect(x + 4, y + h - (w/2-4) - precH, w - 8, precH);
      }

      // silver mirror effect
      if (comp.mirror && progress > 0.3) {
        const mirrorA = (progress - 0.3) / 0.7;
        const grad = ctx.createLinearGradient(x+4, liqY, x+w-4, liqY);
        grad.addColorStop(0,   `rgba(220,220,220,${mirrorA*0.3})`);
        grad.addColorStop(0.5, `rgba(240,240,240,${mirrorA*0.8})`);
        grad.addColorStop(1,   `rgba(200,200,200,${mirrorA*0.3})`);
        ctx.fillStyle = grad;
        ctx.fillRect(x + 4, liqY, w - 8, 8);
      }

      // gas bubbles
      if (comp.gas && progress > 0.2) {
        const numBubbles = Math.floor((progress - 0.2) / 0.8 * 8);
        for (let b = 0; b < numBubbles; b++) {
          const bx = x + 8 + (b * 7) % (w - 16);
          const by = y + h - 30 - (((this._qualAnim ? this._qualAnim.t : 0) * 3 + b * 15) % 80);
          ctx.beginPath();
          ctx.arc(bx, by, 3, 0, Math.PI * 2);
          ctx.fillStyle = 'rgba(200,220,255,0.6)';
          ctx.fill();
        }
      }

      // heat shimmer lines
      if (comp.heat && progress > 0.5) {
        ctx.strokeStyle = 'rgba(255,120,50,0.4)';
        ctx.lineWidth = 1;
        for (let ln = 0; ln < 3; ln++) {
          ctx.beginPath();
          const lx = x + 10 + ln * 12;
          ctx.moveTo(lx, liqY + 10);
          ctx.quadraticCurveTo(lx + 4, liqY + 20, lx, liqY + 30);
          ctx.stroke();
        }
      }
    }

    ctx.restore();

    // result symbol badge
    if (comp.symbol) {
      ctx.fillStyle = comp.symbol === '+' ? '#34d399' : '#EF476F';
      ctx.font = 'bold 14px Manrope,sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(comp.symbol, x + w/2, y - 12);
    } else {
      // pending indicator
      ctx.fillStyle = 'rgba(255,255,255,0.2)';
      ctx.font = '12px Manrope,sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText('?', x + w/2, y - 12);
    }
  }

  /* ── Lifecycle ─────────────────────────────────────────────────── */
  start() {
    this._buildHomologs();
    this._buildQualitative();
    this._setMode(this._mode);
  }

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

  fit() {
    if (this._mode === 'constructor') this._drawMolecule();
    if (this._mode === 'homologs')   this._drawHomologs();
    if (this._mode === 'qualitative') this._drawQual();
  }
}

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

function _openOrganic() {
  document.getElementById('sim-topbar-title').textContent = 'Органическая химия';
  _simShow('sim-organic');

  requestAnimationFrame(() => requestAnimationFrame(() => {
    const wrap = document.getElementById('sim-organic');
    if (!organicSim) {
      organicSim = new OrganicSim(wrap);
      organicSim.start();
    } else {
      organicSim.fit();
    }
  }));
}
