# Refonte auth DA 2.0 + backend + notifications — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans or subagent-driven-development. Steps use checkbox (`- [x]`) syntax.

**Goal:** Migrer toute la suite auth (inscription, connexion, reset mdp) vers la DA 2.0 en layout split immersif, durcir le backend inscription/reset en place, corriger le bug serveur qui casse activation/reset, et refondre le système de notifications.

**Architecture:** Templates Twig étendant un nouveau layout auth sans chrome site ; CSS/JS DA 2.0 isolés (`ds-v2-auth.*`, `ds-v2-toast.*`) réutilisant les atomes existants (`.chk-field`, `.nhp-btn`, tokens `--v2-*`) ; controllers inchangés côté routes/entité, enrichis d'une validation serveur et de re-render inline.

**Tech Stack:** Symfony 7.2, Twig, PHP 8.x, CSS/JS vanilla (public/styles, public/scripts), serveur PHP intégré via router.

## Global Constraints

- Projet **non git** → aucun commit ; vérification par lint + navigateur.
- **Pas de bundles Symfony natifs** (verify-email/reset-password/form_login) — archi custom conservée.
- Routes, noms de route, entité `Client`, champs `action_token`/`dh_token`, auth session, honeypot/timing/rate-limit : **inchangés**.
- Signature `showNotification({success, title, message})` : **inchangée** (compat appels existants).
- Réutiliser les tokens/atomes DA 2.0 existants ; ne pas dupliquer `.chk-field`/`.nhp-btn`.
- Polices : Fraunces (display), Inter (sans), IBM Plex Mono (mono).
- Serveur de dev lancé avec un router fixant `$_SERVER['SCRIPT_FILENAME']` sur `public/index.php`.

---

### Task 1: Corriger le lancement du serveur de dev (bug TypeError)

**Files:**
- Create: `router.php` (racine projet — router de dev versionné, remplace le scratchpad)

**Interfaces:**
- Produces: serveur de dev fonctionnel où toutes les routes dynamiques répondent (plus de TypeError).

- [x] **Step 1: Créer `router.php` à la racine qui fixe SCRIPT_FILENAME**

```php
<?php
// Router du serveur PHP intégré : sert les assets statiques de /public,
// délègue le reste au front controller Symfony en fixant SCRIPT_FILENAME
// pour que Symfony Runtime résolve public/index.php (et non ce router).
$public = __DIR__ . '/public';
$path = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
if ($path !== '/' && is_file($public . $path)) {
    return false; // le serveur intégré sert le fichier statique
}
$_SERVER['SCRIPT_FILENAME'] = $public . '/index.php';
$_SERVER['SCRIPT_NAME'] = '/index.php';
require $public . '/index.php';
```

- [x] **Step 2: Lancer le serveur**

Run (background) : `php -S 127.0.0.1:8000 "D:/Documents/PROJETS/STAGE/Vulcan Affutage/vulcan-aff-v2/router.php"`

- [x] **Step 3: Vérifier qu'une route dynamique répond sans TypeError**

Run : `curl -s -o NUL -w "%{http_code}" http://127.0.0.1:8000/inscription`
Expected : `200` (et non 500 / TypeError).

---

### Task 2: Bugfixes backend B2 + B3

**Files:**
- Modify: `src/Controller/ClientRegisterController.php:355`
- Modify: `src/Controller/EditPasswordController.php:95`

**Interfaces:**
- Produces: lien de renvoi d'activation valide ; variable email non écrasée dans reset.

- [x] **Step 1: B2 — ajouter le paramètre `email` manquant**

Dans `sendNewActivationEmail`, remplacer :
```php
$confirmationLink = $this->generateUrl('app_confirm_email', ['token' => $actionToken], 0);
```
par :
```php
$confirmationLink = $this->generateUrl('app_confirm_email', ['token' => $actionToken, 'email' => $existingClient->getEmail()], 0);
```

- [x] **Step 2: B3 — ne plus écraser `$email` par l'objet Email**

Dans `EditPasswordController::index`, renommer la variable du message :
```php
$mail = (new Email())
    ->from('no-reply@vulcanaffutage.com')
    ->to($client->getEmail())
    ->subject('Modification de votre mot de passe')
    ->html($htmlContent);
$mailer->send($mail);
```
Le `return $this->render('edit_password/reussite.html.twig', ['email' => $email])` et le `catch`
retrouvent alors la string `$email`.

