# Audit dépendances, code mort & doublons — `advmfr-next`

Lecture seule, aucune modification effectuée. Branche `feature/home-best-practices-audit`.

---

## 1. Inventaire des dépendances

### `dependencies`

| Paquet | Version | Fichiers importateurs (`src/`) | Verdict | Action |
|---|---|---|---|---|
| `@base-ui/react` | ^1.5.0 | 3 : `ui/select.tsx`, `ui/button.tsx`, `ui/breadcrumb.tsx` | **Utilisée partiellement** — seul `select.tsx` est réellement consommé (via `LanguageSelect.tsx`) ; `button.tsx` et `breadcrumb.tsx` sont morts | Garder. Après suppression de button/breadcrumb, l'usage tombe à 1 fichier |
| `@next/third-parties` | ^16.2.9 | 1 : `AnalyticsProviders.tsx` | Utilisée (`GoogleAnalytics`) | Garder |
| `@openpanel/nextjs` | ^1.5.1 | 1 : `AnalyticsProviders.tsx` (via `next/dynamic`) | Utilisée | Garder. À noter : **deux systèmes d'analytics** (GA4 + OpenPanel) chargés simultanément si les deux env vars sont définies |
| `@tabler/icons-react` | ^3.44.0 | **0** | **INUTILISÉE** — confirmé par knip. 91 Mo dans `node_modules` | **Supprimer** + retirer de `optimizePackageImports` |
| `class-variance-authority` | ^0.7.1 | 1 : `ui/button.tsx` (mort) | **INUTILISÉE de fait** — confirmé par knip | **Supprimer** avec `button.tsx` |
| `clsx` | ^2.1.1 | 1 : `lib/utils.ts` (`cn`) | Utilisée | Garder |
| `fuse.js` | ^7.4.2 | 2 : `blog/BlogIndexView.tsx`, `blog/BlogSubNav.tsx` | Utilisée (recherche blog, client) | Garder |
| `gray-matter` | ^4.0.3 | 1 : `lib/blog/index.ts` | Utilisée (serveur) | Garder |
| `lucide-react` | ^1.18.0 | 2 : `ui/breadcrumb.tsx` (mort), `ui/select.tsx` | **Utilisée marginalement** : 5 icônes importées, dont 2 dans un fichier mort. Réellement rendues : `ChevronDownIcon`, `CheckIcon`, `ChevronUpIcon` (select) | Garder — c'est l'`iconLibrary` de `components.json`. Voir §3 |
| `next` / `react` / `react-dom` | 16.2.9 / 19.2.7 | — | Utilisées | Garder |
| `next-intl` | ^4.13.0 | **82** | Utilisée massivement | Garder |
| `next-mdx-remote` | ^6.0.0 | 1 : `[category]/[slug]/page.tsx` | Utilisée | Garder |
| `reading-time` | ^1.5.0 | 1 : `lib/blog/index.ts` | Utilisée | Garder |
| `remark-gfm` | ^4.0.1 | 1 : `[category]/[slug]/page.tsx` | Utilisée | Garder |
| `schema-dts` | ^2.0.0 | 1 : `config/seo/structured-data.ts` | Utilisée — **types only** | **Déplacer en `devDependencies`** (import de types, effacé au build) |
| `shadcn` | ^4.11.0 | 1 : `app/globals.css` → `@import "shadcn/tailwind.css"` | **Cas limite** : c'est un CLI, mais son package expose la feuille de style importée par `globals.css`. Le CSS est résolu au build (Tailwind/PostCSS), donc **devDependency suffit** | **Déplacer en `devDependencies`** |
| `tailwind-merge` | ^3.6.0 | 1 : `lib/utils.ts` | Utilisée (runtime client) | Garder |
| `tw-animate-css` | ^1.4.0 | 1 : `app/globals.css` | Utilisée mais **par un seul composant** : les utilitaires `animate-in / fade-in-0 / zoom-in-95 / slide-in-from-*` n'apparaissent que dans `ui/select.tsx:85`. (`animate-pulse`, `animate-ping` sont du Tailwind core, pas `tw-animate-css`) | **Déplacer en `devDependencies`** (CSS build-time). Garder tant que `select.tsx` vit |
| `zod` | ^4.4.3 | 3 : `contact-us/actions.ts`, `actions/newsletter.ts`, `lib/blog/schema.ts` | Utilisée | Garder |

### `devDependencies`

Toutes légitimes et utilisées (`@next/eslint-plugin-next`, `@playwright/test`, `@tailwindcss/postcss`, types, `babel-plugin-react-compiler`, `eslint` + `eslint-plugin-oxlint`, `knip`, `oxfmt`, `oxlint`, `tailwindcss`, `typescript`). Knip n'en signale aucune comme inutilisée.

