Files
Site_web/exercices/Exo_FDS.html
gauthier.chombart d351ee6efb Refonte de la slide 7 FDS en étiquette cliquable (hotspots)
Remplace les menus déroulants de la partie Étiquette par une étiquette
DÉS'INFECT reconstituée (Annexe I du guide FHER) avec 9 zones cliquables :
info-bulle de justification réglementaire au clic, états visuels
correct/incorrect, mise en situation professionnelle en introduction,
et écran de synthèse sur le modèle de la check-list FHER p.15
(étiquette annotée + tableau). Export PDF recâblé sur les nouvelles zones.
2026-07-16 14:36:09 +02:00

1648 lines
68 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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(52px + 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 */
.bottom-bar {
position: fixed;
bottom: 0; left: 0; right: 0;
height: 52px;
background: rgba(250, 238, 218, 0.98);
border-top: 2px solid var(--amber);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 100;
backdrop-filter: blur(10px);
}
.slide-counter { font-size: 12px; font-family: 'Syne', sans-serif; font-weight: 600; color: var(--amber-dark); }
.back-btn {
background: none;
border: 0.5px solid var(--amber);
border-radius: 20px;
padding: 6px 16px;
font-size: 12px;
color: var(--amber-dark);
cursor: pointer;
transition: all 0.2s;
font-family: 'DM Sans', sans-serif;
}
.back-btn:hover { background: rgba(186,117,23,0.08); }
/* 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(52px + 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;
padding: 8px 10px; min-width: 66px; 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(60px, 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; }
.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; }
}
</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>
<!-- Phase badge -->
<div class="phase-badge" id="phaseBadge" style="display:none"></div>
<!-- Bottom bar -->
<div class="bottom-bar" id="bottomBar" style="display:none;">
<button class="back-btn" id="backBtn" onclick="goBack()" style="display:none;">← Retour</button>
<div class="slide-counter" id="slideCounter"></div>
</div>
<!-- ============ 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(--coral-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="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="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="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">
<input type="date" id="idDate">
<input type="text" id="idFormateur" placeholder="Formateur (facultatif)" autocomplete="off">
</div>
</div>
<button class="btn btn-amber" onclick="goTo(2)" style="margin-top:1.5rem;">Commencer la partie 1 →</button>
</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>
<button class="btn btn-amber" onclick="goTo(3)" style="margin-top:1.5rem;">Répondre aux questions →</button>
</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>
<button class="verify-btn btn-amber" id="fds-verify" onclick="verifyFDS()" disabled>✓ Vérifier mes réponses</button>
<div id="fds-result" class="result-message"></div>
<div class="actions-row">
<button class="btn btn-outline" onclick="resetFDS()">↺ Recommencer</button>
<button class="btn btn-coral" onclick="goTo(4)">Passer à la partie 2 →</button>
</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>
<button class="btn btn-coral" onclick="goTo(5)" style="margin-top:1.5rem;">Répondre aux questions →</button>
</div>
<!-- ============ SLIDE 5 — FT : RELIER ============ -->
<div class="slide" id="slide-5">
<div class="scroll-area">
<div class="section-tag" style="background:var(--coral-light);color:var(--coral);">Partie 2 / 3 — FT</div>
<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>
<button class="verify-btn btn-coral" id="ft-verify" onclick="verifyFT()" disabled>✓ Vérifier mes réponses</button>
<div id="ft-result" class="result-message"></div>
<div class="actions-row">
<button class="btn btn-outline" onclick="resetFT()">↺ Recommencer</button>
<button class="btn btn-teal" onclick="goTo(6)">Passer à la partie 3 →</button>
</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>
<button class="btn btn-teal" onclick="goTo(7)" style="margin-top:1.5rem;">Répondre aux questions →</button>
</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="section-tag" style="background:var(--teal-light);color:var(--teal-dark);">Partie 3 / 3 — Étiquette</div>
<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">
<span class="ghs"><span>🐟</span></span>
<span class="ghs"><span>🧪</span></span>
<span class="ghs excl"><span>!</span></span>
</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 class="eti-pop" id="eti-pop" hidden>
<button class="pop-close" onclick="closeEtiPop()" aria-label="Fermer">×</button>
<div class="pop-status" id="pop-status"></div>
<div class="pop-title" id="pop-title"></div>
<div class="pop-reg" id="pop-reg"></div>
</div>
</div>
<div id="etiquette-result" class="result-message"></div>
<div class="actions-row">
<button class="btn btn-outline" onclick="resetEtiquette()">↺ Recommencer</button>
<button class="btn btn-teal" id="eti-recap-btn" onclick="showEtiRecap()" disabled>Voir le récapitulatif →</button>
</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="eti-stage" id="recap-stage"></div>
<table class="checklist-table" id="checklist-table"></table>
<div class="actions-row">
<button class="btn btn-outline" onclick="hideEtiRecap()">← Retour à l'exercice</button>
<button class="btn btn-amber" onclick="goTo(8)">Voir la synthèse générale →</button>
</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>
<ul class="tips-list">
<li><strong>FDS = Sécurité et dangers</strong> → à consulter AVANT manipulation.</li>
<li><strong>FT = Mode d'emploi et applications</strong> → à consulter pour l'utilisation.</li>
<li><strong>Étiquette = Interface rapide</strong> → lecture obligatoire avant ouverture du produit.</li>
<li>Les trois documents sont <strong>complémentaires</strong>, jamais redondants — chacun cible un besoin et un public différents.</li>
</ul>
<div class="actions-row">
<button class="btn btn-amber" onclick="exporterPDF()">⬇ Télécharger mes réponses (PDF)</button>
<a href="correction-fds.html" class="btn btn-outline">✔ Corriger un rendu</a>
</div>
<div class="actions-row" style="margin-top:0.5rem;">
<button class="btn btn-outline" onclick="restartAll()">↺ Recommencer</button>
<a href="../index.html#formations" class="btn btn-outline">Retour aux exercices</a>
</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
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 = '';
document.getElementById('eti-recap-btn').disabled = true;
document.getElementById('eti-exercise').style.display = 'flex';
document.getElementById('eti-recap').style.display = 'none';
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;
// Exploration libre une fois l'exercice terminé
if (etiPos >= etiOrder.length) {
openEtiPop(hs, 'info', clicked);
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;
openEtiPop(hs, 'ok', clicked);
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);
openEtiPop(hs, 'ko', clicked);
}
}
function finishEtiquette() {
const correct = etiZones.filter(z => etiFirstTry[z.zone]).length;
showScore(document.getElementById('etiquette-result'), correct, etiZones.length);
document.getElementById('eti-recap-btn').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 stage = document.getElementById('eti-stage');
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;
pop.hidden = false;
// Positionnement relatif à la scène
const sr = stage.getBoundingClientRect();
const hr = hs.getBoundingClientRect();
const popW = pop.offsetWidth, popH = pop.offsetHeight;
let left = hr.left - sr.left + hr.width / 2 - popW / 2;
left = Math.max(4, Math.min(left, sr.width - popW - 4));
let top = hr.bottom - sr.top + 8;
if (top + popH > sr.height) top = hr.top - sr.top - popH - 8; // bascule au-dessus si déborde
if (top < 0) top = 4;
pop.style.left = left + 'px';
pop.style.top = top + 'px';
}
function closeEtiPop() {
const pop = document.getElementById('eti-pop');
if (pop) pop.hidden = true;
}
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';
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';
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 = '';
document.getElementById('ft-verify').disabled = true;
}
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);
document.getElementById('ft-verify').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), 'R' + i);
for (let i = 9; i <= 16; i++) right += tile(String(i), 'R' + 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 || '') !== '');
document.getElementById('fds-verify').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);
}
/* ======================== 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();
}
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();
}
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';
document.getElementById('backBtn').style.display = history.length > 0 ? 'block' : 'none';
const badge = document.getElementById('phaseBadge');
const set = (bg, col, txt) => { badge.style.display = 'block'; badge.style.background = bg; badge.style.color = col; badge.textContent = txt; };
if (currentSlide === 2 || currentSlide === 3) set('var(--amber-light)', 'var(--amber-dark)', 'Partie 1 — FDS');
else if (currentSlide === 4 || currentSlide === 5) set('var(--coral-light)', 'var(--coral)', 'Partie 2 — FT');
else if (currentSlide === 6 || currentSlide === 7) set('var(--teal-light)', 'var(--teal-dark)', 'Partie 3 — Étiquette');
else badge.style.display = 'none';
}
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();
}
document.addEventListener('DOMContentLoaded', () => {
generateQuizzes();
updateUI();
});
</script>
</body>
</html>