/**
 * GasSim v2 — Ideal Gas simulation (PV=nRT, Maxwell-Boltzmann distribution)
 * v2: hover inspector, velocity vectors, movable piston, v_mp/v_rms markers.
 */
class GasSim {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.W = 0;
    this.H = 0;
    this.particles = [];
    this.N = 80;
    this.T = 1.0;
    this._wallImpulse = 0;
    this._pressureSmooth = 0;
    this._raf = null;
    this._updateTick = 0;
    this.onUpdate = null;
    this._loop = this._loop.bind(this);

    // v2
    this._showVectors = false;
    this._pistonFrac  = 1.0;   // fraction of W — right wall position
    this._hover       = null;  // hovered particle
    this._pistonDrag  = false;

    // LabFX throttle
    this._fxPressureTimer = 0;
    this._fxLastT = 0;

    canvas.addEventListener('mousemove',  e => this._onMouseMove(e));
    canvas.addEventListener('mouseleave', () => { this._hover = null; this._pistonDrag = false; });
    canvas.addEventListener('mousedown',  e => this._onMouseDown(e));
    canvas.addEventListener('mouseup',    () => { this._pistonDrag = false; });
  }

  // ── canvas coordinate helper ────────────────────────────────────────────────
  _cp(e) {
    const r = this.canvas.getBoundingClientRect();
    return {
      x: (e.clientX - r.left) * (this.W / r.width),
      y: (e.clientY - r.top)  * (this.H / r.height),
    };
  }

  _onMouseDown(e) {
    const { x } = this._cp(e);
    const px = this.W * this._pistonFrac;
    if (Math.abs(x - px) < 16) this._pistonDrag = true;
  }

  _onMouseMove(e) {
    const { x, y } = this._cp(e);

    if (this._pistonDrag) {
      this.setPiston(x / this.W);
      return;
    }

    // nearest particle within 28px
    let best = null, bestD = 28;
    for (const p of this.particles) {
      const d = Math.hypot(p.x - x, p.y - y);
      if (d < bestD) { bestD = d; best = p; }
    }
    this._hover = best;
  }

  // ── public API ──────────────────────────────────────────────────────────────
  fit() {
    this.W = this.canvas.offsetWidth;
    this.H = this.canvas.offsetHeight;
    this.canvas.width  = this.W * devicePixelRatio;
    this.canvas.height = this.H * devicePixelRatio;
    this.ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
    this.reset();
  }

  reset() {
    this.particles = [];
    const px = this.W * this._pistonFrac;
    for (let i = 0; i < this.N; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = this._maxwellSpeed();
      this.particles.push({
        x:  20 + Math.random() * (px - 40),
        y:  20 + Math.random() * (this.H - 40),
        vx: s * Math.cos(a),
        vy: s * Math.sin(a),
        r:  5,
      });
    }
    this._wallImpulse    = 0;
    this._pressureSmooth = 0;
    this._updateTick     = 0;
    this._hover          = null;
  }

  setN(n)  { this.N = n; this.reset(); }

  setT(t) {
    const oldT = this.T;
    if (oldT <= 0) { this.T = t; this.reset(); return; }
    const f = Math.sqrt(t / oldT);
    for (const p of this.particles) { p.vx *= f; p.vy *= f; }
    this.T = t;
  }

  setPiston(frac) {
    this._pistonFrac = Math.max(0.3, Math.min(1.0, frac));
    const px = this.W * this._pistonFrac;
    for (const p of this.particles) {
      if (p.x + p.r > px) { p.x = px - p.r; if (p.vx > 0) p.vx = -p.vx; }
    }
  }

  toggleVectors() { this._showVectors = !this._showVectors; }

  start() { if (!this._raf) this._raf = requestAnimationFrame(this._loop); }
  stop()  { cancelAnimationFrame(this._raf); this._raf = null; }

  // ── simulation ──────────────────────────────────────────────────────────────
  _loop(now) {
    const dt = this._fxLastT ? Math.min(now - this._fxLastT, 80) : 16;
    this._fxLastT = now;
    this._step();
    this._step();
    if (window.LabFX) {
      LabFX.particles.update(dt);
      // throttled pressure tick sound (~every 150ms, proportional to pressure)
      this._fxPressureTimer += dt;
      if (this._fxPressureTimer >= 150) {
        this._fxPressureTimer = 0;
        const P = parseFloat(this.info().P);
        if (P > 5) LabFX.sound.play('tick', { volume: 0.05 });
      }
    }
    this.draw();
    this._raf = requestAnimationFrame(this._loop);
  }

  _maxwellSpeed() {
    const u1 = Math.max(1e-10, Math.random());
    const sigma = this.T * 60;
    return Math.abs(Math.sqrt(-2 * Math.log(u1)) * Math.cos(Math.PI * 2 * Math.random()) * sigma + sigma);
  }

  _step() {
    const { W, H, particles } = this;
    const px = W * this._pistonFrac;

    for (const p of particles) { p.x += p.vx; p.y += p.vy; }

    for (const p of particles) {
      if (p.x < p.r) {
        p.x = p.r; p.vx = Math.abs(p.vx);
        this._wallImpulse += 2 * Math.abs(p.vx);
      } else if (p.x > px - p.r) {
        p.x = px - p.r; p.vx = -Math.abs(p.vx);
        this._wallImpulse += 2 * Math.abs(p.vx);
      }
      if (p.y < p.r) {
        p.y = p.r; p.vy = Math.abs(p.vy);
        this._wallImpulse += 2 * Math.abs(p.vy);
      } else if (p.y > H - p.r) {
        p.y = H - p.r; p.vy = -Math.abs(p.vy);
        this._wallImpulse += 2 * Math.abs(p.vy);
      }
    }

    // Spatial grid collision
    const cell = 14, cols = Math.ceil(W / cell), rows = Math.ceil(H / cell);
    const grid = new Map();
    const key  = (cx, cy) => cy * cols + cx;

    for (let i = 0; i < particles.length; i++) {
      const p = particles[i];
      const k = key(Math.floor(p.x / cell), Math.floor(p.y / cell));
      if (!grid.has(k)) grid.set(k, []);
      grid.get(k).push(i);
    }

    const checked = new Set();
    for (let i = 0; i < particles.length; i++) {
      const p  = particles[i];
      const cx = Math.floor(p.x / cell);
      const cy = Math.floor(p.y / cell);
      for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
        const nx = cx + dx, ny = cy + dy;
        if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
        const cell2 = grid.get(key(nx, ny));
        if (!cell2) continue;
        for (const j of cell2) {
          if (j <= i) continue;
          const pk = i * 100000 + j;
          if (checked.has(pk)) continue;
          checked.add(pk);
          const q = particles[j];
          const ddx = q.x - p.x, ddy = q.y - p.y;
          const d2 = ddx * ddx + ddy * ddy;
          const md = p.r + q.r;
          if (d2 < md * md && d2 > 0) {
            const d = Math.sqrt(d2), nx2 = ddx / d, ny2 = ddy / d;
            const dvn = (q.vx - p.vx) * nx2 + (q.vy - p.vy) * ny2;
            if (dvn >= 0) continue;
            p.vx += dvn * nx2; p.vy += dvn * ny2;
            q.vx -= dvn * nx2; q.vy -= dvn * ny2;
            const ov = (md - d) / 2;
            p.x -= ov * nx2; p.y -= ov * ny2;
            q.x += ov * nx2; q.y += ov * ny2;
          }
        }
      }
    }

    this._pressureSmooth = this._pressureSmooth * 0.92 + this._wallImpulse * 0.08;
    this._wallImpulse = 0;

    if (++this._updateTick % 30 === 0 && this.onUpdate) this.onUpdate(this.info());
  }

  info() {
    const speeds   = this.particles.map(p => Math.hypot(p.vx, p.vy));
    const avgSpeed = speeds.length ? speeds.reduce((a, b) => a + b) / speeds.length : 0;
    const pf = this._pistonFrac;
    const P  = this._pressureSmooth / (2 * (this.W * pf + this.H)) * 100;
    const V  = (this.W * pf * this.H) / 10000;
    return {
      N: this.N, T: this.T,
      P: P.toFixed(1), V: V.toFixed(1), PV: (P * V).toFixed(1),
      avgSpeed: avgSpeed.toFixed(0),
      speedData: this._speedHistogram(speeds),
    };
  }

  _speedHistogram(speeds) {
    const maxSpeed = this.T * 200;
    const numBins  = 12;
    const binWidth = maxSpeed / numBins;
    const bins     = new Array(numBins).fill(0);
    for (const s of speeds) {
      const idx = Math.floor(s / binWidth);
      if (idx >= 0 && idx < numBins) bins[idx]++;
    }
    return { bins, max: Math.max(...bins, 1), binWidth };
  }

  _mbCurve(v) {
    const sigma = this.T * 60;
    return (v / (sigma * sigma)) * Math.exp(-v * v / (2 * sigma * sigma));
  }

  // ── drawing ─────────────────────────────────────────────────────────────────
  draw() {
    const { ctx, W, H } = this;
    const pistonX = W * this._pistonFrac;

    // Background
    const bg = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, Math.max(W, H) * 0.7);
    bg.addColorStop(0, '#080818'); bg.addColorStop(1, '#030308');
    ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);

    // Grid
    ctx.strokeStyle = 'rgba(255,255,255,0.03)'; ctx.lineWidth = 1;
    ctx.beginPath();
    for (let x = 0; x <= W; x += 20) { ctx.moveTo(x, 0); ctx.lineTo(x, H); }
    for (let y = 0; y <= H; y += 20) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
    ctx.stroke();

    // Dead zone beyond piston
    if (this._pistonFrac < 0.99) {
      ctx.fillStyle = 'rgba(0,0,0,0.55)';
      ctx.fillRect(pistonX, 0, W - pistonX, H);
    }

    // Pressure wall glow
    const P = parseFloat(this.info().P);
    const wi = Math.min(1, P / 50);
    if (wi > 0) {
      const a = wi * 0.3, gd = 30;
      const glows = [
        [ctx.createLinearGradient(0, 0, gd, 0),                        0,           0, gd,    H],
        [ctx.createLinearGradient(pistonX, 0, pistonX - gd, 0),        pistonX - gd, 0, gd,    H],
        [ctx.createLinearGradient(0, 0, 0, gd),                        0,           0, W,     gd],
        [ctx.createLinearGradient(0, H, 0, H - gd),                    0,     H - gd, W,     gd],
      ];
      for (const [g, rx, ry, rw, rh] of glows) {
        g.addColorStop(0, `rgba(155,93,229,${a})`);
        g.addColorStop(1, 'rgba(155,93,229,0)');
        ctx.fillStyle = g; ctx.fillRect(rx, ry, rw, rh);
      }
    }

    // Velocity vectors
    if (this._showVectors) {
      ctx.save();
      for (const p of this.particles) {
        const scale = 3;
        const ex = p.x + p.vx * scale, ey = p.y + p.vy * scale;
        const ang = Math.atan2(p.vy, p.vx);
        ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 1;
        ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(ex, ey); ctx.stroke();
        const hl = 4;
        ctx.fillStyle = 'rgba(255,255,255,0.3)';
        ctx.beginPath();
        ctx.moveTo(ex, ey);
        ctx.lineTo(ex - hl * Math.cos(ang - 0.4), ey - hl * Math.sin(ang - 0.4));
        ctx.lineTo(ex - hl * Math.cos(ang + 0.4), ey - hl * Math.sin(ang + 0.4));
        ctx.closePath(); ctx.fill();
      }
      ctx.restore();
    }

    // Particles
    for (const p of this.particles) {
      const spd   = Math.hypot(p.vx, p.vy);
      const T     = this.T;
      const color = spd < T * 40 ? '#4CC9F0' : spd < T * 80 ? '#7BF5A4' : spd < T * 120 ? '#FFD166' : '#EF476F';
      const isH   = this._hover === p;
      ctx.save();
      ctx.shadowBlur  = isH ? 20 : 8;
      ctx.shadowColor = color;
      ctx.beginPath(); ctx.arc(p.x, p.y, isH ? p.r + 2 : p.r, 0, Math.PI * 2);
      ctx.fillStyle = color; ctx.fill();
      if (isH) { ctx.strokeStyle = 'rgba(255,255,255,0.6)'; ctx.lineWidth = 1.5; ctx.stroke(); }
      ctx.restore();
    }

    // Piston
    this._drawPiston(ctx, pistonX, H);

    // Hover inspector
    if (this._hover) this._drawInspector(ctx, this._hover, W, H);

    // Histogram
    this._drawHistogram(ctx, W, H);

    if (window.LabFX) LabFX.particles.draw(ctx);
  }

  _drawPiston(ctx, pistonX, H) {
    if (this._pistonFrac >= 0.99) return;
    ctx.save();
    const pw = 8;
    ctx.shadowBlur  = 16; ctx.shadowColor = 'rgba(255,209,102,0.5)';
    const g = ctx.createLinearGradient(pistonX - pw, 0, pistonX + pw, 0);
    g.addColorStop(0, 'rgba(255,209,102,0.4)');
    g.addColorStop(0.5, 'rgba(255,209,102,0.9)');
    g.addColorStop(1, 'rgba(255,209,102,0.3)');
    ctx.fillStyle = g; ctx.fillRect(pistonX - pw / 2, 0, pw, H);

    // Handle
    const hh = 44, hw = 18, hx = pistonX - hw / 2, hy = H / 2 - hh / 2;
    ctx.shadowBlur = 0;
    ctx.fillStyle = 'rgba(255,209,102,0.88)';
    ctx.beginPath(); ctx.roundRect(hx, hy, hw, hh, 4); ctx.fill();
    ctx.strokeStyle = 'rgba(0,0,0,0.25)'; ctx.lineWidth = 1.5;
    for (let i = 0; i < 3; i++) {
      const gy = hy + 10 + i * 10;
      ctx.beginPath(); ctx.moveTo(hx + 4, gy); ctx.lineTo(hx + hw - 4, gy); ctx.stroke();
    }

    ctx.fillStyle = 'rgba(255,209,102,0.7)';
    ctx.font = "bold 9px 'Manrope', sans-serif";
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('⇌', pistonX, hy - 12);
    ctx.restore();
  }

  _drawInspector(ctx, p, W, H) {
    const spd = Math.hypot(p.vx, p.vy);
    const ang = Math.atan2(p.vy, p.vx) * 180 / Math.PI;
    const ke  = 0.5 * spd * spd;
    const T   = this.T;
    const clr = spd < T * 40 ? '#4CC9F0' : spd < T * 80 ? '#7BF5A4' : spd < T * 120 ? '#FFD166' : '#EF476F';

    const rows = [
      ['|v|',  spd.toFixed(1) + ' у.е.'],
      ['vx',   p.vx.toFixed(1)],
      ['vy',   p.vy.toFixed(1)],
      ['KE',   ke.toFixed(0) + ' у.е.'],
      ['угол', ang.toFixed(1) + '°'],
    ];

    const tw = 132, th = 18 + rows.length * 17 + 8;
    let tx = p.x + 14, ty = p.y - th / 2;
    if (tx + tw > W - 10) tx = p.x - tw - 14;
    ty = Math.max(8, Math.min(H - th - 8, ty));

    ctx.save();
    ctx.fillStyle = 'rgba(6,8,28,0.92)';
    ctx.beginPath(); ctx.roundRect(tx, ty, tw, th, 8); ctx.fill();

    ctx.fillStyle = clr;
    ctx.beginPath(); ctx.roundRect(tx, ty, tw, 3, [8, 8, 0, 0]); ctx.fill();

    ctx.strokeStyle = 'rgba(255,255,255,0.08)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.roundRect(tx, ty, tw, th, 8); ctx.stroke();

    ctx.beginPath(); ctx.arc(p.x, p.y, p.r + 5, 0, Math.PI * 2);
    ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 1; ctx.stroke();

    ctx.font = "11px 'Manrope', monospace"; ctx.textBaseline = 'middle';
    for (let i = 0; i < rows.length; i++) {
      const ry = ty + 18 + i * 17;
      ctx.fillStyle = 'rgba(255,255,255,0.42)'; ctx.textAlign = 'left';
      ctx.fillText(rows[i][0], tx + 10, ry);
      ctx.fillStyle = 'rgba(255,255,255,0.92)'; ctx.textAlign = 'right';
      ctx.fillText(rows[i][1], tx + tw - 10, ry);
    }
    ctx.restore();
  }

  _drawHistogram(ctx, W, H) {
    const speeds = this.particles.map(p => Math.hypot(p.vx, p.vy));
    const hist   = this._speedHistogram(speeds);

    const hw = 204, hh = 102;
    const hx = W - hw - 12, hy = H - hh - 12;
    const pad = { l: 8, r: 8, t: 20, b: 18 };
    const barW    = (hw - pad.l - pad.r) / hist.bins.length;
    const barAreaH = hh - pad.t - pad.b;
    const maxV    = this.T * 200;

    ctx.save();
    ctx.fillStyle = 'rgba(0,0,0,0.58)';
    ctx.beginPath(); ctx.roundRect(hx, hy, hw, hh, 6); ctx.fill();

    ctx.fillStyle = 'rgba(255,255,255,0.72)'; ctx.font = '9px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('Распределение скоростей', hx + hw / 2, hy + 11);

    ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.font = '8px sans-serif';
    ctx.fillText('v (у.е.)', hx + hw / 2, hy + hh - 2);

    // Bars
    for (let i = 0; i < hist.bins.length; i++) {
      const ratio = hist.bins[i] / hist.max;
      const bh = ratio * barAreaH;
      const bx = hx + pad.l + i * barW;
      const by = hy + pad.t + barAreaH - bh;
      ctx.fillStyle = 'rgba(155,93,229,0.75)';
      ctx.beginPath(); ctx.roundRect(bx + 0.5, by, barW - 1, bh, 2); ctx.fill();
    }

    // MB theoretical curve
    ctx.strokeStyle = 'rgba(255,209,102,0.9)'; ctx.lineWidth = 1.5;
    ctx.setLineDash([3, 3]); ctx.beginPath();
    let first = true;
    for (let i = 0; i <= 80; i++) {
      const v  = (i / 80) * maxV;
      const sc = this._mbCurve(v) * speeds.length * hist.binWidth / hist.max;
      const cx2 = hx + pad.l + (v / maxV) * (hw - pad.l - pad.r);
      const cy2 = hy + pad.t + barAreaH - sc * barAreaH;
      if (first) { ctx.moveTo(cx2, cy2); first = false; }
      else ctx.lineTo(cx2, cy2);
    }
    ctx.stroke(); ctx.setLineDash([]);

    // Characteristic speed lines
    const sigma   = this.T * 60;
    const v_mp    = sigma;              // v most probable (mode)
    const v_rms   = sigma * Math.sqrt(2); // v_rms in 2D = sqrt(2) * sigma

    const vline = (v, color, label) => {
      if (v > maxV) return;
      const vx2 = hx + pad.l + (v / maxV) * (hw - pad.l - pad.r);
      ctx.strokeStyle = color; ctx.lineWidth = 1;
      ctx.setLineDash([2, 3]);
      ctx.beginPath(); ctx.moveTo(vx2, hy + pad.t); ctx.lineTo(vx2, hy + pad.t + barAreaH); ctx.stroke();
      ctx.setLineDash([]);
      ctx.fillStyle = color; ctx.font = '7px sans-serif'; ctx.textAlign = 'center';
      ctx.fillText(label, vx2, hy + pad.t - 3);
    };
    vline(v_mp,  'rgba(76,201,240,0.9)',  'v_mp');
    vline(v_rms, 'rgba(239,71,111,0.9)',  'v_rms');

    ctx.restore();
  }
}