**Manquante :** `@types/mdx` — `src/components/blog/mdx-components.tsx:1` fait `import type { MDXComponents } from "mdx/types"` mais le paquet n'est pas déclaré. Il ne résout qu'en **dépendance fantôme** via `@mdx-js/mdx → @types/mdx@2.0.13`. À ajouter explicitement en `devDependencies`.

### Lockfiles — incohérence confirmée

```
bun.lock          184 647 o   24 juil. 14:01   ← tracké par git
package-lock.json 272 290 o   24 juil. 15:17   ← NON tracké, NON ignoré (absent de .gitignore)
```

Deux gestionnaires ont résolu l'arbre à ~1 h d'écart. `package-lock.json` n'est ni versionné ni ignoré : il peut être committé par accident et fera diverger CI/local. **Supprimer `package-lock.json` et l'ajouter à `.gitignore`.**

---

## 2. Sortie knip

`bun run deadcode` avec la config du repo est **inexploitable** : elle remonte **390 « unused files »** dont ~380 sont des artefacts de build (`.claude/worktrees/seo-audit/.next/**`). La config `knip.json` n'ignore ni `.claude/**` ni les dossiers parasites non trackés. Relancé avec `/tmp/knip.json` (mêmes `entry`, `ignore` élargi à `.claude/**`, `.kiro/**`, `.codebuddy/**`, `.junie/**`, `.continue/**`, `advmfr-next/**`, `**/.next/**`, et **sans** `src/components/ui/**` qui masquait de vraies trouvailles) :

**Fichiers inutilisés (3)** — tous vérifiés à la main, aucun faux positif :
- `src/components/ui/And.tsx` — `And`, `andTags`, `splitAmpersand` : **zéro usage** hors du fichier lui-même. Contredit la mémoire projet « typo — titres Montserrat, seul le & en Playfair » : le helper existe mais n'est appliqué nulle part.
- `src/components/ui/breadcrumb.tsx` — zéro import (`breadcrumbJsonLd` dans `config/seo/structured-data.ts` est sans rapport).
- `src/components/ui/button.tsx` — zéro import. **Aucun `from "@/components/ui/button"` dans tout `src/`.**

**Dépendances inutilisées (2)** : `@tabler/icons-react` (`package.json:23`), `class-variance-authority` (`package.json:24`).

**Dépendance non déclarée (1)** : `mdx/types` — `src/components/blog/mdx-components.tsx:1:36`.

**Exports inutilisés (11)** — 6 confirmés morts, 5 **faux positifs** :
| Export | Fichier | Vérif |
|---|---|---|
| `SelectGroup`, `SelectLabel`, `SelectScrollDownButton`, `SelectScrollUpButton`, `SelectSeparator`, `SelectValue` | `src/components/ui/select.tsx:182-189` | **Morts** — `LanguageSelect.tsx` n'importe que `Select`, `SelectContent`, `SelectItem`, `SelectTrigger` |
| `isCategoryId` | `src/config/blog/categories.ts:27` | **Mort** — aucun appel |
| `articleJsonLd` | `src/config/seo/structured-data.ts:207` | **Mort** — aucun appel |
| `buildPageAlternates` | `src/config/seo/meta.ts:49` | Faux positif — appelé `meta.ts:102` (usage interne au module) |
| `categoryIdFromSlug` | `src/lib/blog/index.ts:131` | Faux positif — 4 usages externes |
| `buildRssFeed` | `src/lib/blog/rss.ts:23` | Faux positif — appelé `rss.ts:55` |

**Types exportés inutilisés (1)** : `Frontmatter` — `src/lib/blog/schema.ts:24`, aucun usage.

**Action config :** ajouter `.claude/**` et `**/.next/**` à `knip.json:ignore`, et **retirer `src/components/ui/**`** — cette ligne masquait 3 fichiers morts et 6 exports morts.

---

## 3. Icônes — 4 voies concurrentes

| Voie | Occurrences |
|---|---|
| **`lucide-react`** | 2 fichiers, 5 imports : `ui/select.tsx:7` (`ChevronDownIcon`, `CheckIcon`, `ChevronUpIcon`), `ui/breadcrumb.tsx:7` (`ChevronRightIcon`, `MoreHorizontalIcon` — **fichier mort**) |
| **SVG inline** | 8 `<svg>` dans 5 fichiers : `LogoMonogram.tsx:12` (marque, légitime) ; `blog/BlogSubNav.tsx:118` (`ChevronIcon`), `:310` (croix « clear »), `:624` (loupe) ; `blog/ShareButtons.tsx:52` (check), `:65` (lien) ; `blog/TocScrollspy.tsx:59` (chevron bas) ; `contact/ContactInfo.tsx:19` (enveloppe) |
| **Glyphes texte** | `home/GallerySlideshow.tsx:80,88` (`‹` `›`) ; `gallery/CollectionSlider.tsx:133,143` (`‹` `›`) ; `production/ProcessSteps.tsx:88,98` (`←` `→`) ; `production/ProcessHero.tsx:35` (`↓`) ; `home/AboutSection.tsx:31`, `contact/ContactInfo.tsx:99`, `gallery/GalleryCta.tsx:27`, `blog/JournalThemes.tsx:44`, `blog/PostLayout.tsx:208`, `blog/JournalBrief.tsx:34`, `sustainability/SustainabilityPolicies.tsx:113` (`→`) ; `Footer.tsx:33-37` (réseaux sociaux : `"in"`, `"f"`, `"ig"`) ; `blog/ShareButtons.tsx:47` (`"X"`) |
| **`@tabler/icons-react`** | **0** — dépendance fantôme dans `package.json` et `next.config.ts` |

