# Audit du header de la Home — `feature/home-best-practices-audit`

Périmètre lu intégralement : `Header.tsx`, `HeaderShell.tsx`, `HeaderNavBar.tsx`, `BrandLogo.tsx`, `LogoMonogram.tsx`, `LanguageSelect.tsx`, `InquireCta.tsx`, `MobileMenu.tsx`, `header/{MobileMenuPanel,mobile-menu-context,HeaderAutoHide}.tsx`, `ui/{button,select}.tsx`, `app/[locale]/layout.tsx`, `PageLayout.tsx`, `globals.css`, `next.config.ts`, `messages/{en,fr}.json`, `i18n/{routing,navigation}.ts`, `src/proxy.ts`.

**À porter au crédit de l'existant** (vérifié, ne pas « corriger ») : le pattern `children`/`panel` de `HeaderShell` fonctionne — `BrandLogo` et `LogoMonogram` n'ont pas de `"use client"` et restent bien des Server Components (`BrandLogo.tsx:1-5`, `LogoMonogram.tsx:1-10`). Le listener de scroll est `{ passive: true }` (`HeaderShell.tsx:40`). Le burger fait `size-11` = 44px (`MobileMenu.tsx:23`). `aria-controls`/`id` du panneau concordent (`MobileMenu.tsx:21` ↔ `MobileMenuPanel.tsx:26`). `useSelectedLayoutSegment()` renvoie bien le segment **interne** (`about-us`, pas `a-propos`) grâce au rewrite de `src/proxy.ts:4` — l'`aria-current` fonctionne donc dans les deux locales, contrairement à ce qu'on pourrait craindre.

---

## 1. Tableau des constats

