Files
Forge-Engine/docs/plan/maquettes/document-formation-web.html
T
williamandClaude Sonnet 5 b2e933f322
Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur
Support de formation (voir docs/plan/PLAN.md), sans toucher a
l'architecture en couches existante :

- screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D,
  avant l'arrivee d'un second "moteur" cote document) : ~85 imports
  corriges, contrat import-linter mis a jour, meme forme de couches.
- routes/, scripts/, static/, templates/, tests/ : tout ce qui est
  propre au jeu 2D deplace dans un sous-dossier game/ de chacun
  (routes/game/, static/game/, templates/game/, tests/game/,
  scripts/game/) ; ce qui est partage par le site (auth, onboarding,
  dashboard, uploads, db/) reste a la racine de chaque dossier. Un
  sous-dossier document/ (vide) cree dans chacun pour le futur chantier.
- styles/ volontairement inchange : les 3 fichiers sources sont
  concatenes en un seul static/style.css charge par tout le site,
  scinder leur CONTENU (editeur vs partage) serait un refactor CSS
  distinct, pas un deplacement mecanique.
- Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en
  profondeur : copie des assets, URLs d'icones relatives a
  static/style.css (qui ne bouge pas), manifeste, wrapper SCORM.
- Deux regressions d'un sweep de renommage anterieur corrigees au passage
  (screens.js/screens/scene-objects incorrectement convertis en
  game_engine.js/game_engine/scene-objects dans des commentaires).
- Effet de bord Windows decouvert et corrige : git mv + Path.write_text
  convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers
  normalises en LF.
