'use strict';
/* ══════════════════════════════════════════════════════════════
   IsoprocessSim — PV-diagram for 4 ideal-gas isoprocesses
   n = 1, R = 0.0821 L·atm/mol·K; energies in Joules
   Isothermal  PV = const          ΔU=0, W=nRT·ln(V2/V1), Q=W
   Isochoric   V  = const          W=0,  ΔU=νCvΔT,        Q=ΔU
   Isobaric    P  = const          W=PΔV, ΔU=νCvΔT,       Q=ΔU+W
   Adiabatic   PV^γ = const        Q=0,  ΔU=-W,            W=PΔV/(γ-1)
   ══════════════════════════════════════════════════════════════ */

class IsoprocessSim {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');
    this.W = 0; this.H = 0;

    /* physics */
    this.n     = 1;
    this.R     = 0.0821;  // L·atm / mol·K
    this.R_J   = 8.314;   // J / mol·K
    this.gamma = 1.4;     // 7/5 diatomic default

    /* state */
    this.P1 = 3.0;     // atm
    this.V1 = 10.0;    // L
    this._ratio = 0.5; // 0..1, maps end state position along process

    /* process */
    this.process = 'isothermal';

    /* axis range */
    this.Vmin = 1; this.Vmax = 33;
    this.Pmin = 0.2; this.Pmax = 9.5;

    /* margins */
    this.ML = 52; this.MB = 46; this.MT = 20; this.MR = 18;

    this._drag = null; // 'state1' | 'state2'
    this.onUpdate = null;

