La page de correction ne réagissait pas au dépôt d'un PDF : elle encodait le fichier en base64 dans localStorage puis redirigeait vers une page dédiée censée le relire. Ce relais échoue en file://, qui est le mode d'ouverture habituel des pages. corriger-exercice.html devient autonome : sélection de l'exercice, dépôt du PDF et affichage des réponses se font sur place, sans localStorage ni redirection. Les trois parseurs (BioCID, FDS, TP Nuisibles) sont regroupés derrière un format commun avec carrousel exercice par exercice, bandeau participants, correction chiffrée pour la partie FDS et annexe TP3. Ajoute un garde-fou signalant un PDF ne correspondant pas à l'exercice sélectionné, repointe les boutons « Corriger » de Exo_FDS et TP_Nuisibles vers la page unifiée, et supprime les trois pages de correction dédiées devenues sans référence.
1830 lines
74 KiB
HTML
1830 lines
74 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>FDS, FT & Étiquettes — Gauthier Chombart & Nathan Chauwin</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="../header-style.css"/>
|
|
<style>
|
|
/* SLIDES */
|
|
.slide {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: none;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2rem;
|
|
padding-top: calc(70px + 2rem);
|
|
padding-bottom: calc(84px + 2rem);
|
|
opacity: 0;
|
|
transition: opacity 0.4s ease;
|
|
overflow-y: auto;
|
|
min-height: calc(100vh - 70px);
|
|
}
|
|
.slide.active { display: flex; opacity: 1; }
|
|
.slide.fade-in { animation: fadeIn 0.5s ease forwards; }
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(16px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.progress-fill { background: var(--amber); transition: width 0.4s ease; }
|
|
|
|
/* PHASE INDICATOR */
|
|
.phase-badge {
|
|
position: fixed;
|
|
top: 80px; right: 16px;
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
z-index: 100;
|
|
}
|
|
|
|
/* BOTTOM BAR (modèle BioCID) */
|
|
.bottom-bar {
|
|
position: fixed;
|
|
bottom: 0; left: 0; right: 0;
|
|
height: 84px;
|
|
background: rgba(250, 238, 218, 0.98);
|
|
border-top: 2px solid var(--amber);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0 20px;
|
|
z-index: 100;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
.slide-counter {
|
|
position: absolute;
|
|
left: 20px;
|
|
font-size: 12px;
|
|
font-family: 'Syne', sans-serif;
|
|
font-weight: 600;
|
|
color: var(--amber-dark);
|
|
}
|
|
/* Boutons centralisés dans la bottom bar */
|
|
.bar-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
.footer-btn {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
padding: 11px 24px;
|
|
border-radius: 40px;
|
|
border: 1.5px solid var(--amber);
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
line-height: 1;
|
|
transition: all 0.2s;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
}
|
|
.footer-btn:hover { transform: translateY(-1px); }
|
|
.footer-btn:active { transform: scale(0.98); }
|
|
.footer-btn.is-primary { background: var(--amber); color: #fff; }
|
|
.footer-btn.is-primary:hover { background: var(--amber-dark); }
|
|
.footer-btn.is-secondary { background: transparent; color: var(--amber-dark); }
|
|
.footer-btn.is-secondary:hover { background: rgba(186,117,23,0.08); }
|
|
.footer-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; pointer-events: none; }
|
|
@media (max-width: 560px) {
|
|
.slide-counter { display: none; }
|
|
.footer-btn { padding: 9px 16px; font-size: 12px; }
|
|
}
|
|
|
|
/* BUTTONS */
|
|
.btn {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
padding: 12px 28px;
|
|
border-radius: 40px;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
text-decoration: none;
|
|
}
|
|
.btn:hover { transform: translateY(-1px); }
|
|
.btn:active { transform: scale(0.98); }
|
|
.btn-amber { background: var(--amber); color: white; }
|
|
.btn-coral { background: var(--coral-mid); color: white; }
|
|
.btn-teal { background: var(--teal-mid); color: white; }
|
|
.btn-outline { background: transparent; border: 1.5px solid var(--amber); color: var(--amber-dark); }
|
|
|
|
/* HERO / TITLES */
|
|
.hero-label {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--amber);
|
|
margin-bottom: 8px;
|
|
}
|
|
.hero-title {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: clamp(32px, 5.5vw, 58px);
|
|
font-weight: 800;
|
|
line-height: 1.02;
|
|
color: var(--text);
|
|
margin-bottom: 12px;
|
|
}
|
|
.hero-sub {
|
|
font-size: 16px;
|
|
color: var(--text-muted);
|
|
max-width: 480px;
|
|
text-align: center;
|
|
line-height: 1.6;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.section-tag {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
margin-bottom: 10px;
|
|
display: inline-block;
|
|
background: var(--amber-light);
|
|
color: var(--amber-dark);
|
|
}
|
|
.slide-title {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: clamp(22px, 3.5vw, 34px);
|
|
font-weight: 800;
|
|
line-height: 1.15;
|
|
margin-bottom: 0.5rem;
|
|
text-align: center;
|
|
}
|
|
.slide-intro {
|
|
font-size: 14px;
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
max-width: 580px;
|
|
line-height: 1.6;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.actions-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
margin-top: 1.5rem;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* OVERVIEW CARDS */
|
|
.overview-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 14px;
|
|
max-width: 680px;
|
|
width: 100%;
|
|
margin: 1.5rem 0;
|
|
}
|
|
.overview-card {
|
|
background: white;
|
|
border: 0.5px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 1.25rem;
|
|
text-align: center;
|
|
}
|
|
.overview-icon { font-size: 30px; margin-bottom: 8px; }
|
|
.overview-title { font-family: 'Syne', sans-serif; font-size: 13px; font-weight: 700; margin-bottom: 4px; }
|
|
.overview-sub { font-size: 12px; color: var(--text-muted); line-height: 1.4; }
|
|
|
|
.info-card {
|
|
border-radius: 12px;
|
|
padding: 16px 20px;
|
|
max-width: 620px;
|
|
width: 100%;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
background: var(--amber-light);
|
|
color: var(--amber-dark);
|
|
}
|
|
|
|
/* QUIZ ROWS */
|
|
.quiz-list { width: 100%; max-width: 720px; display: flex; flex-direction: column; gap: 12px; }
|
|
.quiz-list.quiz-grid {
|
|
max-width: 1080px;
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 12px;
|
|
}
|
|
@media (max-width: 900px) {
|
|
.quiz-list.quiz-grid { grid-template-columns: 1fr; max-width: 720px; }
|
|
}
|
|
|
|
/* SLIDE 3 — DRAG & DROP */
|
|
#slide-3 {
|
|
justify-content: flex-start;
|
|
padding-top: calc(70px + 0.9rem);
|
|
padding-bottom: calc(84px + 0.6rem);
|
|
}
|
|
#slide-3 .slide-title { margin: 0.1rem 0 0.15rem; font-size: clamp(19px, 2.4vw, 26px); }
|
|
.dnd-hint { font-size: 12.5px; color: var(--text-muted); text-align: center; margin: 0 0 10px; }
|
|
.dnd-layout {
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
align-items: start;
|
|
gap: 14px;
|
|
width: 100%;
|
|
max-width: 1200px;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
#slide-3 .quiz-grid { gap: 8px; }
|
|
#slide-3 .verify-btn { padding: 9px 26px; }
|
|
#slide-3 .actions-row { margin-top: 0.8rem; }
|
|
.dnd-bank { display: grid; grid-template-columns: 1fr; gap: 6px; align-content: start; }
|
|
.dnd-tile {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 12px; font-weight: 800; white-space: nowrap;
|
|
padding: 8px 12px; min-width: 92px; text-align: center;
|
|
border-radius: 8px;
|
|
background: var(--amber-light); color: var(--amber-dark);
|
|
border: 1px solid var(--amber);
|
|
cursor: grab; user-select: none;
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
}
|
|
.dnd-tile:hover { transform: translateY(-1px); box-shadow: 0 4px 10px rgba(186,117,23,0.15); }
|
|
.dnd-tile:active { cursor: grabbing; }
|
|
.dnd-tile.armed { outline: 2px solid var(--amber-dark); box-shadow: 0 0 0 3px rgba(186,117,23,0.18); }
|
|
.dnd-tile.dragging { opacity: 0.4; }
|
|
#slide-3 .answer-row { padding: 6px 12px; gap: 10px; }
|
|
#slide-3 .question-num { min-width: 26px; height: 26px; font-size: 11px; }
|
|
#slide-3 .question-text { font-size: 13px; }
|
|
.drop-slot {
|
|
min-width: 92px; min-height: 36px;
|
|
border: 2px dashed var(--amber); border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-family: 'Syne', sans-serif; font-size: 12px; font-weight: 800;
|
|
color: var(--amber-dark); background: var(--amber-light);
|
|
cursor: pointer; flex-shrink: 0;
|
|
transition: all 0.15s ease;
|
|
}
|
|
.drop-slot::before { content: 'Déposez ici'; font-weight: 600; font-size: 11px; opacity: 0.55; }
|
|
.drop-slot.filled { border-style: solid; background: white; }
|
|
.drop-slot.filled::before { content: none; }
|
|
.drop-slot.drag-over { border-color: var(--amber-dark); background: white; transform: scale(1.04); }
|
|
.drop-slot.slot-correct { border-color: #2e7d32; color: #2e7d32; }
|
|
.drop-slot.slot-wrong { border-color: #c62828; color: #c62828; }
|
|
@media (max-width: 900px) {
|
|
.dnd-layout { grid-template-columns: 1fr; }
|
|
.dnd-bank { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); }
|
|
}
|
|
/* SLIDE 5 — RELIER (MATCHING) */
|
|
.match-hint { font-size: 12.5px; color: var(--text-muted); text-align: center; margin: -0.4rem 0 0.1rem; max-width: 640px; line-height: 1.5; }
|
|
.match-wrap {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 780px;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
column-gap: clamp(56px, 12vw, 140px);
|
|
align-items: start;
|
|
}
|
|
.match-svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; overflow: visible; }
|
|
.match-col { display: flex; flex-direction: column; gap: 14px; }
|
|
.match-right { justify-content: center; }
|
|
.match-item {
|
|
position: relative;
|
|
background: white;
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
font-size: 13.5px;
|
|
color: var(--text);
|
|
line-height: 1.4;
|
|
min-height: 46px;
|
|
display: flex; align-items: center;
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
.match-left .match-item { padding-right: 24px; }
|
|
.match-right .match-item {
|
|
padding-left: 24px;
|
|
justify-content: center; text-align: center;
|
|
font-family: 'Syne', sans-serif; font-weight: 800; font-size: 13px;
|
|
color: var(--coral); background: var(--coral-light); border-color: var(--coral-mid);
|
|
}
|
|
.match-item.connected { border-color: var(--coral); box-shadow: 0 4px 12px rgba(153,60,29,0.10); }
|
|
.match-dot {
|
|
position: absolute; top: 50%; transform: translateY(-50%);
|
|
width: 16px; height: 16px; border-radius: 50%;
|
|
background: white; border: 2.5px solid var(--coral-mid);
|
|
cursor: pointer; z-index: 3; touch-action: none;
|
|
transition: transform 0.15s ease, background 0.15s ease;
|
|
}
|
|
.match-left .match-dot { right: -8px; }
|
|
.match-right .match-dot { left: -8px; }
|
|
.match-dot:hover { transform: translateY(-50%) scale(1.3); background: var(--coral-mid); }
|
|
.match-dot.filled { background: var(--coral-mid); }
|
|
.match-dot.active { background: var(--coral); border-color: var(--coral); transform: translateY(-50%) scale(1.3); }
|
|
@media (max-width: 700px) {
|
|
.match-hint { font-size: 12px; }
|
|
.match-wrap { column-gap: 44px; }
|
|
.match-item { font-size: 12.5px; padding: 10px 12px; }
|
|
.match-left .match-item { padding-right: 20px; }
|
|
.match-right .match-item { padding-left: 20px; }
|
|
}
|
|
|
|
.answer-row {
|
|
background: white;
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
transition: all 0.25s ease;
|
|
}
|
|
.answer-row:hover { border-color: var(--amber); box-shadow: 0 6px 16px rgba(186,117,23,0.08); }
|
|
.question-num {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
min-width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
background: var(--amber-light);
|
|
color: var(--amber-dark);
|
|
flex-shrink: 0;
|
|
}
|
|
.question-text { flex-grow: 1; font-size: 14px; color: var(--text); font-weight: 500; line-height: 1.4; }
|
|
.answer-label {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: var(--amber-dark);
|
|
white-space: nowrap;
|
|
padding: 4px 10px;
|
|
background: var(--amber-light);
|
|
border: 1px solid var(--amber);
|
|
border-radius: 6px;
|
|
flex-shrink: 0;
|
|
}
|
|
.answer-icon { font-size: 1.2em; flex-shrink: 0; }
|
|
select {
|
|
font-family: 'DM Sans', sans-serif;
|
|
padding: 9px 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
min-width: 175px;
|
|
flex-shrink: 0;
|
|
}
|
|
select:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px rgba(186,117,23,0.1); }
|
|
|
|
.verify-btn {
|
|
font-family: 'Syne', sans-serif;
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 30px;
|
|
border-radius: 40px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.verify-btn:hover { transform: translateY(-1px); }
|
|
.verify-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
|
|
|
|
.result-message {
|
|
margin-top: 4px;
|
|
padding: 16px 20px;
|
|
border-radius: 10px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
display: none;
|
|
max-width: 720px;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
.result-message.show { display: block; }
|
|
.result-message.success { background: var(--teal-light); border: 1.5px solid var(--teal-mid); color: var(--teal-dark); }
|
|
.result-message.error { background: var(--coral-light); border: 1.5px solid var(--coral-mid); color: var(--coral); }
|
|
|
|
/* COMPARISON TABLE */
|
|
.comparison-table {
|
|
width: 100%;
|
|
max-width: 760px;
|
|
border-collapse: collapse;
|
|
background: white;
|
|
border: 0.5px solid var(--border);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
font-size: 13px;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.comparison-table th {
|
|
background: var(--amber-light);
|
|
color: var(--amber-dark);
|
|
padding: 12px 16px;
|
|
text-align: left;
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
border-bottom: 2px solid var(--amber);
|
|
}
|
|
.comparison-table td {
|
|
padding: 12px 16px;
|
|
border-bottom: 0.5px solid var(--border);
|
|
color: var(--text-muted);
|
|
}
|
|
.comparison-table tr:last-child td { border-bottom: none; }
|
|
.comparison-table strong { color: var(--text); }
|
|
|
|
/* TIPS LIST */
|
|
.tips-list { list-style: none; padding: 0; max-width: 680px; width: 100%; display: flex; flex-direction: column; gap: 8px; }
|
|
.tips-list li {
|
|
background: white;
|
|
border: 0.5px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 12px 16px 12px 40px;
|
|
position: relative;
|
|
font-size: 13.5px;
|
|
line-height: 1.5;
|
|
color: var(--text-muted);
|
|
}
|
|
.tips-list li::before { content: '💡'; position: absolute; left: 14px; }
|
|
.tips-list strong { color: var(--text); }
|
|
|
|
/* SCROLL AREA */
|
|
.scroll-area {
|
|
overflow-y: auto;
|
|
max-height: calc(100vh - 200px);
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 0.5rem 0 4rem;
|
|
}
|
|
.scroll-area::-webkit-scrollbar { width: 4px; }
|
|
.scroll-area::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
|
|
/* IDENTITY CARD */
|
|
.identity-card {
|
|
background: white;
|
|
border: 0.5px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 16px 18px;
|
|
max-width: 560px;
|
|
width: 100%;
|
|
margin: 0.5rem 0;
|
|
}
|
|
.identity-card .identity-title {
|
|
font-family: 'Syne', sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
color: var(--amber-dark);
|
|
margin-bottom: 10px;
|
|
display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.identity-fields { display: flex; flex-direction: column; gap: 8px; }
|
|
.identity-fields input {
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: 13.5px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 9px 13px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
.identity-fields input:focus { outline: none; border-color: var(--amber); background: white; }
|
|
|
|
/* DECO */
|
|
.deco-circle { position: fixed; border-radius: 50%; pointer-events: none; z-index: 0; }
|
|
.slide > * { position: relative; z-index: 1; }
|
|
|
|
@media (max-width: 700px) {
|
|
.overview-grid { grid-template-columns: 1fr; }
|
|
.answer-row { flex-wrap: wrap; }
|
|
select { width: 100%; min-width: 0; }
|
|
}
|
|
|
|
/* ============ SLIDE 7 — ÉTIQUETTE CLIQUABLE (HOTSPOTS) ============ */
|
|
#slide-7 { justify-content: flex-start; }
|
|
#slide-7 .scroll-area { gap: 0.75rem; }
|
|
|
|
/* Mise en situation */
|
|
.situation-box {
|
|
display: flex; gap: 12px; align-items: flex-start;
|
|
max-width: 680px; width: 100%;
|
|
background: var(--teal-light);
|
|
border: 1px solid var(--teal-mid);
|
|
border-left: 4px solid var(--teal-mid);
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
color: var(--teal-dark);
|
|
font-size: 13.5px; line-height: 1.55;
|
|
text-align: left;
|
|
}
|
|
.situation-box .sit-ico { font-size: 22px; line-height: 1.1; flex-shrink: 0; }
|
|
.situation-box strong { color: var(--teal-dark); }
|
|
|
|
/* Barre de consigne / question courante */
|
|
.eti-prompt {
|
|
display: flex; align-items: center; gap: 12px;
|
|
flex-wrap: wrap; justify-content: center;
|
|
max-width: 680px; width: 100%;
|
|
background: #fff; border: 1px solid var(--border); border-radius: 12px;
|
|
padding: 10px 16px; text-align: center;
|
|
}
|
|
.eti-progress {
|
|
font-family: 'Syne', sans-serif; font-weight: 800; font-size: 11px;
|
|
letter-spacing: 0.04em; text-transform: uppercase;
|
|
color: var(--teal-dark); background: var(--teal-light);
|
|
padding: 4px 10px; border-radius: 20px; white-space: nowrap;
|
|
}
|
|
.eti-question { font-size: 14.5px; font-weight: 600; color: var(--text); }
|
|
|
|
/* Scène de l'étiquette + popover */
|
|
.eti-stage { position: relative; width: 100%; max-width: 680px; display: flex; justify-content: center; }
|
|
|
|
/* Étiquette reconstituée (DÉS'INFECT — Annexe I FHER) */
|
|
.label-card {
|
|
position: relative;
|
|
width: 100%; max-width: 640px;
|
|
background: #fff; color: #1a1a18;
|
|
border: 1px solid #d9d2c4; border-radius: 6px;
|
|
padding: 16px 18px 0; overflow: hidden;
|
|
font-family: 'DM Sans', sans-serif; font-size: 10.5px; line-height: 1.42;
|
|
box-shadow: 0 8px 30px rgba(0,0,0,0.10);
|
|
}
|
|
.lbl-top { display: flex; align-items: center; gap: 12px; border-bottom: 2px solid #1a1a18; padding-bottom: 6px; margin-bottom: 10px; }
|
|
.lbl-flag { font-size: 20px; }
|
|
.lbl-title { font-family: 'Syne', sans-serif; font-weight: 800; font-size: clamp(24px, 5vw, 40px); letter-spacing: -0.01em; margin: 0; }
|
|
.lbl-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
.lbl-h { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.02em; margin-bottom: 2px; }
|
|
.lbl-block { margin-bottom: 9px; }
|
|
.lbl-small { color: #555; font-size: 9.5px; line-height: 1.6; margin-top: 4px; }
|
|
.lbl-bottom { display: grid; grid-template-columns: auto 1fr auto; align-items: end; gap: 14px; margin-top: 4px; padding: 10px 0; }
|
|
.lbl-net { display: flex; flex-direction: column; gap: 2px; }
|
|
.lbl-net span { font-size: 10px; }
|
|
.lbl-net strong { font-family: 'Syne', sans-serif; font-size: 22px; line-height: 1; }
|
|
.lbl-barcode {
|
|
height: 42px; align-self: end; min-width: 90px;
|
|
background: repeating-linear-gradient(90deg, #1a1a18 0 2px, #fff 2px 4px, #1a1a18 4px 5px, #fff 5px 9px);
|
|
}
|
|
.lbl-picto-wrap { display: flex; align-items: center; gap: 8px; }
|
|
.pictos { display: flex; gap: 6px; align-items: center; }
|
|
.ghs-img { width: 42px; height: 42px; object-fit: contain; display: block; }
|
|
.ghs {
|
|
width: 38px; height: 38px; position: relative;
|
|
border: 2.5px solid #c62828; background: #fff;
|
|
transform: rotate(45deg);
|
|
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
|
}
|
|
.ghs span { transform: rotate(-45deg); font-size: 17px; line-height: 1; }
|
|
.ghs.excl span { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 24px; color: #1a1a18; }
|
|
.lbl-danger { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 15px; color: #1a1a18; letter-spacing: 0.02em; }
|
|
.lbl-footer { margin: 6px -18px 0; padding: 8px 18px; background: #2b2b28; color: #e8e2d5; text-align: center; font-size: 9px; line-height: 1.5; }
|
|
|
|
/* Zones cliquables (hotspots) */
|
|
.hotspot {
|
|
position: relative; cursor: pointer; border-radius: 5px;
|
|
outline: 1.5px dashed transparent; outline-offset: 1px;
|
|
transition: background 0.15s ease, outline-color 0.15s ease;
|
|
}
|
|
span.hotspot { display: inline; padding: 0 2px; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
|
|
.label-card:not(.recap) .hotspot:hover { background: rgba(43,122,120,0.10); outline-color: var(--teal-mid); }
|
|
.hotspot.hs-correct { background: rgba(46,125,50,0.14); outline: 1.5px solid #2e7d32; }
|
|
.hotspot.hs-wrong { background: rgba(198,40,40,0.16); outline: 1.5px solid #c62828; animation: hsShake 0.4s ease; }
|
|
@keyframes hsShake { 0%,100%{transform:translateX(0);} 25%{transform:translateX(-3px);} 75%{transform:translateX(3px);} }
|
|
.hs-num {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 16px; height: 16px; border-radius: 50%;
|
|
background: #2e7d32; color: #fff;
|
|
font-family: 'Syne', sans-serif; font-weight: 800; font-size: 9px;
|
|
margin-right: 3px; vertical-align: middle;
|
|
}
|
|
|
|
/* Info-bulle réglementaire */
|
|
.eti-pop {
|
|
position: absolute; z-index: 20; max-width: 290px; width: max-content;
|
|
background: #fff; border: 1px solid var(--border); border-radius: 10px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.18);
|
|
padding: 12px 30px 12px 14px; font-size: 12.5px; line-height: 1.5;
|
|
text-align: left;
|
|
}
|
|
.eti-pop[hidden] { display: none; }
|
|
.eti-pop .pop-status { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
|
|
.eti-pop.ok .pop-status { color: #2e7d32; }
|
|
.eti-pop.ko .pop-status { color: #c62828; }
|
|
.eti-pop.info .pop-status { color: var(--teal-dark); }
|
|
.eti-pop .pop-title { font-weight: 700; color: var(--text); margin-bottom: 3px; }
|
|
.eti-pop .pop-reg { color: var(--text-muted); }
|
|
.eti-pop .pop-close { position: absolute; top: 5px; right: 8px; cursor: pointer; color: var(--text-muted); font-size: 16px; line-height: 1; border: none; background: none; padding: 2px; }
|
|
|
|
/* Check-list de synthèse (modèle FHER p.15) */
|
|
.checklist-table {
|
|
width: 100%; max-width: 780px; border-collapse: collapse;
|
|
background: #fff; border: 1px solid var(--border); border-radius: 12px; overflow: hidden;
|
|
font-size: 12.5px; margin: 0.25rem 0 1rem;
|
|
}
|
|
.checklist-table th { background: var(--teal-light); color: var(--teal-dark); font-family: 'Syne', sans-serif; font-weight: 700; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.03em; padding: 10px 12px; text-align: left; border-bottom: 2px solid var(--teal-mid); }
|
|
.checklist-table td { padding: 9px 12px; border-bottom: 1px solid var(--border); color: var(--text-muted); vertical-align: top; }
|
|
.checklist-table tr:last-child td { border-bottom: none; }
|
|
.checklist-table td.cl-num { text-align: center; }
|
|
.cl-badge { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: #2e7d32; color: #fff; font-family: 'Syne', sans-serif; font-weight: 800; font-size: 10px; }
|
|
.checklist-table strong { color: var(--text); }
|
|
.cl-check { color: #2e7d32; font-weight: 700; }
|
|
.cl-cross { color: #c62828; font-weight: 700; }
|
|
|
|
@media (max-width: 700px) {
|
|
.lbl-cols { grid-template-columns: 1fr; }
|
|
.lbl-title { font-size: 26px; }
|
|
}
|
|
|
|
/* ---- SLIDE 1 — sélection formateur + garde nom ---- */
|
|
.formateur-label { font-size: 11px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted); margin: 4px 0 2px; }
|
|
.formateur-row { display: flex; gap: 8px; }
|
|
.formateur-btn {
|
|
flex: 1; font-family: 'DM Sans', sans-serif; font-size: 13.5px;
|
|
padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px;
|
|
background: var(--bg); color: var(--text); cursor: pointer; transition: all 0.2s ease;
|
|
}
|
|
.formateur-btn:hover { border-color: var(--amber); }
|
|
.formateur-btn.active { background: var(--amber); border-color: var(--amber); color: #fff; }
|
|
.identity-error { color: var(--coral); font-size: 12px; margin-top: 8px; line-height: 1.4; }
|
|
.identity-error[hidden] { display: none; }
|
|
|
|
/* ---- SLIDE 7 — encadré de réponse sur le côté de l'étiquette ---- */
|
|
#eti-stage {
|
|
max-width: 680px;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* ---- SLIDE 7 — récapitulatif : étiquette à gauche, tableau à droite ---- */
|
|
.recap-layout { display: flex; gap: 20px; align-items: flex-start; justify-content: center; width: 100%; max-width: 1120px; }
|
|
.recap-layout #recap-stage { flex: 1 1 0; min-width: 0; max-width: 560px; }
|
|
.recap-layout .checklist-table { flex: 1 1 0; min-width: 0; margin: 0; }
|
|
.recap-layout.animate #recap-stage { animation: recapSlideLeft 0.5s ease both; }
|
|
.recap-layout.animate .checklist-table { animation: recapSlideRight 0.5s ease both; }
|
|
@keyframes recapSlideLeft { from { opacity: 0; transform: translateX(-40px); } to { opacity: 1; transform: none; } }
|
|
@keyframes recapSlideRight { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: none; } }
|
|
|
|
@media (max-width: 1000px) {
|
|
.recap-layout { flex-direction: column; align-items: center; }
|
|
.recap-layout #recap-stage { flex: none; max-width: 560px; width: 100%; }
|
|
.recap-layout .checklist-table { flex: none; width: 100%; max-width: 640px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- HEADER -->
|
|
<header>
|
|
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
|
|
<nav>
|
|
<a href="../index.html">Accueil</a>
|
|
<a href="../index.html#formations">Exercices</a>
|
|
<a href="../index.html#documentation">Documentation</a>
|
|
<a href="../index.html#rapports">Rapports</a>
|
|
<a href="../index.html#about">À propos</a>
|
|
<a href="../index.html#contact">Contact</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<!-- Progress bar -->
|
|
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
|
|
|
|
<!-- Bottom bar -->
|
|
<div class="bottom-bar" id="bottomBar" style="display:none;">
|
|
<div class="slide-counter" id="slideCounter"></div>
|
|
<div class="bar-actions" id="barActions"></div>
|
|
</div>
|
|
|
|
<!-- Confetti -->
|
|
<canvas id="confettiCanvas" style="position:fixed;inset:0;pointer-events:none;z-index:150;display:none;"></canvas>
|
|
|
|
<!-- ============ SLIDE 0 — ACCUEIL ============ -->
|
|
<div class="slide active fade-in" id="slide-0">
|
|
<div class="deco-circle" style="width:400px;height:400px;background:var(--amber-light);top:-100px;right:-80px;opacity:0.6;"></div>
|
|
<div class="deco-circle" style="width:200px;height:200px;background:var(--amber-light);bottom:60px;left:-60px;opacity:0.5;"></div>
|
|
<div style="text-align:center;">
|
|
<div class="hero-label">Certibiocide — Travaux dirigés</div>
|
|
<div class="hero-title">FDS, FT<br>& Étiquettes</div>
|
|
<p class="hero-sub">Maîtrisez les trois documents essentiels des produits biocides : fiche de sécurité, fiche technique et étiquetage réglementaire.</p>
|
|
<button class="btn btn-amber" onclick="goTo(1)">Démarrer le TD →</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ SLIDE 1 — DÉROULÉ ============ -->
|
|
<div class="slide" id="slide-1">
|
|
<div class="deco-circle" style="width:300px;height:300px;background:var(--amber-light);top:-60px;left:-60px;opacity:0.4;"></div>
|
|
<div class="section-tag">Déroulé du TD</div>
|
|
<div class="slide-title">Trois documents, trois usages</div>
|
|
<p class="slide-intro">Vous allez apprendre à localiser une information dans chacun des documents de référence d'un produit biocide.</p>
|
|
<div class="info-card">
|
|
<span>⏱️</span>
|
|
<div>Durée ≈ 30 min. Pour chaque question, sélectionnez la <strong>zone du document</strong> où trouver l'information, puis vérifiez votre score.</div>
|
|
</div>
|
|
<div class="overview-grid">
|
|
<div class="overview-card">
|
|
<div class="overview-icon">📄</div>
|
|
<div class="overview-title">Partie 1 — FDS</div>
|
|
<div class="overview-sub">Fiche de Données de Sécurité — 16 rubriques</div>
|
|
</div>
|
|
<div class="overview-card">
|
|
<div class="overview-icon">📋</div>
|
|
<div class="overview-title">Partie 2 — FT</div>
|
|
<div class="overview-sub">Fiche Technique — mode d'emploi produit</div>
|
|
</div>
|
|
<div class="overview-card">
|
|
<div class="overview-icon">🏷️</div>
|
|
<div class="overview-title">Partie 3 — Étiquette</div>
|
|
<div class="overview-sub">Interface directe avec l'utilisateur</div>
|
|
</div>
|
|
</div>
|
|
<div class="identity-card">
|
|
<div class="identity-title">👥 Identification du groupe</div>
|
|
<div class="identity-fields">
|
|
<input type="text" id="idGroupe" placeholder="Nom du groupe / stagiaires (ex : Groupe 3 — Dupont / Martin)" autocomplete="off" oninput="clearGroupeError()">
|
|
<input type="date" id="idDate">
|
|
<input type="hidden" id="idFormateur">
|
|
<div class="formateur-label">Formateur de la session</div>
|
|
<div class="formateur-row">
|
|
<button type="button" class="formateur-btn" data-formateur="Nathan" onclick="selectFormateur(this)">Nathan</button>
|
|
<button type="button" class="formateur-btn" data-formateur="Gauthier" onclick="selectFormateur(this)">Gauthier</button>
|
|
</div>
|
|
</div>
|
|
<div class="identity-error" id="idGroupe-error" hidden>⚠️ Renseignez le nom du stagiaire ou du groupe pour continuer.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ SLIDE 2 — FDS INTRO ============ -->
|
|
<div class="slide" id="slide-2">
|
|
<div class="deco-circle" style="width:280px;height:280px;background:var(--amber-light);top:-40px;right:-40px;opacity:0.5;"></div>
|
|
<div class="section-tag">Partie 1 / 3 — 10 min</div>
|
|
<div class="slide-title">📄 Fiche de Données de Sécurité</div>
|
|
<p class="slide-intro">La FDS est le document de référence pour connaître les <strong>dangers</strong>, les <strong>EPI</strong>, les <strong>premiers secours</strong> et l'<strong>élimination</strong> d'un produit biocide. Elle contient 16 rubriques standardisées (règlement REACH).</p>
|
|
<div class="info-card">
|
|
<span>🎯</span>
|
|
<div>Pour chaque question, indiquez dans quelle <strong>rubrique (1 à 16)</strong> trouver la réponse — ou « Absent » si l'information n'y figure pas. Les questions sont mélangées à chaque chargement.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ SLIDE 3 — FDS QUIZ (DRAG & DROP) ============ -->
|
|
<div class="slide" id="slide-3">
|
|
<div class="slide-title">Où trouver l'information ?</div>
|
|
<p class="dnd-hint">Glissez-déposez la bonne rubrique dans chaque case — ou cliquez une étiquette puis une case.</p>
|
|
<div class="dnd-layout">
|
|
<div class="dnd-bank" id="fds-bank-left"></div>
|
|
<div class="quiz-list quiz-grid" id="fds-questions"></div>
|
|
<div class="dnd-bank" id="fds-bank-right"></div>
|
|
</div>
|
|
<div id="fds-result" class="result-message"></div>
|
|
</div>
|
|
|
|
<!-- ============ SLIDE 4 — FT INTRO ============ -->
|
|
<div class="slide" id="slide-4">
|
|
<div class="deco-circle" style="width:280px;height:280px;background:var(--coral-light);top:-40px;left:-40px;opacity:0.5;"></div>
|
|
<div class="section-tag" style="background:var(--coral-light);color:var(--coral);">Partie 2 / 3 — 10 min</div>
|
|
<div class="slide-title">📋 Fiche Technique</div>
|
|
<p class="slide-intro">La FT complète la FDS : elle fournit le <strong>mode d'emploi</strong>, les <strong>utilisations</strong>, les <strong>conditionnements</strong> et les <strong>compatibilités matières</strong>. C'est le document pratique de l'utilisateur.</p>
|
|
<div class="info-card" style="background:var(--coral-light);color:var(--coral);">
|
|
<span>🎯</span>
|
|
<div>Indiquez la <strong>section</strong> de la FT où trouver chaque information : Description, Propriétés biocides, Conditionnements, Utilisations — ou « Absent ».</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ SLIDE 5 — FT : RELIER ============ -->
|
|
<div class="slide" id="slide-5">
|
|
<div class="scroll-area">
|
|
<div class="slide-title">Sections de la fiche technique</div>
|
|
<p class="match-hint">Reliez chaque question à la bonne section : cliquez sur un point à gauche, maintenez, puis glissez jusqu'au point de la section correspondante à droite.</p>
|
|
<div class="match-wrap" id="ft-match">
|
|
<svg class="match-svg" id="ft-svg"></svg>
|
|
<div class="match-col match-left" id="ft-left"></div>
|
|
<div class="match-col match-right" id="ft-right"></div>
|
|
</div>
|
|
<div id="ft-result" class="result-message"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ SLIDE 6 — ÉTIQUETTE INTRO ============ -->
|
|
<div class="slide" id="slide-6">
|
|
<div class="deco-circle" style="width:280px;height:280px;background:var(--teal-light);bottom:-40px;right:-40px;opacity:0.6;"></div>
|
|
<div class="section-tag" style="background:var(--teal-light);color:var(--teal-dark);">Partie 3 / 3 — 10 min</div>
|
|
<div class="slide-title">🏷️ Étiquetage</div>
|
|
<p class="slide-intro">L'étiquette est l'interface directe entre le produit et l'utilisateur. Elle communique rapidement les <strong>dangers essentiels</strong>, les <strong>EPI</strong> et les <strong>consignes d'utilisation</strong>.</p>
|
|
<div class="info-card" style="background:var(--teal-light);color:var(--teal-dark);">
|
|
<span>🎯</span>
|
|
<div>Sur une <strong>étiquette réelle</strong> (produit professionnel DÉS'INFECT), <strong>cliquez la zone</strong> qui répond à chaque question : nom commercial, pictogrammes, mention d'avertissement, phrases H, phrases P (EPI, premiers secours, élimination), contenu net et centre antipoison. Chaque clic ouvre la <strong>justification réglementaire</strong> de la zone.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ SLIDE 7 — ÉTIQUETTE CLIQUABLE (HOTSPOTS) ============ -->
|
|
<div class="slide" id="slide-7">
|
|
<div class="scroll-area">
|
|
|
|
<!-- ---- PANNEAU A : exercice ---- -->
|
|
<div id="eti-exercise" style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
|
|
<div class="slide-title">Zones de l'étiquette</div>
|
|
|
|
<div class="situation-box">
|
|
<span class="sit-ico">💬</span>
|
|
<div><strong>Mise en situation —</strong> Vous intervenez chez un client avec un désinfectant de surfaces <strong>à usage exclusivement professionnel</strong>. Avant l'application, il vous interroge sur les dangers et les précautions du produit. Bidon en main, retrouvez rapidement sur l'étiquette chaque information demandée.</div>
|
|
</div>
|
|
|
|
<div class="eti-prompt">
|
|
<span class="eti-progress" id="eti-progress">Question 1 / 9</span>
|
|
<span class="eti-question" id="eti-question">—</span>
|
|
</div>
|
|
|
|
<div class="eti-stage" id="eti-stage">
|
|
<div class="label-card" id="labelCard">
|
|
<!-- Titre / nom commercial -->
|
|
<div class="lbl-top">
|
|
<span class="lbl-flag">🇫🇷</span>
|
|
<span class="hotspot" data-zone="z-nom"><span class="lbl-title">DÉS'INFECT</span></span>
|
|
</div>
|
|
|
|
<div class="lbl-cols">
|
|
<!-- Colonne gauche (contexte, non cliquable) -->
|
|
<div class="lbl-col">
|
|
<div class="lbl-block"><div class="lbl-h">Propriétés :</div>Désinfecte et désodorise les sanitaires. Produit strictement destiné à un usage professionnel.</div>
|
|
<div class="lbl-block"><div class="lbl-h">Mode d'emploi :</div>Concentré à diluer : 5 % pour une activité fongicide, 1 % pour une activité bactéricide. Pulvériser sur les parois, laisser agir 5 à 15 min, rincer à l'eau claire. Type de formulation : concentré soluble (SL). Utilisable en TP2.</div>
|
|
<div class="lbl-small">UFI : 1234-1234-1234-1234<br>Date de péremption : MM/AAAA<br>N° LOT : 123456</div>
|
|
</div>
|
|
|
|
<!-- Colonne droite (précautions, cliquable) -->
|
|
<div class="lbl-col">
|
|
<div class="lbl-block"><div class="lbl-h">Substances actives :</div>Chlorure de didécyldiméthylammonium (N°CAS 7173-51-5) à 75 g/kg ; acide glycolique (CAS 79-14-1) à 35 g/kg.</div>
|
|
<div class="lbl-block">
|
|
<div class="lbl-h">Précautions d'emploi :</div>
|
|
<span>Produit strictement professionnel (FR) — Lire le mode d'emploi ci-joint avant l'emploi. </span>
|
|
<span class="hotspot" data-zone="z-antipoison">Formule déposée au centre anti-poison de Nancy : 03 83 22 50 50.</span>
|
|
<span> </span>
|
|
<span class="hotspot" data-zone="z-h">H302 Nocif en cas d'ingestion. H314 Provoque de graves brûlures de la peau et des lésions oculaires. H410 Très toxique pour les organismes aquatiques, entraîne des effets néfastes à long terme.</span>
|
|
<span> </span>
|
|
<span class="hotspot" data-zone="z-p-epi">P280 Porter des gants de protection, des vêtements de protection et un équipement de protection du visage.</span>
|
|
<span> </span>
|
|
<span class="hotspot" data-zone="z-p-secours">P301+P330+P331 EN CAS D'INGESTION : rincer la bouche, ne pas faire vomir. P305+P351+P338 EN CAS DE CONTACT AVEC LES YEUX : rincer avec précaution à l'eau pendant plusieurs minutes. P310 Appeler immédiatement un CENTRE ANTIPOISON ou un médecin.</span>
|
|
<span> </span>
|
|
<span class="hotspot" data-zone="z-p-elim">P501 Éliminer le contenu / le récipient dans un point de collecte des déchets spéciaux ou dangereux.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bas : contenu net, code-barres, pictogrammes, mention -->
|
|
<div class="lbl-bottom">
|
|
<span class="hotspot" data-zone="z-net">
|
|
<span class="lbl-net"><span>Volume net :</span><strong>5 L</strong></span>
|
|
</span>
|
|
<div class="lbl-barcode" aria-hidden="true"></div>
|
|
<div class="lbl-picto-wrap">
|
|
<span class="hotspot" data-zone="z-picto">
|
|
<span class="pictos">
|
|
<img class="ghs-img" src="../data/ghs09-environnement.jpg" alt="Pictogramme SGH09 — danger pour le milieu aquatique">
|
|
<img class="ghs-img" src="../data/ghs05-corrosif.jpg" alt="Pictogramme SGH05 — corrosif">
|
|
<img class="ghs-img" src="../data/ghs07-nocif.jpg" alt="Pictogramme SGH07 — nocif / irritant">
|
|
</span>
|
|
</span>
|
|
<span class="hotspot" data-zone="z-mention"><span class="lbl-danger">DANGER</span></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lbl-footer">FHER — 2, rue de Sèze, 75009 Paris — Tél. 01 23 45 67 89</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="etiquette-result" class="result-message"></div>
|
|
</div>
|
|
|
|
<!-- ---- PANNEAU B : synthèse / check-list ---- -->
|
|
<div id="eti-recap" hidden style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
|
|
<div class="section-tag" style="background:var(--teal-light);color:var(--teal-dark);">Check-list d'étiquetage</div>
|
|
<div class="slide-title">Récapitulatif — l'étiquette annotée</div>
|
|
<p class="slide-intro">Les 9 zones réglementaires et leurs bonnes réponses, présentées comme la <strong>check-list officielle d'étiquetage des produits biocides</strong> (Guide FHER, p. 15) — un outil réutilisable en conditions réelles.</p>
|
|
|
|
<div class="recap-layout" id="recap-layout">
|
|
<div class="eti-stage" id="recap-stage"></div>
|
|
<table class="checklist-table" id="checklist-table"></table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ SLIDE 8 — SYNTHÈSE / FIN ============ -->
|
|
<div class="slide" id="slide-8">
|
|
<div class="scroll-area">
|
|
<div class="deco-circle" style="width:320px;height:320px;background:var(--amber-light);bottom:-80px;right:-60px;opacity:0.5;"></div>
|
|
<div style="text-align:center;">
|
|
<div style="font-size:44px;margin-bottom:0.5rem;">🎉</div>
|
|
<div class="section-tag">TD terminé</div>
|
|
<div class="slide-title" style="margin-top:0.4rem;">Synthèse comparative</div>
|
|
</div>
|
|
<table class="comparison-table">
|
|
<thead>
|
|
<tr><th>Document</th><th>Utilité principale</th><th>Consultation</th><th>Public</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><strong>FDS</strong></td><td>Sécurité, dangers, EPI, premiers secours</td><td>AVANT manipulation</td><td>Professionnels</td></tr>
|
|
<tr><td><strong>FT</strong></td><td>Mode d'emploi, applications, conditionnements</td><td>Pour l'utilisation</td><td>Utilisateurs finaux</td></tr>
|
|
<tr><td><strong>Étiquette</strong></td><td>Dangers essentiels, interface rapide</td><td>À chaque prise du produit</td><td>Tous les utilisateurs</td></tr>
|
|
</tbody>
|
|
</table>
|
|
<div class="actions-row">
|
|
<button class="btn btn-amber" onclick="exporterPDF()">⬇ Télécharger mes réponses (PDF)</button>
|
|
<a href="corriger-exercice.html?exo=fds" class="btn btn-outline">✔ Corriger un rendu</a>
|
|
<button class="btn btn-outline" onclick="restartAll()">↺ Recommencer</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentSlide = 0;
|
|
let history = [];
|
|
const totalSlides = 8;
|
|
|
|
/* ======================== QUESTIONS ======================== */
|
|
const fdsQuestions = [
|
|
{ id: 1, text: "Quel est le nom du produit et qui est le fabricant ?", answer: "1" },
|
|
{ id: 2, text: "Quels sont les dangers du produit ?", answer: "2" },
|
|
{ id: 3, text: "Quelles sont les molécules actives et leurs concentrations ?", answer: "3" },
|
|
{ id: 4, text: "Quels gestes adopter en premiers secours ?", answer: "4" },
|
|
{ id: 5, text: "En cas d'incendie, quels sont les moyens d'extinction conseillés ?", answer: "5" },
|
|
{ id: 6, text: "Que faire en cas de dispersion accidentelle ?", answer: "6" },
|
|
{ id: 7, text: "Dans quelles conditions stocker mon produit ?", answer: "7" },
|
|
{ id: 8, text: "Quels équipements de protection individuelle (EPI) dois-je porter ?", answer: "8" },
|
|
{ id: 9, text: "Quel est le pH de ce produit ?", answer: "9" },
|
|
{ id: 10, text: "Le produit est-il stable dans des conditions normales ?", answer: "10" },
|
|
{ id: 11, text: "Quels sont les effets potentiels sur la santé ?", answer: "11" },
|
|
{ id: 12, text: "Quel est le degré de toxicité pour les poissons ? Potentiel de bioaccumulation ?", answer: "12" },
|
|
{ id: 13, text: "Comment éliminer le produit ?", answer: "13" },
|
|
{ id: 14, text: "Est-ce que mon produit est classé dangereux pour le transport ?", answer: "14" },
|
|
{ id: 15, text: "En cas d'ingestion du produit, que faire ?", answer: "4,11" },
|
|
{ id: 16, text: "Où trouver le mode d'emploi du produit ?", answer: "absent" }
|
|
];
|
|
|
|
const ftQuestions = [
|
|
{ id: 1, text: "Quel est le nom du produit et qui est le fabricant ?", answer: "Description" },
|
|
{ id: 2, text: "Sur quelles matières ce produit est-il compatible ?", answer: "Absent" },
|
|
{ id: 3, text: "Quel est (sont) le(s) type(s) de ce produit ?", answer: "Propriétés biocides" },
|
|
{ id: 4, text: "Quels sont les types de conditionnements disponibles ?", answer: "Conditionnements" },
|
|
{ id: 5, text: "Où retrouver les différents modes d'utilisation de ce produit ?", answer: "Utilisations" }
|
|
];
|
|
const ftOptions = ["Description", "Propriétés biocides", "Conditionnements", "Utilisations"];
|
|
|
|
/* 9 zones réglementaires de l'étiquette (ordre canonique = check-list FHER p.15).
|
|
zone : identifiant du hotspot correspondant dans l'étiquette
|
|
text : consigne affichée pendant l'exercice
|
|
label : intitulé check-list (réutilisé dans la synthèse et le PDF)
|
|
reg : justification réglementaire (info-bulle) — Guide FHER, Partie 3, p.10-12 */
|
|
const etiZones = [
|
|
{ zone: "z-nom", text: "Retrouvez le nom commercial du produit.",
|
|
label: "Nom commercial",
|
|
reg: "Obligation CLP : le nom commercial doit être identique à celui figurant sur la FDS (Guide FHER, Partie 3 — I.2)." },
|
|
{ zone: "z-picto", text: "Cliquez sur les pictogrammes de danger.",
|
|
label: "Pictogrammes de danger",
|
|
reg: "CLP : pictogrammes aux dimensions requises, regroupés avec la mention d'avertissement et les phrases H/P, se détachant nettement du fond (Partie 3 — I.5)." },
|
|
{ zone: "z-mention", text: "Où figure la mention d'avertissement ?",
|
|
label: "Mention d'avertissement",
|
|
reg: "CLP : « Danger » ou « Attention », disposée dans l'encart d'étiquetage avec pictogrammes et phrases H/P (Partie 3 — I.5)." },
|
|
{ zone: "z-h", text: "Repérez les mentions de danger (phrases H).",
|
|
label: "Mentions de danger (phrases H)",
|
|
reg: "CLP : les phrases H décrivent la nature du danger ; regroupées dans l'encart d'étiquetage (Partie 3 — I.5)." },
|
|
{ zone: "z-p-epi", text: "Où sont indiqués les EPI à porter (phrases P) ?",
|
|
label: "Conseils de prudence — EPI",
|
|
reg: "CLP : conseils de prudence P (ici P280). Six conseils P au maximum, sauf si la nature et la gravité des dangers l'exigent (CLP Art. 28-3) — Partie 3, I.5." },
|
|
{ zone: "z-p-secours", text: "Où trouver les instructions de premiers secours ?",
|
|
label: "Premiers secours (phrases P)",
|
|
reg: "BPR : indications relatives aux premiers soins (Partie 3 — II.8), traduites par les conseils P 3xx (contact, ingestion, inhalation)." },
|
|
{ zone: "z-p-elim", text: "Où sont les consignes d'élimination de l'emballage ?",
|
|
label: "Élimination du produit et de l'emballage",
|
|
reg: "BPR : instructions pour l'élimination en toute sécurité du produit et de son emballage, le cas échéant avec interdiction de le réutiliser (Partie 3 — II.10). Ici P501." },
|
|
{ zone: "z-net", text: "Retrouvez le contenu net du produit.",
|
|
label: "Contenu net",
|
|
reg: "Quantité nette de produit contenue dans l'emballage — mention générale du pavé d'information produit." },
|
|
{ zone: "z-antipoison", text: "Où figurent les coordonnées du centre antipoison ?",
|
|
label: "Centre antipoison / n° d'urgence",
|
|
reg: "Coordonnées d'urgence complétant les instructions de premiers soins (Partie 3 — II.8) ; les mélanges dangereux sont déclarés aux centres antipoison (annexe VIII du CLP)." }
|
|
];
|
|
function etiZoneById(id) { return etiZones.find(z => z.zone === id); }
|
|
|
|
function shuffleArray(array) {
|
|
const a = [...array];
|
|
for (let i = a.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[a[i], a[j]] = [a[j], a[i]];
|
|
}
|
|
return a;
|
|
}
|
|
|
|
function generateQuizzes() {
|
|
// FDS — glisser-déposer : banques d'étiquettes + cases de dépôt
|
|
buildFdsDnd();
|
|
|
|
// FT — exercice « relier » (questions à gauche, sections à droite)
|
|
buildFtMatch();
|
|
|
|
// Étiquette — exercice à zones cliquables (hotspots)
|
|
buildEtiquette();
|
|
}
|
|
|
|
/* ---- Étiquette : zones cliquables ---- */
|
|
let etiOrder = []; // ordre aléatoire des questions
|
|
let etiPos = 0; // question courante
|
|
let etiAnswers = {}; // { zoneCible : zoneCliquée en 1re tentative } — pour le PDF
|
|
let etiFirstTry = {}; // { zoneCible : true si trouvée sans erreur }
|
|
let etiCurError = false; // une erreur a-t-elle eu lieu sur la question courante
|
|
let etiInit = false; // écouteur de clic posé une seule fois
|
|
let etiRecapShown = false;// panneau récapitulatif affiché (slide 7)
|
|
|
|
function buildEtiquette() {
|
|
etiOrder = shuffleArray(etiZones.map((_, i) => i));
|
|
etiPos = 0;
|
|
etiAnswers = {};
|
|
etiFirstTry = {};
|
|
etiCurError = false;
|
|
|
|
const card = document.getElementById('labelCard');
|
|
card.querySelectorAll('.hotspot').forEach(h => h.classList.remove('hs-correct', 'hs-wrong'));
|
|
|
|
closeEtiPop();
|
|
const r = document.getElementById('etiquette-result');
|
|
r.className = 'result-message'; r.innerHTML = '';
|
|
etiRecapShown = false;
|
|
document.getElementById('eti-exercise').style.display = 'flex';
|
|
document.getElementById('eti-recap').style.display = 'none';
|
|
if (currentSlide === 7) renderBarActions();
|
|
|
|
if (!etiInit) {
|
|
card.addEventListener('click', onEtiClick);
|
|
etiInit = true;
|
|
}
|
|
renderEtiQuestion();
|
|
}
|
|
|
|
function renderEtiQuestion() {
|
|
const prog = document.getElementById('eti-progress');
|
|
const ques = document.getElementById('eti-question');
|
|
if (etiPos >= etiOrder.length) {
|
|
prog.textContent = 'Terminé';
|
|
ques.textContent = '✓ Les 9 zones ont été identifiées.';
|
|
return;
|
|
}
|
|
const z = etiZones[etiOrder[etiPos]];
|
|
prog.textContent = 'Question ' + (etiPos + 1) + ' / ' + etiZones.length;
|
|
ques.textContent = z.text;
|
|
}
|
|
|
|
function onEtiClick(e) {
|
|
const hs = e.target.closest('.hotspot');
|
|
if (!hs) return;
|
|
const clicked = hs.dataset.zone;
|
|
|
|
// Exercice terminé : plus d'interaction (les corrections sont dans la correction générale)
|
|
if (etiPos >= etiOrder.length) return;
|
|
|
|
const target = etiZones[etiOrder[etiPos]];
|
|
if (clicked === target.zone) {
|
|
hs.classList.remove('hs-wrong');
|
|
hs.classList.add('hs-correct');
|
|
if (etiAnswers[target.zone] === undefined) etiAnswers[target.zone] = clicked;
|
|
etiFirstTry[target.zone] = !etiCurError;
|
|
etiPos++;
|
|
etiCurError = false;
|
|
renderEtiQuestion();
|
|
if (etiPos >= etiOrder.length) finishEtiquette();
|
|
} else {
|
|
if (etiAnswers[target.zone] === undefined) etiAnswers[target.zone] = clicked; // 1re réponse (erronée) mémorisée
|
|
etiCurError = true;
|
|
etiFirstTry[target.zone] = false;
|
|
hs.classList.remove('hs-wrong'); void hs.offsetWidth; // relance l'animation
|
|
hs.classList.add('hs-wrong');
|
|
setTimeout(() => hs.classList.remove('hs-wrong'), 700);
|
|
}
|
|
}
|
|
|
|
function finishEtiquette() {
|
|
const b = document.getElementById('eti-recap-btn');
|
|
if (b) b.disabled = false;
|
|
}
|
|
|
|
/* Info-bulle réglementaire positionnée près de la zone cliquée */
|
|
function openEtiPop(hs, kind, clickedZone) {
|
|
const pop = document.getElementById('eti-pop');
|
|
const z = etiZoneById(clickedZone);
|
|
|
|
const statusEl = document.getElementById('pop-status');
|
|
pop.className = 'eti-pop ' + kind;
|
|
if (kind === 'ok') statusEl.innerHTML = '✅ Bonne zone';
|
|
else if (kind === 'ko') statusEl.innerHTML = '❌ Ce n\'est pas la bonne zone';
|
|
else statusEl.innerHTML = '🔎 ' + z.label;
|
|
|
|
document.getElementById('pop-title').textContent = z.label;
|
|
document.getElementById('pop-reg').textContent = z.reg;
|
|
|
|
// L'encadré s'affiche dans la colonne latérale, à côté de l'étiquette (plus de superposition).
|
|
const hint = document.getElementById('eti-side-hint');
|
|
if (hint) hint.hidden = true;
|
|
pop.hidden = false;
|
|
}
|
|
|
|
function closeEtiPop() {
|
|
const pop = document.getElementById('eti-pop');
|
|
if (pop) pop.hidden = true;
|
|
const hint = document.getElementById('eti-side-hint');
|
|
if (hint) hint.hidden = false;
|
|
}
|
|
|
|
function resetEtiquette() { buildEtiquette(); }
|
|
|
|
/* ---- Synthèse : étiquette annotée + check-list ---- */
|
|
function showEtiRecap() {
|
|
closeEtiPop();
|
|
const stage = document.getElementById('recap-stage');
|
|
stage.innerHTML = '';
|
|
|
|
// Clone de l'étiquette, toutes zones en « correct » + numéros
|
|
const clone = document.getElementById('labelCard').cloneNode(true);
|
|
clone.removeAttribute('id');
|
|
clone.classList.add('recap');
|
|
clone.querySelectorAll('.hotspot').forEach(h => {
|
|
h.classList.remove('hs-wrong');
|
|
h.classList.add('hs-correct');
|
|
const idx = etiZones.findIndex(z => z.zone === h.dataset.zone);
|
|
if (idx >= 0) {
|
|
const num = document.createElement('b');
|
|
num.className = 'hs-num';
|
|
num.textContent = idx + 1;
|
|
h.insertBefore(num, h.firstChild);
|
|
}
|
|
});
|
|
stage.appendChild(clone);
|
|
|
|
// Table check-list
|
|
const tbl = document.getElementById('checklist-table');
|
|
let rows = '<thead><tr><th style="width:34px;">N°</th><th>Élément d\'étiquetage</th><th>Base réglementaire</th><th style="width:70px;">Statut</th></tr></thead><tbody>';
|
|
etiZones.forEach((z, i) => {
|
|
const ok = etiFirstTry[z.zone];
|
|
const answered = etiAnswers[z.zone] !== undefined;
|
|
const statut = ok
|
|
? '<span class="cl-check">✓ trouvé</span>'
|
|
: (answered ? '<span class="cl-cross">✗ à revoir</span>' : '<span class="cl-cross">—</span>');
|
|
rows += `<tr>
|
|
<td class="cl-num"><span class="cl-badge">${i + 1}</span></td>
|
|
<td><strong>${z.label}</strong></td>
|
|
<td>${z.reg}</td>
|
|
<td>${statut}</td>
|
|
</tr>`;
|
|
});
|
|
rows += '</tbody>';
|
|
tbl.innerHTML = rows;
|
|
|
|
document.getElementById('eti-exercise').style.display = 'none';
|
|
document.getElementById('eti-recap').style.display = 'flex';
|
|
etiRecapShown = true;
|
|
renderBarActions();
|
|
|
|
// Animation : l'étiquette glisse à gauche, le tableau apparaît à sa droite
|
|
const rl = document.getElementById('recap-layout');
|
|
if (rl) { rl.classList.remove('animate'); void rl.offsetWidth; rl.classList.add('animate'); }
|
|
|
|
const sa = document.querySelector('#slide-7 .scroll-area');
|
|
if (sa) sa.scrollTop = 0;
|
|
}
|
|
|
|
function hideEtiRecap() {
|
|
document.getElementById('eti-recap').style.display = 'none';
|
|
document.getElementById('eti-exercise').style.display = 'flex';
|
|
etiRecapShown = false;
|
|
renderBarActions();
|
|
const sa = document.querySelector('#slide-7 .scroll-area');
|
|
if (sa) sa.scrollTop = 0;
|
|
}
|
|
|
|
/* ---- FT : exercice « relier » ---- */
|
|
const ftConnections = {}; // { qId: valeur de la section reliée }
|
|
let ftDrag = null; // { qid } pendant un glissement
|
|
let ftVerified = false;
|
|
const SVGNS = 'http://www.w3.org/2000/svg';
|
|
const FT_COLORS = { line: '#D85A30', temp: '#993C1D', ok: '#2e7d32', ko: '#c62828' };
|
|
|
|
function qById(id) { return ftQuestions.find(q => String(q.id) === String(id)); }
|
|
|
|
function buildFtMatch() {
|
|
Object.keys(ftConnections).forEach(k => delete ftConnections[k]);
|
|
ftDrag = null; ftVerified = false;
|
|
|
|
const leftC = document.getElementById('ft-left');
|
|
const rightC = document.getElementById('ft-right');
|
|
leftC.innerHTML = '';
|
|
rightC.innerHTML = '';
|
|
|
|
ftQuestions.forEach(q => {
|
|
const item = document.createElement('div');
|
|
item.className = 'match-item';
|
|
item.dataset.qid = q.id;
|
|
item.innerHTML = `<span>${q.text}</span><div class="match-dot" data-side="left" data-qid="${q.id}"></div>`;
|
|
leftC.appendChild(item);
|
|
});
|
|
|
|
const sections = shuffleArray([...new Set(ftQuestions.map(q => q.answer))]);
|
|
sections.forEach(sec => {
|
|
const item = document.createElement('div');
|
|
item.className = 'match-item';
|
|
item.dataset.answer = sec;
|
|
item.innerHTML = `<div class="match-dot" data-side="right" data-answer="${sec}"></div><span>${sec}</span>`;
|
|
rightC.appendChild(item);
|
|
});
|
|
|
|
initFtMatch();
|
|
document.getElementById('ft-svg').innerHTML = '';
|
|
refreshFtState();
|
|
}
|
|
|
|
function resetFT() {
|
|
const r = document.getElementById('ft-result');
|
|
r.className = 'result-message'; r.innerHTML = '';
|
|
buildFtMatch();
|
|
}
|
|
|
|
function ftSvgRect() { return document.getElementById('ft-svg').getBoundingClientRect(); }
|
|
function ftPointerPos(e) { const r = ftSvgRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; }
|
|
function ftDotCenter(dot) {
|
|
const r = dot.getBoundingClientRect(); const s = ftSvgRect();
|
|
return { x: r.left + r.width / 2 - s.left, y: r.top + r.height / 2 - s.top };
|
|
}
|
|
|
|
function ftDrawLine(svg, a, b, color, dashed) {
|
|
const line = document.createElementNS(SVGNS, 'line');
|
|
line.setAttribute('x1', a.x); line.setAttribute('y1', a.y);
|
|
line.setAttribute('x2', b.x); line.setAttribute('y2', b.y);
|
|
line.setAttribute('stroke', color);
|
|
line.setAttribute('stroke-width', '3');
|
|
line.setAttribute('stroke-linecap', 'round');
|
|
if (dashed) line.setAttribute('stroke-dasharray', '6 5');
|
|
svg.appendChild(line);
|
|
}
|
|
|
|
function drawFtLines(tempEnd) {
|
|
const svg = document.getElementById('ft-svg');
|
|
if (!svg) return;
|
|
svg.innerHTML = '';
|
|
Object.entries(ftConnections).forEach(([qid, ans]) => {
|
|
const from = document.querySelector('.match-dot[data-side="left"][data-qid="' + qid + '"]');
|
|
const to = document.querySelector('.match-dot[data-side="right"][data-answer="' + CSS.escape(ans) + '"]');
|
|
if (!from || !to) return;
|
|
const color = ftVerified ? (ans === qById(qid).answer ? FT_COLORS.ok : FT_COLORS.ko) : FT_COLORS.line;
|
|
ftDrawLine(svg, ftDotCenter(from), ftDotCenter(to), color);
|
|
});
|
|
if (ftDrag && tempEnd) {
|
|
const from = document.querySelector('.match-dot[data-side="left"][data-qid="' + ftDrag.qid + '"]');
|
|
if (from) ftDrawLine(svg, ftDotCenter(from), tempEnd, FT_COLORS.temp, true);
|
|
}
|
|
}
|
|
|
|
function refreshFtState() {
|
|
ftQuestions.forEach(q => {
|
|
const dot = document.querySelector('.match-dot[data-side="left"][data-qid="' + q.id + '"]');
|
|
if (!dot) return;
|
|
const has = ftConnections[q.id] != null;
|
|
dot.classList.toggle('filled', has);
|
|
dot.closest('.match-item').classList.toggle('connected', has);
|
|
});
|
|
document.querySelectorAll('.match-dot[data-side="right"]').forEach(dot => {
|
|
const used = Object.values(ftConnections).includes(dot.dataset.answer);
|
|
dot.classList.toggle('filled', used);
|
|
dot.closest('.match-item').classList.toggle('connected', used);
|
|
});
|
|
const all = ftQuestions.every(q => ftConnections[q.id] != null);
|
|
const btn = document.getElementById('ft-next');
|
|
if (btn) btn.disabled = !all;
|
|
}
|
|
|
|
function initFtMatch() {
|
|
const wrap = document.getElementById('ft-match');
|
|
wrap.querySelectorAll('.match-dot[data-side="left"]').forEach(dot => {
|
|
dot.addEventListener('pointerdown', e => {
|
|
e.preventDefault();
|
|
const qid = dot.dataset.qid;
|
|
delete ftConnections[qid]; // repartir d'une question efface son lien
|
|
ftVerified = false;
|
|
document.getElementById('ft-result').className = 'result-message';
|
|
ftDrag = { qid };
|
|
dot.classList.add('active');
|
|
refreshFtState();
|
|
drawFtLines(ftPointerPos(e));
|
|
});
|
|
});
|
|
}
|
|
|
|
// Gestionnaires globaux (le glissement peut sortir de la zone)
|
|
document.addEventListener('pointermove', e => {
|
|
if (!ftDrag) return;
|
|
drawFtLines(ftPointerPos(e));
|
|
});
|
|
document.addEventListener('pointerup', e => {
|
|
if (!ftDrag) return;
|
|
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
const answerDot = target && target.closest('.match-dot[data-side="right"]');
|
|
if (answerDot) ftConnections[ftDrag.qid] = answerDot.dataset.answer;
|
|
document.querySelectorAll('.match-dot.active').forEach(d => d.classList.remove('active'));
|
|
ftDrag = null;
|
|
refreshFtState();
|
|
drawFtLines();
|
|
});
|
|
window.addEventListener('resize', () => { if (currentSlide === 5) drawFtLines(); });
|
|
|
|
/* ---- FDS : glisser-déposer ---- */
|
|
function buildFdsDnd() {
|
|
const tile = (v, l) => `<div class="dnd-tile" draggable="true" data-value="${v}" data-label="${l}">${l}</div>`;
|
|
let left = '', right = '';
|
|
for (let i = 1; i <= 8; i++) left += tile(String(i), 'Rubrique ' + i);
|
|
for (let i = 9; i <= 16; i++) right += tile(String(i), 'Rubrique ' + i);
|
|
right += tile('absent', 'Absent');
|
|
document.getElementById('fds-bank-left').innerHTML = left;
|
|
document.getElementById('fds-bank-right').innerHTML = right;
|
|
|
|
const fdsC = document.getElementById('fds-questions');
|
|
fdsC.innerHTML = '';
|
|
shuffleArray(fdsQuestions).forEach((q, i) => {
|
|
fdsC.innerHTML += `
|
|
<div class="answer-row">
|
|
<div class="question-num" style="background:var(--amber-light);color:var(--amber-dark);">Q${i + 1}</div>
|
|
<div class="question-text">${q.text}</div>
|
|
<div class="drop-slot" id="fds${q.id}" data-answer="${q.answer}" data-value=""></div>
|
|
</div>`;
|
|
});
|
|
|
|
initFdsDnd();
|
|
}
|
|
|
|
function resetFDS() {
|
|
const r = document.getElementById('fds-result');
|
|
r.className = 'result-message'; r.innerHTML = '';
|
|
buildFdsDnd();
|
|
}
|
|
|
|
function initFdsDnd() {
|
|
const slide = document.getElementById('slide-3');
|
|
const tiles = slide.querySelectorAll('.dnd-tile');
|
|
const slots = slide.querySelectorAll('.drop-slot');
|
|
let armed = null;
|
|
|
|
const resetResults = () => {
|
|
slide.querySelectorAll('.answer-label, .answer-icon').forEach(e => e.remove());
|
|
slide.querySelectorAll('.drop-slot').forEach(s => s.classList.remove('slot-correct', 'slot-wrong'));
|
|
const r = document.getElementById('fds-result');
|
|
r.className = 'result-message'; r.innerHTML = '';
|
|
};
|
|
const fill = (slot, v, l) => {
|
|
slot.dataset.value = v; slot.textContent = l; slot.classList.add('filled');
|
|
resetResults(); updateFdsGate();
|
|
};
|
|
const clear = (slot) => {
|
|
slot.dataset.value = ''; slot.textContent = ''; slot.classList.remove('filled');
|
|
resetResults(); updateFdsGate();
|
|
};
|
|
|
|
tiles.forEach(t => {
|
|
t.addEventListener('dragstart', e => {
|
|
e.dataTransfer.setData('text/plain', JSON.stringify({ v: t.dataset.value, l: t.dataset.label }));
|
|
t.classList.add('dragging');
|
|
});
|
|
t.addEventListener('dragend', () => t.classList.remove('dragging'));
|
|
t.addEventListener('click', () => {
|
|
if (armed === t) { t.classList.remove('armed'); armed = null; return; }
|
|
if (armed) armed.classList.remove('armed');
|
|
armed = t; t.classList.add('armed');
|
|
});
|
|
});
|
|
|
|
slots.forEach(s => {
|
|
s.addEventListener('dragover', e => { e.preventDefault(); s.classList.add('drag-over'); });
|
|
s.addEventListener('dragleave', () => s.classList.remove('drag-over'));
|
|
s.addEventListener('drop', e => {
|
|
e.preventDefault(); s.classList.remove('drag-over');
|
|
try { const d = JSON.parse(e.dataTransfer.getData('text/plain')); fill(s, d.v, d.l); } catch (_) {}
|
|
});
|
|
s.addEventListener('click', () => {
|
|
if (armed) { fill(s, armed.dataset.value, armed.dataset.label); armed.classList.remove('armed'); armed = null; }
|
|
else if (s.classList.contains('filled')) { clear(s); }
|
|
});
|
|
});
|
|
|
|
updateFdsGate();
|
|
}
|
|
|
|
function updateFdsGate() {
|
|
const slots = document.querySelectorAll('#slide-3 .drop-slot');
|
|
const all = Array.from(slots).every(s => (s.dataset.value || '') !== '');
|
|
const btn = document.getElementById('fds-next');
|
|
if (btn) btn.disabled = !all;
|
|
}
|
|
|
|
function wireVerifyGate(containerId, btnId) {
|
|
const container = document.getElementById(containerId);
|
|
const btn = document.getElementById(btnId);
|
|
if (!container || !btn) return;
|
|
const selects = Array.from(container.querySelectorAll('select'));
|
|
const update = () => { btn.disabled = !selects.every(s => s.value !== ''); };
|
|
selects.forEach(s => s.addEventListener('change', update));
|
|
update();
|
|
}
|
|
|
|
/* ======================== VÉRIFICATION ======================== */
|
|
function showScore(resultDiv, correct, total) {
|
|
const p = Math.round((correct / total) * 100);
|
|
if (p === 100) {
|
|
resultDiv.className = 'result-message success show';
|
|
resultDiv.innerHTML = `<strong>🎉 Excellent !</strong> ${correct}/${total} bonnes réponses (${p}%)`;
|
|
} else if (p >= 80) {
|
|
resultDiv.className = 'result-message success show';
|
|
resultDiv.innerHTML = `<strong>✓ Très bien !</strong> ${correct}/${total} bonnes réponses (${p}%)`;
|
|
} else if (p >= 60) {
|
|
resultDiv.className = 'result-message error show';
|
|
resultDiv.innerHTML = `<strong>⚠️ À revoir</strong> — ${correct}/${total} bonnes réponses (${p}%)`;
|
|
} else {
|
|
resultDiv.className = 'result-message error show';
|
|
resultDiv.innerHTML = `<strong>❌ Insuffisant</strong> — ${correct}/${total} bonnes réponses (${p}%)`;
|
|
}
|
|
}
|
|
|
|
function verifyFDS() {
|
|
let correct = 0;
|
|
fdsQuestions.forEach(q => {
|
|
const slot = document.getElementById('fds' + q.id);
|
|
const userAnswer = slot.dataset.value || '';
|
|
const expected = q.answer.split(',');
|
|
const isCorrect = expected.includes(userAnswer);
|
|
const labelText = expected.length > 1
|
|
? 'Rubriques ' + expected.join(' et ')
|
|
: (expected[0] === 'absent' ? 'Absent' : 'Rubrique ' + expected[0]);
|
|
if (isCorrect) correct++;
|
|
showSlotResult(slot, isCorrect, userAnswer === '', labelText);
|
|
});
|
|
showScore(document.getElementById('fds-result'), correct, fdsQuestions.length);
|
|
}
|
|
|
|
function showSlotResult(slot, isCorrect, isUnanswered, correctAnswerText) {
|
|
const row = slot.closest('.answer-row');
|
|
row.querySelector('.answer-label')?.remove();
|
|
row.querySelector('.answer-icon')?.remove();
|
|
slot.classList.remove('slot-correct', 'slot-wrong');
|
|
|
|
const label = document.createElement('div');
|
|
label.className = 'answer-label';
|
|
label.textContent = correctAnswerText;
|
|
slot.before(label);
|
|
|
|
const icon = document.createElement('div');
|
|
icon.className = 'answer-icon';
|
|
icon.textContent = isUnanswered ? '❓' : (isCorrect ? '✅' : '❌');
|
|
slot.before(icon);
|
|
|
|
if (!isUnanswered) slot.classList.add(isCorrect ? 'slot-correct' : 'slot-wrong');
|
|
}
|
|
|
|
function verifyFT() {
|
|
ftVerified = true;
|
|
let correct = 0;
|
|
ftQuestions.forEach(q => {
|
|
if (ftConnections[q.id] === q.answer) correct++;
|
|
});
|
|
drawFtLines();
|
|
showScore(document.getElementById('ft-result'), correct, ftQuestions.length);
|
|
}
|
|
|
|
/* ======================== EXPORT PDF ======================== */
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
function idVal(id) {
|
|
const el = document.getElementById(id);
|
|
return el ? el.value.trim() : '';
|
|
}
|
|
|
|
// Libellé lisible d'une réponse FDS (numéro de rubrique) et de l'attendu
|
|
function fdsAnswerLabel(v) {
|
|
if (!v) return '(non répondu)';
|
|
if (v === 'absent') return 'Absent';
|
|
return 'Rubrique ' + v;
|
|
}
|
|
function fdsExpectedLabel(answer) {
|
|
const parts = answer.split(',');
|
|
if (parts.length > 1) return 'Rubrique ' + parts.join(' ou ');
|
|
return parts[0] === 'absent' ? 'Absent' : 'Rubrique ' + parts[0];
|
|
}
|
|
function textAnswerLabel(v) {
|
|
if (!v) return '(non répondu)';
|
|
return v === 'absent' ? 'Absent' : v;
|
|
}
|
|
|
|
function isFdsCorrect(q, v) { return v !== '' && q.answer.split(',').includes(v); }
|
|
function isTextCorrect(q, v) { return v !== '' && v === q.answer; }
|
|
|
|
function exporterPDF() {
|
|
const groupe = idVal('idGroupe');
|
|
const date = idVal('idDate') || new Date().toISOString().slice(0, 10);
|
|
const formateur = idVal('idFormateur');
|
|
|
|
let scoreTotal = 0, nbTotal = 0;
|
|
|
|
function partHtml(prefix, title, questions, selPrefix, answerLabelFn, expectedLabelFn, correctFn, valueFn) {
|
|
const rows = questions.map((q, i) => {
|
|
let v;
|
|
if (valueFn) { v = valueFn(q); }
|
|
else { const sel = document.getElementById(selPrefix + q.id); v = sel ? sel.value : ''; }
|
|
const ok = correctFn(q, v);
|
|
nbTotal++; if (ok) scoreTotal++;
|
|
const mark = v === '' ? '❓' : (ok ? '✓' : '✗');
|
|
return `<div style="margin-bottom:10px;">
|
|
<div><strong>[${prefix}-Q${i + 1}]</strong> ${escapeHtml(q.text)}</div>
|
|
<div style="margin-top:2px;">Réponse du groupe : <strong>${escapeHtml(answerLabelFn(v))}</strong> — Attendu : ${escapeHtml(expectedLabelFn(q.answer))} — ${mark}</div>
|
|
</div>`;
|
|
}).join('');
|
|
return `<h2>${title}</h2>${rows}`;
|
|
}
|
|
|
|
const p1 = partHtml('P1', 'Partie 1 — FDS (Fiche de Données de Sécurité)', fdsQuestions, 'fds', fdsAnswerLabel, fdsExpectedLabel, isFdsCorrect);
|
|
const p2 = partHtml('P2', 'Partie 2 — FT (Fiche Technique)', ftQuestions, 'ft', textAnswerLabel, a => a, isTextCorrect, q => ftConnections[q.id] || '');
|
|
|
|
// Partie 3 — Étiquette (zones cliquables) : ordre check-list
|
|
const p3rows = etiZones.map((z, i) => {
|
|
const given = etiAnswers[z.zone];
|
|
const ok = given === z.zone;
|
|
nbTotal++; if (ok) scoreTotal++;
|
|
const givenLabel = given === undefined ? '(non répondu)' : (etiZoneById(given) ? etiZoneById(given).label : given);
|
|
const mark = given === undefined ? '❓' : (ok ? '✓' : '✗');
|
|
return `<div style="margin-bottom:10px;">
|
|
<div><strong>[P3-Q${i + 1}]</strong> ${escapeHtml(z.text)}</div>
|
|
<div style="margin-top:2px;">Zone désignée : <strong>${escapeHtml(givenLabel)}</strong> — Attendu : ${escapeHtml(z.label)} — ${mark}</div>
|
|
</div>`;
|
|
}).join('');
|
|
const p3 = `<h2>Partie 3 — Étiquette (zones réglementaires)</h2>${p3rows}`;
|
|
|
|
const w = window.open('', '_blank');
|
|
w.document.write(`
|
|
<html><head><meta charset="utf-8"><title>FDS, FT & Étiquettes — Réponses (${escapeHtml(groupe) || 'sans nom'})</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; padding: 30px; color: #1a1a18; }
|
|
h1 { font-size: 20px; border-bottom: 2px solid #BA7517; padding-bottom: 8px; }
|
|
h2 { font-size: 15px; margin-top: 26px; color: #633806; }
|
|
.meta { font-size: 13px; color: #555; margin: 2px 0; }
|
|
</style></head>
|
|
<body>
|
|
<h1>TD — FDS, FT & Étiquettes</h1>
|
|
<p class="meta"><strong>Groupe / Stagiaire :</strong> ${escapeHtml(groupe) || '—'}</p>
|
|
<p class="meta"><strong>Date de session :</strong> ${escapeHtml(date)}</p>
|
|
<p class="meta"><strong>Formateur :</strong> ${escapeHtml(formateur) || '—'}</p>
|
|
<p class="meta"><strong>Score global :</strong> ${scoreTotal} / ${nbTotal}</p>
|
|
${p1}${p2}${p3}
|
|
</body></html>`);
|
|
w.document.close();
|
|
setTimeout(() => w.print(), 300);
|
|
}
|
|
|
|
/* ======================== SLIDE 1 — IDENTIFICATION ======================== */
|
|
function selectFormateur(btn) {
|
|
document.querySelectorAll('.formateur-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.getElementById('idFormateur').value = btn.dataset.formateur;
|
|
}
|
|
function clearGroupeError() {
|
|
const g = document.getElementById('idGroupe');
|
|
g.style.borderColor = '';
|
|
document.getElementById('idGroupe-error').hidden = true;
|
|
}
|
|
function startPart1() {
|
|
const g = document.getElementById('idGroupe');
|
|
if (!g.value.trim()) {
|
|
g.style.borderColor = 'var(--coral-mid)';
|
|
document.getElementById('idGroupe-error').hidden = false;
|
|
g.focus();
|
|
return;
|
|
}
|
|
goTo(2);
|
|
}
|
|
|
|
/* ======================== NAVIGATION ======================== */
|
|
function goTo(n) {
|
|
const cur = document.getElementById('slide-' + currentSlide);
|
|
const next = document.getElementById('slide-' + n);
|
|
if (!next) return;
|
|
history.push(currentSlide);
|
|
cur.classList.remove('active', 'fade-in');
|
|
cur.style.display = 'none';
|
|
currentSlide = n;
|
|
next.style.display = 'flex';
|
|
setTimeout(() => next.classList.add('active', 'fade-in'), 10);
|
|
const sa = next.querySelector('.scroll-area');
|
|
if (sa) sa.scrollTop = 0;
|
|
updateUI();
|
|
if (n === 8) setTimeout(launchConfetti, 200);
|
|
}
|
|
|
|
function goBack() {
|
|
if (history.length === 0) return;
|
|
const prev = history.pop();
|
|
const cur = document.getElementById('slide-' + currentSlide);
|
|
const target = document.getElementById('slide-' + prev);
|
|
cur.classList.remove('active', 'fade-in');
|
|
cur.style.display = 'none';
|
|
currentSlide = prev;
|
|
target.style.display = 'flex';
|
|
setTimeout(() => target.classList.add('active', 'fade-in'), 10);
|
|
updateUI();
|
|
}
|
|
|
|
/* Boutons centralisés dans la bottom bar, un jeu par slide */
|
|
function barBtn(bar, html, cls, fn, opts) {
|
|
opts = opts || {};
|
|
const b = document.createElement('button');
|
|
b.className = 'footer-btn ' + cls;
|
|
b.innerHTML = html;
|
|
if (opts.id) b.id = opts.id;
|
|
if (opts.disabled) b.disabled = true;
|
|
b.addEventListener('click', fn);
|
|
bar.appendChild(b);
|
|
return b;
|
|
}
|
|
|
|
function renderBarActions() {
|
|
const bar = document.getElementById('barActions');
|
|
if (!bar) return;
|
|
bar.innerHTML = '';
|
|
// Bouton retour intégré au groupe centré (sauf sur le récapitulatif, qui a son propre retour)
|
|
if (history.length > 0 && !(currentSlide === 7 && etiRecapShown)) {
|
|
barBtn(bar, '← Retour', 'is-secondary', goBack);
|
|
}
|
|
switch (currentSlide) {
|
|
case 1:
|
|
barBtn(bar, 'Commencer la partie 1 →', 'is-primary', startPart1);
|
|
break;
|
|
case 2:
|
|
barBtn(bar, 'Répondre aux questions →', 'is-primary', () => goTo(3));
|
|
break;
|
|
case 3:
|
|
barBtn(bar, '↺ Recommencer', 'is-secondary', resetFDS);
|
|
barBtn(bar, 'Passer à la partie 2 →', 'is-primary', () => goTo(4), { id: 'fds-next' });
|
|
updateFdsGate();
|
|
break;
|
|
case 4:
|
|
barBtn(bar, 'Répondre aux questions →', 'is-primary', () => goTo(5));
|
|
break;
|
|
case 5:
|
|
barBtn(bar, '↺ Recommencer', 'is-secondary', resetFT);
|
|
barBtn(bar, 'Passer à la partie 3 →', 'is-primary', () => goTo(6), { id: 'ft-next' });
|
|
refreshFtState();
|
|
break;
|
|
case 6:
|
|
barBtn(bar, 'Répondre aux questions →', 'is-primary', () => goTo(7));
|
|
break;
|
|
case 7:
|
|
if (etiRecapShown) {
|
|
barBtn(bar, '← Retour à l\'exercice', 'is-secondary', hideEtiRecap);
|
|
barBtn(bar, 'Voir la synthèse générale →', 'is-primary', () => goTo(8));
|
|
} else {
|
|
barBtn(bar, '↺ Recommencer', 'is-secondary', resetEtiquette);
|
|
barBtn(bar, 'Terminer l\'exercice →', 'is-primary', showEtiRecap,
|
|
{ id: 'eti-recap-btn', disabled: etiPos < etiOrder.length });
|
|
}
|
|
break;
|
|
case 8:
|
|
// Boutons placés sous le tableau comparatif, dans la slide (pas dans la bottom bar)
|
|
break;
|
|
}
|
|
}
|
|
|
|
function updateUI() {
|
|
document.getElementById('progressFill').style.width =
|
|
currentSlide === 0 ? '0%' : ((currentSlide / totalSlides) * 100) + '%';
|
|
document.getElementById('slideCounter').textContent =
|
|
currentSlide === 0 ? '' : currentSlide + ' / ' + totalSlides;
|
|
document.getElementById('bottomBar').style.display = currentSlide === 0 ? 'none' : 'flex';
|
|
renderBarActions();
|
|
}
|
|
|
|
function restartAll() {
|
|
history = [];
|
|
document.querySelectorAll('.slide').forEach(s => { s.classList.remove('active', 'fade-in'); s.style.display = 'none'; });
|
|
document.querySelectorAll('.result-message').forEach(r => { r.className = 'result-message'; });
|
|
generateQuizzes();
|
|
currentSlide = 0;
|
|
const first = document.getElementById('slide-0');
|
|
first.style.display = 'flex';
|
|
setTimeout(() => first.classList.add('active', 'fade-in'), 10);
|
|
updateUI();
|
|
}
|
|
|
|
/* ======================== CONFETTIS ======================== */
|
|
function launchConfetti() {
|
|
const canvas = document.getElementById('confettiCanvas');
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
canvas.style.display = 'block';
|
|
|
|
const colors = ['#BA7517', '#D85A30', '#F5DFB8', '#633806', '#F0A855', '#E8C068', '#FF8C5A'];
|
|
const particles = Array.from({ length: 180 }, () => ({
|
|
x: Math.random() * canvas.width,
|
|
y: -(Math.random() * canvas.height * 0.5),
|
|
w: Math.random() * 10 + 5,
|
|
h: Math.random() * 5 + 3,
|
|
color: colors[Math.floor(Math.random() * colors.length)],
|
|
rotation: Math.random() * Math.PI * 2,
|
|
rotSpeed: (Math.random() - 0.5) * 0.18,
|
|
vx: (Math.random() - 0.5) * 2.5,
|
|
vy: Math.random() * 3 + 2,
|
|
opacity: 1,
|
|
shape: Math.random() < 0.4 ? 'circle' : 'rect'
|
|
}));
|
|
|
|
let rafId;
|
|
let start = null;
|
|
const totalDuration = 3800;
|
|
|
|
function draw(ts) {
|
|
if (!start) start = ts;
|
|
const elapsed = ts - start;
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
let active = false;
|
|
|
|
particles.forEach(p => {
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
p.vy += 0.04;
|
|
p.rotation += p.rotSpeed;
|
|
p.opacity = elapsed < totalDuration * 0.55
|
|
? 1
|
|
: Math.max(0, 1 - (elapsed - totalDuration * 0.55) / (totalDuration * 0.45));
|
|
|
|
if (p.y < canvas.height + 20) active = true;
|
|
|
|
ctx.save();
|
|
ctx.translate(p.x, p.y);
|
|
ctx.rotate(p.rotation);
|
|
ctx.globalAlpha = p.opacity;
|
|
ctx.fillStyle = p.color;
|
|
if (p.shape === 'circle') {
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, p.w / 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
} else {
|
|
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
|
}
|
|
ctx.restore();
|
|
});
|
|
|
|
if (active && elapsed < totalDuration * 1.3) {
|
|
rafId = requestAnimationFrame(draw);
|
|
} else {
|
|
canvas.style.display = 'none';
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
}
|
|
|
|
if (rafId) cancelAnimationFrame(rafId);
|
|
rafId = requestAnimationFrame(draw);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
generateQuizzes();
|
|
updateUI();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|