Point clé : **les SVG inline non-marque sont des copies littérales d'icônes lucide.** Preuves :
- `TocScrollspy.tsx:69` → `<path d="m6 9 6 6 6-6" />` = lucide `chevron-down`, identique au tracé.
- `ShareButtons.tsx:62` → `<path d="M20 6 9 17l-5-5" />` = lucide `check`.
- `ShareButtons.tsx:75-76` → les deux `path` de lucide `link-2`.
- `BlogSubNav.tsx:127` → `d={direction === "left" ? "M12 4 6 10l6 6" : "m8 4 6 6-6 6"}` = `chevron-left`/`chevron-right` (viewBox 20 au lieu de 24).

Ces icônes sont donc **maintenues à la main alors que `lucide-react` est déjà installé et déjà listé comme `iconLibrary` dans `components.json`** — exactement ce que le skill `shadcn` proscrit (« use existing components first »).

**Recommandation :** une seule voie.
1. `lucide-react` pour toute icône fonctionnelle : chevrons, croix, loupe, check, lien, mail, flèches (`ArrowRight` remplace `→`), `ChevronLeft`/`ChevronRight` dans les deux carrousels, `ArrowDown` dans `ProcessHero`.
2. SVG inline **réservé à la marque** : `LogoMonogram.tsx` seul. Les glyphes sociaux du footer (`"in"`, `"f"`, `"ig"`) devraient devenir des SVG de marque dédiés — un `"f"` textuel n'est ni le logo Facebook ni accessible en dehors de son `aria-label`.
3. Coût nul : lucide est déjà dans le bundle client via `select.tsx`, et Next 16 l'optimise par défaut (voir §8).

---

## 4. Animation / motion

`motion` et `framer-motion` : **confirmés absents** de `package.json` et de `node_modules`. Aucune librairie d'animation JS.

Cinq mécanismes coexistent :

| Mécanisme | Implémentation | Usage |
|---|---|---|
| `<Reveal>` (JS) | `src/components/motion/Reveal.tsx` — `"use client"`, `IntersectionObserver`, `useState`, fail-safe 2,5 s | **38 fichiers** — toutes les sections de toutes les pages |
| `<CountUp>` (JS) | `src/components/motion/CountUp.tsx` — `"use client"`, `IntersectionObserver` + `requestAnimationFrame`, ease-out cubique | 2 fichiers : `home/StatsSection.tsx:3`, `sustainability/SustainabilityPeople.tsx:4` |
| Classes CSS maison | `globals.css:207-231` (`@keyframes editorial-float/-bounce/-pulse`) + `globals.css:239-278` (`.reveal`, `.reveal-in`, `.float-slow`, `.bounce-slow`, `.pulse-slow`, `.rule-grow`) | `.float-slow` ×3 (`home/AboutSection.tsx:46`, `home/CertificationSection.tsx:35`, `sustainability/SustainabilityRjc.tsx:17`) ; `.bounce-slow` ×1 (`production/ProcessHero.tsx:30`) ; `.rule-grow` ×1 (`design/DesignSteps.tsx:114`) ; **`.pulse-slow` : 0 usage → règle CSS morte** |
| Tailwind core | `animate-pulse` (skeletons), `animate-ping` (`contact/ContactInfo.tsx:81-82`) | ~18 occurrences |
| `tw-animate-css` | `globals.css:2` | **1 seul consommateur** : `ui/select.tsx:85` |

**Doublons réels :**
- **`.pulse-slow` (`globals.css:257-259`) + `@keyframes editorial-pulse` (`globals.css:227-231`) : code CSS mort**, aucun usage. À supprimer (~10 lignes). Le besoin « pulsation » est couvert par `animate-ping` dans `ContactInfo`.
- `Reveal` et `CountUp` réimplémentent chacun leur propre `IntersectionObserver` avec des seuils différents (0.15 + `rootMargin: -8%` vs 0.3). `CollectionSlider.tsx:59` en implémente un **troisième** (seuil 0.4). Trois observers, trois conventions. Un hook partagé `useInView({ threshold, once })` dans `src/hooks/` les unifierait sans changer un pixel.
- `Reveal` gère `prefers-reduced-motion` en CSS (`globals.css:281-287`), `CountUp` le gère en JS (`CountUp.tsx:39`), `CollectionSlider` en JS aussi (`:72`) et en variante Tailwind (`motion-safe:` `:118-119`). Trois stratégies pour la même contrainte.

