<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Sala</title>

<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%2317111f'/%3E%3Ctext x='15' y='47' font-family='Arial,sans-serif' font-size='44' font-weight='800' fill='%23ece6f7'%3Es%3C/text%3E%3Ccircle cx='47' cy='42' r='5' fill='%237fd6c2'/%3E%3C/svg%3E">

<style>

:root{--bg:#17111f;--surface:#241a33;--raised:#2f2342;--text:#ece6f7;--muted:#9a8fb0;--accent:#7fd6c2;--accent-ink:#0f2a25;--danger:#ff8a9b;--r:14px}

*{box-sizing:border-box;margin:0}

html{color-scheme:dark}

body{background:var(--bg);color:var(--text);font:16px/1.5 "Bricolage Grotesque","Avenir Next","Segoe UI",system-ui,sans-serif;min-height:100vh}

button,input{font:inherit;color:inherit}

button{cursor:pointer;border:0;background:none}

:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

header{display:flex;gap:20px;align-items:center;padding:14px clamp(16px,4vw,48px);position:sticky;top:0;background:color-mix(in srgb,var(--bg) 92%,transparent);backdrop-filter:blur(10px);z-index:5;border-bottom:1px solid var(--raised)}

.logo{font-size:1.6rem;font-weight:800;letter-spacing:-.04em}

.logo i{font-style:normal;color:var(--accent)}

.search{flex:1;max-width:560px;position:relative}

.search input{width:100%;background:var(--surface);border:1px solid var(--raised);border-radius:999px;padding:10px 18px}

.search input::placeholder{color:var(--muted)}

.btn{background:var(--accent);color:var(--accent-ink);padding:9px 18px;border-radius:999px;font-weight:700}

.btn.ghost{background:var(--raised);color:var(--text)}

nav{display:flex;gap:6px;padding:18px clamp(16px,4vw,48px) 0}

nav button{padding:7px 16px;border-radius:999px;color:var(--muted)}

nav button[aria-pressed=true]{background:var(--raised);color:var(--text)}

main{padding:20px clamp(16px,4vw,48px) 64px}

.stage{display:none;margin-bottom:32px}

.stage.on{display:block}

.frame{aspect-ratio:16/9;background:#000;border-radius:var(--r);overflow:hidden;display:grid;place-items:center;max-height:72vh;width:100%}

.frame iframe{width:100%;height:100%;border:0}

.blank{max-width:460px;text-align:center;color:var(--muted);padding:24px}

.blank b{color:var(--text);display:block;font-size:1.1rem;margin-bottom:6px}

.meta{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;margin-top:14px;flex-wrap:wrap}

.meta h2{font-size:1.6rem;letter-spacing:-.02em}

.meta p{color:var(--muted);max-width:62ch}

.eps{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}

.eps select{background:var(--surface);border:1px solid var(--raised);border-radius:8px;padding:6px 10px}

h3{font-size:1.2rem;margin:8px 0 14px}

.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:18px}

.card{text-align:left;display:block;width:100%}

.poster{aspect-ratio:2/3;border-radius:var(--r);background-size:cover;background-position:center;display:flex;align-items:flex-end;padding:12px;font-weight:800;font-size:1.15rem;line-height:1.1;letter-spacing:-.02em;transition:transform .15s}

.card:hover .poster{transform:translateY(-3px)}

.card small{display:block;color:var(--muted);margin-top:8px}

.card span.t{display:block;margin-top:2px;font-weight:600}

.empty{color:var(--muted);padding:40px 0}

dialog{margin:auto;background:var(--surface);color:var(--text);border:1px solid var(--raised);border-radius:var(--r);padding:26px;width:min(380px,92vw)}

dialog::backdrop{background:#0009}

dialog h2{margin-bottom:14px}

dialog label{display:block;margin-top:10px;color:var(--muted);font-size:.9rem}

dialog input{width:100%;background:var(--bg);border:1px solid var(--raised);border-radius:8px;padding:9px 12px;margin-top:4px}

.row{display:flex;gap:10px;margin-top:18px;align-items:center;justify-content:space-between}

.err{color:var(--danger);min-height:1.4em;margin-top:8px;font-size:.9rem}

.link{color:var(--accent);text-decoration:underline}

@media (prefers-reduced-motion:reduce){*{transition:none!important}}

.logo{text-decoration:none;color:inherit}

nav a{padding:7px 16px;border-radius:999px;color:var(--muted);text-decoration:none}

nav a[aria-current=page]{background:var(--raised);color:var(--text)}

a.card{text-decoration:none;color:inherit}

.seasonSel{font-size:1.15rem;font-weight:700;background:var(--raised);border:0;border-radius:12px;padding:12px 18px;margin:20px 0 14px;cursor:pointer}

.eprow{display:flex;gap:14px;overflow-x:auto;padding-bottom:12px;cursor:grab;scroll-snap-type:x proximity;user-select:none}

.eprow.drag{cursor:grabbing;scroll-snap-type:none}

.ep{flex:0 0 230px;text-align:left;scroll-snap-align:start}

.ep .still{aspect-ratio:16/9;border-radius:10px;background:var(--surface) center/cover;display:grid;place-items:center;color:var(--muted);font-weight:700}

.ep[aria-current=true] .still{outline:2px solid var(--accent);outline-offset:2px}

.ep b{display:block;margin-top:8px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

.ep small{color:var(--muted)}

body{display:flex;flex-direction:column}

main{flex:1}

footer{padding:0 clamp(16px,4vw,48px) 32px;color:var(--muted);font-size:.85rem}

footer p+p{margin-top:6px}

#acct{margin-left:auto;position:relative}

.search{min-width:0}

.avatar{width:38px;height:38px;border-radius:50%;background:var(--raised);font-size:1.25rem;display:grid;place-items:center}

.menu{position:absolute;right:0;top:calc(100% + 8px);background:var(--surface);border:1px solid var(--raised);border-radius:12px;padding:12px;min-width:210px;z-index:10}

.menu:not([hidden]){display:grid;gap:10px}

.menu .who{font-size:.9rem;color:var(--muted);word-break:break-all}

#toast{position:fixed;left:50%;transform:translateX(-50%);bottom:20px;background:var(--raised);padding:10px 16px;border-radius:12px;max-width:90vw;z-index:20;box-shadow:0 6px 24px #0008}

@media (max-width:560px){

  header{flex-wrap:wrap;gap:10px 12px}

  .search{order:3;flex-basis:100%;max-width:none}

}

.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}

.cardWrap{position:relative}

.rm{position:absolute;top:8px;right:8px;width:30px;height:30px;border-radius:50%;background:#000b;color:#fff;display:grid;place-items:center;font-size:.9rem}

.rm:hover{background:var(--danger);color:#000}

</style>

</head>

<body>


<header>

  <a class="logo" href="#/">sala<i>.</i></a>

  <form class="search" id="searchForm" role="search">

    <input id="q" type="search" placeholder="Search movies and shows" aria-label="Search">

  </form>

  <div id="acct"></div>

</header>


<nav id="nav" aria-label="Sections">

  <a href="#/" data-r="home">Discover</a>

  <a href="#/list" data-r="list">My list</a>

  <a href="#/history" data-r="history">Recently watched</a>

</nav>


<main id="view"></main>

<footer id="foot">

  <p id="tmdbNote"></p>

  <p>sala doesn't host or store any media files. Videos and listings are loaded from external third-party sources.</p>

</footer>


<dialog id="authDlg">

  <form id="authForm" method="dialog">

    <h2 id="authTitle">Sign in</h2>

    <label>Email<input id="uName" type="email" autocomplete="email" required></label>

    <label>Password<input id="uPass" type="password" autocomplete="current-password" minlength="6" required></label>

    <div class="err" id="authErr" role="alert"></div>

    <div class="row">

      <button type="button" class="link" id="authSwitch">Create account</button>

      <span><button type="button" class="btn ghost" id="authCancel">Cancel</button> <button class="btn" id="authGo">Sign in</button></span>

    </div>

  </form>

</dialog>


<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.45.4/dist/umd/supabase.js"></script>

<script>

/* =====================================================================

   1) A TUA FONTE  —  preenche só este bloco, não precisas de mexer no resto

   ---------------------------------------------------------------------

   Endereços: copia-os da documentação da tua fonte.

   Modelos de URL: usa {id}, {season}, {episode} e {query} onde esses

   valores devem entrar. Exemplo: "https://O-TEU-DOMINIO/filme/{id}"

   Enquanto estiver tudo vazio, o site mostra os dados de demonstração.

   ===================================================================== */

const SOURCE_CONFIG = {

  movieEmbed: "https://vidsrc.to/embed/movie/{id}",   // modelo do URL de um filme                (usa {id})

  tvEmbed:    "https://vidsrc.to/embed/tv/{id}/{season}/{episode}",   // modelo do URL de um episódio             (usa {id} {season} {episode})

  listMovies: "https://vidsrc.to/vapi/movie/{type}/{page}",   // endereço da lista de filmes (página inicial)

  listTv:     "https://vidsrc.to/vapi/tv/{type}/{page}",   // endereço da lista de séries (página inicial)

  searchUrl:  "ENDEREÇO-DA-PESQUISA-COM-{query}",   // endereço da pesquisa da tua fonte (usa {query}); ignorado se tmdbKey estiver preenchida

  tmdbKey:    "0318aa0194bc0bb7264ab57039a3c6dd",   // chave da API do TMDB: ativa a pesquisa em todo o catálogo (e a página inicial, se não tiveres listas)


  // Em que campo da resposta está cada coisa. Abre um dos endereços de lista

  // no browser, olha para o texto que aparece e escreve aqui os nomes que vires.

  // Podes pôr alternativas separadas por vírgula (usa-se a primeira que existir)

  // e caminhos com ponto para campos dentro de campos (ex.: "info.titulo").

  fields: {

    results:  "result,results,data,items",  // onde está a lista (se a resposta já for uma lista, ignora-se)

    id:       "tmdb_id,imdb_id,id",         // O IDENTIFICADOR que o embed usa

    title:    "title,name",

    year:     "year,release_date",

    overview: "overview,description",

    poster:   "poster,poster_path",

    type:     "type,media_type"             // "movie" ou "tv" (só preciso na pesquisa)

  }

};


/* --- daqui para baixo é o código que usa a configuração acima --- */

const C = SOURCE_CONFIG;

const set = v => v && !/^(ENDEREÇO|CHAVE)/.test(v);   // texto que ainda não substituíste conta como vazio

const fill = (tpl, v) => tpl.replace(/\{(\w+)\}/g, (_, k) => encodeURIComponent(v[k] ?? ""));

const pick = (o, paths) => {

  for (const p of String(paths).split(",")){

    let v = o; for (const k of p.trim().split(".")) v = v?.[k];

    if (v != null && v !== "") return v;

  }

};

const rowsOf = j => Array.isArray(j) ? j : (pick(j, C.fields.results) || []);

const getJson = async url => { const r = await fetch(url); if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); };

function toItem(r, type){

  const f = C.fields, id = pick(r, f.id);

  if (id == null) return null;

  const t = type || (/tv|serie|show/i.test(String(pick(r, f.type))) ? "tv" : "movie");

  return {id:String(id), type:t, title:pick(r, f.title) || "Untitled",

          year:String(pick(r, f.year) ?? "").slice(0,4), overview:pick(r, f.overview) || "", poster:pick(r, f.poster)};

}

async function tmdb(path, params = {}){

  const u = new URL("https://api.themoviedb.org/3" + path), opt = {};

  Object.entries({language:"en-US", ...params}).forEach(([k, v]) => u.searchParams.set(k, v));

  if (C.tmdbKey.startsWith("eyJ")) opt.headers = {Authorization: "Bearer " + C.tmdbKey};

  else u.searchParams.set("api_key", C.tmdbKey);

  const r = await fetch(u, opt);

  if (!r.ok) throw new Error("TMDB " + r.status);

  return r.json();

}

function tmdbItem(r){

  const type = r.media_type || (r.first_air_date ? "tv" : "movie");

  if (type !== "movie" && type !== "tv") return null;

  return {id:String(r.id), type, title:r.title || r.name || "Untitled",

          year:String(r.release_date || r.first_air_date || "").slice(0,4), overview:r.overview || "",

          poster:r.poster_path ? "https://image.tmdb.org/t/p/w342" + r.poster_path : ""};

}

async function fetchItems(url, type){

  try { return rowsOf(await getJson(url)).map(r => toItem(r, type)).filter(Boolean) }

  catch (e) { console.error("Falhou:", url, e); return [] }

}


/* Completa capas, títulos e sinopses em falta usando o TMDB (se a chave estiver preenchida) */

async function enrich(items){

  if (!set(C.tmdbKey)) return items;

  await Promise.allSettled(items.filter(i => !i.poster).slice(0, 40).map(async it => {

    try {

      let d;

      if (/^tt/.test(it.id)){

        const f = await tmdb("/find/" + it.id, {external_source:"imdb_id"});

        d = (it.type === "tv" ? f.tv_results : f.movie_results)[0];

      } else d = await tmdb(`/${it.type}/${it.id}`);

      if (!d) return;

      if (d.poster_path) it.poster = "https://image.tmdb.org/t/p/w342" + d.poster_path;

      if (it.title === "Untitled") it.title = d.title || d.name || it.title;

      if (!it.overview) it.overview = d.overview || "";

      if (!it.year) it.year = String(d.release_date || d.first_air_date || "").slice(0, 4);

    } catch (e) { console.error("TMDB enrich:", it.id, e) }

  }));

  return items;

}


const SOURCE = {

  async list(){

    let own = [];

    SOURCE.error = "";

    if (set(C.listMovies) || set(C.listTv)){

      const [m, t] = await Promise.all([

        set(C.listMovies) ? fetchItems(C.listMovies, "movie") : [],

        set(C.listTv) ? fetchItems(C.listTv, "tv") : []

      ]);

      own = [...m, ...t];

      if (!own.length) { SOURCE.error = "the source lists returned no titles"; console.warn("As listas da fonte não devolveram títulos. Confirma os endereços e o mapa 'fields' (ou CORS)."); }

    }

    if (own.length) return (SOURCE.all = await enrich(own));

    if (set(C.tmdbKey)){

      try { return (SOURCE.all = (await tmdb("/trending/all/week")).results.map(tmdbItem).filter(Boolean)) }

      catch (e) { SOURCE.error = e.message; console.error("TMDB:", e) }

    }

    return (set(C.listMovies) || set(C.listTv)) ? [] : DEMO;

  },

  async search(query){

    if (set(C.tmdbKey)){

      try { return (await tmdb("/search/multi", {query, include_adult:"false"})).results.map(tmdbItem).filter(Boolean) }

      catch (e) { console.error("TMDB:", e) }

    }

    if (!set(C.searchUrl)) return enrich((SOURCE.all?.length ? SOURCE.all : DEMO).filter(i => i.title.toLowerCase().includes(query.toLowerCase())));

    return enrich(await fetchItems(fill(C.searchUrl, {query}), null));

  },

  async item(type, id){   // para abrir uma página de título diretamente (ex.: ao recarregar)

    const known = [...(SOURCE.all || []), ...DEMO].find(i => i.type === type && i.id === id);

    if (known) return known;

    if (set(C.tmdbKey)){

      try { const it = tmdbItem({...(await tmdb(`/${type}/${id}`)), media_type:type}); if (it) return it }

      catch (e) { console.error("TMDB:", e) }

    }

    return {id, type, title:"Untitled", year:"", overview:"", poster:""};

  },

  async episodes(item, season, count){   // episódios de uma temporada, com título e imagem

    if (set(C.tmdbKey)){

      try {

        return (await tmdb(`/tv/${item.id}/season/${season}`)).episodes.map(e => ({n:e.episode_number, name:e.name,

          still:e.still_path ? "https://image.tmdb.org/t/p/w300" + e.still_path : ""}));

      } catch (e) { console.error("TMDB:", e) }

    }

    return Array.from({length: count || 12}, (_, i) => ({n:i + 1, name:"Episode " + (i + 1), still:""}));

  },

  async seasons(item){   // nº de episódios por temporada, via TMDB

    if (!set(C.tmdbKey) || item.type !== "tv") return null;

    try {

      const o = {};

      ((await tmdb("/tv/" + item.id)).seasons || []).filter(x => x.season_number > 0 && x.episode_count > 0)

        .forEach(x => o[x.season_number] = x.episode_count);

      return Object.keys(o).length ? o : null;

    } catch { return null }

  },

  embedUrl(item, ep){

    const tpl = item.type === "tv" ? C.tvEmbed : C.movieEmbed;

    return set(tpl) ? fill(tpl, {id:item.id, season:ep?.season, episode:ep?.episode}) : "";

  }

};


/* Dados de demonstração para veres o design a funcionar. Apaga quando quiseres. */

const DEMO = [

  {id:"d1",type:"movie",title:"Low Tide",year:2021,overview:"Sample movie to test the layout."},

  {id:"d2",type:"tv",title:"The Lighthouse Keepers",year:2023,overview:"Sample show with seasons and episodes.",seasons:{1:6,2:8}},

  {id:"d3",type:"movie",title:"Line Seven",year:2019,overview:"Another sample movie."},

  {id:"d4",type:"tv",title:"Letters from the Interior",year:2022,overview:"Sample show.",seasons:{1:10}},

  {id:"d5",type:"movie",title:"The Last Station",year:2024,overview:"Another sample title."},

  {id:"d6",type:"movie",title:"North Wind",year:2018,overview:"Another sample title."}

];


/* =====================================================================

   2) CONTAS COM SINCRONIZAÇÃO (Supabase)

   Cola aqui os dois valores do teu projeto Supabase

   (Project Settings -> API): o "Project URL" e a chave "anon public".

   ===================================================================== */

const SUPABASE_URL = "https://lxoouwuouxbmncfntrbn.supabase.co";   // ex.: https://abcdefgh.supabase.co

const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx4b291d3VvdXhibW5jZm50cmJuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3OTE3MzA1NTksImV4cCI6MjEwNzMwNjU1OX0.kmbAWRisiSSc76HRYZ1gxVA34IbD0GEXQZ2D51gWzTA";   // chave "anon public" (é feita para ficar no site)


const $ = s => document.querySelector(s);

const sb = (window.supabase && SUPABASE_URL && SUPABASE_KEY) ? window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY) : null;

let user = null;

let data = {list:[], hist:[]};

const me = () => user ? data : null;


function notify(msg){

  let t = $("#toast");

  if (!t){ t = document.createElement("div"); t.id = "toast"; t.setAttribute("role", "status"); document.body.append(t); }

  t.textContent = msg; t.hidden = false;

  clearTimeout(notify.t); notify.t = setTimeout(() => t.hidden = true, 9000);

}

async function report(query, what){

  try { const {error} = await query; if (error) throw error; }

  catch (e) { console.error("Sync:", what, e); notify("Couldn't sync " + what + ": " + (e.message || e)); }

}

async function loadData(){

  data = {list:[], hist:[]};

  if (!sb || !user) return;

  const {data: rows, error} = await sb.from("library").select("kind,item").order("updated_at", {ascending:false});

  if (error){ console.error("Sync:", error); notify("Couldn't load your saved titles: " + error.message); return; }

  (rows || []).forEach(r => data[r.kind]?.push(r.item));

  data.hist = data.hist.slice(0, 30);

}

const setRow = (kind, it) => report(sb.from("library").upsert({user_id:user.id, kind, item_id:String(it.id), item:it, updated_at:new Date().toISOString()}), kind === "list" ? "your list" : "your history");

const delRow = (kind, it) => report(sb.from("library").delete().eq("kind", kind).eq("item_id", String(it.id)), kind === "list" ? "your list" : "your history");


function addHist(it){

  if (!me()) return;

  data.hist = [it, ...data.hist.filter(x => x.id !== it.id)].slice(0, 30);

  setRow("hist", it);

}

function toggleList(it, saved){

  if (saved){ data.list = data.list.filter(x => x.id !== it.id); delRow("list", it); }

  else { data.list = [it, ...data.list]; setRow("list", it); }

  renderFav();

}

async function initAuth(){

  renderAcct();

  if (!sb) return route();

  const {data:{session}} = await sb.auth.getSession();

  user = session?.user ?? null;

  await loadData(); renderAcct(); route();

  sb.auth.onAuthStateChange(async (_e, s) => {

    const next = s?.user ?? null;

    if (next?.id === user?.id) return;

    user = next; await loadData(); renderAcct(); route();

  });

}


/* =====================================================================

   3) PÁGINAS  —  cada ecrã tem o seu endereço:

   #/  (Discover)   #/list   #/history   #/search/texto   #/title/movie/123

   ===================================================================== */

const view = $("#view");

const state = {current:null, ep:{season:1, episode:1}, token:0};

const cache = new Map();

const keyOf = it => it.type + ":" + it.id;

const hues = id => [...id].reduce((a, c) => a + c.charCodeAt(0), 0) * 47 % 360;

const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));

