Переглянути джерело

feat(client-handover): 4-chapter doc structure + branded HTML/PDF rendering

bastien 1 тиждень тому
батько
коміт
a963faa764

+ 311 - 123
agents/client-handover-writer.md

@@ -1,6 +1,6 @@
 ---
 name: client-handover-writer
-description: Final ship-and-handover orchestrator. Runs SEO+GEO and HARDEN with auto-fix loops in parallel until each ≥17/20, commits/pushes, pauses for deploy confirmation, runs VALIDATE against live site, gates on all-scores ≥17/20, then synthesizes a non-technical client deliverable with before/after scores and an owner-maintenance checklist. Reads git history + .claude/memory/ registries. Optional manual SEO/GEO platform chapter for web/local-business projects and a build/deploy chapter.
+description: Final ship-and-handover orchestrator. Runs SEO+GEO and HARDEN with auto-fix loops in parallel until each ≥17/20, commits/pushes, pauses for deploy confirmation, runs VALIDATE against live site, gates on all-scores ≥17/20, then synthesizes a non-technical client deliverable as Markdown + branded HTML + PDF (ZenQuality cover page, Inter+Playfair Display typography, green palette). The deliverable is structured in 4 chapters: what was needed (and why), what was done (≤300 words, zero jargon, no internal tool names), what the client must do, and technical details for the curious. Reads git history + .claude/memory/ registries. Optional manual SEO/GEO platform chapter for web/local-business projects and a build/deploy chapter.
 tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, AskUserQuestion, Agent
 model: opus
 ---
@@ -9,19 +9,35 @@ model: opus
 
 ## GOAL
 
-Orchestrate a final **ship-and-handover pipeline** then produce a single Markdown
-deliverable (`LIVRAISON.md` or `HANDOVER.md`) that a non-technical client can
-read end-to-end and understand what was built, what was hardened in the final
-pass, and what they must do/maintain going forward.
+Orchestrate a final **ship-and-handover pipeline** then produce a triple
+deliverable next to each other on disk:
+- `LIVRAISON.md` / `HANDOVER.md` — source markdown (editable)
+- `LIVRAISON.html` / `HANDOVER.html` — branded HTML (browser-printable fallback)
+- `LIVRAISON.pdf` / `HANDOVER.pdf` — branded PDF (when a PDF engine is available)
+
+The branded HTML and PDF use the ZenQuality identity: green palette
+(`#1A3A25 / #2D5A3D / #4A7C59 / #87A878`), cream background `#F5F0EB`,
+Inter (body) + Playfair Display (headings), cover page with logo + tagline,
+running header/footer with project name and page numbers.
+
+The deliverable is structured in **4 chapters**, optimised for a non-technical
+client who reads top-to-bottom and may stop after chapter 2:
+1. **Ce qu'il fallait faire (et pourquoi)** — the brief and the underlying problem.
+2. **Ce qui a été fait** — lay summary, ≤300 words, zero jargon, **no internal
+   tool / skill names**.
+3. **Ce qui vous reste à faire** — action-only checklist grouped by cadence.
+4. **Détails techniques (pour les curieux)** — score table, key technical
+   choices, phases, optional glossary. Internal labels may appear here.
+Plus optional annex chapters: §5 external platforms (web), §6 build & deploy.
 
 Pipeline (each step gates the next):
-1. Baseline audits: /seo (SEO+GEO) and /harden in parallel.
+1. Baseline audits: SEO+GEO and security hardening in parallel.
 2. Fix loops: re-invoke each audit with auto-fix until ≥17/20 or `MAX_ITERATIONS` hit.
 3. Commit + push if files changed.
 4. Deploy pause: list deploy artifacts + process, wait for user confirmation.
-5. /validate against the deployed/live site.
-6. Per-audit gate: every score ≥17/20 OR stop + roadmap.
-7. Synthesize client deliverable with before/after scores + owner responsibilities.
+5. Live-site validation against the deployed URL.
+6. Per-axis gate: every score ≥17/20 OR stop + roadmap.
+7. Synthesize the markdown + render the branded HTML + PDF.
 
 Source of truth for the deliverable: git history since first commit + `.claude/memory/`
 registries (decisions, learnings, blockers, journal, evals). Output language follows
@@ -817,8 +833,32 @@ If false, the chapter focuses on general directory + AI search.
 
 ## STEP 12 — SYNTHESIZE THE DOCUMENT
 
-Generate the deliverable section by section. Translate headings to `LANG`.
-Tone: friendly, concrete, no jargon. One short paragraph per idea.
+Generate the deliverable as a tight 4-chapter structure: what was needed,
+what was done (lay summary), what the client must do, then technical
+details for the curious. Translate headings to `LANG`. Tone: friendly,
+concrete, no jargon. One short paragraph per idea.
+
+### Hard rules for this document
+
+1. **Never name internal tools or skill identifiers in chapters 1–3.**
+   Forbidden tokens (do not appear, in any case, in the lay portion):
+   `/seo`, `/harden`, `/validate`, `/cso`, `/feat`, `/bugfix`,
+   `/ship-feature`, `/ship`, `/code-clean`, `/refactor`, `seo-analyzer`,
+   `geo-analyzer`, `validator-analyzer`, `harden`-as-product-name,
+   `SEO.md`, `HARDEN.md`, `VALIDATE.md`, `CSO.md`, `MAX_ITERATIONS`,
+   `ALL_PASS`, `SCORE_*`. Replace with what they correspond to in client
+   language: référencement / visibilité IA / sécurité / conformité
+   technique / audit interne. Internal tool names may appear ONLY in
+   chapter 4 ("Détails techniques") inside the optional glossary.
+2. **Chapter 2 hard cap: 300 words max, zero technical jargon.** Plain
+   French (or plain English if `LANG=en`). No acronyms not already in
+   common usage (HTTPS is fine; CSP is not). Run `wc -w` against the
+   chapter body; if over 300, rewrite shorter.
+3. **Chapter 3 is action-only.** Every bullet starts with a verb the
+   client can act on without a developer.
+4. **Chapter 4 may use technical terms** (SEO, GEO, HSTS, CSP, etc.) but
+   each term gets a one-line plain-language definition the first time it
+   appears, or a glossary at the end of the chapter.
 
 ### Document structure
 
@@ -830,102 +870,78 @@ Tone: friendly, concrete, no jargon. One short paragraph per idea.
 > Ce document récapitule l'ensemble du travail réalisé sur votre projet
 > du JJ/MM/AAAA au JJ/MM/AAAA.
 
-## 1. En une minute
+## 1. Ce qu'il fallait faire (et pourquoi)
 
-[2-3 sentences. What is the project, what does it do, current state.]
+[Briefing + motivation. 100–180 words max. Two short paragraphs.
+- §1.1 (the brief): what the client wanted, in their own words if
+  possible. Pull from the project journal's earliest entry, the README,
+  or the first commit message.
+- §1.2 (the why): the underlying problem this project solves for the
+  client (no audience, weak online presence, manual process to
+  automate, broken legacy site, etc.). Concrete. Their reality, not
+  ours.
 
-## 2. Ce que vous avez maintenant
+End the chapter with a one-line success criterion in their words —
+"À la livraison, vous deviez pouvoir ___." If unknown, omit rather
+than invent.]
 
-[Bullet list of features as USER BENEFITS. Pull from journal + commit clusters.]
+## 2. Ce qui a été fait
 
