/**
 * StatesSim v4 — Aggregate States of Matter (Lennard-Jones MD)
 * Clean rewrite: stable physics, proper layout, canvas clipping, no boundary artifacts.
 */
class StatesSim {
  // ── layout / physics constants ───────────────────────────────────────────
  static PAD_B  = 112;   // px reserved at bottom for charts
  static PAD_L  = 38;    // px reserved on left for temperature bar
  static SIG    = 14;    // Lennard-Jones σ (px)
  static EPS    = 1.0;   // Lennard-Jones ε
  static DT     = 0.16;  // time step
  static CUTOFF = 3.5;   // force cutoff in σ units

  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');
    this.W = 0; this.H = 0;
    this.N = 64;
    this.T = 0.15;
    this.particles = [];

    this._raf            = null;
    this._stepCount      = 0;
    this._loop           = this._loop.bind(this);
    this._fxLastT        = 0;
    this._fxTickTimer    = 0;  // throttle temperature slider tick sound
    this._wallImpulse    = 0;
    this._pressureSmooth = 0;
    this._energyHistory  = [];
    this._rdfData        = null;
    this._rdfMaxG        = 3;
    this._rdfTick        = 0;
    this._phaseFlash     = 0;
    this._flashColor     = '#4CC9F0';
    this._prevPhase      = '';
    this._phasePulse     = 0;
    this._hover          = null;
    this._showVectors    = false;
    this.onUpdate        = null;

