# Thème Citizen Motion — V2

Thème bloc sur mesure, porté depuis les maquettes de `maquettes/V2`. Il coexiste
avec le thème V1 (`citizenmotion`) : on bascule de l'un à l'autre dans
Apparence → Thèmes, ou en une commande.

```powershell
cd "C:\Users\Citizen Motion\Downloads\CiMo\Refonte\Proto1\wp"
.\wp.ps1 theme activate citizenmotion2   # ou citizenmotion pour revenir
```

```
citizenmotion2/
├── theme.json            ← source de vérité : couleurs, polices, échelles
├── style.css             ← CSS global : jetons, fond, couches, en-tête, pied
├── functions.php         ← amorçage : styles, sprite, enregistrement des blocs
├── inc/
│   ├── shapes.php        formes décoratives lues dans assets/svg/
│   ├── box.php           réglages partagés par les blocs « boîte » (PHP)
│   ├── hero-split.php    fabrique du calque du héros fendu (PHP)
│   └── blocks.php        petits blocs : textes, boutons, icônes, empilement
├── blocks/               sept blocs à interface sur mesure
├── patterns/             un fond de héros
├── pages/                LES PAGES DU SITE — voir « Reconstruire les pages »
├── parts/ · templates/   en-tête, pied de page, gabarits
├── assets/
│   ├── css/              blocks · patterns · editor (canevas) · admin (panneaux)
│   ├── js/               cimo-ui (panneaux partagés) · blocks (petits blocs)
│   ├── svg/              11 formes
│   ├── fonts/            Articulat CF en woff2
│   └── img/icons.svg     sprite de 41 icônes
└── demo-blocs-v2.html    contenu de la page « Démo des blocs V2 »
```

## Reconstruire les pages

Les six pages du site ne sont pas saisies dans l'éditeur : elles sont
**générées**, puis chargées. Une retouche de bloc ou de CSS demande de les
rejouer toutes, et le faire à la main coûterait un après-midi à chaque fois.

```powershell
cd "…\Refonte\Proto1\wp\themes\citizenmotion2"
python pages/_generer.py
cd ..\..
.\wp.ps1 eval-file wp-content/themes/citizenmotion2/pages/_construire.php
```

| Fichier | Rôle |
|---|---|
| `pages/_blocs.py` | fabrique de markup : `section()`, `croppe()`, `texte()`… |
| `pages/_generer.py` | le contenu des six pages, porté des maquettes |
| `pages/*.html` | le résultat — c'est ce que WordPress charge |
| `pages/_construire.php` | crée ou met à jour les pages, pose la page d'accueil, contrôle que tous les blocs sont lisibles, nettoie les enregistrements automatiques |

**Pourquoi un générateur et non du HTML écrit à la main.** Les attributs d'un
bloc sont du JSON dans un commentaire HTML. Un guillemet dans un texte — et il
y en a dès qu'on colore un mot, qui s'écrit `<mark class="…">` — casse le JSON,
et **WordPress ne dit rien** : il retombe silencieusement sur les valeurs par
défaut. Les titres de héros se sont affichés vides, en texte courant, sans le
moindre message. `json.dumps` règle le problème une fois pour toutes.

## Le renversement par rapport au V1

Le thème V1 comptait **29 blocs et 105 variations**, chacune ne portant qu'une
classe CSS. L'apparence était figée dans la feuille de style ; la seule liberté
laissée à la rédaction était de choisir dans une liste.

Le V2 compte **12 blocs et 13 rôles de texte**, mais chaque bloc a de vrais
réglages. C'était la demande : construire les blocs d'abord, les compositions
ensuite — et non l'inverse.

| | V1 | V2 |
|---|---|---|
| Nombre de colonnes d'une grille | une variation par cas | un curseur |
| Marges et retraits | dans le CSS | panneau natif de WordPress |
| Couleur d'un titre | la classe la décidait | palette **ou** valeur libre |
| Coins biseautés | polygone écrit à la main par carte | quatre cases à cocher |