-## 3. Comment on en est arrivé là
+[**HARD CAP: 300 words. ZERO technical jargon.** This is the chapter the
+client reads first, possibly the only one they read.
 
-[3 to 7 phases. For each: what was done, why it mattered. Plain phase names.]
+Structure as a single short narrative + a tight bullet list of
+user-visible benefits:
 
-## 4. État de santé du site (avant / après)
+  Para 1 (3–5 sentences): the project today, in their words. What it
+  looks like to a visitor, what the client can do with it. NOT what
+  technologies were used.
 
-[NEW SECTION — score table from STEP 8. SEO classique and GEO (IA) are
-shown on separate rows so the client sees both axes explicitly.]
+  Bullet list (5–10 items): visible benefits, each phrased as something
+  the client or their visitors can now do that they couldn't before.
+  Pattern: "Vos visiteurs peuvent ___" / "Vous pouvez ___" /
+  "Le site est maintenant ___".
 
-Avant la passe finale → après la passe finale (cette semaine) :
+Forbidden in this chapter: framework names, audit names, score numbers,
+file paths, package names, command-line tool names, anything ending in
+`.md`, `.json`, `.yaml`. If you cannot describe a feature without one
+of those, the feature belongs in chapter 4, not here.
 
-| Domaine                                  | Avant      | Après      | Statut |
-|------------------------------------------|-----------:|-----------:|:------:|
-| Référencement Google (SEO classique)     | <X.X>/20   | <Y.Y>/20   | ✅     |
-| Visibilité IA (GEO — ChatGPT, Perplexity)| <X.X>/20   | <Y.Y>/20   | ✅     |
-| Sécurité du site                         | <X.X>/20   | <Y.Y>/20   | ✅     |
-| Conformité technique (W3C)               | —          | <Z.Z>/20   | ✅     |
-
-[If LANG=en: "Site health (before / after)" with the same columns.
-Use these column labels: "Domain" / "Before" / "After" / "Status".
-Row labels: "Google search (classical SEO)", "AI visibility (GEO —
-ChatGPT, Perplexity)", "Site security", "Technical compliance (W3C)".]
-
-Plain explanation under the table:
-- **Référencement Google (SEO classique)** = comment Google, Bing et
-  les autres moteurs traditionnels trouvent et classent votre site.
-  C'est ce qui amène la majorité du trafic aujourd'hui.
-- **Visibilité IA (GEO)** = comment les moteurs de recherche par IA
-  (ChatGPT, Perplexity, Gemini, Google AI Overviews) lisent et citent
-  votre site. Trafic encore minoritaire mais en forte croissance —
-  votre site est maintenant prêt pour ce canal (llms.txt, données
-  structurées pour extraction IA, signaux d'entité).
-- **Sécurité** = protections contre les attaques courantes (en-têtes
-  HTTPS, anti-injection, etc.).
-- **Conformité technique** = respect des standards web (HTML, CSS,
-  accessibilité). Ouvert dans la plupart des navigateurs et lecteurs
-  d'écran sans bug.
-
-[If any score had a notable jump, add a one-liner: "La sécurité est passée
-de 12 à 18 — on a ajouté les en-têtes manquants et forcé le passage en
-HTTPS." Do the same for SEO and GEO independently if either jumped.]
-
-## 5. Les choix importants qu'on a faits
-
-[Vulgarize BDR entries. 3-7 decisions max — design, framework, security,
-hosting choices the client would care about.]
-
-## 6. Ce qu'on a appris en route (optionnel)
-
-[Only if learnings.md has client-relevant entries. 3-5 bullets max.]
-
-## 7. Ce qui reste à faire ou à surveiller
+After drafting, count words. Cap at 300. If over, cut paragraphs not
+bullets — bullets are the value-dense part.]
 
-[From blockers.md (open) + code TODOs. Plain description, urgency,
-trigger.]
+## 3. Ce qui vous reste à faire
 
-## 8. Comment utiliser le projet au quotidien
+[Action-only checklist for the client. Pull from: open `blockers.md`
+entries, ongoing-monitoring items, external platforms to claim,
+content updates only the client can make, deploy steps if self-hosted.
 
-[1-page guide for the client to USE what was delivered. URL, CMS, contact.]
-
-## 9. Ce que vous devez faire et maintenir vous-même
-
-[NEW CONSOLIDATED SECTION — explicit owner-responsibility checklist.
-Pull from: SEO/GEO chapter actions, deploy chapter actions, blockers,
-ongoing-monitoring items.
-
-Format as actionable checklist grouped by cadence:
+Format as a checklist grouped by cadence. Every line starts with a
+verb. Every line is something the client can do without a developer.
 
 ### Une fois (à faire dans les premières semaines)
 - [ ] Réclamer la fiche Google Business Profile et la vérifier (lien : ...)
 - [ ] Compléter le profil Apple Business Connect (lien : ...)
-- [ ] Vérifier la cohérence NAP (Nom / Adresse / Téléphone) sur toutes
-      les plateformes — voir tableau au §10
-- [ ] [Si self-host : configurer le certificat SSL (renouvellement auto Let's Encrypt)]
-- [ ] [Si self-host : programmer une sauvegarde quotidienne]
+- [ ] Vérifier la cohérence Nom / Adresse / Téléphone sur toutes les
+      plateformes — voir l'annexe à la fin du document
+- [ ] [Si vous gérez l'hébergement vous-même : configurer le certificat
+      de sécurité (renouvellement automatique recommandé)]
+- [ ] [Si vous gérez l'hébergement vous-même : programmer une sauvegarde
+      quotidienne]
 - [ ] Sauvegarder ce document hors du dépôt (PDF, email)
 
 ### Mensuel
-- [ ] Ajouter / mettre à jour 5 photos sur Google Business
-- [ ] Répondre aux avis Google (positifs et négatifs)
+- [ ] Ajouter ou mettre à jour 5 photos sur Google Business
+- [ ] Répondre aux avis Google (positifs et négatifs) sous 48 h
 - [ ] Vérifier que le site est toujours en ligne (test simple : ouvrir
       l'URL depuis un autre appareil)
-- [ ] [Si CMS : mettre à jour les contenus saisonniers]
+- [ ] [Si système de gestion de contenu : mettre à jour les contenus
+      saisonniers]
 
 ### Trimestriel
 - [ ] Faire un test de visibilité IA : taper le nom du commerce dans
       ChatGPT, Perplexity, Gemini. Noter ce qui s'affiche.
-- [ ] Demander à 3-5 clients de laisser un avis Google
+- [ ] Demander à 3–5 clients de laisser un avis Google
 - [ ] Publier un post Google Business (offre, événement, actualité)
 
 ### Annuel
@@ -934,25 +950,90 @@ Format as actionable checklist grouped by cadence:
 - [ ] Renouveler les noms de domaine
 
 ### Quand quelque chose change dans la vie du commerce
-- [ ] Changement d'adresse / téléphone / horaires → modifier d'abord sur
-      Google Business, puis sur toutes les autres plateformes (la
-      cohérence est cruciale, voir §10)
+- [ ] Changement d'adresse, de téléphone ou d'horaires → modifier
+      d'abord sur Google Business, puis sur toutes les autres
+      plateformes (la cohérence est cruciale)
 
 [Adapt cadences to project type. For SaaS / non-local: replace
-Google Business with appropriate platforms.]
-```
+Google Business cadences with appropriate platforms (Slack, App Store,
+Play Store, Trustpilot, G2, Capterra, etc.). For pure tooling /
+internal projects, this chapter may shrink to a 5-line "à surveiller"
+list — that is fine, do not pad.]
+
+## 4. Détails techniques (pour les curieux)
+
+[Same content as before but consolidated and labelled as the
+technical-depth chapter. Internal tool names may appear here.
+The client is not required to read this chapter.]
+
+### 4.1 État de santé du site (avant / après)
+
+| Domaine                                  | Avant      | Après      | Statut |
+|------------------------------------------|-----------:|-----------:|:------:|
+| Référencement Google (recherche classique)| <X.X>/20   | <Y.Y>/20   | ✅     |
+| Visibilité IA (ChatGPT, Perplexity, Gemini)| <X.X>/20   | <Y.Y>/20   | ✅     |
+| Sécurité du site                          | <X.X>/20   | <Y.Y>/20   | ✅     |
+| Conformité technique                      | —          | <Z.Z>/20   | ✅     |
+
+[If LANG=en: "Site health (before / after)" with the same columns.
+Use these column labels: "Domain" / "Before" / "After" / "Status".
+Row labels: "Google search (classical)", "AI visibility (ChatGPT,
+Perplexity, Gemini)", "Site security", "Technical compliance".]
+
+Lecture rapide :
+- **Référencement Google** = comment Google, Bing et les autres moteurs
+  classiques trouvent et classent votre site. Majorité du trafic
+  aujourd'hui.
+- **Visibilité IA** = comment les moteurs par IA (ChatGPT, Perplexity,
+  Gemini, Google AI Overviews) lisent et citent votre site. Trafic
+  minoritaire mais en croissance forte — votre site est désormais prêt
+  pour ce canal.
+- **Sécurité** = protections contre les attaques courantes (chiffrement
+  HTTPS, en-têtes anti-injection, redirections sûres).
+- **Conformité technique** = respect des standards web (HTML, CSS,
+  accessibilité). Ouvert dans la plupart des navigateurs et lecteurs
+  d'écran sans bug.
+
+[If any score had a notable jump, add a one-liner per axis: "La sécurité
+est passée de 12 à 18 — en-têtes manquants ajoutés, passage HTTPS forcé."]
+
+### 4.2 Choix techniques importants
 
-## 10. [SEO/GEO manual chapter — web projects only — see STEP 13]
+[Vulgarize 3–7 BDR entries. Design, framework, security, hosting
+decisions the client would care about. One paragraph each:
+what was chosen, why over the alternative, what it changes for the
+client. Drop entries the client cannot act on or care about.]
 
-## 11. [Build & deploy chapter — only if Q1=Yes — see STEP 14]
+### 4.3 Comment on en est arrivé là (phases)
 
-## 12. Pour aller plus loin
+[3–7 phases. For each: what was done, why it mattered, in technical
+detail this time. Reference commit clusters from STEP 10. Plain phase
+names, not skill names.]
 
-[3-5 concrete suggestions. Phrase as opportunities.]
+### 4.4 Glossaire (optionnel)
 
-## Annexe — Détails techniques
+[Include only if at least 4 of the terms below appear in chapter 4.
+Format: term — one-line plain-language definition. Sort alphabetically.
+This is the ONLY place internal tooling names may be mentioned by
+their internal label, and only when explaining what they correspond
+to.]
 
-[Pointer for the technically curious — README, source repo, etc.]
+- **SEO (référencement classique)** — ensemble des pratiques pour
+  apparaître dans Google, Bing, DuckDuckGo.
+- **GEO (visibilité IA)** — équivalent du SEO pour les moteurs par IA
+  comme ChatGPT, Perplexity, Gemini.
+- **HSTS** — en-tête HTTP qui force la navigation en HTTPS.
+- **CSP (Content Security Policy)** — règle qui limite ce que le
+  navigateur charge depuis le site, pour bloquer les injections.
+- **WCAG** — standard d'accessibilité (AA = niveau recommandé).
+- **Schema.org / JSON-LD** — annotations cachées qui aident moteurs et
+  IA à comprendre le contenu.
+- **llms.txt** — fichier qui dit aux moteurs IA quel est le contenu
+  important du site.
+
+## 5. Annexe — Plateformes externes (web)
+
+## 6. Annexe — Build & déploiement (optionnel)
 
 ---
 
@@ -963,20 +1044,24 @@ des audits de santé. Pour toute question, contactez [contact].*
 ### Tone rules
 
 1. Address the client directly ("votre site", "vous pouvez").
-2. Replace tech terms with user-facing equivalents.
-3. No abbreviations the client wouldn't use.
+2. Chapters 1–3: replace every tech term with a user-facing equivalent.
+3. No abbreviations the client wouldn't use (HTTPS yes, CSP no — unless
+   in chapter 4 with definition).
 4. Concrete numbers > adjectives.
 5. Short paragraphs. Bullet lists for things you can count.
 6. **Score deltas explained in plain words**. Never just dump numbers.
-7. **Owner-responsibility section is action-oriented**. Every line starts
-   with a verb. Every line is something the client can do without a dev.
+7. **Chapter 3 is action-oriented**. Every line starts with a verb.
+   Every line is something the client can do without a developer.
+8. **No skill-name leaks in chapters 1–3.** See "Hard rules" above.
 
 ---
 
 ## STEP 13 — SEO/GEO MANUAL CHECKLIST (web projects only)
 
 If `PROJECT_TYPE=web` AND `--skip-seo` NOT set, append this chapter
-(numbered §10 in the doc).
+as **§5 Annexe — Plateformes externes** in the new 4-chapter structure
+(see STEP 12). Replace the §5 stub with the full content rendered from
+the resource file.
 
 Read the resource file:
 `$HOME/.claude/skills/client-handover/checklists/seo-geo-manual.md`
@@ -1027,7 +1112,9 @@ that are recurring belong in §9's cadence checklist.
 
 ## STEP 14 — BUILD & DEPLOY CHAPTER (only if Q1=Yes)
 
-For each `DEPLOY_HINTS` match, generate a short subsection:
+If included, this becomes **§6 Annexe — Build & déploiement** in the new
+4-chapter structure (see STEP 12). For each `DEPLOY_HINTS` match,
+generate a short subsection:
 1. What this means (1 paragraph).
 2. First-time setup (numbered steps + signup link).
 3. Day-to-day deploy (typical command / click sequence).
@@ -1045,7 +1132,7 @@ For each: signup + 5-step deploy walkthrough.
 
 ---
 
-## STEP 15 — WRITE OUTPUT
+## STEP 15 — WRITE MARKDOWN OUTPUT
 
 Default output path: project root.
 - `LIVRAISON.md` if `LANG=fr`
@@ -1058,30 +1145,125 @@ If a file at that path already exists, AskUserQuestion:
 
 Write the file with the `Write` tool.
 
-Sanity check:
+Sanity checks (do them in this order, before STEP 16):
+
+```bash
+wc -l <output>                          # expect 250-800 lines
+grep -c "^## " <output>                 # expect 4-6 top-level chapters
+                                        #   §1, §2, §3, §4, [§5 web], [§6 deploy]
+```
+
+**Chapter 2 word-count gate.** Extract the body of `## 2. Ce qui a été fait`
+(or `## 2. What we did` if `LANG=en`) and run `wc -w` on it. **Hard cap:
+300 words.** If over, edit the chapter (remove paragraphs, keep bullets)
+and re-write before moving to STEP 16. Do not skip this gate — chapter 2
+is the part the client reads first.
+
+```bash
+awk '/^## 2\. /{flag=1; next} /^## 3\. /{flag=0} flag' "$OUTPUT" | wc -w
+# expected: ≤ 300
+```
+
+**Skill-name leak gate.** Forbidden tokens must NOT appear in chapters
+1–3 (chapter 4 may use them in the optional glossary):
+
+```bash
+awk '/^## 1\./{flag=1} /^## 4\./{flag=0} flag' "$OUTPUT" \
+  | grep -niE '/(seo|harden|validate|cso|feat|bugfix|ship-feature|ship|code-clean|refactor)\b|seo-analyzer|geo-analyzer|validator-analyzer|SEO\.md|HARDEN\.md|VALIDATE\.md|CSO\.md|MAX_ITERATIONS|ALL_PASS|SCORE_[A-Z_]+'
+# expected: no matches. Each match is a leak — rewrite the offending
+# chapter in client language before STEP 16.
+```
+
+If either gate fails, fix and re-write the markdown before continuing.
+
+---
+
+## STEP 16 — RENDER BRANDED HTML + PDF
+
+Always produce a branded `.html` next to the `.md`. Produce a branded
+`.pdf` when a PDF engine is available on the host. The file is the
+client-visible deliverable.
+
+### Inputs already known
+
+| Variable          | Source                                      |
+|-------------------|---------------------------------------------|
+| `OUTPUT_MD`       | path written in STEP 15                     |
+| `LANG`            | from STEP 1                                 |
+| `PROJECT_NAME`    | `PROJECT_ROOT` basename or `package.json` `name` |
+| `CLIENT_NAME`     | from journal first entry, README, or AskUserQuestion |
+| `PROJECT_PERIOD`  | `<first commit date> → <last commit date>` (DD/MM/YYYY) |
+| `PROJECT_URL`     | `DEPLOYED_URL` from STEP 6 (or `—` if none) |
+
+If `CLIENT_NAME` is unknown after best-effort detection, ask once with
+AskUserQuestion: `"Nom du client à afficher sur la couverture du PDF
+(ou laisser vide pour ne rien afficher)?"`. A blank answer becomes `—`.
+
+### Run the renderer
+
 ```bash
-wc -l <output>          # expect 200-800 lines
-grep -c "^## " <output> # expect 8-13 chapters (NEW: §4 health, §9 owner-resp)
+PROJECT_NAME="$PROJECT_NAME" \
+CLIENT_NAME="$CLIENT_NAME" \
+PROJECT_PERIOD="$PROJECT_PERIOD" \
+PROJECT_URL="$PROJECT_URL" \
+LANG="$LANG" \
+"$HOME/.claude/skills/client-handover/scripts/handover-to-pdf.sh" \
+  "$OUTPUT_MD"
 ```
 
+The renderer:
+1. Converts the markdown to HTML using the first available engine
+   (pandoc > python-markdown > `npx marked`).
+2. Wraps the body in the ZenQuality template (cover page + branded
+   typography Inter + Playfair Display, ZenQuality green palette
+   `#1A3A25 / #2D5A3D / #4A7C59 / #87A878`, cream page background
+   `#F5F0EB`).
+3. Embeds the ZenQuality logo (default: `https://zenquality.fr/logo-horizontal.svg`;
+   override with `LOGO_URL` env var to use a local file).
+4. Emits `LIVRAISON.html` (or `HANDOVER.html`) next to the `.md`.
+5. Tries PDF engines in order: weasyprint > wkhtmltopdf > chromium >
+   chromium-browser > google-chrome. First match writes
+   `LIVRAISON.pdf` (or `HANDOVER.pdf`).
+6. If no PDF engine is available, exits with code 2 and prints
+   install hints. The HTML file is still produced and viewable —
+   the user can "Print → Save as PDF" from any modern browser.
+
+### Exit code handling
+
+| `$?` | Meaning                                       | Action |
+|------|-----------------------------------------------|--------|
+| 0    | HTML and PDF written                          | continue to STEP 17 |
+| 2    | HTML written, no PDF engine on host           | continue to STEP 17 — final report mentions PDF as MISSING and lists install commands |
+| 1    | Fatal (bad args, unwritable dir, conv error)  | escalate to user with the script's stderr |
+
+### Re-rendering on overwrite (option B in STEP 15)
+
+If STEP 15 chose option B (`LIVRAISON-YYYY-MM-DD.md` versioned),
+the renderer produces matching `LIVRAISON-YYYY-MM-DD.html` and
+`LIVRAISON-YYYY-MM-DD.pdf`. Pass the versioned path as `$OUTPUT_MD`.
+
 ---
 
-## STEP 16 — FINAL REPORT
+## STEP 17 — FINAL REPORT
 
 Output to the user:
 
 ```
 DONE — ship-and-handover pipeline complete.
 
-OUTPUT: <path>
+OUTPUT:
+  Markdown:  <path-to-md>
+  HTML:      <path-to-html>
+  PDF:       <path-to-pdf>     (or: NOT GENERATED — see install hints below)
 LANGUAGE: fr | en
 PROJECT TYPE: web (local-business) | web | cli | library | mobile | other
 COMMITS ANALYZED: <count> from <first date> to <last date>
 
 PIPELINE RESULT (web):
-  SEO       <BEFORE>/20 → <AFTER>/20  ✅ (iterations: <N>)
-  HARDEN    <BEFORE>/20 → <AFTER>/20  ✅ (iterations: <N>)
-  VALIDATE  —          → <AFTER>/20   ✅ (post-deploy)
+  SEO classique  <BEFORE>/20 → <AFTER>/20  ✅ (iterations: <N>)
+  GEO (IA)       <BEFORE>/20 → <AFTER>/20  ✅ (iterations: <N>)
+  HARDEN         <BEFORE>/20 → <AFTER>/20  ✅ (iterations: <N>)
+  VALIDATE       —          → <AFTER>/20   ✅ (post-deploy)
 
 PIPELINE RESULT (non-web):
   CSO       <BEFORE>/20 → <AFTER>/20  ✅ (iterations: <N>)
@@ -1092,23 +1274,29 @@ DECISIONS VULGARIZED: <count>
 BLOCKERS REMAINING: <count> (open)
 
 DOC SECTIONS WRITTEN:
-  §1-3  Project recap
-  §4    Site health (before/after)            ← NEW
-  §5    Key decisions
-  §6    Lessons learned (optional)
-  §7    Open items / things to monitor
-  §8    Day-to-day usage
-  §9    Owner responsibilities checklist      ← NEW
-  §10   SEO/GEO manual chapter (web)
-  §11   Build & deploy chapter (only if requested)
-  §12   Pour aller plus loin
+  §1   Ce qu'il fallait faire (et pourquoi)
+  §2   Ce qui a été fait                  (≤ 300 mots, sans jargon)
+  §3   Ce qui vous reste à faire          (action checklist)
+  §4   Détails techniques                 (scores, choix, glossaire)
+  §5   Annexe — plateformes externes      (web only)
+  §6   Annexe — build & déploiement       (only if requested)
 
 Next steps for the user:
-1. Read the document end-to-end before sending — fill any
-   [À COMPLÉTER] / [À CONFIRMER] markers (especially NAP in §10).
-2. Save a copy outside the repo (PDF, email).
-3. Walk through §9 (owner responsibilities) with the client during the
-   handover meeting — it's the part they MUST act on.
+1. Open <path-to-pdf> (or the .html) — verify cover page, branding,
+   and that the score table renders. Adjust the .md if needed and
+   re-run STEP 16 to regenerate.
+2. Read the document end-to-end before sending — fill any
+   [À COMPLÉTER] / [À CONFIRMER] markers (NAP in §5 especially).
+3. Save a copy outside the repo (the .pdf is already client-ready).
+4. Walk through §3 (ce qui vous reste à faire) with the client
+   during the handover meeting — that's the part they MUST act on.
+
+[If PDF was NOT generated, append:]
+PDF NOT GENERATED — no PDF engine on this host. Install one of:
+  - weasyprint   pip install --user weasyprint   (or: pipx install weasyprint)
+  - wkhtmltopdf  apt install wkhtmltopdf
+  - chromium     apt install chromium-browser
+Then re-run only STEP 16 (the .md does not need to change).
 ```
 
 If anything was skipped or uncertain, list under `CONCERNS:`.

+ 24 - 18
skills/client-handover/SKILL.md

@@ -3,17 +3,22 @@ name: client-handover
 description: |
   Final ship-and-handover orchestrator. End-to-end pipeline that hardens the
   project, commits, pauses for deploy, validates the live site, and only then
-  generates the non-technical client deliverable (LIVRAISON.md / HANDOVER.md).
-  Pipeline: (1) /seo (SEO+GEO) and /harden run in parallel with auto-fix loops
-  until each score ≥17/20, (2) /commit-change + push if changes made, (3) pause
-  to tell user what to deploy and wait for confirmation, (4) /validate against
-  the live site, (5) per-audit gate ≥17/20 — stop and analyze if any below,
-  (6) write client doc with before/after score table and explicit
-  owner-maintenance checklist. Reads git history + .claude/memory/ registries.
-  For local-business projects, appends manual SEO/GEO platform checklist (NAP
-  consistency across Google Business, Pages Jaunes, Yelp, Facebook, Instagram,
-  TikTok, Apple Maps, Bing Places, TripAdvisor, etc.). Asks whether to include
-  build/deploy chapter.
+  generates the non-technical client deliverable as Markdown + branded HTML +
+  PDF (ZenQuality identity: green palette, Inter + Playfair Display fonts,
+  cover page with logo and tagline). The deliverable uses a 4-chapter
+  structure: §1 what was needed and why, §2 what was done (≤300 words, zero
+  jargon, no internal tool/skill names), §3 what the client must do (action
+  checklist), §4 technical details for the curious (scores, key choices,
+  glossary). Pipeline: (1) /seo (SEO+GEO) and /harden run in parallel with
+  auto-fix loops until each score ≥17/20, (2) /commit-change + push if
+  changes made, (3) pause to tell user what to deploy and wait for
+  confirmation, (4) /validate against the live site, (5) per-axis gate
+  ≥17/20 — stop and analyze if any below, (6) write client doc + render
+  branded HTML/PDF. Reads git history + .claude/memory/ registries. For
+  local-business projects, appends manual SEO/GEO platform checklist (NAP
+  consistency across Google Business, Pages Jaunes, Yelp, Facebook,
+  Instagram, TikTok, Apple Maps, Bing Places, TripAdvisor, etc.). Asks
+  whether to include build/deploy chapter.
   Trigger: "client handover", "compte rendu client", "livraison client",
   "synthese projet", "rapport client", "deliverable", "summary for client",
   "recap projet", "handover doc", "livrable", "ship and handover",
@@ -51,13 +56,14 @@ The agent runs a **ship-and-handover pipeline** with explicit gates:
 5. **DEPLOY PAUSE** — List exact deploy artifacts: changed files since baseline, deploy hints from project (vercel.json, netlify.toml, Dockerfile, .github/workflows/deploy.yml, etc.), and the deploy process in plain words. Use AskUserQuestion: "Deploy done? (Yes / Not yet / Skip validate)". Block until Yes or Skip.
 6. **/validate (live site)** — Run validator-analyzer against the deployed URL. Capture `SCORE_VALIDATE`.
 7. **GATE — per-axis threshold ≥17/20** — Compute final `SCORE_*_AFTER` for SEO classique, GEO (IA), HARDEN, VALIDATE. If ANY < 17/20: STOP. Generate `.claude/audits/HANDOVER-ROADMAP.md` with prioritized analysis of what's blocking each below-threshold axis. Do NOT write the client deliverable. Report to user.
-8. **DOC GENERATION (only if all scores ≥17/20)** — Read `.claude/memory/` registries + full git history. Ask whether to include build/deploy chapter. Synthesize concise client deliverable with:
-   - Before/after score table with SEO classique and GEO (IA) on separate rows, plus HARDEN and VALIDATE — values + delta. SEO classique, GEO, HARDEN and VALIDATE are gated independently — each must reach ≥17/20 for the pipeline to pass.
-   - Plain-language summary of all changes since first commit.
-   - **Owner responsibilities** section: explicit checklist of what the client must do / maintain (SEO platforms, content updates, monitoring, deploy if self-hosted).
-   - Optional build/deploy chapter.
-   - For web projects with local-business signals: manual SEO/GEO platform checklist with registration links.
-9. **OUTPUT** — Write to `LIVRAISON.md` (fr) or `HANDOVER.md` (en) at project root.
+8. **DOC GENERATION (only if all scores ≥17/20)** — Read `.claude/memory/` registries + full git history. Ask whether to include build/deploy chapter. Synthesize the client deliverable using the 4-chapter structure:
+   - **§1 Ce qu'il fallait faire (et pourquoi)** — brief + motivation, 100–180 words.
+   - **§2 Ce qui a été fait** — lay summary, **≤300 words, zero technical jargon**, **no internal tool/skill names** (no `/seo`, `/harden`, `/validate`, `seo-analyzer`, etc. — replace with concept names: référencement / sécurité / conformité technique). Forbidden-token grep gate runs before write.
+   - **§3 Ce qui vous reste à faire** — action-only checklist grouped by cadence (one-time / monthly / quarterly / yearly / when something changes).
+   - **§4 Détails techniques (pour les curieux)** — score table (SEO classique + GEO + sécurité + conformité, before/after, gated independently at ≥17/20), vulgarized BDR decisions, phases with technical detail, optional glossary.
+   - **§5 Annexe — plateformes externes** (web/local-business only).
+   - **§6 Annexe — build & déploiement** (only if requested).
+9. **RENDER** — Write `LIVRAISON.md` (fr) or `HANDOVER.md` (en) at project root, then run `scripts/handover-to-pdf.sh` to produce the matching branded `.html` (always) and `.pdf` (when a PDF engine is on the host: weasyprint > wkhtmltopdf > chromium). HTML/PDF use the ZenQuality cover page, green palette, Inter + Playfair Display typography, running header/footer with project name + page numbers.
 
 Flags:
 - `--skip-fix-loop` — run baseline audits once, skip auto-fix iterations.

+ 43 - 0
skills/client-handover/resources/branding/zenquality-template.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="{{LANG}}">
+<head>
+  <meta charset="UTF-8">
+  <title>{{TITLE}}</title>
+  <style>
+{{CSS}}
+  </style>
+</head>
+<body>
+
+<section class="cover">
+  <div class="cover-header">
+    <img class="cover-logo" src="{{LOGO_URL}}" alt="ZenQuality">
+    <div class="cover-tagline">La sérénité numérique,<br>la qualité en plus</div>
+  </div>
+
+  <div class="cover-body">
+    <div class="cover-eyebrow">{{EYEBROW}}</div>
+    <h1 class="cover-title">{{COVER_TITLE}}</h1>
+    <p class="cover-subtitle">{{COVER_SUBTITLE}}</p>
+
+    <div class="cover-meta">
+      <div><strong>{{LABEL_CLIENT}}</strong> {{CLIENT_NAME}}</div>
+      <div><strong>{{LABEL_PROJECT}}</strong> {{PROJECT_NAME}}</div>
+      <div><strong>{{LABEL_DATE}}</strong> {{DATE_HUMAN}}</div>
+      <div><strong>{{LABEL_PERIOD}}</strong> {{PROJECT_PERIOD}}</div>
+      <div><strong>{{LABEL_URL}}</strong> <a href="{{PROJECT_URL}}">{{PROJECT_URL}}</a></div>
+    </div>
+  </div>
+
+  <div class="cover-footer">
+    <span>ZenQuality — {{LABEL_PREPARED_BY}}</span>
+    <a href="https://zenquality.fr">zenquality.fr</a>
+  </div>
+</section>
+
+<main class="content" data-title="{{TITLE}}">
+{{CONTENT}}
+</main>
+
+</body>
+</html>

+ 438 - 0
skills/client-handover/resources/branding/zenquality.css

@@ -0,0 +1,438 @@
+/*
+ * ZenQuality — client handover stylesheet
+ * Used to render LIVRAISON.md / HANDOVER.md as a branded HTML/PDF.
+ * Source brand tokens: zenquality.fr (CSS custom properties extracted from
+ * the live site) — Inter (body) + Playfair Display (headings), green palette.
+ */
+
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@400;600;700&display=swap');
+
+:root {
+  --green-dark: #1A3A25;
+  --green-forest: #2D5A3D;
+  --green-moss: #4A7C59;
+  --green-sage: #87A878;
+  --black-deep: #0A0A0A;
+  --black-soft: #1A1A1A;
+  --gray-dark: #2A2A2A;
+  --gray-mid: #666666;
+  --gray-light: #B0B0B0;
+  --white-cream: #F5F0EB;
+  --white-pure: #FFFFFF;
+
+  --status-ok: #2D5A3D;
+  --status-warn: #b58900;
+  --status-fail: #a83232;
+}
+
+@page {
+  size: A4;
+  margin: 22mm 18mm 22mm 18mm;
+  @top-right {
+    content: string(doctitle);
+    font-family: 'Inter', sans-serif;
+    font-size: 8.5pt;
+    color: var(--green-moss);
+  }
+  @bottom-right {
+    content: counter(page) " / " counter(pages);
+    font-family: 'Inter', sans-serif;
+    font-size: 8.5pt;
+    color: var(--gray-mid);
+  }
+  @bottom-left {
+    content: "ZenQuality — zenquality.fr";
+    font-family: 'Inter', sans-serif;
+    font-size: 8.5pt;
+    color: var(--gray-mid);
+  }
+}
+
+@page :first {
+  margin: 0;
+  @top-right { content: ""; }
+  @bottom-right { content: ""; }
+  @bottom-left { content: ""; }
+}
+
+* { box-sizing: border-box; }
+
+html, body {
+  margin: 0;
+  padding: 0;
+  font-family: 'Inter', system-ui, -apple-system, sans-serif;
+  font-size: 10.5pt;
+  line-height: 1.6;
+  color: var(--black-deep);
+  background: var(--white-pure);
+}
+
+/* ============ COVER PAGE ============ */
+.cover {
+  page-break-after: always;
+  height: 297mm;
+  width: 210mm;
+  padding: 35mm 22mm 22mm 22mm;
+  background:
+    radial-gradient(ellipse at top right, rgba(135, 168, 120, 0.18) 0%, transparent 55%),
+    radial-gradient(ellipse at bottom left, rgba(74, 124, 89, 0.10) 0%, transparent 55%),
+    var(--white-cream);
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  page: cover;
+}
+
+.cover::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 8mm;
+  background: linear-gradient(90deg, var(--green-dark), var(--green-forest), var(--green-moss));
+}
+
+.cover-header {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+
+.cover-logo {
+  width: 55mm;
+  height: auto;
+  max-height: 30mm;
+  object-fit: contain;
+}
+
+.cover-tagline {
+  font-family: 'Playfair Display', Georgia, serif;
+  font-size: 10pt;
+  font-style: italic;
+  color: var(--green-forest);
+  text-align: right;
+  max-width: 70mm;
+  margin-top: 6mm;
+}
+
+.cover-body {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  margin: -10mm 0 0 0;
+}
+
+.cover-eyebrow {
+  font-family: 'Inter', sans-serif;
+  font-size: 9pt;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.18em;
+  color: var(--green-moss);
+  margin-bottom: 6mm;
+}
+
+.cover-title {
+  font-family: 'Playfair Display', Georgia, serif;
+  font-size: 34pt;
+  font-weight: 700;
+  color: var(--green-dark);
+  line-height: 1.1;
+  margin: 0 0 6mm 0;
+  letter-spacing: -0.015em;
+}
+
+.cover-subtitle {
+  font-family: 'Playfair Display', Georgia, serif;
+  font-size: 16pt;
+  font-weight: 400;
+  font-style: italic;
+  color: var(--green-forest);
+  margin: 0 0 18mm 0;
+  max-width: 140mm;
+}
+
+.cover-meta {
+  font-family: 'Inter', sans-serif;
+  font-size: 10.5pt;
+  color: var(--black-soft);
+  line-height: 1.9;
+  border-left: 2px solid var(--green-moss);
+  padding-left: 5mm;
+}
+
+.cover-meta strong {
+  color: var(--green-dark);
+  font-weight: 600;
+  display: inline-block;
+  min-width: 25mm;
+}
+
+.cover-footer {
+  font-family: 'Inter', sans-serif;
+  font-size: 9pt;
+  color: var(--gray-mid);
+  border-top: 1px solid var(--green-sage);
+  padding-top: 5mm;
+  display: flex;
+  justify-content: space-between;
+}
+
+.cover-footer a {
+  color: var(--green-forest);
+  text-decoration: none;
+  font-weight: 500;
+}
+
+.cover-footer a:hover { color: var(--green-dark); }
+
+/* ============ DOCUMENT BODY ============ */
+.content {
+  string-set: doctitle attr(data-title);
+}
+
+h1 {
+  font-family: 'Playfair Display', Georgia, serif;
+  font-size: 22pt;
+  font-weight: 700;
+  color: var(--green-dark);
+  margin: 0 0 6mm 0;
+  page-break-after: avoid;
+  string-set: doctitle content();
+}
+
+h2 {
+  font-family: 'Playfair Display', Georgia, serif;
+  font-size: 17pt;
+  font-weight: 600;
+  color: var(--green-forest);
+  margin: 12mm 0 4mm 0;
+  padding-bottom: 2.5mm;
+  border-bottom: 2px solid var(--green-sage);
+  page-break-before: always;
+  page-break-after: avoid;
+}
+
+.content > h2:first-of-type,
+h2.no-break,
+h2.continue {
+  page-break-before: auto;
+}
+
+h3 {
+  font-family: 'Playfair Display', Georgia, serif;
+  font-size: 13.5pt;
+  font-weight: 600;
+  color: var(--green-forest);
+  margin: 8mm 0 3mm 0;
+  page-break-after: avoid;
+}
+
+h4 {
+  font-family: 'Inter', sans-serif;
+  font-size: 10pt;
+  font-weight: 600;
+  color: var(--green-moss);
+  margin: 6mm 0 2mm 0;
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  page-break-after: avoid;
+}
+
+p { margin: 0 0 3mm 0; }
+p, li { orphans: 3; widows: 3; }
+
+ul, ol { margin: 0 0 3mm 0; padding-left: 6mm; }
+ul li, ol li { margin: 0 0 1.5mm 0; }
+
+ul li::marker { color: var(--green-moss); }
+ol li::marker { color: var(--green-moss); font-weight: 600; }
+
+strong { color: var(--green-dark); font-weight: 600; }
+
+em { color: var(--green-forest); font-style: italic; }
+
+blockquote {
+  border-left: 3px solid var(--green-moss);
+  padding: 3mm 5mm;
+  margin: 4mm 0;
+  background: var(--white-cream);
+  color: var(--gray-dark);
+  font-style: italic;
+  page-break-inside: avoid;
+}
+
+blockquote p:last-child { margin-bottom: 0; }
+
+a { color: var(--green-forest); text-decoration: underline; text-decoration-thickness: 0.5pt; text-underline-offset: 1.5pt; }
+a:hover { color: var(--green-dark); }
+
+code {
+  font-family: 'JetBrains Mono', 'Fira Code', Menlo, monospace;
+  font-size: 9pt;
+  background: var(--white-cream);
+  padding: 0.5mm 1.5mm;
+  border-radius: 1mm;
+  color: var(--green-dark);
+}
+
+pre {
+  background: var(--white-cream);
+  padding: 4mm 5mm;
+  border-radius: 1.5mm;
+  border-left: 3px solid var(--green-moss);
+  font-size: 8.5pt;
+  line-height: 1.45;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+  page-break-inside: avoid;
+  margin: 4mm 0;
+}
+
+pre code { background: none; padding: 0; color: var(--black-deep); font-size: inherit; }
+
+/* ============ TABLES ============ */
+table {
+  width: 100%;
+  border-collapse: collapse;
+  margin: 4mm 0;
+  font-size: 9.5pt;
+  page-break-inside: avoid;
+}
+
+th {
+  font-family: 'Inter', sans-serif;
+  background: var(--green-forest);
+  color: var(--white-pure);
+  text-align: left;
+  padding: 2.5mm 3mm;
+  font-weight: 600;
+  font-size: 9pt;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+  border-bottom: 0;
+}
+
+td {
+  padding: 2.5mm 3mm;
+  border-bottom: 1px solid var(--green-sage);
+  vertical-align: top;
+}
+
+tr:nth-child(even) td { background: rgba(245, 240, 235, 0.55); }
+
+/* Numeric / status cols of score tables auto-detected via header text */
+table th:nth-child(2),
+table th:nth-child(3),
+table th:nth-child(4),
+table td:nth-child(2),
+table td:nth-child(3),
+table td:nth-child(4) {
+  text-align: right;
+  font-variant-numeric: tabular-nums;
+}
+
+table th:last-child,
+table td:last-child {
+  text-align: center;
+}
+
+/* ============ CHECKLISTS ============ */
+ul.checklist,
+ul.task-list {
+  list-style: none;
+  padding-left: 0;
+}
+
+ul.checklist li,
+ul.task-list li {
+  padding-left: 8mm;
+  position: relative;
+  margin-bottom: 2.5mm;
+}
+
+ul.checklist li::before,
+ul.task-list li::before,
+li input[type="checkbox"] + *,
+li.task-list-item::before {
+  content: "☐";
+  position: absolute;
+  left: 0;
+  color: var(--green-moss);
+  font-size: 12pt;
+  line-height: 1;
+}
+
+input[type="checkbox"] {
+  display: none;
+}
+
+input[type="checkbox"]:checked + label::before {
+  content: "☑";
+  color: var(--green-forest);
+}
+
+/* ============ CALLOUTS ============ */
+.callout {
+  padding: 4mm 6mm;
+  margin: 4mm 0;
+  border-radius: 2mm;
+  page-break-inside: avoid;
+  font-size: 10pt;
+}
+
+.callout.info {
+  background: var(--white-cream);
+  border-left: 4px solid var(--green-moss);
+}
+
+.callout.warn {
+  background: #fdf6e3;
+  border-left: 4px solid var(--status-warn);
+}
+
+.callout.success {
+  background: rgba(135, 168, 120, 0.14);
+  border-left: 4px solid var(--green-forest);
+}
+
+.callout-title {
+  font-family: 'Inter', sans-serif;
+  font-weight: 600;
+  font-size: 10pt;
+  color: var(--green-dark);
+  margin-bottom: 2mm;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+}
+
+/* ============ SECTION DIVIDERS ============ */
+hr {
+  border: none;
+  border-top: 1px solid var(--green-sage);
+  margin: 8mm 0;
+}
+
+/* ============ STATUS PILLS (used by text replacement) ============ */
+.status-ok    { color: var(--status-ok);   font-weight: 600; }
+.status-warn  { color: var(--status-warn); font-weight: 600; }
+.status-fail  { color: var(--status-fail); font-weight: 600; }
+
+/* ============ LINK BEHAVIOR IN PRINT ============ */
+@media print {
+  a[href^="http"]::after {
+    content: " (" attr(href) ")";
+    font-size: 7.5pt;
+    color: var(--gray-mid);
+    font-style: italic;
+    font-weight: 400;
+  }
+  a[href^="#"]::after,
+  a[href^="mailto:"]::after,
+  a[href^="tel:"]::after,
+  .cover a::after,
+  table a::after { content: ""; }
+}

+ 265 - 0
skills/client-handover/scripts/handover-to-pdf.sh

@@ -0,0 +1,265 @@
+#!/usr/bin/env bash
+#
+# handover-to-pdf.sh
+# ------------------
+# Renders a client-handover Markdown report (LIVRAISON.md / HANDOVER.md)
+# into a branded HTML and (when a converter is available) a PDF using
+# ZenQuality brand styling.
+#
+# Inputs:
+#   $1  Path to the source Markdown file (required)
+#
+# Optional environment variables:
+#   PROJECT_NAME   Displayed on the cover and as PDF page header.
+#                  Defaults to the source filename.
+#   CLIENT_NAME    Displayed on the cover. Defaults to "—".
+#   PROJECT_PERIOD Displayed on the cover (e.g. "01/01/2026 → 31/03/2026").
+#                  Defaults to "—".
+#   PROJECT_URL    Displayed on the cover. Defaults to "—".
+#   LANG           "fr" (default) or "en". Drives cover labels.
+#   COVER_TITLE    Defaults to PROJECT_NAME.
+#   COVER_SUBTITLE Defaults to "Compte rendu de livraison" (fr) /
+#                  "Project handover recap" (en).
+#   EYEBROW        Eyebrow line above the title. Defaults to
+#                  "Livraison" / "Handover".
+#   LOGO_URL       Logo URL or local path. Defaults to a remote
+#                  ZenQuality logo (no offline fallback).
+#   BRANDING_DIR   Override branding-asset directory. Defaults to the
+#                  resources/branding/ folder next to this script.
+#
+# Behaviour:
+#   1. Convert the Markdown body to HTML.
+#   2. Wrap it in the ZenQuality template (cover + branded body).
+#   3. Convert that HTML into a PDF using the first available engine:
+#        weasyprint > wkhtmltopdf > chromium > headless Chrome
+#   4. Always keep the .html file next to the .md.
+#   5. If no PDF engine is available, exit with code 2 and a clear
+#      message — never fail silently.
+#
+# Exit codes:
+#   0  HTML and PDF written successfully.
+#   1  Fatal error (bad arguments, missing files, conversion error).
+#   2  HTML written but no PDF engine available — manual print needed.
+
+set -euo pipefail
+
+# ---------------------------- CLI ----------------------------------
+
+if [ "$#" -lt 1 ]; then
+  echo "usage: handover-to-pdf.sh <markdown-file>" >&2
+  exit 1
+fi
+
+SRC_MD="$1"
+
+if [ ! -f "$SRC_MD" ]; then
+  echo "error: markdown file not found: $SRC_MD" >&2
+  exit 1
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DEFAULT_BRANDING_DIR="$SCRIPT_DIR/../resources/branding"
+BRANDING_DIR="${BRANDING_DIR:-$DEFAULT_BRANDING_DIR}"
+
+if [ ! -f "$BRANDING_DIR/zenquality.css" ] || [ ! -f "$BRANDING_DIR/zenquality-template.html" ]; then
+  echo "error: branding assets missing under $BRANDING_DIR" >&2
+  echo "       expected: zenquality.css + zenquality-template.html" >&2
+  exit 1
+fi
+
+OUT_DIR="$(cd "$(dirname "$SRC_MD")" && pwd)"
+BASE="$(basename "$SRC_MD" .md)"
+OUT_HTML="$OUT_DIR/$BASE.html"
+OUT_PDF="$OUT_DIR/$BASE.pdf"
+
+LANG_CODE="${LANG:-fr}"
+case "$LANG_CODE" in
+  en|EN|en_*|en-*)
+    LANG_CODE="en"
+    EYEBROW="${EYEBROW:-Project handover}"
+    DEFAULT_SUBTITLE="Project handover recap"
+    LABEL_CLIENT="Client"
+    LABEL_PROJECT="Project"
+    LABEL_DATE="Issued"
+    LABEL_PERIOD="Period"
+    LABEL_URL="Website"
+    LABEL_PREPARED_BY="prepared for the client"
+    ;;
+  *)
+    LANG_CODE="fr"
+    EYEBROW="${EYEBROW:-Livraison}"
+    DEFAULT_SUBTITLE="Compte rendu de livraison"
+    LABEL_CLIENT="Client"
+    LABEL_PROJECT="Projet"
+    LABEL_DATE="Date d'émission"
+    LABEL_PERIOD="Période"
+    LABEL_URL="Site"
+    LABEL_PREPARED_BY="préparé pour le client"
+    ;;
+esac
+
+PROJECT_NAME_RESOLVED="${PROJECT_NAME:-$BASE}"
+CLIENT_NAME_RESOLVED="${CLIENT_NAME:-—}"
+PROJECT_PERIOD_RESOLVED="${PROJECT_PERIOD:-—}"
+PROJECT_URL_RESOLVED="${PROJECT_URL:-—}"
+COVER_TITLE_RESOLVED="${COVER_TITLE:-$PROJECT_NAME_RESOLVED}"
+COVER_SUBTITLE_RESOLVED="${COVER_SUBTITLE:-$DEFAULT_SUBTITLE}"
+LOGO_URL_RESOLVED="${LOGO_URL:-https://zenquality.fr/logo-horizontal.svg}"
+
+if command -v date >/dev/null 2>&1; then
+  if [ "$LANG_CODE" = "fr" ]; then
+    DATE_HUMAN="$(LC_ALL=fr_FR.UTF-8 date "+%d %B %Y" 2>/dev/null || date "+%Y-%m-%d")"
+  else
+    DATE_HUMAN="$(LC_ALL=en_US.UTF-8 date "+%d %B %Y" 2>/dev/null || date "+%Y-%m-%d")"
+  fi
+else
+  DATE_HUMAN="$(date "+%Y-%m-%d")"
+fi
+
+# ---------------------------- MD -> HTML ---------------------------
+
+md_to_html_body() {
+  local src="$1"
+  if command -v pandoc >/dev/null 2>&1; then
+    pandoc --from=gfm --to=html5 --no-highlight "$src"
+    return
+  fi
+  if command -v python3 >/dev/null 2>&1 && python3 -c "import markdown" >/dev/null 2>&1; then
+    python3 -c "
+import sys, markdown
+src = open(sys.argv[1], encoding='utf-8').read()
+print(markdown.markdown(
+    src,
+    extensions=['extra', 'tables', 'sane_lists', 'toc'],
+))" "$src"
+    return
+  fi
+  if command -v npx >/dev/null 2>&1; then
+    npx --yes marked < "$src"
+    return
+  fi
+  echo "error: no Markdown converter available (need pandoc, python3+markdown, or npx)" >&2
+  exit 1
+}
+
+BODY_HTML="$(md_to_html_body "$SRC_MD")"
+
+# ---------------------------- WRAP HTML ----------------------------
+
+CSS_CONTENT="$(cat "$BRANDING_DIR/zenquality.css")"
+
+render_template() {
+  # Read template path from $1, output the substituted HTML on stdout.
+  # Substitution variables are pulled from HQ_* environment variables.
+  HQ_TEMPLATE_PATH="$1" python3 <<'PY'
+import os, sys
+path = os.environ["HQ_TEMPLATE_PATH"]
+with open(path, encoding="utf-8") as f:
+    template = f.read()
+mapping = {
+    "{{LANG}}":              os.environ.get("HQ_LANG", "fr"),
+    "{{TITLE}}":             os.environ.get("HQ_TITLE", ""),
+    "{{CSS}}":               os.environ.get("HQ_CSS", ""),
+    "{{LOGO_URL}}":          os.environ.get("HQ_LOGO_URL", ""),
+    "{{EYEBROW}}":           os.environ.get("HQ_EYEBROW", ""),
+    "{{COVER_TITLE}}":       os.environ.get("HQ_COVER_TITLE", ""),
+    "{{COVER_SUBTITLE}}":    os.environ.get("HQ_COVER_SUBTITLE", ""),
+    "{{CLIENT_NAME}}":       os.environ.get("HQ_CLIENT_NAME", "—"),
+    "{{PROJECT_NAME}}":      os.environ.get("HQ_PROJECT_NAME", ""),
+    "{{DATE_HUMAN}}":        os.environ.get("HQ_DATE_HUMAN", ""),
+    "{{PROJECT_PERIOD}}":    os.environ.get("HQ_PROJECT_PERIOD", "—"),
+    "{{PROJECT_URL}}":       os.environ.get("HQ_PROJECT_URL", "—"),
+    "{{LABEL_CLIENT}}":      os.environ.get("HQ_LABEL_CLIENT", ""),
+    "{{LABEL_PROJECT}}":     os.environ.get("HQ_LABEL_PROJECT", ""),
+    "{{LABEL_DATE}}":        os.environ.get("HQ_LABEL_DATE", ""),
+    "{{LABEL_PERIOD}}":      os.environ.get("HQ_LABEL_PERIOD", ""),
+    "{{LABEL_URL}}":         os.environ.get("HQ_LABEL_URL", ""),
+    "{{LABEL_PREPARED_BY}}": os.environ.get("HQ_LABEL_PREPARED_BY", ""),
+    "{{CONTENT}}":           os.environ.get("HQ_CONTENT", ""),
+}
+for k, v in mapping.items():
+    template = template.replace(k, v)
+sys.stdout.write(template)
+PY
+}
+
+export HQ_LANG="$LANG_CODE"
+export HQ_TITLE="$COVER_TITLE_RESOLVED"
+export HQ_CSS="$CSS_CONTENT"
+export HQ_LOGO_URL="$LOGO_URL_RESOLVED"
+export HQ_EYEBROW="$EYEBROW"
+export HQ_COVER_TITLE="$COVER_TITLE_RESOLVED"
+export HQ_COVER_SUBTITLE="$COVER_SUBTITLE_RESOLVED"
+export HQ_CLIENT_NAME="$CLIENT_NAME_RESOLVED"
+export HQ_PROJECT_NAME="$PROJECT_NAME_RESOLVED"
+export HQ_DATE_HUMAN="$DATE_HUMAN"
+export HQ_PROJECT_PERIOD="$PROJECT_PERIOD_RESOLVED"
+export HQ_PROJECT_URL="$PROJECT_URL_RESOLVED"
+export HQ_LABEL_CLIENT="$LABEL_CLIENT"
+export HQ_LABEL_PROJECT="$LABEL_PROJECT"
+export HQ_LABEL_DATE="$LABEL_DATE"
+export HQ_LABEL_PERIOD="$LABEL_PERIOD"
+export HQ_LABEL_URL="$LABEL_URL"
+export HQ_LABEL_PREPARED_BY="$LABEL_PREPARED_BY"
+export HQ_CONTENT="$BODY_HTML"
+
+render_template "$BRANDING_DIR/zenquality-template.html" > "$OUT_HTML"
+
+echo "wrote: $OUT_HTML"
+
+# ---------------------------- HTML -> PDF --------------------------
+
+PDF_ENGINE=""
+PDF_REASON=""
+
+if command -v weasyprint >/dev/null 2>&1; then
+  PDF_ENGINE="weasyprint"
+elif command -v wkhtmltopdf >/dev/null 2>&1; then
+  PDF_ENGINE="wkhtmltopdf"
+elif command -v chromium >/dev/null 2>&1; then
+  PDF_ENGINE="chromium"
+elif command -v chromium-browser >/dev/null 2>&1; then
+  PDF_ENGINE="chromium-browser"
+elif command -v google-chrome >/dev/null 2>&1; then
+  PDF_ENGINE="google-chrome"
+else
+  PDF_REASON="no PDF engine found (looked for: weasyprint, wkhtmltopdf, chromium, google-chrome)"
+fi
+
+if [ -n "$PDF_ENGINE" ]; then
+  case "$PDF_ENGINE" in
+    weasyprint)
+      weasyprint --base-url "$OUT_DIR/" "$OUT_HTML" "$OUT_PDF"
+      ;;
+    wkhtmltopdf)
+      wkhtmltopdf --enable-local-file-access \
+        --margin-top 0 --margin-bottom 0 \
+        --margin-left 0 --margin-right 0 \
+        --print-media-type \
+        "$OUT_HTML" "$OUT_PDF"
+      ;;
+    chromium|chromium-browser|google-chrome)
+      "$PDF_ENGINE" --headless --disable-gpu --no-sandbox \
+        --no-pdf-header-footer \
+        --print-to-pdf="$OUT_PDF" \
+        --print-to-pdf-no-header \
+        "file://$OUT_HTML"
+      ;;
+  esac
+  echo "wrote: $OUT_PDF (engine: $PDF_ENGINE)"
+  exit 0
+fi
+
+cat <<EOF >&2
+
+note: HTML written, but no PDF engine is available.
+      reason: $PDF_REASON
+
+To generate $OUT_PDF, install one of:
+  - weasyprint   pip install --user weasyprint
+  - wkhtmltopdf  apt install wkhtmltopdf  (or download from wkhtmltopdf.org)
+  - chromium     apt install chromium-browser
+Or open $OUT_HTML in a browser and use "Print → Save as PDF".
+
+EOF
+exit 2