/* ===== ELSA GAMBLING — motore del reveal ===== */
(function () {
  "use strict";

  /* ===== DEBUG / ANTEPRIMA EFFETTI =====================================
     Per vedere gli effetti senza aspettare le vincite jacuzzi casuali.
     Rimetti entrambi a null/false per il gioco normale. */
  // Forza un effetto shalom fisso: "fire" | "video" | "gesu" | null
  const FORCE_SHALOM = null;
  // Forza il risultato del giro su una stanza con idromassaggio (così l'effetto
  // parte a ogni giro). true = jacuzzi a caso; oppure l'id di una stanza
  // ("argo" | "eolo" | "rischio" | "zeus" | ...); false = risultato normale.
  const FORCE_RESULT = false;
  /* ==================================================================== */

  const $ = (sel) => document.querySelector(sel);
  const roomById = Object.fromEntries(ROOMS.map((r) => [r.id, r]));
  const genderOf = Object.fromEntries(
    PARTICIPANTS.map((p) => [p.nome, p.genere]),
  );
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  /* ---------- Audio sintetizzato (nessun file esterno) ---------- */
  const Audio = {
    ctx: null,
    master: null,
    on: true,
    init() {
      if (this.ctx) return;
      const AC = window.AudioContext || window.webkitAudioContext;
      this.ctx = new AC();
      this.master = this.ctx.createGain();
      this.master.gain.value = 0.5;
      this.master.connect(this.ctx.destination);
    },
    resume() {
      if (this.ctx && this.ctx.state === "suspended") this.ctx.resume();
    },
    blip(freq, dur = 0.05, type = "square", vol = 0.3, glideTo) {
      if (!this.on || !this.ctx) return;
      const t = this.ctx.currentTime;
      const o = this.ctx.createOscillator();
      const g = this.ctx.createGain();
      o.type = type;
      o.frequency.setValueAtTime(freq, t);
      if (glideTo) o.frequency.exponentialRampToValueAtTime(glideTo, t + dur);
      g.gain.setValueAtTime(vol, t);
      g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
      o.connect(g).connect(this.master);
      o.start(t);
      o.stop(t + dur + 0.02);
    },
    tick() {
      this.blip(1300, 0.03, "square", 0.12);
    },
    reelStop() {
      this.blip(210, 0.08, "square", 0.28);
      this.blip(120, 0.13, "sine", 0.2);
    },
    ding() {
      this.blip(1318, 0.5, "sine", 0.3); // E6
      this.blip(1568, 0.55, "sine", 0.25); // G6
    },
    coin() {
      this.blip(1760, 0.06, "square", 0.2);
      setTimeout(() => this.blip(2637, 0.12, "square", 0.2), 60);
    },
    buzz() {
      this.blip(140, 0.45, "sawtooth", 0.3, 70);
    },
    // sirena da allarme per il "nessun match"
    siren() {
      if (!this.on || !this.ctx) return;
      const t = this.ctx.currentTime;
      const o = this.ctx.createOscillator();
      const g = this.ctx.createGain();
      o.type = "sawtooth";
      const freqs = [880, 440, 880, 440, 880, 440, 880];
      freqs.forEach((f, i) => o.frequency.setValueAtTime(f, t + i * 0.16));
      g.gain.setValueAtTime(0.16, t);
      g.gain.setValueAtTime(0.16, t + 1.05);
      g.gain.exponentialRampToValueAtTime(0.0001, t + 1.2);
      o.connect(g).connect(this.master);
      o.start(t);
      o.stop(t + 1.25);
    },
    // nota di ottone per la fanfara
    brass(freq, when, dur, vol = 0.25) {
      if (!this.on || !this.ctx) return;
      const t = this.ctx.currentTime + when;
      const o = this.ctx.createOscillator();
      const o2 = this.ctx.createOscillator();
      const g = this.ctx.createGain();
      const lp = this.ctx.createBiquadFilter();
      o.type = "sawtooth";
      o2.type = "square";
      o.frequency.value = freq;
      o2.frequency.value = freq * 2;
      lp.type = "lowpass";
      lp.frequency.value = 2800;
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(vol, t + 0.03);
      g.gain.setValueAtTime(vol, t + dur * 0.65);
      g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
      o.connect(g);
      o2.connect(g);
      g.connect(lp).connect(this.master);
      o.start(t);
      o2.start(t);
      o.stop(t + dur + 0.02);
      o2.stop(t + dur + 0.02);
    },
    // trombe trionfali: "ta ta-ta-taaa"
    fanfare() {
      this.brass(392, 0.0, 0.12, 0.22); // G4
      this.brass(523, 0.14, 0.12, 0.24); // C5
      this.brass(659, 0.28, 0.12, 0.24); // E5
      this.brass(784, 0.42, 0.7, 0.3); // G5 tenuta
      for (let i = 0; i < 6; i++)
        setTimeout(
          () => this.blip(1568 + Math.random() * 700, 0.08, "sine", 0.1),
          520 + i * 60,
        );
    },
    jackpot() {
      const notes = [523, 659, 784, 1046, 1318, 1568];
      notes.forEach((f, i) =>
        setTimeout(() => this.blip(f, 0.35, "square", 0.28), i * 90),
      );
      // scintillio
      for (let i = 0; i < 10; i++)
        setTimeout(
          () => this.blip(1800 + Math.random() * 1400, 0.08, "sine", 0.12),
          600 + i * 70,
        );
    },
    // riser di tensione: ritorna una funzione stop()
    riser(duration) {
      if (!this.on || !this.ctx) return () => {};
      const t = this.ctx.currentTime;
      const o = this.ctx.createOscillator();
      const g = this.ctx.createGain();
      o.type = "sawtooth";
      o.frequency.setValueAtTime(180, t);
      o.frequency.exponentialRampToValueAtTime(900, t + duration);
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.12, t + duration * 0.7);
      g.gain.exponentialRampToValueAtTime(0.0001, t + duration + 0.1);
      const lp = this.ctx.createBiquadFilter();
      lp.type = "lowpass";
      lp.frequency.value = 1200;
      o.connect(g).connect(lp).connect(this.master);
      o.start(t);
      o.stop(t + duration + 0.2);
      return () => {
        try {
          g.gain.cancelScheduledValues(this.ctx.currentTime);
          g.gain.setTargetAtTime(0.0001, this.ctx.currentTime, 0.03);
        } catch (e) {}
      };
    },
  };

  /* ---------- Campioni reali stile slot (Mixkit, royalty-free) ----------
     File in /sounds. Usiamo HTMLAudioElement (non fetch+decode) così
     funziona anche aprendo l'app via file:// dove Chrome blocca fetch dei
     file locali. Rispetta il mute globale (Audio.on) e fa fallback ai suoni
     sintetizzati se un file non carica. */
  const Samples = {
    files: {
      reelSpin: "sounds/reel-spin.mp3", // whirr dei rulli durante il roll
      win: "sounds/win.mp3", // jingle di vincita (match normale)
      jackpot: "sounds/jackpot.mp3", // sirena di jackpot (idromassaggio)
      coins: "sounds/coins.mp3", // pioggia di monete
      shalom: "sounds/shalom.mp3", // musica dell'effetto "shalom" (vittoria jacuzzi)
      leone: "sounds/leone.m4a", // audio dell'effetto leone infuocato (AAC, compat. Safari)
      gesuSong: "sounds/gesu_song.mp3", // canzone dell'effetto "Gesù"
    },
    el: {},
    ready: {},
    preload() {
      Object.entries(this.files).forEach(([k, url]) => {
        const a = new window.Audio(url); // window.Audio: qui "Audio" è l'oggetto synth
        a.preload = "auto";
        a.addEventListener("canplaythrough", () => (this.ready[k] = true), {
          once: true,
        });
        a.addEventListener("error", () => (this.ready[k] = false));
        this.el[k] = a;
      });
    },
    // ritorna una funzione stop(fadeSec) per interromperlo (utile per il loop)
    play(key, opts = {}) {
      const { vol = 0.8, loop = false, fallback, gain } = opts;
      if (!Audio.on) return () => {};
      const base = this.el[key];
      if (!base || this.ready[key] === false) {
        if (fallback) fallback();
        return () => {};
      }
      const a = loop ? base : base.cloneNode(); // clone -> più play sovrapposti
      a.loop = loop;
      a.volume = vol;
      // Boost oltre il max (1.0) solo per QUESTO suono: instrada l'elemento in
      // Web Audio con un GainNode dedicato (non tocca il volume globale).
      if (gain && gain > 1 && Audio.ctx) {
        try {
          const src = Audio.ctx.createMediaElementSource(a);
          const g = Audio.ctx.createGain();
          g.gain.value = gain;
          src.connect(g).connect(Audio.ctx.destination);
        } catch (e) {}
      }
      try {
        a.currentTime = 0;
      } catch (e) {}
      const p = a.play();
      if (p && p.catch) p.catch(() => fallback && fallback());
      return (fade = 0.2) => {
        const steps = 8;
        let i = 0;
        const iv = setInterval(
          () => {
            i++;
            a.volume = Math.max(0, vol * (1 - i / steps));
            if (i >= steps) {
              clearInterval(iv);
              try {
                a.pause();
              } catch (e) {}
            }
          },
          (fade * 1000) / steps,
        );
      };
    },
  };
  Samples.preload();

  /* ---------- Coriandoli (canvas a schermo intero, dimensionato a mano) ---------- */
  // IMPORTANTE: niente useWorker + niente resize automatico. Dimensiono io il
  // canvas alla viewport, altrimenti il buffer resta 300x150 e i coriandoli
  // finiscono tutti ammassati nell'angolo in alto a sinistra.
  const confettiCanvas = $("#confetti");
  function sizeConfetti() {
    confettiCanvas.width = window.innerWidth;
    confettiCanvas.height = window.innerHeight;
  }
  sizeConfetti();
  window.addEventListener("resize", sizeConfetti);
  const myConfetti =
    typeof confetti !== "undefined"
      ? confetti.create(confettiCanvas, { resize: false, useWorker: false })
      : () => {};

  const CONFETTI_COLORS = [
    "#ffd23f",
    "#ff9e00",
    "#ff2e88",
    "#00e5ff",
    "#29e08a",
    "#ff3b5c",
    "#ffffff",
  ];

  // VALANGA di coriandoli su TUTTO lo schermo
  function bigBurst() {
    const C = CONFETTI_COLORS;
    // esplosione centrale gigante a 360°
    myConfetti({
      particleCount: 400,
      spread: 360,
      startVelocity: 45,
      scalar: 1.1,
      origin: { x: 0.5, y: 0.5 },
      colors: C,
    });
    // fila di esplosioni lungo TUTTA la larghezza
    for (let x = 0; x <= 1.0001; x += 0.1) {
      myConfetti({
        particleCount: 60,
        spread: 110,
        startVelocity: 55,
        origin: { x, y: 0.55 },
        colors: C,
      });
    }
    // cannoni laterali potenti dal basso
    myConfetti({
      particleCount: 250,
      angle: 60,
      spread: 120,
      startVelocity: 75,
      origin: { x: 0, y: 0.85 },
      colors: C,
    });
    myConfetti({
      particleCount: 250,
      angle: 120,
      spread: 120,
      startVelocity: 75,
      origin: { x: 1, y: 0.85 },
      colors: C,
    });
    // pioggia dall'alto su tutta la larghezza
    for (let x = 0; x <= 1.0001; x += 0.1) {
      myConfetti({
        particleCount: 40,
        spread: 160,
        startVelocity: 35,
        gravity: 1,
        origin: { x, y: -0.1 },
        colors: C,
      });
    }
  }

  // pioggia continua a schermo pieno per N ms
  function confettiRain(durationMs) {
    const end = Date.now() + durationMs;
    (function frame() {
      for (let x = 0; x <= 1.0001; x += 0.12) {
        myConfetti({
          particleCount: 10,
          spread: 130,
          startVelocity: 45,
          gravity: 1,
          origin: { x, y: -0.05 },
          colors: CONFETTI_COLORS,
        });
      }
      if (Date.now() < end) requestAnimationFrame(frame);
    })();
  }

  function celebrate(kind) {
    bigBurst();
    // seconda ondata immediata per raddoppiare la densità
    setTimeout(bigBurst, 180);
    confettiRain(kind === "jackpot" ? 3500 : 1400);
  }

  function screenShake() {
    const app = $("#app");
    app.classList.remove("shake");
    void app.offsetWidth;
    app.classList.add("shake");
  }
  function screenFlash() {
    let f = $(".flash");
    if (!f) {
      f = document.createElement("div");
      f.className = "flash";
      document.body.appendChild(f);
    }
    f.classList.remove("go");
    void f.offsetWidth;
    f.classList.add("go");
  }

  // EFFETTO PAZZO quando i rulli non combaciano: sirena + strobo + glitch + timbro
  function crazyNoMatch() {
    // buzz di errore "sbagliato!" (doppio, stile game-show) prima del caos
    Audio.buzz();
    setTimeout(() => Audio.buzz(), 180);
    setTimeout(() => Audio.siren(), 320); // la sirena entra dopo il buzz
    screenShake();

    // strobo a schermo intero
    let strobe = $(".strobe");
    if (!strobe) {
      strobe = document.createElement("div");
      strobe.className = "strobe";
      document.body.appendChild(strobe);
    }
    strobe.classList.remove("go");
    void strobe.offsetWidth;
    strobe.classList.add("go");

    // glitch sui rulli
    const win = $(".reel-window");
    win.classList.add("glitch");
    setTimeout(() => win.classList.remove("glitch"), 900);

    // timbro "NESSUN MATCH"
    const stamp = document.createElement("div");
    stamp.className = "nomatch-stamp";
    stamp.innerHTML = "<span>❌ NO MATCH ❌</span>";
    document.body.appendChild(stamp);
    setTimeout(() => stamp.remove(), 1300);

    // coriandoli "sbagliati" (solo rossi) per rincarare la beffa
    myConfetti({
      particleCount: 120,
      spread: 180,
      startVelocity: 40,
      origin: { y: 0.4 },
      colors: ["#ff3b5c", "#8b0022"],
    });
  }

  /* ---------- Fuoco a particelle (canvas 2D additivo) ----------
     Un vero falò: particelle emesse dal bordo inferiore che salgono, si
     allargano e sfumano, colorate dal nucleo bianco-giallo alla brace rossa.
     Blending additivo ("lighter") + sprite pre-renderizzati = fluido e realistico. */
  const Fire = {
    canvas: null,
    ctx: null,
    parts: [],
    sprites: null,
    raf: 0,
    running: false,
    w: 0,
    h: 0,
    last: 0,
    acc: 0,
    // palette giovane -> vecchio (nucleo caldo verso brace scura)
    PALETTE: [
      [255, 255, 235],
      [255, 244, 170],
      [255, 200, 70],
      [255, 130, 20],
      [225, 65, 15],
      [130, 35, 18],
    ],
    buildSprites() {
      this.sprites = this.PALETTE.map(([r, g, b]) => {
        const s = document.createElement("canvas");
        s.width = s.height = 64;
        const c = s.getContext("2d");
        const grad = c.createRadialGradient(32, 32, 0, 32, 32, 32);
        grad.addColorStop(0, `rgba(${r},${g},${b},1)`);
        grad.addColorStop(0.32, `rgba(${r},${g},${b},0.55)`);
        grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
        c.fillStyle = grad;
        c.fillRect(0, 0, 64, 64);
        return s;
      });
    },
    ensure() {
      if (!this.canvas) {
        this.canvas = document.querySelector(".shalom-fire");
        this.ctx = this.canvas.getContext("2d");
      }
      if (!this.sprites) this.buildSprites();
    },
    resize() {
      this.w = this.canvas.width = window.innerWidth;
      this.h = this.canvas.height = window.innerHeight;
    },
    spawn(n) {
      for (let i = 0; i < n; i++) {
        // più denso al centro (sotto il leone): due gaussiane sommate
        const bias = (Math.random() + Math.random()) / 2;
        const x = bias * this.w;
        const life = 700 + Math.random() * 1200;
        this.parts.push({
          x,
          y: this.h + 12,
          vx: (Math.random() - 0.5) * 36,
          vy: -(190 + Math.random() * 240),
          life,
          max: life,
          size: 15 + Math.random() * 30,
          wob: Math.random() * 6.283,
          wobSpeed: 4 + Math.random() * 6,
        });
      }
    },
    start() {
      this.ensure();
      this.resize();
      this.parts.length = 0;
      this.last = 0;
      this.acc = 0;
      this.running = true;
      const loop = (t) => {
        if (!this.running) return;
        if (!this.last) this.last = t;
        let dt = (t - this.last) / 1000;
        this.last = t;
        if (dt > 0.05) dt = 0.05; // niente salti dopo un lag
        this.acc += dt * 900; // ~900 particelle/sec
        const n = Math.floor(this.acc);
        this.acc -= n;
        this.spawn(n);
        const ctx = this.ctx;
        ctx.clearRect(0, 0, this.w, this.h);
        ctx.globalCompositeOperation = "lighter";
        for (let i = this.parts.length - 1; i >= 0; i--) {
          const p = this.parts[i];
          p.life -= dt * 1000;
          if (p.life <= 0) {
            this.parts.splice(i, 1);
            continue;
          }
          const k = p.life / p.max; // 1 -> 0
          p.vy -= 70 * dt; // il calore accelera verso l'alto
          p.wob += dt * p.wobSpeed;
          p.x += (p.vx + Math.sin(p.wob) * 34) * dt;
          p.y += p.vy * dt;
          const age = 1 - k; // 0 -> 1
          const idx = Math.min(
            this.sprites.length - 1,
            (age * this.sprites.length) | 0,
          );
          const size = p.size * (0.5 + k * 0.85);
          const hs = size * (1.5 + (1 - k) * 1.4); // stiro verticale -> lingua di fuoco
          ctx.globalAlpha = Math.min(1, k * 1.6) * 0.28;
          ctx.drawImage(
            this.sprites[idx],
            p.x - size,
            p.y - hs * 1.4,
            size * 2,
            hs * 2,
          );
        }
        ctx.globalAlpha = 1;
        this.raf = requestAnimationFrame(loop);
      };
      this.raf = requestAnimationFrame(loop);
    },
    stop() {
      this.running = false;
      cancelAnimationFrame(this.raf);
      if (this.ctx) this.ctx.clearRect(0, 0, this.w, this.h);
      this.parts.length = 0;
    },
  };

  /* ---------- Effetto SHALOM (vittoria stanze con idromassaggio) ---------- */
  let shalomOverlay = null;
  function ensureShalomOverlay() {
    if (shalomOverlay) return shalomOverlay;
    const o = document.createElement("div");
    o.className = "shalom-overlay";
    o.innerHTML =
      '<div class="shalom-flames left"></div>' +
      '<div class="shalom-flames right"></div>' +
      '<canvas class="shalom-fire"></canvas>' +
      '<img class="shalom-lion" src="image/lion.jpg" alt="" />' +
      '<div class="shalom-rays"></div>' +
      '<img class="shalom-gesu" src="image/gesu.jpeg" alt="" />' +
      '<video class="shalom-video" src="video/videoplayback.mp4" muted playsinline preload="auto"></video>' +
      '<div class="shalom-skip">tap per saltare ▸</div>';
    document.body.appendChild(o);
    shalomOverlay = o;
    return o;
  }
  function showShalom(o, mode) {
    o.classList.remove("mode-fire", "mode-video");
    o.classList.add("on", mode);
  }
  function hideShalom(o) {
    const v = o.querySelector(".shalom-video");
    try {
      v.pause();
    } catch (e) {}
    Fire.stop();
    o.classList.remove("on", "mode-fire", "mode-video");
  }
  // Attende: fine video, tetto di tempo, oppure tap sull'overlay (skip).
  function shalomWait(v, capMs, o) {
    return new Promise((resolve) => {
      let done = false;
      const finish = () => {
        if (done) return;
        done = true;
        clearTimeout(to);
        if (v) v.removeEventListener("ended", finish);
        o.removeEventListener("pointerdown", finish);
        resolve();
      };
      const to = setTimeout(finish, capMs);
      if (v) v.addEventListener("ended", finish);
      o.addEventListener("pointerdown", finish);
    });
  }

  // EFFETTO 1: fiammate dai due angoli in basso + leone (flashbang) ~3.5s
  async function shalomFireLion() {
    const o = ensureShalomOverlay();
    showShalom(o, "mode-fire");
    Fire.start();
    screenShake();
    const stop = Samples.play("leone", {
      vol: 1.0,
      gain: 2.5, // ruggito più forte del resto (boost dedicato)
      fallback: () => Audio.jackpot(),
    });
    await shalomWait(null, 3500, o);
    if (stop) stop(0.4);
    hideShalom(o);
  }

  // EFFETTO 2: video a schermo intero con la musica shalom
  async function shalomVideoOnly() {
    const o = ensureShalomOverlay();
    const v = o.querySelector(".shalom-video");
    showShalom(o, "mode-video");
    v.style.visibility = "hidden"; // primi 3s: solo musica, video nascosto
    const stop = Samples.play("shalom", {
      vol: 0.9,
      fallback: () => Audio.jackpot(),
    });
    await sleep(3000); // dopo 3s il video appare E parte insieme
    v.style.visibility = "";
    try {
      v.currentTime = 0;
      await v.play();
    } catch (e) {}
    await shalomWait(v, 17000, o); // 3s musica + 17s video = 20s (o tap per skippare)
    if (stop) stop(0.4);
    hideShalom(o);
  }

  // EFFETTO 3: Gesù scende dall'alto con luce evangelica + canzone
  async function shalomGesu() {
    const o = ensureShalomOverlay();
    showShalom(o, "mode-gesu");
    const stop = Samples.play("gesuSong", {
      vol: 0.9,
      fallback: () => Audio.fanfare(),
    });
    await shalomWait(null, 14000, o);
    if (stop) stop(0.5);
    hideShalom(o);
  }

  const SHALOM_EFFECTS = [shalomFireLion, shalomVideoOnly, shalomGesu];
  const SHALOM_BY_NAME = {
    fire: shalomFireLion,
    video: shalomVideoOnly,
    gesu: shalomGesu,
  };
  // "sacchetto" mescolato: a ogni vittoria jacuzzi esce un effetto DIVERSO
  // (senza ripetizioni) finché non sono usciti tutti, poi si rimescola.
  let shalomBag = [];
  function playShalom() {
    if (FORCE_SHALOM && SHALOM_BY_NAME[FORCE_SHALOM]) {
      return SHALOM_BY_NAME[FORCE_SHALOM](); // effetto fisso (debug)
    }
    if (shalomBag.length === 0) {
      shalomBag = Matching.shuffle(SHALOM_EFFECTS.slice(), Math.random);
    }
    const fx = shalomBag.shift();
    return fx();
  }

  /* ---------- Sfondo three.js: pioggia di monete ---------- */
  function initBackground() {
    if (typeof THREE === "undefined") return;
    const canvas = $("#bg3d");
    let renderer;
    try {
      renderer = new THREE.WebGLRenderer({
        canvas,
        antialias: true,
        alpha: true,
      });
    } catch (e) {
      return; // WebGL non disponibile: l'app funziona lo stesso senza sfondo 3D
    }
    renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(70, 1, 0.1, 100);
    camera.position.z = 30;

    // texture moneta disegnata su canvas
    const tex = (() => {
      const c = document.createElement("canvas");
      c.width = c.height = 64;
      const g = c.getContext("2d");
      const grad = g.createRadialGradient(24, 20, 4, 32, 32, 30);
      grad.addColorStop(0, "#fff6cf");
      grad.addColorStop(0.4, "#ffd23f");
      grad.addColorStop(1, "#b36b00");
      g.fillStyle = grad;
      g.beginPath();
      g.arc(32, 32, 28, 0, Math.PI * 2);
      g.fill();
      g.fillStyle = "rgba(179,107,0,0.9)";
      g.font = "bold 30px Georgia";
      g.textAlign = "center";
      g.textBaseline = "middle";
      g.fillText("€", 32, 34);
      return new THREE.CanvasTexture(c);
    })();

    const N = 520;
    const geo = new THREE.BufferGeometry();
    const pos = new Float32Array(N * 3);
    const speed = new Float32Array(N);
    for (let i = 0; i < N; i++) {
      pos[i * 3] = (Math.random() - 0.5) * 80;
      pos[i * 3 + 1] = (Math.random() - 0.5) * 80;
      pos[i * 3 + 2] = (Math.random() - 0.5) * 40;
      speed[i] = 6 + Math.random() * 12;
    }
    geo.setAttribute("position", new THREE.BufferAttribute(pos, 3));
    const mat = new THREE.PointsMaterial({
      size: 1.7,
      map: tex,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    });
    const points = new THREE.Points(geo, mat);
    scene.add(points);

    function resize() {
      const w = innerWidth,
        h = innerHeight;
      renderer.setSize(w, h, false);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
    }
    addEventListener("resize", resize);
    resize();

    let last = performance.now();
    let boost = 0;
    window.__coinBoost = () => (boost = 1.6);
    (function loop(now) {
      const dt = Math.min((now - last) / 1000, 0.05);
      last = now;
      const arr = geo.attributes.position.array;
      const mul = 1 + boost;
      for (let i = 0; i < N; i++) {
        arr[i * 3 + 1] -= speed[i] * dt * mul;
        if (arr[i * 3 + 1] < -40) arr[i * 3 + 1] = 40;
      }
      geo.attributes.position.needsUpdate = true;
      points.rotation.y += 0.02 * dt;
      boost *= 0.94;
      renderer.render(scene, camera);
      requestAnimationFrame(loop);
    })(last);
  }

  /* ---------- Rulli slot (3 colonne) ---------- */
  const ITEM_H = 88;
  const reelEls = [$("#reel0"), $("#reel1"), $("#reel2")];

  // Stanze FANTASMA: scorrono nei rulli solo per gaslightare, non escono MAI
  // come risultato (né nel match né nel no-match). Al massimo compaiono in un
  // finto stop, poi scattano via.
  const FAKE_ROOMS = [
    "Piscina",
    "Nel baule di una macchina",
    "QG di Allianz",
    "Cucina",
    "Bagno",
    "Garlasco",
    "Predappio",
    "Backrooms",
  ].map((nome) => ({ nome, fake: true }));
  const FAKES_PER_LOOP = 3; // costante -> indice target uguale su tutti i rulli

  function reelItemHTML(entry) {
    if (entry.fake) {
      return `<div class="reel-item fake" data-fake="1"><span>${entry.nome}</span></div>`;
    }
    const jz = entry.jacuzzi ? " jz" : "";
    const icon = entry.jacuzzi ? '<span class="jz-badge">🛁</span>' : "";
    return `<div class="reel-item${jz}" data-room="${entry.id}">${icon}<span>${entry.nome}</span></div>`;
  }

  // Costruisce la striscia di un rullo che termina sulla stanza target
  // (centrata nella riga di mezzo); ritorna l'indice target. Ogni "giro" della
  // sequenza contiene le stanze reali + alcune fantasma (mischiate).
  function buildStripIn(el, targetId) {
    el.getAnimations().forEach((a) => a.cancel());
    const loops = 12;
    const seq = [];
    for (let l = 0; l < loops; l++) {
      const fakes = Matching.shuffle(FAKE_ROOMS, Math.random).slice(
        0,
        FAKES_PER_LOOP,
      );
      const chunk = Matching.shuffle([...ROOMS, ...fakes], Math.random);
      seq.push(...chunk);
    }
    const targetIndex = seq.length; // la target (reale) va alla payline
    seq.push(roomById[targetId]);
    seq.push(ROOMS[0], ROOMS[1]); // riempi sotto la finestra (stanze reali)
    el.innerHTML = seq.map(reelItemHTML).join("");
    el.style.transform = "translateY(0)";
    return targetIndex;
  }

  // stato a riposo: mostra le stanze reali ferme invece di rulli vuoti
  function fillReelsIdle() {
    reelEls.forEach((el, i) => {
      el.getAnimations().forEach((a) => a.cancel());
      el.innerHTML = ROOMS.map(reelItemHTML).join("");
      const k = i + 1; // centra una stanza diversa per rullo
      el.style.transform = `translateY(${-(k - 1) * ITEM_H}px)`;
    });
  }

  function currentIndexOf(el) {
    const t = getComputedStyle(el).transform;
    if (!t || t === "none") return 0;
    const y = parseFloat(t.split(",")[5]); // matrix(a,b,c,d,e,f) -> f = translateY
    return Math.round(-y / ITEM_H);
  }

  // stanza reale a un indice (null se è una fantasma o non esiste)
  const roomAtIndex = (el, i) => {
    const c = el.children[i];
    return c && c.dataset.room ? roomById[c.dataset.room] : null;
  };
  // nome mostrato a un indice (reale o fantasma)
  const nameAtIndex = (el, i) => {
    const c = el.children[i];
    if (!c) return "";
    return c.dataset.room
      ? roomById[c.dataset.room].nome
      : c.textContent.trim();
  };

  // Probabilità di ragebait (finto stop) per colonna.
  const TEASE_CHANCE = 0.3;
  // Probabilità che il giro NON dia match (3 stanze diverse) -> respin.
  const NOMATCH_CHANCE = 0.3;

  // 3 stanze DIVERSE per un "nessun match" (mai una linea vincente)
  function pickMismatch() {
    return Matching.shuffle(
      ROOMS.map((r) => r.id),
      Math.random,
    ).slice(0, 3);
  }

  // Keyframe di decelerazione con coda lenta LUNGA a parità di durata totale:
  // ~88% della distanza nel primo ~40% del tempo (veloce), poi l'ultimo 12%
  // nel restante ~60% (crawl lento, così si leggono i nomi ~2s in più).
  function decelKeyframes(targetY) {
    return [
      { transform: "translateY(0)", easing: "cubic-bezier(0.17, 0.7, 0.4, 1)" },
      {
        transform: `translateY(${targetY * 0.88}px)`,
        offset: 0.4,
        easing: "cubic-bezier(0.25, 0.1, 0.2, 1)",
      },
      { transform: `translateY(${targetY}px)`, offset: 1 },
    ];
  }

  function bounceReel(el, finalY) {
    return el.animate(
      [
        { transform: `translateY(${finalY}px)` },
        { transform: `translateY(${finalY + 12}px)` },
        { transform: `translateY(${finalY}px)` },
      ],
      { duration: 220, easing: "ease-out", fill: "forwards" },
    ).finished;
  }

  // Atterraggio di un rullo sulla target. Se teaseMs > 0 fa il RAGEBAIT:
  // si ferma su una stanza adiacente sbagliata, tiene la suspense (teaseMs),
  // poi scatta sulla vera. Funziona su QUALSIASI colonna.
  async function reelLand(el, targetIndex, finalY, duration, teaseMs) {
    // eventuale bluff: fermati su una casella adiacente diversa dalla vera
    // (può essere anche una stanza FANTASMA -> gaslighting massimo)
    if (teaseMs > 0) {
      const targetRoom = roomAtIndex(el, targetIndex);
      // true se l'adiacente è proprio la stanza vera (da evitare come finta)
      const adjIsTarget = (d) =>
        roomAtIndex(el, targetIndex + d)?.id === targetRoom.id;
      let dir = Math.random() < 0.5 ? -1 : 1;
      if (adjIsTarget(dir)) dir = -dir;
      if (adjIsTarget(dir)) teaseMs = 0; // entrambe le adiacenti sono la target
      if (teaseMs > 0) {
        const fakeName = nameAtIndex(el, targetIndex + dir);
        const fakeY = -(targetIndex + dir - 1) * ITEM_H;
        const col = el.parentElement;

        // decelera lento e "si ferma" sulla stanza SBAGLIATA
        await el.animate(decelKeyframes(fakeY), {
          duration,
          fill: "forwards",
        }).finished;
        Audio.reelStop();

        // suspense su quella colonna
        Audio.blip(330, 0.13, "sine", 0.14);
        col.classList.add("teasing");
        mc(`${fakeName}...? 🤨`);
        await sleep(teaseMs);

        // SCATTO alla stanza vera
        Audio.blip(200, 0.12, "sawtooth", 0.2, 90); // whoosh
        await el.animate(
          [
            { transform: `translateY(${fakeY}px)` },
            { transform: `translateY(${finalY}px)` },
          ],
          {
            duration: 480,
            easing: "cubic-bezier(0.34, 1.56, 0.64, 1)",
            fill: "forwards",
          },
        ).finished;
        col.classList.remove("teasing");
        Audio.reelStop();
        return;
      }
    }
    // atterraggio diretto (coda lenta lunga) + micro-assestamento
    await el.animate(decelKeyframes(finalY), {
      duration,
      fill: "forwards",
    }).finished;
    await bounceReel(el, finalY);
    Audio.reelStop();
  }

  // GIRO: i 3 rulli partono insieme e si fermano da sinistra a destra,
  // allineandosi sulla stanza assegnata. OGNI colonna può fare il ragebait
  // (in modo indipendente); le colonne più a destra tengono la suspense più a
  // lungo, così il dramma "cascata" verso l'ultimo rullo.
  async function spinReel(reelTargets, opts) {
    const tease = !opts || opts.tease !== false;
    // ogni rullo può atterrare su una stanza diversa (per il "nessun match")
    const targetIndex = reelEls.map((el, i) =>
      buildStripIn(el, reelTargets[i]),
    )[0];
    const finalY = -(targetIndex - 1) * ITEM_H;
    $("#reel-result").innerHTML = "&nbsp;";

    // ZOOM + shake; lo shake si ferma presto, così la parte lenta è calma e
    // leggibile mentre i nomi scorrono piano
    zoomMachineIn();
    const mainDur = 6400;
    setTimeout(zoomMachineSteady, 2500);

    const stopRiser = Audio.riser(7.0);
    // whirr reale dei rulli sotto ai tick/riser sintetizzati
    const stopWheel = Samples.play("reelSpin", { vol: 0.65 });

    // tick sincronizzati sul 3° rullo (gira più a lungo)
    let ticking = true;
    let lastIdx = 0;
    (function loop() {
      if (!ticking) return;
      const i = currentIndexOf(reelEls[2]);
      if (i !== lastIdx) {
        Audio.tick();
        lastIdx = i;
      }
      requestAnimationFrame(loop);
    })();

    const durs = [4200, 5300, 6400];
    await Promise.all(
      reelEls.map((el, i) => {
        // ogni colonna ha una sua chance di bluff; suspense crescente verso destra
        const teaseMs =
          tease && Math.random() < TEASE_CHANCE ? 380 + i * 260 : 0;
        return reelLand(el, targetIndex, finalY, durs[i], teaseMs);
      }),
    );

    ticking = false;
    stopRiser();
    stopWheel(0.25); // taglia il whirr quando l'ultimo rullo atterra
    zoomMachineSteady();
  }

  /* ---------- Zoom macchina ---------- */
  let _shakeTimer = null;
  function zoomMachineIn() {
    const slot = $(".slot");
    slot.getAnimations().forEach((a) => a.cancel()); // pulisce un eventuale ritorno in corso
    slot.classList.add("zoom");
    clearTimeout(_shakeTimer);
    _shakeTimer = setTimeout(() => slot.classList.add("shake"), 460); // shake dopo lo zoom-in
  }
  function zoomMachineSteady() {
    clearTimeout(_shakeTimer);
    $(".slot").classList.remove("shake"); // resta zoomata, ma ferma
  }
  function zoomMachineOut() {
    const slot = $(".slot");
    clearTimeout(_shakeTimer);
    slot.classList.remove("shake");
    slot.classList.remove("zoom");
    // mini-animazione a molla per tornare al posto (leggero rimbalzo sotto l'1)
    slot.animate(
      [
        { transform: "scale(1.14)" },
        { transform: "scale(0.985)" },
        { transform: "scale(1)" },
      ],
      { duration: 620, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)" },
    );
  }

  /* ---------- Stato di gioco ---------- */
  let assignment = null; // {byPerson, roomGender, fRooms, mRooms}
  let revealQueue = []; // nomi in ordine di reveal
  let occupants = {}; // roomId -> [nomi] (svelati finora)
  let cursor = 0;
  let auto = false;
  let busy = false;
  let leverArmed = false;
  let noMatchStreak = 0; // no-match consecutivi (tetto per non incastrare)

  // scommettitori (TU + banco simulato)
  let players = [];
  let currentBet = null; // {roomId}

  function buildRevealOrder() {
    // ordine completamente casuale: ogni persona (e quindi ogni stanza, anche
    // quelle con idromassaggio) può uscire in qualsiasi momento
    return Matching.shuffle(
      PARTICIPANTS.map((p) => p.nome),
      Math.random,
    );
  }

  function newGame(keepChips) {
    assignment = Matching.assign(ROOMS, PARTICIPANTS);
    revealQueue = buildRevealOrder();
    occupants = {};
    ROOMS.forEach((r) => (occupants[r.id] = []));
    cursor = 0;
    currentBet = null;
    noMatchStreak = 0;
    shalomBag = [];
    if (!keepChips) {
      players = [
        { name: "TU", chips: 100, you: true },
        { name: "Il Banco", chips: 100 },
        { name: "Madama Fortuna", chips: 100 },
        { name: "Jinx", chips: 100 },
      ];
    }
    renderRooms();
    renderLeaderboard();
    setProgress();
    fillReelsIdle();
    $("#reel-result").innerHTML = "&nbsp;";
    $("#player-name").textContent = "—";
    $("#odds").innerHTML = "";
    mc("Tira giù la leva per far salire il primo! ⬇️");
    // il bottone principale serve solo per RIGIOCA a fine partita
    $("#btn-primary").style.display = "none";
    $("#btn-primary").onclick = onPrimary;
    armLever(true);
  }

  /* ---------- Rendering ---------- */
  function renderRooms() {
    const board = $("#rooms-board");
    board.innerHTML = ROOMS.map((r) => {
      const g = assignment.roomGender[r.id];
      const occ = occupants[r.id];
      const chips = occ
        .map((n) => `<span class="occ-chip">${n}</span>`)
        .join("");
      const emptyCount = r.posti - occ.length;
      const empties = Array.from({ length: emptyCount })
        .map(() => `<span class="occ-slot-empty"></span>`)
        .join("");
      return `
        <div class="room-card gender-${g} ${r.jacuzzi ? "jacuzzi" : ""}" data-room="${r.id}">
          <div class="room-head">
            <span class="room-name">${r.jacuzzi ? "🛁 " : ""}${r.nome}</span>
            <span class="room-cap">${occ.length}/${r.posti} · ${g}</span>
          </div>
          <div class="room-occupants">${chips}${empties}</div>
        </div>`;
    }).join("");
  }

  function renderLeaderboard() {
    const sorted = players.slice().sort((a, b) => b.chips - a.chips);
    $("#leaderboard").innerHTML = sorted
      .map(
        (p) =>
          `<div class="lb-row ${p.you ? "you" : ""}"><span>${p.you ? "⭐ " : ""}${p.name}</span><span class="lb-chips">${p.chips}</span></div>`,
      )
      .join("");
    $("#balance").textContent = players.find((p) => p.you).chips;
  }

  function setProgress() {
    $("#progress").textContent = `${cursor} / ${revealQueue.length}`;
  }

  function mc(text) {
    $("#mc").textContent = text;
  }

  function log(text, cls) {
    const e = document.createElement("div");
    e.className = "log-entry " + (cls || "");
    e.textContent = text;
    $("#log").prepend(e);
  }

  /* ---------- Scommesse ---------- */
  // Quote cosmetiche: più letti liberi in una stanza => più probabile => quota bassa.
  function computeOdds(person) {
    const g = genderOf[person];
    const rooms = ROOMS.filter(
      (r) =>
        assignment.roomGender[r.id] === g && occupants[r.id].length < r.posti,
    );
    const freeTotal = rooms.reduce(
      (s, r) => s + (r.posti - occupants[r.id].length),
      0,
    );
    return rooms.map((r) => {
      const free = r.posti - occupants[r.id].length;
      let mult = freeTotal / free;
      mult = Math.max(1.3, Math.round(mult * 10) / 10);
      return { room: r, mult };
    });
  }

  function renderOdds(person, odds) {
    const hint = currentBet
      ? `Hai puntato 10 gettoni su <b>${roomById[currentBet.roomId].nome}</b>`
      : "Clicca una stanza per puntare 10 gettoni (o SPINNA)";
    $("#odds").innerHTML =
      `<div class="bet-hint">${hint}</div>` +
      odds
        .map(
          (o) =>
            `<div class="odd-row ${currentBet && currentBet.roomId === o.room.id ? "picked" : ""}" data-bet="${o.room.id}">
               <span class="odd-name">${o.room.jacuzzi ? "🛁 " : ""}${o.room.nome}</span>
               <span class="odd-mult">x${o.mult.toFixed(1)}</span>
             </div>`,
        )
        .join("");
    $("#odds")
      .querySelectorAll(".odd-row")
      .forEach((row) =>
        row.addEventListener("click", () => {
          if (busy) return;
          const rid = row.getAttribute("data-bet");
          currentBet = { roomId: rid };
          Audio.coin();
          renderOdds(person, odds);
        }),
      );
  }

  // I bot puntano sul favorito (quota più bassa) o a caso.
  function botsPlace(odds) {
    players
      .filter((p) => !p.you)
      .forEach((p) => {
        if (p.chips < 10) {
          p._bet = null;
          return;
        }
        const pick =
          Math.random() < 0.6
            ? odds.slice().sort((a, b) => a.mult - b.mult)[0]
            : odds[Math.floor(Math.random() * odds.length)];
        p._bet = pick ? { roomId: pick.room.id, mult: pick.mult } : null;
      });
  }

  function resolveBets(person, landedRoomId, oddsMap) {
    // TU
    const you = players.find((p) => p.you);
    if (currentBet) {
      const o = oddsMap[currentBet.roomId];
      if (currentBet.roomId === landedRoomId) {
        const win = Math.round(10 * o);
        you.chips += win - 10;
        log(
          `Hai vinto ${win} gettoni! (${roomById[landedRoomId].nome})`,
          "win",
        );
      } else {
        you.chips -= 10;
        log(`Hai perso la puntata su ${roomById[currentBet.roomId].nome}.`);
      }
    }
    // bot
    players
      .filter((p) => !p.you && p._bet)
      .forEach((p) => {
        if (p._bet.roomId === landedRoomId)
          p.chips += Math.round(10 * p._bet.mult) - 10;
        else p.chips -= 10;
      });
  }

  /* ---------- Turno di reveal ---------- */
  async function nextTurn() {
    if (busy) return;
    if (cursor >= revealQueue.length) {
      endGame();
      return;
    }
    busy = true;
    armLever(false);
    Audio.init();
    Audio.resume();

    const person = revealQueue[cursor];
    let roomId = assignment.byPerson[person];
    if (FORCE_RESULT) {
      // debug: forza il risultato su una jacuzzi (o su un id specifico)
      const jac = ROOMS.filter((r) => r.jacuzzi);
      roomId =
        typeof FORCE_RESULT === "string"
          ? FORCE_RESULT
          : jac[Math.floor(Math.random() * jac.length)].id;
    }
    const room = roomById[roomId];

    $("#player-name").textContent = person;
    highlightRoom(null);

    // fase scommesse
    const odds = computeOdds(person);
    const oddsMap = Object.fromEntries(odds.map((o) => [o.room.id, o.mult]));
    renderOdds(person, odds);
    botsPlace(odds);
    mc(`Tocca a ${person}! Dove finirà? 🎲`);

    // animazione leva + boost monete
    $("#lever").classList.add("pull");
    setTimeout(() => $("#lever").classList.remove("pull"), 320);
    if (window.__coinBoost) window.__coinBoost();

    // 30% dei giri: NESSUN MATCH (3 stanze diverse) -> effetto pazzo + respin.
    // Tetto di 2 no-match di fila per non bloccare nessuno.
    const noMatch =
      !FORCE_RESULT && noMatchStreak < 2 && Math.random() < NOMATCH_CHANCE;

    if (noMatch) {
      noMatchStreak++;
      await spinReel(pickMismatch(), { tease: false });
      $("#reel-result").innerHTML = `<b class="rr-nomatch">NESSUN MATCH</b>`;
      crazyNoMatch();
      currentBet = null; // puntate annullate (push): nessun gettone cambia
      log(`${person}: nessun match! Si ri-tira 💥`);
      mc("💥 NESSUN MATCH! Ri-tira la leva! ⬇️");
      setTimeout(zoomMachineOut, 1000);
      busy = false;
      // stesso giocatore: NON avanza il cursore
      if (auto) {
        setTimeout(nextTurn, 1600);
      } else {
        setTimeout(() => {
          if (!busy && cursor < revealQueue.length && !auto) {
            armLever(true);
            mc("💥 NESSUN MATCH! Ri-tira la leva! ⬇️");
          }
        }, 1100);
      }
      return;
    }

    // --- MATCH: i 3 rulli si allineano sulla stanza assegnata ---
    noMatchStreak = 0;
    await spinReel([roomId, roomId, roomId], { tease: true });
    $("#reel-result").innerHTML =
      `${room.jacuzzi ? "🛁 " : ""}<b>${room.nome}</b>${room.jacuzzi ? " 🛁" : ""}`;

    // atterraggio
    occupants[roomId].push(person);
    highlightRoom(roomId);
    renderRooms();
    highlightRoom(roomId);

    resolveBets(person, roomId, oddsMap);
    currentBet = null;

    const isJackpot = room.jacuzzi;
    Audio.ding(); // "click" di aggancio della stanza
    if (isJackpot) {
      // sirena reale di jackpot; fallback: fanfara + jackpot sintetizzati
      Samples.play("jackpot", {
        vol: 0.9,
        fallback: () => {
          Audio.fanfare();
          Audio.jackpot();
        },
      });
      celebrate("jackpot");
      screenShake();
      screenFlash();
      mc(`🎉 JACKPOT! ${person} conquista ${room.nome} con IDROMASSAGGIO! 🛁`);
      log(`JACKPOT: ${person} → ${room.nome} 🛁`, "jackpot");
    } else {
      // jingle reale di vincita; fallback: fanfara sintetizzata
      Samples.play("win", { vol: 0.85, fallback: () => Audio.fanfare() });
      celebrate("normal");
      mc(`${person} va in ${room.nome}!`);
      log(`${person} → ${room.nome}`);
    }
    Samples.play("coins", { vol: 0.6, fallback: () => Audio.coin() });

    // la macchina resta zoomata per godersi la vittoria, poi torna al posto
    setTimeout(zoomMachineOut, isJackpot ? 1200 : 800);

    // EFFETTO SHALOM: sulle vittorie jacuzzi (o SEMPRE se FORCE_SHALOM è attivo,
    // per debug: così vedi l'effetto a ogni vittoria, non solo sulle jacuzzi)
    if (isJackpot || FORCE_SHALOM) {
      await sleep(600); // lascia vedere un attimo i coriandoli
      await playShalom();
    }

    cursor++;
    setProgress();
    renderLeaderboard();
    busy = false;

    if (cursor >= revealQueue.length) {
      setTimeout(endGame, 1100);
    } else if (auto) {
      setTimeout(nextTurn, 1400);
    } else {
      // riarma la leva per il prossimo giro
      setTimeout(() => {
        if (!busy && cursor < revealQueue.length && !auto) {
          armLever(true);
          mc("Tira di nuovo la leva! ⬇️");
        }
      }, 1300);
    }
  }

  function highlightRoom(roomId) {
    document
      .querySelectorAll(".room-card")
      .forEach((c) => c.classList.remove("hot"));
    if (roomId) {
      const el = document.querySelector(`.room-card[data-room="${roomId}"]`);
      if (el) el.classList.add("hot");
    }
  }

  function endGame() {
    armLever(false);
    const winner = players.slice().sort((a, b) => b.chips - a.chips)[0];
    const you = players.find((p) => p.you);
    mc(
      `🏁 Tutti sistemati! Vincitore scommesse: ${winner.name} (${winner.chips} gettoni).`,
    );
    log(
      `Fine. I tuoi gettoni: ${you.chips}. Miglior scommettitore: ${winner.name}.`,
      "win",
    );
    Audio.fanfare();
    celebrate("jackpot");
    $("#btn-primary").style.display = "";
    $("#btn-primary").textContent = "RIGIOCA 🔁";
    $("#btn-primary").disabled = false;
    $("#btn-primary").onclick = () => {
      newGame(false);
    };
  }

  /* ---------- Marquee ---------- */
  function initMarquee() {
    const m = $("#marquee");
    m.innerHTML = Array.from({ length: 16 })
      .map(
        (_, i) =>
          `<span class="bulb" style="animation-delay:${(i % 8) * 0.12}s"></span>`,
      )
      .join("");
  }

  /* ---------- Leva (trigger dei giri) ---------- */
  function armLever(on) {
    leverArmed = on;
    const lever = $("#lever");
    lever.classList.toggle("armed", on);
  }

  function pullAndSpin() {
    if (!leverArmed || busy) return;
    armLever(false);
    nextTurn();
  }

  function bindLever() {
    const lever = $("#lever");
    let dragging = false;
    let startY = 0;
    lever.addEventListener("pointerdown", (e) => {
      if (!leverArmed || busy) return;
      dragging = true;
      startY = e.clientY;
      try {
        lever.setPointerCapture(e.pointerId);
      } catch (err) {}
      e.preventDefault();
    });
    lever.addEventListener("pointermove", (e) => {
      if (!dragging) return;
      const dy = Math.max(0, Math.min(70, e.clientY - startY));
      lever.style.transform = `rotate(${(dy / 70) * 28}deg)`;
    });
    const release = (e) => {
      if (!dragging) return;
      dragging = false;
      const dy = Math.max(0, e.clientY - startY);
      lever.style.transform = "";
      // tap secco (dy piccolo) oppure tirata piena => parte; tirata a metà => annulla
      if (dy < 8 || dy >= 26) pullAndSpin();
    };
    lever.addEventListener("pointerup", release);
    lever.addEventListener("pointercancel", () => {
      dragging = false;
      $("#lever").style.transform = "";
    });
  }

  /* ---------- Controlli ---------- */
  function onPrimary() {
    if (busy) return;
    if (cursor >= revealQueue.length) {
      endGame();
      return;
    }
    nextTurn();
  }

  function bindControls() {
    bindLever();
    $("#btn-primary").onclick = onPrimary;
    $("#btn-auto").onclick = (e) => {
      auto = !auto;
      e.currentTarget.classList.toggle("active", auto);
      if (auto) {
        armLever(false);
        if (!busy && cursor < revealQueue.length) nextTurn();
      } else if (!busy && cursor < revealQueue.length) {
        armLever(true);
        mc("Tira la leva! ⬇️");
      }
    };
    $("#btn-reroll").onclick = () => {
      if (busy) return;
      Audio.init();
      Audio.resume();
      newGame(true);
      mc("Nuovo sorteggio pronto! 🎲 Tira la leva ⬇️");
    };
    $("#btn-sound").onclick = (e) => {
      Audio.on = !Audio.on;
      e.currentTarget.textContent = Audio.on ? "🔊" : "🔇";
    };
    // prima interazione: sblocca audio
    document.addEventListener(
      "pointerdown",
      () => {
        Audio.init();
        Audio.resume();
      },
      { once: true },
    );
  }

  /* ---------- Avvio ---------- */
  try {
    initBackground();
  } catch (e) {
    console.warn("Sfondo 3D disattivato:", e);
  }
  initMarquee();
  bindControls();
  newGame(false);

  // hook di debug (per test/verifica; innocuo)
  window.__celebrate = celebrate;
  window.__shalom = {
    fire: shalomFireLion,
    video: shalomVideoOnly,
    gesu: shalomGesu,
    random: playShalom,
  };
})();