**Ce qui n'a pas bougé :** theme.json reste la seule source de vérité des
couleurs, polices et échelles. Les valeurs de palette sont rendues par leur
variable (`var(--wp--preset--color--brand-teal)`), jamais recopiées : changer la
charte dans theme.json se propage aux contenus déjà écrits.

## Les douze blocs

| Bloc | Ce qu'il règle |
|---|---|
| **Section** | hauteur (libre · demi-écran · écran plein), colonnes, espacement, largeur de lecture, fond couleur ou image + voile, balise HTML |
| **Grille de contenu** | colonnes, rangées de hauteur égale, espacement, fond |
| **Bloc croppé** | 4 coins coupés à 45° + profondeur, bordure par côté + bord accentué + opacité, fond couleur ou photo + voile, catégorie, mention, lien, empan |
| **Bloc à coins arrondis** | rayon, équerres d'angle (coins, épaisseur, longueur, couleur), bordure, fond, lien, empan |
| **Image légendée** | deux découpes indépendantes — photo et légende —, rapport, habillage à gauche/droite |
| **Décor** | forme du thème **ou tracé SVG libre**, couleur ou photo découpée, taille, décalages, translation, rotation, échelle, opacité, miroir, ombre, profondeur |
| **Héros fendu** | deux photographies et la forme qui les sépare — laquelle les DÉCOUPE. Choix des deux photos, cadrage et ancrage de chacune, opacité commune, noir et blanc ; forme du thème **ou tracé SVG libre**, couleur, placement, miroir ; hauteur, remontée du calque, largeur de lecture, balise. Il accueille n'importe quels blocs par-dessus |
| **Texte** | 13 rôles, du titre de héros à la mention technique · couleur, taille, police, graisse |
| **Bouton** | 4 variantes, coins carrés, largeur fixe, flèche, nouvel onglet |
| **Étiquette** | la pastille de fil conducteur, couleur de fond réglable |
| **Icône** | 41 symboles, cadre (cercle · médaillon · filigrane), taille, couleur |
| **Empilement** | sens, espacement, alignement, répartition, largeur maximale |

Marges, retraits, hauteur minimale, couleur et typographie passent par les
**réglages natifs de WordPress** (`supports`) et non par des panneaux maison :
les presets de theme.json y apparaissent d'eux-mêmes, à côté des valeurs
libres, et il n'y a rien à réécrire quand la charte change.

## La découpe à 45°

Une seule mécanique, au § 1 de `assets/css/blocks.css`, pilotée par des
propriétés personnalisées (`--cut-tl`, `--bw-top`…) et partagée par le bloc
croppé et le bloc Image. Deux couches :

- l'élément porte la **couleur de bordure** et la découpe extérieure ;
- `.cimo-cut__fill`, en retrait de l'épaisseur des bordures, porte le **fond**
  et une découpe intérieure **retraitée**.

C'est ce retrait qui fait que le filet garde la même épaisseur dans l'angle que
sur les côtés. Il vaut `d − w(2 − √2)` :

> La découpe extérieure suit `x + y = d`. La boîte intérieure est en retrait de
> `w`, sa découpe suit donc `x + y = 2w + d′`. La distance entre les deux
> droites vaut `|2w + d′ − d| / √2` ; l'égaler à `w` donne le résultat.

Les maquettes approchaient ce retrait à la main — 18 px pour une découpe de
20 px — ce qui amincit le filet en diagonale. Ici il est calculé.

**Deux conséquences à connaître :**

- La profondeur se règle **en pixels**, jamais en pourcentage : en %, les deux
  jambes de l'angle se rapportent à des dimensions différentes et l'angle cesse
  de valoir 45°. Le curseur ne propose donc que des pixels.
- **Masquer un bord, c'est lui donner une épaisseur nulle.** Il n'y a pas de
  réglage « bord visible » séparé : les quatre cases à cocher du panneau
  Bordure font exactement cela.