/* ─── lab UI init ─────────────────────────────────── */
  function _openMolPhys(mode) {
    document.getElementById('sim-topbar-title').textContent = 'Молекулярная физика';
    _simShow('sim-molphys');
    _simShow('ctrl-molphys');

    requestAnimationFrame(() => requestAnimationFrame(() => {
      // lazy-init all sims
      if (!gasSim) { gasSim = new GasSim(document.getElementById('gas-canvas')); gasSim.onUpdate = _gasUpdateUI; }
      if (!brownSim) { brownSim = new BrownianSim(document.getElementById('brownian-canvas')); brownSim.onUpdate = _brownUpdateUI; }
      if (!statesSim) { statesSim = new StatesSim(document.getElementById('states-canvas')); statesSim.onUpdate = _statesUpdateUI; }
      if (!diffSim) { diffSim = new DiffusionSim(document.getElementById('diffusion-canvas')); diffSim.onUpdate = _diffUpdateUI; }

      molMode(mode || 'gas');
    }));
  }

  function molMode(mode, btn) {
    _molMode = mode;
    // stop all
    if (gasSim) gasSim.stop();
    if (brownSim) brownSim.stop();
    if (statesSim) statesSim.stop();
    if (diffSim) diffSim.stop();

    // toggle mode buttons
    document.querySelectorAll('.mol-mode').forEach(b => b.classList.remove('active'));
    if (btn) btn.classList.add('active');
    else { const mb = document.getElementById('mol-mode-' + mode); if (mb) mb.classList.add('active'); }

    // toggle panels
    const panels = ['gas', 'brownian', 'states', 'diffusion'];
    panels.forEach(p => {
      document.getElementById('mol-panel-' + p).style.display = p === mode ? '' : 'none';
    });

    // toggle canvases
    document.getElementById('gas-canvas').style.display       = mode === 'gas' ? 'block' : 'none';
    document.getElementById('brownian-canvas').style.display   = mode === 'brownian' ? 'block' : 'none';
    document.getElementById('states-canvas').style.display     = mode === 'states' ? 'block' : 'none';
    document.getElementById('diffusion-canvas').style.display  = mode === 'diffusion' ? 'block' : 'none';

    // toggle topbar diffusion partition button
    document.getElementById('ctrl-mol-diff').style.display = mode === 'diffusion' ? 'contents' : 'none';

    // start active sim
    const titles = { gas: 'Молекулярная физика — Газ', brownian: 'Молекулярная физика — Броуновское', states: 'Молекулярная физика — Фазы', diffusion: 'Молекулярная физика — Диффузия' };
    document.getElementById('sim-topbar-title').textContent = titles[mode] || 'Молекулярная физика';

    if (mode === 'gas')       { gasSim.fit(); gasSim.start(); }
    if (mode === 'brownian')  { brownSim.fit(); brownSim.start(); }
    if (mode === 'states')    { statesSim.fit(); statesSim.start(); }
    if (mode === 'diffusion') { diffSim.fit(); diffSim.start(); }
  }

  function molReset() {
    if (window.LabFX) LabFX.sound.play('click');
    if (_molMode === 'gas' && gasSim) {
      gasSim.reset();
      document.getElementById('sl-gPiston').value = 100;
      document.getElementById('g-piston').textContent = '100%';
    }
    if (_molMode === 'brownian' && brownSim) brownSim.reset();
    if (_molMode === 'states' && statesSim) {
      statesSim.reset();
      document.getElementById('sl-stN').value = 64;
      document.getElementById('st-N').textContent = '64';
      const vBtn = document.getElementById('states-vec-btn');
      if (vBtn) { vBtn.textContent = 'Векторы скоростей: Выкл'; vBtn.style.color = ''; }
    }
    if (_molMode === 'diffusion' && diffSim) {
      diffSim.reset();
      document.getElementById('diffusion-part-btn').textContent = '‖ Раздел';
      document.getElementById('df-part-row').classList.add('active');
      document.getElementById('df-pore-row').classList.remove('active');
    }
  }

  function gasNChange() {
    const n = +document.getElementById('sl-gN').value;
    document.getElementById('g-N').textContent = n;
    if (gasSim) { gasSim.setN(n); }
  }

  function gasTChange() {
    const raw = +document.getElementById('sl-gT').value;
    const t = raw / 10;
    document.getElementById('g-T').textContent = t.toFixed(1) + ' у.е.';
    if (gasSim) gasSim.setT(t);
  }

  function gasPistonChange() {
    const v = +document.getElementById('sl-gPiston').value;
    document.getElementById('g-piston').textContent = v + '%';
    if (gasSim) gasSim.setPiston(v / 100);
  }

  function gasToggleVectors(btn) {
    if (!gasSim) return;
    gasSim.toggleVectors();
    btn.textContent = 'Векторы скоростей: ' + (gasSim._showVectors ? 'Вкл' : 'Выкл');
    btn.style.color = gasSim._showVectors ? '#7BF5A4' : '';
  }

  function _gasUpdateUI(info) {
    document.getElementById('gstat-P').textContent  = info.P;
    document.getElementById('gstat-V').textContent  = info.V;
    document.getElementById('gstat-PV').textContent = info.PV;
    document.getElementById('gstat-v').textContent  = info.avgSpeed + ' у.е.';
    document.getElementById('mpbar-l1').textContent = 'N';
    document.getElementById('mpbar-v1').textContent = info.N;
    document.getElementById('mpbar-l2').textContent = 'T';
    document.getElementById('mpbar-v2').textContent = info.T.toFixed(1);
    document.getElementById('mpbar-l3').textContent = 'P';
    document.getElementById('mpbar-v3').textContent = info.P;
    document.getElementById('mpbar-l4').textContent = 'V';
    document.getElementById('mpbar-v4').textContent = info.V;
    document.getElementById('mpbar-l5').textContent = 'PV';
    document.getElementById('mpbar-v5').textContent = info.PV;
  }

  function brownNChange() {
    const n = +document.getElementById('sl-brN').value;
    document.getElementById('br-N').textContent = n;
    if (brownSim) brownSim.setN(n);
  }

  function brownTChange() {
    const t = +document.getElementById('sl-brT').value / 10;
    document.getElementById('br-T').textContent = t.toFixed(1) + ' у.е.';
    if (brownSim) brownSim.setT(t);
  }

  function _brownUpdateUI(info) {
    document.getElementById('brstat-dr').textContent    = info.displacement + ' px';
    document.getElementById('brstat-msd').textContent   = info.msd + ' px²';
    document.getElementById('brstat-v').textContent     = info.speed;
    document.getElementById('brstat-steps').textContent = info.steps;
    document.getElementById('mpbar-l1').textContent = 'Шагов';
    document.getElementById('mpbar-v1').textContent = info.steps;
    document.getElementById('mpbar-l2').textContent = '|Δr|';
    document.getElementById('mpbar-v2').textContent = info.displacement + ' px';
    document.getElementById('mpbar-l3').textContent = 'MSD';
    document.getElementById('mpbar-v3').textContent = info.msd + ' px²';
    document.getElementById('mpbar-l4').textContent = 'v';
    document.getElementById('mpbar-v4').textContent = info.speed;
    document.getElementById('mpbar-l5').textContent = 'N';
    document.getElementById('mpbar-v5').textContent = info.N;
  }

  function statesTChange() {
    const raw = +document.getElementById('sl-stT').value;
    const t = raw / 100;
    document.getElementById('st-T').textContent = t.toFixed(2);
    if (statesSim) statesSim.setT(t);
  }

  function statesPreset(t) {
    document.getElementById('sl-stT').value = Math.round(t * 100);
    document.getElementById('st-T').textContent = t.toFixed(2);
    if (window.LabFX) {
      const stateIdx = t < 0.2 ? 0 : t < 0.5 ? 1 : 2;
      LabFX.sound.play('whoosh', { pitch: [0.7, 1.0, 1.3][stateIdx], volume: 0.3 });
    }
    if (statesSim) statesSim.setT(t);
  }

  function statesNChange() {
    const n = +document.getElementById('sl-stN').value;
    document.getElementById('st-N').textContent = n;
    if (statesSim) statesSim.setN(n);
  }

  function statesToggleVectors(btn) {
    if (!statesSim) return;
    statesSim.toggleVectors();
    btn.textContent = 'Векторы скоростей: ' + (statesSim._showVectors ? 'Вкл' : 'Выкл');
    btn.style.color = statesSim._showVectors ? '#7BF5A4' : '';
  }

  function _statesUpdateUI(info) {
    const phaseColors = { solid: '#4CC9F0', liquid: '#7BF5A4', gas: '#EF476F' };
    const phaseLabels = { solid: 'Твёрдое', liquid: 'Жидкость', gas: 'Газ' };
    const c = phaseColors[info.phase] || '#fff';
    document.getElementById('ststat-phase').textContent = phaseLabels[info.phase] || info.phase;
    document.getElementById('ststat-phase').style.color = c;
    document.getElementById('ststat-KE').textContent = info.avgKE;
    document.getElementById('ststat-PE').textContent = info.avgPE;
    const pEl = document.getElementById('ststat-P');
    if (pEl) pEl.textContent = info.P !== undefined ? info.P : '—';
    document.getElementById('mpbar-l1').textContent = 'Фаза';
    document.getElementById('mpbar-v1').textContent = phaseLabels[info.phase] || info.phase;
    document.getElementById('mpbar-v1').style.color = c;
    document.getElementById('mpbar-l2').textContent = 'T';
    document.getElementById('mpbar-v2').textContent = info.T.toFixed(2);
    document.getElementById('mpbar-l3').textContent = 'KE';
    document.getElementById('mpbar-v3').textContent = info.avgKE;
    document.getElementById('mpbar-l4').textContent = 'PE';
    document.getElementById('mpbar-v4').textContent = info.avgPE;
    document.getElementById('mpbar-l5').textContent = 'P';
    document.getElementById('mpbar-v5').textContent = info.P !== undefined ? info.P : '—';
  }

  function diffNChange() {
    const n = +document.getElementById('sl-dfN').value;
    document.getElementById('df-N').textContent = n;
    if (diffSim) diffSim.setN(n);
  }

  function diffTChange() {
    const t = +document.getElementById('sl-dfT').value / 10;
    document.getElementById('df-T').textContent = t.toFixed(1) + ' у.е.';
    if (diffSim) diffSim.setT(t);
  }

  function diffPartitionToggle(rowEl) {
    if (!diffSim) return;
    diffSim.togglePartition();
    const on = diffSim.partitionOn;
    rowEl.classList.toggle('active', on);
    document.getElementById('diffusion-part-btn').innerHTML = on ? '‖ Раздел' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Раздел снят';
  }

  function diffPartitionBtn() {
    if (!diffSim) return;
    const on = diffSim.partitionOn;
    document.getElementById('diffusion-part-btn').innerHTML = on ? '‖ Раздел' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Раздел снят';
    document.getElementById('df-part-row').classList.toggle('active', on);
  }

  function diffPoreToggle(rowEl) {
    if (!diffSim) return;
    diffSim.togglePore();
    const pore = diffSim._poreMode;
    const on   = diffSim.partitionOn;
    rowEl.classList.toggle('active', pore);
    const tog = document.getElementById('df-pore-toggle');
    if (tog) tog.style.background = pore ? '#FFB347' : 'rgba(255,255,255,0.15)';
    const span = tog && tog.querySelector('span');
    if (span) span.style.marginLeft = pore ? '14px' : '2px';
    // Also sync partition row
    document.getElementById('df-part-row').classList.toggle('active', on);
  }

  function _diffUpdateUI(info) {
    document.getElementById('dfstat-LA').textContent  = info.leftA;
    document.getElementById('dfstat-LB').textContent  = info.leftB;
    document.getElementById('dfstat-RA').textContent  = info.rightA;
    document.getElementById('dfstat-RB').textContent  = info.rightB;
    document.getElementById('dfstat-mix').textContent = info.mixed + '%';
    document.getElementById('mpbar-l1').textContent = 'Смешивание';
    document.getElementById('mpbar-v1').textContent = info.mixed + '%';
    document.getElementById('mpbar-l2').textContent = 'Лево A/B';
    document.getElementById('mpbar-v2').textContent = info.leftA + '/' + info.leftB;
    document.getElementById('mpbar-l3').textContent = 'Право A/B';
    document.getElementById('mpbar-v3').textContent = info.rightA + '/' + info.rightB;
    document.getElementById('mpbar-l4').textContent = 'Раздел';
    const partLabel = !info.partitionOn ? 'снят' : info.poreMode ? 'пора' : 'вкл';
    document.getElementById('mpbar-v4').textContent = partLabel;
    document.getElementById('mpbar-v4').style.color = !info.partitionOn ? '#34d399' : info.poreMode ? '#FFB347' : '#fff';
    document.getElementById('mpbar-l5').textContent = 'Шагов';
    document.getElementById('mpbar-v5').textContent = info.steps;
  }

  /* ════════════════════════════════
     ЗАКОН КУЛОНА
  ════════════════════════════════ */