const safe = s => { try { return decodeURIComponent(s) } catch { return s } };


function posterStyle(it){

  if (it.poster) return `background-image:url('${esc(it.poster)}')`;

  const h = hues(String(it.id));

  return `background-image:linear-gradient(160deg,hsl(${h} 45% 38%),hsl(${(h+50)%360} 40% 18%))`;

}

function card(it){

  cache.set(keyOf(it), it);

  const a = document.createElement("a");

  a.className = "card";

  a.href = "#/title/" + it.type + "/" + encodeURIComponent(it.id);

  a.innerHTML = `<div class="poster" style="${posterStyle(it)}">${it.poster ? "" : esc(it.title)}</div>

    <span class="t">${esc(it.title)}</span><small>${it.type === "tv" ? "Show" : "Movie"}${it.year ? " · " + esc(it.year) : ""}</small>`;

  return a;

}

function gridPage(items, title, emptyMsg, opts = {}){

  view.replaceChildren();

  const bar = document.createElement("div"); bar.className = "bar";

  const h = document.createElement("h3"); h.textContent = title; bar.append(h);

  if (opts.clear && items.length){

    const c = document.createElement("button"); c.className = "btn ghost"; c.textContent = "Clear all"; c.onclick = opts.clear; bar.append(c);

  }

  view.append(bar);

  if (!items.length){

    const p = document.createElement("p"); p.className = "empty"; p.textContent = emptyMsg; view.append(p); return;

  }

  const g = document.createElement("div"); g.className = "grid"; view.append(g);

  items.forEach(it => {

    const c = card(it);

    if (!opts.remove) return g.append(c);

    const w = document.createElement("div"); w.className = "cardWrap";

    const x = document.createElement("button"); x.className = "rm"; x.textContent = "✕";

    x.setAttribute("aria-label", "Remove " + it.title + " from recently watched");

    x.onclick = () => opts.remove(it);

    w.append(c, x); g.append(w);

  });

}