- [x] **Step 3: Vérifier la syntaxe PHP**

Run : `php -l src/Controller/ClientRegisterController.php && php -l src/Controller/EditPasswordController.php`
Expected : `No syntax errors detected` (x2).

---

### Task 3: Validation serveur inscription + reset

**Files:**
- Modify: `src/Controller/ClientRegisterController.php` (inscription POST + nouvelle méthode privée)
- Modify: `src/Controller/EditPasswordController.php` (editPassword : force mdp serveur)
- Modify: `templates/client_register/index.html.twig` (affichage erreurs + valeurs ressaisies) — finalisé en Task 6

**Interfaces:**
- Produces: `private function validateRegistration(array $data): array` → retourne `['errors' => string[], 'clean' => array]`.

- [x] **Step 1: Ajouter la méthode de validation dans ClientRegisterController**

```php
/** @return array{errors: array<string,string>, clean: array<string,string>} */
private function validateRegistration(Request $request): array
{
    $errors = [];
    $email = trim((string) $request->request->get('email'));
    $nom = trim((string) $request->request->get('nom'));
    $prenom = trim((string) $request->request->get('prenom'));
    $password = (string) $request->request->get('password');
    $confirm = (string) $request->request->get('confirm_password');

    if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 180) {
        $errors['email'] = 'Adresse e-mail invalide.';
    }
    if ($nom === '' || strlen($nom) > 100) {
        $errors['nom'] = 'Nom requis (100 caractères max).';
    }
    if ($prenom === '' || strlen($prenom) > 100) {
        $errors['prenom'] = 'Prénom requis (100 caractères max).';
    }
    $classes = (preg_match('/[a-z]/', $password) ? 1 : 0)
        + (preg_match('/[A-Z]/', $password) ? 1 : 0)
        + (preg_match('/[0-9]/', $password) ? 1 : 0)
        + (preg_match('/[^A-Za-z0-9]/', $password) ? 1 : 0);
    if (strlen($password) < 8 || $classes < 3) {
        $errors['password'] = 'Mot de passe : 8 caractères min. et au moins 3 types (minuscule, majuscule, chiffre, symbole).';
    }
    if ($password !== $confirm) {
        $errors['confirm_password'] = 'Les mots de passe ne correspondent pas.';
    }
    return ['errors' => $errors, 'clean' => ['email' => $email, 'nom' => $nom, 'prenom' => $prenom]];
}
```

- [x] **Step 2: Brancher la validation dans le POST inscription (après CSRF, avant création Client)**

Après le bloc CSRF valide et avant `$emailReceived = $request->request->get('email');`, insérer :
```php
$validation = $this->validateRegistration($request);
if (!empty($validation['errors'])) {
    $token = $csrfTokenManager->getToken('client_register_form');
    $session->set('register_form_loaded_at', time());
    return $this->render('client_register/index.html.twig', [
        'csrf_token' => $token->getValue(),
        'errors' => $validation['errors'],
        'old' => $validation['clean'],
    ]);
}
```
Puis utiliser `$validation['clean']` pour `$emailReceived/$nom/$prenom` (garder `$password` brut).

- [x] **Step 3: Rendre le GET compatible (errors/old vides)**

Le `render` du GET fournit déjà `csrf_token` ; ajouter `'errors' => [], 'old' => []` pour éviter les variables Twig indéfinies.

- [x] **Step 4: Durcir la force mdp côté reset (editPassword)**

Dans `EditPasswordController::editPassword`, après la vérif `$password !== $confirmPassword`, ajouter :
```php
$classes = (preg_match('/[a-z]/', $password) ? 1 : 0)
    + (preg_match('/[A-Z]/', $password) ? 1 : 0)
    + (preg_match('/[0-9]/', $password) ? 1 : 0)
    + (preg_match('/[^A-Za-z0-9]/', $password) ? 1 : 0);
if (strlen($password) < 8 || $classes < 3) {
    return $this->render('erreur/global-error.html.twig', [
        'errorTitle' => 'Mot de passe trop faible',
        'errorMessage' => '8 caractères min. et au moins 3 types (minuscule, majuscule, chiffre, symbole).',
        'errorButton1' => ['label' => 'Refaire la demande', 'link' => $this->generateUrl('app_request_edit_password', ['email' => $client->getEmail()])],
    ]);
}
```