    this._bindEvents();
    new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement);
  }

  /* ── public API ─────────────────────────────── */

  fit() {
    const dpr = window.devicePixelRatio || 1;
    const w = this.canvas.offsetWidth || 600;
    const h = this.canvas.offsetHeight || 400;
    this.canvas.width  = w * dpr;
    this.canvas.height = h * dpr;
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    this.W = w; this.H = h;
  }

  setProcess(p) {
    this.process = p;
    if (window.LabFX) LabFX.sound.play('click');
    this._dustFrame = 0;
    this.draw();
    this._emit();
  }
  setGamma(g)   { this.gamma = +g; this.draw(); this._emit(); }
  getParams() { return { P1: this.P1, V1: this.V1, process: this.process }; }
  setParams({ P1, V1 } = {}) {
    if (P1 !== undefined) this.P1 = Math.max(0.4, Math.min(8, +P1));
    if (V1 !== undefined) this.V1 = Math.max(2, Math.min(28, +V1));
    this.draw(); this._emit();
  }
  setRatio(r) { this._ratio = Math.max(0.01, Math.min(0.99, +r)); this.draw(); this._emit(); }

  /* ── coordinate transforms ─────────────────── */

  _pw() { return this.W - this.ML - this.MR; }
  _ph() { return this.H - this.MT - this.MB; }

  _vx(v) { return this.ML + (v - this.Vmin) / (this.Vmax - this.Vmin) * this._pw(); }
  _py(p) { return this.MT + (1 - (p - this.Pmin) / (this.Pmax - this.Pmin)) * this._ph(); }
  _xv(x) { return this.Vmin + (x - this.ML) / this._pw() * (this.Vmax - this.Vmin); }
  _yp(y) { return this.Pmin + (1 - (y - this.MT) / this._ph()) * (this.Pmax - this.Pmin); }

  /* ── physics ───────────────────────────────── */

  _T(P, V) { return P * V / (this.n * this.R); }

  _state2() {
    const { P1, V1, _ratio, gamma } = this;
    /* ratio in [0..1] → multiplier in [0.2..3.5] for V2/V1 or P2/P1 */
    const mult = 0.2 + _ratio * 3.3;
    const clampV = v => Math.max(this.Vmin + 0.5, Math.min(this.Vmax - 0.5, v));
    const clampP = p => Math.max(this.Pmin + 0.05, Math.min(this.Pmax - 0.1, p));

    switch (this.process) {
      case 'isothermal': {
        const V2 = clampV(V1 * mult);
        return { P2: clampP(P1 * V1 / V2), V2 };
      }
      case 'isochoric': {
        return { P2: clampP(P1 * mult), V2: V1 };
      }
      case 'isobaric': {
        const V2 = clampV(V1 * mult);
        return { P2: P1, V2 };
      }
      case 'adiabatic': {
        const V2 = clampV(V1 * mult);
        return { P2: clampP(P1 * Math.pow(V1 / V2, gamma)), V2 };
      }
    }
    return { P2: P1, V2: V1 };
  }

  info() {
    const { P1, V1, n, R_J, gamma } = this;
    const T1 = this._T(P1, V1);
    const { P2, V2 } = this._state2();
    const T2 = this._T(P2, V2);

    /* internal energy: ΔU = νCvΔT,  Cv = R/(γ-1) */
    const Cv_J = R_J / (gamma - 1);
    const dU_J = n * Cv_J * (T2 - T1);

    /* P in Pa = P_atm * 101325,  V in m³ = V_L * 0.001 */
    const P1Pa = P1 * 101325, P2Pa = P2 * 101325;
    const V1m3 = V1 * 0.001,  V2m3 = V2 * 0.001;

    let W_J = 0, Q_J = 0;
    switch (this.process) {
      case 'isothermal':
        W_J = n * R_J * T1 * Math.log(V2 / V1);
        Q_J = W_J; break;
      case 'isochoric':
        W_J = 0; Q_J = dU_J; break;
      case 'isobaric':
        W_J = P1Pa * (V2m3 - V1m3);
        Q_J = dU_J + W_J; break;
      case 'adiabatic':
        Q_J = 0;
        W_J = -dU_J; break;
    }

    const fmt = x => (x >= 0 ? '+' : '') + Math.round(x);
    return {
      P1: P1.toFixed(2), V1: V1.toFixed(1), T1: Math.round(T1),
      P2: P2.toFixed(2), V2: V2.toFixed(1), T2: Math.round(T2),
      W: fmt(W_J), Q: fmt(Q_J), dU: fmt(Math.round(dU_J)),
      W_raw: W_J, Q_raw: Q_J, dU_raw: dU_J,
      process: this.process,
    };
  }

  _emit() { if (this.onUpdate) this.onUpdate(this.info()); }

  /* ── draw ──────────────────────────────────── */

  draw() {
    const { ctx, W, H } = this;
    if (!W || !H) return;
    ctx.fillStyle = '#0D0D1A';
    ctx.fillRect(0, 0, W, H);
    this._drawGrid(ctx);
    this._drawBgCurves(ctx);
    this._drawActiveCurve(ctx);
    this._drawPoints(ctx);
    this._drawInfoBox(ctx);
    /* emit dust particle along PV curve every ~5 draw calls */
    if (window.LabFX) {
      this._dustFrame = (this._dustFrame || 0) + 1;
      if (this._dustFrame % 5 === 0) {
        const { P2, V2 } = this._state2();
        /* interpolated point at 50% of curve for dust */
        const u = ((this._dustFrame / 5) % 10) / 10;
        let px, py;
        switch (this.process) {
          case 'isothermal': { const v = this.V1 + u * (V2 - this.V1); px = this._vx(v); py = this._py(this.P1 * this.V1 / v); break; }
          case 'isochoric':  { px = this._vx(this.V1); py = this._py(this.P1 + u * (P2 - this.P1)); break; }
          case 'isobaric':   { const v2 = this.V1 + u * (V2 - this.V1); px = this._vx(v2); py = this._py(this.P1); break; }
          case 'adiabatic':  { const v3 = this.V1 + u * (V2 - this.V1); px = this._vx(v3); py = this._py(this.P1 * Math.pow(this.V1 / v3, this.gamma)); break; }
          default: px = this._vx(this.V1); py = this._py(this.P1);
        }
        LabFX.particles.emit({
          ctx, x: px, y: py,
          count: 1, color: '#06D6E0', speed: 8,
          spread: Math.PI * 2, angle: 0, gravity: 0,
          life: 300, shape: 'dust', size: 2,
        });
      }
      LabFX.particles.draw(ctx);
    }
  }

  _drawGrid(ctx) {
    const { ML, MT, MR, MB } = this;
    const pw = this._pw(), ph = this._ph();

    /* plot background */
    ctx.fillStyle = 'rgba(255,255,255,0.018)';
    ctx.fillRect(ML, MT, pw, ph);

    /* grid */
    ctx.strokeStyle = 'rgba(255,255,255,0.055)';
    ctx.lineWidth = 1; ctx.setLineDash([]);
    for (let v = 5; v <= 30; v += 5) {
      const x = this._vx(v);
      ctx.beginPath(); ctx.moveTo(x, MT); ctx.lineTo(x, MT + ph); ctx.stroke();
    }
    for (let p = 1; p <= 9; p++) {
      const y = this._py(p);
      ctx.beginPath(); ctx.moveTo(ML, y); ctx.lineTo(ML + pw, y); ctx.stroke();
    }

    /* axes */
    ctx.strokeStyle = 'rgba(255,255,255,0.3)';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(ML, MT); ctx.lineTo(ML, MT + ph); ctx.lineTo(ML + pw, MT + ph);
    ctx.stroke();

    /* tick labels */
    ctx.font = '11px Manrope, system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.45)';
    ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
    for (let p = 1; p <= 9; p++) {
      const y = this._py(p);
      if (y < MT + 2 || y > MT + ph - 2) continue;
      ctx.fillText(p, ML - 6, y);
    }
    ctx.textAlign = 'center'; ctx.textBaseline = 'top';
    for (let v = 5; v <= 30; v += 5) {
      const x = this._vx(v);
      ctx.fillText(v, x, MT + ph + 5);
    }

    /* axis titles */
    ctx.fillStyle = 'rgba(255,255,255,0.55)';
    ctx.font = '12px Manrope, system-ui, sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('V, л', ML + pw / 2, MT + ph + 32);
    ctx.save();
    ctx.translate(13, MT + ph / 2);
    ctx.rotate(-Math.PI / 2);
    ctx.fillText('P, атм', 0, 0);
    ctx.restore();
  }

  _COLORS = {
    isothermal: '#EF476F',
    isochoric:  '#06D6E0',
    isobaric:   '#7BF5A4',
    adiabatic:  '#FFD166',
  };

  /* draw one process curve through (P1,V1) */
  _curve(ctx, process, alpha, lw, dashed) {
    const { P1, V1, gamma, Vmin, Vmax, Pmin, Pmax } = this;
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.strokeStyle = this._COLORS[process];
    ctx.lineWidth = lw;
    ctx.setLineDash(dashed ? [5, 4] : []);
    ctx.beginPath();

    if (process === 'isochoric') {
      const x = this._vx(V1);
      ctx.moveTo(x, this._py(Pmax));
      ctx.lineTo(x, this._py(Pmin));
    } else {
      let started = false;
      const steps = 300;
      for (let i = 0; i <= steps; i++) {
        const v = Vmin + (Vmax - Vmin) * i / steps;
        let p;
        if (process === 'isothermal') p = P1 * V1 / v;
        else if (process === 'isobaric') p = P1;
        else p = P1 * Math.pow(V1 / v, gamma); // adiabatic
        if (p < Pmin || p > Pmax + 0.1) { started = false; continue; }
        const x = this._vx(v), y = this._py(Math.min(p, Pmax));
        if (!started) { ctx.moveTo(x, y); started = true; }
        else ctx.lineTo(x, y);
      }
    }
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.restore();
  }

  _drawBgCurves(ctx) {
    for (const p of ['isothermal', 'isochoric', 'isobaric', 'adiabatic']) {
      if (p !== this.process) this._curve(ctx, p, 0.14, 1.2, true);
    }
    /* legend dots */
    const names = { isothermal: 'Изотерма', isochoric: 'Изохора', isobaric: 'Изобара', adiabatic: 'Адиабата' };
    ctx.font = '10px Manrope, system-ui, sans-serif';
    let lx = this.ML + this._pw() - 8, ly = this.MT + 8;
    ctx.textAlign = 'right'; ctx.textBaseline = 'top';
    for (const [proc, label] of Object.entries(names)) {
      const col = this._COLORS[proc];
      const isCur = proc === this.process;
      ctx.globalAlpha = isCur ? 0.85 : 0.3;
      ctx.fillStyle = col;
      ctx.beginPath(); ctx.arc(lx + 5, ly + 4, 4, 0, Math.PI * 2); ctx.fill();
      ctx.fillText(label, lx - 3, ly);
      ly += 16;
    }
    ctx.globalAlpha = 1;
  }

  _drawActiveCurve(ctx) {
    /* full curve dimmed */
    this._curve(ctx, this.process, 0.3, 1.5, false);

    /* highlighted segment state1 → state2 */
    const { P1, V1, gamma } = this;
    const { P2, V2 } = this._state2();
    const color = this._COLORS[this.process];

    ctx.save();
    ctx.strokeStyle = color;
    ctx.lineWidth = 2.8;
    ctx.setLineDash([]);

    const steps = 200;
    const [Vs, Ve] = V2 >= V1 ? [V1, V2] : [V2, V1];

    if (this.process === 'isochoric') {
      const x = this._vx(V1);
      const y1c = this._py(P1), y2c = this._py(P2);
      ctx.beginPath(); ctx.moveTo(x, y1c); ctx.lineTo(x, y2c); ctx.stroke();
      this._arrowHead(ctx, x, y1c, x, y2c, color);
    } else {
      ctx.beginPath();
      let started = false;
      for (let i = 0; i <= steps; i++) {
        const v = Vs + (Ve - Vs) * i / steps;
        let p;
        if (this.process === 'isothermal') p = P1 * V1 / v;
        else if (this.process === 'isobaric') p = P1;
        else p = P1 * Math.pow(V1 / v, gamma);
        const x = this._vx(v), y = this._py(p);
        if (!started) { ctx.moveTo(x, y); started = true; }
        else ctx.lineTo(x, y);
      }
      ctx.stroke();

      /* arrow at ~80% of segment */
      const vArr = Vs + (Ve - Vs) * 0.8;
      const vArr2 = Vs + (Ve - Vs) * 0.82;
      let p1a, p2a;
      if (this.process === 'isothermal') { p1a = P1*V1/vArr; p2a = P1*V1/vArr2; }
      else if (this.process === 'isobaric') { p1a = P1; p2a = P1; }
      else { p1a = P1*Math.pow(V1/vArr,gamma); p2a = P1*Math.pow(V1/vArr2,gamma); }
      /* ensure arrow points from 1→2 */
      const dir = V2 > V1 ? 1 : -1;
      this._arrowHead(ctx,
        this._vx(vArr + dir*0), this._py(p1a + dir*0),
        this._vx(vArr2 + dir*0), this._py(p2a + dir*0), color);
    }
    ctx.restore();
  }

  _arrowHead(ctx, x1, y1, x2, y2, color) {
    const angle = Math.atan2(y2 - y1, x2 - x1);
    const s = 10;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - s * Math.cos(angle - 0.4), y2 - s * Math.sin(angle - 0.4));
    ctx.lineTo(x2 - s * Math.cos(angle + 0.4), y2 - s * Math.sin(angle + 0.4));
    ctx.closePath(); ctx.fill();
  }

  _drawPoints(ctx) {
    const { P2, V2 } = this._state2();
    const color = this._COLORS[this.process];

    const dot = (x, y, fill, label, textX, textY) => {
      ctx.fillStyle = fill;
      ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.fill();
      ctx.strokeStyle = 'rgba(255,255,255,0.55)'; ctx.lineWidth = 1.5;
      ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.stroke();
      ctx.font = 'bold 11px Manrope, system-ui, sans-serif';
      ctx.fillStyle = fill; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
      ctx.fillText(label, textX, textY);
    };

    const x1 = this._vx(this.V1), y1 = this._py(this.P1);
    const x2 = this._vx(V2),     y2 = this._py(P2);
    dot(x1, y1, '#9B5DE5', '1', x1 - 12, y1 - 4);
    dot(x2, y2, color,     '2', x2 + 12, y2 - 4);
  }

  _drawInfoBox(ctx) {
    const info = this.info();
    const color = this._COLORS[info.process];
    const names = { isothermal:'Изотермический', isochoric:'Изохорный', isobaric:'Изобарный', adiabatic:'Адиабатический' };
    const formulas = { isothermal:'PV = const', isochoric:'V = const', isobaric:'P = const', adiabatic:'PV^γ = const' };

    const bx = this.ML + 6, by = this.MT + 6;
    const boxW = 205, boxH = 98;

    ctx.fillStyle = 'rgba(13,13,26,0.9)';
    ctx.beginPath(); ctx.roundRect(bx, by, boxW, boxH, 8); ctx.fill();

    ctx.font = 'bold 11px Manrope, system-ui, sans-serif';
    ctx.fillStyle = color; ctx.textAlign = 'left'; ctx.textBaseline = 'top';
    ctx.fillText(`${names[info.process]}  ${formulas[info.process]}`, bx + 10, by + 8);

    ctx.font = '11px Manrope, system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.6)';
    ctx.fillText(`T₁ = ${info.T1} K   →   T₂ = ${info.T2} K`, bx + 10, by + 28);

    const wColor = info.W_raw > 0 ? '#7BF5A4' : info.W_raw < 0 ? '#EF476F' : 'rgba(255,255,255,0.4)';
    const qColor = info.Q_raw > 0 ? '#FFD166' : info.Q_raw < 0 ? '#06D6E0' : 'rgba(255,255,255,0.4)';

    ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fillText('W =', bx + 10, by + 48);
    ctx.fillStyle = wColor; ctx.fillText(`${info.W} Дж`, bx + 38, by + 48);

    ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fillText('Q =', bx + 10, by + 65);
    ctx.fillStyle = qColor; ctx.fillText(`${info.Q} Дж`, bx + 38, by + 65);

    ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fillText('ΔU =', bx + 10, by + 82);
    ctx.fillStyle = 'rgba(255,255,255,0.65)'; ctx.fillText(`${info.dU} Дж`, bx + 40, by + 82);
  }

  /* ── events ─────────────────────────────────── */

  _bindEvents() {
    const cv = this.canvas;

    const pos = e => {
      const r = cv.getBoundingClientRect();
      const t = e.touches ? e.touches[0] : e;
      return {
        px: (t.clientX - r.left) * (this.W / r.width),
        py: (t.clientY - r.top)  * (this.H / r.height),
      };
    };

    const hit = (px, py) => {
      const x1 = this._vx(this.V1), y1 = this._py(this.P1);
      if (Math.hypot(px - x1, py - y1) < 18) return 'state1';
      const { P2, V2 } = this._state2();
      const x2 = this._vx(V2), y2 = this._py(P2);
      if (Math.hypot(px - x2, py - y2) < 18) return 'state2';
      return null;
    };

    const clampV = v => Math.max(this.Vmin + 0.5, Math.min(this.Vmax - 0.5, v));
    const clampP = p => Math.max(this.Pmin + 0.1, Math.min(this.Pmax - 0.1, p));

    const onDown = e => { const { px, py } = pos(e); this._drag = hit(px, py); };

    const onMove = e => {
      if (!this._drag) return;
      if (e.cancelable) e.preventDefault();
      const { px, py } = pos(e);
      const v = this._xv(px), p = this._yp(py);

      if (this._drag === 'state1') {
        this.V1 = clampV(v); this.P1 = clampP(p);
      } else {
        /* constrain state2 to current process curve */
        switch (this.process) {
          case 'isothermal': case 'isobaric': case 'adiabatic': {
            const V2 = clampV(v);
            this._ratio = Math.max(0.01, Math.min(0.99, (V2 / this.V1 - 0.2) / 3.3));
            break;
          }
          case 'isochoric': {
            const P2 = clampP(p);
            this._ratio = Math.max(0.01, Math.min(0.99, (P2 / this.P1 - 0.2) / 3.3));
            break;
          }
        }
      }
      this.draw(); this._emit();
    };

    const onUp = () => { this._drag = null; };

    cv.addEventListener('mousedown', onDown);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    cv.addEventListener('touchstart', e => { if (e.touches.length === 1) onDown(e); }, { passive: true });
    cv.addEventListener('touchmove',  e => onMove(e), { passive: false });
    cv.addEventListener('touchend',   onUp);
    cv.addEventListener('mousemove',  e => {
      if (this._drag) { cv.style.cursor = 'grabbing'; return; }
      const { px, py } = pos(e);
      cv.style.cursor = hit(px, py) ? 'grab' : 'default';
    });
  }
}

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

  function _openIsoprocess() {
    document.getElementById('sim-topbar-title').textContent = 'Изопроцессы';
    _simShow('sim-isoprocess');
    _registerSimState('isoprocess', () => isoSim?.getParams(),
      st => { if (isoSim) { isoSim.setParams(st); if (st.process) isoSim.setProcess(st.process); } });
    if (_embedMode) _startStateEmit('isoprocess');
    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (!isoSim) {
        isoSim = new IsoprocessSim(document.getElementById('isoprocess-canvas'));
        isoSim.onUpdate = _isoUpdateUI;
        isoSim.setGamma(1.667);
      }
      isoSim.fit();
      isoSim.draw();
      isoSim._emit();
    }));
  }

  function isoProc(proc, el) {
    document.querySelectorAll('.iso-proc-btn').forEach(b => b.classList.remove('active'));
    if (el) el.classList.add('active');
    if (isoSim) isoSim.setProcess(proc);
    /* sound already emitted inside setProcess */
  }

  function isoGamma(g, el) {
    document.querySelectorAll('.iso-gamma-btn').forEach(b => b.classList.remove('active'));
    if (el) el.classList.add('active');
    if (isoSim) isoSim.setGamma(g);
  }

  function isoParam(name, val) {
    const v = parseFloat(val);
    if (name === 'P1') { document.getElementById('iso-p1-val').textContent = v.toFixed(1); if (isoSim) isoSim.setParams({ P1: v }); }
    if (name === 'V1') { document.getElementById('iso-v1-val').textContent = v;             if (isoSim) isoSim.setParams({ V1: v }); }
  }

  function isoRatio(val) { if (isoSim) isoSim.setRatio(parseFloat(val)); }

  function isoPreset(name) {
    const P = {
      iso_expand: { proc:'isothermal', P1:4,   V1:8,  ratio:0.75, gamma:1.4 },
      iso_comp:   { proc:'isothermal', P1:1.5, V1:20, ratio:0.25, gamma:1.4 },
      heat_iso:   { proc:'isochoric',  P1:2,   V1:10, ratio:0.72, gamma:1.667 },
      adiab_exp:  { proc:'adiabatic',  P1:5,   V1:6,  ratio:0.7,  gamma:1.667 },
    };
    const p = P[name]; if (!p) return;
    document.querySelectorAll('.iso-proc-btn').forEach(b => b.classList.remove('active'));
    const pb = document.getElementById(`iproc-${p.proc}`); if (pb) pb.classList.add('active');
    document.querySelectorAll('.iso-gamma-btn').forEach(b => b.classList.remove('active'));
    const gb = document.getElementById(p.gamma === 1.4 ? 'igamma-14' : 'igamma-167'); if (gb) gb.classList.add('active');
    document.getElementById('sl-iso-p1').value   = p.P1;    document.getElementById('iso-p1-val').textContent   = p.P1.toFixed(1);
    document.getElementById('sl-iso-v1').value   = p.V1;    document.getElementById('iso-v1-val').textContent   = p.V1;
    document.getElementById('sl-iso-ratio').value = p.ratio;
    if (isoSim) { isoSim.setGamma(p.gamma); isoSim.setProcess(p.proc); isoSim.setParams({ P1: p.P1, V1: p.V1 }); isoSim.setRatio(p.ratio); }
  }

  function _isoUpdateUI(info) {
    const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
    v('isobar-t1', info.T1);
    v('isobar-t2', info.T2);
    v('isobar-w',  info.W);
    v('isobar-q',  info.Q);
    v('isobar-du', info.dU);
  }

  /* ── titration ── */

