diff --git a/public/checklist.html b/public/checklist.html
index 2233d28..2f93a9d 100644
--- a/public/checklist.html
+++ b/public/checklist.html
@@ -72,6 +72,7 @@
Films
Checklist
Statistiques
+ FAQ
Admin
diff --git a/public/control.html b/public/control.html
index e3f71db..b256dba 100644
--- a/public/control.html
+++ b/public/control.html
@@ -43,6 +43,7 @@
Films
Checklist
Statistiques
+ FAQ
Admin
diff --git a/public/faq.html b/public/faq.html
new file mode 100644
index 0000000..77d990e
--- /dev/null
+++ b/public/faq.html
@@ -0,0 +1,178 @@
+
+
+
+
+
+
Pool Oscars - FAQ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Déconnexion
+
+
+
+
+
+
+
+
FAQ
+
Rapide, simple, sans drama.
+
+
+
+
+
+
En 10 secondes
+
+ Pool Oscars te sert à suivre les films de l’année, les regarder dans le lecteur ,
+ cocher ce que tu as vu, et voir les stats (toi + les autres).
+
+
+
+
+
+
+
+
+ C’est une app “pool” pour suivre les films nommés aux Oscars (par année). Tu vois la liste, tu regardes,
+ tu coches, et tu compares ta progression avec les autres.
+
+
+
+
+
+
+
+
+ Tu browses les films. Clique sur l’affiche ou le titre pour ouvrir le lecteur. L’icône IMDb ouvre la fiche
+ du film dans un nouvel onglet.
+
+
+
+
+
+
+
+
+ C’est la version “tableau simple” pour cocher vite ce que tu as vu. Ça calcule ton ratio, une barre de
+ progression et une estimation “à voir par jour” avant la date des Oscars.
+
+
+
+
+
+
+
+
+ Un classement (films vus / points) + des onglets “Gagnants” et “100%”. Tu peux aussi cliquer sur un nom pour
+ voir les films que la personne a regardés.
+
+
+
+
+
+
+
+
+ Ça arrive quand ton compte a un mot de passe temporaire. Tu dois en définir un nouveau une fois, puis c’est
+ réglé.
+
+
+
+
+
+
+
+
+ Seulement pour les admins: réglages (année active, date des Oscars, lecteur), gestion des films et des
+ utilisateurs.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/app_faq.js b/public/js/app_faq.js
new file mode 100644
index 0000000..2d13822
--- /dev/null
+++ b/public/js/app_faq.js
@@ -0,0 +1,184 @@
+function base64UrlToJson(b64url) {
+ const s = String(b64url || '')
+ .replace(/-/g, '+')
+ .replace(/_/g, '/');
+ const padded = s + '='.repeat((4 - (s.length % 4)) % 4);
+ return JSON.parse(atob(padded));
+}
+
+function decodeJwtPayload(token) {
+ const parts = String(token || '').split('.');
+ if (parts.length < 2) return null;
+ try { return base64UrlToJson(parts[1]); } catch (_) { return null; }
+}
+
+function isExpired(decoded) {
+ const exp = Number(decoded?.exp);
+ if (!Number.isFinite(exp) || exp <= 0) return false;
+ const now = Math.floor(Date.now() / 1000);
+ return exp < now;
+}
+
+function createPageLoader(options = {}) {
+ const title = String(options.title || 'Chargement…');
+ let progress = 0;
+ let removed = false;
+ let navResizeObserver = null;
+
+ // Reuse a preloaded overlay if present (prevents initial "clear" flash).
+ const preloadedOverlay = document.getElementById('page-loader-overlay');
+ const overlay = preloadedOverlay || document.createElement('div');
+ if (!preloadedOverlay) {
+ overlay.className = 'page-loader-overlay';
+ overlay.setAttribute('role', 'status');
+ overlay.setAttribute('aria-live', 'polite');
+ overlay.setAttribute('aria-busy', 'true');
+
+ overlay.innerHTML = `
+
+ `;
+ } else {
+ const label = overlay.querySelector('.visually-hidden');
+ if (label) label.textContent = title;
+ }
+
+ const barEl = () => overlay.querySelector('#page-loader-bar');
+
+ function clampPct(p) {
+ const n = Number(p);
+ if (!Number.isFinite(n)) return 0;
+ return Math.max(0, Math.min(100, n));
+ }
+
+ function setProgress(p) {
+ progress = clampPct(p);
+ const bar = barEl();
+ if (bar) bar.style.width = `${progress}%`;
+ }
+
+ function getNavHeightPx() {
+ const nav = document.querySelector('nav.navbar');
+ if (!nav) return 0;
+ const rect = nav.getBoundingClientRect();
+ const h = Number(rect?.height) || 0;
+ return h > 0 ? Math.round(h) : 0;
+ }
+
+ function updateOverlayTopOffset() {
+ overlay.style.setProperty('--page-loader-top', `${getNavHeightPx()}px`);
+ }
+
+ function ensureMounted() {
+ if (removed) return;
+ updateOverlayTopOffset();
+ document.body.classList.add('page-loading');
+ if (!overlay.isConnected) document.body.appendChild(overlay);
+
+ if (!navResizeObserver) {
+ const nav = document.querySelector('nav.navbar');
+ if (nav && typeof ResizeObserver !== 'undefined') {
+ try {
+ navResizeObserver = new ResizeObserver(() => updateOverlayTopOffset());
+ navResizeObserver.observe(nav);
+ } catch (_) {
+ navResizeObserver = null;
+ }
+ }
+ }
+ }
+
+ function hideAndRemoveSoon() {
+ if (removed) return;
+ overlay.classList.add('page-loader-hide');
+ overlay.setAttribute('aria-busy', 'false');
+ document.body.classList.remove('page-loading');
+ window.setTimeout(() => {
+ removed = true;
+ if (navResizeObserver) {
+ try { navResizeObserver.disconnect(); } catch (_) {}
+ navResizeObserver = null;
+ }
+ try { overlay.remove(); } catch (_) {}
+ }, 220);
+ }
+
+ function done() {
+ setProgress(100);
+ hideAndRemoveSoon();
+ }
+
+ function fail() {
+ setProgress(Math.max(progress, 95));
+ window.setTimeout(() => hideAndRemoveSoon(), 200);
+ }
+
+ ensureMounted();
+ setProgress(8);
+
+ return { setProgress, done, fail };
+}
+
+async function fetchActiveYear() {
+ try {
+ const res = await fetch('/api/settings/year', { method: 'GET', cache: 'no-cache' });
+ if (!res.ok) throw new Error('Failed to fetch active year');
+ const data = await res.json();
+ const year = Number(data?.year);
+ return Number.isInteger(year) ? year : null;
+ } catch (_) {
+ return null;
+ }
+}
+
+window.addEventListener('DOMContentLoaded', async function () {
+ const pageLoader = createPageLoader({ title: 'Chargement de la FAQ' });
+ pageLoader.setProgress(14);
+
+ const token = localStorage.getItem('auth_token');
+ if (!token) {
+ window.location.href = '/';
+ return;
+ }
+
+ const decoded = decodeJwtPayload(token);
+ if (!decoded || isExpired(decoded)) {
+ try { localStorage.removeItem('auth_token'); } catch (_) {}
+ window.location.href = '/';
+ return;
+ }
+
+ if (decoded.mustChangePassword) {
+ window.location.href = '/change-password.html';
+ return;
+ }
+
+ // Header is usually set by app_header_boot, but keep it consistent if it ran before DOM was ready.
+ const nameEl = document.getElementById('user-name');
+ if (nameEl && !nameEl.textContent) nameEl.textContent = decoded.name || '';
+
+ if (decoded.admin) {
+ const adminLink = document.getElementById('admin-control-link');
+ if (adminLink) adminLink.classList.remove('d-none');
+ }
+
+ pageLoader.setProgress(40);
+ const activeYear = await fetchActiveYear();
+ if (activeYear) document.title = `Pool Oscars (${activeYear}) - FAQ`;
+ pageLoader.setProgress(90);
+
+ const logoffBtn = document.getElementById('log-off');
+ if (logoffBtn) {
+ logoffBtn.addEventListener('click', function () {
+ localStorage.removeItem('auth_token');
+ window.location.href = '/';
+ });
+ }
+
+ pageLoader.done();
+});
+
diff --git a/public/movies.html b/public/movies.html
index b0ce59d..c4af61b 100644
--- a/public/movies.html
+++ b/public/movies.html
@@ -59,6 +59,7 @@
Films
Checklist
Statistiques
+
FAQ
Admin
diff --git a/public/player.html b/public/player.html
index c4feba4..e12ea44 100644
--- a/public/player.html
+++ b/public/player.html
@@ -127,6 +127,7 @@