← Contextes /
mobile-css-redesign.md 1574 lignes · 37.5 KB
Personnaliser Télécharger
# CLAUDE.md — Redesign Mobile & CSS Design

> Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour guider le travail sur le responsive mobile et le design CSS.

---

## Section 1 : Workflow de redesign mobile

### Audit initial — par où commencer

Avant de toucher une seule ligne de CSS, faire un passage rapide dans cet ordre :

1. **Balise viewport** — présente ? `width=device-width, initial-scale=1` ? Pas de `user-scalable=no` ?
2. **font-size sur les inputs** — inspecter les champs `<input>`, `<textarea>`, `<select>`. Si < 16px : zoom iOS garanti.
3. **Overflow horizontal** — ouvrir Chrome DevTools en mode mobile, regarder si une scrollbar apparaît en bas. Traquer avec `* { outline: 1px solid red; }`.
4. **Touch targets** — tapper sur les boutons et liens de nav en mode émulation. Tout ce qui fait < 44px de hauteur est à corriger.
5. **Textes justifiés** — rechercher `text-align: justify` dans le CSS. Supprimer ou limiter au desktop uniquement.

### Lighthouse mobile — cibles

Lancer Lighthouse en mode "Mobile" (pas Desktop) dans DevTools :

| Métrique | Cible |
|----------|-------|
| Performance | ≥ 80 |
| Accessibility | ≥ 90 |
| Best Practices | ≥ 90 |
| SEO | ≥ 90 |

Les alertes à corriger en priorité : tap targets trop petits, contraste insuffisant, viewport manquant, inputs sans label.

### DevTools device emulation

- Tester avec les profils : **iPhone SE (375px)**, **iPhone 14 (390px)**, **Pixel 7 (412px)**, **iPad (768px)**
- Activer "Show media queries" pour voir les breakpoints actifs
- Tester en mode paysage (landscape)
- Simuler réseau "Slow 4G" pour détecter les problèmes de performance

### Test sur vrai device

