Ajoute la fonctionnalite quiz autonome/plein ecran a la boite a quiz
Build and deploy / test-python (push) Successful in 12m10s
Build and deploy / test-js (push) Successful in 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Introduit la double categorie de modeles (boite de dialogue / page de
quiz plein ecran) avec plein ecran, minuteur, score integre et ecran de
resultat pour les modeles page ; ajoute les modeles "Manga" (boite et
page) et "Classique" (page), pilotables aussi par l'assistant IA Ruby.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-13 15:07:23 +02:00
co-authored by Claude Sonnet 5
parent 559331f9cf
commit 7db4803b93
26 changed files with 3173 additions and 60 deletions
+559 -2
View File
@@ -609,6 +609,24 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
.iconGallery.personnageGallery{ grid-template-columns:repeat(2, 1fr); gap:10px; max-height:420px; }
.personnageThumb{ width:100%; height:100%; object-fit:contain; image-rendering:pixelated; }
/* "🖼️ Modèles" de la "❓ Boîte à quiz" (dialogue ET page, voir
templates/scene_edit.html, screens/rendering/quiz_box_templates.py) —
chaque modèle est un thème complet et autonome (structure/police/
couleurs propres) : plutôt que reproduire sa structure/CSS en
miniature (fragile, vite désynchronisé d'un vrai thème complet), la
vignette est une vraie capture d'écran du modèle (static/
quiz-templates/<clé>.png, une par modèle). */
.quizTemplateGrid{ display:grid; grid-template-columns:repeat(2, 1fr); gap:10px; margin-top:6px; }
.quizTemplateTile{
display:flex; flex-direction:column; gap:6px; padding:6px; background:var(--panel2);
border:2px solid var(--border); border-radius:8px; cursor:pointer;
}
.quizTemplateTile:hover{ border-color:var(--accent); }
.quizTemplateTile.is-selected{ border-color:var(--accent); box-shadow:0 0 0 2px var(--accent) inset; }
.quizTemplateName{ font-size:11.5px; color:var(--text-dim); text-align:center; }
.quizTemplatePreviewImg{ display:block; width:100%; height:170px; overflow:hidden; border-radius:4px; }
.quizTemplatePreviewImg img{ width:100%; height:100%; object-fit:cover; object-position:top; display:block; }
/* "Mes assets" — sous-sections Images/Sons/Vidéos (demande explicite :
"voir, utiliser ou supprimer des son, image et vidéo") : une tuile
image garde son bouton de pose (clic/glisser-déposer, voir
@@ -795,6 +813,27 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
}
.playScreen .quizBoxWidget{ display:none; }
.playScreen .quizBoxWidget.is-active{ display:flex; animation:quizPopIn .3s ease; }
/* Un quiz PLEIN ÉCRAN est déplacé en enfant direct de <body> au premier
affichage (voir forgeEscapeCameraForFullscreen, dialogue-box-controller.js
— ".playScreen.playScene" est transformé/centré, ce qui casserait
position:fixed s'il y restait) : plus dans ".playScreen" ensuite, donc
plus couvert par les deux règles ci-dessus — cette classe reproduit le
même masquage par défaut/affichage sur ".is-active", posée au moment du
déplacement. */
.quizBoxWidget--detachedForFullscreen{ display:none; }
.quizBoxWidget--detachedForFullscreen.is-active{ display:flex; animation:quizPopIn .3s ease; }
/* Bandeau partagé header + score/minuteur (demande explicite : "le score
doit être à l'intérieur de la boîte à quiz, pas à l'extérieur") — le
score (topbar) vit dans un élément FRÈRE du header (voir le
commentaire d'en-tête de screens/rendering/quiz_box_templates.py, JS
écrase les enfants du header via .textContent), posé ici CÔTE À CÔTE
avec lui dans le MÊME bandeau plutôt qu'empilé au-dessus dans une bande
à part — un seul fond commun (posé sur CE conteneur, jamais sur le
header seul, voir _quiz_header_row_html), pas deux bandes de couleurs
différentes. */
.quizBoxHeaderRow{ display:flex; align-items:stretch; }
.quizBoxHeaderRow .quizBoxHeader{ flex:1 1 auto; min-width:0; }
.quizBoxHeaderRow .quizBoxTopBar{ width:auto; flex:0 0 auto; }
.quizBoxHeader{ padding:8px 12px; font-weight:700; }
.quizBoxBody{ padding:10px 12px; flex:1; overflow-y:auto; display:flex; flex-direction:column; gap:10px; }
.quizBoxQuestionText{ margin:0; font-weight:600; }
@@ -818,12 +857,530 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
.quizBoxChoiceBtn.is-wrong{ background:#e0524d !important; border-color:#e0524d !important; animation:quizWrongShake .4s ease; }
/* Bouton "Continuer →" (voir forgeQuizBoxAnswer/forgeQuizBoxContinue,
RGAA/WCAG 2.2.1) — remplace l'ancienne avancée automatique après un
délai fixe : l'apprenant décide lui-même quand passer à la suite. */
délai fixe : l'apprenant décide lui-même quand passer à la suite.
TOUJOURS présent dès la question (posé désactivé par forgeShowQuizBox,
activé par forgeQuizBoxAnswer une fois répondu — demande explicite,
comme la maquette manga validée) : ":disabled" générique ici, commun
à TOUS les modèles, quelle que soit leur propre couleur d'action. */
.quizBoxContinueBtn{
margin-top:4px; background:#2f6fed; color:#fff; border:1px solid #2f6fed;
border-radius:6px; padding:8px 10px; font:inherit; font-size:13px; font-weight:600; cursor:pointer;
}
.quizBoxContinueBtn:hover{ background:#255bc4; }
.quizBoxContinueBtn:hover:not(:disabled){ background:#255bc4; }
.quizBoxContinueBtn:disabled{ opacity:.45; cursor:not-allowed; }
/* "Plein écran" (demande explicite : quiz RH autonome, voir
screens/rendering/quiz_box_config.py) — la boîte à quiz elle-même
devient la surface plein écran (pas besoin d'un overlay séparé comme
static/js/play/video-overlay.js : son propre fond couvre déjà tout).
z-index 1001 : au-dessus de .forgeVideoOverlay (1000) par précaution,
au cas où une vidéo serait encore visible. Commun à tous les modèles. */
.quizBoxWidget.is-fullscreen{
position:fixed !important; inset:0 !important; left:0 !important; top:0 !important;
width:100vw !important; height:100vh !important;
z-index:1001 !important; display:flex !important; flex-direction:column !important;
}
/* Demande explicite : "pas de bordure visible, pas de bord arrondi" en
plein écran — une bordure/un coin arrondi/une ombre portée n'ont plus
de sens une fois que la boîte occupe TOUT l'écran (rien derrière/autour
d'elle à en distinguer), quel que soit le modèle. */
.quizBoxWidget.is-fullscreen{
border-radius:0 !important; box-shadow:none !important; border:none !important;
}
/* Bug corrigé : "rien ne s'affiche" — align-items:center/justify-content:center
posés ici auparavant retiraient l'étirement (stretch) par défaut d'un
conteneur flex en colonne, réduisant .quizBoxHeader/.quizBoxBody à la
largeur de LEUR PROPRE contenu au lieu de toute la largeur — dans
certains navigateurs, combiné à .quizBoxBody (lui-même un conteneur
flex en colonne, voir plus haut), ce rétrécissement en cascade
ramenait la zone de texte à une largeur quasi nulle, rendant le
header/la question/les choix invisibles bien qu'ils soient bien posés
dans le DOM avec leur texte. Un simple empilement plein largeur/pleine
hauteur (comportement déjà éprouvé hors plein écran) suffit : pas
besoin de centrer quoi que ce soit, la boîte occupe déjà tout l'écran.*/
.quizBoxWidget.is-fullscreen .quizBoxBody{ min-height:0; }
/* Éditeur (voir templates/scene_edit.html) : occuper le VIEWPORT du
navigateur (comme au jeu) casserait l'édition — un fond opaque
couvrirait aussi le panneau de propriétés. "Plein écran" occupe ici
tout le CANEVAS (le "monde" de la scène) à la place, posé sur
.canvasElement (voir plus bas, jamais sur .quizBoxWidget lui-même —
déjà forcé à 100%/100% de .canvasElement par .canvasElementInner >
.quizBoxWidget, voir plus haut dans ce fichier) : suffisant pour que
le créateur voie tout de suite l'effet, sans avoir à lancer le jeu.
La "notion de caméra disparaît" (demande explicite) : le cadre "🎥
Champ de la caméra" est masqué côté serveur dès qu'un quiz plein écran
existe sur l'écran (voir routes/scenes/scene_edit_view.py), puisqu'il
n'a plus aucun sens une fois que ce widget couvre tout, caméra ou pas.*/
.canvasElement.isQuizFullscreenPreview{
left:0 !important; top:0 !important; width:100% !important; height:100% !important; z-index:9000 !important;
}
/* Barre score/minuteur — visible SEULEMENT en plein écran (une boîte à
quiz "normale", posée dans le décor, n'a pas besoin d'afficher un
score/minuteur en permanence) : masquée par défaut, quel que soit le
modèle, pour ne rien changer à l'apparence d'un quiz existant. */
.quizBoxTopBar{
display:none; width:100%; padding:6px 10px; font-weight:700; font-size:13px;
align-items:center; justify-content:space-between; gap:10px; box-sizing:border-box;
}
.quizBoxWidget.is-fullscreen .quizBoxTopBar{ display:flex; }
.quizBoxTimerDisplay:empty{ display:none; }
.quizBoxTimerDisplay{
font-variant-numeric:tabular-nums; padding:2px 10px; border-radius:999px;
background:rgba(255,255,255,.15);
}
.quizBoxTimerDisplay.is-urgent{ background:#e0524d; color:#fff; }
/* ---------- Modèle "Manga" (boîte de dialogue) — voir
screens/rendering/quiz_box_templates.py::render_quiz_box_manga_dialogue,
validé par le créateur via une maquette interactive. Pendant, en
petite carte flottante (JAMAIS plein écran), du modèle "page de quiz"
"Manga" plus bas — réutilise la structure PARTAGÉE des modèles
"boîte" (quizBoxHeaderRow/quizBoxHeader/quizBoxTopBar/quizBoxBody/
quizBoxQuestionText/quizBoxChoices/quizBoxChoiceBtn/
quizBoxContinueBtn), seule la décoration change. .quizMangaDialogueFrame
porte la forme/le fond (jamais le conteneur racine, qui doit rester
positionnable/redimensionnable normalement) — même principe que les
anciens modèles "Verre dépoli"/"Néon". Police : Montserrat/Poppins
(déjà auto-hébergées, voir static/vendor/fonts/fonts.css) — jamais
Bangers/Noto Sans JP de la maquette (Google Fonts), même contrainte
SCORM hors-ligne déjà rencontrée pour le modèle "page" "Manga". */
.quizBoxWidget--mangaDialogue{
position:relative; background:#151316; border-radius:0 !important;
box-shadow:6px 6px 0 rgba(0,0,0,.4) !important; border:3px solid #000 !important;
}
.quizBoxWidget--mangaDialogue .quizMangaDialogueFrame{
display:flex; flex-direction:column; flex:1; width:100%; height:100%; overflow:hidden;
font-family:"Poppins", system-ui, sans-serif; color:#f3ead6; background:#151316;
}
.quizBoxWidget--mangaDialogue .quizBoxHeaderRow{
/* Identité de marque FIXE (rouge), jamais la couleur configurable du
panneau "🎨 Style" (header_bg, posée en style INLINE par
_quiz_header_row_html — !important nécessaire pour la dominer) —
même principe déjà appliqué par l'ancien modèle "Ludique" avant sa
suppression, conforme à la maquette validée. */
background-color:#d81e3e !important;
clip-path:polygon(0 0, 100% 0, 97% 100%, 0 100%); padding:10px 20px 12px 18px; position:relative;
background-image:radial-gradient(rgba(255,255,255,.16) 1px, transparent 1.4px); background-size:7px 7px;
/* Un `border` classique ne suit pas un clip-path — le liseré noir le
long du bord dentelé (demande explicite : "des bords stylisés" sur
la maquette) vient d'un drop-shadow qui, lui, épouse la silhouette
RÉELLEMENT visible après découpe (contrairement à box-shadow, qui
resterait un simple rectangle). */
filter:drop-shadow(2px 0 0 #000) drop-shadow(-2px 0 0 #000) drop-shadow(0 2px 0 #000);
}
.quizBoxWidget--mangaDialogue .quizBoxHeader{
font-family:"Montserrat", sans-serif; font-weight:700; font-style:italic; letter-spacing:.02em;
text-transform:uppercase; font-size:15px; color:#fff2ec; text-shadow:2px 2px 0 #8f0f24;
}
/* "Quête : " devant le nom (JS n'écrit QUE le nom brut de l'objet
déclencheur dans [data-quiz-role="header"], voir forgeShowQuizBox,
dialogue-box-controller.js — contrat partagé par tous les modèles
"boîte") — un ::before CSS pur, jamais touché par ce .textContent,
pour reproduire le "Quête : X" de la maquette validée. */
.quizBoxWidget--mangaDialogue .quizBoxHeader::before{ content:"Quête : "; }
.quizBoxWidget--mangaDialogue .quizBoxTopBar{ margin-top:6px; }
.quizBoxWidget--mangaDialogue .quizBoxScoreDisplay,
.quizBoxWidget--mangaDialogue .quizBoxTimerDisplay{ background:rgba(0,0,0,.28); color:#fff; border-radius:2px; }
/* Corps CRÈME (papier), comme la maquette — jamais la couleur
configurable du panneau "🎨 Style" (body_bg, posée en style INLINE) :
!important nécessaire pour la dominer, même raison que le bandeau
ci-dessus. padding-bottom réservé pour ne jamais passer SOUS le
bandeau de pied de page toujours visible (voir
.quizMangaDialogueFooterBand plus bas).
Coin dentelé du bas (demande explicite) SANS clip-path ni décor à
part : un `background` (dégradé quasi-plat, juste 3° d'inclinaison,
qui BASCULE net de crème à encre) — deux essais précédents (clip-path
sur .quizBoxBody, puis un décor z-index:-1 séparé) ont chacun rogné
ou fait disparaître le bouton "Continuer →" du moteur partagé
(DESCENDANT de cet élément, positionné en absolute par rapport au
widget) : un clip-path/overflow sur un ANCÊTRE le rogne TOUJOURS,
quel que soit son propre positionnement ; un contexte d'empilement
(position+z-index) posé ici l'emprisonne aussi, l'empêchant de
ressortir au-dessus du bandeau bas. Un simple `background` ne clippe
ni n'empile RIEN — le bouton reste entièrement libre. */
.quizBoxWidget--mangaDialogue .quizBoxBody{
/* Fond CRÈME plat (demande explicite : coins bas arrondis) — le
dégradé diagonal précédent aurait rendu l'arrondi invisible : ses
18% du bas étaient DÉJÀ encre, un coin arrondi encre sur un fond
encre (le bandeau du bas) ne se voit tout simplement pas, aucun
contraste. Coin plat + arrondi net = le seul réglage qui reste
visible. */
background:#f3ead6 !important;
color:#151316; padding:16px 18px 78px; overflow:visible;
/* Un `border-radius` seul, SANS toucher à overflow (reste visible),
ne clippe RIEN : contrairement à un clip-path ou un
overflow:hidden, il ne rogne le bouton "Continuer →" (descendant
qui doit rester libre de déborder, voir le commentaire au-dessus)
que si overflow est masqué — jamais le cas ici. */
border-bottom-left-radius:16px; border-bottom-right-radius:16px;
}
.quizBoxWidget--mangaDialogue .quizBoxChoices{ overflow:visible; }
.quizBoxWidget--mangaDialogue .quizBoxQuestionText{ font-family:"Montserrat", sans-serif; font-weight:700; color:#151316; }
.quizBoxWidget--mangaDialogue .quizBoxChoices{ gap:8px; counter-reset:mangaDialogueChoice; }
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn{
counter-increment:mangaDialogueChoice; position:relative; background:#fff; color:#151316;
border:2px solid #000; border-radius:0; padding:8px 10px 8px 38px; font-family:inherit;
}
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn::before{
content:counter(mangaDialogueChoice, upper-alpha); position:absolute; left:8px; top:50%; transform:translateY(-50%);
width:22px; height:22px; border-radius:50%; background:#151316; color:#f3ead6;
display:flex; align-items:center; justify-content:center;
font-family:"Montserrat", sans-serif; font-weight:700; font-size:11px;
}
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn:hover:not(:disabled){ background:#fff7e6; }
/* border-color:#000 !important nécessaire : le contrat partagé
(static/style.css, règle générique .quizBoxChoiceBtn.is-correct/
is-wrong) pose déjà un liseré vert/rouge "!important" — sans ce
repli, il resterait visible par-dessus le fond gold/rouge-foncé
propre à ce modèle (demande explicite : "il n'y a pas le liseré
vert" sur la maquette — bordure NOIRE partout, jamais colorée). */
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn.is-correct{ background:#e7b923 !important; border-color:#000 !important; color:#151316; }
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn.is-wrong{ background:#8f0f24 !important; border-color:#000 !important; color:#fff; text-decoration:line-through; }
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn.is-correct::after,
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn.is-wrong::after{
position:absolute; right:10px; top:50%; transform:translateY(-50%);
font-family:"Montserrat", sans-serif; font-weight:700; font-size:10px;
padding:2px 6px; border:1.5px solid currentColor; border-radius:3px;
}
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn.is-correct::after{ content:"正解"; }
.quizBoxWidget--mangaDialogue .quizBoxChoiceBtn.is-wrong::after{ content:"不正解"; }
/* Bandeau de pied de page TOUJOURS visible (voir
quizMangaDialogueFooterBand, render_quiz_box_manga_dialogue) — comme
la maquette validée : une bande sombre distincte, jamais la même
couleur que le corps crème. */
.quizBoxWidget--mangaDialogue .quizMangaDialogueFooterBand{
/* 44px auparavant : trop bas pour le bouton (padding 8px×2 + texte +
bordure 2px×2 ≈ 40px) et son décalage bottom:8px — il débordait par
le HAUT, à moitié caché sous le panneau crème (demande explicite :
"ou dois etre le bouton"). 72px laisse une vraie marge de respir,
comme la maquette. */
position:absolute; left:0; right:0; bottom:0; height:72px;
background:#151316; pointer-events:none;
/* Coin haut-gauche dentelé (demande explicite : "les bords du bas
doivent être retravaillés") — écho du bandeau titre découpé en
haut, plutôt qu'un bas de carte parfaitement rectangulaire. Même
technique drop-shadow que le bandeau titre (un `border` classique
ne suit pas un clip-path) pour un liseré qui épouse la VRAIE
silhouette découpée. */
clip-path:polygon(4% 0, 100% 0, 100% 100%, 0 100%, 0 4%);
filter:drop-shadow(0 -2px 0 #000) drop-shadow(-2px 0 0 #000) drop-shadow(2px 0 0 #000) drop-shadow(0 2px 0 #000);
}
/* Le bouton "Continuer →" du moteur partagé atterrit en DERNIER enfant
de .quizBoxChoices (jamais dans quizMangaDialogueFooterBand, que le
moteur ne remplit jamais) — posé PAR-DESSUS ce bandeau en
position:absolute (.quizBoxWidget--mangaDialogue est position:relative,
voir plus haut), petit et skewé aligné à droite comme la maquette
(sans ce repli, il s'étire à la largeur des choix — .quizBoxChoices
est un flex-column, stretch par défaut). */
.quizBoxWidget--mangaDialogue .quizBoxContinueBtn{
position:absolute; right:20px; bottom:20px; z-index:1; margin:0;
background:#d81e3e; border:2px solid #000; color:#fff2ec; border-radius:0;
padding:8px 20px; transform:skewX(-6deg); font-family:"Montserrat", sans-serif; font-weight:700;
box-shadow:3px 3px 0 #000;
}
.quizBoxWidget--mangaDialogue .quizBoxContinueBtn:hover:not(:disabled){ background:#8f0f24; }
/* ---------- Modèle "Manga" — premier "PAGE DE QUIZ" (voir
screens/rendering/quiz_box_templates.py::render_quiz_box_page_manga),
validé par le créateur via une maquette interactive avant intégration.
Thème complet et autonome : structure/police/formes/couleurs propres,
AUCUNE règle partagée avec les modèles "boîte de dialogue" ci-dessus
(à part .quizBoxChoiceBtn/.quizBoxContinueBtn, injectés par le moteur
JS partagé, voir static/js/play/dialogue-box-controller.js).
Polices : Montserrat/Poppins (déjà auto-hébergées, voir
static/vendor/fonts/fonts.css) plutôt que Bangers/Noto Sans JP de la
maquette d'origine — un paquet SCORM exporté tourne SANS accès
internet dans le LMS du client, une police chargée depuis Google
Fonts y resterait invisible (repli silencieux sur la police système). */
.quizBoxWidget--manga{
background:#151316; color:#f3ead6;
font-family:"Poppins", "Montserrat", system-ui, sans-serif;
display:flex; flex-direction:column; align-items:center; gap:18px; padding:20px;
overflow-y:auto;
background-image:
radial-gradient(circle at 14% 8%, rgba(216,30,62,.16), transparent 42%),
repeating-linear-gradient(115deg, rgba(255,255,255,.03) 0 2px, transparent 2px 26px);
}
/* Contenu centré en colonne confortable (demande explicite, comme la
maquette validée) plutôt qu'étiré sur toute la largeur de l'écran. */
.quizBoxWidget--manga .mangaTopbar,
.quizBoxWidget--manga .mangaProgress,
.quizBoxWidget--manga .mangaPanelQuestion,
.quizBoxWidget--manga .mangaChoices{ width:100%; max-width:860px; flex-shrink:0; }
/* Barre d'étapes (voir forgeQuizMangaOnShow, static/js/play/
quiz-box-templates.js) — un <i> par question de CE dialogue, remplie
EN JEU (le nombre total de questions n'est connu qu'à l'exécution). */
.quizBoxWidget--manga .mangaProgress{ display:flex; gap:6px; }
.quizBoxWidget--manga .mangaProgress i{ flex:1; height:6px; background:rgba(243,234,214,.18); border-radius:1px; font-style:normal; }
.quizBoxWidget--manga .mangaProgress i.is-done{ background:#d81e3e; }
.quizBoxWidget--manga .mangaProgress i.is-current{ background:#e7b923; }
.quizBoxWidget--manga .mangaTopbar{ display:flex; align-items:stretch; gap:12px; flex-wrap:wrap; }
.quizBoxWidget--manga .mangaPlate{
background:#f3ead6; color:#151316; border:3px solid #151316; box-shadow:4px 4px 0 #1d1a1f;
padding:8px 14px; display:flex; flex-direction:column; justify-content:center; gap:2px; min-width:118px;
}
.quizBoxWidget--manga .mangaPlateLabel{ font-size:10px; font-weight:600; letter-spacing:.14em; text-transform:uppercase; opacity:.65; }
.quizBoxWidget--manga .mangaPlateValue{
font-family:"Montserrat", sans-serif; font-weight:700; font-size:22px; line-height:1;
letter-spacing:.02em; font-variant-numeric:tabular-nums;
}
.quizBoxWidget--manga .mangaPlate--score .mangaPlateValue{ color:#8f0f24; }
.quizBoxWidget--manga .mangaPlate--timer:has(.quizBoxTimerDisplay.is-urgent){
background:#d81e3e; color:#f3ead6; animation:mangaTimerPulse .5s ease infinite;
}
.quizBoxWidget--manga .mangaPlate--timer:has(.quizBoxTimerDisplay.is-urgent) .mangaPlateValue{ color:#f3ead6; }
@keyframes mangaTimerPulse{ 0%,100%{ transform:scale(1); } 50%{ transform:scale(1.05); } }
.quizBoxWidget--manga .mangaTitlemark{
flex:1 1 200px; min-width:180px; background:#1d1a1f; border:3px solid #151316; outline:3px solid #f3ead6; outline-offset:-8px;
display:flex; align-items:center; justify-content:center; padding:10px;
background-image:radial-gradient(rgba(243,234,214,.14) 1.3px, transparent 1.5px); background-size:8px 8px;
}
.quizBoxWidget--manga .mangaTitlemark span{
font-family:"Montserrat", sans-serif; font-weight:700; font-style:italic; text-transform:uppercase;
font-size:clamp(16px,3vw,24px); letter-spacing:.02em; color:#f3ead6;
-webkit-text-stroke:1px #f3ead6; text-shadow:2px 2px 0 #d81e3e; transform:skewX(-8deg); display:inline-block;
}
.quizBoxWidget--manga .mangaPanelQuestion{
background:#f3ead6; color:#151316; border:3px solid #151316; box-shadow:6px 6px 0 #1d1a1f;
clip-path:polygon(0 0, 100% 0%, 100% 92%, 97% 100%, 3% 100%, 0 88%);
padding:22px 26px 30px; flex:0 0 auto;
}
.quizBoxWidget--manga .mangaKicker{
display:inline-block; font-size:11px; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
background:#d81e3e; color:#f3ead6; padding:3px 10px; margin-bottom:12px; transform:rotate(-2deg);
}
.quizBoxWidget--manga .mangaQuestionText{
margin:0; font-family:"Montserrat", sans-serif; font-weight:700; font-style:italic; line-height:1.2;
font-size:clamp(19px,2.6vw,28px); letter-spacing:-.01em; text-transform:uppercase; text-wrap:balance;
}
.quizBoxWidget--manga .mangaChoices{
display:grid; grid-template-columns:1fr 1fr; gap:14px; flex:1 1 auto; align-content:start;
}
@media (max-width:620px){ .quizBoxWidget--manga .mangaChoices{ grid-template-columns:1fr; } }
.quizBoxWidget--manga .quizBoxChoiceBtn{
counter-increment:mangaChoice; position:relative; display:flex; align-items:center; gap:14px;
background:#1d1a1f; color:#f3ead6; border:3px solid #151316; outline:2px solid rgba(243,234,214,.5); outline-offset:-7px;
padding:12px 16px 12px 12px; font-family:inherit; font-size:15px; font-weight:500; text-align:left;
border-radius:0; box-shadow:none;
}
.quizBoxWidget--manga .mangaChoices{ counter-reset:mangaChoice; }
.quizBoxWidget--manga .quizBoxChoiceBtn::before{
content:counter(mangaChoice, upper-alpha); flex:0 0 auto; width:34px; height:34px; border-radius:50%;
background:#f3ead6; color:#151316; border:3px solid #151316; display:flex; align-items:center; justify-content:center;
font-family:"Montserrat", sans-serif; font-weight:700; font-size:15px;
}
.quizBoxWidget--manga .quizBoxChoiceBtn:hover:not(:disabled){ background:#26222a; transform:translate(-2px,-2px); }
.quizBoxWidget--manga .quizBoxChoiceBtn.is-correct{ background:#e7b923 !important; color:#151316; border-color:#151316 !important; }
.quizBoxWidget--manga .quizBoxChoiceBtn.is-correct::before{ background:#151316; color:#e7b923; border-color:#151316; }
.quizBoxWidget--manga .quizBoxChoiceBtn.is-wrong{ background:#8f0f24 !important; border-color:#151316 !important; }
.quizBoxWidget--manga .quizBoxChoiceBtn.is-correct::after,
.quizBoxWidget--manga .quizBoxChoiceBtn.is-wrong::after{
position:absolute; top:50%; right:14px; transform:translateY(-50%) rotate(-8deg);
font-family:"Montserrat", sans-serif; font-weight:700; font-size:13px; letter-spacing:.06em;
padding:2px 8px; border:2px solid currentColor; border-radius:4px;
}
.quizBoxWidget--manga .quizBoxChoiceBtn.is-correct::after{ content:"正解"; color:#151316; }
.quizBoxWidget--manga .quizBoxChoiceBtn.is-wrong::after{ content:"不正解"; color:#f3ead6; }
/* Une réponse fausse est barrée (demande explicite, comme la maquette) —
jamais le badge lettré (::before), qui reste un simple repère A/B/C/D. */
.quizBoxWidget--manga .quizBoxChoiceBtn.is-wrong{ text-decoration:line-through; text-decoration-thickness:2px; }
.quizBoxWidget--manga .quizBoxChoiceBtn.is-wrong::before{ text-decoration:none; }
/* Posé DANS la grille des choix (voir forgeQuizBoxAnswer, moteur partagé)
mais sur sa propre ligne, aligné à droite en petit pavé (comme la
maquette) plutôt qu'étiré à la largeur d'une case de la grille — et
relabellisé en "Suite ▶" (texte "Continuer →" du moteur partagé
conservé pour les lecteurs d'écran, juste masqué visuellement). */
.quizBoxWidget--manga .quizBoxContinueBtn{
grid-column:1 / -1; justify-self:end; width:max-content;
font-family:"Montserrat", sans-serif; font-weight:700; letter-spacing:.03em; font-size:0;
background:#d81e3e; color:#f3ead6; border:3px solid #151316; box-shadow:3px 3px 0 #1d1a1f;
padding:9px 22px; transform:skewX(-6deg); border-radius:0;
}
.quizBoxWidget--manga .quizBoxContinueBtn::after{ content:"Suite ▶"; font-size:14px; }
.quizBoxWidget--manga .quizBoxContinueBtn:hover:not(:disabled){ background:#8f0f24; }
/* Écran de résultat de fin de quiz (voir forgeQuizMangaOnQuizComplete,
static/js/play/quiz-box-templates.js) — demande explicite : "il faut
aussi adapter le modele manga pour avoir sa page de résultat", même
mécanique que "classique" mais dans le thème manga propre à ce
modèle. Pas de conteneur "content" à part ici : c'est la RACINE
(.quizBoxWidget--manga) qui bascule en "is-showing-result" — le
bandeau topbar/score et la barre d'étapes (déjà remplie à 100%,
voir le JS) restent visibles, seuls le panneau question et les choix
disparaissent. */
.quizBoxWidget--manga.is-showing-result > .mangaPanelQuestion,
.quizBoxWidget--manga.is-showing-result > .mangaChoices{ display:none; }
.mangaResultZone{
display:none; flex-direction:column; align-items:center; justify-content:center;
width:100%; max-width:860px; text-align:center; flex:1 1 auto;
}
.quizBoxWidget--manga.is-showing-result .mangaResultZone{ display:flex; }
.mangaResultCard{
background:#f3ead6; color:#151316; border:3px solid #151316; box-shadow:6px 6px 0 #1d1a1f;
clip-path:polygon(0 0, 100% 0%, 100% 92%, 97% 100%, 3% 100%, 0 88%);
padding:32px 30px 40px; width:100%;
}
.mangaResultTitle{
font-family:"Montserrat", sans-serif; font-weight:700; font-style:italic; text-transform:uppercase;
font-size:clamp(26px,4vw,38px); text-shadow:3px 3px 0 #d81e3e; transform:skewX(-4deg);
display:inline-block; margin:0 0 10px;
}
.mangaResultScore{ font-family:"Montserrat", sans-serif; font-weight:700; font-size:19px; color:#8f0f24; margin:0 0 18px; }
.mangaResultBadge{
display:inline-block; font-family:"Montserrat", sans-serif; font-weight:700; font-size:22px;
letter-spacing:.05em; padding:6px 22px; border:3px solid #151316; margin-bottom:24px; transform:rotate(-2deg);
}
.mangaResultBadge.pass{ background:#e7b923; }
.mangaResultBadge.fail{ background:#8f0f24; color:#f3ead6; }
.mangaQuitBtn{
font-family:"Montserrat", sans-serif; font-weight:700; letter-spacing:.03em; cursor:pointer;
background:#151316; color:#f3ead6; border:3px solid #151316; box-shadow:3px 3px 0 #d81e3e;
padding:10px 26px; transform:skewX(-6deg); border-radius:0;
}
.mangaQuitBtn:hover{ background:#26222a; }
/* "Classique" (modèle "page de quiz" DE BASE) — maquette interactive
validée par le créateur (formation sécurité incendie comme exemple) :
couleurs VOLONTAIREMENT identiques à celles par défaut du modèle
"boîte de dialogue" de base (#2f6fed/#1a1d24/#11141b, voir
screens/rendering/dialogue_box_style.py::DEFAULT_DIALOGUE_BOX_STYLE)
plutôt qu'une palette propre — contrairement à "manga". */
/* Police : "Poppins" (déjà auto-hébergée pour "manga", voir
static/vendor/fonts/fonts.css) — jamais "IBM Plex Sans/Mono" de la
maquette (Google Fonts), qui casserait l'export SCORM hors-ligne, même
contrainte déjà rencontrée pour "manga". */
.quizBoxWidget--classique{
position:relative; display:flex; flex-direction:column; font-family:"Poppins", system-ui, sans-serif;
}
.classiqueHeaderBar{
display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap;
padding:14px 20px; color:#fff;
}
.classiqueHeaderTitleWrap{ display:flex; flex-direction:column; gap:1px; min-width:0; }
.classiqueHeaderEyebrow{ font-size:10px; font-weight:600; letter-spacing:.09em; text-transform:uppercase; color:rgba(255,255,255,.7); }
.classiqueHeaderTitle{ font-size:16px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.classiqueHeaderChips{ display:flex; gap:8px; flex-shrink:0; }
.classiqueChip{
display:flex; flex-direction:column; align-items:center; justify-content:center;
background:rgba(255,255,255,.14); border-radius:8px; padding:4px 12px; min-width:56px;
}
.classiqueChipLabel{ font-size:9px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:rgba(255,255,255,.65); }
.classiqueChipValue{
font-family:"Roboto", monospace; font-size:13.5px; font-weight:600; color:#fff;
font-variant-numeric:tabular-nums;
}
/* Bug identifié (demande répétée : "le mot aucun a une background
color... un bord arrondi") : [data-quiz-role="timer"] porte AUSSI la
classe partagée .quizBoxTimerDisplay (voir plus haut dans ce fichier —
nécessaire pour le mécanisme commun ":empty"/".is-urgent") qui pose
SA PROPRE pastille (fond + border-radius:999px) — DEUX pastilles
s'empilaient donc : celle, correcte et voulue, du conteneur
".classiqueChip" (même fond que "Score"), et celle, parasite, de
".quizBoxTimerDisplay" collée UNIQUEMENT autour du mot "Aucun"/du
temps restant. On neutralise ICI cette seconde pastille (le
conteneur ".classiqueChip" reste seul responsable du fond, TOUJOURS
visible, identique que pour "Score", avec ou sans minuteur réglé). */
.classiqueChipValue.quizBoxTimerDisplay{ background:none; padding:0; border-radius:0; }
.classiqueChip--timer:has(.quizBoxTimerDisplay.is-urgent){ background:rgba(224,82,77,.35); }
.classiqueChip--timer:has(.quizBoxTimerDisplay.is-urgent) .classiqueChipValue,
.classiqueChip--timer:has(.quizBoxTimerDisplay.is-urgent) .classiqueChipLabel{ color:#ffb3b0; }
.classiqueBody{ flex:1; display:flex; flex-direction:column; align-items:center; padding:24px 20px 96px; overflow-y:auto; }
.classiqueContent{ width:100%; max-width:640px; display:flex; flex-direction:column; flex:1; }
/* Barre d'étapes — voir forgeQuizClassiqueOnShow (static/js/play/
quiz-box-templates.js), même mécanique que "manga". */
.classiqueProgress{ display:flex; gap:6px; margin-bottom:24px; }
.classiqueProgress i{ flex:1; height:4px; border-radius:2px; background:rgba(255,255,255,.14); font-style:normal; }
.classiqueProgress i.is-done{ background:#2ecc71; }
.classiqueProgress i.is-current{ background:#fff; }
.classiqueKicker{
display:inline-block; align-self:flex-start; background:rgba(255,255,255,.14); border-radius:999px;
padding:4px 12px; font-size:11.5px; font-weight:600; color:rgba(255,255,255,.75); margin-bottom:16px;
}
.classiqueQuestionText{ margin:0 0 22px; font-weight:600; line-height:1.35; font-size:clamp(18px,2.6vw,22px); text-wrap:balance; }
.classiqueChoices{ display:flex; flex-direction:column; gap:10px; }
.classiqueChoices .quizBoxChoiceBtn{
display:flex; align-items:center; gap:14px; border-radius:8px; padding:13px 16px;
}
.classiqueChoices .quizBoxChoiceBtn.is-correct{ color:#08210f; }
.classiqueChoices .quizBoxChoiceBtn.is-wrong{ text-decoration:line-through; opacity:.85; }
/* Bandeau de pied de page TOUJOURS visible (voir classiqueFooterBand,
render_quiz_box_page_classique) — couleur configurable "🎨 Style"
(footer_bg, voir --classique-footer-bg posée sur la racine du widget)
plutôt qu'une couleur figée en dur, demande explicite : "il manque le
footer foncé avec le bouton" (comme la maquette validée). */
.classiqueFooterBand{
position:absolute; left:0; right:0; bottom:0; height:66px;
background:var(--classique-footer-bg, #11141b); border-top:1px solid rgba(255,255,255,.08);
pointer-events:none;
}
/* Le bouton "Continuer →" du moteur partagé atterrit en DERNIER enfant
de .classiqueChoices (jamais dans classiqueFooterBand, que le moteur
ne remplit jamais) — posé PAR-DESSUS ce bandeau en position:absolute
(.quizBoxWidget--classique est position:relative, voir plus haut).
Demande explicite : aligné à DROITE — dans la même colonne centrée de
640px que le corps (.classiqueContent) : `max(20px, calc(50vw - 320px))`
recalcule le bord DROIT de CETTE colonne (640px = 2×320px, 20px = le
padding latéral de .classiqueBody) plutôt que le bord droit de TOUT le
widget. 50vw est fiable ici : ce modèle n'existe qu'en plein écran, où
`.is-fullscreen` force déjà `width:100vw`. */
.classiqueChoices .quizBoxContinueBtn{
position:absolute; right:max(20px, calc(50vw - 320px)); bottom:14px; z-index:1; margin:0;
background:#2f6fed; border:1px solid #2f6fed; border-radius:7px; padding:10px 22px; font-weight:600;
}
.classiqueChoices .quizBoxContinueBtn:hover:not(:disabled){ background:#255bc4; }
/* Encart d'explication après réponse — RÉSERVÉ pour une fonctionnalité
FUTURE non encore implémentée (voir le commentaire dans
render_quiz_box_page_classique, screens/rendering/quiz_box_templates.py)
: reste toujours vide aujourd'hui, donc toujours caché — seul :not(:empty)
le fera apparaître le jour où un vrai contenu y sera écrit. */
.classiqueExplain:not(:empty){
margin-top:14px; padding:14px 16px; border-radius:8px; background:rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.14); font-size:13.5px; color:rgba(255,255,255,.75); line-height:1.5;
}
/* Écran de résultat de fin de quiz (voir forgeQuizClassiqueOnQuizComplete/
forgeQuizResultRestart, static/js/play/quiz-box-templates.js et
dialogue-box-controller.js) — demande explicite : "la page de resultat
est cassee... elle affiche un conteneur qui reaffiche le contenu du
quiz au lieu du resultat" (le vrai moteur n'avait jusqu'ici AUCUN écran
de fin, juste la maquette artifact). ".classiqueContent.is-showing-
result" masque tout le reste (progression/question/choix/explication)
pour ne laisser QUE ce bloc visible, sans reconstruire le DOM du
widget (le contrat data-quiz-role="header/question/choices/score/
timer" reste intact pour un "Recommencer" qui relance normalement). */
.classiqueContent.is-showing-result > *{ display:none; }
.classiqueContent.is-showing-result .classiqueResultZone{ display:flex; }
.classiqueResultZone{
display:none; flex:1; flex-direction:column; align-items:center; justify-content:center;
text-align:center; padding:20px 0;
}
.classiqueResultRing{
width:112px; height:112px; margin:0 0 20px; border-radius:50%;
display:flex; align-items:center; justify-content:center; position:relative;
background:conic-gradient(#2f6fed calc(var(--pct, 0) * 1%), rgba(255,255,255,.14) 0);
}
.classiqueResultRing::before{
content:""; position:absolute; inset:8px; border-radius:50%; background:var(--classique-body-bg, #1a1d24);
}
.classiqueResultRing span{ position:relative; font-weight:700; font-size:22px; font-variant-numeric:tabular-nums; }
.classiqueResultTitle{ font-size:19px; font-weight:700; margin:0 0 6px; }
.classiqueResultSub{ font-size:13.5px; color:rgba(255,255,255,.62); margin:0 0 22px; }
.classiqueResultBadge{
display:inline-flex; align-items:center; gap:6px; padding:6px 16px; border-radius:999px;
font-size:12.5px; font-weight:700; margin-bottom:26px;
}
.classiqueResultBadge.pass{ background:rgba(46,204,113,.16); color:#2ecc71; }
.classiqueResultBadge.fail{ background:rgba(224,82,77,.16); color:#e0524d; }
.classiqueQuitBtn{
font:inherit; font-weight:600; font-size:13.5px; color:#fff; background:transparent;
border:1px solid rgba(255,255,255,.3); border-radius:9px; padding:10px 20px; cursor:pointer;
}
.classiqueQuitBtn:hover{ border-color:#2f6fed; background:rgba(255,255,255,.08); }
/* Widget "🏆 Score" — TOUJOURS visible une fois posé (pas de collision/
déclencheur, contrairement aux deux widgets ci-dessus), voir