| # | Sévérité | Axe | Fichier:ligne | Constat | Correction proposée | Effort | Quick win |
|---|---|---|---|---|---|---|---|
| 1 | **bloquant** | 3 | `src/app/[locale]/layout.tsx:114` | Aucun élément `<main>` dans toute l'application (`grep -rn "<main" src/` → 0 résultat) et aucun lien d'évitement. Le clavier doit traverser logo + 8 liens + sélecteur + CTA + burger sur **chaque** page. Échec WCAG 2.4.1 (niveau A) et 1.3.1. | `<div className="flex flex-1 flex-col">` → `<main id="content" className="flex flex-1 flex-col">`, et ajouter avant `<Header />` : `<a href="#content" className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[60] focus:bg-foreground focus:px-4 focus:py-2 focus:text-background">{t("skipToContent")}</a>` (+ clé i18n EN/FR) | S | **oui** |
| 2 | **bloquant** | 3 | `LanguageSelect.tsx:67` | `focus-visible:border-transparent focus-visible:ring-0` neutralise le focus ring, et la classe de base de `SelectTrigger` (`ui/select.tsx:44`) contient déjà `outline-none`. Résultat : **aucun indicateur de focus visible** sur le sélecteur de langue. Échec WCAG 2.4.7 (AA). | `focus-visible:border-transparent focus-visible:ring-0` → `focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground` | S | **oui** |
| 3 | **majeur** | 2 | `HeaderShell.tsx:109` | La barre n'a **pas** de `max-w-[1440px]`, alors que le commentaire lignes 105-108 affirme l'inverse et que **toutes** les sections l'ont (`home/AboutSection.tsx:12`, `ProcessSection.tsx:28`, `StatsSection.tsx:12`, `ValuePropSection.tsx:18`, `GallerySection.tsx:49`, `MaterialsSection.tsx:31`, `CertificationSection.tsx:12`, `Footer.tsx:48`). Au-delà de 1568px le logo et le CTA décrochent de la grille éditoriale. | `"mx-auto flex w-full  items-center…"` → `"mx-auto flex w-full max-w-[1440px] items-center…"` (au passage : double espace à supprimer entre `w-full` et `items-center`) | S | **oui** |
| 4 | **majeur** | 2 | `HeaderShell.tsx:109` vs `MobileMenuPanel.tsx:30` vs sections | Trois paddings horizontaux différents pour la même gouttière : header `px-[22px]`, panneau mobile `px-6` (24px), sections `px-6`. Le burger et les liens du panneau ne sont donc pas alignés verticalement. Aucun ne correspond à `--spacing-gutter: 24px` (`globals.css:20`) côté header. | Header : `px-[22px]` → `px-gutter` ; `lg:px-16` → `lg:px-margin-desktop` (tokens déjà définis `globals.css:20-22`). Panneau : `px-6` → `px-gutter`. | S | **oui** |
| 5 | **majeur** | 3 | `HeaderNavBar.tsx:78-79`, `MobileMenuPanel.tsx:39`, `InquireCta.tsx:19` | `--color-gold: #a6863f` (`globals.css:45`) sur `--background: #f9f9f7` donne **3,26:1** — échec AA 4.5:1 pour du texte de 12,5px/13px (`hover:text-gold`, `isActive && "text-gold"`). Idem pour l'état survolé du CTA : `#f9f9f7` sur `hover:bg-gold` → 3,26:1. *(Le trait `bg-gold` du soulignement, lui, est un objet graphique : 3,26 ≥ 3:1 → conforme 1.4.11.)* | Ajouter dans `@theme` un token dédié au texte : `--color-gold-text: #8a6f34;` (mesuré 4,52:1) et remplacer `text-gold`/`hover:text-gold`/`hover:bg-gold` par `text-gold-text`/`hover:text-gold-text`/`hover:bg-gold-text`. `--color-gold` reste réservé aux filets et numéraux. | M | non |
| 6 | **majeur** | 3 | `BrandLogo.tsx:24` | `text-[9.5px] … text-brushed-steel` : `#8e8e93` sur `#f9f9f7` = **3,09:1**, échec AA pour du texte de 9,5px (le pire cas du header : plus petit texte × plus faible contraste). | `text-brushed-steel` → `text-on-surface-variant` (`#444748`, 8,89:1) et remonter la taille à ≥10px | S | **oui** |
| 7 | **majeur** | 3 | `LanguageSelect.tsx:67` | `size="sm" h-auto px-0 py-0 text-xs` avec `[&_svg]:size-3` → cible d'environ 16×30px. Échec WCAG 2.2 AA 2.5.8 (24×24 minimum), et très loin des 44px recommandés — c'est le seul contrôle interactif du header visible sur mobile avec le burger. | `h-auto … px-0 py-0` → `min-h-11 px-2 py-2 -mx-2` (surface tactile étendue sans décaler la mise en page) | S | **oui** |
| 8 | **majeur** | 1 | `HeaderNavBar.tsx:40-57` | L'effet dépend de `measure`, qui dépend de `target`, qui dépend de `hovered` : **le listener `resize` est détaché puis rattaché à chaque survol d'un item** (8 items → 8 cycles add/remove). Viole `rerender-narrow-effect-dependencies` et `client-event-listeners` (dédup). De plus `measure` appelle 2× `getBoundingClientRect()` sans throttle sur `resize` → layout thrashing (`js-avoid-layout-thrashing`). | Séparer : un `useEffect(() => { window.addEventListener("resize", measureRef.current) }, [])` avec la fonction stockée en ref, + un `useLayoutEffect(measure, [target])` pour la mesure. Ou voir #9 qui supprime le problème à la racine. | M | non |
| 9 | **majeur** | 1 + 2 | `HeaderNavBar.tsx:17-96` | Toute la mécanique (2 refs, 2 `useState`, `useCallback`, `useEffect`, `getBoundingClientRect`, style inline) n'existe que pour un trait de 1px. Réalisable en CSS pur, ce qui rend la liste re-rendable sans état. | Supprimer `indicator`/`hovered`/`containerRef`/`linkRefs`/`measure` et poser sur chaque `<Link>` : `relative after:absolute after:-bottom-1.5 after:left-0 after:h-px after:w-full after:origin-left after:scale-x-0 after:bg-gold-text after:transition-transform after:duration-300 hover:after:scale-x-100 focus-visible:after:scale-x-100 aria-[current=page]:after:scale-x-100 motion-reduce:after:transition-none`. Le composant reste client (pour `useSelectedLayoutSegment`) mais perd tout son JS de mesure. | M | non |
| 10 | **majeur** | 3 | `HeaderNavBar.tsx:75` | `onMouseEnter` uniquement : le soulignement coulissant ne suit **jamais** le focus clavier. Un utilisateur au clavier ne perçoit pas l'affordance que la souris obtient. | Ajouter `onFocus={() => setHovered(i)}` et `onBlur={() => setHovered(null)}` — ou, mieux, la solution #9 qui inclut `focus-visible:after:scale-x-100` par construction | S | **oui** |
| 11 | **majeur** | 1 | `Header.tsx:44-53, 59` | `navItems` est sérialisé **deux fois** dans le payload Flight : une fois comme prop de `HeaderNavBar` (ligne 59), une fois comme prop de `MobileMenuPanel` à l'intérieur de `panel` (ligne 53). Viole `server-avoid-duplicate-serialization`. 8 objets `{href,label}` en double sur chaque navigation. | Faire porter la liste par le contexte : `MobileMenuProvider` (déjà client, déjà monté) expose `items`/`cta`, et `MobileMenuPanel` les lit au lieu de les recevoir en props. Ou rendre les `<Link>` en RSC et les passer en `children` aux deux consommateurs. | M | non |
| 12 | **majeur** | 4 | `package.json:deps` + `next.config.ts:32` | `@tabler/icons-react` est déclaré en dépendance **et** dans `optimizePackageImports`, mais `grep -rn "@tabler" src/` → **0 occurrence**. Seconde librairie d'icônes fantôme à côté de `lucide-react` (qui est bien la lib de `components.json:iconLibrary`). | `bun remove @tabler/icons-react` + retirer la ligne de `optimizePackageImports`. Passer `knip` dans la foulée. | S | **oui** |
| 13 | mineur | 1 | `LanguageSelect.tsx:23, 39` | `closeTimer` n'est jamais nettoyé au démontage : un `setTimeout` de 120ms peut survivre au démontage et appeler `setOpen`. Aucun `useEffect(() => () => clearCloseTimer(), [])`. | Ajouter `useEffect(() => clearCloseTimer, []);` | S | **oui** |
| 14 | mineur | 3 | `LanguageSelect.tsx:32-40, 55` | Ouverture du menu **au survol** (`onMouseEnter`). Sur tactile, `onMouseEnter` est émis au tap et se cumule avec le clic du trigger → double bascule possible. Par ailleurs WCAG 1.4.13 exige persistant/survolable/écartable — c'est le cas ici grâce à base-ui, mais le pattern reste fragile. | Conditionner au pointeur fin : n'attacher `handleEnter`/`handleLeave` que si `window.matchMedia("(hover: hover) and (pointer: fine)").matches`, ou supprimer l'ouverture au survol (un select s'ouvre au clic) | S | oui |
| 15 | mineur | 3 | `LanguageSelect.tsx:71` | Le seul contenu du trigger est `<span aria-hidden="true">{locale}</span>` : le lecteur d'écran annonce « Changer de langue » sans jamais dire quelle langue est active. `SelectValue` (exporté par `ui/select.tsx:21`) n'est pas utilisé. | Retirer `aria-hidden` et envelopper : `<SelectValue className="uppercase">{locale}</SelectValue>`, ou ajouter `aria-label={`${t("label")} — ${t("locale",{locale})}`}` | S | **oui** |
| 16 | mineur | 3 | `LanguageSelect.tsx:87` | `t("locale", { locale: cur })` rend « Français » dans une page `lang="en"` (et « English » dans une page `lang="fr"`) sans attribut `lang` sur l'option. Échec WCAG 3.1.2. | `<SelectItem …><span lang={cur}>{t("locale",{locale:cur})}</span></SelectItem>` | S | **oui** |
| 17 | mineur | 3 | `LanguageSelect.tsx:85` | `[&_svg]:hidden` masque l'indicateur de sélection (`CheckIcon`, `ui/select.tsx:127`) : l'état sélectionné n'est plus signalé **que par la couleur** (`data-selected:text-tertiary`). Échec WCAG 1.4.1 pour la partie visuelle. | Retirer `[&_svg]:hidden` et styler l'indicateur, ou ajouter `data-selected:font-bold` / un filet gauche | S | oui |
| 18 | mineur | 3 | `Header.tsx:52` + `messages/fr.json` | `<nav aria-label="Global">` : libellé non descriptif **et non traduit** — `Header.globalLabel` vaut `"Global"` dans `en.json` **et** dans `fr.json`. La clé `Header.menuTitle` ("Main menu" / "Menu principal"), qui conviendrait, existe mais n'est **jamais utilisée** (0 occurrence dans `src/`). | `label={tHeader("globalLabel")}` → `label={tHeader("menuTitle")}` puis supprimer `globalLabel` | S | **oui** |
| 19 | mineur | 3 | `BrandLogo.tsx:11` | `aria-label="Advance Manufacturers"` : rien n'indique que ce lien mène à l'accueil. La clé `Header.brand.monogramAlt` ("Home"/"Accueil") existe et n'est **jamais utilisée**. | ``aria-label={`${t("primary")} ${t("secondary")} — ${t("monogramAlt")}`}`` | S | **oui** |
| 20 | mineur | 3 | `messages/{en,fr}.json` | Trois clés `Header.*` mortes : `menuTitle`, `brand.logoAlt`, `brand.monogramAlt` (0 occurrence). Au passage `logoAlt` = « Advance Manufacturers logo » contient le mot « logo », que la règle alt proscrit. | Après #18 et #19, supprimer `brand.logoAlt` | S | **oui** |
| 21 | mineur | 2 | `MobileMenuPanel.tsx:30` | `max-h-[calc(100dvh-75px)]` code en dur la hauteur du header alors que `HeaderShell.tsx:51-56` publie `--header-h` en continu — et que cette hauteur **varie** (`py-[15px]` ↔ `lg:py-[22px]`/`lg:py-3.5`). Le panneau déborde ou laisse un blanc selon l'état de scroll. | `max-h-[calc(100dvh-75px)]` → `max-h-[calc(100dvh-var(--header-h,75px))]` (le fallback 75px est déjà la convention des 4 autres consommateurs : `design/DesignHero.tsx:13`, `blog/ReadingProgress.tsx:36`, `blog/BlogSubNav.tsx:552`, `about/AboutHero.tsx:10`) | S | **oui** |
| 22 | mineur | 2 + 4 | `InquireCta.tsx:19` | Le CTA réimplémente à la main ce que `ui/button.tsx` fournit (`cva`, focus ring, `disabled`, `[&_svg]` sizing) sans en réutiliser une seule variante. Viole la règle shadcn « Built-in variants first ». Note : `--radius: 0px` (`globals.css:145`) rend déjà `rounded-lg` = 0px, donc `Button` respecte l'exigence « coins 0px » de `docs/brand.md` sans override. | Ajouter à `buttonVariants` : `variant: { cta: "bg-foreground text-background hover:bg-gold-text" }` et `size: { cta: "h-11 px-[--spacing-cta-x] text-label-xs uppercase tracking-label" }`, puis `<Button render={<Link href={href} />} variant="cta" size="cta">` (pattern `render` base-ui) | M | non |
| 23 | mineur | 1 | `InquireCta.tsx:1-13` | `"use client"` uniquement pour `useSelectedLayoutSegment()` → un `aria-current` sur un CTA « Nous contacter ». Cela force la frontière client sur un lien purement présentationnel, et **duplique** l'abonnement au segment déjà fait par `HeaderNavBar.tsx:24`. | Supprimer le `"use client"` et recevoir `isActive` en prop (calculée une fois par le parent client), ou laisser tomber l'`aria-current` du CTA. Combiné à #22, `InquireCta` disparaît au profit de `<Button variant="cta">`. | M | non |
| 24 | mineur | 1 | `HeaderShell.tsx:47-62` | Le `ResizeObserver` est recréé (`disconnect` + `observe`) à **chaque** bascule de `effectiveHidden`, uniquement pour écrire `0px`. | Sortir l'observer dans un effet `[]` et gérer le cas caché dans `publish` en lisant une ref sur `effectiveHidden` (ou `useEffectEvent`) | S | oui |
| 25 | mineur | 1 | `HeaderShell.tsx:71` | `matchMedia("(min-width: 1440px)")` duplique `--breakpoint-nav: 1440px` (`globals.css:14`), et le commentaire lignes 64-65 reconnaît le risque de dérive. | `const bp = getComputedStyle(document.documentElement).getPropertyValue("--breakpoint-nav").trim(); window.matchMedia(`(min-width: ${bp})`)` | S | oui |
| 26 | mineur | 1 + 3 | `HeaderShell.tsx:97, 109` ; `HeaderNavBar.tsx:90` | `transition-[padding] duration-300` (nav) et `transition-[transform,width,opacity] duration-300` (soulignement) ne sont **pas** conditionnés par `motion-safe`, contrairement au burger qui fait bien `motion-reduce:transition-none` (`MobileMenu.tsx:14`). Le header continue d'animer son padding au scroll en mode « mouvement réduit ». | Préfixer par `motion-safe:` (ou ajouter `motion-reduce:transition-none`), en cohérence avec le bloc `@media (prefers-reduced-motion: reduce)` de `globals.css:277-292` | S | **oui** |
| 27 | mineur | 3 | `HeaderNavBar.tsx:77` | `text-[12.5px]` sans `line-height` → hauteur de cible ≈ 15px. Sous les 24×24 de WCAG 2.2 AA 2.5.8 (l'exception « cible en ligne » ne s'applique pas : ce ne sont pas des liens dans une phrase). | Ajouter `py-1.5` aux liens (porte la cible à ~27px sans changer le rendu visuel, le `gap-x-6` du conteneur absorbant) | S | **oui** |
| 28 | mineur | 1 | `HeaderNavBar.tsx:26` | `items.findIndex(it => it.href === currentPath)` : sur un article `/[category]/[slug]`, le segment vaut la catégorie (ex. `certifications`) → **aucun** item n'est actif, l'entrée « Blog » n'est jamais mise en évidence sur les articles. | Comparer par préfixe, ou déclarer un `matchSegments: string[]` par item pour rattacher `[category]` à `/blog` | M | non |
| 29 | mineur | 2 | `HeaderShell.tsx:97` | `will-change-transform` est appliqué **en permanence** au header fixe, en plus d'un `backdrop-blur` permanent : couche de composition maintenue en mémoire sur toute la durée de vie de la page, y compris quand aucune animation n'est en cours. | Retirer `will-change-transform` (l'auto-hide ne concerne que le blog, via `HeaderAutoHide`), ou le passer en `data-[hidden]:will-change-transform` | S | **oui** |
| 30 | mineur | 2 | `HeaderShell.tsx:97` | Deux déclarations de transition concurrentes dans la même chaîne : `transition-[box-shadow] duration-300` **et** `motion-safe:transition-[box-shadow,transform]`. `twMerge` ne les fusionne pas (préfixes de variante différents) : elles coexistent, seul l'ordre dans la feuille tranche. Fragile. | Garder uniquement `motion-safe:transition-[box-shadow,transform] duration-300` | S | **oui** |
| 31 | mineur | 2 | `LanguageSelect.tsx:67, 85` | 12 utilitaires d'override sur `SelectTrigger` et 9 sur `SelectItem`, dont couleurs et typographie — la règle shadcn « className for layout only » est franchement enfreinte. `[&_svg]:size-3` contredit « No sizing classes on icons inside components ». | Ajouter une variante `variant="bare"` à `SelectTrigger` dans `ui/select.tsx` plutôt que de repeindre depuis l'extérieur | M | non |
| 32 | mineur | 2 | `HeaderNavBar.tsx:78` vs `LanguageSelect.tsx:67, 85` | Deux accents de survol différents dans la **même** barre : `hover:text-gold` pour les liens, `hover:text-tertiary` (+ `data-selected:text-tertiary`) pour le sélecteur de langue. `docs/brand.md` et `feedback_home_editorial_typography` désignent l'or comme accent de la home. | `hover:text-tertiary` → `hover:text-gold-text`, `data-selected:text-tertiary` → `data-selected:text-gold-text` | S | **oui** |
| 33 | nit | 5 | `public/icon.svg` | Doublon **octet pour octet** de `public/logo-advance-manufacturer.svg` (md5 identique : `92edfa3c…`), référencé nulle part dans `src/`. | Supprimer, ou déplacer en `src/app/icon.svg` pour servir la convention d'icône Next 16 (il y aurait alors un usage) | S | **oui** |
| 34 | nit | 5 | `public/android-chrome-{192,512}.png` | Jamais référencés : `src/app/manifest.ts:21-24` ne déclare que `favicon.ico` et le SVG. 57,6 Ko d'assets morts. | Les ajouter au manifest (recommandé : Android veut du PNG `maskable`) ou les supprimer | S | **oui** |
| 35 | nit | 5 | `src/config/site.ts:27-32` | `logo: { width: 512, height: 512 }` alors que le SVG a `viewBox="0 0 435.77 373.29"` (ratio 1,17, pas 1,0). Ces dimensions alimentent le JSON-LD. | `width: 436, height: 373` | S | **oui** |
| 36 | nit | 3 | `LogoMonogram.tsx:13-15` | `aria-hidden="true"` + `role="presentation"` + `focusable="false"` : le premier suffit, les deux autres sont redondants. | Garder `aria-hidden="true"` et `focusable="false"` (utile pour IE/Edge legacy), retirer `role="presentation"` | S | oui |
| 37 | nit | 1 | `HeaderShell.tsx:83-90` ; `HeaderNavBar.tsx:40-51` | `reactCompiler: true` (`next.config.ts:11`) rend `useMemo`/`useCallback` manuels superflus. | Les retirer une fois #8/#9 traités | S | oui |
| 38 | nit | 2 | `HeaderNavBar.tsx:69` vs `MobileMenuPanel.tsx:36` | Clés de liste incohérentes : `key={item.label}` (traduit, donc instable entre locales) d'un côté, `key={String(item.href)}` de l'autre. | Uniformiser sur `key={String(item.href)}` | S | **oui** |
| 39 | nit | 1 | `MobileMenuPanel.tsx:31` | `open ? "flex" : "hidden"` démonte visuellement le panneau ; React 19 propose `<Activity mode>` pour préserver l'état (`rendering-use-activity-component`). Sans effet ici (pas d'état à préserver), mais à connaître. | Optionnel | S | non |
| 40 | nit | 2 | `src/config/site.ts:24` | `themeColor: "#101E33"` (bleu marine) n'existe dans **aucun** token de `globals.css` — la barre de navigateur mobile affiche une couleur étrangère à l'identité, juste au-dessus du header. | `themeColor: "#f9f9f7"` (= `--background`) | S | **oui** |

