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