/* Recently watched: remover um título ou limpar tudo */

const HIST_EMPTY = "You haven't watched anything yet.";

const histOpts = () => ({remove:removeHist, clear:clearHist});

function removeHist(it){

  data.hist = data.hist.filter(x => x.id !== it.id);

  delRow("hist", it);

  gridPage(data.hist, "Recently watched", HIST_EMPTY, histOpts());

}

function clearHist(){

  if (!confirm("Clear your recently watched list?")) return;

  data.hist = [];

  report(sb.from("library").delete().eq("kind", "hist"), "your history");

  gridPage(data.hist, "Recently watched", HIST_EMPTY, histOpts());

}


function renderAcct(){

  const a = $("#acct");

  if (me()){

    a.innerHTML = `<button class="avatar" id="avBtn" aria-label="Account menu" aria-expanded="false">😎</button>

      <div class="menu" id="acctMenu" hidden><div class="who">${esc(user.email)}</div><button class="btn ghost" id="outBtn">Sign out</button></div>`;

    $("#avBtn").onclick = e => {

      e.stopPropagation();

      const m = $("#acctMenu"); m.hidden = !m.hidden;

      $("#avBtn").setAttribute("aria-expanded", String(!m.hidden));

    };

    $("#outBtn").onclick = () => sb.auth.signOut();

  } else {

    a.innerHTML = `<button class="btn" id="inBtn">Sign in</button>`;

    $("#inBtn").onclick = () => openAuth("in");

  }

}