---

## 2. Comptage des valeurs arbitraires `[...]`

Mesuré par extraction regex sur chaque fichier (faux positifs `linkRefs.current[i]` / `current[target]` écartés à la main).

| Fichier | Total `[...]` | dont px | Détail |
|---|---|---|---|
| `Header.tsx` | **0** | 0 | — |
| `HeaderShell.tsx` | **10** | 7 | `px-[22px]`, `py-[15px]` ×2, `lg:py-[22px]`, `backdrop-blur-[20px]`, `nav:backdrop-blur-[14px]`, `shadow-[0_1px_20px_-8px_rgba(26,28,27,0.25)]`, `transition-[box-shadow]`, `transition-[padding]`, `motion-safe:transition-[box-shadow,transform]` |
| `HeaderNavBar.tsx` | **3** | 1 | `text-[12.5px]`, `tracking-[0.12em]`, `transition-[transform,width,opacity]` |
| `BrandLogo.tsx` | **5** | 3 | `text-[19px]`, `text-[9.5px]`, `mt-[3px]`, `tracking-[0.32em]`, `tracking-[0.42em]` |
| `LogoMonogram.tsx` | **1** | 1 | `lg:h-[34px]` |
| `LanguageSelect.tsx` | **3** | 1 | `min-w-[160px]`, `tracking-[0.15em]` ×2 |
| `InquireCta.tsx` | **3** | 2 | `px-[26px]`, `text-[11px]`, `tracking-[0.16em]` |
| `MobileMenu.tsx` | **4** | 4 | `h-[1.5px]`, `gap-[5px]`, `translate-y-[6.5px]`, `-translate-y-[6.5px]` |
| `header/MobileMenuPanel.tsx` | **4** | 3 | `max-h-[calc(100dvh-75px)]`, `backdrop-blur-[18px]`, `text-[13px]`, `tracking-[0.14em]` |
| **Total périmètre** | **33** | **22** | |

