diff --git a/document_engine/labels/element_kind_labels.py b/document_engine/labels/element_kind_labels.py
index 2ef4e9c1..1bd47420 100644
--- a/document_engine/labels/element_kind_labels.py
+++ b/document_engine/labels/element_kind_labels.py
@@ -81,7 +81,20 @@ def element_default_attributes(kind: str) -> dict[str, Any]:
# attribut "ordered" redondant à tenir synchronisé avec le kind.
return {"items": ["Premier élément", "Deuxième élément"]}
if kind == "badge":
- return {"content": "Étiquette"}
+ # Même esprit que titre/paragraphe (bold/align/color... déjà des
+ # attributs par élément, pas des choix figés par le moteur) :
+ # svg_markup/width/border_radius/bold/uppercase vides ou False par
+ # défaut = comportement historique inchangé (pleine largeur, sans
+ # icône, casse normale) ; un thème ou le créateur les règle au cas
+ # par cas (retour utilisateur du 24/09/2026).
+ return {
+ "content": "Étiquette",
+ "svg_markup": "",
+ "width": "",
+ "border_radius": "",
+ "bold": False,
+ "uppercase": False,
+ }
if kind == "carte":
# Contenu pur (lettre/repère court + titre + description) — sans
# aucun choix de couleur/forme, laissé au futur système de
diff --git a/document_engine/labels/labels.md b/document_engine/labels/labels.md
index 52d17227..fb0c5520 100644
--- a/document_engine/labels/labels.md
+++ b/document_engine/labels/labels.md
@@ -11,12 +11,18 @@ top-niveau ou enfants d'une rangée. `"liste_puces"`/`"liste_numerotee"`
partagent la même structure d'attributs (`items`) ; c'est le `kind`
lui-même qui décide `
` ou `` au rendu (voir
`document_engine/rendering/render_document_element.py::_render_list`),
-jamais un attribut `ordered` redondant. `"badge"` et `"carte"` sont du
-contenu PUR, sans aucun attribut de style/couleur/forme (décision du
-24/09/2026 : le moteur ne porte que contenu et mécanisme, tout habillage
-visuel revient à un futur système de templates) — voir aussi le mode SVG
-inline de `"image"` (`svg_markup`) et la pièce jointe téléchargeable de
-`"bouton"` (`attachment_filename`), ci-dessous.
+jamais un attribut `ordered` redondant. `"carte"` reste du contenu PUR,
+sans aucun attribut de style/couleur/forme (décision du 24/09/2026 : le
+moteur ne porte que contenu et mécanisme, tout habillage visuel revient
+à un futur système de templates). `"badge"` a depuis gagné des
+attributs de mise en forme PAR ÉLÉMENT (`svg_markup`/`width`/
+`border_radius`/`bold`/`uppercase`, retour utilisateur du 24/09/2026)
+— même esprit que `bold`/`align`/`color` déjà présents sur
+`"titre"`/`"paragraphe"` : des réglages posés par le créateur ou un
+thème au cas par cas, jamais une valeur figée dans le moteur. Voir
+aussi le mode SVG inline de `"image"` (`svg_markup`) et la pièce
+jointe téléchargeable de `"bouton"` (`attachment_filename`),
+ci-dessous.
## `MINIGAME_KINDS: tuple[str, ...]`
`("quiz", "association", "memory", "mots", "scenario", "zones")` —
@@ -47,8 +53,11 @@ Attributs posés à la création d'un élément de ce type (voir
rendu, voir rendering.md), bouton (`label/target/
attachment_stored_name/attachment_filename` — la pièce jointe est
indépendante de `target`, réservé à la navigation), liste à
- puces/numérotée (`items`, une liste de chaînes), badge (`content`),
- carte (`label/title/description`, contenu pur sans couleur/forme),
+ puces/numérotée (`items`, une liste de chaînes), badge
+ (`content/svg_markup/width/border_radius/bold/uppercase` — tous vides
+ ou `False` par défaut = comportement historique inchangé, voir
+ rendering.md), carte (`label/title/description`, contenu pur sans
+ couleur/forme),
rangée (`gap/align/justify`), quiz (`DEFAULT_QUIZ_CONFIG`, voir
`quiz_config.py`), association (`DEFAULT_ASSOCIATION_CONFIG`, voir
`association_config.py`), memory (`DEFAULT_MEMORY_CONFIG`, voir
diff --git a/document_engine/rendering/render_document_element.py b/document_engine/rendering/render_document_element.py
index c1409b76..801695fc 100644
--- a/document_engine/rendering/render_document_element.py
+++ b/document_engine/rendering/render_document_element.py
@@ -3,6 +3,8 @@ import json
import random
from typing import Any
+from .sanitize_svg_markup import sanitize_svg_markup
+
def render_document(elements: list[dict[str, Any]]) -> str:
"""Assemble le document ENTIER à partir de la liste à plat renvoyée
@@ -79,8 +81,6 @@ def _render_image(el: dict[str, Any], _children_by_parent: dict[int | None, list
# element_kind_labels.element_default_attributes). Nettoyé à
# CHAQUE rendu (jamais seulement à l'écriture) par sanitize_svg_markup,
# même défense en profondeur que html.escape sur les autres kinds.
- from .sanitize_svg_markup import sanitize_svg_markup
-
sanitized = sanitize_svg_markup(svg_markup)
return f'
'
+ a = el["attributes"]
+ content = html_lib.escape(str(a.get("content", "")))
+ svg_markup = str(a.get("svg_markup", "")).strip()
+ icon_html = f'{sanitize_svg_markup(svg_markup)}' if svg_markup else ""
+
+ style_parts = []
+ width = str(a.get("width", "")).strip()
+ if width:
+ # Fixer une largeur implique de ne plus s'étirer sur toute la
+ # largeur de .docPageContent (comportement par défaut d'un enfant
+ # flex en colonne, voir static/document/document-editor.css) —
+ # les deux vont toujours ensemble, jamais l'un sans l'autre.
+ style_parts.append(f"align-self:flex-start; width:{html_lib.escape(width)};")
+ border_radius = str(a.get("border_radius", "")).strip()
+ if border_radius:
+ style_parts.append(f"border-radius:{html_lib.escape(border_radius)};")
+ if a.get("bold"):
+ style_parts.append("font-weight:800;")
+ if a.get("uppercase"):
+ style_parts.append("text-transform:uppercase;")
+ style_attr = f' style="{" ".join(style_parts)}"' if style_parts else ""
+
+ return (
+ f'
{icon_html}{content}
'
+ )
def _render_carte(el: dict[str, Any], _children_by_parent: dict[int | None, list[dict[str, Any]]]) -> str:
diff --git a/document_engine/rendering/rendering.md b/document_engine/rendering/rendering.md
index 32ba88bc..f2e7094a 100644
--- a/document_engine/rendering/rendering.md
+++ b/document_engine/rendering/rendering.md
@@ -50,7 +50,18 @@ regroupement à chaque appel.
partir de `data-element-id` + `FORGE_DOCUMENT.slug`, même principe que
le reste des appels AJAX de l'éditeur).
- **Étiquette** (`badge`) : `
` portant `attributes["content"]`
- échappé — contenu pur, aucun attribut de style.
+ échappé, précédé d'un `` optionnel si
+ `svg_markup` est non vide (nettoyé par `sanitize_svg_markup`, même
+ mécanisme que le mode SVG de `"image"`). `width` (non vide) ajoute
+ `align-self:flex-start; width:{valeur};` en style inline — fixer une
+ largeur implique TOUJOURS de sortir de l'étirement pleine largeur par
+ défaut d'un enfant flex en colonne (voir static/document/
+ document-editor.css, `.docPageContent`), jamais l'un sans l'autre.
+ `border_radius` (non vide) ajoute `border-radius:{valeur};`. `bold`/
+ `uppercase` ajoutent respectivement `font-weight:800;`/
+ `text-transform:uppercase;` quand `True`. Tous ces attributs sont vides
+ ou `False` par défaut (comportement historique inchangé, aucun style
+ inline ajouté).
- **Carte** (`carte`) : `