IMDbCovers / index.html
RaiSantos's picture
Rename movie-poster-downloader.html to index.html
4884767 verified
Raw
History Blame Contribute Delete
27.3 kB
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CineCapas — Baixar Capas de Filmes</title>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0a0a0f;--surface:#111118;--surface2:#1a1a24;
--border:#2a2a3a;--gold:#e8b84b;--gold2:#f5d07a;
--text:#f0ede8;--muted:#6b6880;--red:#e74c3c;--green:#2ecc71;--blue:#3498db;
}
*{margin:0;padding:0;box-sizing:border-box;}
body{background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;font-weight:300;min-height:100vh;overflow-x:hidden;}
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:999;opacity:.35;
background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.04'/%3E%3C/svg%3E");}
/* ── Header ── */
header{text-align:center;padding:52px 20px 32px;}
.logo-row{display:flex;align-items:center;justify-content:center;gap:14px;margin-bottom:8px;}
.film-icon{width:42px;height:42px;display:grid;grid-template-columns:7px 1fr 7px;background:var(--gold);border-radius:4px;overflow:hidden;}
.holes{background:var(--bg);display:flex;flex-direction:column;justify-content:space-around;padding:4px 2px;}
.hole{width:4px;height:4px;background:var(--gold);border-radius:1px;}
h1{font-family:'Bebas Neue',sans-serif;font-size:clamp(40px,8vw,76px);letter-spacing:6px;
background:linear-gradient(135deg,var(--gold),var(--gold2) 50%,var(--gold));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;line-height:.9;}
.divider{width:56px;height:2px;background:var(--gold);margin:16px auto;position:relative;}
.divider::before,.divider::after{content:'';position:absolute;top:50%;width:4px;height:4px;
background:var(--gold);border-radius:50%;transform:translateY(-50%);}
.divider::before{left:-8px;}.divider::after{right:-8px;}
.tagline{color:var(--muted);font-size:11px;letter-spacing:3px;text-transform:uppercase;margin-top:8px;}
/* ── Search ── */
.wrap{max-width:740px;margin:0 auto;padding:0 18px 70px;}
.search-row{display:flex;gap:10px;margin-bottom:10px;}
.iw{position:relative;flex:1;}
.ilabel{position:absolute;top:-9px;left:12px;font-size:10px;letter-spacing:2px;
text-transform:uppercase;color:var(--gold);background:var(--bg);padding:0 5px;z-index:1;}
input[type=text]{width:100%;background:var(--surface);border:1px solid var(--border);
border-radius:8px;color:var(--text);font-family:'DM Sans',sans-serif;font-size:15px;
padding:16px 18px;outline:none;transition:border-color .2s,box-shadow .2s;}
input:focus{border-color:var(--gold);box-shadow:0 0 0 3px rgba(232,184,75,.1);}
input::placeholder{color:var(--muted);}
button{cursor:pointer;font-family:'DM Sans',sans-serif;border:none;border-radius:8px;
font-size:13px;font-weight:500;letter-spacing:1px;text-transform:uppercase;transition:all .2s;}
.btn-go{background:var(--gold);color:#0a0a0f;padding:16px 26px;white-space:nowrap;}
.btn-go:hover{background:var(--gold2);transform:translateY(-1px);box-shadow:0 6px 20px rgba(232,184,75,.25);}
.btn-go:active{transform:translateY(0);}
.btn-go:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none;}
/* tabs */
.tabs{display:flex;gap:6px;margin-bottom:18px;}
.tab{background:var(--surface2);border:1px solid var(--border);color:var(--muted);
padding:8px 16px;border-radius:20px;font-size:12px;letter-spacing:.5px;font-weight:400;}
.tab:hover{border-color:var(--gold);color:var(--gold);}
.tab.on{background:rgba(232,184,75,.12);border-color:var(--gold);color:var(--gold);}
.hint{font-size:12px;color:var(--muted);text-align:center;padding:4px 0 16px;}
.hint em{color:var(--gold);font-style:normal;}
/* ── Loader ── */
.loader{display:none;text-align:center;padding:44px;}
.loader.on{display:block;}
.spin{width:32px;height:32px;border:2px solid var(--border);border-top-color:var(--gold);
border-radius:50%;animation:rot .7s linear infinite;margin:0 auto 12px;}
@keyframes rot{to{transform:rotate(360deg)}}
.loader p{color:var(--muted);font-size:13px;}
/* ── Grid ── */
#grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;margin-top:4px;}
.card{background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;
cursor:pointer;transition:transform .2s,border-color .2s,box-shadow .2s;
animation:up .3s ease forwards;opacity:0;}
@keyframes up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.card:hover{transform:translateY(-4px);border-color:var(--gold);box-shadow:0 10px 28px rgba(0,0,0,.5);}
.card img{width:100%;aspect-ratio:2/3;object-fit:cover;display:block;background:var(--surface2);}
.no-img{width:100%;aspect-ratio:2/3;background:var(--surface2);display:flex;flex-direction:column;
align-items:center;justify-content:center;color:var(--muted);font-size:11px;gap:6px;}
.ci{padding:10px 12px;}
.ct{font-size:12px;font-weight:500;line-height:1.35;margin-bottom:4px;
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
.cm{font-size:11px;color:var(--muted);display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
.badge{font-size:9px;padding:2px 5px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px;font-weight:500;}
.bm{background:rgba(192,57,43,.2);color:var(--red);}
.bt{background:rgba(39,174,96,.2);color:var(--green);}
/* ── Messages ── */
.err{background:rgba(192,57,43,.1);border:1px solid rgba(192,57,43,.3);border-radius:8px;
padding:13px 18px;color:var(--red);font-size:13px;text-align:center;display:none;margin-top:6px;}
.empty{display:none;text-align:center;padding:52px 20px;}
.empty .big{font-family:'Bebas Neue';font-size:54px;letter-spacing:4px;color:var(--border);display:block;margin-bottom:8px;}
.empty p{color:var(--muted);font-size:13px;line-height:1.8;}
/* ── Modal ── */
.ov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:1000;
align-items:center;justify-content:center;padding:20px;backdrop-filter:blur(10px);}
.ov.on{display:flex;}
.modal{background:var(--surface);border:1px solid var(--border);border-radius:16px;
max-width:500px;width:100%;overflow:hidden;animation:min .22s ease;
position:relative;max-height:92vh;overflow-y:auto;}
@keyframes min{from{opacity:0;transform:scale(.93) translateY(14px)}to{opacity:1;transform:scale(1) translateY(0)}}
.mcls{position:sticky;top:10px;float:right;margin:10px 10px -30px 0;
background:rgba(255,255,255,.09);color:var(--text);border:none;width:30px;height:30px;
border-radius:50%;font-size:16px;display:flex;align-items:center;justify-content:center;
cursor:pointer;transition:background .2s;z-index:2;}
.mcls:hover{background:rgba(255,255,255,.18);}
.mpw{position:relative;background:#050507;display:flex;align-items:center;justify-content:center;min-height:180px;}
#mposter{width:100%;max-height:380px;object-fit:contain;display:block;}
#mposter.hide{display:none;}
.noimg-l{padding:70px 20px;text-align:center;color:var(--muted);font-size:13px;width:100%;}
.qbadge{position:absolute;bottom:10px;right:10px;background:var(--gold);color:#0a0a0f;
font-size:9px;font-weight:700;letter-spacing:1px;padding:3px 8px;border-radius:4px;}
.mbody{padding:18px 22px 26px;clear:both;}
#mtitle{font-family:'Bebas Neue',sans-serif;font-size:26px;letter-spacing:2px;
color:var(--gold);line-height:1;margin-bottom:5px;}
#mmeta{color:var(--muted);font-size:12px;margin-bottom:6px;line-height:1.5;}
#mplot{font-size:12.5px;color:#8080a0;line-height:1.65;margin-bottom:16px;font-style:italic;}
/* Seletor de qualidade */
.ql{font-size:10px;text-transform:uppercase;letter-spacing:2px;color:var(--muted);margin-bottom:8px;}
.qopts{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:16px;}
.qb{background:var(--surface2);border:1px solid var(--border);color:var(--muted);
padding:7px 13px;border-radius:6px;font-size:11px;text-transform:none;letter-spacing:0;font-weight:400;}
.qb:hover{border-color:var(--gold);color:var(--gold);}
.qb.on{background:rgba(232,184,75,.12);border-color:var(--gold);color:var(--gold);}
.drow{display:flex;gap:9px;}
.bdl{flex:1;background:var(--gold);color:#0a0a0f;padding:13px;font-size:13px;letter-spacing:1px;
display:flex;align-items:center;justify-content:center;gap:7px;}
.bdl:hover{background:var(--gold2);transform:translateY(-1px);box-shadow:0 6px 20px rgba(232,184,75,.25);}
.bdl:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none;}
.bop{background:var(--surface2);border:1px solid var(--border);color:var(--text);
padding:13px 16px;font-size:13px;letter-spacing:.5px;}
.bop:hover{border-color:var(--gold);color:var(--gold);}
/* ── Toast ── */
.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(8px);
background:var(--gold);color:#0a0a0f;padding:10px 20px;border-radius:8px;
font-weight:500;font-size:13px;z-index:9999;opacity:0;
transition:opacity .25s,transform .25s;pointer-events:none;}
.toast.on{opacity:1;transform:translateX(-50%) translateY(0);}
footer{text-align:center;padding:34px 20px;color:var(--muted);font-size:11px;
letter-spacing:1px;border-top:1px solid var(--border);margin-top:40px;}
footer a{color:var(--muted);text-decoration:none;}
footer a:hover{color:var(--gold);}
</style>
</head>
<body>
<header>
<div class="logo-row">
<div class="film-icon">
<div class="holes"><div class="hole"></div><div class="hole"></div><div class="hole"></div></div>
<div style="background:var(--gold)"></div>
<div class="holes"><div class="hole"></div><div class="hole"></div><div class="hole"></div></div>
</div>
<h1>CineCapas</h1>
</div>
<div class="divider"></div>
<p class="tagline">Baixe capas de filmes e séries em alta qualidade</p>
</header>
<div class="wrap">
<div class="search-row">
<div class="iw">
<span class="ilabel">Buscar</span>
<input type="text" id="q" placeholder="Ex: Reacher, Coringa, Dupla Perigosa, The Dark Knight…"
autocomplete="off" spellcheck="false">
</div>
<button class="btn-go" id="sbtn">Buscar</button>
</div>
<div class="tabs">
<button class="tab on" data-type="multi">🎬 Filmes + Séries</button>
<button class="tab" data-type="movie">🎥 Só Filmes</button>
<button class="tab" data-type="tv">📺 Só Séries</button>
</div>
<p class="hint">
Banco de dados com <em>+1 milhão</em> de títulos · Suporte a <em>português</em> e <em>inglês</em> · Resultados em tempo real
</p>
<div class="loader" id="ldr"><div class="spin"></div><p id="lmsg">Buscando…</p></div>
<div class="err" id="errmsg"></div>
<div class="empty" id="emp">
<span class="big">404</span>
<p>Nenhum resultado encontrado.<br>Tente o nome em inglês ou verifique a ortografia.</p>
</div>
<div id="grid"></div>
</div>
<!-- MODAL -->
<div class="ov" id="ov">
<div class="modal">
<button class="mcls" id="mcls"></button>
<div class="mpw">
<img id="mposter" src="" alt="Capa">
<div class="noimg-l" id="noimg" style="display:none">Sem imagem disponível</div>
<span class="qbadge" id="qbadge"></span>
</div>
<div class="mbody">
<div id="mtitle"></div>
<div id="mmeta"></div>
<div id="mplot"></div>
<div class="ql">Qualidade do download</div>
<div class="qopts" id="qopts">
<button class="qb" data-w="w185">185px · SD</button>
<button class="qb" data-w="w342">342px</button>
<button class="qb on" data-w="w500">500px · HD ★</button>
<button class="qb" data-w="w780">780px · Full HD</button>
<button class="qb" data-w="original">Original</button>
</div>
<div class="drow">
<button class="bdl" id="bdl">⬇ Baixar Capa</button>
<button class="bop" id="bop">🔗 Abrir</button>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<footer>
CineCapas · Dados e imagens via <a href="https://www.themoviedb.org" target="_blank">TMDB</a>
· Este produto usa a API TMDB mas não é endossado ou certificado por TMDB.
</footer>
<script>
// ─────────────────────────────────────────────────────────────────────────────
// TMDB API — gratuita, 1M+ títulos, imagens sem CORS, atualização diária
// Para obter sua própria chave: https://www.themoviedb.org/settings/api
// ─────────────────────────────────────────────────────────────────────────────
const TMDB_KEY = '2dca580c2a14b55200e784d157207b4d'; // chave pública de demo (TMDB permite)
const TMDB_BASE = 'https://api.themoviedb.org/3';
const IMG_BASE = 'https://image.tmdb.org/t/p/';
// ─────────────────────────────────────────────────────────────────────────────
// ESTADO
// ─────────────────────────────────────────────────────────────────────────────
const S = {
type : 'multi', // 'multi' | 'movie' | 'tv'
imgPath : '', // ex: /abc123.jpg
imgWidth : 'w500', // tamanho selecionado
title : '',
searchGen : 0, // geração da busca — ignora resultados de buscas antigas
toastT : null,
};
// ─────────────────────────────────────────────────────────────────────────────
// INIT
// ─────────────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
const q = document.getElementById('q');
q.focus();
document.getElementById('sbtn').addEventListener('click', doSearch);
q.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
// Tabs
document.querySelectorAll('.tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(x => x.classList.remove('on'));
t.classList.add('on');
S.type = t.dataset.type;
});
});
// Modal fechar
document.getElementById('mcls').addEventListener('click', closeModal);
document.getElementById('ov').addEventListener('click', e => {
if (e.target.id === 'ov') closeModal();
});
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
// Qualidade
document.getElementById('qopts').addEventListener('click', e => {
const b = e.target.closest('[data-w]');
if (!b) return;
document.querySelectorAll('#qopts [data-w]').forEach(x => x.classList.remove('on'));
b.classList.add('on');
S.imgWidth = b.dataset.w;
if (S.imgPath) refreshPoster();
});
// Ações modal
document.getElementById('bdl').addEventListener('click', download);
document.getElementById('bop').addEventListener('click', () => {
if (S.imgPath) window.open(imgUrl(S.imgPath, S.imgWidth), '_blank');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// BUSCA
// ─────────────────────────────────────────────────────────────────────────────
async function doSearch() {
const raw = document.getElementById('q').value.trim();
if (!raw) { showErr('Digite um nome para buscar!'); return; }
// Geração controla buscas concorrentes sem AbortController
const gen = ++S.searchGen;
setLoad(true);
clearUI();
try {
const endpoint = S.type === 'multi' ? '/search/multi' : `/search/${S.type}`;
const [resPT, resEN] = await Promise.all([
tmdb(endpoint, { query: raw, language: 'pt-BR', include_adult: false }),
tmdb(endpoint, { query: raw, language: 'en-US', include_adult: false }),
]);
if (gen !== S.searchGen) return; // busca mais recente sobrepõe esta
let results = mergeById([
...(resPT?.results ?? []),
...(resEN?.results ?? []),
]);
if (S.type === 'multi') {
results = results.filter(r => r.media_type === 'movie' || r.media_type === 'tv');
}
results.length ? render(results) : showEmpty();
} catch (e) {
if (gen !== S.searchGen) return;
console.error('[CineCapas]', e);
showErr('Erro ao buscar. Verifique sua conexão e tente novamente.');
} finally {
if (gen === S.searchGen) setLoad(false);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TMDB FETCH
// ─────────────────────────────────────────────────────────────────────────────
async function tmdb(path, params = {}) {
const qs = new URLSearchParams({ api_key: TMDB_KEY, ...params });
const res = await fetch(`${TMDB_BASE}${path}?${qs}`);
if (!res.ok) throw new Error(`TMDB ${res.status}`);
return res.json();
}
// ─────────────────────────────────────────────────────────────────────────────
// RENDER GRID
// ─────────────────────────────────────────────────────────────────────────────
function render(items) {
const grid = document.getElementById('grid');
grid.innerHTML = '';
const frag = document.createDocumentFragment();
items.forEach((item, i) => {
const isTV = item.media_type === 'tv' || S.type === 'tv';
const title = item.title || item.name || '—';
const year = (item.release_date || item.first_air_date || '').slice(0, 4);
const poster = item.poster_path;
const card = document.createElement('div');
card.className = 'card';
card.style.animationDelay = `${Math.min(i, 9) * 0.05}s`;
card.innerHTML =
(poster
? `<img src="${IMG_BASE}w342${esc(poster)}" alt="${esc(title)}" loading="lazy" decoding="async">`
: `<div class="no-img">🎬<br>Sem capa</div>`) +
`<div class="ci">
<div class="ct">${esc(title)}</div>
<div class="cm">
${year ? `<span>${esc(year)}</span>` : ''}
<span class="badge ${isTV ? 'bt' : 'bm'}">${isTV ? 'Série' : 'Filme'}</span>
</div>
</div>`;
card.addEventListener('click', () => openModal(item.id, isTV ? 'tv' : 'movie'));
frag.appendChild(card);
});
grid.appendChild(frag);
}
// ─────────────────────────────────────────────────────────────────────────────
// MODAL
// ─────────────────────────────────────────────────────────────────────────────
async function openModal(id, type) {
resetModal();
document.getElementById('ov').classList.add('on');
document.getElementById('mtitle').textContent = 'Carregando…';
document.getElementById('bdl').disabled = true;
try {
// Pega detalhes em PT, com fallback EN para poster/overview
const [detPT, detEN] = await Promise.all([
tmdb(`/${type}/${id}`, { language: 'pt-BR', append_to_response: 'images' }),
tmdb(`/${type}/${id}`, { language: 'en-US', append_to_response: 'images' }),
]);
const title = detPT.title || detPT.name || detEN.title || detEN.name || '—';
const year = (detPT.release_date || detPT.first_air_date || '').slice(0, 4);
const genres = (detPT.genres || []).map(g => g.name).join(', ');
const runtime = detPT.runtime ? `${detPT.runtime} min` :
detPT.episode_run_time?.[0] ? `${detPT.episode_run_time[0]} min/ep` : '';
// Overview: prefere PT, usa EN se PT vier vazio
const overview= (detPT.overview?.trim()) || detEN.overview || '';
// Poster: prefere versão PT, senão EN
// images.posters[0].file_path tem o melhor poster em alta res
const bestPT = detPT.images?.posters?.[0]?.file_path;
const bestEN = detEN.images?.posters?.[0]?.file_path;
const posterPath = bestPT || bestEN || detPT.poster_path || detEN.poster_path || '';
S.imgPath = posterPath;
S.title = title;
if (posterPath) {
document.getElementById('mposter').src = imgUrl(posterPath, S.imgWidth);
document.getElementById('mposter').classList.remove('hide');
document.getElementById('noimg').style.display = 'none';
document.getElementById('qbadge').textContent = widthLabel(S.imgWidth);
document.getElementById('bdl').disabled = false;
} else {
document.getElementById('mposter').classList.add('hide');
document.getElementById('noimg').style.display = '';
document.getElementById('qbadge').textContent = '';
}
document.getElementById('mtitle').textContent = title;
document.getElementById('mmeta').textContent =
[year, runtime, genres].filter(Boolean).join(' · ');
document.getElementById('mplot').textContent = overview;
} catch (e) {
console.error('[CineCapas] modal:', e);
document.getElementById('mtitle').textContent = 'Erro ao carregar';
}
}
function resetModal() {
S.imgPath = '';
S.title = '';
const img = document.getElementById('mposter');
img.src = '';
img.classList.remove('hide');
document.getElementById('noimg').style.display = 'none';
document.getElementById('mtitle').textContent = '';
document.getElementById('mmeta').textContent = '';
document.getElementById('mplot').textContent = '';
document.getElementById('qbadge').textContent = '';
document.getElementById('bdl').disabled = false;
}
function closeModal() {
document.getElementById('ov').classList.remove('on');
setTimeout(() => {
document.getElementById('mposter').src = '';
S.imgPath = '';
}, 260);
}
function refreshPoster() {
const url = imgUrl(S.imgPath, S.imgWidth);
document.getElementById('mposter').src = url;
document.getElementById('qbadge').textContent = widthLabel(S.imgWidth);
}
// ─────────────────────────────────────────────────────────────────────────────
// DOWNLOAD
// ─────────────────────────────────────────────────────────────────────────────
async function download() {
if (!S.imgPath) { toast('Nenhuma capa disponível.'); return; }
const url = imgUrl(S.imgPath, S.imgWidth);
const btn = document.getElementById('bdl');
btn.disabled = true;
btn.textContent = '⏳ Baixando…';
let objUrl = null;
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blob = await res.blob();
objUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
const safe = S.title.replace(/[^a-z0-9]/gi, '_').toLowerCase().slice(0, 80);
a.href = objUrl;
a.download = `${safe || 'poster'}.jpg`;
a.click();
toast('⬇ Download iniciado!');
} catch {
window.open(url, '_blank');
toast('🔗 Abriu em nova aba — salve com botão direito.');
} finally {
btn.disabled = false;
btn.textContent = '⬇ Baixar Capa';
if (objUrl) setTimeout(() => URL.revokeObjectURL(objUrl), 2000);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────────────────
function imgUrl(path, w) {
return `${IMG_BASE}${w}${path}`;
}
function widthLabel(w) {
const map = { w185:'SD', w342:'SD+', w500:'HD', w780:'FULL HD', original:'ORIGINAL' };
return map[w] ?? w.toUpperCase();
}
// Merge removendo IDs duplicados (PT e EN podem retornar o mesmo título)
function mergeById(arr) {
const seen = new Set();
return arr.filter(x => {
const k = `${x.id}-${x.media_type ?? ''}`;
if (seen.has(k)) return false;
seen.add(k);
return true;
});
}
function setLoad(on) {
document.getElementById('ldr').classList.toggle('on', on);
document.getElementById('sbtn').disabled = on;
}
function clearUI() {
document.getElementById('errmsg').style.display = 'none';
document.getElementById('emp').style.display = 'none';
document.getElementById('grid').innerHTML = '';
}
function showErr(msg) {
const el = document.getElementById('errmsg');
el.textContent = msg;
el.style.display = 'block';
setLoad(false);
}
function showEmpty() {
document.getElementById('emp').style.display = 'block';
}
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('on');
clearTimeout(S.toastT);
S.toastT = setTimeout(() => t.classList.remove('on'), 3200);
}
// Escapa HTML — previne XSS com dados da API
function esc(s) {
return String(s ?? '')
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
</body>
</html>