Constat structurant : **quatre tailles distinctes pour un seul rôle typographique** (le « Label-Caps » de `docs/brand.md`) — 11px (CTA), 12,5px (nav desktop), 12px via `text-xs` (langue), 13px (nav mobile) — croisées avec **six valeurs de `tracking`** (0.12 / 0.14 / 0.15 / 0.16 / 0.32 / 0.42em). Aucune n'est adossée à un token.

### Tokens `@theme` proposés pour absorber les 33 valeurs

```css
@theme {
  /* ── Rôle « Label-Caps » (docs/brand.md) : 3 tailles, tracking porté par le token ── */
  --text-label-xs: 11px;            /* CTA          → absorbe text-[11px] + tracking-[0.16em] */
  --text-label-xs--line-height: 1;
  --text-label-xs--letter-spacing: 0.16em;

  --text-label: 12.5px;             /* nav desktop  → absorbe text-[12.5px] + tracking-[0.12em] */
  --text-label--line-height: 1.2;
  --text-label--letter-spacing: 0.12em;

  --text-label-lg: 13px;            /* nav mobile   → absorbe text-[13px] + tracking-[0.14em] */
  --text-label-lg--line-height: 1;
  --text-label-lg--letter-spacing: 0.14em;

  /* ── Wordmark ── */
  --text-wordmark: 19px;            /* absorbe text-[19px] + tracking-[0.32em] */
  --text-wordmark--letter-spacing: 0.32em;
  --text-wordmark-sub: 10px;        /* absorbe text-[9.5px] + tracking-[0.42em] (et corrige #6) */
  --text-wordmark-sub--letter-spacing: 0.42em;

  /* ── Rythme vertical du header ── */
  --spacing-header-y: 15px;         /* absorbe py-[15px]     → py-header-y */
  --spacing-header-y-lg: 22px;      /* absorbe lg:py-[22px]  → lg:py-header-y-lg */
  --spacing-cta-x: 26px;            /* absorbe px-[26px]     → px-cta-x */

  /* ── Chrome du header ── */
  --container-editorial: 1440px;    /* → max-w-editorial, mutualisé avec les 8 sections (#3) */
  --blur-header: 20px;              /* absorbe backdrop-blur-[20px] / [18px] / [14px] */
  --blur-header-compact: 14px;
  --shadow-header: 0 1px 20px -8px --alpha(var(--color-foreground) / 25%);  /* absorbe le shadow-[...] et détruit le rgba() en dur */

  /* ── Monogramme + burger ── */
  --spacing-monogram-lg: 34px;      /* absorbe lg:h-[34px] */
  --color-gold-text: #8a6f34;       /* 4,52:1 sur --background — corrige #5 */
}
```