L'émulation ne remplace pas le test réel. Points spécifiques à vérifier sur un vrai téléphone :
- Zoom automatique iOS sur les inputs (ne se voit qu'en vrai)
- Scroll momentum et zones de rebond
- Safe areas (encoche, barre de navigation)
- Clavier virtuel qui pousse le layout

### Checklist "mobile done"

- [ ] Aucun scroll horizontal non intentionnel
- [ ] Tous les éléments interactifs ≥ 44×44px
- [ ] Aucun input avec font-size < 16px
- [ ] Viewport meta tag correct
- [ ] Pas de `text-align: justify` sur mobile
- [ ] Corps de texte ≥ 16px, line-height ≥ 1.5
- [ ] Images avec `max-width: 100%`
- [ ] Dark mode fonctionne (si implémenté)
- [ ] Animations respectent `prefers-reduced-motion`
- [ ] Lighthouse Accessibility ≥ 90

---

## Section 2 : Outils de test mobile — screenshots, émulation, MCP

### Installation des dépendances de test

```bash
# Playwright (recommandé — multi-navigateur)
npm init -y
npm install playwright
npx playwright install chromium

# OU Puppeteer (Chromium uniquement)
npm install puppeteer
```

### Screenshots mobile automatisés

```javascript
// screenshot-mobile.mjs
import { chromium } from 'playwright';

const devices = [
  { name: 'iPhone SE', width: 375, height: 667, scale: 2 },
  { name: 'iPhone 14', width: 390, height: 844, scale: 3 },
  { name: 'Pixel 7', width: 412, height: 915, scale: 2.625 },
  { name: 'iPad', width: 768, height: 1024, scale: 2 },
];

const url = process.argv[2] || 'http://localhost:8000';

const browser = await chromium.launch();

for (const device of devices) {
  const context = await browser.newContext({
    viewport: { width: device.width, height: device.height },
    deviceScaleFactor: device.scale,
    isMobile: device.width < 768,
    hasTouch: device.width < 768,
  });
  const page = await context.newPage();
  await page.goto(url, { waitUntil: 'networkidle' });
  await page.screenshot({
    path: `screenshot-${device.name.toLowerCase().replace(/\s/g, '-')}.png`,
    fullPage: true,
  });
  console.log(`✓ ${device.name} (${device.width}px)`);
  await context.close();
}

await browser.close();
```

Usage :

```bash
node screenshot-mobile.mjs http://localhost:8000
# Génère : screenshot-iphone-se.png, screenshot-iphone-14.png, etc.
```

Claude Code peut lire ces screenshots pour vérifier visuellement le rendu.

### Dark mode et reduced motion screenshots

```javascript
// screenshot-modes.mjs — variantes dark mode et reduced motion
import { chromium } from 'playwright';

const url = process.argv[2] || 'http://localhost:8000';
const browser = await chromium.launch();

// Dark mode - iPhone 14
const darkCtx = await browser.newContext({
  viewport: { width: 390, height: 844 },
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true,
  colorScheme: 'dark',
});
const darkPage = await darkCtx.newPage();
await darkPage.goto(url, { waitUntil: 'networkidle' });
await darkPage.screenshot({ path: 'screenshot-dark-mode.png', fullPage: true });
console.log('✓ Dark mode');
await darkCtx.close();

// Reduced motion - iPhone 14
const reducedCtx = await browser.newContext({
  viewport: { width: 390, height: 844 },
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true,
  reducedMotion: 'reduce',
});
const reducedPage = await reducedCtx.newPage();
await reducedPage.goto(url, { waitUntil: 'networkidle' });
await reducedPage.screenshot({ path: 'screenshot-reduced-motion.png', fullPage: true });
console.log('✓ Reduced motion');
await reducedCtx.close();

await browser.close();
```

### MCP Servers utiles

**Puppeteer MCP** (`@modelcontextprotocol/server-puppeteer`) :
- Permet à Claude Code de naviguer sur un site, prendre des screenshots, inspecter le DOM
- Configuration dans `.claude/settings.json` :

```json
{
  "mcpServers": {
    "puppeteer": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-puppeteer"]
    }
  }
}
```

**Playwright MCP** (community `playwright-mcp`) :
- Même principe, multi-navigateur
- Utile pour tester le rendu sur différents moteurs (Chromium, Firefox, WebKit/Safari)

### Lighthouse en CLI

```bash
# Installation
npm install -g lighthouse

# Audit mobile
lighthouse http://localhost:8000 --preset=perf --form-factor=mobile --output=html --output-path=./lighthouse-mobile.html

# Audit mobile en JSON (parsable)
lighthouse http://localhost:8000 --form-factor=mobile --output=json --output-path=./lighthouse-mobile.json
```

Claude Code peut lire le rapport HTML pour identifier les problèmes.

### Debug avec Chrome DevTools Protocol

Playwright et Puppeteer exposent le CDP (Chrome DevTools Protocol), ce qui permet un accès programmatique à :
- **Network throttling** — simuler une connexion Slow 3G/4G pour détecter les goulots de chargement
- **CSS coverage** — identifier les règles CSS jamais appliquées (dead code)
- **Performance traces** — capturer une trace complète pour analyser le rendu frame par frame

---

## Section 3 : Règles CSS Mobile — référence complète

### Viewport meta tag

```html
<!-- ✅ Correct -->
<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- Si on utilise safe-area-inset -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

<!-- ❌ Ne jamais faire : empêche le zoom pour les malvoyants -->
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1">
```

### Reset de base

```css
*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

html, body {
    overflow-x: hidden;
    max-width: 100%;
}

img, video, svg {
    max-width: 100%;
    height: auto;
    display: block;
}
```

### Alignement des textes

Règle simple : **gauche par défaut, centré uniquement pour les CTA, jamais justify sur mobile**.

```css
/* Base mobile */
h1, h2, h3, h4 {
    text-align: left;
}

p, li {
    text-align: left;
}

/* CTA : centré */
.cta-wrapper {
    text-align: center;
}

/* Justify uniquement sur desktop avec césure */
@media (min-width: 768px) {
    .prose p {
        text-align: justify;
        hyphens: auto;
        -webkit-hyphens: auto;
    }
}
```

### Touch targets — 44×44px minimum

```css
/* Boutons */
.btn {
    min-height: 44px;
    min-width: 44px;
    padding: 12px 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* Liens de navigation */
nav a {
    display: block;
    min-height: 44px;
    padding: 12px 16px;
    line-height: 20px;
}

/* Icônes cliquables */
.icon-btn {
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

/* Zone de tap élargie sans changer l'apparence */
.small-icon {
    position: relative;
    width: 20px;
    height: 20px;
}

.small-icon::after {
    content: '';
    position: absolute;
    inset: -12px;
}
```

### Font-size — prévention zoom iOS et lisibilité

```css
/* ❌ Moins de 16px sur les inputs = zoom iOS automatique */

/* ✅ Minimum absolu sur les inputs */
input,
textarea,
select {
    font-size: 16px;
}

/* Si taille différente souhaitée sur desktop */
@media (min-width: 768px) {
    input,
    textarea,
    select {
        font-size: 14px;
    }
}

/* Corps de texte */
body {
    font-size: 16px;
    line-height: 1.6;
}

/* Titres fluides avec clamp() */
h1 { font-size: clamp(1.5rem, 5vw, 2.5rem); }
h2 { font-size: clamp(1.25rem, 4vw, 2rem); }
h3 { font-size: clamp(1.1rem, 3vw, 1.5rem); }
```

### Media queries — mobile-first uniquement

```css
/* ✅ Mobile-first : base = mobile, on ajoute pour les grands écrans */
.card {
    display: block;
    padding: 16px;
}

@media (min-width: 768px) {
    .card {
        display: flex;
        padding: 24px;
    }
}

@media (min-width: 1024px) {
    .card {
        padding: 32px;
    }
}

/* ❌ Desktop-first : à éviter */
.card {
    display: flex;
    padding: 32px;
}

@media (max-width: 767px) {
    .card {
        display: block;
        padding: 16px;
    }
}
```

Breakpoints standards à utiliser : `480px`, `768px`, `1024px`, `1280px`.

### Prévention overflow-x

```css
html, body {
    overflow-x: hidden;
    max-width: 100%;
}

/* Tableaux : scroll plutôt que casser le layout */
.table-wrapper {
    overflow-x: auto;
}

table {
    min-width: 600px;
    width: 100%;
}

/* Debug : identifier l'élément qui déborde */
* { outline: 1px solid red; }
```

Attention : `overflow-x: hidden` sur `body` peut casser `position: sticky`. Si un sticky ne fonctionne plus, isoler `overflow-x: hidden` sur un wrapper dédié.

### Images et aspect-ratio

```css
img {
    max-width: 100%;
    height: auto;
    display: block;
}

/* Ratio fixe pour éviter le CLS (Cumulative Layout Shift) */
.card-image {
    aspect-ratio: 4 / 3;
    width: 100%;
    object-fit: cover;
}

.hero-image {
    aspect-ratio: 16 / 9;
    width: 100%;
    object-fit: cover;
}

/* Ratio différent sur mobile */
@media (max-width: 767px) {
    .hero-image {
        aspect-ratio: 4 / 3;
    }
}
```

### Flex et Grid — pièges courants

```css
/* Flex : min-width: 0 obligatoire pour éviter les débordements de texte */
.flex-item {
    min-width: 0;
}

/* Flex wrap automatique */
.card-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
}

.card-grid .card {
    flex: 1 1 280px;
    min-width: 0;
}

/* Grid responsive sans media query */
.auto-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 16px;
}

/* Grid : colonne unique sur mobile, deux colonnes sur desktop */
.two-col {
    display: grid;
    grid-template-columns: 1fr;
    gap: 24px;
}

@media (min-width: 768px) {
    .two-col {
        grid-template-columns: 1fr 1fr;
    }
}
```

### Safe area insets (encoche, barre de navigation)

```css
/* Nécessite viewport-fit=cover dans la balise meta */

.bottom-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: 56px;
    padding-bottom: env(safe-area-inset-bottom);
    background: var(--color-bg);
}

.top-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    padding-top: env(safe-area-inset-top);
}

.main-content {
    padding-bottom: calc(56px + env(safe-area-inset-bottom));
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
}
```

Si `viewport-fit=cover` n'est pas défini, les valeurs valent 0 — ces règles n'ont aucun effet négatif.

### prefers-reduced-motion

```css
/* Animations définies normalement */
.fade-in {
    animation: fadeIn 0.4s ease;
}

/* Suppression globale si l'utilisateur le demande */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* Approche plus granulaire */
@media (prefers-reduced-motion: reduce) {
    .fade-in {
        opacity: 1;
        animation: none;
    }
}
```

### prefers-color-scheme — dark mode

```css
:root {
    --color-bg: #ffffff;
    --color-text: #1a1a1a;
    --color-text-muted: #6b7280;
    --color-border: #e5e7eb;
    --color-surface: #f9fafb;
    --color-primary: #2563eb;
    --color-primary-text: #ffffff;
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-bg: #0f172a;
        --color-text: #e2e8f0;
        --color-text-muted: #94a3b8;
        --color-border: #1e293b;
        --color-surface: #1e293b;
        --color-primary: #3b82f6;
        --color-primary-text: #ffffff;
        --shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
    }

    img:not([src*=".svg"]) {
        filter: brightness(0.9);
    }
}

/* Toggle manuel qui coexiste avec la préférence système */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --color-bg: #0f172a;
        /* ... */
    }
}

[data-theme="dark"] {
    --color-bg: #0f172a;
    /* ... */
}

[data-theme="light"] {
    --color-bg: #ffffff;
    /* ... */
}
```

### Formulaires

```css
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="number"],
textarea,
select {
    width: 100%;
    font-size: 16px; /* obligatoire — empêche zoom iOS */
    padding: 12px 16px;
    min-height: 44px;
    border-radius: 8px;
    border: 1px solid var(--color-border);
    -webkit-appearance: none;
    appearance: none;
    background: var(--color-bg);
    color: var(--color-text);
}

label {
    display: block;
    font-size: 16px;
    margin-bottom: 6px;
    font-weight: 500;
}

.form-group {
    margin-bottom: 20px;
}
```

Attributs HTML à ne pas oublier (CSS ne peut pas les remplacer) :

```html
<!-- Clavier numérique -->
<input type="text" inputmode="numeric">

<!-- Clavier décimal -->
<input type="text" inputmode="decimal">

<!-- Clavier email avec @ direct -->
<input type="email" inputmode="email">

<!-- Clavier URL -->
<input type="url" inputmode="url">
```

### Espacement latéral minimum

```css
.container {
    padding-left: 16px;  /* minimum absolu sur mobile */
    padding-right: 16px;
    max-width: 1200px;
    margin: 0 auto;
}

@media (min-width: 768px) {
    .container {
        padding-left: 24px;
        padding-right: 24px;
    }
}

@media (min-width: 1024px) {
    .container {
        padding-left: 32px;
        padding-right: 32px;
    }
}
```

---

## Section 4 : Conventions CSS — règles génériques

### CSS custom properties pour le theming

Toujours définir les valeurs répétées en custom properties. Jamais de couleurs ou d'espacements en dur dispersés dans le CSS.

```css
:root {
    /* Couleurs */
    --color-primary: #2563eb;
    --color-primary-hover: #1d4ed8;
    --color-bg: #ffffff;
    --color-text: #1a1a1a;
    --color-text-muted: #6b7280;
    --color-border: #e5e7eb;
    --color-surface: #f9fafb;
    --color-danger: #dc2626;
    --color-success: #16a34a;

    /* Espacements */
    --space-xs: 4px;
    --space-sm: 8px;
    --space-md: 16px;
    --space-lg: 24px;
    --space-xl: 32px;
    --space-2xl: 48px;

    /* Typographie */
    --font-size-sm: 0.875rem;
    --font-size-base: 1rem;
    --font-size-lg: 1.125rem;
    --font-size-xl: 1.25rem;
    --line-height-base: 1.6;
    --line-height-tight: 1.3;

    /* Rayons */
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 12px;
    --radius-full: 9999px;

    /* Ombres */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
    --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);

    /* Transitions */
    --transition-fast: 0.15s ease;
    --transition-base: 0.25s ease;
}
```

### Conventions de nommage

Être cohérent. BEM ou utility-first, pas les deux mélangés dans le même projet.

**BEM** (conseillé pour les composants isolés) :

```css
/* Block */
.card {}

/* Element */
.card__title {}
.card__body {}
.card__footer {}

/* Modifier */
.card--featured {}
.card--compact {}
.card__title--large {}
```

**Ne pas faire** :

```css
/* Mélange de conventions — illisible */
.card {}
.cardTitle {}
.card-body {}
.card_footer--active {}
```

### Accessibilité

**focus-visible** — ne jamais supprimer l'outline sans le remplacer :

```css
/* ❌ À ne jamais faire */
* { outline: none; }
:focus { outline: none; }

/* ✅ Remplacer l'outline natif par un style custom */
:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
    border-radius: 2px;
}

/* Supprimer l'outline seulement pour les clics souris (pas clavier) */
:focus:not(:focus-visible) {
    outline: none;
}
```

**Contrastes WCAG AA** :

| Contexte | Ratio minimum |
|----------|--------------|
| Texte normal (< 18px) | 4.5:1 |
| Texte large (≥ 18px ou ≥ 14px bold) | 3:1 |
| Composants UI, icônes | 3:1 |

Outils : [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/), Chrome DevTools accessibilité.

### Performance CSS

Propriétés coûteuses à éviter sur les éléments animés ou nombreux :

```css
/* ❌ Déclenche du repaint sur chaque frame d'animation */
.card:hover {
    box-shadow: 0 8px 30px rgba(0,0,0,0.2);
}

/* ✅ Mieux : préparer l'ombre au chargement, changer l'opacité */
.card {
    box-shadow: 0 8px 30px rgba(0,0,0,0);
    transition: box-shadow var(--transition-base);
}
.card:hover {
    box-shadow: 0 8px 30px rgba(0,0,0,0.2);
}

/* ❌ filter sur beaucoup d'éléments = coûteux */
.grid-item {
    filter: grayscale(100%);
}

/* ✅ Utiliser opacity ou transform — composités sur le GPU */
.element {
    transform: translateY(0);
    opacity: 1;
    transition: transform var(--transition-base), opacity var(--transition-base);
}

.element.hidden {
    transform: translateY(8px);
    opacity: 0;
}
```

Propriétés "composites" (GPU, performantes pour les animations) : `transform`, `opacity`.
Propriétés qui déclenchent reflow (coûteuses) : `width`, `height`, `margin`, `padding`, `top`, `left`.

### CSS moderne à privilégier

```css
/* clamp() — tailles fluides sans media queries */
h1 { font-size: clamp(1.5rem, 5vw, 2.5rem); }
.container { padding: clamp(16px, 4vw, 48px); }

/* gap — plus lisible que margin sur les enfants */
.grid {
    display: flex;
    gap: 24px; /* ✅ */
}
/* éviter : .grid > * + * { margin-left: 24px; } */

/* aspect-ratio — plus fiable que le padding-top hack */
.video-wrapper {
    aspect-ratio: 16 / 9;
    width: 100%;
}

/* Propriétés logiques — pour le multilinguisme (RTL/LTR) */
.card {
    margin-inline: auto;       /* = margin-left + margin-right */
    padding-block: 24px;       /* = padding-top + padding-bottom */
    border-inline-start: 3px solid var(--color-primary); /* = border-left en LTR */
}

/* inset — remplace top/right/bottom/left */
.overlay {
    position: fixed;
    inset: 0;  /* = top: 0; right: 0; bottom: 0; left: 0; */
}
```

### Règle sur `!important`

`!important` est accepté uniquement dans deux cas :
1. Classes utilitaires (`.sr-only`, `.hidden`, `.visually-hidden`) — par convention
2. Override de styles tiers qu'on ne contrôle pas (plugins, librairies)

En dehors de ces cas, un `!important` signale un problème de spécificité à corriger à la source.

### HTML sémantique en premier

Styliser des éléments sémantiques corrects est plus simple et plus accessible que styler des `<div>` partout.

```html
<!-- ❌ -->
<div class="nav">
    <div class="nav-item" onclick="navigate()">Accueil</div>
</div>

<!-- ✅ -->
<nav>
    <a href="/">Accueil</a>
</nav>
```

```html
<!-- ❌ -->
<div class="button" onclick="submit()">Envoyer</div>

<!-- ✅ -->
<button type="submit">Envoyer</button>
```

Les éléments sémantiques apportent gratuitement : accessibilité clavier, rôles ARIA implicites, compatibilité lecteur d'écran.

---

## Section 5 : Checklist de test

### Devices à tester

| Device | Largeur | Priorité |
|--------|---------|----------|
| iPhone SE (2020/2022) | 375px | Haute — écran le plus étroit mainstream |
| iPhone 14 / 15 | 390px | Haute — référence iOS courante |
| Android mid-range (Pixel 6a, Samsung A54) | 360–412px | Haute |
| iPad / iPad Mini | 768px | Moyenne |
| Desktop laptop | 1280–1440px | Baseline |

### Modes à tester

- [ ] **Portrait** (défaut)
- [ ] **Paysage (landscape)** — vérifier que rien ne déborde, les éléments fixes ne prennent pas trop de hauteur
- [ ] **Dark mode** — activer dans les réglages du device ou de l'OS
- [ ] **Reduced motion** — activer "Réduire les animations" dans l'accessibilité OS
- [ ] **Zoom texte à 150%** — dans les réglages d'accessibilité du navigateur

### Lighthouse — cibles par métrique

Lancer en mode "Mobile" :

| Score | Cible |
|-------|-------|
| Performance | ≥ 80 |
| Accessibility | ≥ 90 |
| Best Practices | ≥ 90 |
| SEO | ≥ 90 |

Points d'accessibilité à corriger en priorité :
- Contraste insuffisant (auto-détecté)
- Images sans `alt`
- Éléments interactifs sans label
- Liens avec un texte générique ("cliquez ici", "en savoir plus")
- Tap targets trop petits

### Vérification lecteur d'écran (bases)

Sur iOS : activer **VoiceOver** (triple-clic bouton latéral).
Sur Android : activer **TalkBack**.

Parcourir la page avec les gestes de base :
- Le titre principal est-il annoncé correctement ?
- Les boutons ont-ils un libellé clair ?
- Les images décoratives ont-elles `alt=""`?
- Les champs de formulaire ont-ils des labels associés ?

### Checklist finale avant livraison

- [ ] Aucun scroll horizontal sur aucun device testé
- [ ] Tous les éléments interactifs accessibles au clavier (Tab + Enter/Space)
- [ ] focus-visible visible et distinctif
- [ ] Tous les inputs avec font-size ≥ 16px
- [ ] Textes avec contraste ≥ 4.5:1 (normal) ou 3:1 (grand)
- [ ] Images avec `alt` descriptif ou `alt=""` si décoratif
- [ ] Formulaires avec `<label>` associé à chaque input
- [ ] Dark mode : texte lisible, pas de blanc éblouissant
- [ ] Reduced motion : aucune animation qui se joue quand même
- [ ] Lighthouse Accessibility ≥ 90
- [ ] Pas de zoom automatique iOS sur les inputs
- [ ] Touch targets ≥ 44×44px

---

## Section 6 : Pièges iOS et Android

### 100vh sur Safari — le piège classique

```css
/* ❌ 100vh inclut la barre d'adresse Safari → contenu coupé */
.hero { height: 100vh; }

/* ✅ Utiliser svh/dvh (Baseline 2023) */
.hero {
    height: 100vh;    /* fallback navigateurs anciens */
    height: 100svh;   /* small viewport height — le plus sûr */
}

/* dvh = dynamique, s'adapte quand les barres apparaissent/disparaissent */
.fullscreen-overlay {
    height: 100dvh;
}
```

### -webkit-tap-highlight-color — le flash bleu au tap

```css
/* Supprimer le highlight par défaut sur mobile WebKit */
* {
    -webkit-tap-highlight-color: transparent;
}

/* Restaurer un feedback visible pour l'accessibilité */
button:active, a:active {
    opacity: 0.7;
}
```

### touch-action — contrôle des gestes

```css
/* Empêcher le double-tap zoom sur les boutons (garde le pinch-zoom) */
.btn, a {
    touch-action: manipulation;
}

/* Zone de carte/canvas : gérer les gestes manuellement */
.map-container {
    touch-action: none;
}

/* Scroll vertical uniquement (ex: carousel horizontal géré en JS) */
.vertical-only {
    touch-action: pan-y;
}
```

### Rubber-banding iOS et scroll des modals

```css
/* Empêcher le scroll de traverser un modal */
.modal-open body {
    overflow: hidden;
    position: fixed;    /* corrige le bleed-through iOS */
    width: 100%;
}

/* Contenir le scroll dans un élément sans propager au parent */
.modal-body,
.dropdown-menu,
.sidebar-scroll {
    overscroll-behavior: contain;
}

/* Désactiver le pull-to-refresh (app-like) */
html {
    overscroll-behavior-y: none;
}
```

### iOS input appearance — reset complet

```css
/* Empêcher le style natif iOS de surcharger le vôtre */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="number"],
input[type="search"],
input[type="date"],
input[type="time"],
textarea,
select {
    -webkit-appearance: none;
    appearance: none;
}
```

### Android text-size-adjust

```css
/* Chrome Android gonfle le texte dans les conteneurs larges (> 217 chars) */
html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}
```

---

## Section 7 : Font loading et images — performance CLS/LCP

### Stratégie font-display

```css
/* ❌ Par défaut (auto) : texte invisible jusqu'à 3s (FOIT) */

/* ✅ swap : affiche le fallback immédiatement, swap quand chargé */
@font-face {
    font-family: "MyFont";
    src: url('/fonts/myfont.woff2') format('woff2');
    font-display: swap;
    font-weight: 400;
    font-style: normal;
}

/* Pour les polices décoratives non-critiques */
@font-face {
    font-family: "DisplayFont";
    src: url('/fonts/display.woff2') format('woff2');
    font-display: optional;  /* navigateur peut ignorer sur connexion lente */
}
```

### Réduire le CLS avec size-adjust sur le fallback

```css
/* Aligner les métriques du fallback sur la police web pour éviter le reflow */
@font-face {
    font-family: "MyFont-fallback";
    src: local("Arial");
    size-adjust: 105%;
    ascent-override: 90%;
    descent-override: 25%;
    line-gap-override: 0%;
}

body {
    font-family: "MyFont", "MyFont-fallback", sans-serif;
}
```

### WOFF2 uniquement

```css
/* En 2025 : seul WOFF2 nécessaire — support > 95%, 30% mieux compressé */
@font-face {
    src: url('/fonts/myfont.woff2') format('woff2');
    /* Plus besoin de woff, ttf, ou eot */
}
```

### Preload et preconnect

```html
<!-- Preload des polices above-the-fold — critique pour le LCP -->
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>

<!-- Preconnect pour Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
```

### Images — width/height obligatoires

```html
<!-- ✅ TOUJOURS déclarer width/height — source #1 de CLS -->
<img src="photo.jpg" width="800" height="600" alt="...">
<!-- Le CSS peut overrider la taille affichée — le navigateur utilise le ratio -->
```

### fetchpriority — indiquer l'image LCP

```html
<!-- Image hero/LCP : chargement eager + haute priorité réseau -->
<img src="hero.jpg" fetchpriority="high" loading="eager" alt="...">

<!-- Images below-the-fold : lazy + basse priorité -->
<img src="gallery.jpg" loading="lazy" fetchpriority="low" alt="...">
```

### srcset et picture — servir la bonne résolution

```html
<!-- srcset : le navigateur choisit la meilleure source -->
<img
    src="image-800.jpg"
    srcset="image-400.jpg 400w, image-800.jpg 800w, image-1600.jpg 1600w"
    sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 800px"
    alt="..."
    width="800" height="600"
    loading="lazy"
>

<!-- picture : servir AVIF/WebP avec fallback -->
<picture>
    <source srcset="image.avif" type="image/avif">
    <source srcset="image.webp" type="image/webp">
    <img src="image.jpg" alt="..." width="800" height="600" loading="lazy">
</picture>
```

---

## Section 8 : Scroll et navigation mobile

### scrollbar-gutter — éviter le CLS de la scrollbar

```css
/* Réserver l'espace de la scrollbar même quand elle n'est pas visible */
html {
    scrollbar-gutter: stable;
}
```

### scroll-behavior — smooth scroll accessible

```css
/* Appliquer uniquement si l'utilisateur n'a pas demandé reduced motion */
@media (prefers-reduced-motion: no-preference) {
    html {
        scroll-behavior: smooth;
    }
}
```

### scroll-snap — carousels CSS natifs

```css
/* Carousel horizontal */
.carousel {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding: 0 16px;
    -webkit-overflow-scrolling: touch;
}

.carousel-item {
    flex-shrink: 0;
    scroll-snap-align: start;
    width: 80%;
}

/* Sections plein écran — 'proximity' plutôt que 'mandatory' pour ne pas piéger */
.sections-wrapper {
    height: 100svh;
    overflow-y: auto;
    scroll-snap-type: y proximity;
}

.section-fullpage {
    height: 100svh;
    scroll-snap-align: start;
}
```

---

## Section 9 : CSS moderne (Baseline 2024-2025)

### Container queries — media queries pour composants

```css
/* Le parent déclare qu'il est un container */
.card-wrapper {
    container-type: inline-size;
    container-name: card;
}

/* Le composant se style selon la largeur de SON container */
@container card (min-width: 400px) {
    .card {
        display: flex;
        flex-direction: row;
    }
}

/* ⚠️ Un container ne peut PAS se styler lui-même */
```

### text-wrap: balance et pretty

```css
/* Équilibrer la longueur des lignes dans les titres */
h1, h2, h3 {
    text-wrap: balance;
}

/* Éviter les mots orphelins dans le corps de texte */
p {
    text-wrap: pretty;
}
```

### CSS nesting natif (Baseline 2024)

```css
/* Plus besoin de préprocesseur */
.card {
    padding: 16px;

    & .card__title {
        font-size: 1.25rem;
    }

    &:hover {
        box-shadow: var(--shadow-md);
    }

    @media (min-width: 768px) {
        padding: 24px;
    }
}
```

### light-dark() — dark mode simplifié

```css
:root {
    color-scheme: light dark;
}

.card {
    background: light-dark(#ffffff, #1e293b);
    color: light-dark(#1a1a1a, #e2e8f0);
    border-color: light-dark(#e5e7eb, #374151);
}
```

### color-mix() — dériver des variantes d'une couleur

```css
:root {
    --color-primary: #2563eb;
    --color-primary-light: color-mix(in srgb, var(--color-primary) 20%, white);
    --color-primary-dark: color-mix(in srgb, var(--color-primary) 80%, black);
}
```

### @layer — contrôle de la cascade

```css
/* Déclarer l'ordre : les layers bas ont moins de priorité */
@layer reset, base, components, utilities;

@layer reset {
    * { box-sizing: border-box; margin: 0; }
}

/* Utile pour isoler du CSS tiers dans un layer faible */
@layer vendor {
    @import url("third-party.css");
}

/* Les styles hors-layer gagnent toujours sur les styles en layer */
```

### View transitions (Baseline 2025)

```css
/* Toujours conditionner à prefers-reduced-motion */
@media (prefers-reduced-motion: no-preference) {
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation-duration: 0.3s;
    }
}

/* Nommer des éléments pour des transitions ciblées */
.hero-image {
    view-transition-name: hero;
}

/* Cross-document (MPA) */
@view-transition {
    navigation: auto;
}
```

---

## Section 10 : Performance CSS avancée

### content-visibility: auto — skip du rendu hors-écran

```css
/* Accélère le rendu initial des pages longues */
.section-below-fold {
    content-visibility: auto;
    contain-intrinsic-block-size: auto 500px; /* OBLIGATOIRE : réserver l'espace */
}

/* ⚠️ Safari : find-in-page ne trouve pas le contenu caché */
/* ⚠️ Ne pas utiliser content-visibility: hidden — retire de l'arbre d'accessibilité */
```

### CSS containment — isoler les sous-arbres coûteux

```css
/* contain: content = layout + paint + style */
.widget {
    contain: content;
}

/* Utile pour les listes dynamiques */
.feed-item {
    contain: layout paint;
}
```

### will-change — avec parcimonie

```css
/* ❌ JAMAIS global — explose la mémoire GPU sur mobile */
* { will-change: transform; }

/* ✅ Uniquement sur les éléments qui animent en boucle */
.spinner {
    will-change: transform;
}

/* Idéalement : ajouter en JS avant l'animation, retirer après */
```

### Animer uniquement transform et opacity

```css
/* ❌ Déclenche un reflow à chaque frame */
.card:hover {
    width: 110%;
    margin-top: -5px;
}

/* ✅ transform et opacity sont composités GPU — jamais de reflow */
.card {
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
    transform: scale(1.02) translateY(-4px);
}
```

---

## Section 11 : Accessibilité avancée

### forced-colors (Windows High Contrast Mode)

```css
@media (forced-colors: active) {
    /* Restaurer les bordures sur les éléments qui utilisent background-color */
    .card {
        border: 1px solid ButtonText;
    }
    /* Garder le contrôle sur les éléments custom */
    .custom-checkbox {
        forced-color-adjust: none;
    }
}
```

### prefers-contrast

```css
@media (prefers-contrast: more) {
    :root {
        --color-border: #000000;
        --color-text-muted: #444444;
    }
    .btn-ghost {
        border-width: 2px;
    }
}
```

### focus-visible — utiliser outline, pas box-shadow

```css
/* ❌ box-shadow invisible en Windows High Contrast */
:focus-visible {
    box-shadow: 0 0 0 2px var(--color-primary);
}

/* ✅ outline survit au mode forced-colors */
:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

/* Supprimer l'outline uniquement pour les clics souris */
:focus:not(:focus-visible) {
    outline: none;
}
```

---

## Section 12 : Print styles

```css
@media print {
    nav, .sidebar, .cookie-banner, .modal, footer, .btn, .social-links {
        display: none !important;
    }

    a[href]::after {
        content: " (" attr(href) ")";
        font-size: 0.875em;
    }

    img, pre, blockquote, table {
        break-inside: avoid;
    }

    h1, h2, h3 {
        break-after: avoid;
    }

    body {
        font-family: Georgia, serif;
        font-size: 12pt;
        color: #000;
        background: #fff;
    }

    .container {
        max-width: 100%;
        padding: 0;
    }
}
```

---

## Section 13 : Checklist complète mise à jour

### Devices à tester

| Device | Largeur | Priorité |
|--------|---------|----------|
| iPhone SE (2020/2022) | 375px | Haute — écran le plus étroit mainstream |
| iPhone 14 / 15 | 390px | Haute — référence iOS courante |
| Android mid-range (Pixel 6a, Samsung A54) | 360–412px | Haute |
| iPad / iPad Mini | 768px | Moyenne |
| Desktop laptop | 1280–1440px | Baseline |

### Modes à tester

- [ ] Portrait
- [ ] Paysage
- [ ] Dark mode
- [ ] Reduced motion
- [ ] Zoom texte à 150%
- [ ] Contrast élevé (forced-colors)

### Core Web Vitals — cibles

| Métrique | Cible | Impact |
|----------|-------|--------|
| LCP (Largest Contentful Paint) | < 2.5s | Image hero, fonts |
| CLS (Cumulative Layout Shift) | < 0.1 | width/height sur img, font size-adjust |
| INP (Interaction to Next Paint) | < 200ms | Animations GPU-only, pas de reflow |

### Lighthouse — cibles

| Score | Cible |
|-------|-------|
| Performance | ≥ 80 |
| Accessibility | ≥ 90 |
| Best Practices | ≥ 90 |
| SEO | ≥ 90 |

### Checklist finale

- [ ] Aucun scroll horizontal sur aucun device testé
- [ ] Tous les éléments interactifs accessibles au clavier (Tab + Enter/Space)
- [ ] focus-visible visible et utilise outline (pas box-shadow)
- [ ] Tous les inputs avec font-size ≥ 16px
- [ ] Textes avec contraste ≥ 4.5:1 (normal) ou 3:1 (grand)
- [ ] Images avec `alt` descriptif ou `alt=""` si décoratif
- [ ] Images avec `width` et `height` déclarés (CLS)
- [ ] Image LCP avec `fetchpriority="high"` et `loading="eager"`
- [ ] Formulaires avec `<label>` associé à chaque input
- [ ] Pas de `height: 100vh` sans fallback `100svh`
- [ ] `overscroll-behavior: contain` sur les overlays scrollables
- [ ] `-webkit-tap-highlight-color: transparent` appliqué
- [ ] `touch-action: manipulation` sur les boutons
- [ ] `font-display: swap` sur toutes les @font-face
- [ ] Polices web en WOFF2 uniquement
- [ ] Polices critiques preload dans le `<head>`
- [ ] Dark mode fonctionne (si implémenté)
- [ ] Animations respectent `prefers-reduced-motion`
- [ ] Print styles testés
- [ ] Lighthouse Accessibility ≥ 90
- [ ] LCP < 2.5s, CLS < 0.1, INP < 200ms

---

*Last updated: 2025-03 — Revoir si : nouvelles directives WCAG (3.0), changements des Core Web Vitals Google, ou nouveaux appareils avec safe areas non standards.*