Audit terminé. Voici le rapport.

---

# Audit — `AboutSection` / `CertificationSection` (home)

Branche `feature/home-best-practices-audit`. Lecture seule, aucun fichier modifié.

**Correction préalable au brief** : les tokens `--text-h2` et `--text-label` **n'existent pas** dans `src/app/globals.css` (le bloc `@theme` s'arrête aux couleurs + 5 tokens d'espacement). Et `--spacing-section: 96px` / `--spacing-section-sm: 64px` (globals.css:23-24) sont **morts** — 0 usage dans tout `src/`. Idem `--spacing-gutter` / `--spacing-margin-mobile` / `--spacing-margin-desktop`.
Autre correction : le pattern « plaque décalée » (`translate-x-[18px]`) n'existe **qu'ici** (AboutSection.tsx:40). Aucune occurrence dans `GalleryHero.tsx` (986 B, pas de cadre) ni `AboutStory.tsx`. Il n'y a donc **pas** de doublon `OffsetFrame` à factoriser aujourd'hui.

---

## 1. Tableau des constats

| # | Sévérité | Axe | Fichier:ligne | Constat | Correction (avant → après) | Effort | QW |
|---|---|---|---|---|---|---|---|
| 1 | **Critique** | 3 | `AboutSection.tsx:43` + `messages/en.json` `IndexPage.about.imageAlt` / `fr.json` | **L'alt décrit une image qui n'existe pas.** J'ai ouvert `craftspeople.webp` : c'est une **photo de groupe d'environ 50 employés en extérieur**, masqués, devant une maison des esprits thaï décorée, avec une table d'offrandes alimentaires au premier plan (cérémonie de mérite / Songkran). Aucun établi, aucune pierre, aucun sertissage. | `alt="…artisans hand-setting stones at the workbench"` → soit **remplacer l'asset** par une vraie photo d'atelier (recommandé : la section parle de transmission à l'établi), soit `"Advance Manufacturers' team at the factory's annual merit-making ceremony in Bangkok"`. Idem FR, rédigé indépendamment. | S (alt) / M (asset) | ✅ |
| 2 | **Élevée** | 3 | `Overline.tsx:35` | `text-gold` (`#a6863f`, globals.css:45) sur `#f9f9f7` = **3,26:1** ; sur `#f4f4f2` (section Certification) = **3,12:1**. Texte 11px semibold ⇒ pas « large text » ⇒ **échec WCAG 2.2 AA 1.4.3** (4,5:1 requis). Impacte tout le site (`<Overline>` × 8 sur la home seule). | `--color-gold: #a6863f` → `#85692f` (4,91:1 sur `#f9f9f7`, 4,70:1 sur `#f4f4f2`). Garder `#a6863f` uniquement pour du décoratif/non-texte. | S | ✅ |
| 3 | **Élevée** | 3, 4 | `CertificationSection.tsx:16-21` + messages | `overline` et `title` ont **la valeur strictement identique** : `"Certification & Sustainability"` / `"Certification & durabilité"`. Le même libellé est rendu deux fois de suite, lu deux fois par un lecteur d'écran. | Différencier : overline `"Responsible sourcing"` / titre `"RJC-certified since…"` — ou supprimer la clé `overline` et la remplacer par un simple trait or. | S | ✅ |
| 4 | **Élevée** | 3 | `AboutSection.tsx:15-34`, `page.tsx:83-90` | **Outline cassé** : `h1` (Hero) → *Stats : aucun heading* → *About : aucun heading* → `h2` (Certification) → `h2` ×4. La section About est le bloc **le plus dense en entités** de la page (RJC, Bangkok, 1989, 3,5 M pièces, 42 pays, 5 000 m², 500 artisans) et il est **orphelin** dans l'arbre de titres. | Cf. §3 bis ci-dessous : rendre `<Overline>` polymorphe (`as="h2"`) → l'overline **devient** le h2, zéro changement visuel, contrainte client respectée. Préférable à un `h2 sr-only` (qui duplique le texte et n'apporte rien de visible). | S | ✅ |
| 5 | **Élevée** | 1 | `Reveal.tsx:1,48` + `globals.css:240-246` | Le HTML SSR sort avec `opacity: 0` / `translateY(24px)`. **Sans JS, tout le corps de texte de About + Certification est invisible** (le failsafe 2 500 ms est lui-même en JS). 67 occurrences de `<Reveal>` dans `src/`. | Reveal 100 % CSS : `animation-timeline: view()` + `animation-range` sous `@supports (animation-timeline: view())`, contenu **visible par défaut** hors support. Supprime le composant client. | M | |
| 6 | Moyenne | 1 | `Reveal.tsx` | 67 racines client + 67 `IntersectionObserver` (12 sur la home). ✔ **Le pattern children est correct** : `<Image>` et `<Link>` restent des Server Components (passés en `children`, aucun import client) — rien à corriger de ce côté. C'est `Reveal` lui-même qui est éliminable. | Voir #5. | M | |
| 7 | Moyenne | 2, 4 | `AboutSection.tsx:40` vs `:46` | **Bug visuel** : la plaque décalée est statique, l'image porte `float-slow` (±10 px sur 8 s). Le décalage « 18 px » **oscille donc entre 8 px et 28 px** en vertical. Le cadre n'est plus un cadre. | Déplacer `float-slow` sur le wrapper `<Reveal>` (plaque + image ensemble), ou le retirer. | S | ✅ |
| 8 | Moyenne | 2, 4 | `AboutSection.tsx:22-33` | Le pattern « lien flèche » est **copié à l'identique 8 fois** : `ContactInfo.tsx:98`, `GalleryCta.tsx:20-27`, `JournalThemes.tsx:37-45`, `JournalBrief.tsx:32`, `PostLayout.tsx:206`, `SustainabilityPolicies.tsx:111`, `ProcessSteps.tsx:98`. | Extraire `<ArrowLink>`. **Ne pas** utiliser `Button variant="link"` : ce variant (`button.tsx`) est `text-primary underline-offset-4 hover:underline` + `rounded-lg h-8 px-2.5 text-sm` — incompatible avec l'identité (radius 0, label-caps 11px, pas de soulignement). Si on veut rester dans la mécanique shadcn : ajouter un variant `editorial` + size `label` au `cva`, et rendre un `<Link>` via la prop **`render`** de `@base-ui/react` (pas `asChild`). | M | |
| 9 | Moyenne | 2 | `AboutSection.tsx:31` | Flèche `→` en **caractère texte** (U+2192). Ni Tenor Sans ni Montserrat ne la garantissent → fallback système, glyphe hétérogène et ligne de base décalée selon l'OS. Contraire à `iconLibrary: "lucide"` (`components.json`). | `<ArrowRightIcon aria-hidden className="size-3.5" />` (ou `data-icon="inline-end"` si intégré au `Button`). **⚠ `lucide-react` n'est pas dans `experimental.optimizePackageImports`** (`next.config.ts:19-32`) alors qu'il est déjà importé par `ui/breadcrumb.tsx` et `ui/select.tsx` — violation de la règle projet « obligatoire pour toute nouvelle dépendance ». | S | ✅ |
| 10 | Moyenne | 3 | `AboutSection.tsx:45` | `sizes="(min-width:1024px) 42vw, 100vw"` **surestime à tous les paliers**. Calcul réel (`max-w-1440` + `lg:px-16` + 12 cols + `gap-6`, `col-span-5`) : 359 px à 1024 (35,1 vw), **533 px à 1440 (37,0 vw)**, 533 px figés à 1920 (**27,7 vw**). En DPR 1 sur desktop large, le navigateur demande le palier 828 au lieu de 640. | → `sizes="(min-width:1440px) 533px, (min-width:1024px) 37vw, 100vw"` | S | ✅ |
| 11 | Moyenne | 4 | `CertificationSection.tsx:30-36` vs `SustainabilityRjc.tsx:17-33` | **Doublon RJC confirmé.** Home = raster 1253×800 dont **tout le contenu est du texte dans l'image** (« RESPONSIBLE JEWELRY COUNCIL », « CERTIFIED MEMBER », « COP 0000 7330 », « LGM 0000 7445 »). Sustainability = la **même carte reconstruite en CSS + i18n** (`rjcMark`/`rjcName`/`rjcMember`/`rjcNumber` = `"COP 0000 7330 · LGM 0000 7445"`). Deux sources de vérité pour la même donnée réglementaire. | Extraire `<RjcHallmark />` depuis `SustainabilityRjc.tsx` et l'utiliser sur la home. Gains : −23 KB, texte **indexable et traduisible**, net à tout DPR, une seule source pour les numéros. | M | |
| 12 | Moyenne | 3 | `messages/*.json` `IndexPage.certification.rjcImageAlt` | Deux erreurs d'entité : (a) **« Responsible Jewelry Council »** (graphie US) alors que le nom officiel est **« Responsible Jewellery Council »** — orthographe correcte utilisée partout ailleurs (`SustainabilityPage.rjc.rjcName`, `metaKeywords`) ; (b) « certificate » : l'asset est une **marque de membre / badge**, pas un certificat. | `"Responsible Jewellery Council certified member mark — COP 0000 7330, LGM 0000 7445"`. Sans objet si #11 est appliqué. | S | ✅ |
| 13 | Moyenne | 3 | `CertificationSection.tsx:10-41` | **Aucun lien interne vers `/sustainability`**, alors que cette page cible exactement `"rjc certified jewellery manufacturer"` (`messages/en.json:832-842`). La section de la home la plus alignée thématiquement n'y renvoie pas — et elle est la seule section de contenu de la home sans CTA. | Ajouter un `<ArrowLink href="/sustainability">` (ancre du type « Our RJC commitments », pas « Learn more »). | S | ✅ |
| 14 | Moyenne | 2 | `globals.css:23-24` vs `brand.md` vs code | **Incohérence à trois niveaux du padding de section.** `docs/brand.md` : 80 mobile / 160 desktop. Tokens : 64 / 96 (**morts**). Code : `py-[76px]` + `lg:py-[110px]` (About) vs `lg:py-[104px]` (Certification) — **deux sections voisines avec deux rythmes différents**. 25+ fichiers concernés. | Trancher sur la valeur réellement en production (76 / 104), corriger `--spacing-section` / `--spacing-section-sm`, créer `@utility section-y`, et **aligner `docs/brand.md`** (le doc est faux aujourd'hui). About passe de 110 → 104. | M | |
| 15 | Faible | 3 | `AboutSection.tsx:24` | Pas de `focus-visible:outline-*`, alors que `HeroHome.tsx:53,59`, `GalleryCta.tsx:20`, `DesignCta.tsx:20` posent tous `focus-visible:outline-2 outline-offset-2 outline-gold`. Incohérence de l'anneau de focus projet. | Ajouter les 3 utilities (ou les centraliser dans `<ArrowLink>`, #8). | S | ✅ |
| 16 | Faible | 3 | `AboutSection.tsx:24` | `hover:text-gold` ⇒ l'état survol du CTA tombe à **3,26:1**. WCAG s'applique à tous les états. | Résolu par #2. | S | ✅ |
| 17 | Faible | 2 | `CertificationSection.tsx:19` | Le style de h2 `font-display text-[31px] font-normal leading-[1.18] lg:text-[40px] lg:leading-[1.15]` est **dupliqué à l'identique 5 fois** : ici, `GallerySection.tsx:52`, `ProcessSection.tsx:31`, `ValuePropSection.tsx:21`, `SustainabilityRjc.tsx:37`. | `@utility text-section-title` ou token fluide `--text-h2: clamp(31px, 2.78vw, 40px)` (supprime la variante `lg:`). | S | ✅ |
| 18 | Faible | 2 | `Overline.tsx:35`, `AboutSection.tsx:24` | Le style label-caps `text-[11px] font-semibold uppercase tracking-[…]` apparaît **20+ fois** avec **4 tracking différents** (0.16 / 0.18 / 0.2 / 0.26em) sans règle documentée. | Tokens `--text-label: 11px`, `--tracking-label: 0.16em`, `--tracking-overline: 0.26em`, et documenter quand utiliser lequel. | M | |
| 19 | Faible | 1, 2 | `AboutSection.tsx:46`, `CertificationSection.tsx:35` | `float-slow` = animation **infinie** sur 2 images de la home. Sur About elle se cumule à `saturate-[.92]` : passe de filtre recomposée à chaque frame, en boucle 8 s, indéfiniment, pour un effet de ±10 px quasi imperceptible. ✔ `prefers-reduced-motion` est correctement géré (`globals.css:283-287`). | Retirer `float-slow` des images (le garder pour la carte `SustainabilityRjc`), ou au minimum retirer `saturate-[.92]` de l'élément animé. | S | ✅ |
| 20 | Faible | 3, 5 | `CertificationSection.tsx:33` vs `AboutSection.tsx:42` | `placeholder="blur"` sur le certificat (23 KB) mais **pas** sur `craftspeople` (**193 KB**) — l'inverse de ce qu'il faudrait. Les deux sont des imports statiques, le blur est disponible gratuitement. | Ajouter `placeholder="blur"` sur l'image About. | S | ✅ |
| 21 | Faible | 5 | `public/production/craftspeople.webp` | **1000×750, 193 KB.** Le slot fait 533 px CSS max ⇒ il faudrait **1066 px** pour du DPR 2 : la source est **sous-dimensionnée en pixels** et **surdimensionnée en poids**. | Ré-export WebP à **1200 px** de large, q75 → cible < 120 KB, et DPR 2 couvert. | S | ✅ |
| 22 | Faible | 2 | `AboutSection.tsx:13` vs `CertificationSection.tsx:13` | Rythmes de grille divergents entre deux sections adjacentes : About = texte cols **2–6**, image **8–12** (colonnes mortes 1 et 7) ; Certification = texte **2–7**, image **8–11** (colonnes mortes 1 et 12). La colonne morte change de côté. | Harmoniser sur un seul schéma (5/5 avec col morte centrale, ou 6/4 aligné à droite). | S | ✅ |
| 23 | Faible | 2 | `CertificationSection.tsx:28` | `justify-stretch` sur un flex n'a aucun effet utile (équivaut à `flex-start`, et l'enfant est déjà `w-full`). Bruit. | `className="flex sm:justify-center lg:col-span-4 lg:col-start-8 lg:justify-end"` | S | ✅ |
| 24 | Info | 3 | `Overline.tsx:8,41,43` | ✔ **RAS** : le word-joiner U+2060 est un caractère de formatage (catégorie Cf), ignoré par les lecteurs d'écran. Les traits sont bien `aria-hidden`. Le `<span>` neutre est **correct tant que ce n'est pas le heading** — mais devient à revoir si on applique #4. | — | — | |

---

## 2. Valeurs arbitraires → remplacement

**Périmètre strict** (les 2 sections + `Overline`). Rappel : `gap-2.5`, `gap-12`, `lg:gap-6`, `mb-5`, `px-6`, `lg:px-16`, `text-base`, `lg:col-start-2` sont **sur l'échelle Tailwind** — pas des valeurs arbitraires (le brief les listait à tort).

| Valeur actuelle | Fichier:ligne | Remplacement | Mécanisme |
|---|---|---|---|
| `max-w-[1440px]` | About:12, Cert:12 (+25 fichiers) | `max-w-editorial` | `@theme { --container-editorial: 1440px }` (namespace `--container-*` → `max-w-*`) |
| `py-[76px]` | About:12, Cert:12 (+20) | `py-19` (19 × 4 px) | échelle native v4 — ou `@utility section-y` |
| `lg:py-[110px]` | About:12 | `lg:py-26` (104 px) | **normaliser sur 104** pour aligner avec Certification (#14) |
| `lg:py-[104px]` | Cert:12 | `lg:py-26` | échelle native |
| `text-[31px]` + `lg:text-[40px]` | Cert:19 | `text-h2` | `--text-h2: clamp(31px, 2.78vw, 40px)` — **supprime la variante `lg:`** |
| `leading-[1.18]` + `lg:leading-[1.15]` | Cert:19 | inclus dans le token | `--text-h2--line-height: 1.16` |
| `mb-[26px]` | Cert:19 | `mb-6` (24 px) | échelle native |
| `mb-[30px]` | About:17 | `mb-8` (32 px) | échelle native |
| `mb-[34px]` | About:21 | `mb-8` (32 px) | échelle native |
| `leading-[1.7]` | About:20,21 ; Cert:22 | `leading-body` | `--leading-body: 1.7` |
| `max-w-[500px]` | Cert:22 | `max-w-125` | valeur nue v4 (125 × 4 px) |
| `sm:max-w-[400px]` | Cert:29 | `sm:max-w-100` | valeur nue v4 |
| `text-[11px]` | About:24, Overline:35 (+20) | `text-label` | `--text-label: 11px` |
| `tracking-[0.16em]` | About:24 | `tracking-label` | `--tracking-label: 0.16em` |
| `tracking-[0.26em]` | Overline:35 | `tracking-overline` | `--tracking-overline: 0.26em` |
| `w-[26px]` | Overline:4 | `w-6.5` | échelle native |
| `translate-x-[18px]` `translate-y-[18px]` | About:40 | `translate-x-4.5 translate-y-4.5` | échelle native (4.5 × 4 px) |
| `z-[1]` | About:46 | `z-1` | valeur nue v4 |
| `saturate-[.92]` | About:46 | à supprimer (#19) | — |
| `group-hover:translate-x-1.5` | About:29 | ✔ déjà sur l'échelle | — |

**Mobile-first** : ✔ les deux sections sont correctement écrites mobile-first (base = 1 colonne centrée, `lg:` = 12 colonnes alignées à gauche). Aucun `max-*:` inversé. Rien à redresser sur cet axe.

---

## 3. Frontière client/serveur — actuel vs cible

**Actuel**

```
page.tsx                        ▸ Server (setRequestLocale, statique)
├─ AboutSection                 ▸ Server ✔
│  ├─ Overline                  ▸ Server ✔ (pas de "use client")
│  ├─ Reveal ×2                 ▸ CLIENT  ← useState + useEffect + IntersectionObserver
│  │  ├─ Link (i18n)            ▸ Server ✔ (children RSC, rendu côté serveur)
│  │  └─ Image (static import)  ▸ Server ✔ (children RSC)
└─ CertificationSection         ▸ Server ✔
   ├─ Overline                  ▸ Server ✔
   └─ Reveal ×2                 ▸ CLIENT
      └─ Image (blur)           ▸ Server ✔
```

Le pattern *children* est **appliqué correctement** : `Reveal` ne fait qu'envelopper, `<Image>` / `<Link>` traversent la frontière en tant que payload RSC déjà rendu et **ne sont jamais réévalués côté client**. Aucun `next-intl` `useTranslations` ne fuit dans le client. Rien à corriger sur ce point précis.

Le vrai coût est ailleurs : **4 racines client sur ces 2 sections, 12 sur la home, 67 sur le site**, chacune avec son propre `IntersectionObserver`, son `useState` et son `setTimeout` — plus le fait que le HTML SSR est livré invisible (#5).

**Cible — Reveal 100 % CSS, zéro client**

```
page.tsx                        ▸ Server
├─ AboutSection                 ▸ Server ✔
│  └─ <div className="reveal">  ▸ Server (plus de composant du tout)
└─ CertificationSection         ▸ Server ✔
```

```css
/* globals.css — visible par défaut, l'animation est un enrichissement */
@supports (animation-timeline: view()) {
  @media not (prefers-reduced-motion: reduce) {
    .reveal {
      animation: reveal-rise linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 30%;
    }
  }
}
@keyframes reveal-rise {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: none; }
}
```

`animation-timeline: view()` est Baseline (Chrome/Edge 115+, Safari 26+, Firefox 144+) ; hors support, le `@supports` laisse le contenu simplement visible — ce qui est **strictement meilleur que l'état actuel**. Le failsafe 2 500 ms et l'`IntersectionObserver` disparaissent. Le `delay` en `style` devient `animation-range` ou une classe de stagger. **Bénéfice : −67 racines client sur le site, contenu SSR immédiatement lisible sans JS.**

### 3 bis. Outline SEO après retrait du h2 About

Le retrait du h2 est une décision client, à respecter. L'**impact** est réel : la section la plus riche en entités de la home (RJC / Bangkok / 1989 / 3,5 M pièces / 42 pays / 5 000 m² / 500 artisans) n'a plus aucun ancrage dans l'arbre de titres, et le saut `h1` → *(rien)* → *(rien)* → `h2` laisse deux sections muettes.

**Option A — recommandée : promouvoir l'overline en `h2`.** Rendre `<Overline>` polymorphe :

```tsx
type OverlineProps = { as?: "span" | "h2" | "h3"; … }
```

`<Overline as="h2" align="responsive">` en `AboutSection.tsx:17`. **Zéro changement visuel** (l'overline garde exactement son style d'eyebrow), l'outline est réparé, aucun texte caché. Il faut en revanche **requalifier la valeur** : « About Us » / « À propos » ne porte aucun signal. Rédiger chaque locale sur ses propres mots-clés (mémoire projet : jamais de traduction de H2), p. ex. EN « An RJC-certified jewellery factory in Bangkok since 1989 ».

**Option B — `h2 sr-only`** : fonctionne, mais duplique le contenu textuel, ajoute du texte visible uniquement pour les robots, et laisse l'overline sémantiquement orphelin juste à côté. Techniquement valide, qualitativement inférieur à A.

**À faire aussi** : `StatsSection` a le même trou (aucun heading, `StatsSection.tsx` — 0 `<h2>`), même remède.

---

## 4. Inventaire images / alt

| Fichier | Dimensions réelles | Poids | Composant:ligne | `sizes` | Largeur rendue max | `priority` | `placeholder` | Alt | Verdict |
|---|---|---|---|---|---|---|---|---|---|
| `public/production/craftspeople.webp` | **1000×750** (4:3) | 193 KB | `AboutSection.tsx:42-47` | `(min-width:1024px) 42vw, 100vw` | **533 px** (à ≥1440) | non ✔ (sous la ligne de flottaison) | ❌ absent | EN « Advance Manufacturers artisans hand-setting stones at the workbench » / FR « Artisans […] sertissant des pierres à l'établi » | ❌ **Alt faux (#1)** · `sizes` surestimé de ~14 pts (#10) · source sous-dimensionnée pour DPR 2 et surdimensionnée en poids (#21) · pas de blur (#20) |
| `public/home/rjc-certified-member.webp` | **1253×800** | 23 KB | `CertificationSection.tsx:30-36` | `(min-width:640px) 400px, 100vw` | **400 px** (borné par `sm:max-w-[400px]`) | non ✔ | ✔ `blur` | EN « Responsible Jewelry Council certified member certificate — COP 0000 7330, LGM 0000 7445 » | ⚠ `sizes` **exact** ✔ (borne sûre ; léger excès entre 1024 et 1376 où le rendu réel tombe à 283 px, mais négligeable à 23 KB) · **graphie « Jewelry » ≠ entité officielle « Jewellery »** et « certificate » ≠ badge (#12) · **texte-dans-l'image non indexable → à remplacer par la carte CSS (#11)** |

Les deux `<Image>` utilisent des **imports statiques** ⇒ `width`/`height` injectés automatiquement ⇒ **aucun risque de CLS** ✔. Aucun `alt=""` manquant ✔. `next.config.ts:15-18` : `formats: ['image/avif','image/webp']`, `deviceSizes` et `imageSizes` conformes à la règle projet ✔.

---

## 5. Top 5 des actions

1. **Corriger l'alt de `craftspeople.webp` — ou remplacer l'image.** (#1) C'est le seul point *critique* : l'alt décrit des artisans sertissant des pierres à l'établi, l'image montre une cérémonie de mérite en extérieur avec ~50 employés et une table d'offrandes. Un utilisateur de lecteur d'écran reçoit une information fausse, et Google indexe une description qui ne correspond à rien. Sur le fond, la section parle de transmission du savoir-faire à l'établi : **une vraie photo d'atelier serait le bon correctif**, l'alt n'étant qu'un pansement. *S — décision client requise sur l'asset.*

2. **Assombrir `--color-gold` à ≈ `#85692f`.** (#2, #16) Une ligne dans `globals.css`, qui fait passer `<Overline>` (8 occurrences sur la home, présent sur toutes les pages) et l'état hover du CTA de **3,1–3,3:1 à ≥4,7:1**, soit de l'échec au succès WCAG 2.2 AA. Meilleur ratio effort/impact du lot. *S.*

3. **Réparer l'outline via `<Overline as="h2">` + différencier overline/titre de Certification.** (#4, #3) Deux corrections sémantiques à changement visuel nul : la section About retrouve un `h2` sans que le client revoie un titre apparaître, et Certification cesse d'annoncer deux fois le même libellé. Requalifier au passage « About Us » en un intitulé porteur, rédigé séparément par locale. *S.*

4. **Remplacer l'image RJC par la carte CSS de `SustainabilityRjc.tsx`.** (#11, #12) Supprime le doublon de représentation, sort les numéros COP/LGM de l'image pour les rendre indexables et traduisibles, règle l'erreur d'entité « Jewelry/Jewellery », économise 23 KB et une requête, et rend le rendu net à tout DPR. À faire en extrayant un `<RjcHallmark />` partagé — une seule source de vérité pour une donnée de certification. *M.*

5. **Passer `Reveal` en CSS pur (`animation-timeline: view()`).** (#5, #6) Aujourd'hui tout le corps de texte de la home est livré en `opacity: 0` et ne dépend que de JS pour apparaître. Le passage en CSS rend le contenu **visible par défaut**, supprime 67 racines client et 67 `IntersectionObserver` sur le site, et conserve l'effet là où il est supporté. C'est le seul chantier structurant du lot, et il déborde du périmètre des deux sections — à traiter comme une tâche dédiée. *M.*

**Bonus quasi gratuit** : ajouter `lucide-react` à `experimental.optimizePackageImports` (`next.config.ts:19-32`) — il est déjà importé par `ui/breadcrumb.tsx` et `ui/select.tsx` et échappe à la règle projet.