const closeMenu = () => { const m = $("#acctMenu"); if (m) { m.hidden = true; $("#avBtn")?.setAttribute("aria-expanded", "false"); } };

document.addEventListener("click", e => { const m = $("#acctMenu"); if (m && !m.hidden && !m.contains(e.target)) closeMenu(); });

document.addEventListener("keydown", e => { if (e.key === "Escape") closeMenu(); });


async function route(){

  const my = ++state.token;          // ignora respostas atrasadas de páginas anteriores

  const parts = location.hash.slice(2).split("/").map(safe);

  const r = parts[0] || "home";

  document.querySelectorAll("#nav a").forEach(a => a.setAttribute("aria-current", a.dataset.r === r ? "page" : "false"));

  scrollTo(0, 0);

  if (r === "title") return titlePage(parts[1], parts.slice(2).join("/"), my);

  view.innerHTML = '<p class="empty">Loading…</p>';

  if (r === "search"){

    const q = parts.slice(1).join("/"); $("#q").value = q;

    const items = await SOURCE.search(q);

    if (my === state.token) gridPage(items, `Results for “${q}”`, "Nothing found. Try another title.");

  } else if (r === "list" || r === "history"){

    const title = r === "list" ? "My list" : "Recently watched";

    if (!me()) return gridPage([], title, "Sign in to see this section.");

    gridPage(r === "list" ? me().list : me().hist, title,

      r === "list" ? "Nothing saved yet. Open a title and choose Save to list." : HIST_EMPTY,

      r === "history" ? histOpts() : {});

  } else {

    const items = await SOURCE.list();

    if (my === state.token) gridPage(items, "Discover", SOURCE.error

      ? `Couldn't load titles (${SOURCE.error}). Check tmdbKey and the list addresses; press F12 for details.` : "No results.");

  }

}


