Files
Forge-Engine/styles/forge-custom.css
T
williamandClaude Sonnet 5 c60884ec46 Corrige une régression fonctionnelle : revient à Bulma 1.0.2 (plus de Sass)
L'utilisateur a signalé des bugs réels en jeu après la refonte design
system : jauges qui ne se remplissent plus, conditions de visibilité
cassées sur une carte (enveloppe ouverte/fermée affichées en même temps).
Racine du problème : Phase A avait basculé Bulma de 1.0.2 (CDN d'origine)
vers 0.9.4, seule version compatible avec libsass (1.0.2 utilise le
système de modules @use/@forward que libsass ne sait pas compiler). Or
screens/rendering/render_jauge.py pilote le remplissage d'une jauge en
fixant en ligne --bulma-progress-value-background-color — une variable
CSS qui n'existe QUE dans le nouveau système de theming de Bulma 1.x,
absente de 0.9.4. Aucune perte de données : les champs d'objet étaient
toujours intacts en base (vérifié directement sur projects/test/game.db)
— uniquement un problème de rendu/comportement en jeu.

Correction : revient à Bulma 1.0.2, vendoré tel quel et non modifié
(static/vendor/bulma.min.css, ~677 Ko, auto-hébergé — toujours aucune
dépendance CDN). Bulma 1.x expose déjà tout son thème via de vraies
variables CSS (--bulma-primary-h/-s/-l, --bulma-radius...), justement
conçues pour être surchargées après coup SANS recompilation Sass —
styles/bulma-override.css les redéfinit avec la palette Forge (teintes
HSL calculées à partir des couleurs de la charte). build_css.py devient
un simple concaténage de 4 fichiers (bulma.min.css + bulma-override.css
+ forge-tokens.css + forge-custom.css), plus besoin de libsass ni
d'aucun compilateur — supprimé de requirements.txt. styles/bulma/ (source
Sass 0.9.4 vendorée en Phase A) et styles/forge-theme.scss supprimés.

Nouvelle règle ajoutée en commentaire dans bulma-override.css : ne plus
jamais changer de version de Bulma sans `grep -rn "\-\-bulma-" screens/
templates/` d'abord — cette dépendance n'est pas que visuelle.