**Sur `animation-timeline: view()` :** techniquement `Reveal` est remplaçable par du CSS pur, ce qui supprimerait 38 frontières client. Mais `animation-timeline` n'est pas supporté par Firefox (derrière `layout.css.scroll-driven-animations.enabled`), donc le fade-in disparaîtrait purement et simplement sur Firefox. **Je ne recommande pas la bascule.** Le gain JS serait de toute façon modeste : `Reveal.tsx` est un chunk unique partagé (~0,6 kB gzip hors React), pas 38 copies — le vrai coût est la sérialisation RSC de 38 frontières, pas le poids du bundle. Priorité basse.

**Gain net réaliste sur ce chapitre : ~10 lignes de CSS mort + une consolidation de 3 `IntersectionObserver` en 1 hook.** Pas de kB significatifs. À traiter comme dette de lisibilité, pas comme optimisation de perf.

---

## 5. Carrousels — `GallerySlideshow` vs `CollectionSlider`

| Critère | `home/GallerySlideshow.tsx` (109 l.) | `gallery/CollectionSlider.tsx` (168 l.) |
|---|---|---|
| Slides | 12 (`home/GallerySection.tsx:24-36`) | 3 par collection, 6 collections |
| État | `current`, `paused`, `hidden`, `reduce` (4 `useState`) | `current`, `inView`, `hovered`, `tookOver` (4 `useState`) |
| Autoplay | `setInterval` 4500 ms, reset à chaque nav (`current` dans les deps `:43`) | `setInterval` 6000 ms, démarre à l'entrée en viewport |
| Pause | survol souris + `document.hidden` (`visibilitychange`) | survol souris + **focus/blur capture** (`:85-86`) — meilleur a11y |
| Reprise manuelle | l'autoplay **repart** après clic | `tookOver` **arrête définitivement** (`:47`) — meilleur a11y |
| `prefers-reduced-motion` | `useState` + `useEffect` (`:26`) | `matchMedia` direct dans l'effet (`:72`) |
| Transition | opacité 1000 ms, classes Tailwind | opacité `FADE_MS` 1400 ms via `style` + dérive d'échelle `scale-[1.05]` sur 8 s |
| Flèches | toujours visibles, `‹`/`›` texte | apparition au survol/focus, `‹`/`›` texte |
| Dots | sous l'image, `aria-current` ✓ | en overlay bas, `aria-current` ✓, variante `dark` |
| Images | `placeholder="blur"`, `loading` eager sur la 1re | `placeholder="blur"`, `priority` sur la 1re de la collection 01 |
| a11y | pas de `aria-live`, pas de `role="region"` | idem |

Les deux sont **~70 % de logique identique** (map de slides en absolu + crossfade opacité, `go(index)` modulo, dots `aria-current`, flèches `aria-label` via `Common.previous`/`Common.next`) avec des divergences de comportement non justifiées par le design.

**`CollectionSlider` est strictement supérieur en a11y** (pause au focus clavier, arrêt définitif après action utilisateur). `GallerySlideshow` relance l'autoplay après un clic — un utilisateur clavier qui navigue aux dots verra l'image bouger sous lui.

**Recommandation : composant maison unique, pas Embla.** Embla (~8 kB gzip + le wrapper shadcn `carousel`) apporterait le drag/swipe et le snap scroll — dont aucun des deux usages n'a besoin : ce sont des **crossfades**, pas des rails défilants. Embla ne fait pas de crossfade nativement (il faut le plugin `fade`, +1 kB). On paierait 9 kB pour perdre la dérive d'échelle et réécrire les deux composants.

API proposée — `src/components/ui/Crossfade.tsx` (`"use client"`) :
```ts
type CrossfadeProps = {
  slides: { src: StaticImageData; alt: string }[];
  sizes: string;
  dwellMs?: number;        // 4500 (home) | 6000 (gallery)
  fadeMs?: number;         // 1000 | 1400
  drift?: boolean;         // false (home) | true (gallery)
  dotsPosition?: "below" | "overlay";
  arrows?: "always" | "hover";
  tone?: "light" | "dark";
  priority?: boolean;
  className?: string;
  labels: { previous: string; next: string; goTo: (n: string) => string };
};
```
Le comportement de pause/reprise devient celui de `CollectionSlider` (le plus sûr) pour les deux. `labels` passé en prop plutôt que deux `useTranslations` internes, pour que les clés i18n restent littérales côté appelant (cf. mémoire projet sur `next-intl` et les clés dynamiques). **Effort : ~3 h. Gain : ~110 lignes supprimées + a11y du carrousel Home corrigée, 0 kB de dépendance ajoutée.**

---

## 6. Familles de doublons UI

