mise à niveau de tout le site

This commit is contained in:
lrdlg
2026-09-21 10:56:04 +02:00
parent 5c644d17fc
commit a489298897
36 changed files with 3667 additions and 929 deletions
+304
View File
@@ -0,0 +1,304 @@
/* ==================================================================
Cours de traitement de texte (3e TQ, 4e TQ)
Chargé après assets/theme.css, dont il reprend toutes les couleurs.
Mise en page « document long » : un sommaire latéral qui suit la
lecture, et une feuille de papier au centre.
================================================================== */
/* ---------- gabarit à deux colonnes ---------- */
.wrap {
flex: 1;
width: 100%;
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
gap: 2rem;
max-width: 80rem;
margin: 0 auto;
padding: 1.5rem;
}
.sommaire-lat {
position: sticky;
top: 1.5rem;
align-self: start;
max-height: calc(100vh - 3rem);
overflow: auto;
}
.sommaire-lat h2 {
font-family: var(--police-texte);
font-size: .78rem;
font-weight: 700;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--encre-douce);
margin: 1.4rem 0 .4rem;
padding: 0;
border: 0;
}
.sommaire-lat h2:first-of-type { margin-top: 0; }
.sommaire-lat a {
display: block;
padding: .28rem .55rem;
color: var(--encre);
text-decoration: none;
border-left: 3px solid transparent;
font-size: .93rem;
}
.sommaire-lat a:hover { background: var(--accent-fond); }
.sommaire-lat a.on {
border-left-color: var(--accent);
background: var(--accent-fond);
font-weight: 600;
}
.sommaire-lat .top {
font-family: var(--police-titre);
font-weight: 700;
font-size: .98rem;
margin-bottom: .8rem;
color: var(--accent-encre);
border: 0;
}
/* ---------- la feuille ---------- */
.feuille {
background: var(--blanc);
border: 1px solid var(--trait);
border-radius: .3rem;
box-shadow: 0 12px 30px -20px rgba(22, 48, 43, .5);
}
/* Règle graduée, comme en haut de la fenêtre de Writer. */
.regle {
height: 20px;
border-bottom: 1px solid var(--trait);
background: var(--papier);
background-image: repeating-linear-gradient(to right, var(--trait) 0 1px, transparent 1px 56px);
background-position: 24px 0;
position: relative;
}
.regle::after {
content: "";
position: absolute;
left: 24px; right: 24px; bottom: 0;
height: 5px;
background: var(--accent);
opacity: .45;
}
.inner { padding: 2.4rem clamp(1.2rem, 4vw, 3.4rem) 3rem; max-width: 46rem; }
.module-tag {
display: inline-block;
font-family: var(--police-titre);
font-size: .75rem;
font-weight: 700;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--papier);
background: var(--accent-encre);
padding: .1rem .5rem;
border-radius: .2rem;
margin-bottom: .3rem;
}
/* ---------- blocs de contenu ---------- */
/* Alias historique de .encadre--cle, conservé pour le contenu existant. */
.regle-cle {
border-left: .35rem solid var(--cible);
background: var(--cible-fond);
border-radius: 0 .4rem .4rem 0;
padding: .9rem 1.1rem;
margin: 1.4rem 0;
max-width: var(--lecture);
}
.regle-cle p { margin: 0; }
.specimen {
font-family: var(--police-doc);
font-size: 1rem;
line-height: 1.75;
background: var(--papier);
border: 1px solid var(--trait);
border-radius: .3rem;
padding: 1rem 1.2rem;
}
.pil { color: var(--cible); font-weight: 700; }
details {
border-bottom: 1px solid var(--trait);
padding: .6rem 0;
max-width: var(--lecture);
}
summary { cursor: pointer; font-weight: 600; }
summary::marker { color: var(--accent); }
details[open] summary { margin-bottom: .6rem; }
/* ---------- boutons ---------- */
button {
font: inherit;
cursor: pointer;
border: 1px solid var(--accent-encre);
background: var(--blanc);
color: var(--accent-encre);
padding: .4rem .85rem;
border-radius: .3rem;
}
button:hover { background: var(--accent-fond); }
button.plein { background: var(--accent-encre); color: var(--papier); }
button.plein:hover { background: var(--encre); border-color: var(--encre); }
/* ---------- démonstration des marques de paragraphe ---------- */
.demo { border: 1px solid var(--trait); border-radius: .3rem; margin: 1.2rem 0; overflow: hidden; }
.demo-barre {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
background: var(--papier);
border-bottom: 1px solid var(--trait);
padding: .55rem .8rem;
font-size: .9rem;
}
.demo-corps { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
.demo-col { padding: .9rem 1rem; font-family: var(--police-doc); line-height: 1.7; overflow: hidden; }
.demo-col:first-child { border-right: 1px solid var(--trait); }
.demo-col h4 {
margin: 0 0 .5rem;
font-family: var(--police-texte);
font-size: .82rem;
color: var(--encre-douce);
}
.marque { color: var(--cible); font-weight: 700; }
.esp { color: var(--trait-fort); }
@media (max-width: 640px) {
.demo-corps { grid-template-columns: 1fr; }
.demo-col:first-child { border-right: 0; border-bottom: 1px solid var(--trait); }
}
/* ---------- zones cliquables de la fenêtre de Writer ---------- */
.zones { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0; }
.zones button { font-size: .9rem; }
.zones button[aria-pressed="true"] { background: var(--accent-encre); color: var(--papier); }
#zone-txt {
min-height: 3.2rem;
background: var(--accent-fond);
border-radius: .3rem;
padding: .7rem .9rem;
font-size: .95rem;
max-width: var(--lecture);
}
/* ---------- validateur de nom de fichier ---------- */
.nomtest input {
font: inherit;
font-family: ui-monospace, "JetBrains Mono", Consolas, monospace;
padding: .45rem .6rem;
border: 1px solid var(--trait-fort);
border-radius: .3rem;
width: min(28rem, 100%);
}
.check { list-style: none; padding: 0; margin: .8rem 0; font-size: .95rem; }
.check li { padding-left: 1.7rem; position: relative; margin-bottom: .3rem; }
.check li::before { position: absolute; left: 0; font-weight: 700; }
.check li.ok::before { content: "\2713"; color: var(--juste); }
.check li.ko::before { content: "\2715"; color: var(--erreur); }
/* ---------- chasse aux fautes ---------- */
.chasse {
font-family: var(--police-doc);
line-height: 2;
background: var(--papier);
border: 1px solid var(--trait);
border-radius: .3rem;
padding: 1rem 1.2rem;
}
.chasse span { cursor: pointer; padding: 0 .1rem; border-bottom: 2px solid transparent; }
.chasse span:hover { background: var(--accent-fond); }
.chasse span.trouve { background: var(--cible); border-bottom-color: #B8912A; }
.chasse span.rate { background: var(--erreur-fond); border-bottom-color: var(--erreur); }
.score { font-weight: 600; margin-top: .6rem; }
/* ---------- quiz ---------- */
.quiz {
border: 1px solid var(--trait);
border-radius: .4rem;
padding: 1.1rem 1.2rem;
margin: 1.2rem 0;
background: var(--blanc);
}
.q { margin-bottom: 1.3rem; }
.q p { font-weight: 600; margin-bottom: .5rem; }
.q label { display: block; padding: .32rem .5rem; cursor: pointer; border-left: 3px solid transparent; }
.q label:hover { background: var(--accent-fond); }
.q input { margin-right: .5rem; }
.q label.bon { background: var(--juste-fond); border-left-color: var(--juste); }
.q label.mauvais { background: var(--erreur-fond); border-left-color: var(--erreur); }
.verdict { margin-top: .5rem; font-size: .93rem; color: var(--encre-douce); }
/* ---------- chronomètre de frappe ---------- */
.chrono {
display: flex;
gap: 1.2rem;
align-items: center;
flex-wrap: wrap;
background: var(--papier);
border: 1px solid var(--trait);
border-radius: .3rem;
padding: .9rem 1rem;
}
.temps {
font-family: ui-monospace, "JetBrains Mono", Consolas, monospace;
font-size: 2rem;
font-weight: 700;
}
#zone-frappe {
width: 100%;
min-height: 9rem;
font-family: var(--police-doc);
font-size: 1rem;
line-height: 1.7;
padding: .8rem;
border: 1px solid var(--trait-fort);
border-radius: .3rem;
margin-top: .8rem;
}
/* ---------- filtre des raccourcis ---------- */
#filtre {
font: inherit;
padding: .45rem .6rem;
border: 1px solid var(--trait-fort);
border-radius: .3rem;
width: min(22rem, 100%);
}
/* ---------- petits écrans ---------- */
@media (max-width: 880px) {
.wrap { grid-template-columns: 1fr; padding: .8rem; }
.sommaire-lat {
position: static;
max-height: none;
background: var(--blanc);
padding: 1rem;
border: 1px solid var(--trait);
border-radius: .3rem;
}
.sommaire-lat a { display: inline-block; border-left: 0; border-bottom: 3px solid transparent; }
.sommaire-lat a.on { border-left: 0; border-bottom-color: var(--accent); }
}
@media print {
.sommaire-lat, .actions, .chrono, #zone-frappe { display: none; }
.feuille { box-shadow: none; border: 0; }
.wrap { display: block; padding: 0; }
}
+96
View File
@@ -0,0 +1,96 @@
/* ==================================================================
Outils communs aux cours de traitement de texte (3e TQ, 4e TQ)
------------------------------------------------------------------
Trois briques réutilisables d'un module à l'autre :
quiz(element, questions) questionnaire à choix, correction immédiate
chasse(...) chasse aux fautes dans un texte
suivreSommaire() surligne l'entrée courante du sommaire
Chaque page de module charge ce fichier, puis déclare ses propres
données dans un <script> qui lui est propre.
================================================================== */
"use strict";
/* ---------- questionnaire à choix multiples ----------
questions : [{q: énoncé, r: [réponses], b: index de la bonne, e: explication}] */
function quiz(el, questions){
if (!el) return;
el.innerHTML = questions.map(function(q, i){
return '<div class="q" data-i="' + i + '"><p>' + (i + 1) + '. ' + q.q + '</p>' +
q.r.map(function(r, j){
return '<label><input type="radio" name="' + el.id + '-' + i + '" value="' + j + '">' + r + '</label>';
}).join('') +
'<div class="verdict" role="status" aria-live="polite"></div></div>';
}).join('');
el.addEventListener('change', function(e){
var bloc = e.target.closest('.q');
var i = +bloc.dataset.i, q = questions[i];
bloc.querySelectorAll('label').forEach(function(l, j){
l.className = (j === q.b) ? 'bon' : (l.querySelector('input').checked ? 'mauvais' : '');
});
bloc.querySelector('.verdict').textContent =
(+e.target.value === q.b ? 'Exact. ' : 'Non. ') + q.e;
});
}
/* ---------- chasse aux fautes ----------
jetons : [{t: mot, f: true si c'est une faute}] */
function chasse(conteneur, scoreEl, resetBtn, solBtn, jetons, total, motFinal){
if (!conteneur) return;
var trouves = new Set(), rates = new Set();
function maj(){
conteneur.querySelectorAll('span').forEach(function(s){
var i = +s.dataset.i;
s.className = trouves.has(i) ? 'trouve' : (rates.has(i) ? 'rate' : '');
});
scoreEl.textContent = trouves.size + ' sur ' + total + ' trouvées' +
(rates.size ? ', ' + rates.size + ' clic' + (rates.size > 1 ? 's' : '') + ' à côté' : '') +
(trouves.size === total ? '. ' + motFinal : '');
}
conteneur.innerHTML = jetons.map(function(j, i){
return '<span data-i="' + i + '">' + j.t + '</span>';
}).join(' ');
conteneur.addEventListener('click', function(e){
var s = e.target.closest('span');
if (!s) return;
var i = +s.dataset.i;
if (jetons[i].f) trouves.add(i); else rates.add(i);
maj();
});
resetBtn.addEventListener('click', function(){
trouves = new Set(); rates = new Set(); maj();
});
solBtn.addEventListener('click', function(){
jetons.forEach(function(j, i){ if (j.f) trouves.add(i); });
rates = new Set(); maj();
});
maj();
}
/* ---------- surlignage de l'entrée courante du sommaire ---------- */
function suivreSommaire(){
var liens = [].slice.call(document.querySelectorAll('.sommaire-lat a[href^="#"]'));
var cibles = liens
.map(function(a){ return document.getElementById(a.getAttribute('href').slice(1)); })
.filter(Boolean);
if (!cibles.length || !window.IntersectionObserver) return;
var obs = new IntersectionObserver(function(entrees){
entrees.forEach(function(en){
if (!en.isIntersecting) return;
liens.forEach(function(a){
a.classList.toggle('on', a.getAttribute('href') === '#' + en.target.id);
});
});
}, { rootMargin: '-10% 0px -80% 0px' });
cibles.forEach(function(c){ obs.observe(c); });
}
window.addEventListener('DOMContentLoaded', suivreSommaire);
+184
View File
@@ -0,0 +1,184 @@
/* ==================================================================
Composants du moteur d'exercices de la 2e commune.
Chargé après assets/theme.css, dont il reprend toutes les couleurs.
================================================================== */
/* ---------- bandeau de score ---------- */
.score {
position: sticky;
top: 0;
z-index: 5;
background: var(--blanc);
border: 1px solid var(--trait);
border-radius: .5rem;
padding: .8rem 1.1rem 1rem;
margin-bottom: 1.2rem;
}
.score__txt { font-size: .95rem; }
.score strong { color: var(--accent-encre); }
.score .action { margin-top: .7rem; }
.jauge {
height: .7rem;
background: var(--accent-fond);
border-radius: .35rem;
overflow: hidden;
margin-top: .5rem;
}
.jauge span {
display: block;
height: 100%;
width: 0;
background: var(--juste);
transition: width .3s;
}
/* ---------- carte d'exercice ---------- */
.ex {
border: 1px solid var(--trait);
border-radius: .5rem;
padding: 1.1rem 1.2rem 1.2rem;
margin: 1.2rem 0;
background: var(--blanc);
}
.ex .consigne {
font-family: var(--police-titre);
font-weight: 700;
font-size: .95rem;
color: var(--accent-encre);
margin: 0 0 .3rem;
max-width: none;
display: flex;
flex-wrap: wrap;
gap: .3rem 1rem;
align-items: baseline;
justify-content: space-between;
}
.ex .enonce { margin: 0 0 .9rem; }
.ex h4 { margin: 0 0 .5rem; color: var(--encre-douce); font-size: .85rem; }
.minuteur {
font-family: var(--police-titre);
font-weight: 700;
color: var(--erreur);
}
/* ---------- boutons de réponse ---------- */
.opt, .jeton, .item {
font: inherit;
font-size: .97rem;
display: inline-block;
margin: .25rem .35rem .25rem 0;
padding: .5rem .8rem;
border: 1px solid var(--trait-fort);
border-radius: .4rem;
background: var(--blanc);
color: var(--encre);
cursor: pointer;
text-align: left;
}
.opt { display: block; width: 100%; }
.jeton--bloc { display: block; width: 100%; }
.jeton--mini { font-size: .8rem; padding: .2rem .55rem; margin-left: .4rem; }
.opt:hover:not([disabled]),
.jeton:hover:not([disabled]),
.item:hover:not([disabled]) { border-color: var(--encre); }
.opt.choisi, .jeton.choisi, .item.choisi {
background: var(--encre);
color: var(--papier);
border-color: var(--encre);
}
.opt.juste, .jeton.juste, .item.juste {
background: var(--juste-fond);
border-color: var(--juste);
}
.opt.faux, .jeton.faux, .item.faux {
background: var(--erreur-fond);
border-color: var(--erreur);
}
.opt[disabled], .jeton[disabled], .item[disabled] { cursor: default; }
/* ---------- tri en paniers ---------- */
.reserve { margin-bottom: .6rem; }
.paniers {
display: flex;
flex-wrap: wrap;
gap: .8rem;
margin-top: .8rem;
}
.panier {
flex: 1 1 11rem;
min-height: 6rem;
border: 2px dashed var(--trait-fort);
border-radius: .5rem;
padding: .6rem;
background: var(--papier);
cursor: pointer;
}
.panier:hover { border-color: var(--accent); }
.panier.vise { border-color: var(--accent); background: var(--accent-fond); }
.panier h4 {
margin: 0 0 .4rem;
font-family: var(--police-titre);
font-size: .85rem;
color: var(--accent-encre);
}
/* ---------- association en deux colonnes ---------- */
.deux-col { display: flex; gap: 1.2rem; flex-wrap: wrap; }
.deux-col > div { flex: 1 1 14rem; }
/* ---------- remise en ordre ---------- */
.liste-ordre { margin-bottom: .6rem; }
.item--bloc { display: block; width: 100%; }
/* ---------- saisie ---------- */
.court {
font: inherit;
padding: .5rem .6rem;
border: 1px solid var(--trait-fort);
border-radius: .4rem;
width: 11rem;
}
/* ---------- schéma à zones cliquables ---------- */
.schema { margin: .4rem 0 .8rem; }
.schema svg { max-width: 100%; height: auto; }
svg .zone {
fill: transparent;
stroke: var(--accent-encre);
stroke-width: 2;
cursor: pointer;
}
svg .zone:hover { fill: var(--accent-fond); fill-opacity: .55; }
svg .zone.juste { fill: var(--juste); fill-opacity: .3; stroke: var(--juste); }
svg .zone.faux { fill: var(--erreur); fill-opacity: .3; stroke: var(--erreur); }
svg text { font-family: inherit; font-size: 13px; fill: var(--encre); pointer-events: none; }
/* ---------- verdict ---------- */
.retour {
margin-top: .8rem;
font-size: .93rem;
padding: .55rem .7rem;
border-radius: .4rem;
display: none;
}
.retour--ok { display: block; background: var(--juste-fond); border: 1px solid var(--juste); }
.retour--ko { display: block; background: var(--erreur-fond); border: 1px solid var(--erreur); }
@media print {
.score, .ex .action, .panier { display: none; }
}
+557
View File
@@ -0,0 +1,557 @@
/* ==================================================================
Moteur d'exercices hors ligne — 2e commune
------------------------------------------------------------------
Aucune dépendance externe. Chaque page de bloc déclare ses exercices
dans window.DATA, un tableau d'objets. Types reconnus :
qcm, vf, assoc, tri, ordre, hotspot, calcul, texte, intrus, chrono
Règle de comptage, la même pour tous les types :
un exercice compte pour un point, une seule fois, au moment où il
est terminé, et il est réussi s'il a été terminé sans erreur.
Recommencer une série chronométrée ne réajoute donc pas de point.
================================================================== */
"use strict";
var TOTAL = 0, REUSSIS = 0, TERMINES = 0;
/* ---------- petits utilitaires ---------- */
function norm(s){
return (s || "").toString().toLowerCase()
.normalize("NFD").replace(/[̀-ͯ]/g, "")
.replace(/[^a-z0-9]/g, "");
}
function el(tag, cls, txt){
var e = document.createElement(tag);
if (cls) e.className = cls;
if (txt !== undefined) e.textContent = txt;
return e;
}
function melange(t){
var a = t.slice();
for (var i = a.length - 1; i > 0; i--){
var j = Math.floor(Math.random() * (i + 1));
var x = a[i]; a[i] = a[j]; a[j] = x;
}
return a;
}
/* ---------- score ---------- */
function majScore(){
var s = document.getElementById("score");
if (!s) return;
var pct = TOTAL ? Math.round(REUSSIS / TOTAL * 100) : 0;
s.querySelector(".score__txt").innerHTML =
"Exercices réussis : <strong>" + REUSSIS + " / " + TOTAL + "</strong> (" + pct + " %)" +
(TERMINES ? " — " + TERMINES + " terminé" + (TERMINES > 1 ? "s" : "") : "");
s.querySelector(".jauge span").style.width = pct + "%";
}
/* Clôture un exercice : affiche le verdict et le compte, une seule fois.
Un exercice déjà clos peut encore être rejoué (série chronométrée),
mais il ne change plus le score. */
function clore(box, reussi, msg){
var r = box.querySelector(".retour");
r.className = "retour " + (reussi ? "retour--ok" : "retour--ko");
r.textContent = (reussi ? "Réussi. " : "Terminé, mais avec des erreurs. ") + (msg || "");
if (box.dataset.compte === "1") return;
box.dataset.compte = "1";
TERMINES++;
if (reussi) REUSSIS++;
majScore();
}
/* Message intermédiaire : ne clôt pas l'exercice, ne compte pas. */
function signaler(box, ok, msg){
var r = box.querySelector(".retour");
r.className = "retour " + (ok ? "retour--ok" : "retour--ko");
r.textContent = msg;
}
/* ---------- cadre commun ---------- */
function cadre(d){
var box = el("article", "ex");
box.appendChild(el("p", "consigne", d.consigne || "Exercice"));
if (d.enonce) box.appendChild(el("p", "enonce", d.enonce));
return box;
}
function finir(box){
/* role="status" : le verdict est annoncé aux lecteurs d'écran
sans déplacer le focus de l'élève. */
var r = el("div", "retour");
r.setAttribute("role", "status");
r.setAttribute("aria-live", "polite");
box.appendChild(r);
return box;
}
/* ---------- QCM ---------- */
function faireQcm(d){
var box = cadre(d), fini = false;
d.options.forEach(function(txt, i){
var b = el("button", "opt", txt);
b.type = "button";
b.onclick = function(){
if (fini) return;
fini = true;
var ok = (i === d.reponse);
b.classList.add(ok ? "juste" : "faux");
if (!ok) box.querySelectorAll(".opt")[d.reponse].classList.add("juste");
box.querySelectorAll(".opt").forEach(function(o){ o.disabled = true; });
clore(box, ok, d.explication || "");
};
box.appendChild(b);
});
return finir(box);
}
/* ---------- Vrai / Faux ---------- */
function faireVf(d){
var box = cadre(d), fini = false;
["Vrai", "Faux"].forEach(function(txt, i){
var b = el("button", "jeton", txt);
b.type = "button";
b.onclick = function(){
if (fini) return;
fini = true;
var ok = ((i === 0) === d.reponse);
b.classList.add(ok ? "juste" : "faux");
box.querySelectorAll(".jeton").forEach(function(o){ o.disabled = true; });
clore(box, ok, d.explication || "");
};
box.appendChild(b);
});
return finir(box);
}
/* ---------- Association : un clic à gauche, un clic à droite ---------- */
function faireAssoc(d){
var box = cadre(d);
var zone = el("div", "deux-col");
var g = el("div"), dr = el("div");
g.appendChild(el("h4", null, "Colonne A"));
dr.appendChild(el("h4", null, "Colonne B"));
var choisi = null, trouves = 0, erreurs = 0;
var droite = melange(d.paires.map(function(p, i){ return { t: p[1], i: i }; }));
d.paires.forEach(function(p, i){
var b = el("button", "jeton jeton--bloc", p[0]);
b.type = "button";
b.dataset.i = i;
b.onclick = function(){
if (b.disabled) return;
if (choisi) choisi.classList.remove("choisi");
choisi = b; b.classList.add("choisi");
};
g.appendChild(b);
});
droite.forEach(function(o){
var b = el("button", "jeton jeton--bloc", o.t);
b.type = "button";
b.onclick = function(){
if (b.disabled) return;
if (!choisi){
signaler(box, false, "Choisis d'abord une étiquette dans la colonne A.");
return;
}
var ok = (parseInt(choisi.dataset.i, 10) === o.i);
if (ok){
choisi.classList.remove("choisi");
choisi.classList.add("juste"); b.classList.add("juste");
choisi.disabled = true; b.disabled = true;
choisi = null; trouves++;
if (trouves === d.paires.length){
clore(box, erreurs === 0, (d.explication || "") +
(erreurs ? " Erreurs : " + erreurs + "." : ""));
} else {
signaler(box, true, "Bonne association. Il en reste " +
(d.paires.length - trouves) + ".");
}
} else {
erreurs++;
b.classList.add("faux");
setTimeout(function(){ b.classList.remove("faux"); }, 600);
signaler(box, false, "Ce n'est pas la bonne correspondance, essaie autre chose.");
}
};
dr.appendChild(b);
});
zone.appendChild(g); zone.appendChild(dr);
box.appendChild(zone);
return finir(box);
}
/* ---------- Tri dans des paniers ----------
Les paniers sont des div : un bouton ne peut pas contenir les
étiquettes déposées, qui sont elles-mêmes des boutons. On leur
donne donc explicitement le rôle, la tabulation et les touches
Entrée et Espace, pour qu'ils soient utilisables au clavier. */
function faireTri(d){
var box = cadre(d);
var reserve = el("div", "reserve");
var choisi = null, places = 0, erreurs = 0;
melange(d.items).forEach(function(it){
var b = el("button", "item", it[0]);
b.type = "button";
b.dataset.p = it[1];
b.onclick = function(){
if (b.disabled) return;
if (choisi) choisi.classList.remove("choisi");
choisi = b; b.classList.add("choisi");
signaler(box, true, "« " + it[0] + " » est sélectionné. Choisis maintenant son panier.");
};
reserve.appendChild(b);
});
box.appendChild(reserve);
var paniers = el("div", "paniers");
d.paniers.forEach(function(nom, pi){
var p = el("div", "panier");
p.tabIndex = 0;
p.setAttribute("role", "button");
p.setAttribute("aria-label", "Panier " + nom);
p.appendChild(el("h4", null, nom));
function deposer(){
if (!choisi){
signaler(box, false, "Choisis d'abord une étiquette dans la réserve.");
return;
}
var ok = (parseInt(choisi.dataset.p, 10) === pi);
if (ok){
choisi.classList.remove("choisi");
choisi.classList.add("juste");
choisi.disabled = true;
p.appendChild(choisi);
choisi = null; places++;
if (places === d.items.length){
clore(box, erreurs === 0, (d.explication || "") +
(erreurs ? " Erreurs : " + erreurs + "." : ""));
} else {
signaler(box, true, "Bien classé. Il en reste " + (d.items.length - places) + ".");
}
} else {
erreurs++;
p.classList.add("vise");
setTimeout(function(){ p.classList.remove("vise"); }, 500);
signaler(box, false, "Ce n'est pas le bon panier.");
}
}
p.onclick = deposer;
p.onkeydown = function(e){
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar"){
e.preventDefault();
deposer();
}
};
paniers.appendChild(p);
});
box.appendChild(paniers);
return finir(box);
}
/* ---------- Remise en ordre ---------- */
function faireOrdre(d){
var box = cadre(d);
var liste = el("div", "liste-ordre");
var ordre = melange(d.etapes.map(function(t, i){ return { t: t, i: i }; }));
var fini = false, essais = 0;
function dessiner(){
liste.innerHTML = "";
ordre.forEach(function(o, pos){
var ligne = el("div", "item item--bloc");
ligne.appendChild(document.createTextNode((pos + 1) + ". " + o.t + " "));
var up = el("button", "jeton jeton--mini", "monter");
var dn = el("button", "jeton jeton--mini", "descendre");
up.type = dn.type = "button";
up.setAttribute("aria-label", "Monter : " + o.t);
dn.setAttribute("aria-label", "Descendre : " + o.t);
up.disabled = (pos === 0);
dn.disabled = (pos === ordre.length - 1);
up.onclick = function(){
var x = ordre[pos - 1]; ordre[pos - 1] = ordre[pos]; ordre[pos] = x; dessiner();
};
dn.onclick = function(){
var x = ordre[pos + 1]; ordre[pos + 1] = ordre[pos]; ordre[pos] = x; dessiner();
};
ligne.appendChild(up); ligne.appendChild(dn);
liste.appendChild(ligne);
});
}
dessiner();
box.appendChild(liste);
var v = el("button", "action", "Vérifier l'ordre");
v.type = "button";
v.onclick = function(){
if (fini) return;
var ok = ordre.every(function(o, pos){ return o.i === pos; });
if (ok){
fini = true; v.disabled = true;
liste.querySelectorAll(".item").forEach(function(i){ i.classList.add("juste"); });
clore(box, essais === 0, (d.explication || "") +
(essais ? " Vérifications ratées : " + essais + "." : ""));
} else {
essais++;
signaler(box, false, "L'ordre n'est pas encore correct, déplace encore des lignes.");
}
};
box.appendChild(v);
return finir(box);
}
/* ---------- Zones cliquables sur schéma ----------
Les zones SVG reçoivent tabindex et role pour être atteignables
au clavier, comme les boutons du reste de la page. */
function faireHotspot(d){
var box = cadre(d);
var wrap = el("div", "schema");
wrap.innerHTML = d.svg;
box.appendChild(wrap);
var question = el("p", "enonce");
question.setAttribute("role", "status");
question.setAttribute("aria-live", "polite");
box.appendChild(question);
var restants = melange(d.zones.slice());
var erreurs = 0;
function demander(){
if (!restants.length){
question.textContent = "Terminé.";
clore(box, erreurs === 0, (d.explication || "") +
(erreurs ? " Erreurs : " + erreurs + "." : ""));
return;
}
question.innerHTML = "Clique sur : <strong>" + restants[0] + "</strong>";
}
wrap.querySelectorAll(".zone").forEach(function(z){
z.setAttribute("tabindex", "0");
z.setAttribute("role", "button");
z.setAttribute("aria-label", z.dataset.zone || "zone");
function essayer(){
if (!restants.length) return;
if (z.dataset.zone === restants[0]){
z.classList.add("juste");
restants.shift();
demander();
} else {
erreurs++;
z.classList.add("faux");
setTimeout(function(){ z.classList.remove("faux"); }, 500);
signaler(box, false, "Ce n'est pas cet élément.");
}
}
z.addEventListener("click", essayer);
z.addEventListener("keydown", function(e){
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar"){
e.preventDefault();
essayer();
}
});
});
demander();
return finir(box);
}
/* ---------- Calcul ou réponse courte ---------- */
function faireSaisie(d){
var box = cadre(d);
var champ = el("input", "court");
champ.type = "text";
champ.setAttribute("aria-label", d.enonce || d.consigne || "Réponse");
var v = el("button", "action", "Vérifier");
v.type = "button";
var fini = false, essais = 0;
function verifier(){
if (fini) return;
var ok;
if (d.type === "calcul"){
var n = parseFloat(champ.value.replace(",", ".").replace(/[^0-9.\-]/g, ""));
ok = !isNaN(n) && Math.abs(n - d.reponse) <= (d.tolerance || 0);
} else {
ok = d.acceptees.some(function(a){ return norm(a) === norm(champ.value); });
}
if (ok){
fini = true; v.disabled = true; champ.disabled = true;
clore(box, essais === 0, (d.explication || "") +
(essais ? " Essais ratés : " + essais + "." : ""));
} else {
essais++;
signaler(box, false, d.indice || "Relis la théorie du chapitre et réessaie.");
}
}
v.onclick = verifier;
champ.onkeydown = function(e){ if (e.key === "Enter"){ e.preventDefault(); verifier(); } };
box.appendChild(champ);
box.appendChild(document.createTextNode(" "));
box.appendChild(v);
return finir(box);
}
/* ---------- Chasse aux affirmations fausses ---------- */
function faireIntrus(d){
var box = cadre(d);
var restants = d.items.filter(function(i){ return i[1]; }).length;
var erreurs = 0;
d.items.forEach(function(it){
var b = el("button", "opt", it[0]);
b.type = "button";
b.onclick = function(){
if (b.disabled) return;
b.disabled = true;
if (it[1]){
b.classList.add("juste");
restants--;
if (restants === 0){
clore(box, erreurs === 0, (d.explication || "") +
(erreurs ? " Affirmations correctes cliquées par erreur : " + erreurs + "." : ""));
} else {
signaler(box, true, "Bien vu. Il reste " + restants + " affirmation" +
(restants > 1 ? "s" : "") + " fausse" + (restants > 1 ? "s" : "") + ".");
}
} else {
b.classList.add("faux");
erreurs++;
signaler(box, false, "Cette affirmation est correcte, ce n'est pas un intrus.");
}
};
box.appendChild(b);
});
return finir(box);
}
/* ---------- Série chronométrée ----------
Le premier passage fixe le score de l'exercice. Les suivants
servent à s'entraîner : ils affichent un résultat mais ne
touchent plus au compteur de la page. */
function faireChrono(d){
var box = cadre(d);
var minuteur = el("span", "minuteur", "");
minuteur.setAttribute("role", "timer");
box.querySelector(".consigne").appendChild(minuteur);
var zone = el("div"); box.appendChild(zone);
var idx = 0, bons = 0, temps = d.secondes || 60, timer = null, encours = false;
var demarrer = el("button", "action", "Démarrer la série");
demarrer.type = "button";
box.appendChild(demarrer);
demarrer.onclick = function(){
if (encours) return;
encours = true;
demarrer.hidden = true;
idx = 0; bons = 0; temps = d.secondes || 60;
minuteur.textContent = temps + " s";
timer = setInterval(function(){
temps--;
minuteur.textContent = temps + " s";
if (temps <= 0) terminer();
}, 1000);
poser();
};
function poser(){
zone.innerHTML = "";
if (idx >= d.questions.length){ terminer(); return; }
var q = d.questions[idx];
zone.appendChild(el("p", "enonce", (idx + 1) + ". " + q.enonce));
q.options.forEach(function(t, i){
var b = el("button", "jeton", t);
b.type = "button";
b.onclick = function(){
if (!encours) return;
if (i === q.reponse) bons++;
idx++;
poser();
};
zone.appendChild(b);
});
}
function terminer(){
clearInterval(timer);
timer = null;
encours = false;
zone.innerHTML = "";
var seuil = Math.ceil(d.questions.length * 0.7);
var ok = bons >= seuil;
var deja = (box.dataset.compte === "1");
var detail = "Score : " + bons + " / " + d.questions.length +
". Seuil de réussite : " + seuil + ".";
if (deja){
/* Déjà compté au premier passage : on informe sans toucher au score. */
signaler(box, ok, detail + " Ce nouvel essai ne change plus le compteur de la page.");
} else {
clore(box, ok, detail);
}
demarrer.textContent = "Recommencer la série";
demarrer.hidden = false;
}
return finir(box);
}
/* ---------- Construction de la page ---------- */
var FABRIQUES = {
qcm: faireQcm, vf: faireVf, assoc: faireAssoc, tri: faireTri,
ordre: faireOrdre, hotspot: faireHotspot, calcul: faireSaisie,
texte: faireSaisie, intrus: faireIntrus, chrono: faireChrono
};
function construire(data){
var cible = document.getElementById("exercices");
if (!cible) return;
TOTAL = data.length; REUSSIS = 0; TERMINES = 0;
var s = el("div", "score");
s.id = "score";
s.appendChild(el("div", "score__txt"));
var jauge = el("div", "jauge");
jauge.appendChild(el("span"));
s.appendChild(jauge);
var rz = el("button", "action action--secondaire", "Tout recommencer");
rz.type = "button";
rz.onclick = function(){ location.reload(); };
s.appendChild(rz);
cible.parentNode.insertBefore(s, cible);
data.forEach(function(d){
var f = FABRIQUES[d.type];
if (f) cible.appendChild(f(d));
});
majScore();
}
window.addEventListener("DOMContentLoaded", function(){
if (window.DATA) construire(window.DATA);
});
+297
View File
@@ -0,0 +1,297 @@
/* ==================================================================
Jeux du module A — 2e différenciée
Chargé après assets/theme.css, dont il reprend toutes les couleurs.
Les jeux sont conçus pour la projection au TBI : cibles larges,
contrastes francs, aucun texte sous 0,85 rem.
================================================================== */
/* ---------- séances affichées une par une ---------- */
section[hidden] { display: none; }
/* ---------- plan et objectifs de la séance ---------- */
ol.plan {
counter-reset: s;
list-style: none;
padding: 0;
max-width: var(--lecture);
}
ol.plan li {
counter-increment: s;
position: relative;
padding: .45rem 0 .45rem 3rem;
border-bottom: 1px solid var(--trait);
}
ol.plan li::before {
content: counter(s);
position: absolute;
left: 0;
top: .45rem;
width: 2rem;
height: 2rem;
line-height: 2rem;
text-align: center;
background: var(--encre);
color: var(--papier);
font-size: .9rem;
border-radius: 50%;
}
ul.but { list-style: none; padding: 0; max-width: var(--lecture); }
ul.but li { padding: .35rem 0 .35rem 1.6rem; position: relative; }
ul.but li::before {
content: "";
position: absolute;
left: 0;
top: .95rem;
width: .7rem;
height: .7rem;
background: var(--accent);
}
/* ---------- carte d'un jeu ---------- */
.atelier {
border: 1px solid var(--trait);
border-radius: .5rem;
margin: 1.2rem 0;
background: var(--blanc);
overflow: hidden;
}
.atelier > h4 {
margin: 0;
padding: .6rem 1rem;
background: var(--encre);
color: var(--papier);
font-family: var(--police-titre);
font-size: .95rem;
font-weight: 700;
display: flex;
gap: .6rem;
align-items: baseline;
}
.atelier > h4 .num {
background: var(--accent);
color: var(--encre);
font-size: .78rem;
padding: .05rem .45rem;
border-radius: .2rem;
}
.atelier .corps { padding: 1rem; }
.consigne { margin: 0 0 .9rem; font-size: .95rem; max-width: var(--lecture); }
/* ---------- barre d'état d'un jeu ---------- */
.barre {
display: flex;
flex-wrap: wrap;
gap: .7rem 1.4rem;
align-items: center;
margin: .2rem 0 .9rem;
font-size: .95rem;
}
.cible-txt { font-weight: 700; font-size: 1.05rem; }
.score { color: var(--encre-douce); font-size: .9rem; }
/* ---------- boutons de jeu ---------- */
.vf button,
.qcm .choix button,
.paires button,
.liste button,
.etape {
font: inherit;
background: var(--blanc);
color: var(--encre);
border: 2px solid var(--encre);
border-radius: .3rem;
cursor: pointer;
text-align: left;
}
/* ---------- aire de pions (jeu des nombres) ---------- */
.aire {
position: relative;
width: 100%;
height: 22rem;
background: var(--papier);
border: 2px solid var(--trait-fort);
border-radius: .4rem;
overflow: hidden;
touch-action: none;
}
.pion {
position: absolute;
width: 3.2rem;
height: 3.2rem;
line-height: 3.2rem;
text-align: center;
font-family: var(--police-titre);
font-size: 1.2rem;
font-weight: 700;
background: var(--blanc);
color: var(--encre);
border: 2px solid var(--encre);
border-radius: 50%;
cursor: pointer;
user-select: none;
padding: 0;
}
.pion.fait { background: var(--juste); border-color: var(--juste); color: #fff; }
.pion.actif { background: var(--cible); border-color: var(--cible); }
.rate { animation: secousse .25s; }
@keyframes secousse { 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }
/* ---------- double-clic : les douze boîtes ---------- */
.boites {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: .7rem;
max-width: 34rem;
}
.boite {
aspect-ratio: 1 / 1;
border: 2px solid var(--encre);
border-radius: .3rem;
background: var(--blanc);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.boite.ouverte { background: var(--juste); border-color: var(--juste); }
.boite .forme { width: 52%; height: 52%; opacity: 0; }
.boite.ouverte .forme { background: #fff; opacity: 1; border-radius: 50%; }
/* ---------- glisser-déposer dans des bacs ---------- */
.tri { display: flex; flex-wrap: wrap; gap: 1.2rem; }
.reserve {
flex: 1 1 16rem;
min-height: 8rem;
padding: .6rem;
border: 2px dashed var(--trait-fort);
border-radius: .4rem;
display: flex;
flex-wrap: wrap;
gap: .5rem;
align-content: flex-start;
}
.bacs { flex: 2 1 22rem; display: flex; gap: .8rem; flex-wrap: wrap; }
.bac {
flex: 1 1 7rem;
min-height: 8rem;
border: 2px solid var(--encre);
border-radius: .4rem;
display: flex;
flex-wrap: wrap;
gap: .5rem;
align-content: flex-start;
padding: .5rem;
cursor: pointer;
}
.bac .titre {
flex: 1 0 100%;
font-size: .85rem;
text-align: center;
color: var(--encre-douce);
}
.bac.vise { background: var(--cible-fond); border-color: var(--cible); }
.jeton {
font: inherit;
padding: .4rem .65rem;
background: var(--blanc);
border: 2px solid var(--encre);
border-radius: .3rem;
cursor: grab;
user-select: none;
font-size: .88rem;
touch-action: none;
}
/* Sélection au clic : alternative au glisser pour qui ne maîtrise pas
encore le maintien du bouton, et seul moyen au clavier. */
.jeton.choisi { background: var(--cible); border-color: var(--cible); }
.jeton.pris {
position: fixed;
z-index: 99;
pointer-events: none;
cursor: grabbing;
box-shadow: 0 6px 14px rgba(22, 48, 43, .3);
}
.jeton .glyphe { display: block; width: 2rem; height: 2rem; }
/* ---------- remise en ordre ---------- */
.etapes { display: flex; flex-direction: column; gap: .5rem; max-width: 42rem; }
.etape { padding: .5rem .75rem; }
.etape.placee { background: var(--juste); border-color: var(--juste); color: #fff; cursor: default; }
.rang { display: inline-block; min-width: 1.6rem; font-weight: 700; }
/* ---------- vrai ou faux ---------- */
.vf { max-width: var(--lecture); }
.vf .ligne {
display: flex;
flex-wrap: wrap;
gap: .5rem;
align-items: center;
padding: .5rem 0;
border-bottom: 1px solid var(--trait);
}
.vf .txt { flex: 1 1 18rem; }
.vf .verdict { font-size: .85rem; flex: 1 1 100%; }
.vf button { padding: .25rem .8rem; }
.vf button.choisi { background: var(--encre); color: var(--papier); }
.juste { color: var(--juste); }
.faux { color: var(--erreur); }
/* ---------- questions à choix ---------- */
.qcm { max-width: var(--lecture); }
.qcm .q { font-weight: 700; margin: .2rem 0 .6rem; }
.qcm .choix { display: flex; flex-wrap: wrap; gap: .5rem; }
.qcm .choix button { padding: .4rem .85rem; }
.qcm .choix button.bon { background: var(--juste); border-color: var(--juste); color: #fff; }
.qcm .choix button.mauvais { background: var(--erreur); border-color: var(--erreur); color: #fff; }
.qcm .retour { margin-top: .7rem; font-size: .9rem; min-height: 1.4rem; }
/* ---------- paires à relier ---------- */
.paires { display: flex; gap: 1rem; flex-wrap: wrap; max-width: var(--lecture); }
.paires .col { flex: 1 1 13rem; display: flex; flex-direction: column; gap: .45rem; }
.paires button { font-size: .88rem; padding: .45rem .7rem; }
.paires button.choisi { background: var(--cible); border-color: var(--cible); }
.paires button.liee { background: var(--juste); border-color: var(--juste); color: #fff; cursor: default; }
/* ---------- parcours de dossiers ---------- */
.chemin { max-width: 40rem; }
.chemin .fil {
font-size: .9rem;
color: var(--encre-douce);
margin-bottom: .6rem;
word-break: break-word;
}
.liste { display: flex; flex-direction: column; gap: .4rem; }
.liste button { padding: .45rem .7rem; }
.liste button.dossier::before { content: "[dossier] "; color: var(--encre-douce); font-size: .8rem; }
.liste button.fichier::before { content: "[fichier] "; color: var(--encre-douce); font-size: .8rem; }
/* ---------- schémas à zones ---------- */
figure.schema { margin: 0; max-width: 30rem; }
.zone { fill: var(--blanc); stroke: var(--encre); stroke-width: 2; cursor: pointer; }
.zone:hover { fill: var(--cible-fond); }
.zone.trouve { fill: var(--juste-fond); stroke: var(--juste); }
svg text { font-family: inherit; font-size: 11px; fill: var(--encre); pointer-events: none; }
@media print {
section[hidden] { display: block; }
.aire, .boites { display: none; }
}
+491
View File
@@ -0,0 +1,491 @@
/* ==================================================================
Thème commun de la plateforme « Mes cours » — M. Courcelle
------------------------------------------------------------------
Une seule feuille pour toute la plateforme : la page d'aiguillage,
les index de classe et les pages de module partagent ces jetons,
cette typographie et ces composants. Les feuilles spécialisées
(exercices-commune.css, jeux-differenciee.css, atelier-texte.css)
ne redéfinissent jamais une couleur en dur : elles consomment les
variables déclarées ici.
Pour ajouter une classe :
1. un couple --xxx-fond / --xxx-trait / --xxx-encre dans :root ;
2. une règle .classe-xxx plus bas ;
3. les règles de la tuile (.classe--xxx) dans index.html.
================================================================== */
:root {
/* --- encres et papiers ------------------------------------------ */
--encre: #16302B; /* texte principal, vert très sombre */
--encre-douce: #3E5751; /* texte secondaire, AA sur tous les fonds */
--papier: #FBFCFB; /* fond de page */
--blanc: #FFFFFF; /* fond des cartes et des champs */
--trait: #D5DEDA; /* bordures neutres */
--trait-fort: #B3C2BC;
/* --- couleurs par classe ---------------------------------------- */
/* -fond : aplat pâle ; -trait : filet vif, jamais du texte ;
-encre : variante sombre du filet, contraste AA pour du texte. */
--tc-fond: #FBE1E7; --tc-trait: #D6336C; --tc-encre: #B82A59;
--diff-fond: #DDF5EC; --diff-trait: #05BC8A; --diff-encre: #036F51;
--comm-fond: #FDF0CF; --comm-trait: #E0A100; --comm-encre: #7F5B00;
--ts-fond: #DFEAFB; --ts-trait: #2F6FDB; --ts-encre: #215FC2;
--ae-fond: #F1E3F7; --ae-trait: #9B4DCA; --ae-encre: #8840B4;
/* --- couleurs de verdict ---------------------------------------- */
--juste: #1E7A4C;
--juste-fond: #E3F3EA;
--erreur: #B0322A;
--erreur-fond: #F9E5E3;
--cible: #F5C542; /* surlignage, marqueur */
--cible-fond: #FDF6E1;
/* --- accent courant --------------------------------------------- */
/* Neutre par défaut ; chaque page de classe le remplace en posant
une classe .classe-xxx sur <body>. Tout le reste de la plateforme
ne connaît que --accent, --accent-fond et --accent-encre. */
--accent: var(--encre);
--accent-fond: #EDF2F0;
--accent-encre: var(--encre);
/* --- typographie ------------------------------------------------- */
--police-titre: "JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace;
--police-texte: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--police-doc: Cambria, Georgia, "Times New Roman", serif; /* specimens de traitement de texte */
/* --- mesures ------------------------------------------------------ */
--lecture: 64ch; /* largeur de confort d'un paragraphe */
--page: 58rem; /* largeur de la colonne de contenu */
--gouttiere: 1.5rem; /* marge latérale, identique partout */
--base: 17px;
}
/* Identité de classe : posée sur <body>. */
.classe-tc { --accent: var(--tc-trait); --accent-fond: var(--tc-fond); --accent-encre: var(--tc-encre); }
.classe-diff { --accent: var(--diff-trait); --accent-fond: var(--diff-fond); --accent-encre: var(--diff-encre); }
.classe-comm { --accent: var(--comm-trait); --accent-fond: var(--comm-fond); --accent-encre: var(--comm-encre); }
.classe-ts { --accent: var(--ts-trait); --accent-fond: var(--ts-fond); --accent-encre: var(--ts-encre); }
.classe-ae { --accent: var(--ae-trait); --accent-fond: var(--ae-fond); --accent-encre: var(--ae-encre); }
/* ==================================================================
Base
================================================================== */
*, *::before, *::after { box-sizing: border-box; }
html {
font-size: var(--base);
scroll-behavior: smooth;
scroll-padding-top: 1rem;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--papier);
color: var(--encre);
font-family: var(--police-texte);
font-size: 1rem;
line-height: 1.55;
-webkit-text-size-adjust: 100%;
}
/* Mode projection : agrandit toute la page d'un coup pour le TBI.
Le jeton doit être posé sur <html> et non sur <body> : c'est <html>
qui porte font-size, et donc la valeur du rem pour toute la page. */
html.projection { --base: 25px; }
p, ul, ol { max-width: var(--lecture); }
p { margin: 0 0 .9rem; }
li { margin-bottom: .35rem; }
a { color: var(--accent-encre); }
a:hover { text-decoration-thickness: 2px; }
strong { font-weight: 600; }
code, kbd {
font-family: ui-monospace, "JetBrains Mono", Consolas, "Courier New", monospace;
font-size: .92em;
background: var(--accent-fond);
padding: .05rem .3rem;
border-radius: .2rem;
}
:focus-visible {
outline: 3px solid var(--accent-encre);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation: none !important; transition: none !important; }
}
/* ==================================================================
En-tête de page
Fil d'Ariane, titre, sous-titre, et le filet de couleur de la
classe — le même motif que les tuiles de la page d'accueil.
================================================================== */
.entete {
background: var(--blanc);
border-bottom: .5rem solid var(--accent);
}
.entete__interieur {
max-width: calc(var(--page) + 2 * var(--gouttiere));
margin: 0 auto;
padding: 1rem var(--gouttiere) 1.1rem;
}
.ariane {
font-size: .85rem;
color: var(--encre-douce);
margin-bottom: .5rem;
}
.ariane a { color: var(--encre-douce); }
.ariane a:hover { color: var(--accent-encre); }
.ariane span[aria-hidden] { padding: 0 .35rem; color: var(--trait-fort); }
.ariane strong { color: var(--encre); font-weight: 600; }
.entete h1 {
margin: 0;
font-family: var(--police-titre);
font-size: clamp(1.35rem, 3.2vw, 1.9rem);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.15;
}
.entete__meta {
margin: .35rem 0 0;
max-width: none;
color: var(--encre-douce);
font-size: .95rem;
}
/* Outils à droite du titre (bouton « Agrandir », par exemple) */
.entete__barre {
display: flex;
flex-wrap: wrap;
gap: .6rem 1.2rem;
align-items: flex-end;
justify-content: space-between;
}
/* ==================================================================
Corps de page
================================================================== */
.contenu {
flex: 1;
width: 100%;
max-width: calc(var(--page) + 2 * var(--gouttiere));
margin: 0 auto;
padding: 2rem var(--gouttiere) 4rem;
}
.chapeau {
font-size: 1.08rem;
color: var(--encre-douce);
max-width: var(--lecture);
}
h2 {
font-family: var(--police-titre);
font-size: clamp(1.15rem, 2.6vw, 1.45rem);
font-weight: 700;
letter-spacing: -0.01em;
line-height: 1.25;
margin: 2.8rem 0 .8rem;
padding-top: .8rem;
border-top: 2px solid var(--accent);
max-width: var(--lecture);
}
.contenu > h2:first-child,
section > h2:first-child,
.inner > h2:first-child {
border-top: 0;
padding-top: 0;
margin-top: .5rem;
}
h3 {
font-family: var(--police-titre);
font-size: 1.02rem;
font-weight: 700;
margin: 1.8rem 0 .5rem;
color: var(--accent-encre);
}
h4 {
font-size: .95rem;
font-weight: 600;
margin: 1.2rem 0 .4rem;
}
/* ==================================================================
Encadrés
.encadre rappel neutre
.encadre--theorie ce qu'il faut savoir avant de jouer
.encadre--cle la règle à retenir
.encadre--attention piège fréquent
================================================================== */
.encadre {
background: var(--accent-fond);
border-left: .4rem solid var(--accent);
border-radius: 0 .4rem .4rem 0;
padding: .9rem 1.1rem;
margin: 1.4rem 0;
max-width: var(--lecture);
}
.encadre > :first-child { margin-top: 0; }
.encadre > :last-child { margin-bottom: 0; }
.encadre h3 { margin-top: 0; color: var(--encre); }
.encadre--theorie { background: var(--accent-fond); }
.encadre--cle { background: var(--cible-fond); border-left-color: var(--cible); }
.encadre--attention { background: var(--erreur-fond); border-left-color: var(--erreur); }
/* ==================================================================
Boutons
================================================================== */
.action {
font: inherit;
font-weight: 600;
font-size: .95rem;
background: var(--encre);
color: var(--papier);
border: 2px solid var(--encre);
border-radius: .4rem;
padding: .5rem 1.05rem;
cursor: pointer;
}
.action:hover { background: var(--accent-encre); border-color: var(--accent-encre); }
.action--secondaire {
background: var(--blanc);
color: var(--encre);
}
.action--secondaire:hover { background: var(--encre); color: var(--papier); border-color: var(--encre); }
.action[disabled] { opacity: .5; cursor: default; }
.action[disabled]:hover { background: var(--encre); border-color: var(--encre); }
.actions {
display: flex;
gap: .5rem;
flex-wrap: wrap;
margin: .9rem 0;
}
/* ==================================================================
Sommaire d'un module (bandeau de blocs / de séances)
================================================================== */
.sommaire {
background: var(--accent-fond);
border-bottom: 1px solid var(--trait);
}
.sommaire__interieur {
max-width: calc(var(--page) + 2 * var(--gouttiere));
margin: 0 auto;
padding: .7rem var(--gouttiere);
display: flex;
flex-wrap: wrap;
gap: .4rem;
}
.sommaire a,
.sommaire button {
font: inherit;
font-size: .9rem;
font-weight: 600;
text-decoration: none;
color: var(--encre);
background: var(--blanc);
border: 1px solid var(--trait-fort);
border-radius: .35rem;
padding: .35rem .8rem;
cursor: pointer;
}
.sommaire a:hover,
.sommaire button:hover { border-color: var(--encre); }
.sommaire a[aria-current="page"],
.sommaire button[aria-current="true"] {
background: var(--encre);
border-color: var(--encre);
color: var(--papier);
}
/* ==================================================================
Index de classe : la liste des modules
Un <li class="module"> par module. Voir le gabarit en commentaire
dans chaque index de classe.
================================================================== */
.modules {
list-style: none;
padding: 0;
margin: 1.2rem 0 0;
max-width: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
gap: 1rem;
}
.module { margin: 0; }
.module__lien {
display: flex;
flex-direction: column;
height: 100%;
gap: .3rem;
background: var(--blanc);
border: 1px solid var(--trait);
border-top: .4rem solid var(--accent);
border-radius: .5rem;
padding: 1.1rem 1.2rem 1.2rem;
color: var(--encre);
text-decoration: none;
}
@media (hover: hover) {
.module__lien:hover { border-color: var(--accent); border-top-color: var(--accent); }
.module__lien:hover .module__titre { text-decoration: underline; text-decoration-thickness: 2px; }
}
.module__num {
font-family: var(--police-titre);
font-size: .78rem;
font-weight: 700;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--accent-encre);
}
.module__titre {
font-family: var(--police-titre);
font-size: 1.12rem;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.01em;
}
.module__resume {
color: var(--encre-douce);
font-size: .95rem;
margin-top: .2rem;
}
.module__meta {
margin-top: auto;
padding-top: .8rem;
color: var(--encre-douce);
font-size: .85rem;
}
/* Module annoncé mais pas encore en ligne : même carte, sans lien. */
.module--bientot .module__lien {
border-top-color: var(--trait-fort);
background: var(--papier);
color: var(--encre-douce);
cursor: default;
}
.module--bientot .module__num { color: var(--encre-douce); }
/* Classe sans aucun module publié */
.vide {
background: var(--blanc);
border: 2px dashed var(--trait-fort);
border-radius: .5rem;
padding: 2rem 1.5rem;
text-align: center;
color: var(--encre-douce);
max-width: none;
}
/* ==================================================================
Tableaux, figures, captures
================================================================== */
table {
border-collapse: collapse;
width: 100%;
max-width: var(--lecture);
margin: 1.2rem 0;
font-size: .95rem;
}
th, td {
border-bottom: 1px solid var(--trait);
padding: .45rem .6rem;
text-align: left;
vertical-align: top;
}
th {
background: var(--accent-fond);
border-bottom: 2px solid var(--accent);
font-weight: 600;
}
figure {
margin: 1.4rem 0;
max-width: var(--lecture);
}
figure img {
width: 100%;
display: block;
border: 1px solid var(--trait);
border-radius: .3rem;
}
figcaption {
font-size: .86rem;
color: var(--encre-douce);
margin-top: .4rem;
}
/* Capture pas encore déposée : la page reste lisible et dit quoi ajouter. */
.manque {
border: 2px dashed var(--trait-fort);
background: var(--papier);
border-radius: .3rem;
padding: 2rem 1rem;
text-align: center;
color: var(--encre-douce);
font-size: .9rem;
}
/* ==================================================================
Pied de page
================================================================== */
.pied {
border-top: 1px solid var(--trait);
background: var(--blanc);
color: var(--encre-douce);
font-size: .88rem;
}
.pied__interieur {
max-width: calc(var(--page) + 2 * var(--gouttiere));
margin: 0 auto;
padding: 1.2rem var(--gouttiere) 2rem;
}
.pied a { color: var(--encre-douce); }
/* ==================================================================
Impression
================================================================== */
@media print {
body { background: #fff; }
.sommaire, .action, .actions, .entete__barre button { display: none; }
.entete { border-bottom-width: 2px; }
.contenu { max-width: none; padding: 0; }
}