    canvas.addEventListener('mousemove',  e => this._onMouse(e));
    canvas.addEventListener('mouseleave', () => { this._hover = null; });
  }

  // ── public API ────────────────────────────────────────────────────────────
  fit() {
    this.W = this.canvas.offsetWidth  || 400;
    this.H = this.canvas.offsetHeight || 400;
    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 { N, T } = this;
    const { SIG, PAD_L, PAD_B } = StatesSim;
    const spacing = SIG * 1.15;
    const simW = this.W - PAD_L;
    const simH = this.H - PAD_B;
    const cols = Math.ceil(Math.sqrt(N));
    const rows = Math.ceil(N / cols);
    const gridW = (cols - 1) * spacing;
    const gridH = (rows - 1) * spacing * Math.sqrt(3) / 2;
    const ox = PAD_L + (simW - gridW) / 2;
    const oy = (simH - gridH) / 2;

    let n = 0;
    for (let r = 0; r < rows && n < N; r++) {
      const xOff = (r % 2) * spacing * 0.5;
      for (let c = 0; c < cols && n < N; c++) {
        this.particles.push({
          x:  ox + xOff + c * spacing,
          y:  oy + r * spacing * Math.sqrt(3) / 2,
          vx: (Math.random() - 0.5) * T * 3,
          vy: (Math.random() - 0.5) * T * 3,
          ax: 0, ay: 0,
        });
        n++;
      }
    }

    this._stepCount      = 0;
    this._wallImpulse    = 0;
    this._pressureSmooth = 0;
    this._energyHistory  = [];
    this._rdfData        = null;
    this._rdfMaxG        = 3;
    this._rdfTick        = 0;
    this._phaseFlash     = 0;
    this._prevPhase      = '';
    this._hover          = null;
  }

  setT(t) {
    const old = this.T;
    this.T = Math.max(0.01, t);
    if (old > 0) {
      const f = Math.min(4, Math.sqrt(this.T / old));
      for (const p of this.particles) { p.vx *= f; p.vy *= f; }
    }
    // throttled tick sound for temperature slider (pitch ∝ T)
    if (window.LabFX) {
      const nowMs = performance.now();
      if (nowMs - this._fxTickTimer > 120) {
        this._fxTickTimer = nowMs;
        const pitch = 0.7 + this.T * 0.9;
        LabFX.sound.play('tick', { pitch: Math.min(2.5, pitch), volume: 0.12 });
      }
    }
  }

  setN(n) {
    this.N = Math.max(16, Math.min(120, n));
    this.reset();
  }

  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;
    for (let i = 0; i < 5; i++) this._stepPhysics();
    if (window.LabFX) LabFX.particles.update(dt);
    this.draw();
    this._raf = requestAnimationFrame(this._loop);
  }

  _stepPhysics() {
    const { particles } = this;
    const { SIG, EPS, DT, CUTOFF, PAD_L, PAD_B } = StatesSim;
    const dt   = DT;
    const pr   = SIG * 0.48;
    const cut2 = (CUTOFF * SIG) ** 2;
    const xMin = PAD_L + pr, xMax = this.W - pr;
    const yMin = pr,         yMax = this.H - PAD_B - pr;

    // Velocity Verlet — step 1
    for (const p of particles) {
      p.vx += 0.5 * p.ax * dt; p.vy += 0.5 * p.ay * dt;
      p.x  += p.vx * dt;       p.y  += p.vy * dt;
      if      (p.x < xMin) { p.x = xMin; p.vx =  Math.abs(p.vx); this._wallImpulse += Math.abs(p.vx); }
      else if (p.x > xMax) { p.x = xMax; p.vx = -Math.abs(p.vx); this._wallImpulse += Math.abs(p.vx); }
      if      (p.y < yMin) { p.y = yMin; p.vy =  Math.abs(p.vy); this._wallImpulse += Math.abs(p.vy); }
      else if (p.y > yMax) { p.y = yMax; p.vy = -Math.abs(p.vy); this._wallImpulse += Math.abs(p.vy); }
    }

    // Lennard-Jones forces
    for (const p of particles) { p.ax = 0; p.ay = 0; }
    for (let i = 0; i < particles.length; i++) {
      for (let j = i + 1; j < particles.length; j++) {
        const pi = particles[i], pj = particles[j];
        const dx = pj.x - pi.x, dy = pj.y - pi.y;
        const r2 = dx * dx + dy * dy;
        if (r2 >= cut2 || r2 < 0.25) continue;
        const sr2 = (SIG * SIG) / r2, sr6 = sr2 * sr2 * sr2;
        const f = Math.max(-40, Math.min(40, 24 * EPS * (2 * sr6 * sr6 - sr6) / r2));
        pi.ax += f * dx; pi.ay += f * dy;
        pj.ax -= f * dx; pj.ay -= f * dy;
      }
    }

    // Velocity Verlet — step 2
    for (const p of particles) {
      p.vx += 0.5 * p.ax * dt; p.vy += 0.5 * p.ay * dt;
    }

    // Berendsen thermostat
    this._stepCount++;
    let ke2 = 0;
    for (const p of particles) ke2 += p.vx * p.vx + p.vy * p.vy;
    const ke = ke2 / (2 * particles.length);
    if (ke > 1e-8) {
      const lam = Math.max(0.92, Math.min(1.08, Math.sqrt(1 + (dt / 60) * (this.T / ke - 1))));
      for (const p of particles) { p.vx *= lam; p.vy *= lam; }
    }

    // smooth pressure
    this._pressureSmooth = this._pressureSmooth * 0.95 + this._wallImpulse * 0.05;
    this._wallImpulse = 0;

    // energy + phase history (every 8 steps)
    if (this._stepCount % 8 === 0) {
      const info = this.info();
      this._energyHistory.push({ ke: +info.avgKE, pe: +info.avgPE, te: +info.avgKE + +info.avgPE });
      if (this._energyHistory.length > 300) this._energyHistory.shift();
      const ph = info.phase;
      if (this._prevPhase && ph !== this._prevPhase) {
        const fc = { solid: '#4CC9F0', liquid: '#7BF5A4', gas: '#FFB347' };
        this._phaseFlash = 1; this._flashColor = fc[ph] || '#ffffff';
      }
      this._prevPhase = ph;
    }

    // RDF every 25 steps
    if (++this._rdfTick % 25 === 0) this._computeRDF();
    if (this._stepCount % 25 === 0 && this.onUpdate) this.onUpdate(this.info());
  }

  // ── RDF g(r) ──────────────────────────────────────────────────────────────
  _computeRDF() {
    const { particles } = this;
    const N = particles.length;
    if (N < 4) return;
    const { SIG, PAD_L, PAD_B } = StatesSim;
    const nBins = 32, maxR = 3.8 * SIG, dr = maxR / nBins;
    const hist = new Float32Array(nBins);
    for (let i = 0; i < N; i++) for (let j = i + 1; j < N; j++) {
      const r = Math.hypot(particles[j].x - particles[i].x, particles[j].y - particles[i].y);
      if (r < maxR) hist[Math.floor(r / dr)]++;
    }
    const area = (this.W - PAD_L) * (this.H - PAD_B);
    const g = new Float32Array(nBins);
    for (let i = 0; i < nBins; i++) {
      const rc = (i + 0.5) * dr;
      const ideal = N * (N - 1) * Math.PI * rc * dr / area;
      g[i] = ideal > 1e-10 ? hist[i] / ideal : 0;
    }
    if (!this._rdfData) { this._rdfData = g; }
    else { for (let i = 0; i < nBins; i++) this._rdfData[i] = this._rdfData[i] * 0.65 + g[i] * 0.35; }
    this._rdfMaxG = Math.max(1.5, ...Array.from(this._rdfData.slice(1)));
  }

  // ── info / phase ──────────────────────────────────────────────────────────
  _phase() {
    return this.T < 0.2 ? 'solid' : this.T < 0.5 ? 'liquid' : 'gas';
  }

  info() {
    const { particles, T } = this;
    const { SIG, EPS, CUTOFF, PAD_L, PAD_B } = StatesSim;
    let ke2 = 0;
    for (const p of particles) ke2 += p.vx * p.vx + p.vy * p.vy;
    const avgKE = particles.length ? 0.5 * ke2 / particles.length : 0;
    const cut2 = (CUTOFF * SIG) ** 2;
    let peTot = 0;
    for (let i = 0; i < particles.length; i++) for (let j = i + 1; j < particles.length; j++) {
      const dx = particles[j].x - particles[i].x, dy = particles[j].y - particles[i].y;
      const r2 = dx * dx + dy * dy;
      if (r2 < cut2 && r2 > 0.1) {
        const sr2 = SIG * SIG / r2, sr6 = sr2 * sr2 * sr2;
        peTot += 4 * EPS * (sr6 * sr6 - sr6);
      }
    }
    const avgPE = particles.length ? peTot / particles.length : 0;
    const perim = 2 * ((this.W - PAD_L) + (this.H - PAD_B));
    const P = this._pressureSmooth / perim * 80;
    return {
      phase:  this._phase(),
      T,
      avgKE:  avgKE.toFixed(3),
      avgPE:  avgPE.toFixed(3),
      P:      P.toFixed(1),
    };
  }

  // ── mouse ─────────────────────────────────────────────────────────────────
  _onMouse(e) {
    const r = this.canvas.getBoundingClientRect();
    const x = (e.clientX - r.left) * (this.W / r.width);
    const y = (e.clientY - r.top)  * (this.H / r.height);
    let best = null, bd = 20;
    for (const p of this.particles) {
      const d = Math.hypot(p.x - x, p.y - y);
      if (d < bd) { bd = d; best = p; }
    }
    this._hover = best;
  }

  // ── draw ──────────────────────────────────────────────────────────────────
  draw() {
    const { ctx, W, H, T } = this;
    const { SIG, PAD_B, PAD_L } = StatesSim;
    const simH  = H - PAD_B;
    const phase = this._phase();

    // full background
    ctx.fillStyle = '#08091a'; ctx.fillRect(0, 0, W, H);

    // ── clip everything to simulation area ─────────────────────────────────
    ctx.save();
    ctx.beginPath(); ctx.rect(0, 0, W, simH); ctx.clip();

    // phase flash
    if (this._phaseFlash > 0) {
      this._phaseFlash = Math.max(0, this._phaseFlash - 0.02);
      const [fr, fg, fb] = this._hex3(this._flashColor);
      ctx.fillStyle = `rgba(${fr},${fg},${fb},${this._phaseFlash * 0.16})`;
      ctx.fillRect(0, 0, W, simH);
    }

    // pressure wall glow (walls at simulation boundaries)
    const P = parseFloat(this.info().P);
    const wi = Math.min(1, P / 25);
    if (wi > 0.04) {
      const a = wi * 0.28, gd = 30;
      const walls = [
        { x: PAD_L,    y: 0,        w: gd, h: simH, d: 'r' },
        { x: W - gd,   y: 0,        w: gd, h: simH, d: 'l' },
        { x: 0,        y: 0,        w: W,  h: gd,   d: 'd' },
        { x: 0,        y: simH-gd,  w: W,  h: gd,   d: 'u' },
      ];
      for (const { x, y, w, h, d } of walls) {
        let gr;
        if      (d==='r') gr = ctx.createLinearGradient(x, 0, x+w, 0);
        else if (d==='l') gr = ctx.createLinearGradient(x+w, 0, x, 0);
        else if (d==='d') gr = ctx.createLinearGradient(0, y, 0, y+h);
        else              gr = ctx.createLinearGradient(0, y+h, 0, y);
        gr.addColorStop(0, `rgba(139,92,246,${a})`);
        gr.addColorStop(1, 'rgba(139,92,246,0)');
        ctx.fillStyle = gr; ctx.fillRect(x, y, w, h);
      }
    }

    // per-particle speeds
    const speeds = this.particles.map(p => Math.hypot(p.vx, p.vy));
    const maxSpd = Math.max(...speeds, 1e-6);

    // bonds (solid / liquid)
    const bondCut = SIG * 1.85;
    if (phase !== 'gas') {
      ctx.save();
      ctx.strokeStyle = phase === 'solid'
        ? 'rgba(96,210,250,0.45)'
        : 'rgba(120,130,255,0.22)';
      ctx.lineWidth = phase === 'solid' ? 1.2 : 0.8;
      ctx.beginPath();
      for (let i = 0; i < this.particles.length; i++) {
        for (let j = i + 1; j < this.particles.length; j++) {
          const pi = this.particles[i], pj = this.particles[j];
          if (Math.hypot(pj.x - pi.x, pj.y - pi.y) < bondCut) {
            ctx.moveTo(pi.x, pi.y); ctx.lineTo(pj.x, pj.y);
          }
        }
      }
      ctx.stroke(); ctx.restore();
    }

    // velocity vectors (optional)
    if (this._showVectors) {
      ctx.save();
      const vScale = SIG * 2 / maxSpd;
      for (let i = 0; i < this.particles.length; i++) {
        const p = this.particles[i];
        const len = speeds[i] * vScale;
        if (len < 1.5) continue;
        const ang = Math.atan2(p.vy, p.vx);
        const ex = p.x + Math.cos(ang) * len, ey = p.y + Math.sin(ang) * len;
        const hue = 240 - (speeds[i] / maxSpd) * 200;
        ctx.strokeStyle = `hsla(${hue},85%,65%,0.55)`;
        ctx.lineWidth = 1.2;
        ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(ex, ey); ctx.stroke();
        const hl = Math.min(7, len * 0.38);
        ctx.fillStyle = `hsla(${hue},85%,65%,0.55)`;
        ctx.beginPath();
        ctx.moveTo(ex, ey);
        ctx.lineTo(ex - hl * Math.cos(ang - 0.45), ey - hl * Math.sin(ang - 0.45));
        ctx.lineTo(ex - hl * Math.cos(ang + 0.45), ey - hl * Math.sin(ang + 0.45));
        ctx.closePath(); ctx.fill();
      }
      ctx.restore();
    }

    // particles
    ctx.save();
    for (let i = 0; i < this.particles.length; i++) {
      const p   = this.particles[i];
      const t   = speeds[i] / maxSpd;
      const hue = 240 - t * 220;             // blue (cold) → green → yellow → red (hot)
      const col = `hsl(${hue},85%,62%)`;
      const isH = this._hover === p;
      const rad = isH ? SIG * 0.62 : SIG * 0.5;
      ctx.shadowBlur  = isH ? 22 : 5 + t * 12;
      ctx.shadowColor = col;
      ctx.fillStyle   = col;
      ctx.beginPath(); ctx.arc(p.x, p.y, rad, 0, Math.PI * 2); ctx.fill();
      if (isH) {
        ctx.shadowBlur = 0;
        ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 1.5;
        ctx.beginPath(); ctx.arc(p.x, p.y, rad + 4, 0, Math.PI * 2); ctx.stroke();
      }
    }
    ctx.restore();

    // phase badge
    this._phasePulse += 0.04;
    this._drawPhaseBadge(ctx, W, phase);

    // temperature bar
    this._drawTempBar(ctx, simH, T);

    ctx.restore(); // end simulation clip

    // chart separator
    ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(0, simH); ctx.lineTo(W, simH); ctx.stroke();

    // charts (outside clip)
    this._drawEnergyChart(ctx, W, H, PAD_B);
    this._drawRDFChart(ctx, W, H, PAD_B);

    // hover inspector (may extend into chart area)
    if (this._hover) this._drawInspector(ctx, this._hover, speeds, maxSpd, W, H);
    if (window.LabFX) LabFX.particles.draw(ctx);
  }

  // ── helpers ───────────────────────────────────────────────────────────────
  _hex3(hex) {
    const h = hex.replace('#', '');
    return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)];
  }

  // ── sub-drawing ───────────────────────────────────────────────────────────
  _drawPhaseBadge(ctx, W, phase) {
    const cfg = {
      solid:  { icon: '❄', label: 'Твёрдое',  color: '#4CC9F0', bg: 'rgba(76,201,240,0.12)' },
      liquid: { icon: '~', label: 'Жидкость', color: '#7BF5A4', bg: 'rgba(123,245,164,0.12)' },
      gas:    { icon: '·', label: 'Газ',       color: '#FFB347', bg: 'rgba(255,179,71,0.12)' },
    }[phase];
    const sc = 1 + 0.028 * Math.sin(this._phasePulse);
    ctx.save();
    ctx.font = 'bold 13px sans-serif';
    const text = `${cfg.icon}  ${cfg.label}`;
    const tw = ctx.measureText(text).width;
    const bw = tw + 24, bh = 27, bx = W / 2 - bw / 2, by = 10;
    ctx.translate(W/2, by+bh/2); ctx.scale(sc,sc); ctx.translate(-W/2, -(by+bh/2));
    ctx.fillStyle = cfg.bg;
    ctx.beginPath(); ctx.roundRect(bx, by, bw, bh, 7); ctx.fill();
    ctx.strokeStyle = cfg.color + '50'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.roundRect(bx, by, bw, bh, 7); ctx.stroke();
    ctx.fillStyle = cfg.color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(text, W/2, by+bh/2);
    ctx.restore();
  }

  _drawTempBar(ctx, simH, T) {
    const bx = 10, by = 50, bw = 9;
    const bh = Math.max(50, Math.min(simH - 72, 260));
    ctx.save();

    // gradient track
    const grad = ctx.createLinearGradient(0, by, 0, by + bh);
    grad.addColorStop(0,   '#EF476F');
    grad.addColorStop(0.4, '#FFD166');
    grad.addColorStop(1,   '#4CC9F0');
    ctx.fillStyle = grad;
    ctx.beginPath(); ctx.roundRect(bx, by, bw, bh, 4); ctx.fill();

    // phase transition markers
    ctx.strokeStyle = 'rgba(255,255,255,0.28)'; ctx.lineWidth = 1; ctx.setLineDash([2,3]);
    ctx.font = '7px sans-serif'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
    for (const [tv, lbl] of [[0.2,'Жидк.'],[0.5,'Газ']]) {
      const y = by + bh - (tv / 0.7) * bh;
      if (y > by + 4 && y < by + bh - 4) {
        ctx.fillStyle = 'rgba(255,255,255,0)';
        ctx.beginPath(); ctx.moveTo(bx-3, y); ctx.lineTo(bx+bw+3, y); ctx.stroke();
        ctx.fillStyle = 'rgba(255,255,255,0.32)'; ctx.fillText(lbl, bx+bw+5, y);
      }
    }
    ctx.setLineDash([]);

    // indicator
    const tNorm = Math.min(1, T / 0.7);
    const iy = by + bh - tNorm * bh;
    ctx.fillStyle = '#fff'; ctx.shadowBlur = 8; ctx.shadowColor = '#fff';
    ctx.beginPath(); ctx.arc(bx + bw/2, iy, 5, 0, Math.PI*2); ctx.fill();
    ctx.shadowBlur = 0;

    // T value
    const labelY = iy < by + 18 ? iy + 14 : iy - 14;
    ctx.fillStyle = 'rgba(255,255,255,0.85)'; ctx.font = "bold 9px 'Manrope',monospace";
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(T.toFixed(2), bx + bw/2, labelY);

    ctx.fillStyle = 'rgba(255,255,255,0.32)'; ctx.font = '9px sans-serif';
    ctx.fillText('T', bx + bw/2, by - 8);
    ctx.restore();
  }

  _drawEnergyChart(ctx, W, H, padB) {
    const hist = this._energyHistory;
    const cw = Math.min(196, Math.floor((W - 16) * 0.46));
    const ch = padB - 18;
    const cx = 8, cy = H - ch - 8;

    ctx.save();
    ctx.fillStyle = 'rgba(4,6,20,0.82)';
    ctx.beginPath(); ctx.roundRect(cx, cy, cw, ch, 6); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.roundRect(cx, cy, cw, ch, 6); ctx.stroke();

    ctx.fillStyle = 'rgba(255,255,255,0.48)'; ctx.font = "9px 'Manrope',sans-serif";
    ctx.textAlign = 'left'; ctx.textBaseline = 'top';
    ctx.fillText('Энергия / частицу', cx + 8, cy + 5);

    if (hist.length > 3) {
      const pL=8, pR=6, pT=17, pB=13;
      const pw = cw-pL-pR, ph = ch-pT-pB;
      const allV = hist.flatMap(h => [h.ke, h.pe, h.te]);
      const minV = Math.min(...allV, 0), maxV = Math.max(...allV, 0.001);
      const rng = maxV - minV || 0.001;
      const px = i => cx + pL + (i / (hist.length-1)) * pw;
      const py = v => cy + pT + ph - ((v - minV) / rng) * ph;

      // zero line
      const zy = py(0);
      if (zy > cy + pT && zy < cy + pT + ph) {
        ctx.strokeStyle = 'rgba(255,255,255,0.1)'; ctx.lineWidth = 1; ctx.setLineDash([3,3]);
        ctx.beginPath(); ctx.moveTo(cx+pL, zy); ctx.lineTo(cx+pL+pw, zy); ctx.stroke();
        ctx.setLineDash([]);
      }

      for (const [key, color, lw, dash] of [
        ['pe','#9B5DE5',1.2,false],
        ['ke','#FFD166',1.2,false],
        ['te','rgba(255,255,255,0.38)',1,true],
      ]) {
        ctx.strokeStyle = color; ctx.lineWidth = lw;
        if (dash) ctx.setLineDash([3,4]);
        ctx.beginPath();
        hist.forEach((h,i) => {
          const x = px(i), y = py(h[key]);
          i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
        });
        ctx.stroke(); ctx.setLineDash([]);
      }

      // legend
      [['#FFD166','КЕ'],['#9B5DE5','ПЭ'],['rgba(255,255,255,0.4)','Е']].forEach(([c,l],li) => {
        const lx = cx + 8 + li * 34;
        ctx.fillStyle = c; ctx.beginPath(); ctx.arc(lx, cy+ch-7, 3, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = 'rgba(255,255,255,0.48)'; ctx.font = '8px sans-serif';
        ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
        ctx.fillText(l, lx+5, cy+ch-7);
      });
    } else {
      ctx.fillStyle = 'rgba(255,255,255,0.22)'; ctx.font = "9px 'Manrope',sans-serif";
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('накапливается…', cx+cw/2, cy+ch/2);
    }
    ctx.restore();
  }

  _drawRDFChart(ctx, W, H, padB) {
    const g = this._rdfData;
    const cw = Math.min(196, Math.floor((W - 16) * 0.46));
    const ch = padB - 18;
    const cx = W - cw - 8, cy = H - ch - 8;

    ctx.save();
    ctx.fillStyle = 'rgba(4,6,20,0.82)';
    ctx.beginPath(); ctx.roundRect(cx, cy, cw, ch, 6); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.roundRect(cx, cy, cw, ch, 6); ctx.stroke();

    ctx.fillStyle = 'rgba(255,255,255,0.48)'; ctx.font = "9px 'Manrope',sans-serif";
    ctx.textAlign = 'left'; ctx.textBaseline = 'top';
    ctx.fillText('g(r) — радиальная функция', cx+8, cy+5);

    if (g) {
      const pL=8, pR=6, pT=17, pB=14;
      const pw = cw-pL-pR, ph = ch-pT-pB;
      const nBins = g.length, barW = pw/nBins, maxG = this._rdfMaxG;

      // g=1 reference
      const refY = cy+pT+ph - (1/maxG)*ph;
      ctx.strokeStyle = 'rgba(255,209,102,0.38)'; ctx.lineWidth=1; ctx.setLineDash([4,3]);
      ctx.beginPath(); ctx.moveTo(cx+pL,refY); ctx.lineTo(cx+pL+pw,refY); ctx.stroke();
      ctx.setLineDash([]);
      ctx.fillStyle = 'rgba(255,209,102,0.35)'; ctx.font='7px sans-serif';
      ctx.textAlign='right'; ctx.textBaseline='middle';
      ctx.fillText('1', cx+pL-2, refY);

      for (let i = 0; i < nBins; i++) {
        const v = Math.min(g[i], maxG), frac = v / maxG;
        const bh = frac * ph;
        const bx = cx+pL+i*barW, by = cy+pT+ph-bh;
        const hue = 220 - frac * 180;
        ctx.fillStyle = `hsla(${hue},70%,55%,0.82)`;
        ctx.beginPath(); ctx.roundRect(bx+0.5, by, barW-1, bh, 1); ctx.fill();
      }

      ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.font='7px sans-serif'; ctx.textAlign='center';
      for (let v=0; v<=3; v++) {
        ctx.fillText(v, cx+pL+(v/3.8)*pw, cy+pT+ph+8);
      }
      ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.textBaseline='bottom';
      ctx.fillText('r / σ', cx+pL+pw/2, cy+ch);
    } else {
      ctx.fillStyle = 'rgba(255,255,255,0.22)'; ctx.font = "9px 'Manrope',sans-serif";
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('накапливается…', cx+cw/2, cy+ch/2);
    }
    ctx.restore();
  }

  _drawInspector(ctx, p, speeds, maxSpd, W, H) {
    const { SIG } = StatesSim;
    const spd = Math.hypot(p.vx, p.vy);
    const ke  = 0.5 * spd * spd;
    const ang = Math.atan2(p.vy, p.vx) * 180 / Math.PI;
    let coord = 0;
    for (const q of this.particles) {
      if (q !== p && Math.hypot(q.x-p.x, q.y-p.y) < SIG*1.5) coord++;
    }
    const t = spd / maxSpd, hue = 240 - t * 220;
    const clr = `hsl(${hue},85%,62%)`;
    const rows = [
      ['|v|', spd.toFixed(3)], ['vx', p.vx.toFixed(2)], ['vy', p.vy.toFixed(2)],
      ['KE',  ke.toFixed(3)],  ['угол', ang.toFixed(1)+'°'], ['z', coord+' сос.'],
    ];
    const tw=136, th=rows.length*17+20;
    let tx = p.x+14, ty = p.y-th/2;
    if (tx+tw > W-8) tx = p.x-tw-14;
    ty = Math.max(8, Math.min(H-th-8, ty));
    ctx.save();
    ctx.fillStyle = 'rgba(5,7,22,0.95)';
    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.07)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.roundRect(tx, ty, tw, th, 8); ctx.stroke();
    ctx.font = "11px 'Manrope',monospace"; ctx.textBaseline = 'middle';
    rows.forEach(([k,v],i) => {
      const ry = ty+15+i*17;
      ctx.fillStyle='rgba(255,255,255,0.38)'; ctx.textAlign='left';  ctx.fillText(k, tx+10, ry);
      ctx.fillStyle='rgba(255,255,255,0.9)';  ctx.textAlign='right'; ctx.fillText(v, tx+tw-10, ry);
    });
    ctx.restore();
  }
}
