Wheel of Names /* ============================================================
RESET & VARIABLES
============================================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }:root {
--bg: #0f0f1a;
--sidebar: #161625;
--card: #1e1e30;
--inp: #252538;
--bdr: #2e2e48;
--bdr2: #3a3a58;
--txt: #e8e8f4;
--muted: #8888aa;
--hint: #55556a;
--blue: #4a9eff;
--blue2: #2a7edd;
--red: #e24b4a;
--red2: #b83a39;
--font: 'Quicksand', system-ui, sans-serif;
--topbar-h: 54px;
--sidebar-w: 300px;
}html { height: 100%; }body {
font-family: var(--font);
background: var(--bg);
color: var(--txt);
min-height: 100%;
overflow-x: hidden;
}button { cursor: pointer; font-family: var(--font); border: none; background: none; color: inherit; }
input { font-family: var(--font); }::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-thumb { background: var(--bdr2); border-radius: 4px; }/* ============================================================
APP SHELL
============================================================ */
#app {
display: flex;
flex-direction: column;
height: 100dvh; /* dynamic viewport height — mobile safe */
min-height: 500px;
}/* ============================================================
TOPBAR
============================================================ */
#topbar {
display: flex;
align-items: center;
gap: 10px;
padding: 0 16px;
height: var(--topbar-h);
min-height: var(--topbar-h);
background: var(--sidebar);
border-bottom: 1px solid var(--bdr);
flex-shrink: 0;
z-index: 30;
}.logo {
display: flex;
align-items: center;
gap: 9px;
font-size: 16px;
font-weight: 700;
color: var(--txt);
text-decoration: none;
white-space: nowrap;
}.logo-icon {
width: 34px; height: 34px;
background: #3369e8;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}.logo span { display: none; }.topbar-right { margin-left: auto; display: flex; gap: 6px; align-items: center; }.tbtn {
display: flex; align-items: center; gap: 5px;
padding: 6px 12px;
border: 1px solid var(--bdr2);
border-radius: 7px;
font-size: 12px; font-weight: 600;
color: var(--muted);
transition: background .15s, color .15s;
white-space: nowrap;
}
.tbtn:hover { background: var(--card); color: var(--txt); }
.tbtn.primary { background: #3369e8; color: #fff; border-color: #3369e8; }
.tbtn.primary:hover { background: #1a4fc4; }
.tbtn .lbl { display: none; }/* hamburger */
#menu-toggle {
display: flex; flex-direction: column; gap: 4px;
padding: 6px; border-radius: 6px;
transition: background .15s;
}
#menu-toggle:hover { background: var(--card); }
#menu-toggle span {
display: block; width: 20px; height: 2px;
background: var(--txt); border-radius: 2px;
transition: transform .25s, opacity .25s;
}
#menu-toggle.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
#menu-toggle.open span:nth-child(2) { opacity: 0; }
#menu-toggle.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }/* ============================================================
MAIN LAYOUT
============================================================ */
#main {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
}/* ============================================================
SIDEBAR
============================================================ */
#sidebar {
width: var(--sidebar-w);
background: var(--sidebar);
display: flex;
flex-direction: column;
border-right: 1px solid var(--bdr);
flex-shrink: 0;
transition: transform .28s cubic-bezier(.4,0,.2,1);
z-index: 20;
}/* TABS */
.tabs { display: flex; border-bottom: 1px solid var(--bdr); flex-shrink: 0; }.tab {
flex: 1; padding: 12px 6px;
font-size: 13px; font-weight: 700;
color: var(--hint);
border-bottom: 2px solid transparent;
transition: color .15s;
display: flex; align-items: center; justify-content: center; gap: 5px;
}
.tab.active { color: var(--txt); border-bottom-color: var(--blue); }.badge {
background: var(--blue); color: #fff;
font-size: 10px; font-weight: 700;
border-radius: 10px; padding: 1px 6px;
}/* TOOLBAR */
.toolbar {
display: flex; gap: 6px; padding: 9px;
border-bottom: 1px solid var(--bdr);
flex-shrink: 0; flex-wrap: wrap;
}.tool-btn {
display: flex; align-items: center; gap: 4px;
padding: 6px 10px;
background: var(--inp); border: 1px solid var(--bdr2);
border-radius: 6px;
font-size: 12px; font-weight: 600; color: var(--muted);
transition: background .15s, color .15s;
}
.tool-btn:hover { background: var(--card); color: var(--txt); }/* ADVANCED ROW */
.adv-row {
display: flex; align-items: center; gap: 8px;
padding: 7px 11px;
font-size: 12px; color: var(--muted);
border-bottom: 1px solid var(--bdr);
flex-shrink: 0;
}
.adv-row input[type=checkbox] { accent-color: var(--blue); width: 14px; height: 14px; cursor: pointer; }
.adv-row label { cursor: pointer; }/* PANEL */
.panel { display: flex; flex-direction: column; flex: 1; overflow: hidden; }#entries-scroll {
flex: 1; overflow-y: auto;
padding: 8px;
display: flex; flex-direction: column; gap: 6px;
}/* ENTRY CARD */
.entry-card {
background: var(--card);
border: 1px solid var(--bdr);
border-radius: 8px;
padding: 8px;
transition: border-color .15s;
}
.entry-card:hover { border-color: var(--bdr2); }
.entry-card.disabled { opacity: .42; }.entry-top {
display: flex; align-items: center; gap: 6px;
}.arrows { display: flex; flex-direction: column; gap: 1px; flex-shrink: 0; }.arr {
color: var(--hint); font-size: 12px; line-height: 1;
padding: 3px 5px; border-radius: 3px;
transition: background .1s, color .1s;
}
.arr:hover { background: var(--inp); color: var(--txt); }
.arr:disabled { opacity: .2; cursor: default; }.entry-name-input {
flex: 1;
background: var(--inp); border: 1px solid var(--bdr2);
border-radius: 6px;
padding: 6px 9px; color: var(--txt);
font-size: 13px; outline: none; min-width: 0;
transition: border-color .15s;
}
.entry-name-input:focus { border-color: var(--blue); }.settings-btn {
width: 28px; height: 28px; border-radius: 50%;
background: var(--blue); color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 13px; flex-shrink: 0;
transition: background .15s;
}
.settings-btn:hover { background: var(--blue2); }.del-x { color: var(--hint); font-size: 17px; line-height: 1; padding: 0 3px; flex-shrink: 0; transition: color .15s; }
.del-x:hover { color: var(--red); }.entry-cb { accent-color: var(--blue); width: 15px; height: 15px; cursor: pointer; flex-shrink: 0; }/* ADVANCED BOTTOM */
.entry-bottom {
display: flex; align-items: center; gap: 8px;
margin-top: 8px; padding-left: 30px;
}.swatch-wrap {
width: 34px; height: 34px; border-radius: 7px;
border: 2px solid rgba(255,255,255,.18);
position: relative; overflow: hidden; flex-shrink: 0; cursor: pointer;
}
.swatch-wrap input[type=color] { opacity: 0; position: absolute; inset: 0; width: 100%; height: 100%; cursor: pointer; border: none; }.img-btn {
width: 34px; height: 34px;
background: var(--inp); border: 1px solid var(--bdr2);
border-radius: 6px; color: var(--muted);
display: flex; align-items: center; justify-content: center;
font-size: 15px; flex-shrink: 0;
transition: background .15s;
}
.img-btn:hover { background: var(--card); color: var(--txt); }.weight-inp {
width: 40px; background: var(--inp); border: 1px solid var(--bdr2);
border-radius: 6px; color: var(--muted);
font-size: 12px; text-align: center; padding: 5px 3px; outline: none;
}
.weight-inp:focus { border-color: var(--blue); }.pct-lbl { font-size: 12px; color: var(--hint); min-width: 34px; }/* ADD ENTRY */
.add-btn {
margin: 8px; padding: 11px;
background: var(--blue); border-radius: 8px;
color: #fff; font-size: 14px; font-weight: 700;
transition: background .15s; flex-shrink: 0;
}
.add-btn:hover { background: var(--blue2); }/* SIDEBAR FOOTER */
.sidebar-foot {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 11px;
border-top: 1px solid var(--bdr); flex-shrink: 0;
}
.ver { font-size: 11px; color: var(--hint); }
.new-tag { background: var(--red); color: #fff; font-size: 9px; border-radius: 4px; padding: 1px 5px; margin-left: 3px; font-weight: 700; }
.beta-tag { background: #f5a623; color: #fff; font-size: 9px; border-radius: 4px; padding: 1px 5px; font-weight: 700; }
.add-wheel-btn {
display: flex; align-items: center; gap: 5px;
padding: 5px 9px;
background: var(--inp); border: 1px solid var(--bdr2);
border-radius: 6px; font-size: 11px; font-weight: 600; color: var(--muted);
transition: background .15s;
}
.add-wheel-btn:hover { background: var(--card); color: var(--txt); }/* RESULTS */
#results-scroll { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 5px; }.r-item {
display: flex; align-items: center; gap: 9px;
padding: 9px 11px;
background: var(--card); border: 1px solid var(--bdr);
border-radius: 8px;
}
.r-dot { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; }
.r-name { flex: 1; font-size: 13px; font-weight: 600; color: var(--txt); }
.r-time { font-size: 11px; color: var(--hint); white-space: nowrap; }
.r-empty { color: var(--muted); font-size: 13px; text-align: center; padding: 28px 16px; }/* OVERLAY (mobile sidebar backdrop) */
#overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,.55);
z-index: 19;
}
#overlay.show { display: block; }/* ============================================================
WHEEL AREA
============================================================ */
#wheel-area {
flex: 1;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 16px; padding: 20px;
background: var(--bg);
overflow: hidden;
min-width: 0;
}#wheel { border-radius: 50%; cursor: pointer; display: block; }.winner-strip {
background: var(--sidebar); border: 1px solid var(--bdr);
border-radius: 12px;
padding: 10px 28px;
width: min(100%, 340px); min-height: 46px;
display: flex; align-items: center; justify-content: center;
text-align: center;
}#winner-txt { font-size: 15px; font-weight: 600; color: var(--muted); transition: color .3s; }#spin-btn {
padding: 13px 52px;
font-size: 17px; font-weight: 700; letter-spacing: 1.5px;
background: var(--red); color: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(226,75,74,.3);
transition: background .2s, transform .1s;
width: min(100%, 280px);
}
#spin-btn:hover { background: var(--red2); }
#spin-btn:active { transform: scale(.97); }
#spin-btn:disabled { background: #44445a; box-shadow: none; cursor: not-allowed; }/* ============================================================
WINNER MODAL
============================================================ */
#modal-overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,.72);
z-index: 100;
align-items: center; justify-content: center;
padding: 16px;
}
#modal-overlay.show { display: flex; }#modal {
background: var(--sidebar); border: 1px solid var(--bdr2);
border-radius: 18px;
padding: clamp(24px, 5vw, 40px) clamp(20px, 6vw, 48px);
text-align: center;
width: min(400px, 92vw);
display: flex; flex-direction: column; align-items: center; gap: 14px;
animation: popIn .3s cubic-bezier(.34,1.56,.64,1);
}
@keyframes popIn { from { transform: scale(.7); opacity: 0; } to { transform: scale(1); opacity: 1; } }#modal-emoji { font-size: clamp(36px, 10vw, 52px); line-height: 1; }
#modal-label { font-size: 11px; font-weight: 700; letter-spacing: 2px; color: var(--muted); text-transform: uppercase; }
#modal-name { font-size: clamp(20px, 6vw, 30px); font-weight: 700; word-break: break-word; }
.modal-actions { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; justify-content: center; }.mbtn {
padding: 10px 18px; border-radius: 9px;
font-size: 13px; font-weight: 700;
border: 1px solid var(--bdr2); color: #fff; cursor: pointer;
transition: background .15s;
}
.mbtn.again { background: var(--red); border-color: var(--red); }
.mbtn.again:hover { background: var(--red2); }
.mbtn.remove { background: var(--inp); }
.mbtn.remove:hover { background: var(--card); }
.mbtn.closem { background: transparent; }
.mbtn.closem:hover { background: var(--inp); }/* ============================================================
MOBILE BOTTOM SHEET (entries shortcut)
============================================================ */
#mob-bar {
display: none;
background: var(--sidebar);
border-top: 1px solid var(--bdr);
padding: 10px 12px;
gap: 8px; flex-shrink: 0;
}
#mob-bar .add-btn { margin: 0; flex: 1; padding: 10px; font-size: 13px; }/* ============================================================
≥ 480px — show logo text & tbtn labels
============================================================ */
@media (min-width: 480px) {
.logo span { display: inline; }
.tbtn .lbl { display: inline; }
}/* ============================================================
< 768px — sidebar as drawer
============================================================ */
@media (max-width: 767px) {
:root { --sidebar-w: min(88vw, 320px); }#sidebar {
position: fixed;
top: var(--topbar-h); left: 0;
height: calc(100dvh - var(--topbar-h));
transform: translateX(-100%);
box-shadow: 4px 0 24px rgba(0,0,0,.5);
}
#sidebar.open { transform: translateX(0); }#wheel-area { padding: 14px 12px; gap: 12px; }#mob-bar { display: flex; }.tbtn:not(.primary):not(#open-sidebar) { display: none; }
}/* ============================================================
768px–1023px — narrow sidebar
============================================================ */
@media (min-width: 768px) {
:root { --sidebar-w: 270px; }
#menu-toggle { display: none; }#sidebar { position: static; height: auto; transform: none !important; box-shadow: none; }.tbtn { display: flex; }
.tbtn .lbl { display: inline; }
}/* ============================================================
≥ 1024px — full sidebar
============================================================ */
@media (min-width: 1024px) {
:root { --sidebar-w: 300px; }
}/* ============================================================
≥ 1280px — wide sidebar
============================================================ */
@media (min-width: 1280px) {
:root { --sidebar-w: 320px; }
}/* ============================================================
LANDSCAPE PHONE fix
============================================================ */
@media (max-height: 500px) and (orientation: landscape) {
:root { --topbar-h: 44px; }
#wheel-area { gap: 8px; padding: 8px; }
.winner-strip { padding: 6px 16px; min-height: 36px; }
#winner-txt { font-size: 13px; }
#spin-btn { padding: 9px 36px; font-size: 14px; }
}/* ============================================================
PRINT
============================================================ */
@media print {
#topbar, #sidebar, #mob-bar, #spin-btn, .winner-strip { display: none !important; }
#wheel-area { height: 100vh; }
}
Spin the wheel to pick a winner!
SPIN
+ Add entry
☰ Entries
🎉
Winner
Spin Again
Remove
✕ Close
/* ---- PALETTE ---- */
const COLORS = [
'#4a9eff','#e24b4a','#f5a623','#1d9e75',
'#7f77dd','#d4537e','#639922','#d85a30',
'#534ab7','#993c1d','#0f6e56','#ba7517'
];/* ---- STATE ---- */
let entries = [];
let results = [];
let spinning = false;
let advancedMode = true;
let currentAngle = 0;
let winner = null;
let animId = null;
let sidebarOpen = false;/* ---- HELPERS ---- */
function uid() { return Math.random().toString(36).slice(2,9); }
function rnd() { const a=new Uint32Array(1); crypto.getRandomValues(a); return a[0]/(0xFFFFFFFF+1); }
function esc(s) { return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/ entries.push(mkEntry(n)));/* ---- PERCENTAGES ---- */
function calcPcts() {
const act = entries.filter(e => e.enabled);
const tot = act.reduce((s,e) => s + e.weight, 0);
const map = {};
act.forEach(e => map[e.id] = tot ? Math.round(e.weight / tot * 100) : 0);
return map;
}/* ---- RENDER ENTRIES ---- */
function renderEntries() {
const pcts = calcPcts();
const scroll = document.getElementById('entries-scroll');
scroll.innerHTML = '';
document.getElementById('entry-count').textContent = entries.length;entries.forEach((e, i) => {
const pct = pcts[e.id] ?? 0;
const div = document.createElement('div');
div.className = 'entry-card' + (e.enabled ? '' : ' disabled');
div.innerHTML = `
${advancedMode ? `
🖼
${e.enabled ? pct+'%' : '—'} ` : ''}
`;
scroll.appendChild(div);
});
drawWheel();
}/* ---- ENTRY ACTIONS ---- */
function addEntry() {
entries.push(mkEntry(''));
renderEntries();
setTimeout(() => {
const s = document.getElementById('entries-scroll');
s.scrollTop = s.scrollHeight;
const inputs = s.querySelectorAll('.entry-name-input');
if (inputs.length) inputs[inputs.length - 1].focus();
}, 50);
}function deleteEntry(i) { entries.splice(i, 1); renderEntries(); }function moveEntry(i, dir) {
const j = i + dir;
if (j = entries.length) return;
[entries[i], entries[j]] = [entries[j], entries[i]];
renderEntries();
}function shuffleEntries() {
for (let i = entries.length - 1; i > 0; i--) {
const j = Math.floor(rnd() * (i + 1));
[entries[i], entries[j]] = [entries[j], entries[i]];
}
renderEntries();
}function sortEntries() {
entries.sort((a, b) => a.name.localeCompare(b.name));
renderEntries();
}function doSettings(i) {
const n = prompt('Rename entry:', entries[i].name);
if (n !== null && n.trim()) { entries[i].name = n.trim(); renderEntries(); }
}/* ---- TABS ---- */
function switchTab(tab) {
document.getElementById('tab-entries').classList.toggle('active', tab === 'entries');
document.getElementById('tab-results').classList.toggle('active', tab === 'results');
document.getElementById('entries-panel').style.display = tab === 'entries' ? 'flex' : 'none';
document.getElementById('results-panel').style.display = tab === 'results' ? 'flex' : 'none';
if (tab === 'results') renderResults();
}/* ---- RESULTS ---- */
function renderResults() {
const sc = document.getElementById('results-scroll');
sc.innerHTML = '';
document.getElementById('result-count').textContent = results.length;
if (!results.length) {
sc.innerHTML = '
No spin results yet.
';
return;
}
[...results].reverse().forEach(r => {
const d = document.createElement('div');
d.className = 'r-item';
d.innerHTML = `
${esc(r.name)}
${r.time}
`;
sc.appendChild(d);
});
}function clearResults() {
if (confirm('Delete all results?')) { results = []; renderResults(); }
}/* ---- RESPONSIVE SIDEBAR (mobile drawer) ---- */
function toggleSidebar() {
sidebarOpen ? closeSidebar() : openSidebarFn();
}function openSidebarFn() {
sidebarOpen = true;
document.getElementById('sidebar').classList.add('open');
document.getElementById('overlay').classList.add('show');
document.getElementById('menu-toggle').classList.add('open');
}function closeSidebar() {
sidebarOpen = false;
document.getElementById('sidebar').classList.remove('open');
document.getElementById('overlay').classList.remove('show');
document.getElementById('menu-toggle').classList.remove('open');
}/* ---- CANVAS WHEEL ---- */
const canvas = document.getElementById('wheel');
const ctx = canvas.getContext('2d');function getWheelSize() {
const area = document.getElementById('wheel-area');
const W = area.clientWidth;
const H = area.clientHeight;
/* subtract space for winner strip + spin button + gap */
const available = Math.min(W - 32, H - 130);
return Math.max(180, Math.min(available, 480));
}function drawWheel() {
const sz = getWheelSize();
canvas.width = sz;
canvas.height = sz;
canvas.style.width = sz + 'px';
canvas.style.height = sz + 'px';const cx = sz/2, cy = sz/2, r = sz/2 - 4;
ctx.clearRect(0, 0, sz, sz);const active = entries.filter(e => e.enabled);if (!active.length) {
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2);
ctx.fillStyle = '#1e1e30'; ctx.fill();
ctx.fillStyle = '#55556a'; ctx.textAlign = 'center';
ctx.font = `${Math.max(11, sz/26)}px Quicksand`;
ctx.fillText('No entries added', cx, cy + 5);
return;
}const total = active.reduce((s, e) => s + e.weight, 0);
let angle = currentAngle;active.forEach(e => {
const slice = (e.weight / total) * Math.PI * 2;/* Segment */
ctx.beginPath(); ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, angle, angle + slice); ctx.closePath();
ctx.fillStyle = e.color; ctx.fill();
ctx.strokeStyle = '#0f0f1a'; ctx.lineWidth = 2; ctx.stroke();/* Label */
ctx.save();
ctx.translate(cx, cy); ctx.rotate(angle + slice/2);
ctx.textAlign = 'right'; ctx.fillStyle = '#fff';
const fs = active.length > 14 ? Math.max(8, sz/42)
: active.length > 9 ? Math.max(9, sz/36)
: Math.max(11, sz/30);
ctx.font = `700 ${fs}px Quicksand`;
ctx.shadowColor = 'rgba(0,0,0,.55)'; ctx.shadowBlur = 3;
const maxCh = active.length > 10 ? 10 : 15;
const lbl = e.name.length > maxCh ? e.name.slice(0, maxCh) + '…' : e.name;
ctx.fillText(lbl, r - 10, fs/3);
ctx.restore();angle += slice;
});/* Hub */
const hubR = Math.max(12, sz/22);
ctx.beginPath(); ctx.arc(cx, cy, hubR, 0, Math.PI*2);
ctx.fillStyle = '#e8e8f4'; ctx.fill();
ctx.strokeStyle = '#0f0f1a'; ctx.lineWidth = 2; ctx.stroke();
ctx.beginPath(); ctx.arc(cx, cy, hubR/2.5, 0, Math.PI*2);
ctx.fillStyle = '#0f0f1a'; ctx.fill();/* Pointer */
const pA = Math.max(10, sz/30);
ctx.beginPath();
ctx.moveTo(sz - 2, cy - pA);
ctx.lineTo(sz - 2, cy + pA);
ctx.lineTo(sz - pA*2 - 4, cy);
ctx.closePath();
ctx.fillStyle = '#fff'; ctx.fill();
ctx.strokeStyle = '#0f0f1a'; ctx.lineWidth = 1.5; ctx.stroke();
}let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(drawWheel, 60);
});/* ---- SPIN ---- */
function startSpin() {
const act = entries.filter(e => e.enabled);
if (spinning || act.length < 2) return;spinning = true; winner = null;
document.getElementById('spin-btn').disabled = true;
const wt = document.getElementById('winner-txt');
wt.style.color = ''; wt.textContent = 'Spinning...';/* close sidebar on mobile when spinning */
if (window.innerWidth < 768) closeSidebar();const totalRad = (1440 + rnd() * 720) * (Math.PI / 180);
const duration = 4000 + rnd() * 2000;
const startA = currentAngle, t0 = performance.now();function easeOut(t) { return 1 - Math.pow(1 - t, 4); }function frame(now) {
const prog = Math.min((now - t0) / duration, 1);
currentAngle = startA + totalRad * easeOut(prog);
drawWheel();if (prog s + e.weight, 0);
const norm = (Math.PI*2 - (currentAngle % (Math.PI*2))) % (Math.PI*2);
let cum = 0; winner = act[act.length - 1];
for (const e of act) {
cum += (e.weight / tot) * Math.PI * 2;
if (norm e.id !== winner.id);
winner = null; closeModal(); renderEntries();
}/* ---- TOPBAR ---- */
function openCustomize() { alert('Customize panel — Colors, sounds, spin speed (coming soon!)'); }
function saveWheel() { alert('Save — localStorage save feature coming soon!'); }
function shareWheel() {
const url = window.location.href;
navigator.clipboard ? navigator.clipboard.writeText(url).then(() => alert('URL copied!')) : alert('URL: ' + url);
}/* ---- KEYBOARD SHORTCUT ---- */
document.addEventListener('keydown', e => {
if ((e.key === ' ' || e.key === 'Enter') && e.target.tagName !== 'INPUT' && e.target.tagName !== 'BUTTON') {
e.preventDefault(); startSpin();
}
if (e.key === 'Escape') { closeModal(); closeSidebar(); }
});/* ---- SWIPE TO CLOSE SIDEBAR (touch) ---- */
let touchStartX = 0;
document.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; }, { passive: true });
document.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - touchStartX;
if (dx 60 && !sidebarOpen && touchStartX < 30) openSidebarFn();
}, { passive: true });/* ---- INIT ---- */
renderEntries();