diff --git a/STATE.md b/STATE.md
index 495d625..de93fbc 100644
--- a/STATE.md
+++ b/STATE.md
@@ -1,6 +1,6 @@
# STATE — Simpl'Résultat
-> Derniere MAJ : 2026-07-18 (**v0.14.0 shippée** — milestone `planned-2026-07-15-collapse-multi-niveaux` 4/4 : repli des catégories à **chaque niveau** sur les 3 rapports hiérarchiques + grille Budget + unification des 2 arbres de catégories, persistance par-profil via `user_preferences` (ADR 0016). Pile de 4 PRs #292-#295 → `/pr-review` **APPROVE ×4**, ff-merge (`main` `5a3d87b`), tag `v0.14.0` (`9c18e10`) → CI release #322. Auparavant cette session : **v0.13.0 taggée** (2026-07-13) — le backlog `[Unreleased]` accumulé depuis v0.12.0 (filtres de comptes #272-#279, tendances hiérarchiques #262-#265, collapse niveau-1 #254). **828 vitest** + 98 Rust. Aucune migration DB (v1→v16). #259 mergée (PR #296, `/pr-review` APPROVE) ; epic #260 « rapports uniformes » fermée — 4/5 rapports conformes, 3 déviations documentées entérinées)
+> Derniere MAJ : 2026-07-19 (**v0.14.0 shippée** — milestone `planned-2026-07-15-collapse-multi-niveaux` 4/4 : repli des catégories à **chaque niveau** sur les 3 rapports hiérarchiques + grille Budget + unification des 2 arbres de catégories, persistance par-profil via `user_preferences` (ADR 0016). Pile de 4 PRs #292-#295 → `/pr-review` **APPROVE ×4**, ff-merge (`main` `5a3d87b`), tag `v0.14.0` (`9c18e10`) → CI release #322. Auparavant cette session : **v0.13.0 taggée** (2026-07-13) — le backlog `[Unreleased]` accumulé depuis v0.12.0 (filtres de comptes #272-#279, tendances hiérarchiques #262-#265, collapse niveau-1 #254). **828 vitest** + 98 Rust. Aucune migration DB (v1→v16). #259 mergée (PR #296, `/pr-review` APPROVE) ; epic #260 « rapports uniformes » fermée — 4/5 rapports conformes, 3 déviations documentées entérinées)
## Position actuelle
@@ -16,6 +16,7 @@ Audit critique de la page Bilan livré (`docs/audit-bilan-2026-05.md`, revue CPA
## Decisions recentes
+- 2026-07-19 : **Chantier gating par tier planifié via `/spec` → milestone `spec-feature-gating` (#297-#302)**. Origine : réflexion monétisation de Max (« changer le périmètre des abonnements »). Matrice tranchée : **Free** = Dashboard/Import/Transactions/Catégories/**rapport Tendance**/Export chiffré/Changelog (mono-profil) ; **Base** = + les 4 autres rapports/Budget/multi-profils/auto-update ; **Premium** = + Bilan complet (patrimoine + cours). Admin (Max) = licence Premium auto-émise (pas de 4e édition). Décisions clés : upsell **verrouillé** (pas masqué) ; durcissement **sec non destructif** (blocage d'accès, données conservées, récupérables par upgrade) ; entitlements **statique + override `features[]`** ; **enforcement UI-only** (soft-paywall GPL assumé, seul le gate cours reste dur/server-enforced) ; **#271 absorbé** (auto-update Base+, fermé superseded). Re-ancrage Phase 3b : socle déjà là (3 éditions Ed25519, `current_edition`, `check_entitlement`) mais `is_feature_allowed` ignore `features[]` + `useLicense` per-appel → **LicenseProvider** requis pour un `useEntitlement` sync ; rapports = routes distinctes (gate par route) ; #271 = 1 ligne. 6 issues (socle→garde UI→routes/sidebar→multi-profils→#271 Rust→docs), specs force-add (gitignorées, précédent #295), aucune migration DB. Point ouvert : `/adjustments` non classé (défaut Free). (ref spec-feature-gating, #297-#302)
- 2026-07-18 : **Epic #260 « rapports uniformes » fermée + #259 livrée en PR #296 (review)**. #260 fermée après `/analyze` : **4/5 rapports pleinement conformes** (income-statement + filtres partagés + collapse par-profil) ; les 3 écarts restants vs le texte de l'epic sont des **décisions de conception assumées**, entérinés par Max — (1) lignes vides budget non masquées (spec décision 6 : grille = surface d'édition) ; (2) collapse budget replié « comme partout » (#289/ADR 0016 inverse le « sauf budget » de l'epic) ; (3) dashboard convergé sur le modèle Cartes plutôt qu'une table income-statement hiérarchique (décision 5). **Correction STATE** : les entrées 07-08/07-11 « reste de #260 = #259 » sont fausses — #259 est une migration de taxonomie sans lien avec l'epic (recadrée 07-12), et les vraies déviations #260 n'y étaient pas tracées. **#259** (fusion des catégories custom) livrée en PR #296 (`issue-259-merge-custom-categories`) : bloc préservé de `StepSimulate` rendu en `MappingRow`, reducer `RESOLVE_ROW` résout rows+preserved (`unresolved` compté sur seed only → ne bloque pas « Suivant »), writer via helper `isResolvedTarget` (fourre-tout créé seulement s'il reste une custom non fusionnée ; customs fusionnées désactivées au lieu d'être re-parentées). Plan-check pass (6 MINOR, 3 intégrés), **836 vitest** + build tsc/vite propres, régression parent/enfant custom couverte, aucune migration DB. `/pr-review` **APPROVE**, mergée (rebase) le 2026-07-19 → `main` `2314a64`, #259 fermée, branche supprimée. (ref #260, #259 PR #296)
- 2026-07-18 : **Milestone collapse multi-niveaux (#288-291) livrée → v0.14.0**. Cycle complet en une session : `/plan-run` (spec 2 fichiers, 8 décisions drainées) → `/review-spec` (3 experts, **verdict 🔴**) → refonte v2 → `/autopilot` (4 workers) → `/pr-review` ×4 → ff-merge → release. **Le point clé** : la revue a tué l'algo v1. Il suivait un curseur de profondeur supposant un **ordre DFS** ; or la grille Budget (`useBudget.ts:361`) trie par **niveau** — les 3 experts l'ont trouvé indépendamment. Refonte v2 : **visibilité par remontée de `parent_id`** (une ligne visible ssi tous ses ancêtres dépliés), order-independant → résout d'un coup l'ordre budget, le tri par type qui sépare parent/enfant, la feuille « (direct) » qui partage la clé du parent, et la contrainte `visible()`-avant-`reorderRows`. **Pile linéaire forcée** (B/C/D dépendent tous du nouveau hook de A → pas de wave parallèle) : PRs #292-#295, chacune basée sur la précédente. `/pr-review` **APPROVE ×3 + 1 REQUEST_CHANGES** (#295 : ligne `- Spec:` de l'ADR 0016 pointant des specs gitignorées → 404 ; corrigé par **force-add des specs à la racine**, précédent repo `spec-refonte-rapports.md` ; au passage la revue s'est trompée en disant que 0015 n'avait pas de ligne Spec — elle en a une, cassée pareil, bug pré-existant signalé). **ff-merge** de la pile (4 issues auto-fermées via `Resolves #N`, milestone 4/4, 4 branches supprimées) → **v0.14.0** taggée. **Persistance migrée `localStorage` → `user_preferences`** (base du profil) : `deleteProfile` ne purge aucun `localStorage` → une clé par-profil y serait un résidu survivant à la suppression, révélant les catégories explorées d'un profil PIN-protégé (exigence privacy-first, **ADR 0016**, frontière tracée : état UI par-profil → DB profil, état UI machine → localStorage). Le hook gagne `defaultExpanded` + `storageKey` nullable, qui **unifie aussi les 2 arbres de catégories** (#290 : `CategoryTree` déplié-par-défaut, guide replié ; corrige le bug `allExpanded = size>0` du guide ; worker a trouvé un **4e consommateur** `StepDiscover` non listé au plan, migré). 828 vitest, aucune migration DB. Écarts protocole tracés au [rapport](reports/DAILY-REPORT-2026-07-15.md) : workers auto-validés **sans forker `/pr-review`** (évite le double-post [[feedback-pr-review-subagent-forks]]) ; #294 sans test (refactor de rendu, non testable sans jsdom) → vérif runtime déléguée à la revue. Reste `spec-ci-build-optimization` (2/4) + `spec-paiements` (#270/#271) + #259. (ref #288-291, PRs #292-#295)
- 2026-07-12 : **#259 recadrée — l'issue décrivait un flux qui n'existe pas** (rectifie les entrées des 2026-07-05 / 07-11 qui la classaient « mapping manuel compte sans similaire auto » et « reliquat de l'epic #260 » : les deux sont faux). Le corps d'origine, rédigé à chaud le 2026-07-05 pendant le run v0.12.0, avait **transposé par analogie** le signalement de Max vers le module Bilan sans ouvrir le fichier : il décrivait une auto-association des comptes standards aux comptes existants du profil dans `StarterAccountsModal`, avec « case désactivée quand aucun similaire n'est auto-identifié ». **Aucune notion de mapping n'existe dans ce flux** — la case est un simple « créer ce compte : oui/non », désactivée **quand une collision EST détectée** (`StarterAccountsModal.tsx:160`), soit la polarité inverse ; le commentaire `l.8` cité (« the matching checkbox ») désignait « la case **correspondante** », pas « la case de matching ». **Le vrai sujet** (confirmé par Max) est la **migration des catégories** : `computeMigrationPlan` range les catégories en 2 seaux et un seul est éditable — `plan.rows` (seed) passe par le moteur d'appariement + type-ahead par ligne (#246/#252), tandis que `plan.preserved` (catégories **custom**) est poussé avec `v1TargetId: null` **sans même être soumis au moteur d'appariement**, rendu en `
` texte brut (`StepSimulate.tsx:202-216`, aucun picker) et déversé d'office par le writer sous le fourre-tout « Catégories personnalisées (migration) ». Sémantique tranchée avec Max : **fusion** (choisir une feuille standard réassigne tx/budgets/mots-clés/fournisseurs et fait disparaître la custom ; ne rien choisir = comportement actuel, et ne doit **pas** bloquer le bouton « Suivant »). Re-parentage écarté. Travail = câblage sur 3 couches (UI `StepSimulate` → `MappingRow` réutilisable tel quel ; reducer `RESOLVE_ROW` qui ne voit que `plan.rows` ; 4 retouches du writer) — la machinerie de fusion est **déjà générique** (`buildMappingFromRows` filtre les cibles nulles, étapes 3-7 bouclent sur la Map). Complexité Medium. Piège à couvrir : fusionner une custom **parente ayant des enfants custom** (liste `preserved` plate → l'enfant non résolu retombe au fourre-tout, pas d'orphelin, mais aucun test ne le garantit). Leçon process : une issue rédigée par analogie en fin de run, sans lecture du code, peut inverser la prémisse **et** se tromper de module — le `/analyze` l'a rattrapée 6 jours plus tard. (ref #259)
@@ -30,4 +31,4 @@ Audit critique de la page Bilan livré (`docs/audit-bilan-2026-05.md`, revue CPA
## Blockers actifs
- Aucun blocker externe dur. `spec-monetisation` **fermée 12/12** (2026-07-08) — #50/#52/#53/#135/#136 livrées, ne sont plus des blockers.
-- Backlog ouvert (`status:ready`, non bloqué) : `spec-paiements` (#270 activation /v1 + product explicite + URL achat localisée ; #271 re-gate auto-update derrière la licence Base) ; `spec-ci-build-optimization` 2/4 (#234 connectivité cache runner, #232 split workflows).
+- Backlog ouvert (`status:ready`, non bloqué) : `spec-feature-gating` (#297-#302, gating par tier Free/Base/Premium — planifié 2026-07-19) ; `spec-paiements` (#270 activation /v1 + product explicite + URL achat localisée ; #271 absorbé par #301) ; `spec-ci-build-optimization` 2/4 (#234 connectivité cache runner, #232 split workflows).
diff --git a/spec-decisions-feature-gating.md b/spec-decisions-feature-gating.md
new file mode 100644
index 0000000..04e725e
--- /dev/null
+++ b/spec-decisions-feature-gating.md
@@ -0,0 +1,80 @@
+# Spec Decisions — Gating des fonctionnalités par édition de licence
+
+> Date: 2026-07-19
+> Projet: simpl-resultat
+> Statut: Draft
+> Slug: feature-gating
+
+## Contexte
+
+Simpl'Résultat n'applique aujourd'hui **aucun gating de fonctionnalités** : un utilisateur Free a accès à tout, sauf la récupération des cours (seul gate dur, vérifié côté serveur via maximus-api). L'infrastructure de licence est pourtant déjà là — 3 éditions signées Ed25519 (`free`/`base`/`premium`), table `FEATURE_TIERS`, champ `features[]` signé — mais seuls 3 call-sites de gate existent (auto-update, laissé ouvert ; cours, Premium). Max veut définir un vrai périmètre d'abonnement en verrouillant des modules selon le tier.
+
+## Objectif
+
+Appliquer une matrice d'entitlements Free/Base/Premium à travers l'app : chaque module hors du tier de l'utilisateur reste **visible mais verrouillé**, avec un écran d'upsell. Le gating est piloté par un hook central côté React, adossé à l'édition de licence déjà vérifiée, avec un override par-licence optionnel. Aucune donnée n'est supprimée — le durcissement bloque l'accès, il ne détruit rien.
+
+## Scope
+
+### IN
+- Matrice d'entitlements Free/Base/Premium (ci-dessous).
+- Hook central `useEntitlement(feature)` + garde de navigation (Sidebar) et de route.
+- Écran/zone d'upsell « verrouillé » (cadenas, CTA upgrade), i18n FR/EN.
+- Enrichir `FEATURE_TIERS` (Rust) + câbler l'override par-licence (`features[]` consulté par `is_feature_allowed`).
+- Gating **par onglet** des rapports (`/reports/*`) : Free = Tendance uniquement.
+- Absorption du re-gate auto-update (#271) : l'auto-update devient Base+.
+- Durcissement sec **non destructif** : blocage d'accès immédiat, données conservées, récupérables par upgrade.
+
+### OUT (explicitement exclu)
+- Le champ `product` explicite (#270) — reste dans spec-paiements.
+- Le go-live Stripe / le flux d'achat en ligne (l'upsell pointe vers l'achat, dont l'URL localisée relève de #270).
+- Un vrai système de codes d'invitation (l'émission de clés reste manuelle via l'endpoint admin `generate`).
+- Tout gate « dur » server-enforced au-delà de l'existant (cours) — le gating local est un soft paywall assumé (GPL).
+- Un 4e tier « admin » : l'accès total de Max = une licence Premium auto-émise.
+- Mode lecture-seule des features verrouillées (écarté : durcissement sec, pas de distinction voir/éditer).
+
+## Matrice d'entitlements
+
+| Module | Free | Base | Premium |
+|---|:--:|:--:|:--:|
+| Dashboard | ✅ | ✅ | ✅ |
+| Import CSV | ✅ | ✅ | ✅ |
+| Transactions | ✅ | ✅ | ✅ |
+| Catégories (+ guide, migration) | ✅ | ✅ | ✅ |
+| Rapport **Tendance** | ✅ | ✅ | ✅ |
+| Autres rapports (Highlights, Comparaison, Catégorie, Cartes) | ❌ | ✅ | ✅ |
+| Budget | ❌ | ✅ | ✅ |
+| Multi-profils | ❌ | ✅ | ✅ |
+| Auto-update | ❌ | ✅ | ✅ |
+| **Bilan complet** (patrimoine + cours) | ❌ | ❌ | ✅ |
+| Export/Import chiffré | ✅ | ✅ | ✅ |
+| Changelog / Docs | ✅ | ✅ | ✅ |
+
+Admin (Max) = licence **Premium** auto-émise (superset).
+
+## Décisions prises
+
+| Question | Décision | Raison |
+|---|---|---|
+| UX du gate (feature hors tier) | **Verrouillé + upsell** : entrée visible avec cadenas ; clic → écran « Passez à Base/Premium » | Meilleure conversion — l'utilisateur voit ce qu'il rate |
+| Données existantes lors du durcissement | **Durcissement sec non destructif** : blocage d'accès (vue + édition) immédiat, mais **données conservées**, jamais supprimées, récupérables par upgrade | Simple à coder (pas de mode lecture-seule) ; ne détruit aucune donnée financière (clarifié par Max) |
+| Modèle d'entitlements | **Statique + override licence** : map `tier → features` en dur, PLUS `license.features[]` consulté en additif (`allowed = feature ∈ tier OU feature ∈ license.features`) | Matrice centralisée et simple, + débloque le champ `features[]` déjà signé pour des licences spéciales sans créer de tier |
+| Coordination spec-paiements | **Absorber #271** (re-gate auto-update) ici ; **#270** (`product`) reste séparé | L'auto-update est dans la matrice → gaté ici, #271 redondant ; `product`/URL d'achat = go-live, hors scope |
+| Enforcement | **UI-only** (gardes React : nav + route). `current_edition()` (Rust) reste la source de vérité lue ; les commandes Tauri ne refusent pas (sauf l'existant : cours server-enforced) | Cohérent avec le soft-paywall GPL assumé — un gate Rust serait aussi retirable par un fork, sans gain de sécurité |
+| Écran d'upsell | **Composant générique paramétré** (feature + tier requis), i18n FR/EN ; CTA « Obtenir Base/Premium » (lien d'achat, dépend de #270) + « J'ai déjà une clé » → Réglages → Licence | Un seul composant réutilisable, pas un écran par feature |
+| Admin (accès total de Max) | **Licence Premium auto-émise** (endpoint admin `generate`), pas de 4e édition ni mode dev | Premium = superset ; réutilise l'infra existante, zéro code neuf |
+| Free multi-profils existants | **Profil actif conservé** ; profils supplémentaires verrouillés → upsell (données conservées) | Cohérent avec durcissement sec + conservation des données |
+
+## Contraintes
+
+- **GPL-3.0 + privacy-first** : le gating local est un soft paywall **assumé** (contournable par fork). Seules les features adossées à maximus-api (les cours, aujourd'hui) sont dures. Aucune donnée financière ne quitte l'appareil.
+- **Séquencement** : le gating est livrable et testable **indépendamment de Stripe** (pas encore en live) via des clés admin-émises. Corollaire : sans ce chantier, distribuer des clés n'a aucun effet visible (tout est déjà ouvert).
+
+## Références
+
+| Source | Pertinence |
+|---|---|
+| `spec-monetisation.md` | Modèle des 3 tiers, contraintes GPL / soft-paywall, format de licence |
+| `src-tauri/src/commands/entitlements.rs` | `FEATURE_TIERS` + `is_feature_allowed` — table à enrichir, override `features[]` à câbler |
+| `src-tauri/src/commands/license_commands.rs` | `current_edition()` (source de vérité de l'édition) + `LicenseClaims.features` (champ signé à consulter) |
+| `src/services/licenseService.ts`, `src/hooks/useLicense.ts`, `src/hooks/useIsPremium.ts` | Base du futur hook `useEntitlement` |
+| `src/App.tsx` (routes `/reports/*`), `src/components/layout/Sidebar` | Points de garde nav + route |
diff --git a/spec-plan-feature-gating.md b/spec-plan-feature-gating.md
new file mode 100644
index 0000000..4cd4c29
--- /dev/null
+++ b/spec-plan-feature-gating.md
@@ -0,0 +1,172 @@
+# Spec Plan — Gating des fonctionnalités par édition de licence
+
+> Date: 2026-07-19
+> Projet: simpl-resultat
+> Statut: Draft
+> Slug: feature-gating
+> Decisions: [spec-decisions-feature-gating.md](./spec-decisions-feature-gating.md)
+
+## Ancrage code (vérifié 2026-07-19)
+
+- **Rapports = routes distinctes** (`App.tsx:117-122`) : `/reports` (hub), `/reports/highlights|trends|compare|category|cartes`. Le gating « par onglet » = **gating par route**.
+- **Édition + features déjà en mémoire** : `useLicense` (`useLicense.ts:59`) charge `edition` + `info.features[]` au boot (`getEdition` + `readLicense`). ⇒ `useEntitlement` peut être **synchrone**. MAIS `useLicense` est un état **par-appel** (chaque montage refait 2 invokes) → il faut un **Context** partagé.
+- **`FEATURE_TIERS` Rust** (`entitlements.rs:14-21`) ne déclare que 4 features ; `is_feature_allowed` ne regarde **que l'édition** (`entitlements.rs:24-30`) — l'override `features[]` n'est pas câblé. Une feature absente = deny-all.
+- **Auto-update déjà branché** sur `check_entitlement("auto-update")` (`useUpdater.ts:79`), UI gère l'état `notEntitled`. ⇒ #271 = **1 ligne** dans `FEATURE_TIERS`.
+- **`LicenseInfo.features[]`** est peuplé depuis le JWT (`license_commands.rs:132`) et exposé au front (`licenseService.ts:8`) → l'override par-licence est **gratuit côté TS**.
+- **Sidebar** lit `NAV_ITEMS` (`constants/index.ts:6-61`), liste de config `{key, path, icon, labelKey}`. **`useIsPremium`** (`useIsPremium.ts`) remonte `useLicense` (à rebrancher sur le Context). **`LicenseCard`** est monté à `/settings/users` (`UsersSettingsPage.tsx:34`) → cible du lien « J'ai déjà une clé ».
+
+## Design
+
+### Architecture
+
+Source de vérité de l'**édition** = inchangée (`current_edition()` Rust, fail-closed). Nouveau : une **couche d'entitlements côté TS** pour un gating UI synchrone.
+
+```
+LicenseProvider (context, chargé 1× au boot)
+ ├── edition, features[], info, refresh, submitKey
+ └── consommé par :
+ useEntitlement(featureKey) → sync, lit ENTITLEMENTS + override features[]
+ useIsPremium() → refactoré pour lire le context (au lieu d'un useLicense par-appel)
+```
+
+- **Matrice UI** (`src/shared/entitlements.ts`, nouveau) — source unique côté front :
+ ```ts
+ export type FeatureKey = "budget" | "reports.advanced" | "multi-profile" | "balance";
+ export const ENTITLEMENTS: Record = {
+ "budget": ["base", "premium"],
+ "reports.advanced":["base", "premium"],
+ "multi-profile": ["base", "premium"],
+ "balance": ["premium"],
+ };
+ export function isEntitled(f: FeatureKey, edition: Edition, licenseFeatures: string[]): boolean {
+ return ENTITLEMENTS[f].includes(edition) || licenseFeatures.includes(f);
+ }
+ ```
+ `auto-update` n'est **pas** ici : il reste géré côté Rust (`FEATURE_TIERS`), car c'est le seul gate consulté par du code Rust. Les modules Free (dashboard, import, transactions, catégories, `reports/trends`, export, changelog, docs) n'ont **pas** de clé → jamais gatés.
+
+- **Enforcement UI-only** (soft-paywall assumé) : aucune commande Tauri ne refuse (sauf l'existant : cours server-enforced). Le Rust ne change que pour `auto-update` (#271) + le câblage de l'override dans `check_entitlement`.
+
+### UX / Interface
+
+- **``** (nouveau, `src/components/shared/`) : écran plein — cadenas, titre « Fonctionnalité Base/Premium », description paramétrée par feature, deux CTA : « Obtenir » (lien d'achat — placeholder jusqu'à #270) + « J'ai déjà une clé » → `navigate("/settings/users")`. i18n FR/EN.
+- **``** (nouveau) : `useEntitlement(feature) ? children : `. Wrappe les routes gatées.
+- **Sidebar** : `NavItem` gagne `feature?: FeatureKey`. Un item non autorisé reste **visible avec un cadenas** (icône `Lock`) ; le clic mène à la route → `RequireFeature` affiche l'upsell (décision « verrouillé + upsell », pas masqué).
+- **Hub `/reports`** (Free) : accessible, mais les tuiles vers les rapports avancés portent un cadenas.
+- **Multi-profils** : `ProfileSwitcher` marque d'un cadenas les profils au-delà de l'actif quand `!useEntitlement("multi-profile")` ; « créer un profil » est verrouillé (upsell) pour un Free ayant déjà ≥ 1 profil. **Données conservées** : rien n'est supprimé de `profiles.json`, seul l'accès est bloqué.
+
+### Données
+
+Aucune nouvelle table, **aucune migration DB** — le gating lit la licence (fichier `license.key`), pas la base profil. `NavItem` (type TS) gagne un champ optionnel `feature`.
+
+## Plan de travail
+
+### Issue 1 — Socle : LicenseProvider + matrice d'entitlements + useEntitlement [type:feature]
+Dependances : aucune
+- [ ] `src/contexts/LicenseContext.tsx` : provider encapsulant la logique de `useLicense` (charge édition + info 1×), expose `{ status, edition, features, info, refresh, submitKey }`. Monter dans `main.tsx` au niveau racine (licence = machine-level, au-dessus de `ProfileProvider`).
+- [ ] `src/shared/entitlements.ts` : type `FeatureKey`, `ENTITLEMENTS`, `isEntitled()` (pur).
+- [ ] `src/hooks/useEntitlement.ts` : `useEntitlement(f: FeatureKey): boolean` (sync, lit le context).
+- [ ] Refactorer `src/hooks/useIsPremium.ts` pour lire `LicenseContext` (behavior-preserving) → supprime le double-invoke sur `PriceFetchControl`/`PriceFetchConsentToggle`.
+- [ ] Refactorer `LicenseCard` pour consommer le context (au lieu de son `useLicense` local).
+- [ ] Tests : `entitlements.test.ts` (matrice, override `features[]`, édition inconnue).
+
+### Issue 2 — Garde UI : RequireFeature + UpsellGate + i18n [type:feature]
+Dependances : Issue 1
+- [ ] `src/components/shared/UpsellGate.tsx` : écran verrouillé paramétré (feature, tier requis), 2 CTA.
+- [ ] `src/components/shared/RequireFeature.tsx` : wrapper de route.
+- [ ] i18n `src/i18n/locales/{fr,en}.json` : clés `upsell.*` (titres/descriptions par feature, CTA) + `nav.locked` (aria cadenas).
+
+### Issue 3 — Gating des routes + Sidebar [type:feature]
+Dependances : Issue 2
+- [ ] `App.tsx` : wrapper `` sur `/budget` ; `"reports.advanced"` sur `/reports/highlights|compare|category|cartes` (PAS `/reports/trends`, PAS `/reports` hub) ; `"balance"` sur `/balance`, `/balance/accounts`, `/balance/snapshot`.
+- [ ] `src/shared/types` : `NavItem` + `feature?: FeatureKey`. `constants/index.ts` : renseigner `feature` sur budget/reports/balance.
+- [ ] `Sidebar.tsx` : cadenas sur item non autorisé (via `useEntitlement`).
+- [ ] `ReportsPage` (hub) : cadenas sur les tuiles des rapports avancés.
+
+### Issue 4 — Gating multi-profils [type:feature]
+Dependances : Issue 1 (+ Issue 2 pour l'upsell)
+- [ ] `ProfileSwitcher` : cadenas + upsell sur profils au-delà de l'actif si `!multi-profile`.
+- [ ] Bouton « créer un profil » (ProfileSwitcher / ProfileSelectionPage) : verrouillé pour Free ayant ≥ 1 profil.
+- [ ] Garde non-destructive : ne rien retirer de `profiles.json`.
+
+### Issue 5 — Absorber #271 : auto-update Base+ + override licence (Rust) [type:feature]
+Dependances : aucune (Rust indépendant — parallélisable)
+- [ ] `entitlements.rs:17` : `("auto-update", &[EDITION_BASE, EDITION_PREMIUM])`, retirer le commentaire « temporarily open », ajuster le test `free_allows_auto_update_temporarily` → `free_denied_auto_update`.
+- [ ] Câbler l'**override `features[]`** dans `check_entitlement` : `is_feature_allowed(feature, edition) || license_features.contains(feature)` (charger les features de la licence courante).
+- [ ] Dev override (confort de test) : en `#[cfg(debug_assertions)]`, `current_edition` lit `SR_DEV_EDITION` (jamais en build release) pour tester les 3 tiers sans jongler avec des clés.
+- [ ] Ferme/rescope l'issue Forgejo #271.
+
+### Issue 6 — Docs : ADR + architecture + CHANGELOG [type:feature]
+Dependances : Issues 1-5
+- [ ] ADR `docs/adr/00XX-feature-gating-par-tier.md` : modèle (matrice tier→features + override par-licence, enforcement UI-only, soft-paywall GPL assumé).
+- [ ] `docs/architecture.md` : nouveau context/hook/module d'entitlements.
+- [ ] `docs/guide-utilisateur.md` + i18n `docs.*` : ce que débloque chaque tier.
+- [ ] CHANGELOG.md + CHANGELOG.fr.md : **une** entrée globale (durcissement : liste des modules désormais Base/Premium).
+
+### Ordre d'execution
+```
+Issue 1 → Issue 2 → Issue 3
+Issue 1 → Issue 4
+Issue 5 (indépendant, parallèle)
+Issues 1-5 → Issue 6
+```
+
+## Fichiers concernes
+
+| Fichier | Action | Raison |
+|---|---|---|
+| `src/contexts/LicenseContext.tsx` | Créer | Provider licence partagé (1 chargement) |
+| `src/shared/entitlements.ts` | Créer | Matrice `ENTITLEMENTS` + `isEntitled` (pur, testable) |
+| `src/hooks/useEntitlement.ts` | Créer | Hook sync de check |
+| `src/hooks/useIsPremium.ts` | Modifier | Lire le context (dé-duplique l'invoke) |
+| `src/components/settings/LicenseCard.tsx` | Modifier | Consommer le context |
+| `src/components/shared/UpsellGate.tsx` | Créer | Écran verrouillé + CTA |
+| `src/components/shared/RequireFeature.tsx` | Créer | Wrapper de route |
+| `src/App.tsx` | Modifier | Wrapper les routes budget / reports avancés / balance |
+| `src/shared/types` (NavItem) | Modifier | Champ `feature?` |
+| `src/shared/constants/index.ts` | Modifier | Renseigner `feature` sur les items gatés |
+| `src/components/layout/Sidebar.tsx` | Modifier | Cadenas sur item non autorisé |
+| `src/pages/ReportsPage.tsx` | Modifier | Cadenas sur tuiles avancées |
+| `src/components/profile/ProfileSwitcher.tsx` | Modifier | Cadenas profils + gate création |
+| `src-tauri/src/commands/entitlements.rs` | Modifier | auto-update Base+ (#271) + override `features[]` |
+| `src-tauri/src/commands/license_commands.rs` | Modifier | Dev override `SR_DEV_EDITION` (debug only) |
+| `src/i18n/locales/{fr,en}.json` | Modifier | Clés `upsell.*`, `nav.locked` |
+| `docs/adr/00XX-*.md`, `docs/architecture.md`, `CHANGELOG*.md` | Créer/Modifier | Documentation |
+
+## Plan de tests
+
+### Tests unitaires
+- `entitlements.ts` : `isEntitled` — chaque feature × chaque édition, override `features[]` (une licence Free avec `features:["budget"]` débloque budget), feature absente.
+- Rust `entitlements.rs` : auto-update denied Free / allowed Base+Premium ; override (`is_feature_allowed` OR `license.features`).
+
+### Tests d'integration
+- Rust : `current_edition` + `check_entitlement` bout-en-bout avec une licence signée de test (le harness de test de `license_commands.rs` signe déjà des JWT) portant `features:[…]`.
+
+### Tests de regression
+- `useIsPremium` : le refactor vers le context ne change pas le résultat (Premium ⇢ true) — figer via un test de `isEntitled`/edition avant de toucher `PriceFetchControl`.
+- Suite existante verte (le gate cours via `useIsPremium` ne doit pas régresser).
+
+**Contrainte** : ni jsdom ni @testing-library → les composants React (`RequireFeature`, `UpsellGate`, `Sidebar`, `ProfileSwitcher`) ne sont pas rendables en test. La logique testable vit dans `isEntitled` (TS pur) et les checks Rust ; le rendu est délégué à la vérif runtime / `/pr-review`.
+
+## Criteres d'acceptation
+
+- [ ] Un Free voit Budget, les 4 rapports avancés et le Bilan **verrouillés** (cadenas + upsell), garde Dashboard/Import/Transactions/Catégories/Tendance/Export/Changelog.
+- [ ] Un Base débloque Budget, tous les rapports, multi-profils, auto-update ; Bilan reste verrouillé.
+- [ ] Un Premium a tout.
+- [ ] Le durcissement est **non destructif** : les budgets/Bilan/profils déjà créés réapparaissent intacts après upgrade.
+- [ ] Un Free avec 2 profils garde l'actif ; les autres sont verrouillés (conservés) ; « créer profil » est verrouillé.
+- [ ] Auto-update refusé pour Free (#271 absorbé), autorisé Base+.
+- [ ] Une licence portant `features:[""]` débloque cette feature quel que soit le tier (override).
+- [ ] `useEntitlement` est synchrone (pas de flash « verrouillé » au chargement une fois la licence lue).
+- [ ] Aucune migration DB ; suite de tests verte.
+
+## Edge cases et risques
+
+| Cas | Mitigation |
+|---|---|
+| `/adjustments` non classé dans la matrice | **À trancher** — défaut proposé : Free (proche des Transactions), pas de gate. Confirmer avec Max. |
+| Max se bloque de son propre Bilan en dev (dev = édition `free`) | Dev override `SR_DEV_EDITION=premium` (`#[cfg(debug_assertions)]`) — Issue 5. Sinon, poser une vraie clé Premium locale. |
+| Flash « verrouillé » pendant le chargement de la licence au boot | `useEntitlement` renvoie un état pendant `status !== "ready"` : rendre un loader (pas l'upsell) tant que la licence n'est pas résolue. |
+| Désync matrice TS ↔ Rust | Non applicable : la matrice UI vit **uniquement** en TS ; Rust ne gère qu'`auto-update`. Pas de double source pour les features UI. |
+| Rollout : gating partiel sur `main` entre les issues | Sans effet en prod — le durcissement ne devient visible qu'à la prochaine **release** taggée. Ordre des issues garde `main` cohérent (socle avant gardes). |
+| Contournement (fork retire le `if`) | Assumé (GPL, soft-paywall). Hors périmètre — seul le gate serveur (cours) est dur. |
+| Override `features[]` sur une Premium-via-compte (pas de license.key) | L'override vient du JWT `license.key` ; un Premium-via-abonnement n'a pas de `features[]` custom → comportement normal (tier seul). |