- [x] **Step 5: Vérifier la syntaxe PHP**

Run : `php -l src/Controller/ClientRegisterController.php && php -l src/Controller/EditPasswordController.php`
Expected : `No syntax errors detected` (x2).

- [x] **Step 6: Vérifier la validation via POST sans JS**

Run : `curl -s -X POST http://127.0.0.1:8000/inscription -d "email=bad&nom=&prenom=x&password=abc&confirm_password=zzz" | findstr /C:"invalide" /C:"correspondent"`
Expected : au moins un message d'erreur présent (après finalisation Task 6 pour l'affichage). Sinon vérifier via retour HTTP 200 + re-render du formulaire.

---

### Task 4: Rework notifications — système global unifié

**Files:**
- Create: `public/styles/ds-v2-toast.css`
- Create: `public/scripts/ds-v2-toast.js`
- Modify: `templates/base.html.twig` (charger les 2 assets ; remplacer `#notification-container`)
- Modify: `public/scripts/script.js` (supprimer l'ancienne `showNotification`)
- Modify: `public/scripts/ds-v2-app.js:149-186` (supprimer la redéfinition locale)
- Modify: `public/styles/ds-v2-pages.css:468-495` (retirer le bloc `.v2-toast`)

**Interfaces:**
- Produces: `window.showNotification({success:true|false|'attention', title?, message?})` global, rendu `.v2-toast` dans `.v2-toast-region` (offset haut).

- [x] **Step 1: Créer `ds-v2-toast.css`** (région positionnée + toast forge retravaillé)

```css
/* ============ Notifications DA 2.0 (global) ============ */
.v2-toast-region {
    position: fixed; top: clamp(84px, 9vh, 116px); right: clamp(14px, 3vw, 28px);
    z-index: 4000; display: flex; flex-direction: column; gap: 12px;
    width: min(380px, calc(100vw - 28px)); pointer-events: none;
}
.v2-toast {
    pointer-events: auto; position: relative; display: flex; gap: 13px; align-items: flex-start;
    padding: 15px 17px 15px 19px; border-radius: 14px;
    background: linear-gradient(180deg, #1c1f21 0%, #14161750 100%), #17191a;
    color: #edede9; border: 1px solid rgba(237,235,229,0.10);
    box-shadow: 0 18px 40px -18px rgba(0,0,0,0.65), 0 2px 8px rgba(0,0,0,0.35);
    overflow: hidden; animation: v2ToastIn 0.4s cubic-bezier(.2,.8,.2,1) both;
}
.v2-toast::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--v2-green, #2d9446); }
.v2-toast.is-out { animation: v2ToastOut 0.28s ease forwards; }
.v2-toast-ico { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border-radius: 8px; background: rgba(45,148,70,0.14); color: var(--v2-green, #2d9446); margin-top: 1px; }
.v2-toast-ico svg { width: 16px; height: 16px; display: block; }
.v2-toast-body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.v2-toast-kind { font-family: var(--v2-font-mono, 'IBM Plex Mono', monospace); font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--v2-green, #2d9446); }
.v2-toast-title { font-family: var(--v2-font-sans, 'Inter', sans-serif); font-weight: 600; font-size: 0.95rem; line-height: 1.25; color: #fff; }
.v2-toast-msg { font-size: 0.85rem; color: rgba(237,235,229,0.72); line-height: 1.45; }
.v2-toast--warn::before, .v2-toast--warn .v2-toast-kind { background: var(--v2-ember, #d67b2e); color: var(--v2-ember, #d67b2e); }
.v2-toast--warn .v2-toast-ico { background: rgba(214,123,46,0.16); color: var(--v2-ember, #d67b2e); }
.v2-toast--warn .v2-toast-kind { background: none; }
.v2-toast--error::before { background: #d9534f; }
.v2-toast--error .v2-toast-kind { color: #e8776f; background: none; }
.v2-toast--error .v2-toast-ico { background: rgba(217,83,79,0.16); color: #e8776f; }
@keyframes v2ToastIn { from { opacity: 0; transform: translateX(24px) scale(0.98); } to { opacity: 1; transform: translateX(0) scale(1); } }
@keyframes v2ToastOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(24px); } }
@media (prefers-reduced-motion: reduce) { .v2-toast, .v2-toast.is-out { animation: none; } }
```

- [x] **Step 2: Créer `ds-v2-toast.js`** (implémentation unique)

```javascript
/* Notifications DA 2.0 — showNotification global unique. */
(function () {
    var ICONS = {
        success: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
        warn: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg>',
        error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>'
    };
    var LABELS = { success: 'Succès', warn: 'Attention', error: 'Erreur' };
    function region() {
        var r = document.querySelector('.v2-toast-region');
        if (!r) { r = document.createElement('div'); r.className = 'v2-toast-region'; document.body.appendChild(r); }
        return r;
    }
    window.showNotification = function (n) {
        n = n || {};
        var kind = n.success === true ? 'success' : (n.success === 'attention' ? 'warn' : 'error');
        var el = document.createElement('article');
        el.className = 'v2-toast v2-toast--' + kind;
        el.setAttribute('role', kind === 'error' ? 'alert' : 'status');
        el.innerHTML =
            '<span class="v2-toast-ico" aria-hidden="true">' + ICONS[kind] + '</span>' +
            '<div class="v2-toast-body">' +
                '<span class="v2-toast-kind">' + LABELS[kind] + '</span>' +
                '<p class="v2-toast-title">' + (n.title || LABELS[kind]) + '</p>' +
                (n.message ? '<p class="v2-toast-msg">' + n.message + '</p>' : '') +
            '</div>';
        region().appendChild(el);
        setTimeout(function () {
            el.classList.add('is-out');
            el.addEventListener('animationend', function () { el.remove(); });
        }, 5000);
    };
})();
```

- [x] **Step 3: Charger les assets globalement dans `base.html.twig`**

Dans `<head>` (après les autres styles) : `<link rel="stylesheet" href="{{ asset('styles/ds-v2-toast.css') }}">`
Avant `</body>` (ou près des autres scripts) : `<script src="{{ asset('scripts/ds-v2-toast.js') }}"></script>`

- [x] **Step 4: Remplacer le wrapper notification dans `base.html.twig:126-130`**

```twig
{% block notification %}
    <div class="v2-toast-region" aria-live="polite" aria-atomic="false"></div>
{% endblock %}
```

- [x] **Step 5: Supprimer l'ancienne `showNotification` de `script.js:300`→fin de fonction**

Retirer entièrement la fonction `function showNotification(jsonNotification) { ... }` (elle est
remplacée par le global de `ds-v2-toast.js`, chargé et utilisé par tous les appels existants).

- [x] **Step 6: Supprimer la redéfinition locale dans `ds-v2-app.js:149-186`**

Retirer le bloc `window.showNotification = function (n) {...}` + les constantes `TOAST_ICONS`/`TOAST_TITLES` associées (désormais dans le global).

- [x] **Step 7: Retirer `.v2-toast` de `ds-v2-pages.css:468-495`**

Supprimer le bloc `.v2-toast*` + keyframes `v2ToastIn/Out` de `ds-v2-pages.css` (déplacés dans `ds-v2-toast.css`).

- [x] **Step 8: Vérifier au navigateur**

Sur une page (ex. produits new*), déclencher `showNotification({success:false,title:'Test',message:'msg'})` via console.
Expected : toast sombre thémé, décalé du haut, à droite, disparaît après ~5 s. 0 erreur console.

---

### Task 5: Layout auth + CSS/JS auth

**Files:**
- Create: `templates/_auth/base_auth.html.twig`
- Create: `public/styles/ds-v2-auth.css`
- Create: `public/scripts/ds-v2-auth.js`

**Interfaces:**
- Produces: layout `_auth/base_auth.html.twig` avec blocks `auth_aside`, `auth_panel`, `title`, `metaDesc`, `seoIndex`, `auth_scripts` ; classes `.auth-shell/.auth-aside/.auth-panel/.chk-field/.auth-strength/.auth-alt-link/.auth-result`.

- [x] **Step 1: Créer `base_auth.html.twig`** (head + shell split, sans chrome site)

```twig
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Vulcan' Affûtage{% endblock %}</title>
    <meta name="description" content="{% block metaDesc %}{% endblock %}">
    {% block seoIndex %}<meta name="robots" content="noindex, follow">{% endblock %}
    <link rel="icon" href="/assets/logo-50p.jpg">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400..700;1,9..144,400..600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="{{ asset('styles/ds-v2.css') }}">
    <link rel="stylesheet" href="{{ asset('styles/ds-v2-pages.css') }}">
    <link rel="stylesheet" href="{{ asset('styles/ds-v2-auth.css') }}">
    <link rel="stylesheet" href="{{ asset('styles/ds-v2-toast.css') }}">
</head>
<body class="auth-body">
    <div class="v2-toast-region" aria-live="polite"></div>
    <main class="auth-shell">
        <aside class="auth-aside">
            <a href="{{ path('app_accueil') }}" class="auth-brand" aria-label="Retour à l'accueil">
                <img src="/assets/logo-sf2.webp" alt="Vulcan' Affûtage">
            </a>
            <div class="auth-aside-copy">
                {% block auth_aside %}{% endblock %}
            </div>
        </aside>
        <section class="auth-panel">
            <div class="auth-panel-inner">
                {% block auth_panel %}{% endblock %}
            </div>
        </section>
    </main>
    <script src="{{ asset('scripts/ds-v2-toast.js') }}"></script>
    <script src="{{ asset('scripts/ds-v2-auth.js') }}"></script>
    {% block auth_scripts %}{% endblock %}
</body>
</html>
```

- [x] **Step 2: Créer `ds-v2-auth.css`** (shell split + panneau + jauge). Squelette à ajuster visuellement :

```css
.auth-body { margin: 0; background: var(--v2-forge, #17191a); font-family: var(--v2-font-sans, 'Inter', sans-serif); color: var(--v2-ink, #1a1c1d); }
.auth-shell { display: grid; grid-template-columns: 1.02fr 1fr; min-height: 100vh; }
.auth-aside { position: relative; display: flex; flex-direction: column; justify-content: space-between; padding: clamp(28px, 4vw, 56px); color: #edede9; background: var(--v2-forge, #17191a); overflow: hidden; }
.auth-aside::before { content: ""; position: absolute; inset: 0; background: url('/assets/logo-sf2.webp') center/cover no-repeat; opacity: 0.18; }
.auth-aside::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(23,25,26,0.4), rgba(23,25,26,0.85)); }
.auth-aside > * { position: relative; z-index: 1; }
.auth-brand img { width: clamp(64px, 8vw, 96px); border-radius: 10px; }
.auth-aside-copy h2 { font-family: var(--v2-font-display, 'Fraunces', serif); font-size: clamp(1.8rem, 3.2vw, 2.8rem); line-height: 1.05; font-weight: 600; margin: 0 0 14px; }
.auth-aside-copy p { color: rgba(237,235,229,0.74); font-size: 1rem; line-height: 1.6; max-width: 42ch; }
.auth-aside-eyebrow { font-family: var(--v2-font-mono, 'IBM Plex Mono', monospace); font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--v2-green-glow, #4bbf68); }

.auth-panel { display: flex; align-items: center; justify-content: center; background: var(--v2-paper, #edede9); padding: clamp(28px, 5vw, 64px) clamp(20px, 4vw, 56px); }
.auth-panel-inner { width: 100%; max-width: 420px; }
.auth-panel-inner h1 { font-family: var(--v2-font-display, 'Fraunces', serif); font-size: clamp(1.7rem, 3vw, 2.3rem); font-weight: 600; color: var(--v2-ink, #1a1c1d); margin: 0 0 6px; }
.auth-alt-link { display: inline-block; margin-bottom: 26px; font-size: 0.9rem; color: var(--v2-green-deep, #1f6f34); text-decoration: underline; text-underline-offset: 3px; }
.auth-form { display: flex; flex-direction: column; gap: 14px; }
.auth-strength { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-top: -4px; }
.auth-strength span { height: 4px; border-radius: 3px; background: var(--v2-steel-200, #d5d8d3); transition: background 0.25s ease; }
.auth-strength span.on-1 { background: #d9534f; } .auth-strength span.on-2 { background: var(--v2-ember, #d67b2e); }
.auth-strength span.on-3 { background: #d3b13a; } .auth-strength span.on-4 { background: var(--v2-green, #2d9446); }
.auth-field-error { font-size: 0.78rem; color: #d9534f; margin-top: -8px; }
.auth-form .nhp-btn { width: 100%; justify-content: center; margin-top: 6px; }
.auth-result { text-align: center; }
.auth-result-ico { width: 60px; height: 60px; margin: 0 auto 18px; display: grid; place-items: center; border-radius: 50%; background: rgba(45,148,70,0.12); color: var(--v2-green, #2d9446); }
.auth-result-ico svg { width: 30px; height: 30px; }

@media (max-width: 900px) {
    .auth-shell { grid-template-columns: 1fr; }
    .auth-aside { min-height: 34vh; }
}
```

- [x] **Step 3: Créer `ds-v2-auth.js`** (force mdp + match + garde submit, via toast)

```javascript
/* Auth DA 2.0 — force mdp, correspondance, garde-fous submit. */
document.addEventListener('DOMContentLoaded', function () {
    var form = document.querySelector('[data-auth-form]');
    if (!form) return;
    var pwd = form.querySelector('#password');
    var confirm = form.querySelector('#confirm_password');
    var bars = form.querySelectorAll('.auth-strength span');

    function score(v) {
        var s = 0;
        if (v.length >= 8) s++;
        if (/[a-z]/.test(v)) s++;
        if (/[A-Z]/.test(v)) s++;
        if (/[0-9]/.test(v)) s++;
        if (/[^A-Za-z0-9]/.test(v)) s++;
        return Math.min(4, s); // 0..4
    }
    function paint(n) {
        bars.forEach(function (b, i) {
            b.className = ''; if (i < n) b.classList.add('on-' + n);
        });
    }
    if (pwd && bars.length) pwd.addEventListener('input', function () { paint(score(pwd.value)); });

    form.addEventListener('submit', function (e) {
        if (pwd && score(pwd.value) < 3) {
            e.preventDefault();
            window.showNotification({ success: 'attention', title: 'Mot de passe trop faible', message: "8 caractères min. et au moins 3 types." });
            return;
        }
        if (pwd && confirm && pwd.value !== confirm.value) {
            e.preventDefault();
            window.showNotification({ success: false, title: 'Attention', message: 'Les mots de passe ne correspondent pas.' });
        }
    });
});
```

- [x] **Step 4: Vérifier le lint Twig du layout**

Run : `php bin/console lint:twig templates/_auth/base_auth.html.twig`
Expected : `OK`.

---

### Task 6: Migration des templates auth vers DA 2.0

**Files:**
- Rewrite: `templates/client_register/index.html.twig` (inscription — formulaire + erreurs Task 3)
- Rewrite: `templates/client-login/index.html.twig` (connexion)
- Rewrite: `templates/edit_password/request_email.html.twig` (saisie email)
- Rewrite: `templates/edit_password/index.html.twig` (nouveau mdp)
- Rewrite: `templates/client_register/reussite.html.twig` (email envoyé)
- Rewrite: `templates/client_register/activation_reussi.html.twig`
- Rewrite: `templates/edit_password/reussite.html.twig`
- Rewrite: `templates/edit_password/password_edited.html.twig`
- Review: `templates/erreur/global-error.html.twig` (compat DA 2.0 — chrome site existant conservé si déjà correct)

**Interfaces:**
- Consumes: `_auth/base_auth.html.twig` (blocks `auth_aside`/`auth_panel`), `.chk-field`, `.auth-strength`, `errors`/`old` (Task 3), routes existantes.

- [x] **Step 1: Inscription** — étend `_auth/base_auth.html.twig`. Champs `.chk-field` (email, nom, prénom, password, confirm_password), jauge `.auth-strength`, honeypot conservé, `_csrf_token`, affichage `errors`/`old`. Modèle de champ :

```twig
<div class="chk-field">
    <input id="email" name="email" type="email" placeholder=" " value="{{ old.email|default('') }}" required>
    <label for="email">E-mail</label>
</div>
{% if errors.email is defined %}<p class="auth-field-error">{{ errors.email }}</p>{% endif %}
```
Le `<form>` porte `data-auth-form action="{{ path('app_inscription') }}" method="post"`. Aside : eyebrow + titre « Rejoignez l'atelier » + accroche. Lien alt vers `app_client_login`.

- [x] **Step 2: Connexion** — même layout ; champs email + password ; `data-auth-form` (sans jauge) ; action = route de login existante (`app_client_login` POST) ; lien alt vers `app_inscription` + lien « Mot de passe oublié ? » vers `app_request_email_edit_password`. Conserver le CSRF/mécanisme de login actuel (lire `templates/client-login/index.html.twig` avant réécriture pour préserver noms de champs & hidden).

- [x] **Step 3: Reset étape 1** (`request_email`) — un champ email, action `app_request_edit_password`, aside « Mot de passe oublié ».

- [x] **Step 4: Reset étape 2** (`edit_password/index`) — champs password + confirm_password + jauge, hidden `token`/`email`, `data-auth-form`, action `app_edit_password`.

- [x] **Step 5: Pages résultat** — `.auth-result` (icône + titre + message + bouton `.nhp-btn`). reussite inscription (« E-mail envoyé à {{ email }} »), activation_reussi (« Compte activé » → login), edit_password/reussite, password_edited (→ login).

- [x] **Step 6: Lint Twig global**

Run : `php bin/console lint:twig templates/client_register templates/client-login templates/edit_password templates/_auth`
Expected : `OK` sur tous les fichiers.

---

### Task 7: Emails DA 2.0

**Files:**
- Rewrite: `templates/emails/confirmation_inscription.html.twig`
- Rewrite: `templates/emails/activation_compte.html.twig`
- Rewrite: `templates/emails/reset_password.html.twig`

**Interfaces:**
- Consumes: variables existantes (`confirmationLink`, `nom`, `prenom`, `connectLink`).

- [x] **Step 1: Gabarit e-mail commun** — HTML tables + styles inline (compat clients mail), en-tête forge sombre (#17191a) + logo, corps clair, bouton CTA vert (#2d9446), pied de page. Conserver les variables Twig. Largeur 600px centrée.

- [x] **Step 2: Décliner les 3 emails** — confirmation inscription (CTA `confirmationLink` « Activer mon compte »), activation compte (CTA `connectLink` « Se connecter »), reset password (CTA `confirmationLink` « Choisir un nouveau mot de passe » + mention expiration 20 min).

- [x] **Step 3: Lint Twig**

Run : `php bin/console lint:twig templates/emails`
Expected : `OK`.

---

### Task 8: Vérification bout-en-bout (navigateur + DB)

**Files:** aucun (vérification).

- [x] **Step 1: Parcours inscription** — ouvrir `/inscription`, remplir, soumettre ; vérifier page réussite ; récupérer `action_token` du client en base :

Run : `php bin/console dbal:run-sql "SELECT email, action_token, status FROM client ORDER BY id DESC LIMIT 1"`

- [x] **Step 2: Activation** — ouvrir `http://127.0.0.1:8000/confirm-email/{token}/{email}` ; vérifier page activation réussie + `status=ACTIVE` en base. **Plus de TypeError.**

- [x] **Step 3: Reset** — `/modification-mdp` → soumettre email → récupérer nouveau token → `/entrer-nouveau-mdp/{email}/{token}` → nouveau mdp valide → page mdp modifié.

- [x] **Step 4: Validation serveur** — POST inscription avec email invalide / mdp faible / confirm ≠ (JS désactivé) → erreurs inline affichées, pas de compte créé.

- [x] **Step 5: Notifications** — déclencher succès/attention/erreur ; vérifier style forge, offset haut, empilement, auto-dismiss.

- [x] **Step 6: Contrôle visuel responsive** — 1440px + 390px sur inscription/login/reset ; 0 débordement, aside/panel corrects ; 0 erreur console.

---

## Self-review

- **Couverture spec :** B1 (T1), B2/B3 (T2), validation serveur (T3), notifications rework+offset+unification (T4), layout split + assets (T5), migration 8 templates + erreur (T6), emails (T7), vérif flux+TypeError+validation+notifs (T8). ✓
- **Placeholders :** aucun TBD ; code fourni pour router, validation, toast css/js, layout, auth css/js. Migrations templates = pattern `.chk-field` explicite + fichiers listés. ✓
- **Cohérence types :** `showNotification({success,title,message})` constant T4/T5 ; `validateRegistration(Request)→{errors,clean}` constant T3/T6 ; blocks `auth_aside/auth_panel` constants T5/T6. ✓
- **Risque connu :** login (T6.2) — lire le template actuel avant réécriture pour préserver champs/CSRF/mécanisme d'auth. Note portée dans la tâche.