| Famille | Occurrences (fichier:ligne) | Composant cible + API | Effort |
|---|---|---|---|
| **Boutons CTA faits main** | 13 occurrences de `uppercase tracking-[0.16em]`, dont 6 vrais boutons/liens d'action : `home/HeroHome.tsx:53` (plein), `:59` (contour), `InquireCta.tsx:19` (plein), `gallery/GalleryCta.tsx:20` (plein), `footer/NewsletterForm.tsx:30` (plein inversé), `blog/PostLayout.tsx:156` (plein inversé). **`ui/button.tsx` n'est importé nulle part** (0 usage) et son style shadcn (`rounded-lg`, `h-8`, `bg-primary`) est incompatible avec la charte (`docs/brand.md` : « shape language strictement Sharp (0px) »). | Généraliser `InquireCta` en `ui/Cta.tsx` : `<Cta variant="solid"\|"outline"\|"inverse" size="md"\|"lg" as={Link\|"button"} />`. Supprimer `ui/button.tsx` (donc `class-variance-authority`) ou le réécrire avec `cva` aux tokens de marque — mais ne pas garder les deux. | Moyen |
| **Liens-flèche « → »** | 7 occurrences du même bloc `group inline-flex items-center gap-2.5 text-[11px] font-semibold uppercase tracking-[0.16em] ... hover:text-gold` + glyphe `→` : `home/AboutSection.tsx:24`+`:31`, `blog/JournalThemes.tsx:37`+`:44`, `blog/PostLayout.tsx:201`+`:208`, `blog/JournalBrief.tsx:27`+`:34`, `gallery/GalleryCta.tsx:27`, `contact/ContactInfo.tsx:95`+`:99`, `sustainability/SustainabilityPolicies.tsx:107`+`:113` | `ui/ArrowLink.tsx` : `<ArrowLink href tone="gold"\|"foreground">{label}</ArrowLink>`, `<ArrowRight className="size-3.5 transition-transform group-hover:translate-x-1" />` lucide à la place du glyphe | Faible |
| **Cartes numérotées** | `home/ValuePropSection.tsx:26-39` et `about/AboutPhilosophy.tsx:26-39` sont **identiques à la virgule près** (même `grid-cols-[repeat(auto-fit,minmax(240px,1fr))] gap-4 lg:gap-6`, même `border border-hairline bg-background p-[32px_26px]`, même `motion-safe:hover:-translate-y-1 motion-safe:hover:shadow-[0_18px_40px_-22px_rgba(26,28,27,0.35)]`, même `font-display text-[15px] text-burnished-gold`, même `Reveal delay={90*index}`) — seule différence : `text-center` présent ligne 29 côté ValueProp. Numérotation `"01"…"06"` en dur dans 6 autres fichiers (`home/ProcessSection.tsx:15-20`, `design/DesignSteps.tsx:16-54`, `production/ProcessSteps.tsx:21-26`, `gallery/GalleryCollections.tsx:40-100`, `design/DesignFinal.tsx:19`) | `ui/NumberedCardGrid.tsx` : `<NumberedCardGrid cards={[{key,title,body}]} align="center"\|"start" />`, numérotation dérivée de l'index (`String(i+1).padStart(2,"0")`) au lieu d'être saisie à la main | Faible |
| **Filets or `h-px`** | 29 occurrences de `h-px` dont **21 en `bg-burnished-gold`** avec 9 largeurs différentes (`w-4`, `w-5`, `w-7`, `w-8`, `w-10`, `w-11`, `w-12`, `w-[18px]`, `w-[26px] sm:w-12`, `w-[52px]`) : `ui/Overline.tsx:4`, `home/StatsSection.tsx:38`, `home/MaterialsSection.tsx:43`, `design/DesignFinal.tsx:46`, `design/DesignSteps.tsx:114`+`:126`, `contact/ContactInfo.tsx:119`, `gallery/GalleryCollections.tsx:187`, `about/AboutVision.tsx:34`+`:36`+`:48`, `blog/JournalNewsletterBand.tsx:30`, `blog/PostLayout.tsx:151`, `blog/JournalBrief.tsx:16`, `blog/BlogSubNav.tsx:350`, `sustainability/SustainabilityWaste.tsx:46`, `sustainability/SustainabilityPolicies.tsx:102`, `sustainability/SustainabilityRjc.tsx:40`+`:61`, `sustainability/SustainabilityMaterials.tsx:49`, `production/ProcessXray.tsx:14` | `ui/Rule.tsx` : `<Rule size="xs"\|"sm"\|"md"\|"lg" tone="gold"\|"steel"\|"soft" />` avec 4 tailles canoniques. `Overline.tsx` consomme `Rule` en interne. Rend l'incohérence visible et corrigeable en un point | Faible |
| **Overlines ad hoc** | `ui/Overline.tsx` est bien adopté (**27 fichiers**), mais 3 sections réimplémentent l'eyebrow à la main : `gallery/GalleryCollections.tsx:187`, `about/AboutVision.tsx:34-36`, `blog/JournalBrief.tsx:16` (filet + label small-caps) et `blog/JournalNewsletterBand.tsx:12` (label `tracking-[0.26em] text-gold-bright` sans filet, exactement la typo d'`Overline`) | Migrer vers `<Overline>`. Ajouter `tone="gold" \| "gold-bright"` pour couvrir le cas fond sombre de `JournalNewsletterBand` | Faible |
| **Wordmark dupliqué** | `BrandLogo.tsx:15-28` (`font-display text-[19px] tracking-[0.32em]` + `text-[9.5px] tracking-[0.42em]`) et `Footer.tsx:64-77` (`text-[20px] tracking-[0.32em]` + `text-[10px] tracking-[0.42em]`). Mêmes `tracking`, tailles à 1 px près, **deux namespaces i18n distincts** (`Header.brand` vs `Footer.brand`) pour le même texte. A11y divergente : `BrandLogo` met `aria-label` sur le `<Link>`, `Footer` utilise un `<span className="sr-only">` | `ui/Wordmark.tsx` : `<Wordmark size="sm"\|"md" tone="light"\|"dark" withMonogram />`. `BrandLogo` = `<Wordmark withMonogram />`. Unifier sur une seule clé i18n (`Brand.primary` / `Brand.secondary`) | Faible |
| **Cadre d'image décalé** | 1 seule occurrence réelle : `home/AboutSection.tsx:40` (`absolute inset-0 z-0 translate-x-[18px] translate-y-[18px] bg-surface-container-highest`). `PageLayout.tsx:12` est un dégradé sans rapport | **Pas un doublon** — pas d'action. À surveiller si le motif se répète | — |
| **Shell de section** | `mx-auto max-w-[1440px] px-6 lg:px-16` dans **33 fichiers**, `py-[76px] lg:py-[104px]` dans 6 | `ui/Section.tsx` : `<Section padded tone="white"\|"surface"\|"dark" bordered="top"\|"y"\|"bottom">`. Gain de lisibilité fort, risque de régression visuelle non nul (les variantes de bordure/fond diffèrent) | Moyen |
| **Formulaires newsletter** | **Faux positif** : `blog/JournalNewsletterBand.tsx:21` importe et réutilise déjà `footer/NewsletterForm`. Aucun doublon | — | — |
| **Formulaire contact** | `contact/ContactForm.tsx:25` utilise `mb-2 text-xs font-semibold uppercase tracking-[0.15em]` pour ses labels, `contact/ContactInfo.tsx:43` le même pour ses `<dt>`, `footer/NewsletterForm.tsx:16` un `sr-only` | `ui/FieldLabel.tsx`. Marginal | Faible |

