`) quand une légende ou un comportement au
clic en ajoutait une — `align-self` posé sur un simple descendant du
flex-item n'a aucun effet CSS, d'où le bouton "position du bloc" sans
effet visible dès qu'une légende ou un lien était configuré. Corrigé
dans `_render_image` (`document_engine/rendering/
render_document_element.py`) : le style de bloc cible désormais
toujours l'élément réellement top-niveau (enfant direct de
`.docPageContent`), quel que soit l'emboîtement. Changement de
comportement réel sur du contenu existant : une image avec légende/lien
et un fond/une bordure/un padding déjà réglés les verra désormais
appliqués à TOUT le bloc (image + légende), pas seulement à l'image —
c'est le comportement correct/attendu, mais je le signale car ça change
le rendu visuel d'éléments déjà créés.
**Bug réel corrigé (retour utilisateur : "quand j'ajoute une image ça
crée des pages à l'infini")** : une photo importée à sa taille native
pouvait dépasser une page entière à elle seule ; la pagination
automatique (`forgeDocCheckPageOverflow`, static/document/js/
document-editor.js) la déplaçait alors sans fin vers une page neuve,
qui débordait identiquement. Corrigé une première fois par un garde-fou générique (si tout le
contenu de la page déborde déjà, rien à répartir, on arrête) + une
hauteur maximale par défaut sur `img.docImage` (plafonnée à la hauteur
intérieure de la page, `static/document/document-editor.css`).
**Affiné ensuite (retour utilisateur : "si l'image uploadée est trop
grande je préfère qu'elle soit redimensionnée plutôt que bouger sur une
autre page")** : `forgeDocCheckPageOverflow` (static/document/js/
document-editor.js) évite de paginer une image qui déborde quand il
reste assez de place pour un résultat encore utilisable — elle est
rétrécie SUR PLACE (`max-height` posé en style inline,
calculé à partir de l'espace réellement disponible sous elle sur SA
page actuelle, légende comprise) à chaque rafraîchissement du canevas.
Purement visuel, jamais persisté en attribut (l'espace disponible
dépend du contenu au-dessus, qui change en éditant). Un réglage
explicite de hauteur maximale via le panneau reste prioritaire (une
valeur inline posée par un attribut serait recalculée par-dessus à
l'affichage suivant si elle déborde encore). Le garde-fou anti-boucle
et le défaut CSS restent en place pour les AUTRES kinds et comme filet
de sécurité au tout premier rendu (avant que le JS n'ait tourné).
**Changement de conception (retour utilisateur : "ce cadre ne devrait
pas changer de taille en fonction de la taille de l'image mais être
fixe et contraindre l'image dedans")** : plutôt que de rétrécir
dynamiquement une image trop grande selon l'espace disponible (approche
fragile, source des deux bugs ci-dessus), un nouvel élément image a
maintenant un cadre FIXE dès sa création — `object_fit="cover"` +
`height="220px"` par défaut au lieu de vides (`document_engine/labels/
element_kind_labels.py`) — une photo importée est donc TOUJOURS rognée
pour remplir ce cadre, quelle que soit sa résolution native. "Taille
réelle" reste un choix explicite possible dans le panneau (segmented
"Ajustement dans son cadre"). **Ne s'applique qu'aux NOUVEAUX éléments
image** — un élément déjà créé avant ce changement garde ses attributs
`object_fit`/`height` existants (jamais re-migré automatiquement,
`sanitize_element_attributes` ne touche pas au kind "image", voir sa
docstring) ; pour en faire bénéficier une image déjà présente, régler
manuellement "Couvrir" + une hauteur via le panneau. Le rétrécissement
dynamique (garde-fou anti-boucle + `forgeDocCheckPageOverflow` appelé
après upload/mise à jour d'attribut) reste en place comme filet de
sécurité pour une hauteur explicite déraisonnablement grande.
**Bug réel corrigé une seconde fois (retour utilisateur : "ce n'est pas
redimensionner")** : le rétrécissement ci-dessus vit dans
`forgeDocCheckPageOverflow`, appelée uniquement par
`forgeDocRefreshCanvas` (ajout/déplacement/suppression/Annuler-
Rétablir) — mais **téléverser un fichier dans une image existante**
(`forgeDocApiUploadImage`) et **changer un attribut quelconque**
(`forgeDocUpdateAttributes`, le point d'entrée central de tous les
panneaux) patchent chacun le DOM directement, SANS jamais passer par
ce chemin : le rétrécissement ne se déclenchait donc jamais après un
upload. Corrigé en appelant explicitement `forgeDocCheckPageOverflow()`
à la fin de ces deux fonctions. Pas de test automatisé possible ici :
aucune suite de tests n'existe pour `document-editor.js`
(`package.json` ne couvre que `static/game/js/`), vérification
manuelle uniquement.
**Affiné une troisième fois (retour utilisateur : "l'ajout de page
quand le contenu déborde dois rester et même être plus intelligent, la
nouvelle page dois etre sous la page qui deborde meme si ya d'autre
page")** : la pagination automatique (pour tout kind, pas seulement les
images désormais épargnées ci-dessus) insérait toujours la page neuve
en toute fin de la bande d'onglets (`add_document_page` l'ajoute
toujours à la fin), même si d'autres pages existaient déjà après celle
qui déborde — déborder sur la page 2 d'un support qui en compte 5
ajoutait la nouvelle page en position 6 au lieu de 3. Corrigé dans
`forgeDocCheckPageOverflow` : la page est créée puis immédiatement
déplacée (`forgeDocApiPageMove`, mécanisme déjà existant pour le
glisser-déposer du panneau Pages) juste après la page active, décalant
les pages suivantes d'un cran. Extraction d'un helper partagé
`forgeDocReorderLocalPage` (état client après un déplacement) réutilisé
par le glisser-déposer ET par ce nouveau cas, pour ne pas dupliquer ce
calcul. Vérification côté serveur déjà couverte par les tests existants
de `move_document_page`/`document_page_move` (l'insertion "au milieu"
de la bande d'onglets y est déjà testée) ; le déclenchement côté client
reste manuel faute de suite de tests JS.
**Affiné une quatrième fois** : sous un seuil de place restante
(`FORGE_DOC_MIN_IMAGE_HEIGHT`, 60px) il n'y a plus de place RÉELLE sur
la page (pas seulement pour cette image) — l'image bascule alors dans
la pagination normale ci-dessus au lieu d'être rétrécie à une taille
inutilisable, ce qui empêchait sinon une page pleine d'images
d'enchaîner sur une nouvelle page. Vérifié par un test de bout en bout
en conditions réelles (navigateur automatisé Playwright contre le
serveur local, sur un support jetable créé puis supprimé pour
l'occasion) : 5 images vides ajoutées à la suite produisent bien 3
pages, chacune avec le cadre fixe "Couvrir" actif par défaut.
**Audit final** : chaque ligne du tableau d'audit initial pour
l'élément Image est couverte — implémentée (dimensionnement/ratio/
object-fit, bordure+ombre via `box_style.py`, filtre, lien/lightbox/
lazy-load/légende, position du bloc dont l'alignement, upload de
fichier) ou explicitement différée ci-dessus avec sa raison
(`object-position`, `mix-blend-mode`, `clip-path`, overlay au survol,
arrondi par coin, responsive par taille d'écran — mêmes exclusions que
Titre/Paragraphe, valeur douteuse pour du contenu de formation). Rien
d'oublié constaté à cette relecture. Validé par l'utilisateur, prêt à
committer.
### 3. Bouton — ✅ audité et validé (commité)
Implémenté : gras/italique, majuscules/minuscules/capitales, police
(liste déroulante web-safe, réutilise `FORGE_DOC_FONT_FAMILY_OPTIONS`
déjà créé pour Titre/Paragraphe), taille de police, espacement des
lettres, couleur du texte (sélecteur natif), icône SVG optionnelle
(position avant/après le texte, taille réglable — le bouton dépasse ici
l'étiquette, qui n'a toujours qu'une icône fixe sans position ni taille
réglables), tous les attributs de boîte partagés (padding, margin,
couleur de fond, arrondi, largeur/hauteur, ombre portée, opacité,
position du bloc, bordure par côté). États interactifs survol/actif :
effet visuel UNIVERSEL (assombrissement léger au survol, léger
tassement au clic), automatique pour tous les boutons sans réglage à
faire — jamais une couleur de survol personnalisable par bouton
(mécanisme CSS fragile pour une valeur ajoutée faible en contenu de
formation). Curseur (`cursor:pointer`) et transition au survol déjà en
place.
Mécanisme partagé réutilisé tel quel : `document_engine/rendering/
box_style.py` (comme Titre/Paragraphe/Image) + `sanitize_svg_markup`
(comme l'Image et l'Étiquette pour l'icône). Extraction d'une constante
JS partagée `FORGE_DOC_TEXT_TRANSFORM_OPTIONS` (utilisée par Titre/
Paragraphe ET Bouton, plus de duplication de ce tableau d'options).
Volontairement laissé de côté (même rationale que Titre/Paragraphe/
Image) : dégradé/image de fond, arrondi par coin (un seul rayon pour
les 4 coins), réglages responsive par taille d'écran. **État
"désactivé"** délibérément absent : un bouton de contenu de formation
n'est pas un vrai contrôle de formulaire avec un état programmatique
désactivé — aucun équivalent clair sans inventer un concept artificiel;
à ajouter si un besoin précis se présente.
**Audit final** : chaque ligne du tableau d'audit initial pour
l'élément Bouton est couverte — typo (police/taille/gras/italique/
transformation/espacement), boîte (padding/margin/largeur/hauteur/
position du bloc), bordure/ombre (style/couleur/épaisseur/par côté +
le nouveau réglage "les 4 côtés à la fois"/ombre portée), fond
(couleur), icône, effets (transition/curseur) — chacune implémentée ou
explicitement différée ci-dessus avec sa raison (dégradé/image de fond,
arrondi par coin, responsive, état désactivé). 9 tests dédiés passent
(224 au total dans `tests/document/`), ruff/mypy --strict/bandit/
vulture/import-linter/eslint/stylelint tous clean. Rien d'oublié
constaté à cette relecture. Validé par l'utilisateur, prêt à committer.
**Ajout transversal pendant le test (retour utilisateur : "pour les
bordures, il faudrait une option pour modifier les 4 bordures en même
temps")** : une rangée "Bordure — les 4 côtés à la fois" a été ajoutée
au-dessus du réglage par côté existant, dans le module PARTAGÉ
`forgeDocRenderBoxFieldsHtml`/`forgeDocBindBoxFields` (static/document/
js/document-editor.js) — un changement de style/épaisseur/couleur y
applique la MÊME valeur aux 4 côtés d'un coup (en plus, jamais à la
place, du réglage par côté qui reste utilisable après pour affiner).
Repart de l'état actuel si les 4 côtés portent déjà la même valeur,
sinon d'un état neutre. Bénéficie automatiquement à TOUS les éléments
qui utilisent déjà ce module (Titre/Paragraphe/Image/Bouton), aucune
modification nécessaire ailleurs.
### 4. Liste à puces / numérotée — ✅ audité et validé (commité)
**Portée actée avec l'utilisateur avant implémentation** (question
posée explicitement, deux catégories du tableau impliquaient de
transformer `items` — une simple liste de chaînes — en objets
structurés) : style sur la LISTE ENTIÈRE uniquement, jamais par élément
individuel ni de sous-listes imbriquées (chantier bien plus lourd,
différé à une demande séparée si besoin). Seule exception actée : un
padding UNIFORME appliqué à chaque élément (`item_padding`), pas encore
réglable individuellement.
Implémenté : gras/italique/souligné, police (liste déroulante web-safe,
`FORGE_DOC_FONT_FAMILY_OPTIONS` réutilisé), taille de police, hauteur de
ligne, couleur du texte, style de puce (`list_style_type`, options
propres à chaque kind — disque/cercle/carré/aucune pour puces,
1-2-3/01-02-03/i-ii-iii/I-II-III/a-b-c/A-B-C/aucune pour numérotée),
position de la puce (intérieure/extérieure), couleur et taille de puce
indépendantes du texte, puce personnalisée (icône SVG, liste à puces
uniquement — encodée en URI de données pour `list-style-image`),
espacement intérieur par élément (`item_padding`, **"6px" par défaut,
retour utilisateur explicite : "il faut un padding de base par élément
de liste car y en a pas aujourd'hui"** — n'existait pas du tout avant),
espacement entre éléments réglable (`item_spacing`), tous les attributs
de boîte partagés sur la liste entière (padding/margin/fond/bordure/
ombre/opacité/position du bloc).
Mécanisme technique notable : `marker_color`/`marker_size`/
`item_padding`/`item_spacing` ne peuvent pas passer par un style inline
classique sur le `
`/`` (impossible de cibler le `::marker` ou le
padding des `- ` enfants depuis le style de leur parent) — résolu via
des propriétés personnalisées CSS (`--doc-marker-color` etc.), posées
en inline sur le conteneur et consommées par une règle CSS dédiée
(`.docList li`/`.docList li::marker`) qui en hérite. Même technique déjà
utilisée pour la position du bloc des éléments enveloppés (Image).
Volontairement laissé de côté (portée actée ci-dessus) : style/bordure/
fond par élément individuel, sous-listes imbriquées — nécessiteraient
de transformer `items` (liste de chaînes) en objets structurés, refonte
du panneau et du stockage. Comme les autres éléments : responsive par
taille d'écran.
**Existant retroactivement inchangé** : une liste déjà créée avant ce
commit garde ses anciens attributs (juste `items`) — `item_padding`
n'apparaît en style inline QUE pour les nouvelles listes ; ouvrir le
panneau Propriétés d'une ancienne liste et toucher un réglage la fait
bénéficier des nouveaux défauts au passage.
**Bug réel corrigé pendant le test (retour utilisateur : "si j'enlève
les puces ou que les puces se mettent à l'intérieur, il reste un
espace devant la liste, cet espace doit être supprimé")** : le
`padding-left:1.4em` par défaut (réservé pour une puce EXTÉRIEURE)
n'a plus lieu d'être dès que `list_style_type="none"` ou
`list_style_position="inside"` — `_render_list` ajoute alors
automatiquement `padding-left:0;`, prioritaire sur le CSS mais toujours
cédant la place à un `padding` uniforme réglé explicitement par
ailleurs (attributs de boîte partagés).
**Deux ajouts transversaux pendant le test, bénéficiant à plusieurs
éléments à la fois :**
1. **Sections "Contenu"/"Conteneur" dans le panneau Propriétés (retour
utilisateur : "il faut distinguer par des sections la propriété qui
touche au conteneur de celles qui touchent à l'élément qu'il
contient sinon c'est pas compréhensible")** : un sous-titre visuel
(bordure du dessus) sépare maintenant, dans CHAQUE panneau qui
utilise `box_style.py` (Titre/Paragraphe, Image, Bouton, Liste), les
champs propres au CONTENU (texte, icône, puces...) de ceux qui
touchent au CONTENEUR (padding/margin/fond/bordure/ombre/opacité/
position du bloc/alignement du contenu — voir point 2). Le sous-titre
"Conteneur" est posé UNE SEULE FOIS, à l'intérieur de
`forgeDocRenderBoxFieldsHtml` (le module déjà partagé), jamais dupliqué
à chaque appelant.
2. **Alignement vertical du CONTENU dans son bloc (retour utilisateur :
"je peux augmenter la hauteur d'un conteneur mais pas l'alignement
vertical à l'intérieur, pour les listes et il faut aussi vérifier
pour les autres")** : nouvel attribut partagé `content_align`
(`"top"` par défaut, `"center"`/`"bottom"`) posé dans `BOX_DEFAULTS`,
rendu via `render_content_align(a)` — volontairement PAS fusionné
dans `render_box_style` (l'alignement du contenu dépend de l'axe
interne du conteneur : `justify-content` convient à un conteneur en
COLONNE — Titre/Paragraphe/Liste, rendus `display:flex;
flex-direction:column;` pour l'occasion, et la figure d'une image
légendée, déjà flex-colonne — mais serait FAUX pour le Bouton, une
RANGÉE icône+texte qui gère déjà son axe vertical via `align-items`,
déjà correct sans réglage). Contrôle exclu explicitement du panneau
Bouton (`includeContentAlign: false`) pour ne jamais afficher un
réglage sans effet.
**Bug réel corrigé, puis CORRIGÉ UNE SECONDE FOIS après un retour de
régression (capture à l'appui, thème "Sécurité incendie")** :
1. *Premier symptôme* ("dans les liste numéroté [...] la position des
élément à l'intérieur de base est verticale au lieu d'être
horizontale") : ce thème remplace la puce native d'une liste par un
badge (`::before`, voir `static/document/themes/
securite-incendie.css`), pensé pour s'afficher À CÔTÉ du texte.
Corrigé une première fois en posant `display:flex` sur `.docList li`
(`static/document/document-editor.css`).
2. *Régression introduite par ce premier correctif* ("quand j'enlève le
thème [...] on voit plus les puces ou les numéros") : `display:flex`
posé DIRECTEMENT sur le `
- ` remplace entièrement son
`display:list-item` natif — ça supprime le marqueur natif (puce/
numéro) pour TOUTE liste, avec ou sans thème (l'affirmation inverse
dans le premier correctif était FAUSSE, corrigée après une
vérification en conditions réelles, navigateur automatisé). **Corrigé
en ciblant le `::before` LUI-MÊME** (`display:inline-block;
vertical-align:middle; margin-right:10px;`), jamais son parent : le
`
- ` garde son `display:list-item` natif (donc son marqueur), et le
badge s'aligne quand même à côté du texte qui le suit dans le flux
normal.
3. *Redondance restante* : une fois le marqueur natif restauré, les
listes du thème affichaient À LA FOIS leur badge personnalisé ET le
marqueur natif (disc/decimal) en double — le thème ne les avait
jamais explicitement désactivés (aucun `list-style:none` dans
`securite-incendie.css`, il comptait implicitement sur le marqueur
natif pour disparaître tout seul). Corrigé en ajoutant ce reset —
avec le sélecteur d'élément (`ul.docList[...]`/`ol.docList[...]`),
jamais seulement les classes/attributs : `static/style.css` (site
large) porte une règle `.content ol:not([type]) {
list-style-type: decimal; }` d'une spécificité légèrement supérieure
qui l'emportait sinon silencieusement.
4. *Dernière régression du même correctif* : le badge numéroté du thème
posait lui-même `display:flex` (pour centrer son chiffre) — une
valeur qui BLOCKIFIE le `::before` (le repousse hors du flux en
ligne, au-dessus du texte), défaisant le point 1 pour ce cas précis.
Corrigé en `display:inline-flex` (garde le badge en ligne tout en
centrant quand même le chiffre à l'intérieur).
Chaque étape vérifiée par un navigateur automatisé (Playwright) contre
le serveur local, sur un support jetable créé puis supprimé pour
l'occasion — thème appliqué et retiré successivement, capture d'écran
et styles calculés (`getComputedStyle`) inspectés à chaque fois, pas
seulement supposés corrects.
**Audit final** : chaque ligne du tableau d'audit initial pour l'élément
Liste est couverte —
**Typo** (police/taille/gras/italique/souligné/couleur/interligne) ✅,
**Puces/numéros** (style de puce validé par kind, image de puce
personnalisée pour les puces, position intérieure/extérieure, couleur/
taille de puce indépendantes du texte via propriétés personnalisées
CSS) ✅,
**Boîte** (padding/margin sur la liste entière via `box_style.py`,
padding UNIFORME par élément — `item_padding`, avec un vrai défaut non
vide comme demandé —, espacement entre éléments réglable —
`item_spacing` —, indentation couverte par le padding partagé) ✅,
**Bordure/fond** sur la liste entière ✅.
Volontairement différés, portée actée explicitement AVANT
implémentation avec l'utilisateur (question posée, réponse : liste
entière seulement) : bordure/fond/padding/margin **par élément
individuel** (au-delà du padding uniforme) et **listes imbriquées** —
transformeraient `items` (liste de chaînes) en objets structurés, un
chantier bien plus lourd que le reste du tableau, à traiter séparément
si demandé. Comme les autres éléments : responsive par taille d'écran.
245 tests passent, ruff/mypy --strict/bandit/vulture/import-linter/
eslint/stylelint tous clean. Rien d'oublié constaté à cette relecture.
Validé par l'utilisateur (y compris les 4 bugs de thème trouvés et
corrigés pendant la validation), prêt à committer.
### 5. Étiquette (badge) — à compléter
Déjà fait (session précédente) : icône SVG, largeur, arrondi uniforme,
gras, majuscules. Restant selon le tableau ci-dessus : italique,
souligné, barré, police, taille de police, couleur du texte, espacement
des lettres, hauteur, padding, margin, bordure par côté, ombre portée,
position de l'icône. Pourra réutiliser directement `box_style.py`.
### 6. Carte — à faire
### 7. Rangée (row) — à faire
### 8. Mini-jeux — à faire
## Décisions transversales actées pendant ce chantier
- Un seul rayon d'arrondi pour les 4 coins (pas par coin) — cohérent
avec l'étiquette déjà en place, à revoir si demandé explicitement.
- Padding/margin : une valeur UNIFORME sur les 4 côtés (curseur unique),
pas par côté — la bordure, elle, reste par côté (demande explicite).
- Ombres (texte et bloc) : 3 préréglages (Aucune/Légère/Marquée) plutôt
que des curseurs séparés offset/flou/couleur — reste simple à
utiliser, ajustable si besoin de plus de finesse plus tard.
- Polices : liste fermée de polices web-safe (aucun chargement dynamique
de Google Fonts depuis l'éditeur) — évite d'introduire un mécanisme de
chargement de police, hors périmètre de cet audit.
## Fonctionnalité hors tableau : glisser-déposer un élément vers une autre page
Retour utilisateur du 26/09/2026 : "j'aimerais pouvoir glisser déposer
un élément d'une page dans une autre page" — sans rapport avec l'audit
de mise en forme élément par élément, mais traité dans la foulée.
Le mécanisme bas niveau (`document_engine.move_document_element_to_page`
+ la route `/elements//move-to-page`) existait déjà, utilisé
uniquement par la pagination automatique (voir
`forgeDocCheckPageOverflow`) — déjà bien testé côté serveur
(`test_document_elements.py`/`test_document_routes.py`).
Ajout : glisser un élément du canevas jusqu'à une rangée du panneau
Pages (onglet "Pages" du panneau gauche) le déplace vers cette page —
même charge utile de glisser (`"text/forge-doc-move"`) que le
réordonnancement au sein d'une page, aucune modification de la source
de glisser côté canevas. Nouvelle fonction `forgeDocMoveElementToPage`
(commande Annuler/Rétablir complète, comme le reste des mutations).
Surlignage visuel de la rangée survolée (`.docPageRow--dropTarget`),
distinct de `.is-active`/`.is-dragging`. Pas de test automatisé
possible côté client (aucune suite de tests n'existe pour
`document-editor.js`), vérification manuelle uniquement — le mécanisme
serveur sous-jacent, lui, reste couvert par les tests existants.
## Fonctionnalité hors tableau : retirer le thème appliqué
Retour utilisateur du 26/09/2026 : après avoir choisi un thème puis
vidé toutes les pages, le thème restait appliqué — question légitime
("c'est voulu ?"), réponse : OUI pour la séparation contenu/thème (déjà
le cas), mais il manquait un moyen de retirer un thème une fois choisi.
Ajout demandé : "dans la modale de choix des modèles, ajoute-en un qui
s'appelle Aucun modèle, si l'utilisateur le choisit ça enlève tout
modèle de style choisi pour revenir à un document de base".
- **Nouvelle fonction bas niveau** `db.remove_document_theme(slug)`
(`db/supports/remove_document_theme.py`) — supprime la LIGNE `_meta`
plutôt que d'y stocker une chaîne vide, pour que `get_document_theme`
continue de renvoyer `None` (son contrat documenté : "aucun thème
n'a jamais été appliqué"), jamais une chaîne vide qui le violerait
silencieusement pour tout appelant qui compare à `None` (dont le test
déjà existant `test_new_support_has_no_theme_by_default`).
- **Route** `/document//theme/apply` : `theme_id` vide retire
désormais le thème et s'arrête là — `mode` n'a alors aucun sens
(aucun contenu de démonstration pour "aucun modèle") et est ignoré,
jamais validé ni utilisé dans ce cas.
- **Modale "Utiliser un modèle"** : nouvelle carte "Aucun modèle"
toujours en tête de liste (même catalogue vide), id sentinelle `""`
— distincte de `null` (qui reste réservé à "rien n'a encore été
cliqué dans la modale", `forgeDocSelectedTemplateId` à l'ouverture).
La sélectionner remplace l'aperçu (rien à prévisualiser, "aucun
modèle" n'a pas de contenu de démonstration) par un message explicite
et un unique bouton "Retirer le modèle" (jamais les deux boutons
"contenu actuel/du modèle", qui supposent un vrai thème choisi) —
confirmation native avant l'action (changement visuel notable, même
si le contenu n'est jamais touché).
- Testé : `db/supports/remove_document_theme.py` (2 tests bas niveau,
`tests/document/test_support_lifecycle.py`) + la route (2 tests,
`tests/document/test_document_routes.py` — retire vraiment le thème,
laisse le contenu intact, ignore `mode`). Le déclenchement côté
client (clic sur la carte/le bouton) reste manuel faute de suite de
tests JS, comme le reste de l'éditeur.