- .eslintrc.json/package.json : uniquement les chemins de glob mis a jour
  (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7
  reste volontairement non committee (package-lock.json restaure a la
  version precedente).

Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit,
lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ;
demarrage serveur + requetes HTTP manuelles confirmant que les assets
deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien.

SKIP=djlint : backlog H021 (styles inline) deja documente comme dette
assumee dans CODE_QUALITY.md section 6, aucun template touche par ce
commit au-dela d'un deplacement de fichier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:27:53 +02:00

367 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Consignes d'évacuation — Formation incendie</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0b0e14;
--bg-2:#11151d;
--card:#161b25;
--border:#232a38;
--text:#e8ecf4;
--muted:#8b95a7;
--accent1:#ff5f2e;
--accent2:#ffb020;
--gradient:linear-gradient(120deg,#ff5f2e,#ffb020);
--glow:0 0 0 1px rgba(255,95,46,0.25), 0 10px 30px -8px rgba(255,95,46,0.25);
--overlay:rgba(11,14,20,0.9);
--success-bg:rgba(59,165,93,0.12);
--success-border:#3ba55d;
--success-text:#8fe3ab;
--danger-bg:rgba(255,95,46,0.12);
--danger-border:#ff5f2e;
--danger-text:#ffb3a0;
}
:root[data-theme="light"]{
--bg:#F6F5F2;
--bg-2:#FFFFFF;
--card:#FFFFFF;
--border:#E3E1DB;
--text:#14161C;
--muted:#6B7280;
--glow:0 0 0 1px rgba(255,95,46,0.3), 0 10px 30px -8px rgba(255,95,46,0.3);
--overlay:rgba(255,255,255,0.85);
--success-bg:#E4F5EA;
--success-border:#2E7D46;
--success-text:#1D4C2C;
--danger-bg:#FDEDE8;
--danger-border:#C1451F;
--danger-text:#7A1418;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;}
body{
font-family:'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background:var(--bg); color:var(--text);
min-height:100vh;
}
/* barre de progression du parcours */
.course-bar{
position:sticky; top:0; z-index:10;
background:var(--overlay); backdrop-filter:blur(6px);
border-bottom:1px solid var(--border);
padding:14px 24px;
display:flex; align-items:center; justify-content:space-between; gap:16px;
}
.course-bar .brand{ display:flex; align-items:center; gap:10px; min-width:0; }
.course-bar .label{ font-size:13px; font-weight:600; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.theme-toggle{
width:34px; height:34px; border-radius:10px; border:1px solid var(--border); background:transparent;
display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--text); flex-shrink:0;
}
.theme-toggle:hover{ border-color:var(--muted); }
.forge-badge{
padding:0.35rem 0.85rem; border-radius:999px; background:rgba(255,95,46,0.1);
border:1px solid rgba(255,95,46,0.35); color:var(--accent1); font-size:0.72rem;
font-weight:600; letter-spacing:0.04em; flex-shrink:0;
}
.progress-track{ height:4px; background:var(--border); border-radius:2px; overflow:hidden; margin-top:10px; }
.progress-fill{ height:100%; background:var(--gradient); width:60%; }
main{ max-width:720px; margin:0 auto; padding:48px 24px 80px; }
.hero-title{
font-weight:800;
font-size:clamp(2.6rem, 6vw, 4.6rem);
line-height:1.05;
margin:0 0 18px;
background:var(--gradient);
-webkit-background-clip:text;
background-clip:text;
color:transparent;
}
.hero-intro{ font-size:17px; line-height:1.6; color:var(--muted); max-width:56ch; margin:0 0 40px; }
section{ margin-bottom:40px; }
.image-frame{
width:100%; border-radius:16px; background:var(--bg-2); border:1px solid var(--border);
height:260px; display:flex; align-items:center; justify-content:center; gap:10px; color:var(--muted);
margin-bottom:40px;
}
.image-frame span{ font-size:14px; font-weight:600; }
h2{ font-size:2rem; font-weight:800; margin:0 0 16px; line-height:1.15; }
.cols{ display:flex; gap:32px; flex-wrap:wrap; }
.cols .text{ flex:1; min-width:240px; font-size:16px; line-height:1.65; color:var(--text); }
.cols .steps{ flex:1; min-width:240px; display:flex; flex-direction:column; gap:14px; }
.step{ display:flex; gap:14px; align-items:flex-start; }
.step .num{
flex-shrink:0; width:30px; height:30px; border-radius:999px; background:var(--card);
border:1px solid var(--border); display:flex; align-items:center; justify-content:center;
font-weight:800; font-size:13px; color:var(--accent1);
}
.step .txt{ font-size:15px; line-height:1.5; padding-top:4px; }
.divider{ height:1px; background:var(--border); margin:40px 0; }
/* ===== QUIZ (forge-card) ===== */
.forge-card{
background:linear-gradient(180deg, var(--card), var(--bg-2));
border:1px solid var(--border); border-radius:16px; padding:1.8rem;
transition:box-shadow .2s ease;
}
.forge-card:hover{ box-shadow:var(--glow); }
.quiz-kicker{ font-size:12px; font-weight:700; letter-spacing:0.04em; color:var(--accent1); margin-bottom:6px; }
.quiz-title{ font-size:1.15rem; font-weight:700; margin:0 0 20px; }
.progress-track.quiz{ margin-top:0; margin-bottom:10px; }
.quiz-meta{ display:flex; justify-content:space-between; font-size:13px; color:var(--muted); font-weight:600; margin-bottom:20px; }
.q-text{ font-size:17px; font-weight:700; line-height:1.3; margin:0 0 18px; }
.q-options{ display:flex; flex-direction:column; gap:10px; }
.q-option{
text-align:left; padding:13px 15px; border:1px solid var(--border); background:var(--bg-2);
color:var(--text); border-radius:10px; font-size:15px; font-weight:500; cursor:pointer;
display:flex; align-items:center; gap:12px; transition:border-color .15s ease;
}
.q-option:hover:not(:disabled){ border-color:var(--accent1); }
.q-option .letter{
flex-shrink:0; width:24px; height:24px; border-radius:50%; border:1px solid var(--border);
display:flex; align-items:center; justify-content:center; font-weight:700; font-size:12.5px; color:var(--muted);
}
.q-option.correct{ border-color:var(--success-border); background:var(--success-bg); }
.q-option.correct .letter{ border-color:var(--success-border); background:var(--success-border); color:#0b0e14; }
.q-option.wrong{ border-color:var(--danger-border); background:var(--danger-bg); }
.q-option.wrong .letter{ border-color:var(--accent1); background:var(--accent1); color:#0b0e14; }
.q-option:disabled:not(.correct):not(.wrong){ opacity:0.45; }
.q-feedback{ margin-top:16px; padding:13px 15px; border-radius:10px; font-size:14px; line-height:1.5; display:none; }
.q-feedback.show{ display:block; }
.q-feedback.ok{ background:var(--success-bg); color:var(--success-text); border:1px solid var(--success-border); }
.q-feedback.ko{ background:var(--danger-bg); color:var(--danger-text); border:1px solid var(--danger-border); }
.q-nextbar{ margin-top:20px; display:flex; justify-content:flex-end; }
.forge-btn{
padding:0.75rem 1.6rem; border-radius:10px; border:none;
background:var(--gradient); color:#14100c; font-weight:700; font-size:14px; cursor:pointer;
opacity:0; pointer-events:none; transition:opacity .2s ease;
}
.forge-btn.show{ opacity:1; pointer-events:auto; }
.q-result{ text-align:center; padding:6px 4px 4px; }
.q-result .big{ font-weight:800; font-size:52px; background:var(--gradient); -webkit-background-clip:text; background-clip:text; color:transparent; line-height:1; }
.q-result .sub{ font-size:13px; color:var(--muted); font-weight:600; margin-top:6px; }
.q-result p.msg{ font-size:15px; margin:18px 0 24px; color:var(--text); line-height:1.5; }
.forge-btn-secondary{
padding:0.7rem 1.5rem; border-radius:10px; background:transparent; border:1px solid var(--border);
color:var(--text); font-weight:600; font-size:14px; cursor:pointer;
}
footer{
max-width:720px; margin:0 auto; padding:0 24px 60px;
display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap;
}
.next-section{ font-size:14px; color:var(--muted); }
.next-section strong{ color:var(--text); }
@media (max-width:640px){
main{ padding:32px 18px 60px; }
.course-bar{ padding:12px 16px; }
h2{ font-size:1.5rem; }
.cols{ gap:24px; }
.forge-card{ padding:1.3rem; }
footer{ padding:0 18px 48px; }
}
</style>
</head>
<body>
<div class="course-bar">
<div style="flex:1; min-width:0;">
<div class="brand">
<span class="label">Formation incendie · Module 3 sur 4</span>
<span class="forge-badge">En cours</span>
</div>
<div class="progress-track"><div class="progress-fill"></div></div>
</div>
<button class="theme-toggle" id="themeToggle" aria-label="Changer de thème"></button>
</div>
<main>
<h1 class="hero-title">Consignes d'évacuation</h1>
<p class="hero-intro">Ce module présente les étapes à suivre en cas de déclenchement de l'alarme incendie, ainsi que les bons réflexes à adopter avant l'arrivée des secours.</p>
<div class="image-frame">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="10" r="1.4"/><path d="M21 16.5l-5-5-4 4-3-3-5 5"/></svg>
<span>Plan d'évacuation de l'étage</span>
</div>
<section>
<div class="cols">
<div class="text">Dès le signal d'alarme, l'évacuation est immédiate. Aucune tâche en cours ne justifie un délai, même de quelques secondes. Les trois étapes ci-contre doivent devenir un réflexe.</div>
<div class="steps">
<div class="step"><div class="num">1</div><div class="txt">Donner l'alerte</div></div>
<div class="step"><div class="num">2</div><div class="txt">Évacuer par l'issue la plus proche</div></div>
<div class="step"><div class="num">3</div><div class="txt">Rejoindre le point de rassemblement</div></div>
</div>
</div>
</section>
<div class="divider"></div>
<section>
<div class="forge-card" id="quizCard">
<div class="quiz-kicker">Quiz</div>
<div class="quiz-title">Vérifiez vos réflexes</div>
<div class="progress-track quiz"><div class="progress-fill" id="qFill" style="width:0%;"></div></div>
<div class="quiz-meta"><span id="qProg"></span><span id="qScore"></span></div>
<div class="q-text" id="qText"></div>
<div class="q-options" id="qOptions"></div>
<div class="q-feedback" id="qFeedback"></div>
<div class="q-nextbar"><button class="forge-btn" id="qNext">Question suivante</button></div>
</div>
<div class="forge-card hidden" id="qResultCard" style="display:none;">
<div class="q-result">
<div class="big" id="qBig">0/5</div>
<div class="sub">bonnes réponses</div>
<p class="msg" id="qMsg"></p>
<button class="forge-btn-secondary" id="qRestart">Recommencer le quiz</button>
</div>
</div>
</section>
</main>
<footer>
<div class="next-section">Section suivante : <strong>Utilisation des extincteurs</strong></div>
<button class="forge-btn">Continuer</button>
</footer>
<script>
const questions = [
{ q:"Vous découvrez un début d'incendie. Premier réflexe ?",
options:["Donner l'alerte et déclencher l'alarme","Éteindre seul sans prévenir","Récupérer ses affaires avant de sortir","Prendre l'ascenseur pour évacuer"],
correct:0,
ok:"C'est le bon réflexe : alerter avant d'agir permet de déclencher l'évacuation au plus vite.",
ko:"Le premier réflexe est toujours de donner l'alerte, avant toute autre action." },
{ q:"Quel extincteur utiliser sur un feu d'origine électrique ?",
options:["Extincteur à eau pulvérisée","Extincteur au CO2","Extincteur à mousse","Seau d'eau"],
correct:1,
ok:"Exact : le CO2 ne conduit pas l'électricité et ne laisse aucun résidu.",
ko:"Seul le CO2 convient : les autres types conduisent l'électricité ou abîment le matériel." },
{ q:"Le signal d'évacuation retentit. Que faites-vous ?",
options:["Je termine ma tâche puis je sors","Je sors immédiatement par l'issue la plus proche","J'attends une confirmation orale","Je vérifie d'abord l'origine de l'alarme"],
correct:1,
ok:"Correct : toute alarme impose une évacuation immédiate, sans délai.",
ko:"Il faut évacuer immédiatement, sans attendre ni vérifier la cause." },
{ q:"Où se retrouver après avoir évacué le bâtiment ?",
options:["Sur le parking le plus proche de sa voiture","Au point de rassemblement désigné","Devant l'entrée principale, peu importe où","Chacun rentre chez soi"],
correct:1,
ok:"Le point de rassemblement permet de faire l'appel du personnel.",
ko:"Il faut toujours se rendre au point de rassemblement désigné." },
{ q:"Une porte est chaude au toucher pendant l'évacuation. Que faites-vous ?",
options:["Je l'ouvre rapidement et je passe","Je ne l'ouvre pas, je cherche une autre issue","Je l'ouvre en la poussant du pied","J'attends 2 minutes puis j'ouvre"],
correct:1,
ok:"Bonne réaction : une porte chaude signale un feu derrière, il ne faut jamais l'ouvrir.",
ko:"Une porte chaude ne doit jamais être ouverte : cherchez une autre issue." }
];
let cur=0, score=0, answered=false;
const qText=document.getElementById('qText'), qOptions=document.getElementById('qOptions'),
qFeedback=document.getElementById('qFeedback'), qNext=document.getElementById('qNext'),
qFill=document.getElementById('qFill'), qProg=document.getElementById('qProg'), qScore=document.getElementById('qScore'),
qCard=document.getElementById('quizCard'), qResultCard=document.getElementById('qResultCard');
function render(){
answered=false;
const item = questions[cur];
qText.textContent = item.q;
qOptions.innerHTML='';
qFeedback.className='q-feedback'; qFeedback.textContent='';
qNext.classList.remove('show');
const letters=['A','B','C','D'];
item.options.forEach((opt,idx)=>{
const btn=document.createElement('button');
btn.className='q-option';
btn.innerHTML=`<span class="letter">${letters[idx]}</span><span>${opt}</span>`;
btn.addEventListener('click',()=>select(idx,btn));
qOptions.appendChild(btn);
});
qFill.style.width=(cur/questions.length*100)+'%';
qProg.textContent=`Question ${cur+1} sur ${questions.length}`;
qScore.textContent=`Score : ${score}`;
}
function select(idx,btn){
if(answered) return;
answered=true;
const item=questions[cur];
const all=qOptions.querySelectorAll('.q-option');
all.forEach(o=>o.disabled=true);
if(idx===item.correct){
btn.classList.add('correct'); score++;
qFeedback.classList.add('show','ok'); qFeedback.textContent=item.ok;
} else {
btn.classList.add('wrong'); all[item.correct].classList.add('correct');
qFeedback.classList.add('show','ko'); qFeedback.textContent=item.ko;
}
qScore.textContent=`Score : ${score}`;
qNext.classList.add('show');
qNext.textContent = (cur===questions.length-1)?'Voir mon résultat':'Question suivante';
}
qNext.addEventListener('click',()=>{
cur++;
if(cur<questions.length) render(); else showResult();
});
function showResult(){
qFill.style.width='100%';
qCard.style.display='none'; qResultCard.style.display='block';
document.getElementById('qBig').textContent=`${score}/${questions.length}`;
const msg=document.getElementById('qMsg');
if(score===questions.length) msg.textContent="Excellent ! Les bons réflexes sont parfaitement maîtrisés.";
else if(score>=questions.length-1) msg.textContent="Très bon résultat, une relecture rapide suffira.";
else if(score>=questions.length/2) msg.textContent="Résultat correct, certains réflexes essentiels restent à revoir.";
else msg.textContent="Il est recommandé de revoir la formation avant une situation réelle.";
}
document.getElementById('qRestart').addEventListener('click',()=>{
cur=0; score=0;
qResultCard.style.display='none'; qCard.style.display='block';
render();
});
render();
/* ---- thème clair / sombre ---- */
const SUN_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M2 12h2M20 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg>';
const MOON_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"/></svg>';
const themeToggle = document.getElementById('themeToggle');
function applyTheme(theme){
document.documentElement.setAttribute('data-theme', theme);
themeToggle.innerHTML = theme === 'light' ? MOON_ICON : SUN_ICON;
themeToggle.setAttribute('aria-label', theme === 'light' ? 'Passer en thème sombre' : 'Passer en thème clair');
}
let storedTheme = 'dark';
try {
const saved = localStorage.getItem('forge-doc-theme');
if(saved === 'light' || saved === 'dark') storedTheme = saved;
} catch(e) { /* stockage indisponible, on reste sur le thème par défaut */ }
applyTheme(storedTheme);
themeToggle.addEventListener('click', () => {
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
applyTheme(next);
try { localStorage.setItem('forge-doc-theme', next); } catch(e) { /* ignore */ }
});
</script>
</body>
</html>