---

## 7. `public/` — orphelins, poids, formats

**Total `public/` : 94 `.webp`, 4 `.png`, 3 `.svg`, 1 `.ico`, 1 `.DS_Store`.**

Les images sont importées en **static imports** (`import X from "../../../public/..."`), donc un fichier non importé n'est jamais servi.

### Orphelins — 5,6 Mo, 18 fichiers

| Poids | Fichier | Note |
|---|---|---|
| **4 496 kB** | `public/home/advance-manufacture-building.png` | **Doublon PNG du `.webp` (304 kB) qui, lui, est utilisé** (`home/ProcessSection.tsx:10`). Viole la règle projet « WebP minimum, jamais PNG ». Suppression = **93 % du gain** |
| 268 kB | `public/home/slider/az4951_wpqknt.webp` | Slide non retenue |
| 200 kB | `public/home/slider/az5332_i9vpn2.webp` | Slide non retenue |
| 116 kB | `public/home/success-team.webp` | |
| 76 kB | `public/home/oem/sampling.webp` | Doublon fonctionnel de `public/design/sampling.webp` (44 kB, utilisé) |
| 60 kB | `public/home/oem/sketching.webp` | |
| 60 kB | `public/home/glimpses/wax.webp` | Doublon de `production/wax-injection.webp` (utilisé) |
| 60 kB | `public/home/glimpses/diamond-setting.webp` | Doublon de `production/stone-setting.webp` (utilisé) |
| 56 kB | `public/home/glimpses/polishing.webp` | Doublon de `production/polishing.webp` (utilisé) |
| 32 kB | `public/home/glimpses/cad-printing.webp` | |
| 28 kB | `public/home/glimpses/enamelling.webp` | |
| 32 kB | `public/production/cad-printing.webp` | |
| 28 kB | `public/production/enamel.webp` | |
| 20 kB | `public/sustainability/rjc-certified-member.webp` | Doublon de `public/home/rjc-certified-member.webp` (24 kB, utilisé par `home/CertificationSection.tsx:5`) |
| 48 kB | `public/android-chrome-512x512.png` | **Absent de `src/app/manifest.ts`** (qui ne déclare que `/favicon.ico` et `site.logo.src`) |
| 12 kB | `public/android-chrome-192x192.png` | Idem |
| 4 kB | `public/icon.svg` | Non référencé. Convention Next 16 : un `icon.svg` n'est auto-détecté que dans `src/app/`, pas dans `public/` |
| 12 kB | `public/.DS_Store` | Artefact macOS commité. À supprimer + ajouter `.DS_Store` à `.gitignore` |

