diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..624824f
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,22 @@
+# Le Dockerfile fait COPY . /var/www/html : tout ce qui est listé ici reste
+# hors de l'image, donc hors de la racine web et hors des couches publiées.
+.env
+.env.*
+!.env.example
+.git
+.gitignore
+.dockerignore
+
+# Historique et documentation de version : inutiles dans l'image.
+README*.md
+CLAUDE.md
+
+# Journaux, sauvegardes et artefacts locaux
+*.log
+*.sql.gz
+*.dump
+logs/
+tmp/
+.idea/
+.vscode/
+.DS_Store
diff --git a/.env.example b/.env.example
index 0f3a3a4..a6789ca 100644
--- a/.env.example
+++ b/.env.example
@@ -4,12 +4,17 @@ APP_PORT=8080
TZ=Europe/Paris
# MariaDB
+# Remplacer impérativement les deux mots de passe avant tout déploiement.
DB_NAME=pta
DB_USER=pta
-DB_PASSWORD=pta
-DB_ROOT_PASSWORD=pta
+DB_PASSWORD=a_remplacer
+DB_ROOT_PASSWORD=a_remplacer_aussi
+
+# Débogage : 1 renvoie le détail des exceptions au navigateur. Laisser à 0.
+PTA_DEBUG=0
# phpMyAdmin
# 127.0.0.1 = uniquement accessible depuis le serveur lui-même.
+# phpMyAdmin demande désormais un identifiant MariaDB à chaque connexion.
PMA_BIND=127.0.0.1
PMA_PORT=8081
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..00636d6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,28 @@
+# Configuration locale : contient les mots de passe MariaDB.
+# Seul .env.example doit être versionné.
+.env
+.env.*
+!.env.example
+
+# Dumps et sauvegardes de base
+*.sql.gz
+*.dump
+database/backup/
+database/*_backup*.sql
+
+# Journaux et fichiers temporaires
+*.log
+logs/
+tmp/
+
+# Dépendances éventuelles
+/vendor/
+/node_modules/
+
+# Artefacts d'éditeur et de système
+.DS_Store
+Thumbs.db
+.idea/
+.vscode/
+*.swp
+*~
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..ca893a5
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,188 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Contexte
+
+PTA (Planning de Travail Annualisé) — application PHP 8.3 multipage (MVC maison) pour la
+planification annualisée des agents d'accueils de loisirs périscolaires/extrascolaires.
+
+**Aucune dépendance** : pas de Composer, pas de npm, pas de framework, pas de bundler, pas de
+suite de tests. Uniquement PHP + PDO/MySQL + JavaScript vanilla. Ne pas introduire de dépendance
+sans raison forte : le PDF (`app/Services/SimplePdf.php`) et l'autoloader (`app/autoload.php`) sont
+volontairement réécrits à la main.
+
+**Tout est en français** : identifiants SQL (`creneau_horaire`, `id_agent`, `statut`), messages
+utilisateur, commentaires. Conserver cette langue pour tout nouveau code.
+
+## Commandes
+
+```bash
+# Démarrage complet (nginx :8080, phpMyAdmin :8081, MariaDB, php-fpm)
+cp .env.example .env # puis renseigner DB_PASSWORD / DB_ROOT_PASSWORD
+docker compose up -d --build
+
+# IMPORTANT : le code source est COPIÉ dans l'image au build (Dockerfile), il n'est pas monté.
+# Toute modification PHP/JS/CSS exige un rebuild des DEUX services :
+# (nginx sert lui-même les assets statiques depuis sa propre copie de /var/www/html)
+docker compose up -d --build php nginx
+
+docker compose logs -f php nginx db
+docker compose ps
+docker compose down # arrêt en conservant les données
+docker compose down -v # DÉTRUIT le volume MariaDB
+
+# Accès SQL direct
+docker compose exec db mariadb -upta -p pta
+
+# Vérification syntaxique (il n'existe ni linter ni tests automatisés)
+find . -name '*.php' -not -path './.git/*' -exec php -l {} \;
+```
+
+Le schéma `database/pta_prod_schema_mariadb.sql` n'est joué **qu'à la création initiale du volume**
+MariaDB (`docker-entrypoint-initdb.d`). Sur une base existante, toute évolution de schéma doit être
+appliquée manuellement en SQL — il n'y a pas d'outil de migration.
+
+## Architecture
+
+### Deux types de points d'entrée, tous à la racine ou dans `api/`
+
+Il n'y a pas de routeur. Chaque URL correspond à un fichier PHP réel de 5 à 10 lignes.
+
+**Pages HTML** — `planning.php`, `agents.php`, `annual.php`, `pta.php`, `teams.php`, `coverage.php`,
+`structures.php`, `pending.php`, `vacations.php`, `administration.php` :
+
+```php
+require __DIR__ . '/page_bootstrap.php';
+runPage('planning'); // -> PageController::planning()
+```
+
+`page_bootstrap.php` attrape les `Throwable` et affiche une page d'erreur HTML.
+
+**Endpoints JSON/PDF** — `api/*.php` plus `save_planning.php`, `validate_planning.php`,
+`reopen_planning.php` (ces trois-là sont restés à la racine pour raisons historiques) :
+
+```php
+JsonResponse::bootstrap(); // handlers d'erreur -> JSON, jamais du HTML
+$pdo = require dirname(__DIR__) . '/app/bootstrap.php';
+(new PlanningController($pdo))->show();
+```
+
+`JsonResponse::bootstrap()` convertit warnings et exceptions en réponses JSON 500 — indispensable,
+car le JS refuse toute réponse non-JSON. **Les endpoints PDF ne l'appellent pas** (ils émettent des
+octets binaires via `PdfResponse::inline()`).
+
+Pour ajouter un endpoint : créer le fichier mince dans `api/`, ajouter la méthode au contrôleur.
+Pour ajouter une page : fichier racine + méthode dans `PageController` + vue `app/Views/pages/` +
+partiel `app/Views/partials/` + module JS + **inscription de la clé dans `Access::allowedPages()`
+et dans le tableau `$navigation` de `app/Views/layouts/header.php`**.
+
+### Chaîne d'un appel
+
+```
+api/x.php → Controller (Access + Csrf + Request) → Model (SQL PDO) → JsonResponse::send()
+page.php → PageController → Models → Views/pages/x.php
+ ├── layouts/header.php (nav filtrée par rôle)
+ ├── partials/x.php (markup)
+ └── layouts/footer.php (injecte window.PTA_CONFIG + scripts)
+```
+
+`app/bootstrap.php` démarre la session, ouvre le PDO (`db.php` ← `config.php` ← variables
+d'environnement `PTA_DB_*`) et appelle `SchemaModel::assertCurrent()` **à chaque requête**. Ajouter
+une table ou une colonne obligatoire impose de mettre à jour les listes de `SchemaModel`, sinon
+l'application entière refuse de démarrer.
+
+### Rôles et périmètres (`app/Core/Access.php`)
+
+Trois rôles stockés en session : `AGENT` (lecture seule sur soi), `RESPONSABLE_STRUCTURE` (édition
+des brouillons de son seul lieu), `SERVICE_ENFANCE` (tout, seul habilité à valider).
+
+`role.php` est un **sélecteur de rôle, pas une authentification** — l'utilisateur choisit librement
+son identité. C'est la faille ouverte la plus importante du projet ; toute évolution vers la
+production doit remplacer cette sélection par un compte authentifié, sans toucher au reste de la
+mécanique de droits.
+
+**`Access.php` ne contient aucune règle de droit** : tout est lu dans les vues SQL créées par
+`database/migration_v7_8_securite_acces.sql` (voir `README_V7_8.md`). Modifier un droit se fait en
+SQL dans `acces_role_page` / `acces_role_permission`, sans redéploiement. Une permission absente de
+la base vaut « refusé ». `SchemaModel` vérifie la présence des sept vues au démarrage et fait
+échouer l'application si la migration manque — c'est volontairement fail-closed.
+
+Les contrôles sont doublés : l'UI masque, mais le serveur tranche. Toute méthode de contrôleur
+commence par un garde `requireService()`, `requireDraftEditor()`, `requireStructureEdit($id)`,
+`requireAgentView($id)`, `requireEntryEdit($id)`, `requirePlanningEdit($id)`… Ces gardes émettent
+directement un 401/403 JSON et coupent l'exécution. **Ne jamais ajouter un endpoint mutant sans
+garde ni sans `Csrf::assertPayload($payload)`.**
+
+CSRF : le jeton voyage **dans le corps JSON** (`csrf_token`), pas dans un en-tête. Côté JS :
+`csrf_token: window.PTA_CONFIG.csrfToken`.
+
+### Contraintes de sécurité à respecter
+
+- **CSP à nonce** (`app/Core/Security.php`) : `script-src 'self' 'nonce-…'`, sans `unsafe-inline`.
+ Tout `
-
+
-
+
-
+