## Ajouter quelque chose

**Un petit bloc** → une entrée dans le tableau de `inc/blocks.php`. Le
JavaScript lit les définitions et fabrique l'interface. Formes gérées :
`styled-text`, `link`, `icon`, `stack`.

**Un rôle de texte** → une entrée dans `roles` du bloc `text`, plus une règle
`.cimo-t--<rôle>` au § 8 de `blocks.css`. Le rôle ne pose que ce qui fait
l'identité du niveau — graisse, interlettrage, capitales — pour que couleur,
taille et police restent réglables.

**Un bloc à interface sur mesure** → un dossier dans `blocks/` contenant
`block.json`, `index.js`, `index.asset.php` et `render.php`. `functions.php`
balaie le dossier : il n'y a rien à y déclarer. Les attributs partagés
(découpe, bordure, fond, lien, empan) s'obtiennent en ajoutant une ligne à
`cimo_block_attr_groups()` dans `inc/box.php`.

**Une forme décorative** → un SVG dans `assets/svg/` et un libellé dans
`cimo_shape_labels()`. Elle devient aussitôt disponible des deux côtés.

**Une icône** → un `<symbol id="icon-NOM" viewBox="…">` dans
`assets/img/icons.svg`. Si elle est dessinée **au trait**, lui donner
`data-stroke="1"` : `functions.php` lit cet attribut et bascule `fill` et
`stroke`. Sans cela, un tracé au trait s'affiche en pâté.

**Une composition** → un fichier dans `patterns/`, en-tête `Title:` + `Slug:` +
`Categories:`. Ce sont des fichiers **PHP** : on peut y boucler et interpoler
`get_theme_file_uri()`.

## La composition qui reste

Une seule : le fond du héros Éducation aux médias. Les textes et les boutons
se posent par-dessus, en blocs ordinaires — c'était la consigne.

| Composition | Contenu |
|---|---|
| `fond-hero-eam` | la pluie de 25 lames du diaphragme, dont 5 habillées d'une photo. Un bloc HTML d'un seul tenant : en blocs Décor, cela ferait trente blocs à régler un par un. Les cinq adresses d'images sont les seuls endroits à toucher. |

Le fond du héros d'**accueil** en était une lui aussi — deux blocs Image
portant chacun un `clip-path` dessiné point par point dans `patterns.css`.
C'est devenu le bloc **Héros fendu** : les photos se choisissent dans la
médiathèque, la forme qui les sépare dans une liste, et les polygones n'ont
plus à être redessinés pour en changer.

Dans les deux cas, le `<defs>` du SVG vit **dans le même `<svg>`** que les
`<use>` qui le référencent. Les maquettes le posaient en haut du `<body>` : le
canevas de l'éditeur étant une iframe, l'identifiant n'y aurait pas existé et
toutes les lames seraient restées vides côté rédaction.

## Le héros fendu

C'est le seul bloc dont la mécanique ne se devine pas à la lecture du rendu.

Les maquettes dessinaient **deux polygones de découpe**, un par photo, qui
laissaient entre eux la silhouette d'un éclair. Juste, mais figé : changer de
forme demandait de redessiner les deux polygones, point par point.

Le bloc part de la **forme seule** et en déduit les deux découpes :

| | |
|---|---|
| photo de gauche | tout ce qui est à gauche de la forme |
| photo de droite | tout ce qui est à droite de la forme |
| la forme | ce qui les sépare : le fond de la page, ou une couleur |

« Tout ce qui est à droite » s'obtient en **balayant** la forme vers la droite
jusqu'à couvrir la largeur : le bord gauche de l'empilement reste le bord
gauche de la forme, exactement. Le détail — et la liste de ce qu'il ne faut pas
défaire — est en tête de `inc/hero-split.php`.