async function titlePage(type, id, my){

  view.innerHTML = '<p class="empty">Loading…</p>';

  const it = cache.get(type + ":" + id) || await SOURCE.item(type, id);

  if (my !== state.token) return;

  state.current = it; state.ep = {season:1, episode:1};

  view.innerHTML = `<section class="stage on"><div class="frame" id="frame"></div>

    <div class="meta"><div><h2>${esc(it.title)}${it.year ? " (" + esc(it.year) + ")" : ""}</h2><p>${esc(it.overview)}</p></div>

    <div><button class="btn ghost" id="favBtn"></button></div></div><div id="eps"></div></section>`;

  renderFav(); play(); addHist(it);

  if (it.type === "tv") loadEpisodes(it, my);

}


async function loadEpisodes(it, my){

  const seasons = it.seasons || await SOURCE.seasons(it) || {1:12};

  if (my !== state.token) return;

  const box = $("#eps");

  const sel = document.createElement("select"); sel.className = "seasonSel"; sel.setAttribute("aria-label", "Season");

  Object.keys(seasons).forEach(n => sel.add(new Option("Season " + n, n)));

  const row = document.createElement("div"); row.className = "eprow";

  box.replaceChildren(sel, row);

  const mark = () => row.querySelectorAll(".ep").forEach(b =>

    b.setAttribute("aria-current", state.ep.season === +sel.value && state.ep.episode === +b.dataset.n));

  const show = async () => {

    const s = +sel.value, eps = await SOURCE.episodes(it, s, seasons[s]);

    if (my !== state.token || +sel.value !== s) return;

    row.replaceChildren(...eps.map(e => {

      const b = document.createElement("button"); b.className = "ep"; b.dataset.n = e.n;

      b.innerHTML = `<div class="still" style="${e.still ? `background-image:url('${esc(e.still)}')` : ""}">${e.still ? "" : "E" + e.n}</div>

        <b>${e.n}. ${esc(e.name)}</b><small>Season ${s} · Episode ${e.n}</small>`;

      b.onclick = () => {

        if (suppressClick) return;

        state.ep = {season:s, episode:e.n}; mark(); play();

        $("#frame").scrollIntoView({behavior:"smooth", block:"center"});

      };

      return b;

    }));

    row.scrollLeft = 0; mark();

  };

  sel.onchange = show;

  show();

}


