Feature #1110
Updated by Redmine Admin about 1 month ago
## Spec — Polish visuel du PDF de contrat (#1110)
Moteur cible inchangé : Chromium/Paged.js via le builder PUR `server/src/application/functions/contract-html.builder.ts` + print CSS. Enrichissement CSS/structure uniquement, aucune nouvelle dépendance ni plomberie de données.
### Portée (builder pur, testable)
1. **Blocs de signature en encarts légers bordés** : fond #f9fafb, bordure #d1d5db, coins arrondis, padding ; aux 3 placements (left/right/side_by_side). Référence design #82.
2. **Champs société vides masqués** : un <p> ne contenant qu'un libellé « … : » sans valeur (ex. « N° SIRET : », « Code APE : », « Adresse du siege social : ») est RETIRE du rendu. Un champ renseigne (« N° SIRET : 123 ») est conserve. Les titres (h1/h2/h3) ne sont JAMAIS retires (helper pur borne, conservateur).
3. **Hygiene de substitution** : p/li devenus totalement vides (residus de balises blanchies, ex. <p>{{signature}}</p>) masques via CSS :empty.
4. **Polish typographique & identite** : corps justifie + cesure, titres d'article (h2/h3) stylises (filet d'accent), tableaux (en-tete trame, bordures fines), controle veuves/orphelines + pas de titre seul en bas de page (break-after: avoid).
### Hors scope
- Refonte des templates auteur (les encarts EXPLOITANT/destinataire vivent dans le HTML auteur) ; pas de page de garde ; pas de changement de moteur.
### Criteres d'acceptation
- Blocs de signature rendus en encarts bordes (#f9fafb / #d1d5db) aux 3 placements.
- <p> « label : » sans valeur retire ; <p> « label : valeur » conserve ; h1/h2/h3 jamais retires.
- Corps HTML preserve verbatim (hors lignes de champ vide) — non-regression des tests builder existants.
- Corps justifie ; titres d'article stylises ; en-tete/pied/pagination/signatures inchanges fonctionnellement.
- Tests builder (TDD) + e2e contrat verts ; lint/typecheck/test:unit verts ; preuve visuelle avant/apres.
### Portee : un seul lot backend (contract-html.builder.ts + tests). Route : backend (NestJS server/). Clot le dernier sous-ticket de l'epopee #82.