**Trois limites, toutes de la même famille.** Elles tiennent au fait que le
balayage avance par pas de 3 % de la largeur de la forme, et que « à gauche de
la forme » veut dire à gauche de **toute** la forme :

- une forme **en sablier**, plus mince que le pas à la taille, laisse filer un
  filet de photo au travers. Les éclairs du thème sont deux fois plus épais que
  le pas à leur endroit le plus fin ;
- un bord **presque horizontal** s'escalade par petites marches, larges d'un
  pas. Les bords redressés, eux, sont nets ;
- le **creux d'un zigzag** n'est ni à gauche ni à droite de la forme : il y
  laisse voir le fond de la page. C'est cohérent avec la définition, mais cela
  se voit sur une forme très découpée.

L'éclair de l'accueil ne rencontre aucune des trois ; `eclair-3` rencontre les
deux dernières. C'est à regarder avant de choisir une forme.

**Le coût de peinture est réel** : environ 70 ms par repeinte sur une machine
sans accélération graphique, contre 17 ms pour le reste de la page. C'est payé
une fois à l'affichage, et à chaque redimensionnement de fenêtre. Trois
mécaniques ont été mesurées avant de retenir celle-ci ; `inc/hero-split.php`
dit lesquelles et pourquoi elles ont été écartées.

**La remontée et le rognage sont deux éléments distincts.** Le calque est
remonté de 15 % pour que la pointe de l'éclair tombe derrière le titre ; c'est
l'enveloppe, et non le calque, qui porte `overflow: hidden`. Dans la première
version du héros, c'était l'élément remonté lui-même : il n'y avait donc plus
rien pour rogner en haut, et la photo de droite passait par-dessus la barre de
navigation.

**Le séparateur doit traverser le héros de haut en bas.** Au-dessus et en
dessous de sa boîte, il n'y a plus rien pour départager les deux photos, et
c'est celle de droite qui l'emporte.

**Les photos sont des `<image>` SVG**, pas des `<img>` : elles n'ont donc ni
`srcset` ni chargement différé, et leur cadrage se règle par `preserveAspectRatio`
— neuf points d'ancrage, et non un pourcentage libre. Pour un cadrage au pixel
près, recadrer la photo dans la médiathèque.

## Le responsive

Le nombre de colonnes réglé dans l'éditeur s'applique **à partir de 1024 px**.
En dessous, section et grille dégradent d'elles-mêmes : deux colonnes, puis
une. Les cartes à empan suivent.

Il n'y a **pas de réglage par palier** : trois jeux de curseurs rendraient le
panneau illisible pour un gain rare. Si le besoin apparaît, c'est le § 3 de
`blocks.css` qu'il faut toucher.

L'habillage d'une image légendée ne s'applique qu'à partir de 768 px ; en
dessous elle passe en pleine largeur.

## Où vit quel CSS

| Fichier | Contenu |
|---|---|
| `style.css` | jetons, fond de page, couches z-index, en-tête, pied de page, garde-fous |
| `assets/css/blocks.css` | les douze blocs, en dix-sept sections |
| `assets/css/patterns.css` | les deux fonds de héros, et le titre alternant |
| `assets/css/editor.css` | **canevas** de l'éditeur : décors sélectionnables et en absolu, calque de clic neutralisé, blocs vides visibles |
| `assets/css/admin.css` | **panneaux** de l'éditeur. `add_editor_style()` ne les atteint pas : la colonne de droite est dans un autre document que le canevas. |

## Le titre alternant

Les deux phrases qui se relaient dans le héros d'accueil ne sont pas une
composition : poser **deux blocs Texte dans un Empilement** et donner à
celui-ci la classe `cimo-loop`. Les enfants sont superposés dans la même
cellule de grille et défilent à tour de rôle.

L'animation est coupée sous `prefers-reduced-motion`, et seule la première
phrase reste — un texte qui défile en boucle est inutilisable pour qui lit
lentement, et la maquette ne prévoyait aucun recours.

## Vérifications