`public/home/glimpses/*` (5 fichiers, 236 kB) et `public/home/oem/` (partiellement) semblent être les restes d'une section Home supprimée. `public/home/oem/cad.webp` est le seul survivant du dossier, utilisé comme `cover:` dans `content/blog/jewellery-design-trends-2026/{en,fr}.mdx:8`.

### Poids — 12 images chargées d'un coup sur la Home

`home/GallerySlideshow.tsx:53-72` rend **les 12 slides simultanément dans le DOM**, en `absolute inset-0` avec `opacity-0`. Le `loading` lazy des slides 2 à 12 (`:68`) est inopérant : elles sont dans le viewport (juste transparentes), donc le navigateur les télécharge toutes dès que la section approche. **Poids cumulé des 12 sources : 2 256 kB** (avant transformation `next/image`, qui les servira en AVIF/WebP redimensionnés — mais 12 requêtes `/_next/image` quand même).

Piste : ne monter dans le DOM que `current`, `current-1` et `current+1` (fenêtre glissante de 3), ce qui ramène le chargement initial à ~700 kB de sources. À arbitrer avec l'agent qui audite les images de la Home.

### Formats

- Aucun `.jpg`/`.jpeg`. ✅
- 4 `.png` : `android-chrome-192x192.png` et `android-chrome-512x512.png` (orphelins), `apple-touch-icon.png` (12 kB, **légitime** — référencé `src/app/[locale]/layout.tsx:92`, iOS exige du PNG), `home/advance-manufacture-building.png` (orphelin, à supprimer).
- 3 `.svg` : `logo-advance-manufacturer.svg` (4 kB, utilisé via `site.logo.src`), `contact/world.svg` (**152 kB** — le 15ᵉ fichier le plus lourd du repo, utilisé par `contact/ContactInfo.tsx`, candidat à un passage par SVGO), `icon.svg` (orphelin).
- Aucun fichier > 300 kB une fois les orphelins retirés. Le plus lourd restant : `home/advance-manufacture-building.webp` à 304 kB.

---

## 8. `next.config.ts`

```ts
optimizePackageImports: [
  "next-intl", "@base-ui/react", "swiper", "zod", "next-mdx-remote",
  "gray-matter", "remark-gfm", "reading-time", "fuse.js", "@tabler/icons-react",
]
```

**Vérifié dans `node_modules/next/dist/server/config.js:984-1069`** : Next 16 fusionne la liste utilisateur avec une liste par défaut qui contient déjà **`lucide-react`** (ligne 988) et **`@tabler/icons-react`** (ligne 1029).

| Entrée | Verdict |
|---|---|
| `"swiper"` | **Config morte** — paquet non installé (`node_modules/swiper` absent), 0 import dans `src/`. Sans import, la transformation ne s'applique jamais : pas d'erreur de build, mais une ligne trompeuse. **Retirer** |
| `"@tabler/icons-react"` | **Doublement inutile** : 0 import ET déjà dans la liste par défaut de Next. **Retirer** avec la dépendance |
| `"lucide-react"` absent | **Pas un bug** — inclus par défaut. L'ajouter explicitement est cosmétique (auto-documentation). Optionnel |
| `"next-intl"`, `"@base-ui/react"`, `"zod"`, `"fuse.js"` | Pertinent — packages à barrel importés partiellement côté client |
| `"gray-matter"`, `"reading-time"`, `"remark-gfm"`, `"next-mdx-remote"` | Sans effet mesurable : consommés **uniquement côté serveur** (`lib/blog/index.ts`, `[slug]/page.tsx`). Inoffensif, garder |

**Images** — conformes à `docs/nextjs-16.x.md` §2 : `formats: ["image/avif","image/webp"]` ✅, `minimumCacheTTL: 31_536_000` ✅, `deviceSizes: [640,750,828,1080,1200,1920,2048]` ✅ (pas de 3840 inutile), `imageSizes: [16,32,48,64,96,128,256,384]` ✅. `qualities: [75]` : cohérent — **aucun `quality=` n'est passé dans `src/`**, tous les `<Image>` utilisent donc le défaut 75, qui est la seule valeur autorisée. Aucune régression possible.

**Headers** : `Referrer-Policy`, `Permissions-Policy`, `X-Robots-Tag: noimageai` sur `/_next/image` — corrects.

---

## Top 5 des actions