/* Arrastar a fila de episódios com o rato (no telemóvel basta deslizar) */

let drag = null, suppressClick = false;

document.addEventListener("pointerdown", e => {

  const el = e.target.closest?.(".eprow");

  if (el && e.pointerType === "mouse") drag = {el, sx:e.clientX, sl:el.scrollLeft, moved:false};

});

document.addEventListener("pointermove", e => {

  if (!drag) return;

  const dx = e.clientX - drag.sx;

  if (Math.abs(dx) > 5){ drag.moved = true; drag.el.classList.add("drag"); }

  if (drag.moved) drag.el.scrollLeft = drag.sl - dx;

});

document.addEventListener("pointerup", () => {

  if (!drag) return;

  drag.el.classList.remove("drag");

  if (drag.moved){ suppressClick = true; setTimeout(() => suppressClick = false, 0); }

  drag = null;

});


function renderFav(){

  const b = $("#favBtn"), it = state.current;

  if (!b || !it) return;

  const saved = !!me() && me().list.some(x => x.id === it.id);

  b.textContent = saved ? "Remove from list" : "Save to list";

  b.onclick = () => { if (!me()) return openAuth("in"); toggleList(it, saved); };

}


function play(){

  const it = state.current, f = $("#frame");

  const url = SOURCE.embedUrl(it, it.type === "tv" ? state.ep : null);

  f.replaceChildren();

  if (!url){

    f.innerHTML = `<div class="blank"><b>Embed not set up</b>Fill in <code>movieEmbed</code> and <code>tvEmbed</code> in SOURCE_CONFIG, at the top of the script, and the video will appear here.</div>`;

    return;

  }

  const ifr = document.createElement("iframe");

  ifr.src = url;

  ifr.allowFullscreen = true;

  ifr.allow = "autoplay; fullscreen; picture-in-picture";

  ifr.referrerPolicy = "origin";

  ifr.title = it.title;

  f.append(ifr);

}


