// sounds.jsx — Web Audio API synthesized sound effects for Ashfall
// All sounds generated in real-time via Web Audio API. No external files needed.

// ── Audio Context singleton ──
let _ctx = null;
function getCtx() {
  if (!_ctx) {
    try { const C = window.AudioContext || window.webkitAudioContext; if (C) _ctx = new C(); } catch (_) {}
  }
  return _ctx;
}
function resumeCtx() {
  const ctx = getCtx();
  if (ctx && ctx.state === "suspended") { try { ctx.resume(); } catch (_) {} }
  return ctx;
}

// ── Active sound tracking ──
const active = new Set();
const timeouts = [];
function add(n) { active.add(n); return n; }
function rem(n) { active.delete(n); }
window.__ashfallStopAll = function () {
  // Cancel pending staggered timeouts
  while (timeouts.length) { clearTimeout(timeouts.pop()); }
  for (const n of active) {
    try {
      if (n instanceof HTMLAudioElement) { n.pause(); n.currentTime = 0; }
      else { if (n.stop) n.stop(); if (n.disconnect) n.disconnect(); }
    } catch (_) {}
  }
  active.clear();
};

// ── Utility: noise buffer ──
function noiseBuf(ctx, dur, color) {
  const sr = ctx.sampleRate, len = Math.floor(sr * dur);
  const buf = ctx.createBuffer(1, len, sr), d = buf.getChannelData(0);
  if (color === "pink") {
    let b0 = 0, b1 = 0, b2 = 0;
    for (let i = 0; i < len; i++) {
      const w = Math.random() * 2 - 1;
      b0 = 0.99765 * b0 + w * 0.0990460; b1 = 0.963 * b1 + w * 0.2965164; b2 = 0.57 * b2 + w * 1.0526913;
      d[i] = (b0 + b1 + b2 + w * 0.1848) * 0.12;
    }
  } else if (color === "brown") {
    let last = 0;
    for (let i = 0; i < len; i++) { const w = Math.random() * 2 - 1; last = (last + 0.02 * w) / 1.02; d[i] = last * 3; }
  } else {
    for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
  }
  return buf;
}

