// particles.js — 2D hard-sphere gas (visual) + THEORETICAL kinetics model (A + B → 2Γ)
// IMPORTANT CHANGE (per Νίκος):
// - No reaction is triggered by collisions.
// - Particle motion is only a temperature-dependent visualization.
// - Amounts (nA, nB, nΓ) evolve ONLY by the theoretical rate law: v = k [A][B].
// - Stoichiometry enforced: υA = υB = 0.5 υΓ always (d[A]/dt = -v, d[B]/dt = -v, d[Γ]/dt = +2v).
// - k is constant for constant T and changes ONLY with T. Rule of thumb: +10 °C (≈ +10 K) doubles k.

(function(){


  let disturbed = false;

let manualDisturbance = false;
let hasStartedOnce = false;

const controlsToLock = [
  document.getElementById("addBtn"),
  document.getElementById("removeBtn"),
  document.getElementById("volumeSlider"),
  document.getElementById("temperatureSlider")
].filter(Boolean);




//window.addEventListener("thermochange", () => {
 // equilibriumLocked = false;
 // equilibriumReached = false;
 // eqTimer = 0;
//});




  const EQ_TOL = 1e-5;      // ανοχή |υ→ − υ←|
const EQ_HOLD_TIME = 2.0; // s συνεχόμενης ισορροπίας

  const vessel = document.getElementById("vessel");
  const piston = document.getElementById("piston");
  if(!vessel || !piston) return;

  // --- canvas overlay inside vessel
  const canvas = document.createElement("canvas");
  canvas.id = "vesselCanvas";
  canvas.style.position = "absolute";
  canvas.style.left = "0";
  canvas.style.top = "0";
  canvas.style.width = "100%";
  canvas.style.height = "100%";
  canvas.style.pointerEvents = "none";
  vessel.appendChild(canvas);
  const ctx = canvas.getContext("2d");

  function currentSpeedScale() {
  if (!particles || particles.length === 0) return null;

  let v2 = 0;
  for (const p of particles) {
    v2 += p.vx*p.vx + p.vy*p.vy;
  }
  return Math.sqrt(v2 / (2 * particles.length));
}


  function fitCanvas(){
    const dpr = window.devicePixelRatio || 1;
    const w = Math.max(50, Math.floor(vessel.clientWidth));
    const h = Math.max(50, Math.floor(vessel.clientHeight));
    canvas.width = Math.floor(w * dpr);
    canvas.height = Math.floor(h * dpr);
    ctx.setTransform(dpr,0,0,dpr,0,0);
  }
  window.addEventListener("resize", fitCanvas);
  fitCanvas();

  // --- buttons (assumes order: start, empty)
const btnAdd    = document.getElementById("addBtn");
const btnRemove = document.getElementById("removeBtn");
const btnStart  = document.getElementById("startBtn");
const btnEmpty  = document.getElementById("emptyBtn");


  // --- simulation parameters
  const MOL_TO_PARTICLES = 1000;
  const FIXED_RADIUS = 3.5; // px
let extentXi = 0;   // extent of reaction (mol)

let eqTimer = 0;
let equilibriumReached = false;
let equilibriumLocked = false;   // ⬅️ ΤΟ ΣΗΜΑΝΤΙΚΟ
// === SNAPSHOT ΘΕΩΡΗΤΙΚΟΥ ΣΤΗΝ ΙΣΟΡΡΟΠΙΑ ===
let eq_nA = null, eq_nB = null, eq_nG = null;
let eq_CA = null, eq_CB = null, eq_CG = null;
let eq_vf = null, eq_vr = null;




  const species = {
    A: { color: "#d32f2f", mass: 1.0 },
    B: { color: "#1976d2", mass: 1.0 },
    G: { color: "#388e3c", mass: 1.0 },
    I: { color: "#000000ff", mass: 1.0 }
  };

  // Temperature -> speed scale (px/s), purely visual
const BASE_SIGMA = 55; // px/s at 300 K
const VISUAL_SPEED_GAIN = 1.0;     // ⬅️ μόνο οπτικό
const VISUAL_TEMP_EXP   = 4.0;     // ⬅️ 1.0 = γραμμικό, 0.5 = sqrt

function sigmaFromT(T){
  const x = Math.max(1, T) / 300;
  return BASE_SIGMA * VISUAL_SPEED_GAIN * Math.pow(x, VISUAL_TEMP_EXP);
}


  // --- kinetics model: k(T)
  // Choose a reasonable visual baseline: at 300 K, k ≈ 1.0 L/mol·s

  // ===== equilibrium =====



  const K_REF = 0.2;
  const T_REF = 300; // K (≈ 27°C)
  function kFromT(T){
    // +10 K ~ double
    const dT = (T - T_REF);
    return K_REF * Math.pow(2, dT / 10);
  }


  function KcFromT(T){
  const T1 = 300;
  const T2 = 350;

  const isExo = window.thermoState.isExothermic;

  const K1 = isExo ? 16 : 4;   // στους 300 K
  const K2 = isExo ? 4  : 16;  // στους 350 K

  const frac = (T - T1) / (T2 - T1);
  return K1 * Math.pow(K2 / K1, frac);
}
window.KcFromT = KcFromT;

function computeQc() {
  const sp = window.simulationParams || {};
  const V = Math.max(1e-12, +(sp.V || 0.5));

  // ⛔ ΑΔΕΙΟ ΔΟΧΕΙΟ → ΔΕΝ ΥΠΑΡΧΕΙ Qc
  if (nA === 0 && nB === 0 && nG === 0) {
    return null;
  }

  // 🔒 ΣΤΗΝ ΙΣΟΡΡΟΠΙΑ
  if (equilibriumLocked && equilibriumReached) {
    return KcFromT(sp.T || 300);
  }

  // ⛔ ΧΩΡΙΣ ΑΝΤΙΔΡΩΝΤΑ
  if (nA <= 0 || nB <= 0) return null;

  const CA = nA / V;
  const CB = nB / V;
  const CG = nG / V;

  return (CG * CG) / (CA * CB);
}





window.getQc = computeQc;


  // expose kinetics for theoretical rates (no fitting)
  window.Kinetics = { kFromT, T_REF, K_REF };

  // ===== initial rate (for vt_graph label) =====
  function computeInitialRate(){
    const sp = window.simulationParams || {};
    const V = Math.max(1e-6, +(sp.V || 0.5));
    const T = +(sp.T || 300);
    const CA = nA / V;
    const CB = nB / V;
    const k = kFromT(T);
    return k * CA * CB;
  }

  function publishInitialRate(){
    if(window.VTGraph && typeof window.VTGraph.setInitialRate === 'function'){
      // if vessel is empty, clear label
      const v0 = (nA>0 && nB>0) ? computeInitialRate() : null;
      window.VTGraph.setInitialRate(v0);
    }
  }

  // --- RNG helpers

function thermoRebaseline() {
disturbed = true;
  // 1. τρέχοντα → αρχικά
  nA0 = nA;
  nB0 = nB;
  nG0 = nG;

  extentXi = 0;

  // 2. καθάρισμα ICE μεταβολών
  [
    "ice_r_1","ice_r_2","ice_r_3",
    "ice_p_1","ice_p_2","ice_p_3"
  ].forEach(id=>{
    const el = document.getElementById(id);
    if(el) el.textContent = "0.000";
  });

  // 3. τελικά = αρχικά
  ice_At.textContent = nA0.toFixed(3);
  ice_Bt.textContent = nB0.toFixed(3);
  ice_Gt.textContent = nG0.toFixed(3);

  // 4. reset ισορροπίας
  equilibriumLocked  = false;
  equilibriumReached = false;
  eqTimer = 0;

  // 5. pause χωρίς reset χρόνου
  running = false;
  lastTs = null;
  hasStartedOnce = true;

  setStartLabel();          // θα δείξει ΣΥΝΕΧΙΣΗ
  window.controlsLocked = false;
  updateControlsLock();

  // 6. ανανέωση ICE / γραφημάτων (συνεχίζουν)
  updateICEAndGraph();
  publishInitialRate();

  eq_nA = eq_nB = eq_nG = null;
eq_CA = eq_CB = eq_CG = null;
eq_vf = eq_vr = null;

}




  function rand(){ return Math.random(); }
  function randn(){
    let u = 0, v = 0;
    while(u === 0) u = rand();
    while(v === 0) v = rand();
    return Math.sqrt(-2*Math.log(u)) * Math.cos(2*Math.PI*v);
  }

function poisson(lambda){
  if (lambda <= 0) return 0;

  if (lambda < 30) {
    const L = Math.exp(-lambda);
    let k = 0, p = 1;
    do { k++; p *= Math.random(); } while (p > L);
    return k - 1;
  }

  const z = randn();
  const x = Math.round(lambda + Math.sqrt(lambda) * z);
  return Math.max(0, x);
}





  // --- boundaries (internal free space)
  function getBounds(){
    const xMax = Math.max(20, parseFloat(piston.style.left || "100"));
    const yMax = vessel.clientHeight;
    return { xMin: 0, xMax, yMin: 0, yMax };
  }

  // --- grid (spatial hash)
  let cellSize = 16;
  function key(cx, cy){ return cx + "," + cy; }

  function buildGrid(particles){
    const grid = new Map();
    for(let i=0;i<particles.length;i++){
      const p = particles[i];
      const cx = (p.x / cellSize) | 0;
      const cy = (p.y / cellSize) | 0;
      const k = key(cx,cy);
      let arr = grid.get(k);
      if(!arr){ arr = []; grid.set(k, arr); }
      arr.push(i);
    }
    return grid;
  }

  function neighborCellCoords(cx, cy){
    const coords = [];
    for(let dy=-1; dy<=1; dy++){
      for(let dx=-1; dx<=1; dx++){
        coords.push([cx+dx, cy+dy]);
      }
    }
    return coords;
  }

  // --- collision resolution (elastic)
  function resolvePair(p1, p2, r){
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    const dist2 = dx*dx + dy*dy;
    const minDist = 2*r;
    if(dist2 <= 1e-9 || dist2 >= minDist*minDist) return false;

    const dist = Math.sqrt(dist2);
    const nx = dx / dist;
    const ny = dy / dist;

    // separate overlap
    const overlap = (minDist - dist);
    const totalMass = p1.m + p2.m;
    const w1 = p2.m / totalMass;
    const w2 = p1.m / totalMass;
    p1.x -= nx * overlap * w1;
    p1.y -= ny * overlap * w1;
    p2.x += nx * overlap * w2;
    p2.y += ny * overlap * w2;

    // relative velocity along normal
    const rvx = p2.vx - p1.vx;
    const rvy = p2.vy - p1.vy;
    const relN = rvx*nx + rvy*ny;
    if(relN >= 0) return false;

    const e = 1.0;
    const j = -(1+e) * relN / (1/p1.m + 1/p2.m);

    p1.vx -= (j/p1.m) * nx;
    p1.vy -= (j/p1.m) * ny;
    p2.vx += (j/p2.m) * nx;
    p2.vy += (j/p2.m) * ny;

    // === ΟΠΤΙΚΗ "ΑΝΤΙΔΡΑΣΗ" ΣΤΗΝ ΙΣΟΡΡΟΠΙΑ ===
if (equilibriumLocked) {

  const isAB =
    (p1.type === "A" && p2.type === "B") ||
    (p1.type === "B" && p2.type === "A");

  const isGG =
    (p1.type === "G" && p2.type === "G");

// Πιθανότητα αποτελεσματικής σύγκρουσης (ΟΠΤΙΚΗ ΜΟΝΟ)
// pEff: ΟΠΤΙΚΗ πιθανότητα αποτελεσματικής σύγκρουσης
// Δεν επηρεάζει k, Kc ή τις μακροσκοπικές ταχύτητες
const pEff = (window.kineticsState?.catalyst ? 0.95 : 0.40);

if (isAB && Math.random() < pEff) {
  // A + B → G + G
  p1.type = "G";
  p2.type = "G";
  p1.m = species.G.mass;
  p2.m = species.G.mass;
 // p1.flash = p2.flash = 0.05;
}

if (isGG && Math.random() < pEff) {
  // G + G → A + B
  p1.type = "A";
  p2.type = "B";
  p1.m = species.A.mass;
  p2.m = species.B.mass;
  // p1.flash = p2.flash = 0.05;
}


}


    return true;


  }

  function syncParticlesToTheoreticalModel() {
  applyModelToParticles();   // ΕΣΥ ΤΟ ΕΧΕΙΣ ΗΔΗ
}

  // --- state (particles)
  let particles = [];
  let running = false;
  let lastTs = null;
  let simTime = 0;

  // --- state (model in moles)
  let nA = 0, nB = 0, nG = 0, nI = 0;  ;         // current (mol)
  let nA0 = 0, nB0 = 0, nG0 = 0;      // baseline (initial at t=0 for ICE)
  // ⬇️ ΠΡΟΣΘΗΚΗ ΕΔΩ
function getXi() {
  return (nG - nG0) / 2;
}

  let vesselEmpty = true;


function setModelFromSimulationParams(){
  const sp = window.simulationParams || {};
  nA = Math.max(0, +sp.nA || 0);
  nB = Math.max(0, +sp.nB || 0);
  nG = Math.max(0, +sp.nG || 0);

}


  function writeModelToSimulationParams(){
    if(!window.simulationParams) window.simulationParams = {};
    window.simulationParams.nA = nA;
    window.simulationParams.nB = nB;
    window.simulationParams.nG = nG;
  }

  function makeParticles(){
    const sp = window.simulationParams || {T:300, nA:0, nB:0, nG:0};
    const NA = Math.max(0, Math.round((sp.nA||0) * MOL_TO_PARTICLES));
    const NB = Math.max(0, Math.round((sp.nB||0) * MOL_TO_PARTICLES));
    const NG = Math.max(0, Math.round((sp.nG||0) * MOL_TO_PARTICLES));
    const NI = Math.max(0, Math.round(nI * MOL_TO_PARTICLES));

    const N = NA + NB + NG + NI;
    

    const r = FIXED_RADIUS;
    cellSize = Math.max(8, Math.floor(4*r));

    const b = getBounds();
    const margin = r + 1;

    const out = [];
    const maxAttempts = Math.max(8000, N * 40);

    function addOne(type){
      const m = species[type].mass;
      const visualSig =  currentSpeedScale() ?? sigmaFromT(sp.T || 300);

const vx = randn() * visualSig;
const vy = randn() * visualSig;


      for(let a=0; a<maxAttempts; a++){
        const x = margin + rand() * Math.max(1, (b.xMax - margin*2));
        const y = margin + rand() * Math.max(1, (b.yMax - margin*2));

        let ok = true;
        for(let j=0;j<out.length;j++){
          const q = out[j];
          const dx = x - q.x;
          const dy = y - q.y;
          if(dx*dx + dy*dy < (2*r)*(2*r)) { ok = false; break; }
        }
        if(ok){
          out.push({ type, m, x, y, vx, vy, flash: 0});
          
          return;
        }
      }
      out.push({ type, m,
        x: margin + rand()*(b.xMax-2*margin),
        y: margin + rand()*(b.yMax-2*margin),
        vx, vy
      });
    }

    for(let i=0;i<NA;i++) addOne("A");
    for(let i=0;i<NB;i++) addOne("B");
    for(let i=0;i<NG;i++) addOne("G");
    for(let i=0;i<NI;i++) addOne("I");


    particles = out;
    particles._r = r;
  }

  function setInert(enabled){
  const MOL_INERT = 0.02;

  if (enabled) {
    nI = MOL_INERT;
  } else {
    nI = 0;
  }

  makeParticles();   // απλό rebuild
}


function clearICETable(){

  [
    "ice_A0","ice_B0","ice_G0",
    "ice_r_1","ice_r_2","ice_r_3",
    "ice_p_1","ice_p_2","ice_p_3",
    "ice_At","ice_Bt","ice_Gt"
  ].forEach(id=>{
    const el = document.getElementById(id);
    if(el) el.textContent = "0.000";
  });

  const tEl = document.getElementById("ice_time");
  if(tEl) tEl.textContent = "0.0";

  const tCell = document.getElementById("ice_time_cell");
  if(tCell) tCell.innerHTML = 'Σε t = <span id="ice_time">0.0</span> s';
}




  // --- ICE + graph values come ONLY from the model
function updateICEAndGraph(){



  const sp = window.simulationParams || { V: 0.5, T: 300 };
  const V = Math.max(1e-6, +(sp.V || 0.5));

  // συγκεντρώσεις (για γραφήματα – ΟΧΙ για ICE)
  const CA = nA / V;
  const CB = nB / V;
  const CG = nG / V;

  // extent (ΠΑΡΑΓΩΓΟ)
  const xi = getXi();
  const xiSnap = snap(xi, 3);   // ⬅️ ΜΟΝΗ ΠΗΓΗ ΑΛΗΘΕΙΑΣ

  /* ================= ICE TABLE ================= */

  if (!equilibriumLocked) {

    // ICE INITIAL
    ice_A0.textContent = nA0.toFixed(3);
    ice_B0.textContent = nB0.toFixed(3);
    ice_G0.textContent = nG0.toFixed(3);

    // καθάρισμα μεταβολών
    [
      "ice_r_1","ice_r_2","ice_r_3",
      "ice_p_1","ice_p_2","ice_p_3"
    ].forEach(id=>{
      const el = document.getElementById(id);
      if (el) el.textContent = "0.000";
    });

    // μεταβολές (ΣΤΟΙΧΕΙΟΜΕΤΡΙΚΑ ΣΥΝΕΠΕΙΣ)
    if (xiSnap >= 0) {
      ice_r_1.textContent = (-xiSnap).toFixed(3);
      ice_r_2.textContent = (-xiSnap).toFixed(3);
      ice_p_3.textContent = (2 * xiSnap).toFixed(3);
    } else {
      ice_r_3.textContent = (2 * xiSnap).toFixed(3);
      ice_p_1.textContent = (-xiSnap).toFixed(3);
      ice_p_2.textContent = (-xiSnap).toFixed(3);
    }

    // ICE FINAL (ΑΝΑΚΑΤΑΣΚΕΥΗ – ΟΧΙ ΜΕΤΡΗΣΗ)
    ice_At.textContent = (nA0 - xiSnap).toFixed(3);
    ice_Bt.textContent = (nB0 - xiSnap).toFixed(3);
    ice_Gt.textContent = (nG0 + 2 * xiSnap).toFixed(3);
  }

  /* ================= C–t GRAPH ================= */

  if (running && window.CTGraph) {
    if (!equilibriumLocked) {
      window.CTGraph.addPoint(simTime, CA, CB, CG);
    } else {
      window.CTGraph.addPoint(simTime, eq_CA, eq_CB, eq_CG);
    }
  }

  if (window.Rates) {
    window.Rates.push(simTime, CA, CB, CG);
    window.Rates.maybeCompute(simTime);
  }

  /* ================= v–t GRAPH ================= */

  if (running && window.VTGraph) {

const baseK = window.Kinetics.kFromT(sp.T);
const kf = baseK * (window.kineticsState?.catalyst ? 5 : 1);
const Kc = KcFromT(sp.T);
const kr = kf / Kc;


const vForward = kf * CA * CB;
const vReverse = kr * CG * CG;


    window.VTGraph.addPoint(simTime, vForward, vReverse);
  }

  /* ================= Qc ================= */

/* ================= Qc ================= */

// ❌ Qc ΔΕΝ ορίζεται σε άδειο δοχείο
if (nA === 0 && nB === 0 && nG === 0) {
  if (window.updateQcDisplay) {
    window.updateQcDisplay(null);
  }
} else {
  const qc = computeQc();
  if (window.updateQcDisplay) {
    window.updateQcDisplay(qc);
  }
}


}


function rebaselineFromICE() {
  const xiSnap = snap(getXi(), 3);

  // 🔒 ΤΑ ΘΕΩΡΗΤΙΚΑ ΤΕΛΙΚΑ ΓΙΝΟΝΤΑΙ ΑΡΧΙΚΑ
  nA0 = snap(nA0 - xiSnap, 3);
  nB0 = snap(nB0 - xiSnap, 3);
  nG0 = snap(nG0 + 2 * xiSnap, 3);

  // 👉 συγχρονισμός μοντέλου
  nA = nA0;
  nB = nB0;
  nG = nG0;

  // 👉 νέο πρόβλημα
  extentXi = 0;

  equilibriumLocked = false;
  equilibriumReached = false;
  eqTimer = 0;

  writeModelToSimulationParams();
}




  function resetSoft({keepInventory=true}={}){
    window.controlsLocked = false;
extentXi = 0;
equilibriumReached = false;
eqTimer = 0;
updateControlsLock();

    if(keepInventory){
      // SOFT reset: keep inventory
      setModelFromSimulationParams();
      makeParticles();
    }else{
      // HARD reset: empty vessel + reset time + clear graph
      if(window.simulationParams){
        window.simulationParams.nA = 0;
        window.simulationParams.nB = 0;
        window.simulationParams.nG = 0;
      }
      setModelFromSimulationParams();
      makeParticles();

      simTime = 0;
      if(window.CTGraph && window.CTGraph.clear) window.CTGraph.clear();
      if(window.Rates && window.Rates.reset) window.Rates.reset();

      const tEl = document.getElementById("ice_time");
      if(tEl) tEl.textContent = "0.0";
    }

    lastTs = null;
    running = false;
    setStartLabel();

    if (!vesselEmpty) updateICEAndGraph();

    publishInitialRate();
  }

function stepAwayFromEquilibrium(dt, vf, vr, V){
  let vnet = vf - vr;
  let dxi = vnet * V * dt;

  if (dxi > 0) {
    dxi = Math.min(dxi, nA, nB);
  } else {
    dxi = -Math.min(Math.abs(dxi), nG / 2);
  }

  nA -= dxi;
  nB -= dxi;
  nG += 2 * dxi;

  extentXi += dxi;
  disturbed = false;
}

function stepAtEquilibrium(dt, vf, vr, V){
  const q = 1 / MOL_TO_PARTICLES;

  const nf = poisson((vf * V * dt) / q);
  const nr = poisson((vr * V * dt) / q);

  const maxF = Math.floor(Math.min(nA, nB) / q);
  const maxR = Math.floor((nG / 2) / q);

  const f = Math.min(nf, maxF);
  const r = Math.min(nr, maxR);

  nA -= f * q;  nB -= f * q;  nG += 2 * f * q;
  nA += r * q;  nB += r * q;  nG -= 2 * r * q;

  extentXi += (f - r) * q;
}


function stepModel(dt){

  if (manualDisturbance) {
  manualDisturbance = false;
}


  const sp = window.simulationParams || {};
  const V = Math.max(1e-6, +(sp.V || 0.5));
  const T = +(sp.T || 300);

  const CA = nA / V;
  const CB = nB / V;
  const CG = nG / V;

const baseK = kFromT(T);
const kf = baseK * (window.kineticsState?.catalyst ? 5 : 1);
const Kc = KcFromT(T);
const kr = kf / Kc;


  const vf = kf * CA * CB;
  const vr = kr * CG * CG;

const hasReactants = (nA > 0 && nB > 0);
const hasProducts  = (nG > 0);

const equilibriumCandidate =
  hasReactants &&
  hasProducts &&
  Math.abs(vf - vr) < EQ_TOL;



if (!equilibriumLocked) {

  if (equilibriumCandidate) {
    eqTimer += dt;

   if (!equilibriumLocked && equilibriumCandidate) {

  equilibriumLocked  = true;
  equilibriumReached = true;

  // === SNAPSHOT ΘΕΩΡΗΤΙΚΟΥ (MACRO) ===
  eq_nA = nA;
  eq_nB = nB;
  eq_nG = nG;

  eq_CA = CA;
  eq_CB = CB;
  eq_CG = CG;

  eq_vf = vf;
  eq_vr = vr;

  window.controlsLocked = false;

  lastTs = null;
  setStartLabel();
  updateControlsLock();
}



  } else {
    eqTimer = 0;
    equilibriumReached = false;
  }
}


// εξέλιξη μοντέλου
if (!equilibriumLocked || disturbed) {
  stepAwayFromEquilibrium(dt, vf, vr, V);
}
// ΣΤΗΝ ΙΣΟΡΡΟΠΙΑ: ΚΑΜΙΑ ΜΕΤΑΒΟΛΗ




  nA = Math.max(0, nA);
  nB = Math.max(0, nB);
  nG = Math.max(0, nG);

  writeModelToSimulationParams();
}



  // --- force particles types to match the model (total particles constant)
  function applyModelToParticles(){
    const targetA = Math.max(0, Math.round(nA * MOL_TO_PARTICLES));
    const targetB = Math.max(0, Math.round(nB * MOL_TO_PARTICLES));
    const targetG = Math.max(0, Math.round(nG * MOL_TO_PARTICLES));
    const targetTotal = targetA + targetB + targetG;

    const N = particles.length;
    let tA = targetA, tB = targetB, tG = targetG;
    if(targetTotal !== N){
      tG = Math.max(0, N - tA - tB);
    }

    let cA=0, cB=0, cG=0;
    for(const p of particles){
      if(p.type==='A') cA++;
      else if(p.type==='B') cB++;
      else cG++;
    }

    function pickIndexOf(type){
      const idxs = [];
      for(let i=0;i<particles.length;i++){
        if(particles[i].type === type) idxs.push(i);
      }
      if(!idxs.length) return -1;
      return idxs[(Math.random()*idxs.length)|0];
    }

    function setType(i, type){
      if(i<0) return;
      particles[i].type = type;
      particles[i].m = species[type].mass;
    }

    while(cA > tA){
      const i = pickIndexOf('A');
      setType(i, 'G'); cA--; cG++;
    }
    while(cB > tB){
      const i = pickIndexOf('B');
      setType(i, 'G'); cB--; cG++;
    }
    while(cG > tG){
      if(cA < tA){
        const i = pickIndexOf('G');
        setType(i, 'A'); cG--; cA++;
      }else if(cB < tB){
        const i = pickIndexOf('G');
        setType(i, 'B'); cG--; cB++;
      }else{
        break;
      }
    }
    while(cA < tA){
      const i = pickIndexOf('G');
      if(i<0) break;
      setType(i, 'A'); cG--; cA++;
    }
    while(cB < tB){
      const i = pickIndexOf('G');
      if(i<0) break;
      setType(i, 'B'); cG--; cB++;
    }
  }

  function kineticStats(){
    let ke = 0;
    let n = particles.length || 1;
    for(const p of particles){
      ke += 0.5*p.m*(p.vx*p.vx + p.vy*p.vy);
    }
    return { ke, kePer: ke/n };
  }

  function stepMicro(dt){
    const b = getBounds();
    const r = particles._r || 3;

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

      if(p.x < r){ p.x = r; p.vx = -p.vx; }
      if(p.x > b.xMax - r){ p.x = b.xMax - r; p.vx = -p.vx; }
      if(p.y < r){ p.y = r; p.vy = -p.vy; }
      if(p.y > b.yMax - r){ p.y = b.yMax - r; p.vy = -p.vy; }
    }

    const grid = buildGrid(particles);
    for(const [k, idxs] of grid.entries()){
      const [cx, cy] = k.split(",").map(Number);
      const neigh = neighborCellCoords(cx, cy);
      for(const i of idxs){
        const p1 = particles[i];
        for(const [nx, ny] of neigh){
          const arr = grid.get(key(nx, ny));
          if(!arr) continue;
          for(const j of arr){
            if(j <= i) continue;
            resolvePair(p1, particles[j], r);
          }
        }
      }
    }
  }

  function draw(){
    fitCanvas();
    const b = getBounds();
    const r = particles._r || 3;

    ctx.clearRect(0,0,vessel.clientWidth, vessel.clientHeight);

    ctx.save();
    ctx.beginPath();
    ctx.rect(0, 0, b.xMax, b.yMax);
    ctx.clip();

    for(const p of particles){
      let color = species[p.type].color;

if (p.flash > 0) {
  color = "#ffb73bff";        // χρυσό = “αντίδραση”
  p.flash *= 0.8;           // fade
  if (p.flash < 0.05) p.flash = 0;
}

ctx.fillStyle = color;

      ctx.beginPath();
      ctx.arc(p.x, p.y, r, 0, Math.PI*2);
      ctx.fill();
    }

    ctx.restore();

    const st = kineticStats();
    ctx.fillStyle = "rgba(0,0,0,0.8)";
    ctx.font = "12px Arial";
    // ctx.fillText(`N=${particles.length}  r=${r.toFixed(0)}px  KE/N=${st.kePer.toFixed(0)}`, 8, 16);
  }

  let lastICEUpdate = 0;

  function raf(ts){
if (vesselEmpty) {
  ctx.clearRect(0,0,vessel.clientWidth, vessel.clientHeight);
  requestAnimationFrame(raf);
  return;
}

    
    if(!lastTs) lastTs = ts;
    const rawDt = (ts - lastTs) / 1000;
    lastTs = ts;

    const dt = Math.min(0.03, Math.max(0.001, rawDt));

    if (running) {

      const substeps = (particles.length > 1200) ? 2 : 1;
      const h = dt / substeps;
      for(let s=0; s<substeps; s++) stepMicro(h);

      stepModel(dt);
      applyModelToParticles();

      simTime += dt;

const tCell = document.getElementById("ice_time_cell");

if (tCell) {

  if (equilibriumLocked) {
    // ΜΟΝΟ όταν ΕΙΝΑΙ ΤΩΡΑ σε ισορροπία
    tCell.innerHTML =
      `<span style="color:#1b5e20;font-weight:bold;">
        Χημ. Ισορροπία
      </span>`;
  } else {
    // σε ΟΠΟΙΑΔΗΠΟΤΕ διαταραχή φεύγει ΑΜΕΣΑ
    tCell.innerHTML =
      `<span style="color:#000;font-weight:bold;">
        Σε t = <span id="ice_time">${simTime.toFixed(1)}</span> s
      </span>`;
  }

}




      if(simTime - lastICEUpdate >= 0.02){
        updateICEAndGraph();
        lastICEUpdate = simTime;
      }
    }

    draw();
    requestAnimationFrame(raf);
  }

function setStartLabel() {

  if (running) {
    btnStart.textContent = "ΠΑΥΣΗ";
    return;
  }

  if (!hasStartedOnce || simTime === 0) {
    btnStart.textContent = "ΕΝΑΡΞΗ";
    return;
  }

  btnStart.textContent = "ΣΥΝΕΧΙΣΗ";
}





if(btnStart){
  
btnStart.onclick = ()=>{
  if (vesselEmpty) return;

if (!running && simTime === 0) {
  hasStartedOnce = true;   // ⬅️ ΜΟΝΟ εδώ
  rebaselineForNewRun();
}

//if (!running && equilibriumLocked) {
//  equilibriumLocked = false;   // ⬅️ ΞΕΚΛΕΙΔΩΜΑ ΙΣΟΡΡΟΠΙΑΣ
 // updateControlsLock();
 // eqTimer = 0;
//}

running = !running;
lastTs = null;                 // ⬅️ καθαρό dt
window.controlsLocked = running;
setStartLabel();
updateControlsLock();
};

}


if (btnEmpty) {
btnEmpty.addEventListener("pointerdown", e => {
  e.preventDefault();
  e.stopPropagation();


  hasStartedOnce = false;

  running = false;
  vesselEmpty = true;        // ⬅️ ΠΡΩΤΟ
  equilibriumReached = false;
eqTimer = 0;
updateControlsLock();

btnStart.textContent = "ΕΝΑΡΞΗ";
  simTime = 0;
  extentXi = 0;
  lastTs = null;
  lastICEUpdate = 0;

  // μηδενισμός μοντέλου
  nA = nB = nG = 0;
  nA0 = nB0 = nG0 = 0;

  if (window.simulationParams) {
    window.simulationParams.nA = 0;
    window.simulationParams.nB = 0;
    window.simulationParams.nG = 0;
  }

  particles = [];

  // καθαρισμοί UI
  clearICETable();
  if (window.CTGraph) window.CTGraph.clear();
  if (window.VTGraph) window.VTGraph.clear();
  if (window.Rates) window.Rates.reset();

  publishInitialRate();
  setStartLabel();
  
});


}


  // temperature changes: rescale velocities (visual only)
  let lastT = null;
function syncTemperature(){
  const T = window.simulationParams?.T || 300;

  const targetSigma = sigmaFromT(T);
  const currentSigma = currentSpeedScale();

  if (!currentSigma || currentSigma <= 0) return;

  const factor = targetSigma / currentSigma;

  for (const p of particles) {
    p.vx *= factor;
    p.vy *= factor;
  }
}

  setInterval(syncTemperature, 150);

function adaptVolume() {

  const wasAtEquilibrium = equilibriumLocked;

  const sp = window.simulationParams || {};
  const V = Math.max(1e-6, +sp.V || 0.5);

  const b = getBounds();
  const oldMax = adaptVolume._lastXMax || b.xMax;
  const newMax = b.xMax;

  if (oldMax > 0 && Math.abs(newMax - oldMax) > 1) {
    const scale = newMax / oldMax;

    // μόνο οπτική αναπροσαρμογή
    for (const p of particles) {
      p.x = Math.min(
        newMax - particles._r,
        Math.max(particles._r, p.x * scale)
      );
    }
  }

  adaptVolume._lastXMax = newMax;

  // 🔁 ΝΕΟ SNAPSHOT ΣΥΓΚΕΝΤΡΩΣΕΩΝ
  eq_CA = nA / V;
  eq_CB = nB / V;
  eq_CG = nG / V;

  // ICE baseline (χωρίς αντίδραση)
  nA0 = nA;
  nB0 = nB;
  nG0 = nG;
  extentXi = 0;

  equilibriumLocked  = wasAtEquilibrium;
  equilibriumReached = wasAtEquilibrium;
  eqTimer = 0;

  if (equilibriumLocked) {
  const sp = window.simulationParams || {};
  const V = Math.max(1e-6, +sp.V || 0.5);
  const T = +sp.T || 300;

  const kf = kFromT(T);
  const CA = nA / V;
  const CB = nB / V;

  const vEq = kf * CA * CB;

  eq_vf = vEq;
  eq_vr = vEq;
}


  updateICEAndGraph();
}



function adaptTemperature(){

  // 🔥 έξοδος από ισορροπία
  equilibriumLocked  = false;
  equilibriumReached = false;
  eqTimer = 0;

  // ✅ ICE baseline = τρέχουσες ποσότητες
  nA0 = nA;
  nB0 = nB;
  nG0 = nG;
  extentXi = 0;

  syncTemperature();   // μόνο οπτικό
  updateICEAndGraph(); // Qc από συγκεντρώσεις
}








  function addMoles(dA, dB, dG){
      rebaselineFromICE();
  //  προσαρμογή moles
   
    hasStartedOnce = false;
    restoreFromEquilibriumSnapshot();
   manualDisturbance = true;
equilibriumLocked = false;
equilibriumReached = false;
eqTimer = 0; 
    
      

    vesselEmpty = false;


    // Adds particles AND updates the model baseline (soft reset at t=0 as requested)
    const sp = window.simulationParams || {T:300};
    const r = particles._r || FIXED_RADIUS;
    const b = getBounds();
    const margin = r + 1;

    const addCountA = Math.max(0, Math.round((dA||0) * MOL_TO_PARTICLES));
    const addCountB = Math.max(0, Math.round((dB||0) * MOL_TO_PARTICLES));
    const addCountG = Math.max(0, Math.round((dG||0) * MOL_TO_PARTICLES));

    const totalNow = particles.length;
    const totalAdd = addCountA + addCountB + addCountG;
    if(totalNow + totalAdd > 1000){
      alert("Υπέρβαση χωρητικότητας: έως 1000 σωματίδια στο δοχείο.");
      return;
    }

    const sig = sigmaFromT(sp.T || 300);
    function addOne(type){
      
      const m = species[type].mass;
      const vx = randn() * sig;
      const vy = randn() * sig;

      for(let a=0; a<3000; a++){
        const x = margin + rand() * Math.max(1, (b.xMax - margin*2));
        const y = margin + rand() * Math.max(1, (b.yMax - margin*2));
        let ok = true;
        for(let j=0;j<particles.length;j++){
          const q = particles[j];
          const dx = x - q.x;
          const dy = y - q.y;
          if(dx*dx + dy*dy < (2*r)*(2*r)) { ok = false; break; }
        }
        if(ok){
          particles.push({ type, m, x, y, vx, vy, flash: 0 });
          
          return;
        }
      }
      particles.push({ type, m,
        x: margin + rand()*(b.xMax-2*margin),
        y: margin + rand()*(b.yMax-2*margin),
        vx, vy
      });
    }




    for(let i=0;i<addCountA;i++) addOne("A");
    for(let i=0;i<addCountB;i++) addOne("B");
    for(let i=0;i<addCountG;i++) addOne("G");

    for(const q of particles){
      q.x = margin + rand() * Math.max(1, (b.xMax - margin*2));
      q.y = margin + rand() * Math.max(1, (b.yMax - margin*2));
    }

    // IMPORTANT: re-baseline (reads simulationParams that were already updated in controls.js)
    // ⬅️ ΠΡΟΣΘΗΚΗ mol ΣΤΟ ΜΟΝΤΕΛΟ (ΑΥΤΟ ΕΛΕΙΠΕ)
nA += dA || 0;
nB += dB || 0;
nG += dG || 0;

// γράψ' τα πίσω στο simulationParams
writeModelToSimulationParams();

    resetSoft({keepInventory:true});
  publishInitialRate();
disturbanceRebaseline({freeze:true});




  }


function removeMoles(dA, dB, dG){
    rebaselineFromICE();
  //  προσαρμογή moles

  hasStartedOnce = false;
  restoreFromEquilibriumSnapshot();
  if (vesselEmpty) return;
   manualDisturbance = true;
equilibriumLocked = false;
equilibriumReached = false;
eqTimer = 0; 

  


  // clamp ώστε να μην πάει ποτέ κάτω από 0
  const remA = Math.min(Math.max(0, dA || 0), nA);
  const remB = Math.min(Math.max(0, dB || 0), nB);
  const remG = Math.min(Math.max(0, dG || 0), nG);

  // ενημέρωση μοντέλου
  nA -= remA;
  nB -= remB;
  nG -= remG;

  nA = Math.max(0, nA);
  nB = Math.max(0, nB);
  nG = Math.max(0, nG);

  // ενημέρωση simulationParams
  writeModelToSimulationParams();

  // ⚠️ ΝΕΟ baseline για ICE
  nA0 = nA;
  nB0 = nB;
  nG0 = nG;
  extentXi = 0;

  // ανανέωση σωματιδίων
  applyModelToParticles();

  // UI
  updateICEAndGraph();
  publishInitialRate();
  disturbanceRebaseline({freeze:true});






}


function rebaselineICE() {
  // τρέχουσες ποσότητες γίνονται αρχικές
  nA0 = nA;
  nB0 = nB;
  nG0 = nG;

  extentXi = 0;

  // καθάρισε ισορροπία
  equilibriumReached = false;
  eqTimer = 0;

  // ενημέρωση πίνακα
  updateICEAndGraph();
}

function rebaselineForNewRun() {
  // τρέχοντα → αρχικά
  nA0 = nA;
  nB0 = nB;
  nG0 = nG;

  // μηδενισμός εξέλιξης
  extentXi = 0;

  equilibriumReached = false;
  eqTimer = 0;

  // καθάρισμα γραμμών μεταβολής
  [
    "ice_r_1","ice_r_2","ice_r_3",
    "ice_p_1","ice_p_2","ice_p_3"
  ].forEach(id=>{
    const el = document.getElementById(id);
    if (el) el.textContent = "0.000";
  });

  // ενημέρωση πίνακα (χωρίς χρόνο / γραφήματα)
  updateICEAndGraph();

  // κουμπί → Συνέχιση
  if (btnStart) btnStart.textContent = "ΣΥΝΕΧΙΣΗ";
// ===== ΑΡΧΙΚΟ ΘΕΩΡΗΤΙΚΟ ΣΗΜΕΙΟ t = 0 =====
const sp = window.simulationParams || {};
const V = sp.V || 0.5;
const T = sp.T || 300;

// συγκεντρώσεις στο t=0
const CA0 = nA / V;
const CB0 = nB / V;
const CG0 = nG / V;

// ---- C–t ----
if (window.CTGraph) {
  window.CTGraph.clear();
  window.CTGraph.addPoint(0, CA0, CB0, CG0);
}

// ---- υ–t ----
if (window.VTGraph && window.Kinetics) {
  const kf = window.Kinetics.kFromT(T);
  const Kc = KcFromT(T);
  const kr = kf / Kc;

  const vF0 = kf * CA0 * CB0;
  const vR0 = kr * CG0 * CG0;

  window.VTGraph.clear();
  window.VTGraph.addPoint(0, vF0, vR0);
}


}

function snap(x, digits = 3) {
  const f = Math.pow(10, digits);
  return Math.round(x * f) / f;
}


function restoreFromEquilibriumSnapshot() {
  if (!equilibriumLocked) return;

  nA = eq_nA;
  nB = eq_nB;
  nG = eq_nG;

  writeModelToSimulationParams();

  equilibriumLocked = false;
  equilibriumReached = false;
  eqTimer = 0;
}



function disturbanceRebaseline({freeze=true} = {}) {

hasStartedOnce = false;
  // 1. τρέχουσες ποσότητες → νέα αρχικά
  nA0 = nA;
  nB0 = nB;
  nG0 = nG;

  // 2. μηδενισμός εξέλιξης
  extentXi = 0;

  // 3. καθάρισμα γραμμών μεταβολής ICE
  [
    "ice_r_1","ice_r_2","ice_r_3",
    "ice_p_1","ice_p_2","ice_p_3"
  ].forEach(id=>{
    const el = document.getElementById(id);
    if (el) el.textContent = "0.000";
  });

  // 4. τελικές = αρχικές
  ice_At.textContent = nA0.toFixed(3);
  ice_Bt.textContent = nB0.toFixed(3);
  ice_Gt.textContent = nG0.toFixed(3);

  // 5. ισορροπία reset
  equilibriumLocked = false;
  equilibriumReached = false;
  eqTimer = 0;

  // 6. παύση προσομοίωσης
  if (freeze) {
    running = false;
    lastTs = null;
    setStartLabel();          // θα γράψει ΣΥΝΕΧΙΣΗ
    window.controlsLocked = false;
    updateControlsLock();
  }

  updateICEAndGraph();
}




function updateControlsLock() {

  const lock =
    running === true &&
    equilibriumLocked === false;

  for (const el of controlsToLock) {
    el.disabled = lock;
    el.classList.toggle("disabled", lock);
  }

  if (btnStart) {
  btnStart.disabled = false;
  btnStart.classList.remove("disabled");
}
}





window.ParticlesSim = {
  reset: (opts)=>resetSoft(opts ?? { keepInventory: true }),
  adaptVolume,
  adaptTemperature,
  setRunning: (v)=>{ running = !!v; setStartLabel(); },
  getRunning: ()=>running,
  addMoles,
  removeMoles,
  setInert        
};



setStartLabel();
requestAnimationFrame(raf);

})();