/* Pesquisa: abre a página de resultados */

$("#searchForm").onsubmit = e => {

  e.preventDefault();

  const q = $("#q").value.trim(), h = q ? "#/search/" + encodeURIComponent(q) : "#/";

  if (location.hash === h) route(); else location.hash = h;

};


/* Janela de conta */

let mode = "in";

function openAuth(m){

  mode = m;

  $("#authTitle").textContent = m === "in" ? "Sign in" : "Create account";

  $("#authGo").textContent = m === "in" ? "Sign in" : "Create account";

  $("#authSwitch").textContent = m === "in" ? "Create account" : "I have an account";

  $("#authErr").textContent = "";

  $("#authDlg").showModal();

}

$("#authSwitch").onclick = () => openAuth(mode === "in" ? "up" : "in");

$("#authCancel").onclick = () => $("#authDlg").close();

$("#authForm").onsubmit = async e => {

  e.preventDefault();

  const err = $("#authErr");

  if (!sb){ err.textContent = "Paste SUPABASE_URL and SUPABASE_KEY into the code first."; return; }

  const email = $("#uName").value.trim(), password = $("#uPass").value;

  const {data: d, error} = mode === "up" ? await sb.auth.signUp({email, password}) : await sb.auth.signInWithPassword({email, password});

  if (error){ err.textContent = error.message; return; }

  if (mode === "up" && !d.session){ err.textContent = "Account created. Confirm the email we sent you, then sign in."; return; }

  $("#authDlg").close(); $("#authForm").reset();

};


if (set(C.tmdbKey)) $("#tmdbNote").textContent = "This product uses the TMDB API but is not endorsed or certified by TMDB.";

addEventListener("hashchange", route);

initAuth();


</script>

</body>

</html>