Restent hors tokens, à corriger autrement : `px-[22px]` → `px-gutter` (existe déjà, #4) ; `max-h-[calc(100dvh-75px)]` → `var(--header-h)` (#21) ; `min-w-[160px]` → `min-w-40` (échelle Tailwind) ; les quatre valeurs du burger (`h-[1.5px]`, `gap-[5px]`, `±translate-y-[6.5px]`) sont une géométrie d'icône, légitimement arbitraire — les laisser ; les `transition-[…]` sont des listes de propriétés, pas des valeurs de design — les laisser.

**Bilan** : 33 → **7** valeurs arbitraires résiduelles (les 4 du burger + les 3 `transition-[…]` de `HeaderShell`), soit −79 %.

---

## 3. Frontière client/serveur

### Actuel

```
app/[locale]/layout.tsx                                    ▓ RSC
└─ NextIntlClientProvider
   └─ <Header />                                           ▓ RSC   ← useTranslations, aucun hook client
      └─ <HeaderShell>                                     ░ CLIENT ── 3 useEffect, 3 useState, 2 refs,
         │                                                             ResizeObserver, matchMedia, Context
         │  children (prop) ────────────────────────────────────────┐
         │  ├─ <div><BrandLogo/></div>                    ▓ RSC     │ préservés en RSC grâce au
         │  │     └─ <LogoMonogram/>                      ▓ RSC     │ pattern `children` ✔
         │  ├─ <HeaderNavBar items={navItems}/>           ░ CLIENT  │ ← 8 liens + 2 states + refs +
         │  │                                                       │   getBoundingClientRect + resize
         │  └─ <div>                                      ▓ RSC     │
         │     ├─ <LanguageSelect/>                       ░ CLIENT  │ ← + @base-ui/react/select
         │     │                                                    │   + 3 icônes lucide (1 masquée)
         │     ├─ <InquireCta/>                           ░ CLIENT  │ ← useSelectedLayoutSegment SEUL
         │     └─ <MobileMenu/>                           ░ CLIENT  │ ← useContext (justifié, feuille ✔)
         │                                          ───────────────┘
         └─ panel (prop)
            └─ <MobileMenuPanel items={navItems} cta/>    ░ CLIENT  ← useContext
               └─ <InquireCta/>                           ░ CLIENT

⚠ navItems traverse la frontière RSC→client DEUX fois (#11)
⚠ useSelectedLayoutSegment abonné DEUX fois (HeaderNavBar + InquireCta)
```

### Cible

```
app/[locale]/layout.tsx                                    ▓ RSC
├─ <a href="#content">skip link</a>                        ▓ RSC   ← #1
└─ <Header />                                              ▓ RSC
   └─ <HeaderShell items={navItems} cta={ctaItem}>         ░ CLIENT ── état scroll + open + Context
      │                                                                (fournit AUSSI items/cta : 1 seule
      │                                                                 sérialisation, #11)
      │  children (prop)
      │  ├─ <BrandLogo/>                                   ▓ RSC   inchangé ✔
      │  │     └─ <LogoMonogram/>                          ▓ RSC   inchangé ✔
      │  ├─ <HeaderNavBar/>                                ░ CLIENT ← useSelectedLayoutSegment SEUL,
      │  │                                                            zéro ref / zéro mesure / zéro
      │  │                                                            listener resize (soulignement
      │  │                                                            en `after:scale-x-*`, #9)
      │  └─ <div>
      │     ├─ <LanguageSelect/>                           ░ CLIENT justifié (router.replace)
      │     ├─ <Button variant="cta" render={<Link/>}/>    ▓ RSC   ← #22 + #23 : plus de hook,
      │     │                                                        plus de frontière client
      │     └─ <MobileMenu/>                               ░ CLIENT feuille minimale ✔
      └─ panel (prop)
         └─ <MobileMenuPanel/>                             ░ CLIENT lit items/cta au Context
            └─ <Button variant="cta" render={<Link/>}/>    ▓ RSC
```

Gains : un composant client de moins (`InquireCta` ×2 usages), une sérialisation de `navItems` en moins, un abonnement `useSelectedLayoutSegment` en moins, `HeaderNavBar` réduit à un abonnement de segment (plus de `getBoundingClientRect`, plus de listener `resize`, plus de re-souscription au survol).

---

## 4. Inventaire alt / aria du périmètre

| Élément | Fichier:ligne | Attribut | Valeur | Nature | Verdict |
|---|---|---|---|---|---|
| Monogramme SVG inline | `LogoMonogram.tsx:13-15` | `aria-hidden="true"` + `role="presentation"` + `focusable="false"` | — | décoratif | ✅ correct (redondance à nettoyer, #36) |
| Lien logo | `BrandLogo.tsx:11` | `aria-label` | « Advance Manufacturers » | informatif | ⚠️ n'indique pas la destination (#19) |
| Wordmark ligne 1 | `BrandLogo.tsx:17` | `aria-hidden="true"` | — | décoratif (doublon aria-label) | ✅ |
| Wordmark ligne 2 | `BrandLogo.tsx:23` | `aria-hidden="true"` | — | décoratif | ✅ |
| `<nav>` | `Header.tsx:52` → `HeaderShell.tsx:103` | `aria-label` | « Global » / « Global » | landmark | ❌ non traduit + non descriptif (#18) |
| Liens de nav | `HeaderNavBar.tsx:74` | `aria-current` | `"page"` si actif | état | ✅ (segment interne, OK en EN et FR) |
| Soulignement coulissant | `HeaderNavBar.tsx:89` | `aria-hidden="true"` | — | décoratif | ✅ |
| CTA « Contact » | `InquireCta.tsx:17` | `aria-current` | `"page"` si actif | état | ✅ |
| Trigger langue | `LanguageSelect.tsx:64` | `aria-label` | « Change language » / « Changer de langue » | contrôle | ⚠️ valeur courante non annoncée (#15) |
| Code locale dans le trigger | `LanguageSelect.tsx:71` | `aria-hidden="true"` | « en » / « fr » | — | ❌ masque la seule info de valeur (#15) |
| Options de langue | `LanguageSelect.tsx:87` | *(aucun `lang`)* | « English » / « Français » | contenu multilingue | ❌ WCAG 3.1.2 (#16) |
| Bouton burger | `MobileMenu.tsx:20-22` | `aria-expanded`, `aria-controls="mobile-menu-panel"`, `aria-label` dynamique | « Open main menu » / « Close » — traduits | contrôle | ✅ exemplaire |
| Barres du burger | `MobileMenu.tsx:25-27` | `aria-hidden="true"` ×3 | — | décoratif | ✅ |
| Panneau mobile | `MobileMenuPanel.tsx:26` | `id="mobile-menu-panel"` | — | cible de `aria-controls` | ✅ concordance vérifiée |
| **Élément `<main>`** | `layout.tsx:114` | **absent** | — | landmark | ❌ **inexistant dans tout le projet** (#1) |
| **Lien d'évitement** | `layout.tsx` | **absent** | — | bypass | ❌ **inexistant** (#1) |
| `apple-touch-icon.png` | `layout.tsx:92` | — | 180×180, 9,3 Ko | icône | ✅ PNG légitime (exigence iOS) |
| `favicon.ico` | `manifest.ts:22` | — | 8,1 Ko | icône | ✅ |
| `logo-advance-manufacturer.svg` | `manifest.ts:23` / `site.ts:27` | `alt: "ADV MFR"` (JSON-LD) | 1,2 Ko | icône + JSON-LD | ⚠️ dimensions déclarées fausses (#35) |
| `public/icon.svg` | *aucune* | — | 1,2 Ko | orphelin | ❌ doublon md5 exact (#33) |
| `public/android-chrome-*.png` | *aucune* | — | 57,6 Ko | orphelins | ❌ non référencés (#34) |

Aucun `<img>` ni `next/image` dans le périmètre header — donc aucune question de `priority`, `sizes` ou `loading` à trancher ici.

---

## 5. Les 5 actions les plus rentables

1. **`<main>` + lien d'évitement** (#1) — effort S, impact maximal. C'est un échec WCAG niveau A qui touche **toutes** les pages du site, pas seulement la home, et qui rend la navigation clavier pénible partout. Un `<main id="content">` et six lignes de skip link. Rien d'autre dans cet audit n'a ce rapport coût/bénéfice.

2. **Restaurer les indicateurs visuels manquants et le token or accessible** (#2 + #5 + #6) — effort S+M. Trois échecs AA groupés qui se corrigent ensemble : rendre le focus du sélecteur de langue visible, introduire `--color-gold-text: #8a6f34` (4,52:1, mesuré) pour tous les usages **texte** de l'or, et passer le sous-titre du wordmark de `text-brushed-steel` (3,09:1) à `text-on-surface-variant` (8,89:1). Un audit Lighthouse/axe passe de rouge à vert sur le header.

3. **`max-w-[1440px]` + gouttières tokenisées sur la barre** (#3 + #4) — effort S. Le header est aujourd'hui le **seul** bloc de la page à ne pas respecter la grille éditoriale que les huit autres sections appliquent ; au-delà de 1568px le logo décroche visiblement. Deux classes à changer, et l'écart de 2px entre `px-[22px]` (barre) et `px-6` (panneau mobile + sections) disparaît au passage.

4. **Soulignement en CSS pur + suppression du churn de listener** (#9 + #8 + #10 + #37) — effort M. Un seul refactor qui supprime : deux `useState`, deux `useRef`, un `useCallback`, un `useEffect`, deux `getBoundingClientRect()` par mesure, le cycle add/remove du listener `resize` à **chaque survol**, et le style inline. Il corrige gratuitement le fait que le soulignement ignore le focus clavier. `HeaderNavBar` passe de 99 à ~45 lignes.

5. **Absorber les 33 valeurs arbitraires dans les tokens `@theme`** (§2) + **purger `@tabler/icons-react`** (#12) — effort M. Le bloc `@theme` proposé fait tomber le périmètre à 7 valeurs arbitraires résiduelles (−79 %) et fixe enfin **une seule** définition du rôle « Label-Caps » de `docs/brand.md`, au lieu des quatre tailles × six trackings actuels. La suppression de la librairie d'icônes fantôme (0 occurrence dans `src/`, pourtant déclarée en dépendance **et** dans `optimizePackageImports`) est un `bun remove` + une ligne — à enchaîner avec un passage de `knip` qui ramassera aussi les trois clés i18n mortes et les trois assets orphelins de `public/`.