**1. Purger `public/` — 5,6 Mo, 18 fichiers.** Supprimer `home/advance-manufacture-building.png` (4,5 Mo, doublon PNG d'un `.webp` déjà utilisé — viole la règle « WebP minimum »), les 5 `home/glimpses/*`, `home/oem/{sampling,sketching}.webp`, `production/{cad-printing,enamel}.webp`, `sustainability/rjc-certified-member.webp`, les 2 slides non retenues, `success-team.webp`, `icon.svg`, les 2 `android-chrome-*.png`, `.DS_Store` (+ l'ajouter à `.gitignore`). *Gain : 5,6 Mo de repo, −18 fichiers. Effort : 15 min. Risque : nul (aucune référence, vérifié par chemin relatif complet).*

**2. Supprimer le code mort confirmé par knip et corriger la config knip.** Fichiers : `ui/And.tsx`, `ui/breadcrumb.tsx`, `ui/button.tsx`. Dépendances : `@tabler/icons-react` (91 Mo de `node_modules`), `class-variance-authority`. Exports : les 6 `Select*` non utilisés, `isCategoryId`, `articleJsonLd`, le type `Frontmatter`. Puis, dans `knip.json`, **retirer `src/components/ui/**` de `ignore`** (c'est cette ligne qui masquait les 3 fichiers morts) et ajouter `.claude/**` + `**/.next/**` — sans quoi `bun run deadcode` reste inexploitable (390 faux positifs) et `bun run ci` ne protège rien. *Gain : −3 fichiers, −2 deps, −10 exports, 91 Mo de `node_modules`, ~0,5 kB de bundle client (cva). Effort : 45 min.*

**3. Assainir `package.json` et les lockfiles.** Supprimer `package-lock.json` (non tracké, non ignoré, résolu 1 h après `bun.lock`) et l'ajouter à `.gitignore`. Déplacer `schema-dts`, `shadcn` et `tw-animate-css` de `dependencies` vers `devDependencies` (types effacés au build + CSS build-time). Ajouter `@types/mdx` en `devDependencies` — actuellement dépendance fantôme via `@mdx-js/mdx`, elle cassera au premier hoisting différent. Retirer `"swiper"` et `"@tabler/icons-react"` de `optimizePackageImports` dans `next.config.ts`. *Gain : `dependencies` de production ramené de 23 à 19 entrées, un seul lockfile, plus de dépendance fantôme. Effort : 20 min.*

**4. Unifier les icônes sur `lucide-react`.** Remplacer les 7 SVG inline non-marque — qui sont des copies littérales de `chevron-down`, `chevron-left`, `chevron-right`, `check`, `link-2`, `x`, `search`, `mail` (tracés identiques vérifiés) — et les glyphes texte `→` / `‹` / `›` / `↓` par les composants lucide. Ne garder en SVG inline que `LogoMonogram.tsx`. Coût bundle : **nul** — lucide est déjà chargé via `ui/select.tsx` et Next 16 l'optimise par défaut. *Gain : ~90 lignes de JSX supprimées, une seule convention, conforme à `iconLibrary: lucide` de `components.json` et au principe « use existing components first » du skill shadcn. Effort : 2 h.*

**5. Fusionner les deux carrousels et corriger l'a11y de la Home.** `GallerySlideshow` (109 l.) et `CollectionSlider` (168 l.) partagent ~70 % de logique. `CollectionSlider` est strictement meilleur : il met en pause au focus clavier (`onFocusCapture` `:85`) et arrête définitivement l'autoplay après une action utilisateur (`tookOver` `:47`) — deux comportements que `GallerySlideshow` n'a pas, ce qui fait bouger l'image sous les doigts d'un utilisateur clavier sur la Home. Extraire `ui/Crossfade.tsx` avec l'API décrite en §5, **sans Embla** (8-9 kB pour du drag/snap dont aucun des deux n'a besoin — ce sont des crossfades). *Gain : ~110 lignes supprimées, a11y du carrousel Home corrigée, 0 kB ajouté. Effort : 3 h.*

Actions de rang inférieur, non chiffrées ci-dessus : `ui/Cta.tsx` + `ui/ArrowLink.tsx` (7 liens-flèche et 6 boutons faits main), fusion `ValuePropSection`/`AboutPhilosophy` (identiques à un `text-center` près), `ui/Rule.tsx` (21 filets or en 9 largeurs), `ui/Wordmark.tsx` (2 implémentations, 2 namespaces i18n), suppression de `.pulse-slow` + `@keyframes editorial-pulse` (CSS mort), consolidation des 3 `IntersectionObserver` en un hook, et fenêtre glissante sur les 12 slides Home (2 256 kB chargés d'un coup, le `loading="lazy"` étant inopérant sur des slides `opacity-0` dans le viewport).

**Note méthodo :** `bun run deadcode` tel que configuré aujourd'hui remonte 390 fichiers dont ~380 sont des artefacts `.next` du worktree `.claude/worktrees/seo-audit`. La règle maison « knip avant chaque PR » n'est donc pas applicable en l'état — le point 2 ci-dessus la rétablit.