```powershell
cd "C:\Users\Citizen Motion\Downloads\CiMo\Refonte\Proto1\wp"
docker compose exec -T wordpress sh -c 'cd /var/www/html/wp-content/themes/citizenmotion2 && for f in functions.php inc/*.php blocks/*/*.php patterns/*.php; do php -l "$f"; done | grep -v "^No syntax"'
```

Et **toujours dans l'éditeur**, pas seulement sur le site public : deux pannes
du V1 ont été invisibles côté visiteur et totales côté rédaction. Ouvrir la
page « Démo des blocs V2 », puis dans la console :

```js
const sel = wp.data.select('core/block-editor');
const t = []; (function w(bs){bs.forEach(b=>{t.push(b); if(b.innerBlocks) w(b.innerBlocks);});})(sel.getBlocks());
'blocs=' + t.length + ' invalides=' + t.filter(b=>!b.isValid).length
  + ' manquants=' + t.filter(b=>b.name==='core/missing').length;
```

La page de démonstration pose chaque bloc au moins une fois. Pour la
reconstruire après une modification :

```powershell
.\wp.ps1 eval '
wp_set_current_user( 1 );
$p = get_page_by_path( "demo-blocs-v2" );
wp_update_post( wp_slash( array( "ID" => $p->ID, "post_content" => file_get_contents( get_theme_file_path( "demo-blocs-v2.html" ) ) ) ) );
echo "reconstruite\n";'
```

`wp_set_current_user(1)` **et** `wp_slash()` : sans le premier, les blocs HTML
sont filtrés ; sans le second, la page affiche `u003cbr` en toutes lettres.

## Pièges rencontrés au portage des pages

Tous invisibles : aucun ne produit d'erreur, aucun ne laisse de trace dans le
journal.

| Piège | Symptôme | Cause |
|---|---|---|
| Slug de taille commençant par un chiffre | tous les titres à la taille héritée | WordPress ne génère **aucune** variable pour `--wp--preset--font-size--5xl`. Les slugs sont devenus `xxs`, `xxl`, `xxxl`, `display`, `hero` |
| `box-sizing` absent | deux boutons réglés à 14rem en mesuraient 18 | le retrait s'ajoutait à la largeur. Reset posé au § 2 de `style.css` |
| Règle de couches trop large | les équerres d'angle s'affichaient en bas de la carte, dans le texte | `position: relative` était imposé à TOUS les enfants d'une boîte, y compris à ceux qui doivent rester en absolu. On nomme désormais ce qui doit monter |
| Même règle, sur une section | le fond du héros d'accueil disparaissait | le groupe se positionne lui-même ; forcé en `relative`, il s'écrasait à zéro. D'où la classe d'échappement `.cimo-layer` |
| `float` dans une grille | les images légendées ne s'habillaient pas de texte | un élément de grille ne flotte pas. Le corps d'article repasse en flux (`.cimo-section.cimo-prose`) |
| Bloc statique auto-fermant | le filet sous les icônes ne s'affichait pas | `core/separator` est un bloc **statique** : son markup doit figurer dans la page, un commentaire auto-fermant ne rend rien |
| Spécificité d'un garde-fou | la pluie de lames tenait dans 50 px | `svg:not():not():not()` pesait plus lourd que n'importe quelle classe. Enveloppé dans `:where()`, il garde son rôle sans commander |
| Attribut en trop sur un bloc du cœur | « ce bloc contient du contenu invalide » sur les deux photos du héros d'accueil, rendu public parfait | `loading`, `decoding` et `fetchpriority` avaient été écrits à la main sur les `<img>`. Le `save()` de `core/image` n'écrit que `src`, `alt`, `class` et `style`, et la comparaison porte sur le **nombre** d'attributs : trois de trop suffisent. Ces trois-là sont posés au rendu par `wp_filter_content_tags()` — ne jamais les écrire dans le markup enregistré |
| Couleur de texte sur un séparateur | « contenu invalide » sur les trois cartes piliers de la page Éducation aux médias | `core/separator` déclare `supports.color.text = false` : `style.color.text` n'y existe pas, et son `save()` ne le resérialise donc pas. La couleur passe par **`backgroundColor`** — un slug de la palette —, que le bloc sérialise à la fois en `color` et en `background-color`, d'où ses cinq classes. Et `has-alpha-channel-opacity` est **toujours** présente : l'attribut `opacity` vaut « alpha-channel » par défaut |
| Alignement de texte écrit en attribut de premier niveau | tous les textes à gauche, alors que l'alignement est demandé partout | Le support s'appelle `typography.textAlign`, mais le cœur lit l'attribut dans **`style.typography.textAlign`**. Écrit en `textAlign` de premier niveau, il était enregistré dans la page, relu sans erreur et ignoré au rendu. Les titres du héros d'accueil et ceux des cartes s'affichaient à gauche |
| `feMorphology` à grand rayon | masque entièrement vide, les deux photos se recouvrent, rendu éditeur identique | Au-delà d'environ **255 pixels écran** de rayon, Chrome abandonne la primitive et rend du vide. Aucun message. Le rayon dépendant de la largeur rendue, que le serveur ignore, la primitive n'est plus employée pour les grands écarts |
| `feMorphology` à rayon vertical nul | même symptôme | La spécification demande de désactiver la primitive quand un rayon vaut zéro — ce que Chrome fait. Un millième de la hauteur suffit à la garder active |
| Spécificité du cœur sur le filet | le filet mesurait 100 px au lieu des 2,5 rem de la maquette | `.wp-block-separator:not(.is-style-wide):not(.is-style-dots) { width: 100px }` pèse trois classes, contre deux à `.wp-block-separator.cimo-rule`. Notre règle reprend désormais les deux `:not()` du cœur. Ce piège ne vit que si `add_theme_support( 'wp-block-styles' )` est posé — il l'est |