// ── Noise source helper ──
function noiseSource(ctx, dur, color, freq, Q, vol, rampUp, rampDown) {
  const now = ctx.currentTime;
  const buf = noiseBuf(ctx, dur || 3, color || "pink");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const filt = ctx.createBiquadFilter();
  filt.type = "lowpass"; filt.frequency.value = freq || 3000; filt.Q.value = Q || 0.5;
  const gain = ctx.createGain();
  const startV = vol || 0.25;
  gain.gain.setValueAtTime(0, now);
  if (rampUp) gain.gain.linearRampToValueAtTime(startV, now + rampUp);
  else gain.gain.linearRampToValueAtTime(startV, now + 0.2);
  if (rampDown) gain.gain.linearRampToValueAtTime(0, now + dur - rampDown);
  else if (dur > 0) gain.gain.linearRampToValueAtTime(0, now + dur - 0.1);
  src.connect(filt).connect(gain).connect(ctx.destination);
  src.start(now); if (dur > 0) src.stop(now + dur + 0.05);
  add(src); add(filt); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Rain ──
function playRain(ctx, dur, heavy) {
  const now = ctx.currentTime;
  const d = dur || 5;
  const buf = noiseBuf(ctx, d, "pink");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass"; bp.frequency.value = heavy ? 4000 : 2500; bp.Q.value = heavy ? 0.4 : 0.6;
  // LFO on filter for variation (gusts)
  const lfo = ctx.createOscillator(); const lg = ctx.createGain();
  lfo.type = "sine"; lfo.frequency.value = 0.15 + Math.random() * 0.1;
  lg.gain.value = heavy ? 1200 : 600;
  lfo.connect(lg).connect(bp.frequency);
  lfo.start(now); lfo.stop(now + d + 0.1);
  const gain = ctx.createGain();
  const vol = heavy ? 0.3 : 0.2;
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(vol, now + 0.3);
  gain.gain.linearRampToValueAtTime(vol, now + d - 0.3);
  gain.gain.linearRampToValueAtTime(0, now + d);
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + d + 0.1);
  add(src); add(bp); add(gain); add(lfo); add(lg);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Wind ──
function playWind(ctx, dur) {
  return noiseSource(ctx, dur || 5, "pink", 800, 1.2, 0.15, 0.5, 0.5);
}

// ── Thunder ──
function playThunder(ctx) {
  const now = ctx.currentTime;
  const dur = 2.5;
  const buf = noiseBuf(ctx, dur, "brown");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const lp = ctx.createBiquadFilter();
  lp.type = "lowpass"; lp.frequency.value = 200; lp.Q.value = 1;
  const hp = ctx.createBiquadFilter();
  hp.type = "highpass"; hp.frequency.value = 40;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.5, now + 0.05);
  gain.gain.exponentialRampToValueAtTime(0.15, now + 0.6);
  gain.gain.linearRampToValueAtTime(0.3, now + 0.9);
  gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
  src.connect(lp).connect(hp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(lp); add(hp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Footsteps ──
function playFootsteps(ctx, count, surface) {
  const now = ctx.currentTime;
  count = count || 3;
  const interval = surface === "fast" || surface === "running" ? 0.2 : surface === "slow" ? 0.7 : 0.45;
  const isHeels = surface === "heels";
  const isWood = surface === "wood";
  const dur = count * interval + 0.2;
  const buf = ctx.createBuffer(1, Math.floor(ctx.sampleRate * dur), ctx.sampleRate);
  const d = buf.getChannelData(0);
  for (let i = 0; i < count; i++) {
    const pos = Math.floor(ctx.sampleRate * (0.05 + i * interval));
    for (let j = 0; j < 200; j++) {
      const t = j / 200;
      const env = Math.exp(-t * 15);
      const noise = (Math.random() * 2 - 1) * env;
      const tone = isHeels ? Math.sin(j * 0.3) * env * 0.4 : 0;
      d[pos + j] = (noise + tone) * (isHeels ? 0.6 : isWood ? 0.35 : 0.3);
    }
  }
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const hp = ctx.createBiquadFilter();
  hp.type = "highpass"; hp.frequency.value = isHeels ? 2000 : 500;
  const gain = ctx.createGain();
  gain.gain.value = 0.4;
  src.connect(hp).connect(gain).connect(ctx.destination);
  src.start(now);
  add(src); add(hp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Door ──
function playDoor(ctx, type) {
  const now = ctx.currentTime;
  const isOpen = type === "open" || type === "large";
  const isClose = type === "close" || type === "heavy";
  const isKnock = type === "knock";
  const dur = isKnock ? 0.15 : isOpen ? 2.5 : 1.5;
  const buf = noiseBuf(ctx, dur, "pink");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass";
  const gain = ctx.createGain();
  if (isKnock) {
    bp.frequency.value = 800; bp.Q.value = 3;
    gain.gain.setValueAtTime(0, now);
    gain.gain.linearRampToValueAtTime(0.5, now + 0.005);
    gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
  } else if (isClose) {
    bp.frequency.value = 400; bp.Q.value = 1.5;
    gain.gain.setValueAtTime(0, now);
    gain.gain.linearRampToValueAtTime(0.4, now + 0.02);
    gain.gain.exponentialRampToValueAtTime(0.2, now + 0.2);
    // creak
    const cFilt = ctx.createBiquadFilter();
    cFilt.type = "lowpass"; cFilt.frequency.value = 1200;
    const cGain = ctx.createGain();
    cGain.gain.setValueAtTime(0, now); cGain.gain.linearRampToValueAtTime(0.12, now + 0.3); cGain.gain.exponentialRampToValueAtTime(0.001, now + 1.2);
    const cSrc = ctx.createBufferSource();
    const cBuf = noiseBuf(ctx, 1.2, "pink");
    cSrc.buffer = cBuf;
    // modulate creak frequency
    const lfo = ctx.createOscillator(); const lg = ctx.createGain();
    lfo.frequency.value = 4; lg.gain.value = 400;
    cSrc.connect(cFilt).connect(cGain).connect(ctx.destination);
    lfo.connect(lg).connect(cFilt.frequency);
    lfo.start(now); lfo.stop(now + 1.3);
    cSrc.start(now); cSrc.stop(now + 1.3);
    add(cSrc); add(cFilt); add(cGain); add(lfo); add(lg);
  } else {
    bp.frequency.value = 600; bp.Q.value = 2;
    gain.gain.setValueAtTime(0, now);
    gain.gain.linearRampToValueAtTime(0.3, now + 0.1);
    gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.1);
  }
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(bp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Lock / Key / Click ──
function playClick(ctx, type) {
  const now = ctx.currentTime;
  const dur = 0.08;
  const buf = ctx.createBuffer(1, Math.floor(ctx.sampleRate * dur), ctx.sampleRate);
  const d = buf.getChannelData(0);
  for (let i = 0; i < d.length; i++) {
    const t = i / ctx.sampleRate;
    const env = Math.exp(-t * 60);
    d[i] = (Math.sin(t * 8000) * 0.3 + (Math.random() * 2 - 1) * 0.7) * env * 0.5;
  }
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const gain = ctx.createGain();
  gain.gain.value = 0.3;
  src.connect(gain).connect(ctx.destination);
  src.start(now);
  add(src); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Beep ──
function playBeep(ctx, count) {
  const now = ctx.currentTime;
  count = count || 1;
  for (let i = 0; i < count; i++) {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = "sine";
    osc.frequency.value = 1200;
    const t = now + i * 0.3;
    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(0.2, t + 0.005);
    gain.gain.linearRampToValueAtTime(0, t + 0.1);
    osc.connect(gain).connect(ctx.destination);
    osc.start(t); osc.stop(t + 0.12);
    add(osc); add(gain);
  }
  return { stop: () => {} };
}

// ── Notification ──
function playNotification(ctx) {
  const now = ctx.currentTime;
  [800, 1200].forEach((freq, i) => {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = "sine";
    osc.frequency.value = freq;
    const t = now + i * 0.12;
    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(0.15, t + 0.005);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
    osc.connect(gain).connect(ctx.destination);
    osc.start(t); osc.stop(t + 0.25);
    add(osc); add(gain);
  });
  return { stop: () => {} };
}

// ── Phone Ring ──
function playPhoneRing(ctx) {
  const now = ctx.currentTime;
  for (let cycle = 0; cycle < 3; cycle++) {
    for (let i = 0; i < 2; i++) {
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = "sine";
      osc.frequency.value = i === 0 ? 440 : 480;
      const t = now + cycle * 0.8 + i * 0.2;
      gain.gain.setValueAtTime(0, t);
      gain.gain.linearRampToValueAtTime(0.2, t + 0.005);
      gain.gain.linearRampToValueAtTime(0, t + 0.18);
      osc.connect(gain).connect(ctx.destination);
      osc.start(t); osc.stop(t + 0.2);
      add(osc); add(gain);
    }
  }
  return { stop: () => {} };
}

// ── Vibrate ──
function playVibrate(ctx) {
  const now = ctx.currentTime;
  const dur = 1.5;
  const buf = noiseBuf(ctx, dur, "pink");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass"; bp.frequency.value = 150; bp.Q.value = 3;
  const amp = ctx.createGain();
  const lfo = ctx.createOscillator();
  const lg = ctx.createGain();
  lfo.frequency.value = 25; lg.gain.value = 0.2;
  amp.gain.value = 0.4;
  lfo.connect(lg).connect(amp.gain);
  src.connect(bp).connect(amp).connect(ctx.destination);
  lfo.start(now); src.start(now); src.stop(now + dur + 0.1);
  add(src); add(bp); add(amp); add(lfo); add(lg);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Alarm ──
function playAlarm(ctx, soft, dur) {
  const now = ctx.currentTime;
  const d = dur || 3;
  const baseFreq = soft ? 600 : 800;
  for (let t = 0; t < d; t += 0.5) {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = soft ? "sine" : "square";
    osc.frequency.value = t % 1 < 0.5 ? baseFreq : baseFreq * 1.3;
    const start = now + t;
    gain.gain.setValueAtTime(0, start);
    gain.gain.linearRampToValueAtTime(soft ? 0.1 : 0.2, start + 0.01);
    gain.gain.linearRampToValueAtTime(0, start + 0.45);
    osc.connect(gain).connect(ctx.destination);
    osc.start(start); osc.stop(start + 0.48);
    add(osc); add(gain);
  }
  return { stop: () => {} };
}

// ── Gunshot ──
function playGunshot(ctx, close) {
  const now = ctx.currentTime;
  const dur = close ? 1.5 : 1.0;
  const buf = noiseBuf(ctx, dur, "white");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const hp = ctx.createBiquadFilter();
  hp.type = "highpass"; hp.frequency.value = close ? 80 : 300;
  const lp = ctx.createBiquadFilter();
  lp.type = "lowpass"; lp.frequency.value = close ? 8000 : 4000;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(close ? 0.5 : 0.35, now + 0.002);
  gain.gain.exponentialRampToValueAtTime(0.3, now + 0.02);
  gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.1);
  src.connect(hp).connect(lp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(hp); add(lp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Impact / Banging / Metal ──
function playImpact(ctx, heavy, metal) {
  const now = ctx.currentTime;
  const dur = heavy ? 0.8 : 0.3;
  const buf = noiseBuf(ctx, dur, "pink");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass";
  bp.frequency.value = metal ? 1800 : heavy ? 200 : 500;
  bp.Q.value = metal ? 4 : heavy ? 1.5 : 2;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(heavy ? 0.5 : 0.3, now + 0.003);
  if (metal) {
    gain.gain.linearRampToValueAtTime(0.25, now + 0.05);
    gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
  } else {
    gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.05);
  }
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(bp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Glass break ──
function playGlassBreak(ctx) {
  const now = ctx.currentTime;
  const dur = 1.2;
  const buf = noiseBuf(ctx, dur, "white");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const hp = ctx.createBiquadFilter();
  hp.type = "highpass"; hp.frequency.value = 3000;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.3, now + 0.002);
  gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.1);
  // Add ringing tones
  for (let i = 0; i < 3; i++) {
    const osc = ctx.createOscillator();
    const og = ctx.createGain();
    osc.type = "sine";
    osc.frequency.value = 4000 + i * 2000 + Math.random() * 1000;
    const t = now + 0.02 + i * 0.05;
    og.gain.setValueAtTime(0, now);
    og.gain.linearRampToValueAtTime(0.06, t);
    og.gain.exponentialRampToValueAtTime(0.001, now + 0.8 + i * 0.15);
    osc.connect(og).connect(ctx.destination);
    osc.start(now); osc.stop(now + 1.0);
    add(osc); add(og);
  }
  src.connect(hp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(hp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Glass clink ──
function playGlassClink(ctx) {
  const now = ctx.currentTime;
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = "sine";
  osc.frequency.value = 2500;
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.12, now + 0.002);
  gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
  const osc2 = ctx.createOscillator();
  const g2 = ctx.createGain();
  osc2.type = "sine";
  osc2.frequency.value = 3800;
  g2.gain.setValueAtTime(0, now);
  g2.gain.linearRampToValueAtTime(0.06, now + 0.002);
  g2.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
  osc.connect(gain).connect(ctx.destination);
  osc2.connect(g2).connect(ctx.destination);
  osc.start(now); osc.stop(now + 0.65);
  osc2.start(now); osc2.stop(now + 0.45);
  add(osc); add(gain); add(osc2); add(g2);
  return { stop: () => { try { osc.stop(); } catch(_) {} } };
}

// ── Paper rustle ──
function playPaper(ctx) {
  const now = ctx.currentTime;
  const dur = 0.4;
  const buf = noiseBuf(ctx, dur, "pink");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass"; bp.frequency.value = 2500; bp.Q.value = 1.5;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.12, now + 0.05);
  gain.gain.linearRampToValueAtTime(0.08, now + 0.15);
  gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.05);
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(bp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Paper turn ──
function playPaperTurn(ctx) {
  const now = ctx.currentTime;
  const dur = 0.6;
  const buf = noiseBuf(ctx, dur, "pink");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass"; bp.frequency.value = 1800; bp.Q.value = 1.2;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.15, now + 0.08);
  gain.gain.linearRampToValueAtTime(0.1, now + 0.2);
  gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.05);
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(bp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Keyboard typing ──
function playKeyboard(ctx, count) {
  const now = ctx.currentTime;
  count = count || 4;
  for (let i = 0; i < count; i++) {
    const buf = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.06), ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let j = 0; j < d.length; j++) {
      const t = j / ctx.sampleRate;
      d[j] = (Math.sin(t * 6000) * 0.3 + (Math.random() * 2 - 1)) * Math.exp(-t * 80) * 0.3;
    }
    const src = ctx.createBufferSource();
    src.buffer = buf;
    const gain = ctx.createGain();
    gain.gain.value = 0.2;
    src.connect(gain).connect(ctx.destination);
    src.start(now + i * (0.08 + Math.random() * 0.08));
    add(src); add(gain);
  }
  return { stop: () => {} };
}

// ── Heartbeat ──
function playHeartbeat(ctx, count, fast) {
  const now = ctx.currentTime;
  count = count || 4;
  const interval = fast ? 0.35 : 0.6;
  for (let i = 0; i < count; i++) {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = "sine";
    osc.frequency.value = 60;
    const t = now + i * interval;
    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(0.25, t + 0.01);
    gain.gain.exponentialRampToValueAtTime(0.15, t + 0.04);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
    osc.connect(gain).connect(ctx.destination);
    osc.start(t); osc.stop(t + 0.18);
    add(osc); add(gain);
    // echo thump
    if (i % 1 === 0) {
      const o2 = ctx.createOscillator();
      const g2 = ctx.createGain();
      o2.type = "sine"; o2.frequency.value = 45;
      g2.gain.setValueAtTime(0, t + 0.08);
      g2.gain.linearRampToValueAtTime(0.15, t + 0.09);
      g2.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
      o2.connect(g2).connect(ctx.destination);
      o2.start(t); o2.stop(t + 0.22);
      add(o2); add(g2);
    }
  }
  return { stop: () => {} };
}

// ── Engine ──
function playEngine(ctx, dur, rev) {
  const now = ctx.currentTime;
  const d = dur || 4;
  const buf = noiseBuf(ctx, d, "brown");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass"; bp.frequency.value = 120; bp.Q.value = 2;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(rev ? 0.3 : 0.2, now + 0.3);
  if (rev) {
    gain.gain.linearRampToValueAtTime(0.4, now + d * 0.5);
    const lfo = ctx.createOscillator(); const lg = ctx.createGain();
    lfo.frequency.value = 6; lg.gain.value = 40;
    lfo.connect(lg).connect(bp.frequency);
    lfo.start(now); lfo.stop(now + d + 0.1);
    add(lfo); add(lg);
  }
  if (d > 0) gain.gain.linearRampToValueAtTime(0, now + d - 0.3);
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + d + 0.1);
  add(src); add(bp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Brakes / Tires squeal ──
function playBrakes(ctx) {
  const now = ctx.currentTime;
  const dur = 1.5;
  const buf = noiseBuf(ctx, dur, "white");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass"; bp.frequency.value = 3000; bp.Q.value = 5;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.25, now + 0.1);
  gain.gain.linearRampToValueAtTime(0.3, now + 0.3);
  gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.1);
  // modulate frequency for that screeching sound
  const lfo = ctx.createOscillator(); const lg = ctx.createGain();
  lfo.frequency.value = 8; lg.gain.value = 500;
  lfo.connect(lg).connect(bp.frequency);
  lfo.start(now); lfo.stop(now + dur + 0.1);
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(bp); add(gain); add(lfo); add(lg);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Water splash ──
function playWaterSplash(ctx) {
  const now = ctx.currentTime;
  const dur = 1.0;
  const buf = noiseBuf(ctx, dur, "pink");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass"; bp.frequency.value = 1200; bp.Q.value = 2;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.25, now + 0.02);
  gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.1);
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(bp); add(gain);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Water drops ──
function playWaterDrops(ctx, count) {
  const now = ctx.currentTime;
  count = count || 3;
  for (let i = 0; i < count; i++) {
    const t = now + i * (0.3 + Math.random() * 0.3);
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = "sine";
    osc.frequency.value = 2000 + Math.random() * 1000;
    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(0.12, t + 0.002);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
    osc.connect(gain).connect(ctx.destination);
    osc.start(t); osc.stop(t + 0.15);
    add(osc); add(gain);
  }
  return { stop: () => {} };
}

// ── Boat horn / Fog horn ──
function playBoatHorn(ctx) {
  const now = ctx.currentTime;
  const dur = 2.5;
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = "sine";
  osc.frequency.value = 120;
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.3, now + 0.3);
  gain.gain.linearRampToValueAtTime(0.25, now + dur - 0.3);
  gain.gain.linearRampToValueAtTime(0, now + dur);
  osc.connect(gain).connect(ctx.destination);
  osc.start(now); osc.stop(now + dur + 0.1);
  add(osc); add(gain);
  // Add harmonic
  const o2 = ctx.createOscillator();
  const g2 = ctx.createGain();
  o2.type = "sine"; o2.frequency.value = 80;
  g2.gain.setValueAtTime(0, now);
  g2.gain.linearRampToValueAtTime(0.15, now + 0.3);
  g2.gain.linearRampToValueAtTime(0, now + dur);
  o2.connect(g2).connect(ctx.destination);
  o2.start(now); o2.stop(now + dur + 0.1);
  add(o2); add(g2);
  return { stop: () => { try { osc.stop(); } catch(_) {} } };
}

// ── Birds ──
function playBirds(ctx) {
  const now = ctx.currentTime;
  const count = 4;
  for (let i = 0; i < count; i++) {
    const t = now + i * (0.4 + Math.random() * 0.6);
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = "sine";
    const baseFreq = 2000 + Math.random() * 2000;
    osc.frequency.setValueAtTime(baseFreq, t);
    osc.frequency.linearRampToValueAtTime(baseFreq * 1.5, t + 0.05);
    osc.frequency.linearRampToValueAtTime(baseFreq * 0.8, t + 0.1);
    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(0.08, t + 0.01);
    gain.gain.linearRampToValueAtTime(0, t + 0.12);
    osc.connect(gain).connect(ctx.destination);
    osc.start(t); osc.stop(t + 0.15);
    add(osc); add(gain);
  }
  return { stop: () => {} };
}

// ── Clock ──
function playClock(ctx, count) {
  const now = ctx.currentTime;
  count = count || 4;
  for (let i = 0; i < count; i++) {
    const t = now + i * 0.5;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = "sine";
    osc.frequency.value = i % 2 === 0 ? 1500 : 2000;
    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(0.1, t + 0.002);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.04);
    osc.connect(gain).connect(ctx.destination);
    osc.start(t); osc.stop(t + 0.05);
    add(osc); add(gain);
  }
  return { stop: () => {} };
}

// ── Fire crackle ──
function playFireCrackle(ctx, dur) {
  const now = ctx.currentTime;
  const d = dur || 5;
  // continuous noise base
  const s1 = noiseSource(ctx, d, "pink", 2500, 2, 0.12, 0.2, 0.3);
  // random pops
  const popCount = Math.floor(d * 3);
  for (let i = 0; i < popCount; i++) {
    const t = now + Math.random() * d;
    const buf = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.05), ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let j = 0; j < data.length; j++) data[j] = (Math.random() * 2 - 1) * Math.exp(-j / (ctx.sampleRate * 0.01));
    const src = ctx.createBufferSource();
    src.buffer = buf;
    const hp = ctx.createBiquadFilter();
    hp.type = "highpass"; hp.frequency.value = 2000;
    const gain = ctx.createGain();
    gain.gain.value = 0.2 + Math.random() * 0.15;
    src.connect(hp).connect(gain).connect(ctx.destination);
    src.start(t);
    add(src); add(hp); add(gain);
  }
  return { stop: () => { try { s1.stop(); } catch(_) {} } };
}

// ── Explosion ──
function playExplosion(ctx) {
  const now = ctx.currentTime;
  const dur = 2.0;
  const buf = noiseBuf(ctx, dur, "brown");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const lp = ctx.createBiquadFilter();
  lp.type = "lowpass"; lp.frequency.value = 500;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.6, now + 0.01);
  gain.gain.exponentialRampToValueAtTime(0.2, now + 0.1);
  gain.gain.exponentialRampToValueAtTime(0.001, now + dur - 0.1);
  // low rumble
  const osc = ctx.createOscillator();
  const og = ctx.createGain();
  osc.type = "sine"; osc.frequency.value = 40;
  og.gain.setValueAtTime(0, now);
  og.gain.linearRampToValueAtTime(0.4, now + 0.02);
  og.gain.exponentialRampToValueAtTime(0.001, now + 1.5);
  src.connect(lp).connect(gain).connect(ctx.destination);
  osc.connect(og).connect(ctx.destination);
  osc.start(now); osc.stop(now + 1.6);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(lp); add(gain); add(osc); add(og);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Elevator ──
function playElevator(ctx) {
  const now = ctx.currentTime;
  const dur = 3;
  const buf = noiseBuf(ctx, dur, "brown");
  const src = ctx.createBufferSource();
  src.buffer = buf;
  const bp = ctx.createBiquadFilter();
  bp.type = "bandpass"; bp.frequency.value = 250; bp.Q.value = 2;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.15, now + 0.3);
  gain.gain.linearRampToValueAtTime(0.2, now + 1.5);
  gain.gain.linearRampToValueAtTime(0, now + dur);
  // rising frequency
  bp.frequency.setValueAtTime(200, now);
  bp.frequency.linearRampToValueAtTime(350, now + dur);
  src.connect(bp).connect(gain).connect(ctx.destination);
  src.start(now); src.stop(now + dur + 0.1);
  add(src); add(bp); add(gain);
  // ding
  setTimeout(() => playBeep(ctx, 1), (dur - 0.5) * 1000);
  return { stop: () => { try { src.stop(); } catch(_) {} } };
}

// ── Crowd murmur ──
function playCrowd(ctx) {
  return noiseSource(ctx, 4, "pink", 800, 1.5, 0.08, 0.3, 0.3);
}

// ── Category → audio file mapping ──
const CATEGORY_FILE = {
  "rain": "rain_light.mp3", "rain_heavy": "rain_heavy.mp3", "rain_window": "rain_window.mp3",
  "rain_light": "rain_light.mp3", "rain_distant": "rain_light.mp3", "rain_ambient": "rain_light.mp3",
  "rain_night": "rain_light.mp3", "rain_windshield": "rain_window.mp3", "rain_start": "rain_light.mp3",
  "rain_stop": "silence.mp3", "thunder": "thunder.mp3",
  "wind": "wind.mp3", "wind_trees": "wind.mp3", "wind_harbor": "wind.mp3",
  "door": "door_open.mp3", "door_open": "door_open.mp3", "door_close": "door_close.mp3",
  "door_knock": "door_knock.mp3", "door_wood": "door_open.mp3", "door_metal": "door_open.mp3",
  "door_old": "door_open.mp3", "door_large": "door_open.mp3", "door_heavy": "door_close.mp3",
  "gate": "door_knock.mp3", "lock": "lock.mp3", "key": "lock.mp3",
  "beep": "beep.mp3", "beep_medical": "beep.mp3",
  "footsteps": "footsteps_stone.mp3", "footsteps_stone": "footsteps_stone.mp3",
  "footsteps_wood": "footsteps_stone.mp3", "footsteps_heels": "footsteps_heels.mp3",
  "footsteps_slow": "footsteps_stone.mp3", "footsteps_fast": "running.mp3",
  "running": "running.mp3", "stairs": "stairs.mp3", "elevator": "elevator.mp3",
  "alarm": "alarm.mp3", "alarm_soft": "alarm.mp3", "alarm_distant": "alarm.mp3",
  "gunshot": "gunshot.mp3", "gunshot_close": "gunshot_close.mp3",
  "impact": "metal_bang.mp3", "impact_heavy": "metal_bang.mp3", "impact_metal": "metal_bang.mp3",
  "banging": "metal_bang.mp3", "metal": "metal_bang.mp3", "metal_bang": "metal_bang.mp3",
  "glass": "glass_break.mp3", "glass_break": "glass_break.mp3", "glass_clink": "glass_clink.mp3",
  "paper": "paper_rustle.mp3", "paper_rustle": "paper_rustle.mp3", "paper_turn": "paper_turn.mp3",
  "notebook": "paper_turn.mp3", "keyboard": "keyboard.mp3",
  "notification": "notification.mp3", "phone": "phone_ring.mp3", "phone_call": "phone_ring.mp3",
  "vibrate": "phone_vibrate.mp3",
  "fire": "fire_crackle.mp3", "fire_crackle": "fire_crackle.mp3", "candle": "candle.mp3",
  "engine": "car_engine.mp3", "engine_idle": "car_engine.mp3", "engine_drive": "car_drive.mp3",
  "engine_slow": "car_engine.mp3", "engine_stop": "car_engine.mp3",
  "car": "car_drive.mp3", "car_door": "car_door.mp3", "car_drive": "car_drive.mp3",
  "tires": "car_drive.mp3", "brakes": "car_brakes.mp3", "brakes_squeal": "car_brakes.mp3",
  "wipers": "rain_window.mp3", "boat": "boat_horn.mp3", "foghorn": "boat_horn.mp3",
  "water": "water_drops.mp3", "water_drops": "water_drops.mp3", "water_splash": "water_splash.mp3",
  "water_turbulent": "water_splash.mp3",
  "silence": "silence.mp3", "silence_heavy": "silence.mp3",
  "breathing": "heartbeat.mp3", "breath": "heartbeat.mp3", "heartbeat": "heartbeat.mp3",
  "crowd": "alarm.mp3", "crowd_murmur": "alarm.mp3",
  "birds": "birds.mp3", "clock": "clock.mp3", "explosion": "explosion.mp3",
  "click": "lock.mp3", "hospital": "beep.mp3", "medical": "beep.mp3"
};

// Auto-detect: are real audio files available in audio_cues/?
let __hasRealFiles = false;
(function detectRealFiles() {
  const a = new Audio();
  a.src = "audio_cues/silence.mp3";
  a.addEventListener("canplaythrough", () => { __hasRealFiles = true; }, { once: true });
  a.addEventListener("error", () => { __hasRealFiles = false; }, { once: true });
  a.load();
  fetch("audio_cues/silence.mp3", { method: "HEAD" })
    .then(r => { if (r.ok) __hasRealFiles = true; })
    .catch(() => {});
})();

// Play a real audio file — plays its natural duration, stopped on page change
function playRealFile(category, callback) {
  const file = CATEGORY_FILE[category];
  if (!file) { callback(); return; }
  const audio = new Audio();
  audio.src = "audio_cues/" + file;
  audio.volume = 0.08;
  const cleanup = () => { rem(audio); callback(); };
  audio.addEventListener("ended", cleanup, { once: true });
  audio.addEventListener("error", cleanup, { once: true });
  audio.play().then(() => { add(audio); }).catch(() => { callback(); });
}

// ── Category dispatcher ──
const SOUND_MAP = {
  rain: (ctx) => playRain(ctx, 5, false),
  rain_light: (ctx) => playRain(ctx, 5, false),
  rain_heavy: (ctx) => playRain(ctx, 5, true),
  rain_window: (ctx) => playRain(ctx, 5, false),
  rain_distant: (ctx) => playRain(ctx, 4, false),
  rain_ambient: (ctx) => playRain(ctx, 6, false),
  rain_night: (ctx) => playRain(ctx, 5, false),
  rain_start: (ctx) => playRain(ctx, 3, false),
  rain_stop: () => {},
  wind: (ctx) => playWind(ctx, 5),
  wind_trees: (ctx) => playWind(ctx, 4),
  wind_harbor: (ctx) => playWind(ctx, 5),
  thunder: (ctx) => playThunder(ctx),
  door: (ctx) => playDoor(ctx, "open"),
  door_open: (ctx) => playDoor(ctx, "open"),
  door_close: (ctx) => playDoor(ctx, "close"),
  door_knock: (ctx) => playDoor(ctx, "knock"),
  door_wood: (ctx) => playDoor(ctx, "wood"),
  door_metal: (ctx) => playDoor(ctx, "open"),
  door_old: (ctx) => playDoor(ctx, "wood"),
  door_large: (ctx) => playDoor(ctx, "large"),
  door_heavy: (ctx) => playDoor(ctx, "heavy"),
  gate: (ctx) => playDoor(ctx, "wood"),
  lock: (ctx) => playClick(ctx),
  lock_old: (ctx) => playClick(ctx),
  lock_metal: (ctx) => playClick(ctx),
  key: (ctx) => playClick(ctx),
  key_lock: (ctx) => playClick(ctx),
  beep: (ctx) => playBeep(ctx, 1),
  beep_card: (ctx) => playBeep(ctx, 1),
  beep_medical: (ctx) => playBeep(ctx, 2),
  beep_elevator: (ctx) => playBeep(ctx, 1),
  beep_keypad: (ctx) => playBeep(ctx, 2),
  footsteps: (ctx) => playFootsteps(ctx, 3, "normal"),
  footsteps_slow: (ctx) => playFootsteps(ctx, 3, "slow"),
  footsteps_fast: (ctx) => playFootsteps(ctx, 4, "fast"),
  footsteps_stone: (ctx) => playFootsteps(ctx, 3, "normal"),
  footsteps_wood: (ctx) => playFootsteps(ctx, 3, "wood"),
  footsteps_heels: (ctx) => playFootsteps(ctx, 3, "heels"),
  running: (ctx) => playFootsteps(ctx, 6, "running"),
  stairs: (ctx) => playFootsteps(ctx, 5, "slow"),
  elevator: (ctx) => playElevator(ctx),
  alarm: (ctx) => playAlarm(ctx, false, 3),
  alarm_soft: (ctx) => playAlarm(ctx, true, 3),
  alarm_low: (ctx) => playAlarm(ctx, true, 3),
  alarm_distant: (ctx) => playAlarm(ctx, true, 2),
  gunshot: (ctx) => playGunshot(ctx, false),
  gunshot_close: (ctx) => playGunshot(ctx, true),
  impact: (ctx) => playImpact(ctx, false, false),
  impact_heavy: (ctx) => playImpact(ctx, true, false),
  impact_metal: (ctx) => playImpact(ctx, false, true),
  banging: (ctx) => playImpact(ctx, true, false),
  metal: (ctx) => playImpact(ctx, false, true),
  metal_bang: (ctx) => playImpact(ctx, true, true),
  glass: (ctx) => playGlassBreak(ctx),
  glass_break: (ctx) => playGlassBreak(ctx),
  glass_clink: (ctx) => playGlassClink(ctx),
  paper: (ctx) => playPaper(ctx),
  paper_rustle: (ctx) => playPaper(ctx),
  paper_turn: (ctx) => playPaperTurn(ctx),
  paper_open: (ctx) => playPaper(ctx),
  paper_close: (ctx) => playPaper(ctx),
  notebook: (ctx) => playPaperTurn(ctx),
  keyboard: (ctx) => playKeyboard(ctx, 4),
  computer: (ctx) => playKeyboard(ctx, 3),
  notification: (ctx) => playNotification(ctx),
  phone: (ctx) => playPhoneRing(ctx),
  phone_call: (ctx) => playPhoneRing(ctx),
  vibrate: (ctx) => playVibrate(ctx),
  fire: (ctx) => playFireCrackle(ctx, 5),
  fire_crackle: (ctx) => playFireCrackle(ctx, 5),
  candle: (ctx) => playFireCrackle(ctx, 3),
  engine: (ctx) => playEngine(ctx, 4, false),
  engine_idle: (ctx) => playEngine(ctx, 4, false),
  engine_drive: (ctx) => playEngine(ctx, 4, true),
  engine_slow: (ctx) => playEngine(ctx, 3, false),
  engine_stop: (ctx) => playEngine(ctx, 2, false),
  car: (ctx) => playEngine(ctx, 3, true),
  car_door: (ctx) => playDoor(ctx, "close"),
  car_drive: (ctx) => playEngine(ctx, 4, true),
  tires: (ctx) => playEngine(ctx, 3, false),
  brakes: (ctx) => playBrakes(ctx),
  brakes_squeal: (ctx) => playBrakes(ctx),
  wipers: (ctx) => playRain(ctx, 3, false),
  boat: (ctx) => playBoatHorn(ctx),
  foghorn: (ctx) => playBoatHorn(ctx),
  water: (ctx) => playWaterDrops(ctx, 3),
  water_drops: (ctx) => playWaterDrops(ctx, 3),
  water_splash: (ctx) => playWaterSplash(ctx),
  water_turbulent: (ctx) => playWaterSplash(ctx),
  silence: () => {},
  silence_heavy: () => {},
  breathing: (ctx) => playHeartbeat(ctx, 4, false),
  breath: (ctx) => playHeartbeat(ctx, 3, false),
  heartbeat: (ctx) => playHeartbeat(ctx, 5, false),
  crowd: (ctx) => playCrowd(ctx),
  crowd_murmur: (ctx) => playCrowd(ctx),
  birds: (ctx) => playBirds(ctx),
  clock: (ctx) => playClock(ctx, 4),
  explosion: (ctx) => playExplosion(ctx),
  mechanical: (ctx) => playClick(ctx),
  click: (ctx) => playClick(ctx),
  radio: (ctx) => playBeep(ctx, 1),
  hospital: (ctx) => playBeep(ctx, 2),
  medical: (ctx) => playBeep(ctx, 2),
  corridor: (ctx) => playFootsteps(ctx, 3, "normal"),
};

// ── Keyword → category matching ──
const KEYWORD_RULES = [
  [/explosion|explose|boom/i, "explosion"],
  [/orage|tonnerre|thunder/i, "thunder"],
  [/pluie.*forte|pluie.*dense|heavy.*rain|rain.*heavy/i, "rain_heavy"],
  [/pluie.*vitre|pluie.*pare.?brise|rain.*window|rain.*windshield/i, "rain_window"],
  [/pluie.*lointain|rain.*distant|pluie.*fine.*loin/i, "rain_distant"],
  [/pluie|rain|il pleut|goutte|averse/i, "rain"],
  [/vent.*arbre|wind.*tree/i, "wind_trees"],
  [/vent.*port|wind.*harbor/i, "wind_harbor"],
  [/vent|wind/i, "wind"],
  [/porte.*grince|door.*squeak|porte.*bois|door.*wood|porte.*ancien|door.*old/i, "door_wood"],
  [/porte.*métal|porte.*metal|door.*metal/i, "door_metal"],
  [/porte.*lourd|door.*heavy|porte.*grande|door.*large/i, "door_heavy"],
  [/porte.*ouvre|door.*open|porte.*s.ouvre|door.*open/i, "door_open"],
  [/porte.*ferm|door.*close/i, "door_close"],
  [/frappe.*porte|toque|door.*knock|knock/i, "door_knock"],
  [/portail|gate/i, "gate"],
  [/pas.*lent|pas.*pierre|footsteps.*stone|footsteps.*slow/i, "footsteps_slow"],
  [/pas.*bois|footsteps.*wood/i, "footsteps_wood"],
  [/talon|footsteps.*heel|heels/i, "footsteps_heels"],
  [/course|courent|running|footsteps.*fast/i, "footsteps_fast"],
  [/pas.*marche|stairs|escalier/i, "stairs"],
  [/pas|footstep|bruit.*pas/i, "footsteps"],
  [/serrure|lock|clef|clé/i, "lock"],
  [/clavier|keyboard|tape|typing/i, "keyboard"],
  [/alarme.*méd|alarm.*med|medical.*beep|hôpital|hospital/i, "hospital"],
  [/alarme.*doux|alarm.*soft/i, "alarm_soft"],
  [/alarme.*lointain|alarm.*distant/i, "alarm_distant"],
  [/alarme|alarm/i, "alarm"],
  [/coup.*feu|gunshot|gun.*shot|arme.*feu|détonat/i, "gunshot"],
  [/coup.*feu.*proche|gunshot.*close/i, "gunshot_close"],
  [/impact.*lourd|impact.*heavy|cognement|banging/i, "banging"],
  [/impact.*métal|metal.*impact/i, "impact_metal"],
  [/impact|choc/i, "impact"],
  [/métal|metalique|metal/i, "metal"],
  [/verre.*casse|glass.*break|bris.*verre/i, "glass_break"],
  [/verre.*tinte|glass.*clink|clink/i, "glass_clink"],
  [/papier.*tourne|paper.*turn|page.*tourne|notebook/i, "paper_turn"],
  [/papier|paper/i, "paper"],
  [/notification|notif/i, "notification"],
  [/téléphone|phone.*ring|sonne|téléphone/i, "phone"],
  [/vibreur|vibrate|vibration/i, "vibrate"],
  [/bip.*carte|beep.*card/i, "beep_card"],
  [/bip|beep/i, "beep"],
  [/feu.*bois|fire.*crackl|cheminée|fire/i, "fire"],
  [/bougie|candle/i, "candle"],
  [/moteur.*ralenti|engine.*idle/i, "engine_idle"],
  [/moteur.*accel|engine.*drive|moteur.*fort|engine.*accel/i, "engine_drive"],
  [/moteur.*éteint|moteur.*arrêt|engine.*stop|silence.*moteur/i, "engine_stop"],
  [/moteur.*lent|engine.*slow/i, "engine_slow"],
  [/moteur|engine/i, "engine"],
  [/portière|car.*door|car_door|voiture.*porte/i, "car_door"],
  [/pneu.*crisser|tires.*squeal|brakes.*squeal|frein.*crisser/i, "brakes"],
  [/frein|brakes/i, "brakes"],
  [/pneu|tires|roue/i, "tires"],
  [/essuie.?glace|wipers/i, "wipers"],
  [/voiture.*roule|car.*drive|voiture.*pluie/i, "car_drive"],
  [/voiture|car.*rain|vehicule/i, "car"],
  [/bateau|boat.*horn|corne.*brume|foghorn|portuaire|harbor/i, "foghorn"],
  [/sirène.*lointain|siren.*distant/i, "foghorn"],
  [/eau.*éclabousse|water.*splash|plouf|splash/i, "water_splash"],
  [/eau.*goutte|water.*drop|goutte.*eau|eau.*sombre|eau.*noir|eau.*souterr/i, "water_drops"],
  [/eau.*turbulent|water.*turbulent/i, "water_turbulent"],
  [/mer|sea.*wave|vague|océan|ocean/i, "water_splash"],
  [/eau|water/i, "water"],
  [/ascenseur|elevator/i, "elevator"],
  [/horloge|clock.*tic|tic.*tac|pendule|clock/i, "clock"],
  [/oiseau|birds|chant.*oiseau/i, "birds"],
  [/explosion|explose/i, "explosion"],
  [/foule|crowd.*murmur|murmure|voix.*lointain|bruit.*foule|crowd/i, "crowd"],
  [/souffle|respiration|breath|breathing|respire/i, "breathing"],
  [/coeur|heartbeat|cœur|battement.*cœur|pouls|heart/i, "breathing"],
  [/cri|scream/i, "alarm"],
  [/gifle|slap|claque/i, "impact"],
  [/silence.*lourd|silence.*heavy|silence.*long|silence.*total/i, "silence_heavy"],
  [/silence|silent|calme.*plat|calm/i, "silence"],
  [/bruit.*ambiant|ambiant|ambient|atmosphère/i, "silence"],
  [/click|clic/i, "click"],
];

function categoryFromText(text) {
  for (const [re, cat] of KEYWORD_RULES) {
    if (re.test(text)) return cat;
  }
  // fallback — try to match any word
  const words = text.toLowerCase().split(/[\s,;.]+/);
  for (const w of words) {
    if (SOUND_MAP[w]) return w;
  }
  return "silence";
}

// ── Public API ──

// Called on page entry: parse all .audio-cue divs and play their sounds
function playAudioCues() {
  const ctx = resumeCtx();
  if (!ctx) return;
  window.__ashfallStopAll();

  // Small delay to let React finish rendering
  const t0 = setTimeout(() => {
    const cues = document.querySelectorAll(".audio-cue");
    if (!cues.length) return;

    // Play sounds sequentially with stagger
    if (__hasRealFiles) {
      let delay = 0;
      cues.forEach(el => {
        const text = el.getAttribute("data-audio") || el.textContent || "";
        const cat = categoryFromText(text);
        const file = CATEGORY_FILE[cat];
        if (file && file !== "silence.wav") {
          const t = setTimeout(() => playRealFile(cat, () => {}), delay);
          timeouts.push(t);
          delay += 800;
        }
      });
    } else {
      // Fall back to Web Audio API synthesis
      let delay = 0;
      cues.forEach(el => {
        const text = el.getAttribute("data-audio") || el.textContent || "";
        const cat = categoryFromText(text);
        const fn = SOUND_MAP[cat];
        if (fn && cat !== "silence" && cat !== "silence_heavy") {
          const t = setTimeout(() => { try { fn(ctx); } catch (_) {} }, delay);
          timeouts.push(t);
          delay += 600;
        }
      });
    }
  }, 50);
  timeouts.push(t0);
}

// React hook: call playAudioCues on every page/screen change
const { useEffect } = React;
function useAudioCues(pageIdx) {
  useEffect(() => {
    playAudioCues();
  }, [pageIdx]);
}

// Register for cleanup
window.addEventListener("beforeunload", () => {
  window.__ashfallStopAll();
});

// Export public API
window.useAudioCues = useAudioCues;
window.playAudioCues = playAudioCues;
