<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<title>Roleta do Alemão</title>
<style>
  :root{
    --bg-image: url('../uploads/roulette_64/fundo-de-teste_68c329a1dc43c7.08769420.jpg');
    --overlay: none;
    --txt:#eef3ff;
    --wheel-size: 80vmin;

    /* Ponteiro: tamanho e “mordida” dentro da roda (padrão para todas as posições) */
    --pointer-size: clamp(40px, 5.2vw, 62px);
    --pointer-inset: clamp(26px, 3.2vw, 36px);
  }
  *{ box-sizing:border-box }
  html,body{ height:100% }
  body{
    margin:0; color:var(--txt);
    font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
    min-height:100svh; overflow:hidden;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    padding: max(10px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
    gap:14px;
  }
  body::before{
    content:""; position:fixed; inset:0; z-index:-2;
    background-image: var(--bg-image);
    background-size: cover, cover; background-position:center,center; background-repeat:no-repeat,no-repeat;
  }
  .title{
    text-align:center; font-weight:1000; letter-spacing:.18em; text-transform:uppercase;
    font-size:clamp(14px,2.6vw,60px); margin:0 0 4px 0;
    color:#fefe00;
    font-family: 'Teko', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    text-shadow:0 2px 10px rgba(0,0,0,.6), 0 0 12px rgba(96,165,250,.45);
}
  .subtitle{
    margin-top:-4px; text-align:center; opacity:.85; font-weight:700;
    font-size:clamp(11px,1.8vw,40px); letter-spacing:.05em;
    color:#eef3ff;
    font-family: 'Teko', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

  .wheelbox{ position:relative; width:var(--wheel-size); height:var(--wheel-size); margin:0 auto; cursor: pointer; margin-top: 120px; }
  .wheelbox>.layer{ position:absolute; inset:0 }
  .layer canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; user-select:none; -webkit-tap-highlight-color:transparent; touch-action:none }
  #fxBG{ z-index:1; pointer-events:none } #wheel{ z-index:2 } #fx{ z-index:3; pointer-events:none }

  /* ===== PONTEIRO BONITO (cor + posição) ===== */
  .pointer{
    position:absolute; z-index:5; pointer-events:none;
    width:var(--pointer-size); height:var(--pointer-size);

    /* vars preenchidas via JS */
    --pc:#A855F7;      /* base */
    --pc-hi:#C28CFF;   /* highlight */
    --pc-mid:#A855F7;  /* meio */
    --pc-lo:#6E2CB2;   /* sombra */
    --pc-rim:#4B1A86;  /* aro/borda */

    border:clamp(3px,.6vw,5px) solid var(--pc-rim);
    filter:drop-shadow(0 12px 16px rgba(0,0,0,.55));

    /* gradiente 3D estável pra qualquer cor */
    background:
      radial-gradient(120% 120% at 28% 20%, rgba(255,255,255,.55) 0 18%, rgba(255,255,255,0) 55%),
      linear-gradient(180deg, var(--pc-hi) 0%, var(--pc-mid) 55%, var(--pc-lo) 100%);
    box-shadow:
      0 0 0 1px rgba(255,255,255,.14) inset,
      0 -10px 16px rgba(0,0,0,.33) inset;
  }
  .pointer::after{
    content:""; position:absolute; inset:0; pointer-events:none;
    background: radial-gradient(110% 90% at 36% 10%, rgba(255,255,255,.55), rgba(255,255,255,0) 60%);
    mix-blend-mode:screen; opacity:.75;
  }
  .pointer.bump{ animation:glowPulse .18s ease }
  @keyframes glowPulse{ 50%{ filter: drop-shadow(0 12px 16px rgba(0,0,0,.55)) brightness(1.07); } }

  /* POSIÇÕES padronizadas (mesma “mordida” em todas) */
  .pointer.pos-right{
    top:50%;
    left: calc(100% - var(--pointer-inset));
    transform: translateY(-50%);
    clip-path: polygon(100% 0, 0 50%, 100% 100%);
    border-left: 0;
  }
  .pointer.pos-left{
    top:50%;
    left: calc(0px - (var(--pointer-size) - var(--pointer-inset)));
    transform: translateY(-50%);
    clip-path: polygon(0 0, 100% 50%, 0 100%);
    border-right: 0;
  }
  .pointer.pos-top{
    top: calc(0px - (var(--pointer-size) - var(--pointer-inset)));
    left: 50%;
    transform: translateX(-50%);
    clip-path: polygon(0 0, 50% 100%, 100% 0);
    border-bottom: 0;
  }
  .pointer.pos-bottom{
    top: calc(100% - var(--pointer-inset));
    left: 50%;
    transform: translateX(-50%);
    clip-path: polygon(0 100%, 50% 0, 100% 100%);
    border-top: 0;
  }

  /* modal */
  .modal{ position:fixed; inset:0; display:none; align-items:center; justify-content:center; z-index:50 }
  .modal.open{ display:flex }
  .modal .backdrop{ position:absolute; inset:0; background:rgba(3,6,23,.72) }
  .modal canvas#fxModal{ position:absolute; inset:0; width:100%; height:100%; z-index:1; pointer-events:none }
  .modal .card{ position:relative; z-index:2; width:min(92vw,520px); background:linear-gradient(180deg, rgba(17,24,39,.92), rgba(10,15,30,.92)); color:#e5e7eb; border:1px solid rgba(255,255,255,.14); border-radius:18px; box-shadow:0 20px 80px rgba(0,0,0,.6); padding:20px; display:grid; gap:12px }
  .modal .close{ position:absolute; top:8px; right:8px; width:45px; height:45px; border-radius:999px; border:1px solid rgba(255,255,255,.2); background:rgba(255,255,255,.08); cursor: pointer; font-size: 40px; color: #ffff; }
  .modal .bubble{ width:140px; height:140px; border-radius:999px; display:grid; place-items:center; background:#ef4444; box-shadow:0 10px 30px rgba(255,60,60,.35) inset; overflow:hidden; justify-self:center; text-align:center; padding:8px 10px }
  .modal .bubble .qty{ font:900 36px/1 system-ui; color:#fff }
  .modal .bubble .unit{ font:800 12px/1 system-ui; color:#fff; margin-top:4px }
  .modal .bubble #modalGameInBubble{ font:700 11px/1.15 system-ui; color:#fff; opacity:.95; margin-top:6px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; width:100% }
@media (max-width: 768px){
            .wheelbox{
                margin-top: -200px;
            }
            h1.title{
              /*font-size: calc(var(--title-size, 60px) * 0.80) !important;*/
              display: none;
            }
            .subtitle{
              /*font-size: calc(var(--subtitle-size, 40px) * 0.40) !important;*/
              display: none;
            }
        }
</style>

<!-- Google Fonts (gerado) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Teko:wght@400;600;700;800;900&amp;display=swap">
</head>
<body style="--overlay: none;">
  <h1 class="title" id="title">Roleta</h1>
  <div class="subtitle" id="subtitle">Jogue na Lider</div>

  <div class="wheelbox" id="wheelbox" aria-live="polite">
    <div class="layer"><canvas id="fxBG" aria-hidden="true"></canvas></div>
    <div class="layer">
      <div class="pointer pos-right" id="pointer" aria-hidden="true"></div>
      <canvas id="wheel" aria-label="Roleta"></canvas>
    </div>
    <div class="layer"><canvas id="fx" aria-hidden="true"></canvas></div>
  </div>

  <!-- áudios (preenchidos pelo generator) -->
  <audio id="bgm"  src=""   preload="none" playsinline></audio>
  <audio id="tick" src=""    preload="none" playsinline></audio>
  <audio id="inicio" src="../uploads/roulette_64/2inicio_68c2f92e2d4dd5.79940579.mp3" preload="auto" playsinline></audio>
  <audio id="ganhou" src="" preload="none" playsinline></audio>
  <audio id="perdeu" src="" preload="none" playsinline></audio>

  <!-- modal -->
  <div class="modal" id="resultModal" aria-hidden="true">
    <div class="backdrop" id="modalBackdrop"></div>
    <canvas id="fxModal"></canvas>
    <div class="card" role="dialog" aria-labelledby="modalTitle">
      <button class="close" id="modalClose" type="button" aria-label="Fechar">×</button>

      <div class="bubble" id="modalBubble">
        <div class="qty" id="modalQtyBig" style="display:none">50</div>
        <div class="unit" id="modalUnit" style="display:none">GIROS</div>
        <div id="modalGameInBubble" style="display:none"></div>
      </div>

      <h2 id="modalH2" style="margin:0;text-align:center;font:900 20px/1.3 system-ui">
        <span id="modalTitle">Resultado</span>
      </h2>
      <div id="modalDetail" style="text-align:center;opacity:.85">—</div>
      <div id="modalGame" style="display:none; text-align:center; opacity:.9; font-weight:800;"></div>

      <button id="modalCTA" type="button" style="cursor:pointer;appearance:none;border:0;padding:12px 18px;border-radius:12px;font-weight:900;background:linear-gradient(180deg,#3b82f6,#06b6d4);color:#fff;box-shadow:0 14px 40px rgba(3,105,161,.45)">Fechar</button>
    </div>
  </div>

<script>
/* ===== PRIZES (gerado) ===== */
const PRIZES = [{"primary":"10","unit":"GIROS","color":"#e80032","weight":1,"kind":"LOSE","type":"LOSE","game":"Jogo 1 aviao LOSE","url":"","image":"../uploads/roulette_64/ft-roleta-1_68c2e79d0b03b3.27108237.png","imageFit":"cover","imageOpacity":100,"text_color":"#ffffff","text_size":40,"unit_text_size":24},{"primary":"20","unit":"GIROS","color":"#b40023","weight":1,"kind":"LOSE","type":"LOSE","game":"Jogo 2 do zeus LOSE","url":"https://www.google.com/","image":"../uploads/roulette_64/ft-roleta-2_68c2e7cdbaf8f4.84863800.png","imageFit":"cover","imageOpacity":100,"text_color":"#ffffff","text_size":40,"unit_text_size":24},{"primary":"30","unit":"GIROS","color":"#e80032","weight":1,"kind":"LOSE","type":"LOSE","game":"Jogo 3 coelho LOSE","url":"","image":"../uploads/roulette_64/ft-roleta-3_68c2e841b0db36.19547558.png","imageFit":"cover","imageOpacity":100,"text_color":"#ffffff","text_size":40,"unit_text_size":24},{"primary":"40","unit":"GIROS","color":"#b40023","weight":1,"kind":"WIN","type":"WIN","game":"Jogo do rato LOSE","url":"","image":"../uploads/roulette_64/ft-roleta-4_68c2e8a5d53052.86013310.png","imageFit":"cover","imageOpacity":100,"text_color":"#ffffff","text_size":40,"unit_text_size":24},{"primary":"50","unit":"GIROS","color":"#e80032","weight":10,"kind":"WIN","type":"WIN","game":"Jogo 5 tigrinho","url":"","image":"../uploads/roulette_64/ft-roleta-5_68c2e9057bd278.24684690.png","imageFit":"cover","imageOpacity":95,"text_color":"#ffffff","text_size":40,"unit_text_size":24},{"primary":"60","unit":"GIROS","color":"#b40023","weight":1,"kind":"LOSE","type":"LOSE","game":"Jogo 6 tigre 2 LOSE","url":"","image":"../uploads/roulette_64/ft-roleta-6_68c2e9332f6238.01442946.png","imageFit":"cover","imageOpacity":100,"text_color":"#ffffff","text_size":40,"unit_text_size":24}];
// DEBUG: Segment weights analysis
console.log('🎰 ROULETTE SEGMENT WEIGHTS:');
PRIZES.forEach((p, i) => {
  console.log(`Segment ${i}: Weight ${p.weight} -> Effective: ${p.weight <= 1 ? 0 : p.weight} - ${p.primary} ${p.unit}`);
});

/* ===== Botão central (o generator substitui) ===== */
const SPIN_TEXT = "GIRAR";
/* ===== FONTE (gerado) ===== */
const FONT_FAMILY = "Teko";
const FONT_STACK  = "'" + FONT_FAMILY + "', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";



// --- Audio guard: evita NotSupportedError quando não há fonte carregada ---
(function(){
  try {
    var P = (typeof HTMLMediaElement!=='undefined') ? HTMLMediaElement.prototype : null;
    if (!P || P.__spinPatched) return;
    var _play = P.play;
    P.play = function(){
      try {
        var hasSrc = !!(this.currentSrc || this.src || (this.querySelector && this.querySelector('source') && this.querySelector('source').src));
        if (!hasSrc) return Promise.resolve();
        var ret = _play.apply(this, arguments);
        if (ret && typeof ret.catch==='function') ret.catch(function(){});
        return ret;
      } catch(e){ return Promise.resolve(); }
    };
    P.__spinPatched = true;
  } catch(_) {}
})();
// ---------------------------------------------------------------------------
/* ===== ANIMAÇÃO (o generator substitui) ===== */
const ANIM_SPINS = 2;            // voltas completas antes de parar
const ANIM_SPEED = 3;          // 1.0 = 100%

/* ===== POINTER (gerado) ===== */
const POINTER_COLOR = "#ffffff";
const POINTER_POSITION = "top";
/* ===== POSIÇÃO TEXTO/IMAGEM ===== */
const IMG_R_POS  = 0.90; // imagem mais para a borda (0..1)*r
const TEXT_R_POS = 0.48;
const TEXT_CENTER_POS = 0.64; // posição radial quando NÃO há imagem

/* ===== IMAGEM DO SEGMENTO ===== */
const IMAGE_STYLE = 'slice'; // 'slice' | 'ring'

/* ===== API ===== */
const API_SPIN = '/api/spin.php';
const API_ACK  = '/api/spin_ack.php';
const MAX_WAIT_RESULT = 6.0; // s esperando o resultado do servidor antes de fallback

/* helpers */
function shade(hex, pct){
  hex = String(hex||'').trim();
  if(!/^#?[0-9a-f]{6}$/i.test(hex)) return '#A855F7';
  if(hex[0] !== '#') hex = '#'+hex;
  const n = parseInt(hex.slice(1), 16);
  let r=(n>>16)&255, g=(n>>8)&255, b=n&255;
  const f = (100+pct)/100;
  r = Math.max(0, Math.min(255, Math.round(r*f)));
  g = Math.max(0, Math.min(255, Math.round(g*f)));
  b = Math.max(0, Math.min(255, Math.round(b*f)));
  const toHex = v => v.toString(16).padStart(2,'0');
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function parseHex(hex){
  hex = String(hex||'').trim();
  if(hex[0] !== '#') hex = '#'+hex;
  if(!/^#[0-9a-f]{6}$/i.test(hex)) hex = '#A855F7';
  const n = parseInt(hex.slice(1),16);
  return { r:(n>>16)&255, g:(n>>8)&255, b:n&255, hex };
}
function escHtml(s){ return String(s||'').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
function normalizeUrl(u){ u=(u||'').trim(); if(!u) return ''; if(/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(u)) return u; return 'https://'+u.replace(/^\/+/, ''); }

/* ===== OFFSET do ponteiro ===== */
function getPointerOffset(pos){
  switch(String(pos).toLowerCase()){
    case 'bottom': return Math.PI/2;
    case 'left':   return Math.PI;
    case 'top':    return Math.PI*1.5;
    case 'right':
    default:       return 0;
  }
}
let POINTER_OFFSET = getPointerOffset(POINTER_POSITION);

/* constantes */
const N = PRIZES.length || 8;
const TAU = Math.PI*2, SLICE = TAU/N;

/* ===== CALIBRAÇÃO DE PARADA =====
   Garantir que o ponteiro pare DENTRO do setor escolhido, evitando ficar na borda.
   - TARGET_SAFETY empurra o alvo levemente para dentro do setor
   - MICRO-CORRECTION faz um ajuste final de até ~8% de um setor se ainda houver divergência
*/
const TARGET_SAFETY_FRAC = 0.06;      // 6% do setor
const TARGET_SAFETY = SLICE * TARGET_SAFETY_FRAC;
const MICRO_MAX_FRAC = 0.10;          // 10% de um setor
const MICRO_MAX = SLICE * MICRO_MAX_FRAC; 
const MICRO_TIME = 0.22;              // 220ms (correção suave)


/* spin params */
const BASE_MAX_SPEED=12.0, BASE_ACCEL=28.0;
const SPD = Math.max(0.25, Math.min(3.0, +ANIM_SPEED || 1.0));
const MAX_SPEED = BASE_MAX_SPEED * SPD;
const ACCEL     = BASE_ACCEL     * SPD;
const POST_AUDIO_CRUISE_T=0.45, MIN_TOTAL_CRUISE_T=0.40, MAX_WAIT_FOR_AUDIO=1.80;

/* refs */
const titleEl=document.getElementById('title');
const subtitleEl=document.getElementById('subtitle');
const wheelbox=document.getElementById('wheelbox');
const wheel=document.getElementById('wheel'), ctx=wheel.getContext('2d');
const fx=document.getElementById('fx'), fxx=fx.getContext('2d');
const fxBG=document.getElementById('fxBG'), fxb=fxBG.getContext('2d');
const pointer=document.getElementById('pointer');

const bgm=document.getElementById('bgm'), tick=document.getElementById('tick');
const inicio=document.getElementById('inicio'), ganhou=document.getElementById('ganhou'), perdeu=document.getElementById('perdeu');

const resultModal=document.getElementById('resultModal');
const modalBackdrop=document.getElementById('modalBackdrop');
const modalClose=document.getElementById('modalClose');
const modalCTA=document.getElementById('modalCTA');
const fxModal=document.getElementById('fxModal'), fxm=fxModal.getContext('2d');
const modalQtyBig=document.getElementById('modalQtyBig');
const modalUnit=document.getElementById('modalUnit');
const modalH2=document.getElementById('modalH2');
const modalDetail=document.getElementById('modalDetail');
const modalBubble=document.getElementById('modalBubble');
const modalGame=document.getElementById('modalGame');
const modalGameInBubble=document.getElementById('modalGameInBubble');

/* estado */
let muted=false;
let modalIsWin=false;
let phase='idle', speed=0, currentAngle=0, lastT=null, spinIndex=null;
let cruiseClock=0, postAudioClock=0, audioEnded=false;
let anglePath=0; let diagClock=0; const DIAG=true;
let decelStartAngle=0, decelTargetAngle=0, decelDistance=0, decelTime=0, decelClock=0, decelAccel=0;
let microTargetAngle=0, microClock=0;
let lastTickSector=null;
let confetti=[]; let modalPrevT=null;
let resultTimeoutClock=0; let resultPromise=null;

/* layout */
const WHEEL_SCALE_PCT = 100; // 40..100 (100 = tamanho atual)

/* >>> NOVO: boosts por breakpoint */
const WHEEL_MOBILE_BOOST_PCT = 105;
const WHEEL_TABLET_BOOST_PCT = 104;
const WHEEL_DESKTOP_BOOST_PCT = 100;

function fitWheelToViewport(){
  const vw = Math.min(window.innerWidth, document.documentElement.clientWidth || window.innerWidth);
  const vh = Math.min(window.innerHeight, document.documentElement.clientHeight || window.innerHeight);
  const bodyStyles = getComputedStyle(document.body);
  const padH = parseFloat(bodyStyles.paddingLeft)+parseFloat(bodyStyles.paddingRight);
  const padV = parseFloat(bodyStyles.paddingTop)+parseFloat(bodyStyles.paddingBottom);
  const headerH = titleEl.offsetHeight + (subtitleEl?.offsetHeight||0);
  const safety=8;
  const availableW = Math.max(160, vw - padH - safety);
  const availableH = Math.max(160, vh - padV - headerH - 10);
  const base  = Math.max(220, Math.min(availableW, availableH, 1120));
  const factor = (vw <= 480) ? 0.92 : (vw <= 768 ? 0.86 : 0.74);

  const __sclRaw = (typeof WHEEL_SCALE_PCT==='number' ? WHEEL_SCALE_PCT : parseFloat(WHEEL_SCALE_PCT))/100;
  const __scl = Math.max(0.70, Math.min(1.50, __sclRaw));

  const boost = (vw <= 480)
    ? (WHEEL_MOBILE_BOOST_PCT/100)
    : (vw <= 768 ? (WHEEL_TABLET_BOOST_PCT/100) : (WHEEL_DESKTOP_BOOST_PCT/100));

  let __size = base * factor * __scl * boost;
  __size = Math.max(220, Math.min(__size, 1120));
  document.documentElement.style.setProperty('--wheel-size', (__size) + 'px');

  try{
    const ptrSize = Math.max(28, Math.min(72, __size * 0.085));
    const ptrInset = Math.max(18, Math.min(48, ptrSize * 0.62));
    document.documentElement.style.setProperty('--pointer-size',  ptrSize + 'px');
    document.documentElement.style.setProperty('--pointer-inset', ptrInset + 'px');
  }catch(e){}

  try{
    console.groupCollapsed('WheelScale');
    console.log('WHEEL_SCALE_PCT =', WHEEL_SCALE_PCT);
    console.log({vw, vh, base, factor, __scl, __size});
    console.log('--pointer-size  =', getComputedStyle(document.documentElement).getPropertyValue('--pointer-size').trim());
    console.log('--pointer-inset =', getComputedStyle(document.documentElement).getPropertyValue('--pointer-inset').trim());
    console.log('--wheel-size    =', getComputedStyle(document.documentElement).getPropertyValue('--wheel-size').trim());
    console.groupEnd();
  }catch(e){}
}

/* util */
const dpr=()=>Math.max(1, window.devicePixelRatio||1);
function resizeCanvas(c){
  const r=c.getBoundingClientRect(); const dw=Math.round(r.width*dpr()), dh=Math.round(r.height*dpr());
  if(dw<10||dh<10){ requestAnimationFrame(()=>resizeCanvas(c)); return false; }
  if(c.width!==dw||c.height!==dh){ c.width=dw; c.height=dh; return true; }
  return false;
}
const normalize=a=>{ a%=TAU; if(a<0)a+=TAU; return a; };
/* >>> Fallback local respeitando "peso > 1" */
function fallbackWeightedPick(){
  const W = PRIZES.map(p => (+p.weight>1)? +p.weight : 0);
  const total = W.reduce((s,v)=>s+v,0);
  if (total<=0) return 0;
  let r=Math.random()*total;
  for(let i=0;i<W.length;i++){ r-=W[i]; if(r<=0) return i; }
  return W.length-1;
}
function sectorAngleMid(i){ return i*SLICE + SLICE/2; }
function centerAngleForIndex(i){
  const mid = sectorAngleMid(i);
  return normalize(-mid + POINTER_OFFSET);
}
function shortestDelta(from, to){
  let d = normalize(to - from);
  if (d > Math.PI) d -= TAU; // escolhe caminho mais curto
  return d;
}
function maybeTick(angle){
  const sector = Math.floor(normalize((angle - POINTER_OFFSET)) / SLICE);
  if(lastTickSector!==sector){
    lastTickSector=sector;
    if(!muted){ try{ tick.currentTime=0; tick.play(); }catch{} }
    pointer.classList.remove('bump'); void pointer.offsetWidth; pointer.classList.add('bump');
  }
}

/* imagens por segmento */
const SEG_IMAGES = new Array(PRIZES.length).fill(null);
PRIZES.forEach((p, i) => {
  if (!p.image) return;
  const img = new Image();
  img.decoding = 'async'; img.loading = 'eager'; img.src = p.image;
  img.onload = () => { SEG_IMAGES[i] = img; if (typeof drawWheel === 'function') drawWheel(currentAngle); };
  img.onerror = () => { SEG_IMAGES[i] = null; };
});

/* desenho da roda */

/* === Helpers: clip annular wedge + draw image into sector === */
function clipAnnularWedge(ctx, inner, outer, a0, a1){
  ctx.beginPath();
  ctx.arc(0,0, outer, a0, a1, false);
  ctx.arc(0,0, inner, a1, a0, true);
  ctx.closePath();
  ctx.clip();
}
function drawWheel(angle){
  resizeCanvas(wheel);
  const w=wheel.width,h=wheel.height,cx=w/2,cy=h/2;
  const r=Math.min(w,h)*0.40;
  const SL=SLICE;

  ctx.setTransform(1,0,0,1,0,0); ctx.clearRect(0,0,w,h);
  ctx.translate(cx,cy); ctx.rotate(angle);

  const outer=r+Math.max(42, r*0.1), inner=r+Math.max(16, r*0.04);
  const g1=ctx.createRadialGradient(0,0,inner,0,0,outer+10);
  g1.addColorStop(0,'#c3cad6'); g1.addColorStop(0.55,'#7b8396'); g1.addColorStop(1,'#3b4356');
  ctx.lineWidth=outer-inner; ctx.strokeStyle=g1; ctx.beginPath(); ctx.arc(0,0,(outer+inner)/2,0,TAU); ctx.stroke();

  ctx.lineWidth=Math.max(6, r*0.025); ctx.strokeStyle='#e8ecf4'; ctx.beginPath(); ctx.arc(0,0,r+10,0,TAU); ctx.stroke();

  for(let i=0;i<N;i++){
    const a0=i*SL, a1=a0+SL, mid=a0+SL/2, p=PRIZES[i]||{};
    ctx.save();
    ctx.beginPath(); ctx.moveTo(0,0); ctx.arc(0,0,r,a0,a1); ctx.closePath(); ctx.clip();

    const baseCol=(p.color||'#ddd'); ctx.fillStyle=baseCol; ctx.fillRect(-r,-r,r*2,r*2);

    const img = SEG_IMAGES[i];
    if (img){
      const fit   = String(p.imageFit||'contain').toLowerCase();
      const alpha = Math.max(0, Math.min(100, +p.imageOpacity||95))/100;
      const aMid  = (a0 + a1) / 2;

      if (IMAGE_STYLE === 'ring'){
        const rInner = Math.max(0, r * 0.27 - 2);
        const rOuter = r;
        const bandH  = Math.max(2, rOuter - rInner);
        const bias   = (typeof p.imageBiasIn === 'number'
                        ? Math.max(0, Math.min(1, p.imageBiasIn))
                        : 0.40);
        const rc     = rInner + bandH * bias;
        const arcLen = Math.max(2, 2 * rc * Math.tan(SL/2));

        const iw = img.naturalWidth || img.width, ih = img.naturalHeight || img.height;
        const ir = iw/ih, tr = arcLen/bandH;
        let sx=0, sy=0, sw=iw, sh=ih;
        if ((fit === 'cover'  && ir < tr) || (fit === 'contain' && ir > tr)) { sw = ih * tr; sx = (iw - sw) * 0.5; }
        else { sh = iw / tr; sy = (ih - sh) * 0.5; }

        ctx.save();
        (function clipAnnularWedge(ctx, inner, outer, a0, a1){
          ctx.beginPath();
          ctx.arc(0,0, outer, a0, a1, false);
          ctx.arc(0,0, inner, a1, a0, true);
          ctx.closePath();
          ctx.clip();
        })(ctx, rInner, rOuter, a0+0.003, a1-0.003);

        ctx.rotate(aMid);
        ctx.translate(rc, 0);
        ctx.rotate(Math.PI/2);

        const prevA = ctx.globalAlpha; ctx.globalAlpha = alpha;
        ctx.drawImage(img, sx, sy, sw, sh, -arcLen/2, -bandH/2, arcLen, bandH);
        ctx.globalAlpha = prevA;

        ctx.restore();
      } else {
        const boxW = Math.min(r*0.95, (a1-a0)*r*0.98);
        const boxH = r * 0.80;
        const rPos = r * IMG_R_POS;
        const iw = img.naturalWidth || img.width, ih = img.naturalHeight || img.height;
        const ir = iw/ih, tr = boxW/boxH;
        let dw, dh;
        if ((fit==='cover' && ir<tr) || (fit==='contain' && ir>tr)) { dh = boxH; dw = dh*ir; }
        else { dw = boxW; dh = dw/ir; }

        ctx.save();
        ctx.rotate((a0 + a1) / 2);
        ctx.translate(rPos, 0);
        ctx.rotate(Math.PI/2);

        const prevA = ctx.globalAlpha; ctx.globalAlpha = alpha;
        ctx.drawImage(img, -dw/2, -dh/2, dw, dh);
        ctx.globalAlpha = prevA;

        ctx.restore();
      }
    }
    const ring=ctx.createRadialGradient(0,0,r*0.85,0,0,r);
    ring.addColorStop(0,'rgba(0,0,0,0)'); ring.addColorStop(1,'rgba(0,0,0,.25)');
    ctx.fillStyle=ring; ctx.fillRect(-r,-r,r*2,r*2);
    ctx.restore();

    ctx.beginPath(); ctx.lineWidth=Math.max(4, r*0.02); ctx.strokeStyle='rgba(255,255,255,.95)';
    ctx.arc(0,0,r,a0,a0+0.0025); ctx.stroke();
  }

  for(let i=0;i<N;i++){
    const mid=i*SL+SL/2, p=PRIZES[i]||{};
    const _hasImg = !!SEG_IMAGES[i];
    const _textR  = _hasImg ? r*TEXT_R_POS : r*TEXT_CENTER_POS;
    ctx.save(); ctx.rotate(mid); ctx.translate(_textR,0); ctx.rotate(Math.PI/2);
    ctx.textAlign='center'; ctx.textBaseline='middle';ctx.fillStyle = p.text_color || '#ffffff';const primarySize = p.text_size || Math.max(24, Math.round(r*0.09));ctx.font=`900 ${primarySize}px ${FONT_STACK}`;ctx.shadowColor = 'rgba(0, 0, 0, 0.6)';ctx.shadowBlur = 4;ctx.shadowOffsetX = 1;ctx.shadowOffsetY = 1;ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)';ctx.lineWidth = Math.max(4, r*0.018);ctx.strokeText(p.primary||'',0,-6);ctx.fillText(p.primary||'',0,-6);const unitSize = p.unit_text_size || Math.max(13, Math.round(r*0.045));ctx.font=`800 ${unitSize}px ${FONT_STACK}`;ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)';ctx.lineWidth = Math.max(3, r*0.015);ctx.strokeText(p.unit||'',0,18);ctx.fillText(p.unit||'',0,18);ctx.shadowColor = 'transparent';ctx.shadowBlur = 0;ctx.shadowOffsetX = 0;ctx.shadowOffsetY = 0;
    ctx.restore();
  }

  const cr=r*0.27; ctx.rotate(-angle);ctx.beginPath(); ctx.arc(0,0,cr,0,TAU); ctx.fillStyle='#000000'; ctx.fill();ctx.lineWidth=Math.max(2,cr*0.075); ctx.strokeStyle='rgba(255,255,255,.92)'; ctx.beginPath(); ctx.arc(0,0,cr-ctx.lineWidth*0.55,0,TAU); ctx.stroke();ctx.textAlign='center'; ctx.textBaseline='middle';ctx.shadowColor='transparent'; ctx.shadowBlur=0; ctx.shadowOffsetX=0; ctx.shadowOffsetY=0;const spinSize = 30;ctx.font=`900 ${spinSize}px ${FONT_STACK}`;const __txtColor = '#ffffff';const __tc=__txtColor.replace('#','');const __tr=parseInt(__tc.substring(0,2),16), __tg=parseInt(__tc.substring(2,4),16), __tb=parseInt(__tc.substring(4,6),16);const __tl=0.2126*__tr+0.7152*__tg+0.0722*__tb;const __haloMain=(__tl>160)?'rgba(0,0,0,0.70)':'rgba(255,255,255,0.90)';const __haloSub =( __tl>160)?'rgba(0,0,0,0.34)':'rgba(255,255,255,0.42)';ctx.lineJoin='round'; ctx.miterLimit=2;ctx.strokeStyle=__haloMain; ctx.lineWidth=Math.max(2, r*0.014); ctx.strokeText(SPIN_TEXT,0,0);ctx.strokeStyle=__haloSub;  ctx.lineWidth=Math.max(1, r*0.008); ctx.strokeText(SPIN_TEXT,0,0);ctx.fillStyle=__txtColor; ctx.fillText(SPIN_TEXT,0,0);
}

/* FX: LEDs + halo com a cor do ponteiro */
function drawLeds(){
  resizeCanvas(fx);
  const W=fx.width,H=fx.height,cx=W/2,cy=H/2;
  const r=Math.min(wheel.width,wheel.height)*0.40;
  const outer=r+44, inner=r+18;
  fxx.setTransform(1,0,0,1,0,0); fxx.clearRect(0,0,W,H);
  const t=performance.now()/1000;
  const bulbs=Math.max(72, Math.round(r*0.68));
  const bulbs2=Math.max(56, Math.round(r*0.52));
  for(let i=0;i<bulbs;i++){ const a=i/bulbs*TAU,x=cx+Math.cos(a)*outer,y=cy+Math.sin(a)*outer,k=(Math.cos(a - t*2.1)*.5+.5),sz=2.4+k*2.4;
    fxx.beginPath(); fxx.fillStyle=`rgba(255,240,200,${0.26+k*0.66})`; fxx.shadowColor='rgba(255,230,170,.85)'; fxx.shadowBlur=10+k*10; fxx.arc(x,y,sz,0,TAU); fxx.fill(); }
  for(let i=0;i<bulbs2;i++){ const a=i/bulbs2*TAU,x=cx+Math.cos(a)*inner,y=cy+Math.sin(a)*inner,k=(Math.cos(a + t*2.9)*.5+.5),sz2=2.0+k*2.0;
    fxx.beginPath(); fxx.fillStyle=`rgba(180,220,255,${0.20+k*0.48})`; fxx.shadowColor='rgba(200,220,255,.85)'; fxx.shadowBlur=9+k*9; fxx.arc(x,y,sz2,0,TAU); fxx.fill(); }
}
function drawFxBG(){
  resizeCanvas(fxBG);
  const W=fxBG.width,H=fxBG.height,cx=W/2,cy=H/2;
  const r=Math.min(wheel.width,wheel.height)*0.40;
  fxb.setTransform(1,0,0,1,0,0); fxb.clearRect(0,0,W,H);
  const outerR=Math.max(W,H)*0.55;

  const {r:rr,g:rg,b:rb} = parseHex(POINTER_COLOR);
  const halo=fxb.createRadialGradient(cx,cy,r*0.9,cx,cy,outerR);
  halo.addColorStop(0, `rgba(${rr},${rg},${rb},.12)`);
  halo.addColorStop(1, 'rgba(0,0,0,0)');
  fxb.fillStyle=halo; fxb.beginPath(); fxb.arc(cx,cy,outerR,0,TAU); fxb.fill();
}

/* modal & confete */
function openModal(){ resultModal.classList.add('open'); confetti=[]; modalPrevT=null; }
function closeModal(){ resultModal.classList.remove('open'); confetti=[]; modalPrevT=null; }
modalBackdrop.onclick = modalClose.onclick = ()=>closeModal();
function drawFxModal(){
  resizeCanvas(fxModal);
  const W=fxModal.width,H=fxModal.height;
  const t=performance.now()/1000;
  const dt=modalPrevT?Math.min(0.033,t-modalPrevT):1/60; modalPrevT=t;
  fxm.setTransform(1,0,0,1,0,0); fxm.clearRect(0,0,W,H);
  if(!resultModal.classList.contains('open') || !modalIsWin) return;
  const SPAWN=160, spawn=Math.floor(SPAWN*dt);
  for(let i=0;i<spawn;i++){
    const s=4+Math.random()*3;
    confetti.push({x:Math.random()*W,y:-10,vxBase:(Math.random()*2-1)*20,vy:80+Math.random()*180,rot:Math.random()*TAU,vr:(Math.random()*2-1)*4,w:s,h:s*2,hue:Math.floor(Math.random()*360),seed:Math.random()*TAU});
  }
  const G=40;
  for(let i=confetti.length-1;i>=0;i--){
    const p=confetti[i], sway=Math.sin((t+p.seed)*4)*30, vx=p.vxBase+sway;
    p.vy+=G*dt; p.x+=vx*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt;
    fxm.save(); fxm.translate(p.x,p.y); fxm.rotate(p.rot); fxm.fillStyle=`hsla(${p.hue},95%,60%,.95)`; fxm.fillRect(-p.w/2,-p.h/2,p.w,p.h); fxm.restore();
    if(p.y>H+24) confetti.splice(i,1);
  }
}

/* ======================= SINCRONIZAÇÃO COM SERVIDOR ======================= */
function findServerIndex(data){
  if (!data) return null;
  if (typeof data.index === 'number') return data.index;
  if (typeof data.chosen_index === 'number') return data.chosen_index;
  const cd = data.chosen_details || {};
  if (typeof cd.index === 'number') return cd.index;
  const id = (cd && (cd.id ?? cd.segment_id)) ?? (data.id ?? data.segment_id);
  if (id != null){
    const i = PRIZES.findIndex(p => String(p.id) === String(id));
    if (i >= 0) return i;
  }
  return null;
}
async function requestServerSpin(){
  try{
    const res = await fetch(API_SPIN, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ ts: Date.now() }) });
    const json = await res.json();
    try{
      console.group('🎰 ROULETTE DEBUG - API Response');
      console.log('Data:', json);
      if (json?.debug?.step) console.log('Forced step:', json.debug.step);
      console.groupEnd();
    }catch(_){}
    if (json?.debug?.step) window.__forcedStep = json.debug.step;
    const idx = findServerIndex(json);
    return { idx, raw: json };
  }catch(e){
    try{ console.warn('[SPIN] server error', e); }catch(_){}
    return { idx: null, raw: null, error: e };
  }
}
function ackResult(){
  try{
    fetch(API_ACK, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ index: spinIndex, ts: Date.now() }) }).catch(()=>{});
  }catch(_){}
}
/* ========================================================================== */

/* cálculo da desaceleração até o meio do setor target */
function planDecelExact(){
  const targetMid = sectorAngleMid(spinIndex);
  const finalLocal = normalize(-targetMid + POINTER_OFFSET);
  const curLocal   = normalize(currentAngle);
  let delta = finalLocal - curLocal; if (delta < 0) delta += TAU;

  const spinsTarget = Math.max(0, +ANIM_SPINS || 0);
  const doneSpins   = Math.max(0, anglePath / TAU);
  const extraTurns  = Math.max(0, spinsTarget - doneSpins);

  const v0 = Math.max(MAX_SPEED*0.55, Math.min(speed, MAX_SPEED));
  const D  = delta + extraTurns*TAU;
  const T  = Math.max(0.25, 2*D / v0);

  try{ console.log('[SPIN] planDecelExact target=%s done=%s extra=%s delta(rad)=%s D(rad)=%s v0=%s T=%s', spinsTarget, doneSpins.toFixed?doneSpins.toFixed(3):doneSpins, extraTurns.toFixed?extraTurns.toFixed(3):extraTurns, delta.toFixed?delta.toFixed(3):delta, D.toFixed?D.toFixed(3):D, v0.toFixed?v0.toFixed(3):v0, T.toFixed?T.toFixed(3):T); }catch(_){}

  decelStartAngle = currentAngle;
  decelTargetAngle = currentAngle + D - TARGET_SAFETY; // empurra levemente para dentro do setor
  decelDistance = D;
  decelTime = T;
  decelClock = 0;
  decelAccel = -v0 / T;
}
function startSpin(){
  if(phase!=='idle') return;
  spinIndex = null; // será definido pelo servidor
  phase='accelerate'; speed=0; cruiseClock=0; postAudioClock=0; audioEnded=false; anglePath=0; diagClock=0; resultTimeoutClock=0;
  try{ console.log('[SPIN] start - ANIM_SPINS=%s ANIM_SPEED=%s', ANIM_SPINS, ANIM_SPEED); }catch(_){}
  // dispara áudio de início
  inicio.onended = ()=>{ audioEnded = true; };
  if(!muted){
    try{ inicio.currentTime=0; inicio.play().catch(()=>{ audioEnded=true; }); }catch{ audioEnded=true; }
    if(bgm.paused){ try{ bgm.play(); }catch{} }
  } else { audioEnded = true; }
  // requisita o resultado ao servidor
  resultPromise = requestServerSpin().then(({idx}) => {
    if (typeof idx === 'number' && idx >= 0 && idx < N){
      spinIndex = idx;
      try{ console.log('[SPIN] server index =', spinIndex); }catch(_){}
    } else {
      try{ console.warn('[SPIN] server did not return a valid index'); }catch(_){}
    }
  });
}
function stopSpin(){
  try { const total=(anglePath/TAU); const diff=Math.abs((+ANIM_SPINS||0)-total); console.log('[SPIN] finish total=%s target=%s diff=%s', total.toFixed?total.toFixed(3):total, ANIM_SPINS, diff.toFixed?diff.toFixed(3):diff); } catch(_) {}

  // apenas loga; não sobrepõe o índice vindo do servidor
  const idxFromAngle = Math.floor(normalize(POINTER_OFFSET - currentAngle) / SLICE) % N;
  if (idxFromAngle !== spinIndex){
    try{ console.warn('[SPIN] WARNING: angle-derived index (%s) != spinIndex (%s)', idxFromAngle, spinIndex); }catch(_){}
  }

  const p=PRIZES[spinIndex]||{}; const type = String(p.kind || p.type || '').toUpperCase();
  modalIsWin = (type === 'WIN');
  if (window.__forcedStep === 'L') { modalIsWin = false; } // suporta force_pattern
  modalDetail.textContent = modalIsWin
    ? `Você ganhou ${p.primary||''} ${p.unit||''}.`
    : 'Não foi dessa vez. Tente novamente!';
  window.__forcedStep = '';

  if (modalIsWin){
    const base = (p.color || '#ef4444');
    modalBubble.style.display=''; modalBubble.style.background = `radial-gradient(60% 60% at 50% 40%, ${shade(base, 15)}, ${shade(base, -10)})`;
    const gb = (p.game || p.game_name || '').trim();
    if (gb){ modalGameInBubble.style.display = 'block'; modalGameInBubble.textContent = gb; }
    else { modalGameInBubble.style.display = 'none'; modalGameInBubble.textContent = ''; }
  } else { modalBubble.style.display='none'; modalGameInBubble.style.display='none'; modalGameInBubble.textContent=''; }

  if(/^\d+$/.test(String(p.primary||''))){
    modalQtyBig.style.display = 'block'; modalUnit.style.display = 'block';
    modalQtyBig.textContent = String(p.primary); modalUnit.textContent = String(p.unit||'GIROS'); modalH2.style.display = 'none';
  }else{
    modalQtyBig.style.display = 'none'; modalUnit.style.display = 'none'; modalH2.style.display = 'block';
    document.getElementById('modalTitle').textContent = modalIsWin ? 'Parabéns!' : 'Ah, que pena…';
  }

  const game = (p.game || p.game_name || '').trim();
  const url  = normalizeUrl(p.url  || p.game_url  || '');
  modalDetail.textContent = modalIsWin ? `Você ganhou ${p.primary||''} ${p.unit||''}.` : 'Não foi dessa vez. Tente novamente!';
  if (modalIsWin && game){ modalGame.style.display = 'block'; modalGame.innerHTML = 'Jogo: <strong>'+escHtml(game)+'</strong>'; }
  else { modalGame.style.display = 'none'; modalGame.innerHTML = ''; }

  if (modalIsWin && url){ modalCTA.textContent = 'Ir para o jogo'; modalCTA.onclick = () => window.open(url, '_blank', 'noopener'); }
  else { modalCTA.textContent = 'Fechar'; modalCTA.onclick = closeModal; }

  openModal(); try{ (modalIsWin?ganhou:perdeu).currentTime=0; (modalIsWin?ganhou:perdeu).play(); }catch{}
  ackResult();
}

/* loops */

function ensureAlignedThenStop(){
  // Confere índice derivado pelo ângulo atual
  const idxFromAngle = Math.floor(normalize(POINTER_OFFSET - currentAngle) / SLICE) % N;
  if (idxFromAngle === spinIndex){
    phase='idle'; stopSpin(); return;
  }
  // Planeja micro-correção (até 10% de um setor) até o centro do setor correto
  const target = centerAngleForIndex(spinIndex);
  let d = shortestDelta(currentAngle, target);
  // limita correção máxima para não ficar perceptível
  if (Math.abs(d) > MICRO_MAX){
    d = (d > 0 ? MICRO_MAX : -MICRO_MAX);
  }
  microTargetAngle = currentAngle + d;
  microClock = 0;
  phase = 'micro-correct';
  try{ console.warn('[SPIN] micro-correction deg=', (d*180/Math.PI).toFixed(2)); }catch(_){}
}
function animate(){
  const now=performance.now()/1000; const dt=lastT?Math.min(0.033, now-lastT):1/60; lastT=now;
  if(phase==='accelerate'){
    speed=Math.min(MAX_SPEED, speed+ACCEL*dt); currentAngle+=speed*dt; anglePath+=speed*dt; maybeTick(currentAngle);
    diagClock+=dt; if(diagClock>=0.5){ try{ console.log('[SPIN] phase=accelerate spins=%s speed=%s rpm=%s', (anglePath/TAU).toFixed(2), speed.toFixed(2), (speed/TAU*60).toFixed(1)); }catch(_){ } diagClock=0; }
    if(speed>=MAX_SPEED-1e-3){ phase='cruise'; cruiseClock=0; postAudioClock=0; }
  }
  else if(phase==='cruise'){
    cruiseClock+=dt; if(audioEnded) postAudioClock+=dt;
    currentAngle+=speed*dt; anglePath+=speed*dt; maybeTick(currentAngle);
    diagClock+=dt; if(diagClock>=0.5){ try{ console.log('[SPIN] phase=cruise spins=%s speed=%s rpm=%s', (anglePath/TAU).toFixed(2), speed.toFixed(2), (speed/TAU*60).toFixed(1)); }catch(_){ } diagClock=0; }

    const timeOk = (audioEnded && postAudioClock>=POST_AUDIO_CRUISE_T && cruiseClock>=MIN_TOTAL_CRUISE_T) || (cruiseClock>=MAX_WAIT_FOR_AUDIO);
    if (timeOk){
      // espera resultado do servidor, com timeout e fallback
      if (spinIndex == null){
        resultTimeoutClock += dt;
        if (resultTimeoutClock >= MAX_WAIT_RESULT){
          spinIndex = fallbackWeightedPick();
          try{ console.warn('[SPIN] fallback index =', spinIndex); }catch(_){}
        }
      }
      if (spinIndex != null){
        planDecelExact(); phase='decelerate';
      }
    }
  }
  else if(phase==='decelerate'){
    decelClock+=dt; speed = Math.max(0, speed + decelAccel*dt);
    currentAngle += speed*dt; anglePath+=Math.max(0,speed*dt); maybeTick(currentAngle);
    diagClock+=dt; if(diagClock>=0.5){ try{ console.log('[SPIN] phase=decelerate spins=%s speed=%s rpm=%s', (anglePath/TAU).toFixed(2), speed.toFixed(2), (speed/TAU*60).toFixed(1)); }catch(_){ } diagClock=0; }
    if((currentAngle - decelStartAngle) >= decelDistance - 1e-4 || decelClock>=decelTime){
      currentAngle = decelTargetAngle; speed=0; ensureAlignedThenStop();
    }
  }
  else if(phase==='micro-correct'){
    microClock += dt;
    // easing out (quadrático)
    const t = Math.min(1, microClock / MICRO_TIME);
    const ease = 1 - (1 - t)*(1 - t);
    // interpola do ângulo atual (ao entrar) até o alvo
    // Nota: ao entrar no estado, microTargetAngle já foi definido. Precisamos do start fixo:
    if (!animate.__microStart){ animate.__microStart = currentAngle; }
    const start = animate.__microStart;
    currentAngle = start + shortestDelta(start, microTargetAngle) * ease;
    maybeTick(currentAngle);
    if (t >= 1){
      animate.__microStart = null;
      phase='idle'; stopSpin();
    }
  }
  drawWheel((currentAngle%TAU+TAU)%TAU); requestAnimationFrame(animate);
}
function drawAll(){ fitWheelToViewport(); resizeCanvas(wheel); resizeCanvas(fx); resizeCanvas(fxBG); drawWheel(currentAngle); drawLeds(); drawFxBG(); drawFxModal(); }
function fxLoop(){ drawLeds(); drawFxBG(); drawFxModal(); requestAnimationFrame(fxLoop); }

/* ===== interações ===== */
function tryStart(e){
  e.preventDefault();
  if(!muted && bgm.paused){ try{ bgm.play(); }catch{} }
  if(phase==='idle') startSpin();
}
wheelbox.addEventListener('click', tryStart);
wheelbox.addEventListener('touchstart', tryStart, {passive:false});

/* aplicar pointer (cor/posição) */
function applyPointerColor(hex){
  const base = parseHex(hex).hex;
  const hi   = shade(base, 18);
  const mid  = base;
  const lo   = shade(base, -24);
  const rim  = shade(base, -38);
  pointer.style.setProperty('--pc', base);
  pointer.style.setProperty('--pc-hi', hi);
  pointer.style.setProperty('--pc-mid', mid);
  pointer.style.setProperty('--pc-lo', lo);
  pointer.style.setProperty('--pc-rim', rim);
}
function applyPointerPosition(pos){
  const p = String(pos||'right').toLowerCase();
  pointer.classList.remove('pos-top','pos-right','pos-bottom','pos-left');
  pointer.classList.add(
    p==='top' ? 'pos-top' :
    p==='bottom' ? 'pos-bottom' :
    p==='left' ? 'pos-left' : 'pos-right'
  );
  POINTER_OFFSET = getPointerOffset(p);
}

/* start */
addEventListener('resize', fitWheelToViewport);
titleEl.textContent = document.title || 'Roleta';
fitWheelToViewport(); drawAll();
applyPointerColor(POINTER_COLOR);
applyPointerPosition(POINTER_POSITION);
requestAnimationFrame(animate); requestAnimationFrame(fxLoop);

// ===== DEBUG EXTRA =====
(function() {
  const originalFetch = window.fetch;
  window.fetch = async function(...args) {
    const response = await originalFetch.apply(this, args);
    const clone = response.clone();
    if (args[0] && (String(args[0]).includes('/api/spin.php') || String(args[0]).includes('/api/spin_ack.php'))) {
      clone.json().then(data => {
        console.group('🎰 ROULETTE DEBUG - API Response');
        console.log('URL:', args[0]);
        console.log('Method:', args[1]?.method || 'GET');
        console.log('Data:', data);
        if (data && data.debug) {
          console.group('🔍 Detailed Debug Info');
          console.log('Pattern:', data.debug.pattern || 'none');
          console.log('Step:', data.debug.step || 'none');
          console.log('ACK Count:', data.debug.ack_count || 0);
          if (data.debug.segments) {
            console.group('📊 Segments Analysis');
            data.debug.segments.forEach((seg, index) => {
              console.log(`Segment ${index}:`, seg);
            });
            console.groupEnd();
          }
          if (data.debug.chosen_details) {
            console.group('✅ Chosen Segment');
            console.log('ID:', data.debug.chosen_details.id);
            console.log('Index:', data.debug.chosen_details.index);
            console.log('Type:', data.debug.chosen_details.type);
            console.log('Weight:', data.debug.chosen_details.weight);
            console.log('Effective Weight:', data.debug.chosen_details.effective_weight);
            console.log('Forced:', data.debug.chosen_details.forced);
            console.groupEnd();
          }
          console.groupEnd();
        }
        console.groupEnd();
      }).catch(() => {});
    }
    return response;
  };

  const originalStartSpin = window.startSpin;
  window.startSpin = function() {
    console.group('🎯 ROULETTE SPIN STARTED');
    console.log('Timestamp:', new Date().toISOString());
    console.log('Current Phase:', typeof phase !== 'undefined' ? phase : 'undefined');
    console.groupEnd();
    return originalStartSpin.apply(this, arguments);
  };

  const originalStopSpin = window.stopSpin;
  window.stopSpin = function() {
    console.group('🛑 ROULETTE SPIN STOPPED');
    console.log('Timestamp:', new Date().toISOString());
    console.log('Final Angle:', currentAngle);
    console.log('Spin Index:', spinIndex);
    if (PRIZES[spinIndex]) {
      console.log('Prize:', PRIZES[spinIndex]);
      console.log('Prize Weight:', PRIZES[spinIndex].weight);
      console.log('Effective Weight:', (PRIZES[spinIndex].weight <= 1) ? 0 : PRIZES[spinIndex].weight);
    }
    console.groupEnd();
    return originalStopSpin.apply(this, arguments);
  };

  console.group('🎰 ROULETTE INITIALIZATION');
  console.log('Total Segments:', PRIZES.length);
  console.log('Segment Weights:');
  PRIZES.forEach((prize, index) => {
    console.log(`  ${index}: ${prize.weight} (effective: ${prize.weight <= 1 ? 0 : prize.weight}) - ${prize.primary} ${prize.unit}`);
  });
  console.groupEnd();
})();
</script>
</body>
</html>
<script>\n
// ===== server-authoritative spin injection (auto) =====
(function(){
  try{
    (function(){
    const p = location.pathname;
    const i = p.indexOf('/public/');
    const base = (i >= 0) ? p.slice(0, i) : '';
    window.API_SPIN = base + '/api/spin.php';
    window.API_ACK  = base + '/api/spin_ack.php';
    window.ROULETTE_ID = (typeof window.ROULETTE_ID !== 'undefined') ? window.ROULETTE_ID : %ROULETTE_ID%;
    })();
  }catch(e){}

  var __tok = null;
  var __origStop  = (typeof window.stopSpin === 'function') ? window.stopSpin : null;

  // Substitui startSpin para buscar o resultado no servidor (form-urlencoded) e só então animar
  window.startSpin = async function(){
    if (typeof phase !== 'undefined' && phase !== 'idle') return;
    try{
      var params = new URLSearchParams();
      if (typeof window.ROULETTE_ID !== 'undefined') params.set('roleta', String(window.ROULETTE_ID));
      var resp = await fetch(window.API_SPIN, {
        method: 'POST',
        credentials: 'same-origin',
        headers: {
          'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
          'Accept':'application/json',
          'X-Requested-With':'XMLHttpRequest'
        },
        body: params
      });
      var text = await resp.text();
      var data;
      try { data = JSON.parse(text); } catch (_){ throw new Error('Resposta não-JSON do servidor: ' + text.slice(0,160)); }
      if (!resp.ok || !data || !data.ok) throw new Error((data && data.error) ? data.error : ('HTTP '+resp.status));

      // === DEBUG: Loga o JSON completo do server no console ===
      console.log('[SPIN DEBUG] Resposta completa do servidor:', data);
      console.log('[SPIN DEBUG] Método de seleção:', data.__debug.selection_method);
      console.log('[SPIN DEBUG] Pools:', data.__debug.pool_all, data.__debug.pool_wins, data.__debug.pool_loses);
      console.log('[SPIN DEBUG] Escolhido:', data.__debug.picked);
      console.log('[SPIN DEBUG] Pattern/Step:', data.__debug.pattern, '/', data.__debug.step);
      console.log('[SPIN DEBUG] Index imposto:', data.index, 'Forced:', data.forced);

      __tok = data;
      if (data && data.verify) { __tok.nonce = data.verify.nonce; __tok.hmac = data.verify.hmac; } // <-- NOVO
      window.__forcedStep = (data.forced || ''); // mantém
      spinIndex = parseInt(data.index, 10);      // índice do SERVIDOR

      // inicia sua animação padrão
      if (typeof phase !== 'undefined'){ phase='accelerate'; speed=0; cruiseClock=0; postAudioClock=0; audioEnded=false; }
      try{
        if (typeof inicio!=='undefined' && inicio){
          inicio.onended = function(){ audioEnded = true; };
          if (!muted){
            try { inicio.currentTime=0; await inicio.play(); } catch (e) { audioEnded = true; }
          } else { audioEnded = true; }
        } else { audioEnded = true; }
        if (typeof bgm!=='undefined' && bgm && !muted){ try { await bgm.play(); } catch(e){} }
      }catch(e){}
    }catch(err){
      console.error('[SPIN ERROR]', err);
      alert('Erro ao sortear. Tente novamente.');
    }
  };

  // Envia ACK quando a animação terminar
  window.stopSpin = function(){
    try{ if (__origStop) __origStop(); }catch(e){}
    if (__tok){
      try{
        var ack = new URLSearchParams();
        ack.set('spin_id', String(__tok.spin_id));
        if (__tok.segment_id) ack.set('segment_id', String(__tok.segment_id));
        if (__tok.index !== undefined) ack.set('index', String(__tok.index));
        ack.set('nonce', __tok.nonce);
        ack.set('hmac', __tok.hmac);
        ack.set('expires', String(__tok.expires));

        fetch(window.API_ACK, {
          method:'POST',
          credentials:'same-origin',
          headers:{
            'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
            'Accept':'application/json',
            'X-Requested-With':'XMLHttpRequest'
          },
          body: ack
        }).then(resp => {
          console.log('[ACK DEBUG] Resposta ACK:', resp.ok ? 'Sucesso' : 'Falha');
        });
      }catch(e){
        console.error('[ACK ERROR]', e);
      }
      __tok = null;
    }
  };
})();
// ===== end server-authoritative spin injection =====
\n</script>