Et un **artefact d'outillage**, à ne pas confondre avec un piège du site :
les captures d'écran manquaient une des deux photos du héros, jamais la même.
Le navigateur peint sans attendre la fin du décodage. On avait posé
`decoding="sync"` sur les `<img>` — ce qui a cassé l'éditeur, voir la première
ligne du tableau. Le remède est côté capture, pas côté markup : attendre
`img.decode()` avant de déclencher la photographie.

## Points ouverts

- **Articulat CF n'existe qu'en graisse 400.** Le thème demande 800 et 900 pour
  les titres : les navigateurs fabriquent un faux gras, plus large et moins
  dense que l'Oswald condensé des maquettes. Les vraies graisses sont vendues
  séparément par Connary Fagen, et la **licence webfont reste à confirmer**.
- **Pas encore de moteur d'actualités.** Les grilles sont posées à la main. Le
  jour où les projets deviendront un type de contenu, la grille deviendra une
  boucle de requête — le bloc croppé est déjà prêt à y servir de carte.
- **Une seule icône par bloc Icône.** Les illustrations filaires des zones
  d'activité (Bruxelles, Liège…) des maquettes ne sont pas dans le sprite :
  ce sont des dessins, pas des pictogrammes.
- **Trois pages du thème V1 sont restées en base** — `association`, `contact`,
  `actualites`. Elles sont écrites avec les blocs du V1, qui n'existent plus
  ici : sous le thème V2 elles s'affichent cassées. Leur contenu est intact et
  revient en basculant sur le V1. Elles sont à refaire quand leurs maquettes
  existeront ; en attendant, le menu y renvoie.
- **La seconde phrase du titre alternant** de l'accueil est une proposition :
  la maquette portait un texte de remplissage, celle-ci reprend la formule du
  prototype V1. À valider ou à réécrire.
- `WP_DEVELOPMENT_MODE = 'theme'` dans `wp-config.php` court-circuite le cache
  des compositions. **À retirer en production.**