197 tests toujours verts (ils ne couvrent que le HTML généré, jamais le
rendu réel — c'est pour ça que cette régression n'avait pas été détectée
avant que l'utilisateur ne la signale en jouant pour de vrai).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 08:40:16 +02:00

618 lines
36 KiB
CSS

/* Tokens (--bg, --panel, --accent...) définis une seule fois dans
forge-theme.scss, qui @importe ce fichier APRÈS Bulma — ce fichier
continue de référencer les mêmes noms de variable qu'avant la charte
Forge (voir les alias --bg/--panel/--panel2/--border/--text/--text-dim/
--danger dans forge-theme.scss), pour n'avoir à toucher qu'aux VALEURS
des tokens, jamais aux ~600 lignes de règles ci-dessous qui les
consomment déjà. */
*{ box-sizing:border-box; }
body{ margin:0; font-family:system-ui,-apple-system,"Segoe UI",sans-serif; background:var(--bg); color:var(--text); }
h1{ font-size:20px; margin:0 0 6px; }
h2{ font-size:13px; text-transform:uppercase; letter-spacing:.05em; color:var(--text-dim); margin:24px 0 8px; }
h3{ font-size:13px; color:var(--accent); margin:14px 0 6px; }
label{ display:block; font-size:12px; color:var(--text-dim); margin:8px 0 4px; }
input[type=text], input[type=number], input[type=date], textarea, select{
width:100%; background:var(--panel2); border:1px solid var(--border); border-radius:6px;
color:var(--text); padding:8px 10px; font-size:13px; font-family:inherit;
}
input[type=checkbox]{ width:auto; margin-top:6px; }
/* .topbar : Bulma fournit déjà la mise en page (navbar) — cette classe ne
sert plus qu'à la repérer dans les layouts plein-écran
(body.builderBody/objectEditBody, voir plus bas) et à la rattacher au
thème sombre. */
.topbar{ border-bottom:1px solid var(--border); }
/* Barre de navigation d'UN JEU (voir base.html) — toujours visible entre
les différents éditeurs d'un même jeu, en ligne (contrairement à
.navList/.navRow ci-dessous, empilés verticalement dans le panneau
"Créer" du tableau de bord). */
.gameNavBar{ display:flex; flex-wrap:wrap; justify-content:center; gap:6px; padding:8px 20px; background:transparent; border-bottom:1px solid var(--border); }
.gameNavLink{ display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border:1px solid var(--border); border-radius:6px; background:var(--panel); text-decoration:none; color:var(--text); font-size:12.5px; }
.gameNavLink:hover{ background:var(--panel2); border-color:var(--accent); }
.gameNavLink.active{ background:var(--panel2); border-color:var(--accent); color:var(--accent); }
.content{ max-width:760px; margin:0 auto; padding:28px 20px; }
.card{ border:1px solid var(--border); border-radius:10px; padding:16px; background:var(--panel); margin:12px 0; }
.inline-form .fieldRow{ display:flex; gap:8px; align-items:flex-end; }
.inline-form .fieldRow input{ flex:1; }
.fieldRow{ display:flex; gap:10px; }
.fieldRow > div{ flex:1; }
.formField{ margin-bottom:6px; }
button{ background:var(--panel2); border:1px solid var(--border); color:var(--text); border-radius:10px; padding:9px 14px; cursor:pointer; font-size:13px; }
/* Un <button> qui doit se comporter comme un <a> visuellement (ex. ouvrir
un panneau plutôt que suivre un href, mais garder l'apparence d'un lien
dans un tableau) — annule le style bouton par défaut ci-dessus. */
.linkLikeButton{ background:none; border:none; padding:0; color:var(--accent); text-decoration:none; font-size:inherit; }
.linkLikeButton:hover{ text-decoration:underline; }
/* button.primary : couleur/dégradé posés une seule fois, centralement,
dans forge-theme.scss (".button.is-primary, button.primary") — pas ici,
pour ne pas entrer en conflit de cascade avec cette règle centrale. */
button.smallBtn{ padding:5px 10px; font-size:11.5px; }
button.danger{ color:var(--danger); border-color:rgba(240,68,56,.4); }
.actionsRow{ margin-top:16px; }
/* Nav compacte à une seule ligne (icône + libellé) — pour une liste
d'actions fixe (ex. colonne "Créer" du tableau de bord d'un jeu) : sobre
et dense plutôt qu'une carte à deux lignes. */
.navList{ display:flex; flex-direction:column; gap:3px; }
.navRow{ display:flex; align-items:center; gap:8px; padding:7px 10px; border:1px solid var(--border); border-radius:6px; background:var(--panel); text-decoration:none; color:var(--text); font-size:12.5px; }
.navRow:hover{ background:var(--panel2); border-color:var(--accent); }
.hint{ font-size:11.5px; color:var(--text-dim); line-height:1.5; }
.autosaveStatus{ display:inline-flex; align-items:center; gap:6px; transition:color .2s ease; }
.autosaveStatus.saving{ color:var(--text-dim); }
.autosaveStatus.saved{ color:var(--status-success); }
.warningBanner{ border:1px solid var(--danger); background:rgba(240,68,56,.1); color:var(--text);
border-radius:8px; padding:10px 14px; font-size:12.5px; margin:12px 0; }
.dangerZone{ border:1px solid rgba(240,68,56,.4); border-radius:8px; padding:10px 12px; margin-top:14px; }
.dangerZone h3{ color:var(--danger); margin-top:0; }
.fieldsExistingRow{ display:flex; align-items:center; justify-content:space-between;
padding:8px 10px; border:1px solid var(--border); border-radius:6px; margin-bottom:6px; font-size:12.5px; }
.fieldsExistingRow form{ margin:0; }
.fieldRowCard{ margin-bottom:10px; }
.relationTarget{ margin-top:6px; }
/* Liste de conditions de "Donnée liée" (voir c_clause_list.py) : un nombre
ILLIMITÉ de lignes peut s'y accumuler — sans limite de hauteur, le
panneau de propriétés entier deviendrait interminable à faire défiler
dès qu'on en pose plus de 3-4. max-height + overflow-y borne cette
LISTE elle-même à une hauteur confortable, le reste du panneau (et le
bouton "+ Ajouter une condition" juste en dessous) reste toujours à
portée sans avoir à chercher le bas de la liste. */
.clauseListWrap{ max-height:260px; overflow-y:auto; padding-right:2px; }
.clauseRow{ margin-bottom:8px; }
.clauseRow:last-child{ margin-bottom:0; }
/* .dataTable : Bulma fournit déjà la mise en page du tableau (classes
"table is-fullwidth is-hoverable", voir data_list.html) — ceci ne fait
plus que tronquer les cellules trop longues avec "...", pour garder une
vue d'ensemble compacte (le détail complet s'ouvre dans la modale Bulma
ci-dessous via le bouton 👁️). */
.dataTable td{ max-width:220px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* ---------- Détail d'une entrée (bouton 👁️) ---------- */
/* Contenu du panneau de détail (voir openRowDetailPanel() dans
game_dashboard.html) — affiche chaque champ en entier, sans troncature,
contrairement au tableau dense qui les tronque. */
.rowDetailField{ padding:10px 0; border-bottom:1px solid var(--border); }
.rowDetailField:last-child{ border-bottom:none; }
.rowDetailLabel{ font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.05em; margin-bottom:4px; }
.rowDetailValue{ font-size:13.5px; white-space:pre-wrap; word-break:break-word; }
.content-wide{ max-width:1600px; }
.twoCol{ display:flex; gap:24px; align-items:flex-start; margin-top:10px; }
.twoCol .colLeft{ flex:0 0 340px; }
.twoCol .colRight{ flex:1; min-width:0; }
@media (max-width: 820px){
.twoCol{ flex-direction:column; }
.twoCol .colLeft{ flex:none; width:100%; }
/* En dessous de 820px, les deux colonnes s'empilent : le défilement
interne séparé par colonne n'a plus de sens (il n'y a plus qu'une
colonne) — on revient à une page classique qui défile normalement,
comme pour l'éditeur d'écran ci-dessous. */
body.objectEditBody{ height:auto; overflow:visible; }
body.objectEditBody > main.content{ overflow:visible; }
.content-objectEdit .colLeft, .content-objectEdit .colRight{ height:auto; overflow:visible; }
.fieldsTableWrap{ max-height:60vh; }
}
/* ---------- Modifier un objet : tableau des champs existants ---------- */
/* Comme l'éditeur d'écran (body.builderBody, plus bas) : la page ne défile
jamais elle-même, seules les zones internes (colonne de gauche, tableau
des champs) défilent chacune de leur côté — indispensable dès qu'un objet
a beaucoup de champs, pour garder une vue d'ensemble sans perdre le
formulaire "Ajouter un champ" hors de l'écran. */
body.objectEditBody{ display:flex; flex-direction:column; height:100vh; margin:0; overflow:hidden; }
body.objectEditBody > #pageChrome{ flex:0 0 auto; }
body.objectEditBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; display:flex; flex-direction:column; }
.content-objectEdit{ width:100%; display:flex; flex-direction:column; min-height:0; }
.content-objectEdit > h1, .content-objectEdit > .dashPanelHeader, .content-objectEdit > .hint, .content-objectEdit > form, .content-objectEdit > .warningBanner{ flex:0 0 auto; }
/* Un <form> qui doit envelopper les DEUX colonnes à la fois (un seul
formulaire soumis ensemble, ex. object_form.html : nom de l'objet à
gauche, liste de champs à droite) ne doit pas devenir lui-même une boîte
dans la mise en page flex — sinon .twoCol à l'intérieur perd la hauteur
disponible qu'il doit occuper. "display:contents" le rend transparent :
.twoCol se comporte comme s'il était directement enfant de
.content-objectEdit. */
.formPassthrough{ display:contents; }
.content-objectEdit .twoCol{ flex:1 1 auto; min-height:0; align-items:stretch; }
.content-objectEdit .colLeft{ overflow-y:auto; height:100%; }
.content-objectEdit .colRight{ display:flex; flex-direction:column; min-height:0; height:100%; }
.content-objectEdit .colRight h2{ flex:0 0 auto; }
.content-objectEdit .colRight > .hint, .content-objectEdit .colRight > button{ flex:0 0 auto; }
.fieldsTableWrap{ flex:1 1 auto; min-height:0; overflow:auto; border:1px solid var(--border); border-radius:8px; }
.fieldsTable{ width:100%; border-collapse:collapse; font-size:12.5px; }
.fieldsTable th, .fieldsTable td{ border-bottom:1px solid var(--border); padding:8px 10px; text-align:left; white-space:nowrap; }
.fieldsTable th{ position:sticky; top:0; background:var(--panel2); color:var(--text-dim); z-index:1; }
.fieldsTable tbody tr:hover{ background:rgba(255,255,255,.02); }
.fieldsTable tbody tr:last-child td{ border-bottom:none; }
.fieldsTable td input, .fieldsTable td select{ min-width:120px; width:auto; }
.fieldsTableActions{ display:flex; gap:6px; }
/* ---------- Éditeur d'écrans (façon Elementor/Bricks) ---------- */
/* La page de l'éditeur d'écran ne doit jamais défiler elle-même : elle
occupe exactement la hauteur de la fenêtre (topbar + zone de travail), et
ce sont les zones internes (panneaux, écran, panneau de logique) qui
défilent chacune de leur côté si besoin. */
body.builderBody{ display:flex; flex-direction:column; height:100vh; margin:0; overflow:hidden; }
body.builderBody > #pageChrome{ flex:0 0 auto; }
body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; display:flex; flex-direction:column; }
.content-builder{
/* .content (ci-dessus) pose "margin:0 auto" pour centrer une page normale
— mais sur cette page, le body est un flex-container en colonne
(body.builderBody), et une marge horizontale "auto" y désactive le
stretch d'un flex-item : sa largeur est alors recalculée en
"shrink-to-fit" à partir de son contenu, un calcul qui traverse toute
la chaîne flex/%/aspect-ratio jusqu'au canvas et redonne un résultat
légèrement différent à chaque changement de contenu du panneau (élément
sélectionné, grille affichée...) — d'où le canvas qui grandit/rétrécit
sans raison apparente à chaque interaction. "width:100%" donne une
taille définie à la place : la marge "auto" ne fait alors que centrer
l'espace restant une fois "max-width" atteint, comme voulu à l'origine. */
width:100%;
max-width:1600px;
/* Sans fil d'Ariane sur cette page (voir breadcrumb_wrap dans
screen_edit.html), .content garde quand même son padding par défaut
(28px en haut) — réduit ici pour que le canevas commence le plus haut
possible sur la page. */
padding:10px 20px 8px;
}
/* 3 colonnes : ajout d'éléments (gauche) / écran en grand (centre) / propriétés (droite) —
toutes les trois occupent exactement la même hauteur (celle qui reste
disponible), avec leur propre défilement interne si leur contenu dépasse. */
.builder3{ display:flex; gap:20px; align-items:stretch; margin-top:8px; flex:1 1 auto; min-height:0; }
/* overflow:hidden (pas auto) : le défilement doit se faire DANS le
canevas lui-même (voir .canvasFrame plus bas) ou dans l'onglet actif
(.flowCanvasWrap/.animTimelineWrap ont déjà leur propre défilement),
jamais sur cette zone entière — sans quoi la barre d'onglets et la barre
d'outils défileraient avec le contenu au lieu de rester fixes en haut. */
.builderCanvasArea{ flex:1 1 auto; min-width:0; height:100%; overflow:hidden; display:flex; flex-direction:column; }
/* ---------- Panneaux gauche/droite flottants ---------- */
/* Les deux panneaux (éléments de l'écran / propriétés) sont sortis du flux
flex de .builder3 (position:fixed) : .builderCanvasArea (flex:1 1 auto)
récupère automatiquement tout l'espace libéré, sans réglage
supplémentaire — les fermer donne donc une prévisualisation aussi large
que possible, aussi fidèle que possible à ce que "Jouer" affiche
réellement. Position et état ouvert/fermé mémorisés par écran (voir
floatPanel*() dans screen_edit.html) pour survivre à un changement de
sélection (qui remplace tout #builder3) ou un rechargement. */
.floatPanel{
position:fixed; top:90px; max-height:calc(100vh - 120px);
min-width:0; display:flex; flex-direction:column; z-index:60;
background:var(--panel); border:1px solid var(--border); border-radius:10px;
box-shadow:0 12px 40px rgba(0,0,0,.55);
}
.floatPanel.hidden{ display:none; }
/* Coin de redimensionnement (voir wireFloatPanelResize() dans
game_dashboard.html) — un panneau devient redimensionnable simplement en
lui ajoutant ce coin, JS mis à part rien d'autre à faire côté CSS. */
.floatPanelResizeHandle{
position:absolute; right:0; bottom:0; width:16px; height:16px; cursor:nwse-resize;
background:linear-gradient(135deg, transparent 50%, var(--border) 50%, var(--border) 60%, transparent 60%, transparent 70%, var(--border) 70%, var(--border) 80%, transparent 80%);
border-radius:0 0 10px 0;
}
.floatPanel--left{ left:24px; width:280px; }
.floatPanel--right{ right:24px; width:340px; }
.floatPanel--center{ left:50%; width:560px; max-width:calc(100vw - 48px); transform:translateX(-50%); }
/* Plein écran (voir makeFloatPanelFullscreenable() dans game_dashboard.html)
— juste un repère visuel (bordure accent) sur le panneau agrandi, tout le
positionnement réel est posé directement en style inline par le JS (pour
pouvoir revenir exactement à la position/taille d'avant en sortant). */
.floatPanel--fullscreen{ border-color:var(--accent); }
.floatPanelHeader{
display:flex; align-items:center; justify-content:space-between; gap:8px;
padding:8px 10px; border-bottom:1px solid var(--border); background:var(--panel2);
border-radius:10px 10px 0 0; cursor:move; flex:0 0 auto; user-select:none;
}
.floatPanelHeader .floatPanelTitle{ font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.05em; }
.floatPanelBody{ padding:12px; overflow-y:auto; min-height:0; flex:1 1 auto; }
.floatPanelReopenBtn{ position:fixed; top:90px; z-index:60; display:none; }
.floatPanelReopenBtn.visible{ display:inline-flex; }
#panelLeftReopenBtn{ left:24px; }
#propsPanelReopenBtn{ right:24px; }
.widgetGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
/* Icônes (widget Icône + galerie du panneau de propriétés) — SVG posé en
silhouette via "mask-image" plutôt qu'une police d'icônes (@font-face) :
se colore avec "color"/"background-color" comme du texte, MAIS ne dépend
d'aucun fichier de police externe — certains navigateurs/extensions
bloquent le chargement de polices web tierces (protection anti-
fingerprinting), ce qui rendait toutes les icônes Font Awesome invisibles
quelle que soit leur source (CDN ou même hébergées ici). Un simple SVG
affiché en masque n'est jamais concerné par ce blocage. Voir
render_icone.py (widget) et fontawesome_icons.py (liste + chemin des
fichiers, statiques dans static/icons/). */
.icon-svg{
display:inline-block; width:1em; height:1em; flex:0 0 auto;
background-color:currentColor;
-webkit-mask-image:var(--icon-url); mask-image:var(--icon-url);
-webkit-mask-size:contain; mask-size:contain;
-webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
-webkit-mask-position:center; mask-position:center;
}
/* Galerie d'icônes (panneau de propriétés) — voir
fontawesome_icons.py et addIconElement()/filterIconGallery() plus bas. */
.iconGallery{ display:grid; grid-template-columns:repeat(6, 1fr); gap:6px; max-height:220px; overflow-y:auto; margin-top:8px; }
.iconGalleryTile{
aspect-ratio:1; display:flex; align-items:center; justify-content:center;
background:var(--panel2); border:1px solid var(--border); border-radius:6px;
color:var(--text); cursor:grab; padding:0;
}
.iconGalleryTile .icon-svg{ width:16px; height:16px; }
.iconGalleryTile:hover{ border-color:var(--accent); color:var(--accent); }
.iconGalleryTile.dragging{ opacity:0.4; }
.iconGalleryTile.iconHidden{ display:none; }
.widgetBtnForm{ margin:0; }
.widgetBtn{
width:100%; display:flex; flex-direction:column; align-items:center; gap:4px;
padding:12px 6px; border:1px solid var(--border); border-radius:10px; background:var(--panel);
color:var(--text); cursor:pointer;
}
.widgetBtn:hover{ border-color:var(--accent); background:var(--panel2); }
.widgetIcon{ font-size:22px; }
.widgetLabel{ font-size:11px; text-align:center; line-height:1.2; color:var(--text-dim); }
.canvasToolbar{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:10px; }
.aspectBtn.active{ background:var(--accent); border-color:var(--accent); color:#fff; }
.canvasFrame{
/* flex:1 1 auto + min-height:0 : occupe tout l'espace restant sous la
barre d'outils dans .builderTabPanel (voir screen_edit.html). Le
canevas (voir .canvas ci-dessous) est réglé pour remplir EXACTEMENT
ce cadre (100% x 100%, sans respecter l'aspect-ratio Portrait/Paysage/
Carré dans l'éditeur — ce réglage ne sert qu'à l'aperçu jouable/"Jouer",
voir screen_set_aspect.py) : "prendre toute la place disponible" et
"garder l'aspect-ratio" sont deux exigences contradictoires dès que la
fenêtre n'a pas exactement ce ratio, la première l'emporte ici. C'est
LUI qui défile (overflow:auto) si jamais le contenu déborde malgré
tout — jamais .builderCanvasArea (voir plus haut), pour garder la
barre d'onglets/d'outils toujours visible. */
flex:1 1 auto; min-height:0; width:100%; padding:4px; border:1px solid var(--border); border-radius:16px; background:var(--panel);
display:flex; overflow:auto;
}
.canvas{
position:relative; width:100%; height:100%; flex:1 1 auto; margin:0; background:#0b0d12; border-radius:8px;
overflow:hidden; border:1px solid var(--border);
}
.canvasElement{
/* Pas d'overflow:hidden ici : une échelle (transform:scale) ou une
animation posée sur cet élément (voir la Timeline d'animation) vit sur
CE cadre (voir filters/element_style_filter.py) — le rogner clippait
l'effet à la taille d'origine de la boîte au lieu de le laisser
grossir/bouger avec elle. La zone de l'écran elle-même (.canvas,
ci-dessus) reste rognée, donc rien ne peut déborder de l'écran final. */
box-sizing:border-box; cursor:move; user-select:none;
border:1px dashed transparent;
}
.canvasElement:hover{ border-color:rgba(91,140,255,.5); }
.canvasElement.selected{ border:2px solid var(--accent); }
.canvasElementInner{ width:100%; height:100%; pointer-events:none; }
/* Surbrillance de l'élément sélectionné dans l'arborescence, posée en JS sur
sa propre balise (voir applySelectionHighlight) — seul moyen de la voir
sur le canevas quand cet élément est imbriqué dans un conteneur/répéteur/
groupe de champs, qui n'a pas de cadre ".canvasElement" séparé comme un
élément posé directement sur l'écran. Un "outline" qui dépasse la boîte
(offset positif) se fait couper par le premier ancêtre en overflow:hidden
(le canevas lui-même, ou un conteneur aux coins arrondis) dès que
l'élément touche un de ses bords — très fréquent pour une rangée de
jauges collée en haut d'un conteneur. Un box-shadow EN CREUX (inset)
reste toujours à l'intérieur de la boîte, jamais rogné, et épouse
naturellement les coins arrondis de l'élément lui-même. */
.elementHighlighted{
box-shadow: inset 0 0 0 2px var(--accent), inset 0 0 0 6px rgba(91,140,255,.2);
}
.resizeHandle{
position:absolute; right:0; bottom:0; width:14px; height:14px; cursor:nwse-resize;
background:var(--accent); border-radius:3px 0 6px 0; opacity:.85;
}
.listRow.selected{ border-color:var(--accent); background:var(--panel2); }
.childList{ display:flex; flex-direction:column; gap:6px; margin:2px 0 10px 18px; padding-left:10px; border-left:2px dashed var(--border); }
.childRow{ padding:7px 10px; font-size:12px; }
.childRow .rowTitle{ font-size:12px; }
/* Arborescence compacte des éléments de l'écran (panneau gauche) — une
ligne par élément, indentation par imbrication réelle (profondeur
illimitée) plutôt qu'un seul niveau d'enfants comme l'ancienne .childList,
pour rester lisible même avec beaucoup d'éléments. */
.elementTree{ display:flex; flex-direction:column; gap:1px; max-height:55vh; overflow-y:auto; margin-bottom:14px; }
.treeNode{ display:flex; flex-direction:column; }
.treeRow{ display:flex; align-items:center; gap:2px; }
.treeToggle{
flex:0 0 auto; width:16px; height:16px; padding:0; border:none; background:none; cursor:pointer;
color:var(--text-dim); font-size:10px; display:flex; align-items:center; justify-content:center;
transition:transform .12s ease;
}
.treeToggle:hover{ color:var(--text); }
.treeToggleSpacer{ flex:0 0 auto; width:16px; height:16px; }
.treeNode.collapsed > .treeRow > .treeToggle{ transform:rotate(-90deg); }
.treeNode.collapsed > .treeChildren{ display:none; }
.treeRowLink{
flex:1; display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:5px;
text-decoration:none; color:var(--text); font-size:12.5px; line-height:1.5;
white-space:nowrap; overflow:hidden; min-width:0;
}
.treeRowLink:hover{ background:var(--panel2); }
.treeRowLink.selected{ background:var(--accent); color:#0b0e14; font-weight:600; }
.treeIcon{ flex:0 0 auto; font-size:12px; }
.treeLabel{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.treeChildren{ display:flex; flex-direction:column; margin-left:10px; padding-left:8px; border-left:1px dashed var(--border); }
.treeRow.dragging{ opacity:0.4; }
.treeRow.dropBefore{ box-shadow: inset 0 2px 0 0 var(--accent); }
.treeRow.dropAfter{ box-shadow: inset 0 -2px 0 0 var(--accent); }
.treeRow.dropInside{ background:rgba(91,140,255,.25); box-shadow: inset 0 0 0 1px var(--accent); border-radius:4px; }
.contextMenu{
position:fixed; z-index:500; background:var(--panel); border:1px solid var(--border);
border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,.4); padding:4px; min-width:160px;
}
.contextMenu button{
display:block; width:100%; text-align:left; background:none; border:none; color:var(--text);
padding:7px 10px; border-radius:5px; font-size:13px; cursor:pointer;
}
.contextMenu button:hover{ background:var(--panel2); }
/* Élément à onglets (voir render_onglets.py) : la barre d'onglets elle-même
est un vrai composant Bulma ("tabs" > "ul" > "li", voir render_onglets.py)
— il ne reste ici que les panneaux, que Bulma n'a pas de composant pour. */
.forgeTabPanels{ min-width:0; }
.forgeTabPanel{ padding-top:10px; }
.propGroup{ border:1px solid var(--border); border-radius:8px; margin-bottom:8px; background:var(--panel); }
.propGroup > summary{
list-style:none; cursor:pointer; padding:9px 12px; font-size:13px; color:var(--accent);
font-weight:600; user-select:none; display:flex; align-items:center; justify-content:space-between;
}
.propGroup > summary::-webkit-details-marker{ display:none; }
.propGroup > summary::after{ content:"▸"; color:var(--text-dim); font-size:11px; transition:transform .15s; }
.propGroup[open] > summary::after{ transform:rotate(90deg); }
.propGroup > summary:hover{ background:var(--panel2); }
.propGroupBody{ padding:2px 12px 12px; }
.propGroupBody h3{ margin-top:0; }
/* .posGrid/.controlRow/.sizeField/.sliderRow/.alignGroup : Bulma fournit
déjà la mise en page (columns/field/control/buttons has-addons, voir
screen_edit.html) — ce qui reste ici n'est que ce que Bulma ne fournit
pas nativement : l'espacement propre à un curseur (slider) et son
pastille de valeur affichée à côté. */
.posGrid input:disabled{ opacity:0.4; cursor:not-allowed; }
.sliderRow{ display:flex; align-items:center; gap:10px; }
.sliderRow input[type=range]{ flex:1; }
.sliderValue{ min-width:44px; text-align:right; }
input[type=color]{ width:52px; height:34px; padding:2px; }
.propsPanel .sectionDivider, .floatPanel .sectionDivider{ border:none; border-top:1px solid var(--border); margin:14px 0; }
.actionAddForm{ margin-top:10px; }
.uploadRow{ margin-top:6px; }
.uploadInput{ font-size:11px; color:var(--text-dim); }
.uploadStatus{ display:block; margin-top:4px; }
code{ background:var(--panel2); border:1px solid var(--border); border-radius:4px; padding:1px 6px; font-size:11.5px; }
@media (max-width: 1300px){
/* Sous cette largeur, les 3 colonnes s'empilent : le principe "tout tient
dans la fenêtre, chaque zone défile pour elle-même" n'a plus de sens
(il n'y a plus qu'une seule colonne) — on revient à une page classique
qui défile normalement. */
body.builderBody{ height:auto; overflow:visible; }
body.builderBody > main.content{ overflow:visible; }
.builder3{ flex-direction:column; flex:none; }
.builderCanvasArea{ flex:none; width:100%; height:auto; max-height:none; overflow:visible; }
/* Le "letterboxing" par hauteur (voir .canvas plus haut) suppose une
chaîne de hauteurs définies jusqu'ici — qui n'existe plus une fois
.builderCanvasArea repassée en "height:auto" ci-dessus. Revient donc à
un dimensionnement par LARGEUR, comme une page qui défile normalement. */
.canvasFrame{ flex:none; overflow:visible; }
.canvas{ height:auto; width:100%; }
/* Trop peu de largeur pour qu'un panneau flottant ait un sens (il
couvrirait tout l'écran) : les deux rejoignent la pile normale et
restent toujours ouverts/visibles, quel que soit l'état mémorisé —
.hidden est ignoré grâce au "!important" sur "display" ci-dessous. */
.floatPanel{ position:static; flex:none; width:100%; height:auto; max-height:none; box-shadow:none; top:auto; left:auto; right:auto; display:flex !important; }
.floatPanelReopenBtn{ display:none !important; }
}
/* ---------- Onglets du centre : Écran / Logique de la scène / Timeline
d'animation ---------- */
/* Un seul onglet visible à la fois (voir switchBuilderTab() dans
screen_edit.html), chacun occupant tout l'espace disponible dans
.builderCanvasArea — remplace l'ancien panneau du bas rétractable/
redimensionnable à la souris : un onglet "prend toute la place" par
nature, pas besoin d'une poignée de redimensionnement séparée, et en
changer d'onglet équivaut à "fermer" celui qu'on quitte. */
.builderTabs{ display:flex; gap:2px; flex:0 0 auto; border-bottom:1px solid var(--border); margin-bottom:10px; }
.builderTabBtn{
background:transparent; border:none; border-bottom:2px solid transparent; border-radius:0;
padding:8px 12px; color:var(--text-dim); font-size:12.5px; cursor:pointer;
}
.builderTabBtn:hover{ color:var(--text); }
.builderTabBtn.active{ color:var(--accent); border-bottom-color:var(--accent); font-weight:600; }
.builderTabPanel{ flex:1 1 auto; min-height:0; display:none; flex-direction:column; }
.builderTabPanel.active{ display:flex; }
/* Sous-onglets Champs/Données À L'INTÉRIEUR d'un panneau "Modifier un
objet" (voir game_dashboard.html) — classes distinctes de .builderTabs/
.builderTabBtn/.builderTabPanel (utilisées par le style, réutilisé tel
quel) pour que switchDashTab() (onglets du tableau de bord lui-même,
ciblage global) n'aille jamais les toucher par erreur. */
.objectPanelTabs{ margin:14px 0 10px; }
.objectPanelTabPanel{ display:none; flex-direction:column; }
.objectPanelTabPanel.active{ display:flex; }
.flowToolbar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px; flex:0 0 auto; }
.nodeForm{ border:1px solid var(--border); border-radius:8px; padding:12px; margin-bottom:12px; background:var(--panel2); max-width:420px; flex:0 0 auto; }
.nodeForm label{ margin-top:8px; }
.flowCanvasWrap{ position:relative; width:100%; flex:1 1 auto; min-height:120px; overflow:auto; border:1px dashed var(--border); border-radius:8px; background:var(--bg); }
.flowCanvas{ position:relative; width:2200px; height:1400px; }
.flowEdgesSvg{ position:absolute; top:0; left:0; width:2200px; height:1400px; pointer-events:none; z-index:0; }
.flowEdgeLine{ stroke:var(--accent); stroke-width:2; pointer-events:stroke; cursor:pointer; }
.flowEdgeLine.flowEdgeTrue{ stroke:var(--status-success); }
.flowEdgeLine.flowEdgeFalse{ stroke:var(--danger); }
.flowNode{
position:absolute; width:220px; min-height:52px; border-radius:8px; border:1px solid var(--border);
background:var(--panel2); padding:10px 12px; font-size:12.5px; cursor:grab; z-index:1; user-select:none;
}
.flowNode-trigger{ border-color:var(--status-info); }
.flowNode-condition{ border-color:var(--status-warning); }
.flowNode-action{ border-color:var(--status-success); }
.flowNodeBody{ padding-right:16px; line-height:1.4; word-break:break-word; }
.flowNodeDelete{
position:absolute; top:4px; right:4px; width:18px; height:18px; padding:0; line-height:1;
font-size:11px; border-radius:4px; background:transparent; border:none; color:var(--text-dim);
}
.flowNodeDelete:hover{ color:var(--danger); }
.flowPort{
position:absolute; width:14px; height:14px; border-radius:50%; background:var(--panel);
border:2px solid var(--accent); top:50%; transform:translateY(-50%); cursor:crosshair; z-index:2;
}
.flowPortIn{ left:-8px; }
.flowPortOut{ right:-8px; }
.flowPortTrue{ right:-8px; top:35%; border-color:var(--status-success); }
.flowPortFalse{ right:-8px; top:65%; border-color:var(--danger); }
/* ---------- Timeline d'animation : panneau sous "Logique de la scène" ---------- */
.animTimelineWrap{
position:relative; width:100%; flex:1 1 auto; min-height:120px; overflow:auto;
border:1px dashed var(--border); border-radius:8px; background:var(--bg);
}
.animRuler{ display:flex; height:22px; border-bottom:1px solid var(--border); position:sticky; top:0; background:var(--bg); z-index:3; }
.animRulerSpacer{ position:sticky; left:0; width:140px; flex:0 0 140px; background:var(--panel); z-index:2; border-right:1px solid var(--border); }
.animRulerTicksWrap{ position:relative; flex:0 0 auto; }
.animRulerTick{
position:absolute; top:0; bottom:0; font-size:10px; color:var(--text-dim);
border-left:1px solid var(--border); padding-left:3px;
}
.animTracks{ position:relative; }
.animTrack{ position:relative; height:38px; border-bottom:1px solid var(--border); display:flex; align-items:stretch; }
.animTrackLabel{
position:sticky; left:0; width:140px; flex:0 0 140px; background:var(--panel); z-index:2;
padding:0 8px; font-size:12px; display:flex; align-items:center; border-right:1px solid var(--border);
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.animTrackLane{ position:relative; flex:0 0 auto; }
.animClip{
position:absolute; top:5px; height:28px; border-radius:6px; background:var(--accent); color:#fff;
font-size:11px; padding:5px 8px; cursor:grab; overflow:hidden; white-space:nowrap; user-select:none;
box-sizing:border-box;
}
.animClip.selected{ outline:2px solid #fff; }
.animClipResizeHandle{ position:absolute; right:0; top:0; width:8px; height:100%; cursor:ew-resize; }
.animKeyframeRow{ align-items:flex-start; }
.animKeyframeRow textarea{ font-family:monospace; font-size:11px; }
/* ---------- Tableau de bord d'un jeu (game_dashboard.html) ---------- */
/* Une seule fenêtre, un onglet par catégorie (écrans/objets/éléments de
jeu/variables) — réutilise .builderTabs/.builderTabBtn/.builderTabPanel
(voir plus bas, éditeur d'écran) pour la même sensation de "fenêtre à
onglets" qu'ailleurs dans l'outil, plutôt que d'inventer un 2e système.
Contrairement à leur usage dans l'éditeur d'écran, ici la page défile
normalement (pas de hauteur figée) : une liste peut être longue, pas
besoin d'un défilement interne séparé par panneau. */
.dashTabCount{ font-size:10px; color:var(--text-dim); background:var(--panel2); border-radius:999px; padding:1px 6px; margin-left:4px; }
.dashPanelHeader{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; }
.dashPanelHeader h2{ margin:0; }
.dashCreateBar{ display:flex; gap:8px; align-items:center; margin-bottom:12px; padding:10px; border:1px solid var(--border); border-radius:8px; background:var(--panel); }
.dashCreateBar input, .dashCreateBar select{ width:auto; flex:1 1 160px; }
.dashDangerZone{ margin-top:28px; }
/* ---------- Filtre de colonnes (onglet Données, game_dashboard.html) ---------- */
/* Menu déroulant compact sous le bouton "🔧 Colonnes" — un objet avec
beaucoup de champs rend le tableau illisible, ce menu permet d'en
masquer certains (voir toggleDataColumn() : agit directement sur
[data-col] plutôt que de reconstruire le tableau). */
.columnFilterWrap{ position:relative; display:inline-block; }
.columnFilterMenu{
position:absolute; top:100%; left:0; margin-top:4px; z-index:70;
background:var(--panel); border:1px solid var(--border); border-radius:8px;
padding:8px; box-shadow:0 12px 40px rgba(0,0,0,.55);
display:flex; flex-direction:column; gap:2px; min-width:180px; max-height:280px; overflow-y:auto;
}
.columnFilterMenu.hidden{ display:none; }
.columnFilterOption{ display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:4px; font-size:12.5px; white-space:nowrap; }
.columnFilterOption:hover{ background:var(--panel2); }
/* ---------- Panneau "Configurer la valeur" (variable Objet/Tableau) ---------- */
/* La colonne "Clé" n'a de sens que pour un Objet (paires clé/valeur) — un
Tableau n'est qu'une liste de valeurs, voir openVarValuePanel() dans
game_dashboard.html. */
#varValuePanel[data-mode="tableau"] .varValueKeyField{ display:none; }
/* ---------- Inscription : schéma visuel du mot de passe ---------- */
/* Jauge + liste de critères mise à jour en direct (voir register.html) —
les mêmes 5 critères que auth/password_strength.py côté serveur. */
.passwordStrengthBar{ height:6px; border-radius:3px; background:var(--panel2); overflow:hidden; margin-top:8px; }
.passwordStrengthFill{ height:100%; width:0; border-radius:3px; background:var(--status-danger); transition:width .15s ease, background-color .15s ease; }
.passwordStrengthFill.weak{ background:var(--status-danger); }
.passwordStrengthFill.medium{ background:var(--status-warning); }
.passwordStrengthFill.good{ background:var(--accent-2); }
.passwordStrengthFill.strong{ background:var(--status-success); }
.passwordChecklist{ list-style:none; margin:8px 0 0; padding:0; display:flex; flex-direction:column; gap:2px; }
.passwordChecklist li{ font-size:12.5px; color:var(--text-dim); padding-left:18px; position:relative; }
.passwordChecklist li::before{ content:"○"; position:absolute; left:0; }
.passwordChecklist li.ok{ color:var(--status-success); }
.passwordChecklist li.ok::before{ content:"✓"; }
.passwordChecklist-compact{ flex-direction:row; flex-wrap:wrap; column-gap:10px; row-gap:2px; }
.passwordChecklist-compact li{ font-size:11px; }
/* ---------- Page de profil : compacte, sur 2 colonnes ---------- */
.profileSection{ padding-top:1.5rem; padding-bottom:1.5rem; }
.profileContainer{ max-width:980px; }
.profileNotif{ padding:0.6rem 0.9rem; margin-bottom:0.75rem; }
.profileGrid{ display:grid; grid-template-columns:1fr 1fr; gap:14px; align-items:start; }
.profileCol{ display:flex; flex-direction:column; gap:14px; min-width:0; }
.profileBox{ padding:0.85rem 1rem; }
.profileBox-danger{ border:1px solid var(--status-danger); }
.profileForm .field{ margin-bottom:8px; }
.profileForm .field:last-child{ margin-bottom:0; }
.profileForm-row{ display:flex; flex-wrap:wrap; gap:8px; }
.profileForm-row .field{ flex:1 1 120px; margin-bottom:0; }
.profileForm-row .field.profileForm-full{ flex-basis:100%; }
@media (max-width:720px){
.profileGrid{ grid-template-columns:1fr; }
}
/* ---------- Inscription : QR code de la double authentification ---------- */
.totpQrWrap{ background:#fff; padding:16px; border-radius:8px; display:inline-block; max-width:220px; }
.totpQrWrap svg{ display:block; width:100%; height:auto; }
.recoveryCodesGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.recoveryCode{ display:block; text-align:center; padding:8px; background:var(--panel2); border-radius:10px; letter-spacing:1px; }