Files
Site_web/exercices/Exo_FDS.html
gauthier.chombart 21a7359057 Retravaille l'exercice FDS : étiquette libre et corrigé déplacé
- Slide 5 (FT) : reformule la question sur le type de produit en « Où se
  trouve la liste des TP (type de produit) ? »
- Slide 7 : l'utilisateur peut désormais cliquer n'importe quelle zone,
  même incorrecte ; le clic est enregistré et l'exercice avance.
- Slide 7 : « Terminer l'exercice » mène directement à la synthèse ; le
  récapitulatif « l'étiquette annotée » quitte le déroulé des slides.
- Déplace ce récapitulatif (étiquette annotée + check-list réglementaire)
  vers corriger-exercice.html, en corrigé de référence du rendu FDS.
2026-07-21 08:01:12 +02:00

1725 lines
68 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; }
@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;
}
</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. La <strong>justification réglementaire</strong> de chaque zone est fournie sur la page de correction de l'exercice.</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>
</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: "Où se trouve la liste des TP (type de 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 etiInit = false; // écouteur de clic posé une seule fois
function buildEtiquette() {
etiOrder = shuffleArray(etiZones.map((_, i) => i));
etiPos = 0;
etiAnswers = {};
etiFirstTry = {};
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 = '';
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 (la correction est sur la page de correction).
if (etiPos >= etiOrder.length) return;
marquerModifie();
const target = etiZones[etiOrder[etiPos]];
// La zone cliquée — bonne ou mauvaise — est enregistrée comme réponse, puis on passe
// à la question suivante. Aucun retour de justesse n'est affiché ici : la correction
// (étiquette annotée + check-list) est présentée sur la page de correction de l'exercice.
etiAnswers[target.zone] = clicked;
etiFirstTry[target.zone] = (clicked === target.zone);
etiPos++;
renderEtiQuestion();
if (etiPos >= etiOrder.length) finishEtiquette();
}
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(); }
/* ---- 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) { marquerModifie(); 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) => {
marquerModifie();
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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 &amp; É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é
if (history.length > 0) {
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:
barBtn(bar, '↺ Recommencer', 'is-secondary', resetEtiquette);
barBtn(bar, 'Terminer l\'exercice →', 'is-primary', () => goTo(8),
{ 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);
}
/* ======================== PROTECTION CONTRE LA PERTE DES RÉPONSES ======================== */
/* Les réponses ne sont pas enregistrées : on avertit avant tout rechargement ou départ.
Le texte de la boîte est imposé par le navigateur, il n'est pas personnalisable. */
let reponsesModifiees = false;
function marquerModifie() { reponsesModifiees = true; }
// Les champs texte déclenchent input/change ; les parties FDS (glisser-déposer),
// FT (liaisons) et Étiquette (zones cliquables) appellent marquerModifie() directement.
document.addEventListener('input', marquerModifie, true);
document.addEventListener('change', marquerModifie, true);
window.addEventListener('beforeunload', e => {
if (!reponsesModifiees) return;
e.preventDefault();
e.returnValue = '';
});
document.addEventListener('DOMContentLoaded', () => {
generateQuizzes();
updateUI();
});
</script>
</body>
</html>