<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rifa IGSA – 21 Autos (Precargada)</title>
<style>
:root { --bg:#0b1020; --card:#121935; --accent:#00e0ff; --accent2:#72ff9f; --text:#e9f0ff; --muted:#8aa1c2; --danger:#ff6b6b; --gold:#ffd166; }
*{box-sizing:border-box}
body{margin:0;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,"Helvetica Neue",Noto Sans,Arial,"Apple Color Emoji","Segoe UI Emoji";background:radial-gradient(1200px 800px at 20% 10%,#19234d 0%,#0b1020 60%,#080b17 100%);color:var(--text)}
.wrap{max-width:1200px;margin:0 auto;padding:24px}
header{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
h1{font-size:clamp(22px,3vw,32px);margin:8px 0;letter-spacing:.3px}
.badge{font-size:12px;padding:6px 10px;border:1px solid #2b386a;border-radius:999px;color:var(--muted)}
.grid{display:grid;grid-template-columns:1.1fr .9fr;gap:24px}
@media(max-width:980px){.grid{grid-template-columns:1fr}}
.card{background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.02));border:1px solid rgba(255,255,255,.08);border-radius:18px;box-shadow:0 10px 30px rgba(0,0,0,.3)}
.card-head{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:space-between}
.card-body{padding:18px 20px}
.controls{display:flex;gap:10px;flex-wrap:wrap}
button,.btn{background:linear-gradient(180deg,rgba(0,224,255,.18),rgba(0,224,255,.08));border:1px solid rgba(0,224,255,.35);color:var(--text);padding:10px 14px;border-radius:12px;cursor:pointer;font-weight:600;letter-spacing:.2px;transition:transform .05s ease,box-shadow .2s ease}
button:hover{box-shadow:0 0 0 3px rgba(0,224,255,.18) inset}
button:active{transform:translateY(1px)}
button.secondary{background:transparent;border-color:#3b4778}
button.danger{background:linear-gradient(180deg,rgba(255,107,107,.18),rgba(255,107,107,.08));border-color:rgba(255,107,107,.5)}
button.gold{background:linear-gradient(180deg,rgba(255,209,102,.25),rgba(255,209,102,.08));border-color:rgba(255,209,102,.6);color:#0b0f1e}
.row{display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.field{display:flex;flex-direction:column;gap:6px}
.field label{font-size:12px;color:var(--muted)}
input[type=number],input[type=text],textarea,select{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);color:var(--text);padding:10px 12px;border-radius:12px;outline:none;min-width:120px}
textarea{min-height:120px;width:100%}
.stage{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:stretch}
@media(max-width:740px){.stage{grid-template-columns:1fr}}
.roller{background:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,.02));border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:14px;text-align:center;position:relative;overflow:hidden}
.roller h3{margin:0 0 10px;font-weight:700;letter-spacing:.3px;color:var(--muted)}
.window{height:96px;border-radius:12px;background:radial-gradient(400px 120px at 50% 0%,rgba(255,255,255,.08),rgba(255,255,255,.02));display:flex;align-items:center;justify-content:center;font-size:52px;font-weight:800;letter-spacing:1px;text-shadow:0 8px 24px rgba(0,0,0,.55);border:1px solid rgba(255,255,255,.1)}
.window.big{height:120px;font-size:64px}
.blink{animation:blink 1s infinite}
@keyframes blink{0%,60%{opacity:1}70%{opacity:.5}100%{opacity:1}}
.result{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}
.result div{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:10px 12px}
.muted{color:var(--muted);font-size:13px}
table{width:100%;border-collapse:collapse}
th,td{padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.08);font-size:14px}
th{text-align:left;color:var(--muted);font-weight:600}
tbody tr:hover{background:rgba(255,255,255,.03)}
.pill{display:inline-block;padding:4px 8px;border-radius:999px;font-size:11px;border:1px solid rgba(255,255,255,.16)}
.footer{color:var(--muted);font-size:12px;text-align:center;padding:10px}
.hidden{display:none!important}
.confetti{position:fixed;inset:0;pointer-events:none;overflow:hidden}
.confetti i{position:absolute;width:10px;height:16px;background:var(--accent);top:-20px;opacity:.9;transform:rotate(0deg);animation:fall linear forwards}
@keyframes fall{to{transform:translateY(110vh) rotate(360deg);opacity:1}}
#messageBar{margin-top:12px;padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.04);font-size:14px;display:none}
#messageBar.ok{border-color:rgba(114,255,159,.5)}
#messageBar.err{border-color:rgba(255,107,107,.5)}
</style>
</head>
<body>
<div class="wrap">
<header>
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M12 2l2.39 4.84 5.34.78-3.86 3.77.91 5.32L12 14.77 6.22 16.71l.91-5.32L3.27 7.62l5.34-.78L12 2z" fill="url(#g)"/>
<defs><linearGradient id="g" x1="0" y1="0" x2="24" y2="24"><stop stop-color="#00E0FF"/><stop offset="1" stop-color="#72FF9F"/></linearGradient></defs>
</svg>
<div>
<h1>Rifa IGSA · 21 Autos</h1>
<div class="badge">Seguro · Transparente · Divertido</div>
</div>
</header>
<div class="grid" id="app">
<section class="card">
<div class="card-head">
<strong>Configuración y sorteo</strong>
<div class="controls">
<button id="btnFullscreen" class="secondary" title="Pantalla completa">Pantalla completa</button>
<button id="btnExport" class="secondary" title="Exportar resultados CSV">Exportar resultados</button>
<button id="btnReset" class="danger" title="Reiniciar rifa">Reiniciar</button>
</div>
</div>
<div class="card-body">
<div class="row" style="margin-bottom:12px;">
<div class="field">
<label>Total de participantes</label>
<input type="number" id="totalParticipants" value="900" min="1" />
</div>
<div class="field" style="min-width:280px;">
<label>Participantes válidos (lista y rangos)</label>
<input type="text" id="validList" placeholder="Ej. 1-900, 12, 34, 100-120" />
<div class="muted">Si lo dejas vacío, se consideran válidos 1..N. Rangos separados por comas.</div>
</div>
<div class="field">
<label>Lista de autos (CSV) – opcional</label>
<input type="file" id="carsCsv" accept=".csv" />
<div class="muted">Ya incluye tus 21 autos precargados. Puedes reemplazarlos cargando un CSV. Formato: <code>id,descripcion,modelo,serie,kilometraje</code></div>
</div>
<div class="field">
<label>Semilla opcional (para reproducir)</label>
<input type="text" id="seedInput" placeholder="Ej. IGSA-2025" />
</div>
</div>
<div class="stage">
<div class="roller">
<h3>Ganador (1…N)</h3>
<div class="window big" id="winnerWindow">—</div>
</div>
<div class="roller">
<h3>Auto asignado</h3>
<div class="window big" id="carWindow">—</div>
</div>
</div>
<div class="controls" style="margin-top:14px;">
<button id="btnDraw" class="gold">🎉 ¡Sortear!</button>
<button id="btnUndo" class="secondary">↩️ Deshacer último</button>
</div>
<div id="messageBar"></div>
<div class="result" id="resultDetail" style="display:none;">
<div>
<div class="muted">Ganador</div>
<div id="resultWinner" style="font-weight:800; font-size:20px;">—</div>
</div>
<div>
<div class="muted">Auto</div>
<div id="resultCar" style="font-weight:800; font-size:20px;">—</div>
</div>
</div>
</div>
</section>
<aside class="card">
<div class="card-head"><strong>Estado de la rifa</strong>
<span class="pill" id="statusCounters">Participantes únicos: 0 · Autos restantes: 0</span>
</div>
<div class="card-body" style="max-height:58vh;overflow:auto;">
<table>
<thead>
<tr>
<th>#</th>
<th>Ganador</th>
<th>Auto</th>
<th>Modelo</th>
<th>Serie</th>
<th>Km</th>
<th>Hora</th>
</tr>
</thead>
<tbody id="resultsBody"></tbody>
</table>
</div>
</aside>
</div>
<div id="confetti" class="confetti hidden"></div>
<div class="footer">Hecho para IGSA · Uso interno de evento · Precargado con tu listado · Imparcialidad: aleatoriedad segura (<code>crypto.getRandomValues</code>) y validación manual (sin perder premios por intentos inválidos).</div>
</div>
<script>
const EMBEDDED_CARS = [{"id": "", "descripcion": "NP300 FRONT", "modelo": "2019", "serie": "", "kilometraje": "171,603"}, {"id": "", "descripcion": "NP300 FR", "modelo": "2019", "serie": "", "kilometraje": ""}, {"id": "", "descripcion": "NP300 FRONTIER", "modelo": "2019", "serie": "", "kilometraje": "126,474"}, {"id": "", "descripcion": "MARCH TM", "modelo": "2019", "serie": "", "kilometraje": "126,831"}, {"id": "", "descripcion": "MARCH TM AC", "modelo": "2019", "serie": "", "kilometraje": "111,425"}, {"id": "", "descripcion": "MARCH TM", "modelo": "2020", "serie": "", "kilometraje": "77,768"}, {"id": "", "descripcion": "MARCH COUPE TM", "modelo": "2021", "serie": "", "kilometraje": "105,106"}, {"id": "", "descripcion": "MARCH TM", "modelo": "2021", "serie": "", "kilometraje": "128,190"}, {"id": "", "descripcion": "MARCH TM", "modelo": "2022", "serie": "", "kilometraje": "85,474"}, {"id": "", "descripcion": "TAOS HIGHLINE", "modelo": "2023", "serie": "", "kilometraje": "8,933"}, {"id": "", "descripcion": "JETTA DSG", "modelo": "2022", "serie": "", "kilometraje": "25,874"}, {"id": "", "descripcion": "VIRTUS COMFORTLINE", "modelo": "2023", "serie": "", "kilometraje": "39,419"}, {"id": "", "descripcion": "TIGUAN COMFORTLINE", "modelo": "2022", "serie": "", "kilometraje": "60,665"}, {"id": "", "descripcion": "JETTA TIPTRONIC", "modelo": "2019", "serie": "", "kilometraje": "84,626"}, {"id": "", "descripcion": "ATLAS TRENDLINE", "modelo": "2021", "serie": "", "kilometraje": "59,662"}, {"id": "", "descripcion": "JETTA TIPTRONIC", "modelo": "2021", "serie": "", "kilometraje": "41,760"}, {"id": "", "descripcion": "KWID INTENSE TM", "modelo": "2022", "serie": "", "kilometraje": "66,225"}, {"id": "", "descripcion": "TRACKER TURBO AT", "modelo": "2022", "serie": "", "kilometraje": "69,997"}, {"id": "", "descripcion": "AVEOLINE LT", "modelo": "2019", "serie": "", "kilometraje": "56,727"}, {"id": "", "descripcion": "ONIX TURBO", "modelo": "2021", "serie": "", "kilometraje": "78,758"}, {"id": "", "descripcion": "TRAILBLAZER", "modelo": "2021", "serie": "", "kilometraje": "52,918"}];
</script>
<script>
const $ = (sel)=>document.querySelector(sel);
const $$ = (sel)=>document.querySelectorAll(sel);
function formatTime(d=new Date()){return d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}
function csvEscape(v){if(v==null)return'';const s=String(v);return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s}
function showMsg(txt,type='ok'){const bar=$('#messageBar');bar.className='';bar.classList.add(type==='ok'?'ok':'err');bar.textContent=txt;bar.style.display='block';clearTimeout(showMsg._t);showMsg._t=setTimeout(()=>{bar.style.display='none'},4200)}
function xmur3(str){let h=1779033703^str.length;for(let i=0;i<str.length;i++){h=Math.imul(h^str.charCodeAt(i),3432918353);h=(h<<13)|(h>>>19)}return function(){h=Math.imul(h^(h>>>16),2246822507);h=Math.imul(h^(h>>>13),3266489909);h^=h>>>16;return h>>>0}}
function mulberry32(a){return function(){var t=a+=0x6D2B79F5;t=Math.imul(t^t>>>15,t|1);t^=t+Math.imul(t^t>>>7,t|61);return((t^t>>>14)>>>0)/4294967296}}
function secureRandomInt(min,max){const range=max-min+1;if(window.crypto&&crypto.getRandomValues&&range<=2**32){const maxUnbiased=Math.floor(0xFFFFFFFF/range)*range;let x=new Uint32Array(1);do{crypto.getRandomValues(x)}while(x[0]>=maxUnbiased);return min+(x[0]%range)}else{return Math.floor(Math.random()*range)+min}}
function seededInt(rand,min,max){return Math.floor(rand()*(max-min+1))+min}
let participantsTotal=900;
let participantsDrawn=new Set();
let validSet=null;
let cars=[]; let carsRemaining=[]; let results=[]; let drawCount=0; let seeded=false; let seededRand=null;
function updateCounters(){$('#statusCounters').textContent=`Participantes únicos: ${participantsDrawn.size} · Autos restantes: ${carsRemaining.length}`}
function renderResults(){const tbody=$('#resultsBody');tbody.innerHTML=results.map((r,idx)=>{const a=r.auto||{};return `<tr><td>${idx+1}</td><td>#${r.ganador}</td><td>${csvEscape(a.descripcion||'-')}</td><td>${csvEscape(a.modelo||'-')}</td><td><span class="pill">${csvEscape(a.serie||'-')}</span></td><td>${csvEscape(a.kilometraje||'-')}</td><td>${r.time}</td></tr>`}).join('')}
function makeConfetti(){const layer=$('#confetti');layer.innerHTML='';const n=120;const colors=['#00e0ff','#72ff9f','#ffd166','#ffffff'];for(let i=0;i<n;i++){const el=document.createElement('i');el.style.left=Math.random()*100+'vw';el.style.animationDuration=2.5+Math.random()*2+'s';el.style.background=colors[i%colors.length];el.style.transform=`translateY(-20px) rotate(${Math.random()*360}deg)`;layer.appendChild(el)}layer.classList.remove('hidden');setTimeout(()=>layer.classList.add('hidden'),3600)}
function spinWindow(el,finalText,duration=2200){const start=performance.now();const tick=(now)=>{const t=now-start;if(t<duration){el.textContent=seeded?seededInt(seededRand,1,999):secureRandomInt(1,999);requestAnimationFrame(tick)}else{el.textContent=finalText;el.classList.add('blink');setTimeout(()=>el.classList.remove('blink'),2000)}};requestAnimationFrame(tick)}
function parseCSV(text){const lines=text.split(/\r?\n/).filter(Boolean);const out=[];const headers=lines[0].split(',').map(h=>h.trim().toLowerCase());const idx={id:headers.indexOf('id'),descripcion:headers.indexOf('descripcion'),modelo:headers.indexOf('modelo'),serie:headers.indexOf('serie'),kilometraje:headers.indexOf('kilometraje')};for(let i=1;i<lines.length;i++){const row=csvSmartSplit(lines[i]);if(!row.length)continue;out.push({id:row[idx.id]||String(i),descripcion:row[idx.descripcion]||'',modelo:row[idx.modelo]||'',serie:row[idx.serie]||'',kilometraje:row[idx.kilometraje]||''})}return out}
function csvSmartSplit(line){const res=[];let cur='';let inQ=false;for(let i=0;i<line.length;i++){const ch=line[i];if(ch==='"'){if(inQ&&line[i+1]==='"'){cur+='"';i++}else inQ=!inQ}else if(ch===','&&!inQ){res.push(cur);cur=''}else cur+=ch}res.push(cur);return res.map(s=>s.trim())}
function exportCSV(){const header=['orden','ganador','id_auto','descripcion','modelo','serie','kilometraje','hora'];const lines=[header.join(',')];results.forEach((r,idx)=>{const a=r.auto||{};lines.push([idx+1,r.ganador,csvEscape(a.id),csvEscape(a.descripcion),csvEscape(a.modelo),csvEscape(a.serie),csvEscape(a.kilometraje),r.time].join(','))});const blob=new Blob([lines.join('\n')],{type:'text/csv;charset=utf-8;'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download='resultados_rifa_IGSA.csv';a.click();URL.revokeObjectURL(url)}
function setSeedFromInput(){const seedStr=($('#seedInput').value||'').trim();if(seedStr){const seedFn=xmur3(seedStr);const s1=seedFn();seededRand=mulberry32(s1);seeded=true}else{seeded=false;seededRand=null}}
function parseValidList(){const txt=($('#validList').value||'').trim();if(!txt){validSet=null;return}const parts=txt.split(',').map(s=>s.trim()).filter(Boolean);const set=new Set();for(const p of parts){const dash=p.indexOf('-');if(dash>-1){let a=parseInt(p.slice(0,dash),10);let b=parseInt(p.slice(dash+1),10);if(Number.isFinite(a)&&Number.isFinite(b)){if(a>b){const t=a;a=b;b=t}for(let n=a;n<=b;n++)set.add(n)}}else{const n=parseInt(p,10);if(Number.isFinite(n))set.add(n)}}validSet=set.size?set:null}
function isWinnerValid(n){if(n<1||n>participantsTotal)return false;if(participantsDrawn.has(n))return false;if(validSet&&!validSet.has(n))return false;return true}
function loadEmbeddedCars(){cars=(Array.isArray(EMBEDDED_CARS)?EMBEDDED_CARS:[]).map((x,i)=>({id:String(x.id||''),descripcion:String(x.descripcion||''),modelo:String(x.modelo||''),serie:String(x.serie||''),kilometraje:String(x.kilometraje||''),_idx:i}));carsRemaining=cars.map((_,i)=>i)}
function drawOnce(){if(carsRemaining.length===0){alert('Todos los autos ya fueron asignados.');return}setSeedFromInput();parseValidList();let winner;const limit=participantsTotal*3;let tries=0;while(true){winner=seeded?seededInt(seededRand,1,participantsTotal):secureRandomInt(1,participantsTotal);if(!participantsDrawn.has(winner))break;tries++;if(tries>limit){alert('No quedan participantes disponibles.');return}}spinWindow($('#winnerWindow'),`#${winner}`);setTimeout(()=>{if(!isWinnerValid(winner)){$('#winnerWindow').style.borderColor='rgba(255,107,107,.8)';setTimeout(()=>$('#winnerWindow').style.borderColor='rgba(255,255,255,.1)',1200);showMsg(`Número #${winner} inválido o no permitido. El premio NO se asignó. Vuelve a sortear.`,'err');return}const idxInRemain=seeded?seededInt(seededRand,0,carsRemaining.length-1):secureRandomInt(0,carsRemaining.length-1);const carIndex=carsRemaining[idxInRemain];const car=cars[carIndex];const displayCarLabel=(car.id&&String(car.id).trim())?`#${car.id}`:`Auto ${carIndex+1}`;spinWindow($('#carWindow'),displayCarLabel);setTimeout(()=>{carsRemaining.splice(idxInRemain,1);participantsDrawn.add(winner);const rec={n:++drawCount,ganador:winner,auto:car,time:formatTime()};results.push(rec);$('#resultWinner').textContent=`Participante #${winner}`;$('#resultCar').textContent=`${car.descripcion} · Modelo ${car.modelo} · Serie ${car.serie}`;$('#resultDetail').style.display='grid';renderResults();updateCounters();makeConfetti();showMsg(`¡Ganador #${winner}! Premio asignado: ${car.descripcion}`,'ok')},2300)},2300)}
function undoLast(){if(!results.length)return;const last=results.pop();participantsDrawn.delete(last.ganador);const carIdx=cars.findIndex(a=>String(a.id)===String(last.auto.id));if(carIdx>=0)carsRemaining.push(carIdx);drawCount--;if(drawCount<0)drawCount=0;renderResults();updateCounters();$('#winnerWindow').textContent='—';$('#carWindow').textContent='—';$('#resultDetail').style.display=results.length?'grid':'none'}
function resetAll(){if(!confirm('¿Reiniciar la rifa? Esto borrará los resultados actuales.'))return;participantsTotal=parseInt($('#totalParticipants').value||'900',10);participantsDrawn=new Set();results=[];drawCount=0;setSeedFromInput();parseValidList();if(!cars.length)loadEmbeddedCars();carsRemaining=cars.map((_,i)=>i);renderResults();updateCounters();$('#winnerWindow').textContent='—';$('#carWindow').textContent='—';$('#resultDetail').style.display='none';showMsg('Rifa reiniciada.','ok')}
function goFullscreen(){const el=document.documentElement;if(!document.fullscreenElement){el.requestFullscreen?.()}else{document.exitFullscreen?.()}}
$('#btnDraw').addEventListener('click',drawOnce);
$('#btnUndo').addEventListener('click',undoLast);
$('#btnReset').addEventListener('click',resetAll);
$('#btnFullscreen').addEventListener('click',goFullscreen);
$('#btnExport').addEventListener('click',exportCSV);
$('#carsCsv').addEventListener('change',e=>{const file=e.target.files?.[0];if(!file)return;const fr=new FileReader();fr.onload=()=>{try{const text=String(fr.result);const parsed=parseCSV(text);if(!parsed.length)throw new Error('CSV vacío o sin encabezados válidos');cars=parsed;carsRemaining=cars.map((_,i)=>i);participantsDrawn=new Set();results=[];drawCount=0;renderResults();updateCounters();alert(`Autos cargados: ${cars.length}`)}catch(err){alert('Error al leer CSV: '+err.message)}};fr.readAsText(file)});
loadEmbeddedCars();
parseValidList();
updateCounters();
</script>
</body>
</html>