diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/MEMORY.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/MEMORY.md
new file mode 100644
index 0000000..b54138b
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/MEMORY.md
@@ -0,0 +1,97 @@
+# MEMORY.md — Crowdlending App
+
+- [Profil utilisateur](user_profile.md) — Olivier, propriétaire de l'app crowdlending, développeur qui pilote les évolutions
+- [Modèle plateforme — détenteur & logo](project_plateforme_detenteur_logo.md) — Champs investisseur_id, date_ouverture, logo_filename ajoutés aux plateformes ; contrainte UNIQUE modifiée
+- [Pattern investisseurForPlat](feedback_investisseur_for_plat.md) — Helper à créer dans chaque page formulaire pour synchroniser le détenteur avec la plateforme choisie
+- [Fonctionnalité réinvestissements](project_reinvestissements.md) — Table reinvestissements, route /api/reinvestissements, capital_total, generateSimulWithReinvestissements, adjustSimulForActuals modifiée
+- [adjustSimulForActuals — règles complètes](feedback_adjustsimul.md) — Capital effectif, capital soldé (DELETE futures), reprocess doit appeler adjustSimulForActuals après transaction
+- [Calcul solde porte-monnaie](project_solde_portefeuille.md) — Formule complète : dépôts − retraits_manuels + net_recu_portefeuille + bonus − capital_investi ; implémentée backend (dashboard.js) et frontend (DepotsRetraits.jsx)
+- [Pattern multiDetenteur](feedback_multi_detenteur.md) — Le détenteur ne s'affiche dans les selects plateforme que si plusieurs détenteurs distincts existent (`new Set(plats.map(p => p.investisseur_id)).size > 1`)
+- [Fonctionnalité corrections de solde](project_corrections_solde.md) — Table corrections_solde, route /api/corrections, CorrectionModal dans DepotsRetraits, badge dans Remboursements, intégration fiscal2778
+- [Troncature et corruption fichiers — toujours passer par Python](feedback_file_truncation.md) — Edit/Write et sed -i tronquent les fichiers volumineux ; jamais sed, toujours python3 + vérifier tail/hex
+- [Composant ConfirmModal et suppressions](project_confirm_modal.md) — ConfirmModal.jsx créé ; pattern standard pour toute future suppression ; 6 fichiers déjà migrés
+- [Correction intérêts nets — Investissements](project_interets_nets_investissements.md) — interets_nets_total ajouté au backend ; net_recu_total ≠ intérêts nets (inclut capital+cashback)
+- [Fiscalité locale sur remboursements](project_fiscalite_locale_remboursements.md) — Champs interets_bruts_avant_local et taxe_locale ajoutés (DB + backend + 2 formulaires + tableau détail)
+- [Page Centre d'aide](project_aide.md) — Route /aide, layout Settings, FAQ accordéon ; bouton dans UserMenu
+- [Menus ⋮ actions sur les tableaux et blocs](project_menu_actions_tables.md) — Pattern menu contextuel fixe (position: fixed + backdrop + icônes SVG) ; scroll ferme les menus ; AdminPlateformes ajouté
+- [Format menu ⋮ — règle obligatoire](feedback_menu_actions_format.md) — Tout nouveau tableau doit utiliser le menu ⋮ avec icônes SVG + label, jamais de boutons inline
+- [Logo plateforme topbar InvestissementDetail](project_logo_topbar_detail.md) — logo remplace le nom si disponible ; plateforme_logo retourné par le backend ; classe logo-plateforme pour dark mode
+- [onClick direct avec paramètre par défaut](feedback_onclick_default_param.md) — onClick={fn} passe l'event comme arg1, écrase les defaults ; toujours utiliser onClick={() => fn()}
+- [Réinvestissement automatique des intérêts](project_auto_reinvest.md) — auto_reinvest sur investissements, source sur reinvestissements, déclencheur POST remboursement, tabs Manuel/Auto dans la modale
+- [Panneau détail unifié DepotsRetraits](project_detail_panel_depots.md) — DetailPanel unifié dépôts/retraits/corrections ; libellé bouton adapté au type ; props { row, onEdit, onDeleteCorrection }
+- [Menu ⋮ carte Informations du projet](project_card_menu_investissement.md) — cardMenu state séparé ; Modifier/Réinvestir/Exporter/Supprimer + Désactiver auto-reinvest ; utiliser ConfirmModal pas confirmingInvDelete
+- [Méthode de remboursement sur investissements](project_methode_remboursement_investissement.md) — methode_remboursement + nom_compte_courant sur investissements ; champ conditionnel si plateforme=choix_investisseur ; retrait auto depots_retraits ; affichage fiche détail
+- [Override fiscalité par investissement](project_fiscalite_override.md) — fiscalite_override='exonere' ; badge + menu ⋮ + modale confirmation ; prelev indicatifs (pas forcés à 0) ; voir [[project_istaxindicatif]]
+- [Règle isTaxIndicatif + route reprocess](project_istaxindicatif.md) — prelev toujours calculés, net_recu = brut si indicatif ; POST /api/remboursements/reprocess ; bouton dans MonCompte?section=nettoyage
+- [Fallback methode remboursement formulaire](feedback_remb_methode_fallback.md) — openRembFromSimul/openNewRemb : utiliser inv.methode_remboursement si plateforme=choix_investisseur, jamais 'portefeuille' en dur
+- [Formulaires remboursement refactorisés](project_formulaires_remboursement.md) — Grille 3 col / 900px, sections Remboursement/Imposition/Versement, Total prélèvements, commentaire Montant versé, isIndicatif aligné
+- [Tableau Remboursements enregistrés](project_tableau_remboursements_detail.md) — Fusion PS+IR→Imposition, tooltip CSS instantané (.cell-tooltip), remb-table CSS, clic ligne → modale édition
+- [Graphiques Dashboard — barres + donut](project_interets_mensuels_chart.md) — InteretsMensuelsChart (barres empilées) + InteretsDonutChart (donut caps arrondis) + InteretsChartContext partagé ; mode TOUT (vue multi-années) ; layout 2/3+1/3 stretch
+- [Donut interactif — tooltip, clic, mémoire, légende](project_donut_interactif.md) — Tooltip SVG, clic isole un type ou Reçu/Projeté, retour arrière via centre (↩), légende cliquable, ordre Intérêts→Capital→Cashback
+- [YearSelectorKpi — sélecteur année Dashboard](project_year_selector_kpi.md) — Card violet/indigo sur la ligne KPI, dropdown custom années croissant + "Depuis le début", sync bidirectionnelle avec InteretsChartContext
+- [Couleurs personnalisables des graphiques](project_chart_colors.md) — UiContext : cl_chart_interets/capital/cashback ; Settings > Apparence : palette MD2, PaletteSelector, ChartColorPicker, preview Reçu/Projeté (lightenColor 72%)
+- [Préférences utilisateur en DB](project_user_preferences.md) — Table user_preferences (user_id, key, value) ; route /api/preferences GET+PATCH ; UiContext sync DB→localStorage au montage, PATCH asynchrone
+- [Bibliothèque d'icônes](project_icons_library.md) — Tables app_icons + history, route /api/icons, section Admin, nettoyage fond SVG, composant AppIcon dans Settings
+- [InvestissementDetail — nouvelles fonctionnalités](project_investissement_detail_features.md) — Menu simulMenu : modifier DPE + traitement en masse remboursements ; correction ConfirmModal rowDeleteConfirm manquant dans le JSX
+- [PUT investissements — payload minimal](feedback_put_payload_minimal.md) — Ne jamais spread `inv` ; construire le payload champ par champ selon le schéma Zod backend
+- [KPIs Dashboard — TrendBadge, logique M vs M-1](project_dashboard_kpis.md) — 5 KPIs refaits ; année N = mois courant vs M-1 ; année N-x = annuel vs N-1 ; modeGlobal = cumul sans badge
+- [Tableau intérêts par plateforme](project_tableau_interets_plateforme.md) — TableauInteretsPlateforme.jsx dans Dashboard ; endpoint /api/dashboard/interets-par-plateforme ; synchronisé InteretsChartContext
+- [Capital investi — formule unifiée](project_invchart_capital_encours.md) — montant_investi + reinvests(≤date) − capital_remboursé(≤date) ; dashboard.js + Investissements.jsx + InvChart
+- [Timezone — ne pas utiliser .toISOString() pour dates locales](feedback_timezone_lastday.md) — `.toISOString().split('T')[0]` décale d'un jour en UTC+1 ; toujours getFullYear/getMonth/getDate
+- [Vision mensuelle — capital par plateforme/mois](project_capital_mensuel_table.md) — CapitalMensuelTable.jsx, onglet entre Plateformes et Investissements, calcul frontend, classes tip-table
+- [Exonération fiscale sur comptes](project_comptes_exoneration.md) — Colonne exoneration_fiscale (aucune/pfnl_5ans) sur table comptes ; DB+backend+frontend Settings
+- [Vision mensuelle — dépôts/retraits par plateforme/mois](project_depots_mensuel_table.md) — DepotsMensuelTable.jsx, onglet entre Plateformes et Mouvements dans DepotsRetraits, filtres icônes dépôt/retrait
+- [UX formulaires Investissement & Remboursement](project_formulaires_ux.md) — Pas de présélection plateforme/investissement ; détenteur masqué (auto) ; taux/durée/DPE requis
+- [UserMenu — portail React obligatoire](feedback_usermenu_portal.md) — Popup et sous-panel via createPortal(…, document.body) pour échapper au stacking context de la sidebar sticky
+- [TableauInteretsPlateforme — padding wrapper](feedback_tiptable_padding.md) — Envelopper dans `
` dans chaque page (Dashboard, Remboursements)
+- [Toggle Détaillé/Consolidé tableaux mensuels](project_toggle_consolidation.md) — Bouton dans th Plateforme, clé localStorage `cl_tip_group_by_nom`, sur 4 composants ; mergeMaps pour TableauInteretsPlateforme
+- [Icônes bibliothèque — sidebar & titres pages](project_icons_sidebar_titles.md) — NavIcon dans Layout.jsx (24px, fallback SVG) ; PageIcon inline dans h2 (40px, cache module-level) ; 5 pages équipées
+- [Règle icônes topbar — ne pas modifier flex](feedback_pageicon_topbar.md) — Toujours insérer PageIcon comme img inline dans le h2, jamais de wrapper div ni display:flex sur le h2
+- [Pagination listes](project_pagination.md) — Hook usePagination + composant Pagination ; 15/25/50/100 items, localStorage par page, reset sur filtre
+- [Export Excel SheetJS](project_xlsx_export.md) — xlsx installé dans frontend/ ; pattern json_to_sheet → book_append_sheet → write type:array ; MIME xlsx
+- [TrendBadge KPIs Investissements](project_trendbadge_investissements.md) — TrendBadge sur les 5 KPIs ; platYear initialisé à l'année courante ; effectiveYear pour badges en vue "Toutes les années"
+- [TrendBadge KPIs DepotsRetraits + Remboursements](project_trendbadge_depots_remb.md) — Même pattern déployé sur DepotsRetraits (4 KPIs) et Remboursements (3 KPIs)
+- [Page Fiscalité — refonte TaxReport](project_taxreport.md) — Fiscal2778→TaxReport, route /taxreport, onglets dr-tabs, pagination détail, YearSelector violet autonome
+- [Simulation CERFA 2561](project_cerfa2561.md) — Cerfa2561Preview.jsx, onglet inline TaxReport, cases 2TT/2TR/2BH/2CK/2TY, type_produit_fiscal sur plateformes, impression PDF
+- [FK compte_id sur investissements et remboursements](project_compte_id_remboursements.md) — DB+backend+4 formulaires frontend ; resolvedMethode depuis plateforme ; auto-sélection premier compte_courant ; colonne Versement dans Remboursements
+- [Déclaration 2778-SD et PFO](project_2778sd.md) — Onglet TaxReport conditionné par pfoAssujetti ; matrice mensuelle par plateforme étrangère ; simulation CERFA cases BA/IA/PQ/PV/PF1/PG1/QR ; report 2042 (2TR/2BH/2CK) ; taux dynamiques depuis taux_pfu (csg/crds/solidarite)
+- [Architecture onglets fiscaux — 2042 / 2561 / 2778-SD](project_cerfa_fiscal.md) — Cerfa2042Preview (nouveau) + refonte 2561 et 2778-SD : sidebar 3 vues, suivi mensuel combiné, données 2042 mixtes avec badges auto/à déclarer, fullName helper, route /taxreport/2778 reconstruite
+- [Référentiel taux crédit d'impôt 2047](project_taux_credit_impot.md) — Table taux_credit_impot (124 pays), route /api/taux-credit-impot, section Admin "Référentiels" avec TciModal, TciImportBlock, TciPromptBlock
+- [Pièges CSS — fieldset min-width et checkbox width](feedback_fieldset_checkbox_css.md) — fieldset : toujours minWidth:0 ; input[type=checkbox] : toujours width:'auto'
+- [Référentiel plateformes — architecture complète](project_referentiel_plateformes.md) — Tables referentiel + categories + notation, HERITABLE_FIELDS, routes push/reset/lier/importer, similarité Levenshtein 80%, frontend Settings + Admin
+- [Bibliothèque logos référentiel](project_logos_referentiel.md) — Section Admin "Logos des plateformes" (id: logos-ref) ; POST/DELETE /api/referentiel/:id/logo ; auto-push logo_filename ; placeholder croix si logo manquant ; useRef pas React.useRef dans Admin.jsx
+- [Recherche/filtres/pagination référentiel](project_referentiel_search_filter.md) — Recherche nom + filtre domiciliation + filtre catégorie + pagination dans ReferentielSection ; icone_filename prioritaire sur logo_filename, tous deux servis via /api/logos/
+- [Catégories & secteurs d'investissement](project_categories_secteurs_inv.md) — Tables categories_inv/secteurs_inv, routes ref-categories/ref-secteurs, fusion, chips dans PlatformeProfile et RefModal, filtre référentiel via ?filterCat=
+- [Catégories/secteurs utilisateur — deux niveaux](project_categories_secteurs_utilisateur.md) — user_id sur categories_inv/secteurs_inv, tables jonction plateforme+investissement, routes /api/categories-inv et /api/secteurs-inv, push admin étendu, sync auto investissements, chips formulaires, filtres liste
+- [Domiciliation → codes ISO pays](project_domiciliation_iso.md) — Migration complète : Zod string libre, DB 'france'→'FR', sentinel partout, CountrySelect intégré, DOMICILIATION_LABELS supprimé, badges hérité + ConfirmModal reset
+- [ResultBanner — règle bannières succès/erreur](feedback_result_banner.md) — Toujours utiliser ResultBanner (× à droite, auto-dismiss 4s) ; result toujours { ok, msg } ; redirect via onDismiss si besoin
+- [Tags suggérés admin — catégories/secteurs](project_tags_suggeres_admin.md) — Section inv-suggestions dans /admin/plateformes ; 6 routes backend ; bouton compteur dans RefListSection ; promouvoir/supprimer
+- [ProfilImportBlock — import IA avec tags inconnus](project_profil_import_ia_tags.md) — Flow 3 étapes (json→creation→diff) ; resolveTags Levenshtein≤2 ; création tags approuvés avant PUT ; categories_inv_ids/secteurs_inv_ids dans payload
+- [Sauvegarde ZIP référentiel](project_zip_backup_referentiel.md) — zip.js pur Node (no deps) ; 3 routes export/import ; api.blob() ; boutons ReferentielSection ; images incluses ; tags résolus par nom
+- [Express — ordre des routes statiques vs /:id](feedback_express_route_order.md) — Routes statiques (/export, /import-zip) TOUJOURS avant /:id ; vérifier avec grep après toute insertion
+- [Héritage catégories/secteurs — merge](project_categories_secteurs_inv_heritage.md) — Tags référentiel fusionnés dans les plateformes (is_inherited) ; InvSelect.inheritedIds ; associations-inv.js re-ajoute les inherited IDs au PUT ; compteur bandeau séparé des scalaires
+- [Admin sous-pages — structure](project_admin_sous_pages.md) — AdminPlateformes (garanties + notation référentiel) ; AdminFiscalite (PfuSection + TauxCreditImpotSection) ; pattern account-layout
+- [Settings NAV — structure actuelle](project_settings_nav.md) — 4 groupes : Interface / Mon paramétrage / Mes tags / Mes données ; membres+nettoyage déplacés de MonCompte vers Settings ; MonCompte réduit à profil+securite
+- [Refactorisation Settings.jsx + Admin.jsx](project_settings_admin_refacto.md) — Découpés en composants autonomes (pages/settings/ et pages/admin/) ; imports manquants fréquents ; bugs d'extraction documentés
+- [Page Plateformes](project_plateformes_page.md) — /plateformes : vue par plateforme, 4 onglets, simul/all endpoint, sélecteurs TIP identiques, grisage tip-td-closed
+- [Grisage cellules hors-période tip-table](feedback_tip_td_closed.md) — .tip-td-closed pour mois avant souscription ou après dernier remb (rembourse) ; tip-col-current prioritaire
+- [Anti-doublon projection mois courant](feedback_projection_no_double_count.md) — `real === 0` requis avant d'ajouter projAmt dans buildCellValue ; corrigé dans Plateformes + TableauInteretsPlateforme
+- [DrillCellPanel Dashboard + navigation croisée](project_drillcellpanel_dashboard.md) — Panel permanent mois courant sur Dashboard ; clic ligne → Remboursements (URL params) + auto-ouverture modal + retour Dashboard avec drill restauré
+- [Déploiement Docker — pièges et corrections](project_docker_deployment.md) — DATA_DIR env, fallthrough:false logos, nginx upstream name, Traefik @file, volume seed icons, deploy.sh
+- [Sidebar overlay — couleur parasite au survol](feedback_sidebar_expand_overlay.md) — `background: transparent !important` + appearance:none sur .sidebar-expand-overlay et ses états hover/focus/active ; effet visuel via filter:brightness sur le logo uniquement
+- [Safari mobile — viewport et dvh](feedback_safari_mobile_viewport.md) — Toujours doubler height:100vh + height:100dvh ; utiliser visualViewport.height au lieu de innerHeight en JS ; écouter visualViewport resize+scroll pour les menus fixed
+- [Auth email — vérification, reset, pages layout](project_auth_email.md) — smtp_config table, email_verified sur users, tokens vérification/reset, pages ForgotPassword/ResetPassword/VerifyEmail, MonCompte badges
+- [2FA — TOTP + email OTP + appareils de confiance](project_2fa.md) — totp_secret/totp_enabled sur users, tables two_fa_*, routes /2fa/setup|confirm-setup|disable|send-email-code|verify, trusted-devices CRUD, Login 3 étapes, TwoFASection + TrustedDevicesSection dans MonCompte
+- [otplib ESM — API fonctionnelle uniquement](feedback_otplib_esm.md) — Pas d'export `authenticator` ; utiliser `generateSecret`, `generateURI`, `verifySync` importés nommément depuis 'otplib'
+- [db/index.js — export default en double](feedback_dbindex_export_duplicate.md) — Un `export default db` existe en milieu de fichier (~ligne 942) ; toujours vérifier avec grep avant d'ajouter des migrations en fin de fichier
+- [Suppression détenteur — réassignation au principal](project_suppression_detenteur.md) — DELETE investisseur réassigne investissements/depots_retraits/plateformes/comptes au principal avant delete ; toute nouvelle table CASCADE doit être ajoutée à la transaction
+- [Export / Restore prod→dev](project_export_restore.md) — ZIP DB+assets, stockage serveur max 10, upload externe, restauration pending-restore+process.exit, job 3h00 (autoExport.js), ExportSection.jsx
+- [SQLITE_CORRUPT — VACUUM INTO obligatoire](feedback_sqlite_vacuum_into.md) — db.backup() produit un fichier WAL-mode incomplet → toujours VACUUM INTO ; valider avec integrity_check avant d'écrire le pending-restore
+- [Formule montant versé + style champs calculés](feedback_montant_verse_formula.md) — montantRembourse conditionnel isIndicatif (bruts si étranger/exonéré, netRecu si flat_tax) ; champs readonly : var(--surface-2)+var(--text-muted), jamais var(--bg-input-readonly)
+- [Affichage projections — montants barrés & échéances non tenues](project_projections_display.md) — matchRembsAll somme tous les rembs du mois ; barré+réel si différence >0,01€ ; "✗ Échéance non tenue" rouge si passé sans paiement
+- [Bug prêt différé — recalcul date_premiere_echeance](feedback_differe_date_calcul.md) — Passage à 'differe' doit toujours recalculer DPE si duree_mois présent ; validation dans submit ; label dynamique du champ
+- [DrillCellPanel — filtrage lignes à 0](project_drillcell_filtrage_zero.md) — recusVisible/projetesVisible filtrent les lignes à 0 selon critères actifs ; compteur X/Y si lignes cachées
+- [CSS tip-table + DrillCell mode consolidé](project_tip_css_drillcell_consolidation.md) — tip-th-name violet comme year ; bug consolidé : _ids[] dans merge, platIds dans onCellClick, plateforme_ids CSV au backend
+- [Système de notifications](project_notifications.md) — Table notifications, 8 types (system/ticket_reply/info/team/success/warning/security/announcement), cloche topbar polling 30s, page /notifications, event notif:refresh pour rechargement immédiat
+- [Audience des notifications — règle obligatoire](feedback_notifications_audience.md) — Toujours demander si la notif doit être vue par tous les admins (notifyAdmins) ou un utilisateur spécifique (notifyUser) avant d'implémenter
+- [Page Communication — tickets & notifications](project_communication.md) — Architecture 3 volets, filtres chips multi-select, dropdowns portal, droits, jobs autoCleanNotifs + autoTicketStatus, pièges React (is_admin=0, msg-img-wrap)
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul.md
new file mode 100644
index 0000000..f41e47c
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul.md
@@ -0,0 +1,33 @@
+---
+name: feedback_adjustsimul
+description: "Règles complètes pour adjustSimulForActuals — capital effectif, capital soldé, reprocess"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: cce8aa12-fe3a-4aa6-a101-357b5ce867e8
+---
+
+`adjustSimulForActuals` dans `backend/src/utils/schedule.js` est le point central déclenché après chaque remboursement. Trois règles à toujours respecter :
+
+**1. Capital effectif = initial + réinvestissements**
+
+**Why:** Après l'ajout des réinvestissements (mai 2026), la projection s'affichait correctement après un réinvestissement, mais dès qu'un remboursement était saisi, `adjustSimulForActuals` recalculait sur `montant_investi` seul, effaçant l'impact.
+
+**How to apply:** À chaque nouvelle fonctionnalité modifiant le capital réel (réinvestissement, abondement…), vérifier et adapter :
+1. `adjustSimulForActuals` dans `schedule.js`
+2. `syncInvestissementStatut` dans `remboursements.js`
+3. Le bouton ↺ (recalcul manuel) dans `simul.js`
+
+**2. Capital soldé (remainingCapital <= 0)**
+
+**Why:** Sans correction, l'échéance courante gardait `capital_prevu = 0` pour les prêts in fine, et des lignes à 0,00 € restaient dans le tableau au lieu d'être supprimées.
+
+**How to apply:** Quand `remainingCapital <= 0` :
+- L'échéance courante (date_prevue <= last_date) reçoit `capital_prevu = capitalAtLastDate`
+- Les échéances futures (date_prevue > last_date) sont **supprimées** (DELETE), pas mises à zéro
+
+**3. Route /reprocess doit appeler adjustSimulForActuals**
+
+**Why:** Sans cet appel, recalculer les champs fiscaux en masse ne mettait pas à jour les échéanciers.
+
+**How to apply:** La route `POST /api/remboursements/reprocess` doit sélectionner `r.investissement_id`, collecter les IDs dans un `Set` pendant la transaction, puis itérer avec `adjustSimulForActuals(db, invId)` après le `transaction()()`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul_capital_solde.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul_capital_solde.md
new file mode 100644
index 0000000..5562007
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_adjustsimul_capital_solde.md
@@ -0,0 +1,17 @@
+---
+name: feedback_adjustsimul_capital_solde
+description: "Comportement de adjustSimulForActuals quand le capital est intégralement soldé avant l'échéance finale"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 054dabdd-2751-4898-95ba-26cc36f7e7e6
+---
+
+Quand `remainingCapital <= 0` dans `adjustSimulForActuals` (capital entièrement remboursé) :
+
+1. **L'échéance courante** (date_prevue <= last_date, la plus récente) doit recevoir `capital_prevu = capitalAtLastDate` — c'est là que le capital a réellement été soldé.
+2. **Les échéances futures** (date_prevue > last_date) doivent être **supprimées** (DELETE), pas mises à zéro.
+
+**Why:** Avant correction, la fonction ne touchait pas l'échéance courante (capital_prevu restait à 0 pour les prêts in fine) et laissait des lignes à 0,00 € dans le tableau de projection au lieu de les supprimer.
+
+**How to apply:** Toute modification de `adjustSimulForActuals` dans `backend/src/utils/schedule.js` doit préserver ce comportement dans le branchement `remainingCapital <= 0`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_dbindex_export_duplicate.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_dbindex_export_duplicate.md
new file mode 100644
index 0000000..db61c17
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_dbindex_export_duplicate.md
@@ -0,0 +1,15 @@
+---
+name: feedback_dbindex_export_duplicate
+description: db/index.js avait export default db en milieu de fichier — les migrations ajoutées après créaient un doublon
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: e9aff1a4-9c1c-4cab-b7ec-90146b832fb0
+---
+
+`backend/src/db/index.js` a un `export default db;` placé **au milieu du fichier** (ligne ~942, après un groupe de migrations). Lorsqu'on ajoute des migrations à la fin du fichier, Node lève `SyntaxError: Identifier '.default' has already been declared`.
+
+**Règle :** Avant d'ajouter quoi que ce soit à la fin de `db/index.js`, vérifier avec `grep -n "export default db"` qu'il n'y a qu'une seule occurrence. Si deux occurrences existent, supprimer la première (celle en milieu de fichier) et garder uniquement celle tout à la fin.
+
+**Why:** La première occurrence est un artefact d'une ancienne organisation du fichier ; les migrations continuent après elle.
+**How to apply:** Vérification systématique après toute modification de ce fichier. Utiliser Python (pas sed) pour l'édition. [[feedback_file_truncation]]
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_differe_date_calcul.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_differe_date_calcul.md
new file mode 100644
index 0000000..7e79346
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_differe_date_calcul.md
@@ -0,0 +1,37 @@
+---
+name: feedback-differe-date-calcul
+description: "Bug corrigé — recalcul date_premiere_echeance lors du passage au type 'differe' dans les formulaires investissement"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 43bfd5e1-0e82-496d-bdd2-437e92339323
+---
+
+Quand l'utilisateur change le type vers 'differe' alors que `duree_mois` est déjà renseigné, l'ancienne logique gardait la `date_premiere_echeance` issue du calcul in_fine (souscription + 1 mois) au lieu de recalculer pour un différé (souscription + durée totale). Seul `date_cible` était mis à jour.
+
+**Why:** Pour un prêt différé, `date_premiere_echeance` = date d'échéance unique = souscription + duree_mois. Si l'utilisateur sélectionne 'differe' après avoir saisi la durée pour un in_fine, la date affichée était visuellement cohérente mais erronée.
+
+**How to apply:** Dans les deux formulaires (Investissements.jsx et InvestissementDetail.jsx), le onChange du select `type_remb` doit, lors du passage à 'differe', **toujours recalculer** date_premiere_echeance si date_souscription ET duree_mois sont disponibles — et ne garder l'ancienne valeur qu'en fallback si duree_mois est absent. Ne pas inverser l'ordre des conditions (duree_mois check en premier).
+
+```js
+if (t === 'differe') {
+ if (form.date_souscription && form.duree_mois) {
+ const dpe = addMonthsFE(form.date_souscription, 1);
+ const cible = addMonthsFE(dpe, Number(form.duree_mois) - 1);
+ next.date_premiere_echeance = cible;
+ next.date_cible = cible;
+ } else if (form.date_premiere_echeance) {
+ next.date_cible = form.date_premiere_echeance;
+ }
+}
+```
+
+Validation ajoutée dans les deux submit handlers :
+```js
+if (form.type_remb === 'differe' && !form.date_premiere_echeance) {
+ setErr("La date d'échéance est requise pour un prêt différé.");
+ return;
+}
+```
+
+Label du champ rendu dynamique : "Date d'échéance (versement unique) *" quand type=differe, "Date 1ère échéance *" sinon.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_express_route_order.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_express_route_order.md
new file mode 100644
index 0000000..46abc87
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_express_route_order.md
@@ -0,0 +1,14 @@
+---
+name: feedback-express-route-order
+description: Les routes statiques Express doivent être déclarées avant les routes paramétrées /:id
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
+---
+
+Les routes statiques comme `/export` ou `/import-zip` doivent toujours être enregistrées **avant** les routes paramétrées `/:id` dans Express. Sinon, Express interprète `/export` comme `/:id` avec `id = "export"`.
+
+**Why:** Bug rencontré lors de l'ajout des routes export/import dans referentiel.js — les routes ont d'abord été ajoutées en fin de fichier (après `/:id`), ce qui les rendait inaccessibles.
+
+**How to apply:** Quand on ajoute une nouvelle route statique à un router qui a déjà `/:id`, vérifier l'ordre. Utiliser un ancre précis pour l'insertion (ex : le commentaire `// ── GET /api/referentiel/:id ──`). Toujours vérifier avec `grep -n "router.get\|router.post"` après insertion.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_fieldset_checkbox_css.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_fieldset_checkbox_css.md
new file mode 100644
index 0000000..564c588
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_fieldset_checkbox_css.md
@@ -0,0 +1,17 @@
+---
+name: feedback_fieldset_checkbox_css
+description: Pièges CSS globaux — fieldset min-width et input width:100% sur checkboxes
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 6ab3db95-bde5-486d-8d53-7e58134a13ca
+---
+
+Deux pièges CSS globaux dans ce projet :
+
+1. **`fieldset` déborde de son conteneur** : le navigateur applique `min-width: min-content` par défaut aux fieldsets, ce qui permet au contenu de dépasser la modale ou le conteneur parent. Toujours ajouter `minWidth: 0` en style inline sur tout `
`.
+
+2. **Checkbox prend toute la largeur** : le CSS global `input { width: 100% }` s'applique aussi aux `input[type="checkbox"]`, les forçant sur toute la largeur et cassant l'alignement inline. Toujours ajouter `width: 'auto'` en style inline sur tout `input[type="checkbox"]`.
+
+**Why:** Découvert lors de l'implémentation de TciModal (section Admin 2047). Plusieurs tentatives ratées avec flexbox avant d'identifier les causes racines.
+**How to apply:** Systématiquement sur tout nouveau formulaire contenant des fieldsets ou des checkboxes dans ce projet.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_file_truncation.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_file_truncation.md
new file mode 100644
index 0000000..ac59457
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_file_truncation.md
@@ -0,0 +1,32 @@
+---
+name: troncature-et-corruption-fichiers-toujours-passer-par-python
+description: Edit/Write tronquent les fichiers >~35 Ko ou introduisent des octets nuls ; sed -i tronque aussi les JSX ; toujours utiliser python3 via bash
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: cce8aa12-fe3a-4aa6-a101-357b5ce867e8
+---
+
+## ⚠️ RÈGLE ABSOLUE : **NE JAMAIS utiliser Edit, Write ou sed sur les fichiers backend ou frontend volumineux. TOUJOURS utiliser python3 via mcp__workspace__bash, sans exception.**
+
+Les outils `Edit`, `Write` et `sed -i` peuvent corrompre les fichiers JSX/JS volumineux de deux façons :
+1. **Troncature** : le contenu est coupé en cours d'écriture au-delà de ~35 Ko, sans avertissement.
+2. **Octets nuls** : des `\x00` sont ajoutés en fin de fichier, causant une erreur Vite `Unexpected character ''`.
+
+**Why:** Plusieurs fichiers clés ont été affectés (dashboard.js, fiscal2778.js, InvestissementDetail.jsx, DepotsRetraits.jsx). Un `sed -i` de remplacement de couleurs hex en session 2026-06-01 a tronqué Cerfa2561Preview.jsx à mi-fichier sans git pour récupérer. La troncature de `db/index.js` a aussi laissé une migration incomplète (`fiscalite_locale`) réparée manuellement.
+
+**How to apply:**
+- Toujours utiliser `python3` via `mcp__workspace__bash` pour écrire ou modifier des fichiers volumineux (pages JSX, routes backend complexes) — jamais `sed -i`
+- Pattern sûr : `python3 -c "content = open(path).read(); content = content.replace(old, new); open(path,'w').write(content)"`
+- Après chaque écriture, vérifier avec `python3 -c "open(path,'rb').read()[-50:].hex()"` — des `00` en fin signalent des octets nuls
+- Nettoyer les octets nuls : `raw = open(path,'rb').read(); open(path,'wb').write(raw.rstrip(b'\x00'))`
+- Vérifier les fichiers backend avec `node --check` après modification
+- Pour le frontend, vérifier avec `tail -5` que les fermetures JSX sont correctes
+- **InvestissementDetail.jsx spécifiquement** : la fin du fichier (menus contextuels) se tronque régulièrement. Chercher `grep -n "^ "` et réécrire la queue via python3.
+- **Troncature lors de passes multiples** : même avec python3, enchaîner plusieurs scripts indépendants lisant/écrivant le même fichier peut tronquer la fin (confirmé en session 2026-06-13 sur referentiel.js ~46 Ko). Le fichier semblait intact après chaque passe mais était corrompu en git. Pattern sûr : **un seul script Python qui lit depuis `git show :path` via `subprocess`, applique TOUS les patches en mémoire, écrit une seule fois, puis relit et vérifie `assert written == content` et `'export default router' in content`**. Après CHAQUE write, vérifier `tail -5`.
+- **`content.replace()` avec caractères spéciaux** : les fichiers JSX contiennent des espaces insécables (U+202F), flèches et autres Unicode. Le `replace()` Python échoue silencieusement si l'old string ne matche pas exactement — préférer la manipulation par numéros de ligne (`lines[i] = new_line`).
+- **Cerfa2778Preview.jsx** : particulièrement sensible aux troncatures (U+202F dans les templates). Toujours travailler par index de lignes et vérifier `tail -5` après chaque écriture.
+- **DepotsRetraits.jsx et Remboursements.jsx** : troncatures en session 2026-05-31 lors de l'ajout des TrendBadge — reconstruire depuis les patterns des autres pages.
+- Si le backend refuse de démarrer avec "Unexpected end of input" : `xxd | tail -4` pour diagnostiquer, puis reconstruire via python3.
+- **auth.js et admin.js** : troncatures confirmées en session 2026-06-15 lors d'instrumentation via Edit (python3 replace). Pour ces fichiers, utiliser exclusivement python3 en mode binaire (`open(path,'rb')` / `open(path,'wb')`) pour éviter les erreurs d'encodage UTF-8 sur les accents. Restaurer le `export default router;` final en dernier si manquant.
+- **Récupération depuis git** : `git checkout HEAD -- file` est bloqué ("Operation not permitted") dans le sandbox. Pattern de récupération : `git show HEAD:path/to/file > /tmp/restored.js && python3 -c "import shutil; shutil.copy('/tmp/restored.js', 'path/to/file')"`. Confirmer avec `wc -l`. Incident admin.js 2026-06-16 : tronqué à 414 lignes (sur 672), récupéré via ce pattern.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_investisseur_for_plat.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_investisseur_for_plat.md
new file mode 100644
index 0000000..9a03a0f
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_investisseur_for_plat.md
@@ -0,0 +1,24 @@
+---
+name: Pattern investisseurForPlat
+description: Dans tout formulaire avec un select plateforme, synchroniser automatiquement le détenteur quand la plateforme change
+type: feedback
+originSessionId: b87fd892-4ff2-4b8b-b3e6-f0f92e303c8e
+---
+Quand un formulaire propose un select "Plateforme" et un select "Détenteur", le choix de la plateforme doit automatiquement mettre à jour le détenteur avec `investisseur_id` de la plateforme sélectionnée.
+
+**Why:** Sans cette synchronisation, l'utilisateur doit changer manuellement le détenteur après avoir choisi la plateforme — comportement déroutant signalé sur DepotsRetraits et Investissements.
+
+**How to apply:** Dans chaque page concernée, ajouter ce helper (après `defaultInvestisseurId`) :
+
+```js
+const investisseurForPlat = (platId) => {
+ const plat = plats.find(p => String(p.id) === String(platId));
+ return plat?.investisseur_id ? String(plat.investisseur_id) : defaultInvestisseurId();
+};
+```
+
+L'utiliser dans :
+1. Le `onChange` du select plateforme → `setForm(f => ({ ...f, plateforme_id: platId, investisseur_id: investisseurForPlat(platId) }))`
+2. Toutes les initialisations du formulaire (`openNew`, les `useEffect` qui ouvrent la modal avec `?new=1`)
+
+Pages déjà corrigées : `DepotsRetraits.jsx`, `Investissements.jsx`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_menu_actions_format.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_menu_actions_format.md
new file mode 100644
index 0000000..7248da0
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_menu_actions_format.md
@@ -0,0 +1,20 @@
+---
+name: feedback-menu-actions-format
+description: "Tout nouveau tableau doit utiliser le menu ⋮ avec icônes SVG + label, pas de boutons inline"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 2fef7a75-e0f3-4f7d-9c80-79c3be16af03
+---
+
+Toujours utiliser le menu contextuel ⋮ avec icônes pour les actions sur les lignes de tableau — jamais de boutons "Modifier" / "Supprimer" inline.
+
+**Why:** Olivier l'a explicitement demandé après avoir vu des boutons inline dans le tableau des catégories/secteurs. Le format avec icônes SVG (IconEdit, IconTrash, etc.) a été validé comme référence.
+
+**How to apply:** Dès qu'un nouveau tableau est construit avec des actions par ligne, appliquer le pattern complet de [[project_menu_actions_tables]] :
+- Bouton ⋮ dans la dernière colonne (pas de texte, juste le symbole)
+- Menu `position: fixed` avec backdrop
+- Items : `{ icon: , label: '…', onClick, disabled?, color? }`
+- Rendu : `display: flex, gap: 8` avec `{icon} `
+- Hover : `onMouseEnter/Leave` pour background `var(--surface-2)`
+- Fermeture au scroll via `useEffect`
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_montant_verse_formula.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_montant_verse_formula.md
new file mode 100644
index 0000000..40cf2ec
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_montant_verse_formula.md
@@ -0,0 +1,31 @@
+---
+name: feedback_montant_verse_formula
+description: "Formule du montant versé dans les formulaires remboursement — conditionnel isIndicatif, style champs calculés"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 1035faf3-2491-41aa-85f7-cfaabbeda8da
+---
+
+Le champ "Montant versé" dans les formulaires remboursement doit être conditionnel selon `isIndicatif` :
+
+```js
+const montantRembourse = isIndicatif
+ ? round2(capital + cashback + interets_bruts) // plateforme étrangère ou exonéré : brut versé intégralement
+ : netRecu; // plateforme française flat_tax : net après prélèvements
+```
+
+`isIndicatif = isExonere || currentPlat?.fiscalite !== 'flat_tax'`
+
+**Pourquoi :** bug découvert en juin 2026 — `InvestissementDetail.jsx` utilisait toujours `interets_bruts` au lieu de brancher sur `isIndicatif`, contrairement à `Remboursements.jsx` qui avait le bon pattern. Pour une plateforme française, la flat_tax est prélevée à la source, donc le versement réel est diminué des prélèvements.
+
+**Comment appliquer :** pour tout nouveau formulaire remboursement, vérifier que `montantRembourse`/`netRecu` est calculé avec cette logique conditionnelle. `Remboursements.jsx` (lignes ~1422-1424) est la référence correcte.
+
+## Style champs calculés / en lecture seule
+
+Utiliser systématiquement :
+```jsx
+style={{ background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' }}
+```
+
+Ne **jamais** utiliser `var(--bg-input-readonly)` — cette variable CSS n'est pas définie dans le thème.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_multi_detenteur.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_multi_detenteur.md
new file mode 100644
index 0000000..e100bc0
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_multi_detenteur.md
@@ -0,0 +1,25 @@
+---
+name: Pattern multiDetenteur pour les listes de plateformes
+description: Le nom du détenteur ne s'affiche dans les selects plateforme que si plusieurs détenteurs distincts existent
+type: feedback
+originSessionId: 680bfd99-577f-454e-b5b8-ea58460277f9
+---
+Dans tous les composants affichant une liste déroulante de plateformes, le détenteur (`investisseur_nom`) est affiché de façon conditionnelle — uniquement si au moins deux détenteurs distincts sont déclarés sur l'ensemble des plateformes.
+
+**Pattern à appliquer :**
+
+```js
+// Calculé une fois dans le composant, depuis le tableau de plateformes
+const multiDetenteur = new Set(plats.map(p => p.investisseur_id)).size > 1;
+
+// Dans le JSX
+{plats.map(p => (
+
+ {p.nom}{multiDetenteur && p.investisseur_nom ? ` — ${p.investisseur_nom}` : ''}
+
+))}
+```
+
+**Why:** Si toutes les plateformes appartiennent au même détenteur, afficher le nom en permanence est du bruit inutile. La distinction n'a de sens que dans un contexte multi-membres/multi-entités.
+
+**How to apply:** Chaque page/composant qui charge `plats` ou `plateformes` et affiche un select doit déclarer `multiDetenteur` et l'appliquer sur toutes ses listes de plateformes — filtres ET modales. Pages concernées : `Investissements.jsx`, `InvestissementDetail.jsx`, `DepotsRetraits.jsx`, `Remboursements.jsx`, `Imports.jsx`, `Settings.jsx` (composants `ImportsSection` et `NotationPanel`).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_notifications_audience.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_notifications_audience.md
new file mode 100644
index 0000000..2601f09
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_notifications_audience.md
@@ -0,0 +1,17 @@
+---
+name: feedback-notifications-audience
+description: "Avant de créer une notification, toujours demander si elle doit être visible par tous les admins ou seulement par un utilisateur spécifique"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 19a3ce4d-7c1a-418b-9ca2-65b6157249db
+---
+
+Avant d'implémenter toute nouvelle notification, toujours poser la question : **cette notification doit-elle être vue par l'ensemble des administrateurs, ou seulement par un utilisateur spécifique ?**
+
+**Why:** Le système stocke une notification par `user_id`. `notifyAdmins()` insère une ligne par admin. Si la notification n'est pas destinée à tous les admins, il ne faut pas utiliser `notifyAdmins()` — il faut cibler le `user_id` concerné avec `notifyUser()`.
+
+**How to apply:**
+- Réponse "tous les admins" → utiliser `notifyAdmins(title, body, link)`
+- Réponse "non / utilisateur spécifique" → utiliser `notifyUser(userId, title, body, link)`
+- Ne jamais présumer l'audience : toujours demander avant de coder.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_onclick_default_param.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_onclick_default_param.md
new file mode 100644
index 0000000..a373ede
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_onclick_default_param.md
@@ -0,0 +1,11 @@
+---
+name: onClick direct sur une fonction avec paramètre par défaut
+description: Utiliser onClick={fn} passe l'événement React comme premier argument, écrasant les valeurs par défaut ; toujours utiliser onClick={() => fn()}
+type: feedback
+originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
+---
+Quand une fonction a des paramètres par défaut (ex. `openReinvModal(tab = 'manuel')`), l'écrire directement dans un handler React (`onClick={openReinvModal}`) fait que React passe l'objet événement comme premier argument, écrasant la valeur par défaut.
+
+**Why:** Bug découvert sur `openReinvModal` — le modal s'ouvrait parfois sur l'onglet "Automatique" au lieu de "Manuel" parce que `tab` recevait un SyntheticEvent au lieu de `'manuel'`.
+
+**How to apply:** Toujours envelopper dans une lambda : `onClick={() => fn()}` ou `onClick={() => fn(arg)}`. Ne jamais écrire `onClick={fn}` si `fn` a des paramètres avec valeur par défaut qu'on ne veut pas écraser.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_otplib_esm.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_otplib_esm.md
new file mode 100644
index 0000000..14bc57e
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_otplib_esm.md
@@ -0,0 +1,22 @@
+---
+name: feedback_otplib_esm
+description: "otplib v12+ n'exporte pas authenticator — utiliser l'API fonctionnelle ESM"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: e9aff1a4-9c1c-4cab-b7ec-90146b832fb0
+---
+
+`otplib` (version installée dans le projet) n'expose pas de named export `authenticator` en ESM. L'import `{ authenticator } from 'otplib'` lève `SyntaxError: does not provide an export named 'authenticator'`. Même `createRequire` échoue car le package a changé d'API.
+
+**Utiliser l'API fonctionnelle** directement :
+```js
+import { generateSecret as totpGenerateSecret, generateURI as totpGenerateURI, verifySync as totpVerifySync } from 'otplib';
+
+const secret = totpGenerateSecret();
+const uri = totpGenerateURI({ issuer, label: email, secret });
+const valid = totpVerifySync({ token: code, secret, strategy: 'totp' });
+```
+
+**Why:** Le package a migré vers une API fonctionnelle (v12+), l'ancienne interface orientée objet (`authenticator`) a été supprimée.
+**How to apply:** Toujours utiliser ces 3 imports nommés ESM pour toute fonctionnalité TOTP.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_pageicon_topbar.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_pageicon_topbar.md
new file mode 100644
index 0000000..4b94a06
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_pageicon_topbar.md
@@ -0,0 +1,20 @@
+---
+name: feedback-pageicon-topbar
+description: Ne jamais modifier display ou ajouter de wrapper dans .topbar pour les icônes de titre de page
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 743b204f-7d6b-45ed-b91c-387060b5941e
+---
+
+Ne pas modifier la structure flex du `.topbar` pour y insérer des icônes.
+
+**Why:** `.topbar` est `display: flex; justify-content: space-between`. Ajouter un wrapper div ou mettre `display: flex` sur le `h2` change le nombre/type de flex items, ce qui casse le layout en cascade (KPIs `dr-kpi-row`, tabs `dr-tabs`, et même `account-layout` sur d'autres pages non modifiées).
+
+**How to apply:** Insérer l'icône comme ` ` inline directement dans le `h2` existant, sans toucher à sa structure ni à son `display`. Utiliser `style={{ display: 'inline', verticalAlign: 'middle', marginRight: 10 }}` sur l'img. Le `h2` reste un élément block normal, l'img est du contenu inline — zéro interaction avec flex/grid.
+
+```jsx
+ Tableau de bord
+```
+
+Le composant `PageIcon` est dans `frontend/src/components/PageIcon.jsx` avec un cache module-level (un seul appel `/api/icons` pour toutes les pages). Taille par défaut : 40px.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_projection_no_double_count.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_projection_no_double_count.md
new file mode 100644
index 0000000..b6080c7
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_projection_no_double_count.md
@@ -0,0 +1,22 @@
+---
+name: feedback_projection_no_double_count
+description: Pas de guard real===0 dans buildCellValue — le backend filtre déjà les projections par NOT EXISTS par investissement
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: b8b23026-381b-4ec2-b150-02080d5f4720
+---
+
+Le backend (`dashboard/interets-par-plateforme`) filtre les projections avec `NOT EXISTS` **au niveau investissement** : `projections[mois]` ne contient que les projets qui n'ont pas encore reçu de remboursement réel ce mois. Il n'y a donc pas de risque de double-comptage.
+
+```js
+// CORRECT — le filtre NOT EXISTS backend garantit l'absence de doublon
+if (showProjected && proj) { /* ajouter proj */ }
+
+// INCORRECT — bloque l'affichage des projections sur les projets non encore remboursés
+// dès qu'un autre projet de la même plateforme a été remboursé ce mois
+if (showProjected && proj && real === 0) { /* ajouter proj */ }
+```
+
+**Why:** Maclear en juin 2026 : 7 projets remboursés (real > 0) + 9 projets encore en attente (dans projections). Le guard `real === 0` masquait à tort les 9 projections.
+**How to apply:** Ne PAS ajouter `real === 0` dans `buildValue` / `buildCellValue`. Corrigé dans `Plateformes.jsx` et `TableauInteretsPlateforme.jsx`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_put_payload_minimal.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_put_payload_minimal.md
new file mode 100644
index 0000000..45d0d52
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_put_payload_minimal.md
@@ -0,0 +1,14 @@
+---
+name: feedback-put-payload-minimal
+description: "Ne jamais spread l'objet inv pour un PUT investissements — construire un payload minimal champ par champ"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 06bf2c9d-6350-45bd-a65f-4399f1218408
+---
+
+Ne jamais faire `{ ...inv, champModifie: valeur }` pour un `PUT /investissements/:id`.
+
+**Why:** L'objet `inv` côté frontend contient des champs extra (`simul`, `remboursements`, `auto_reinvest`, `fiscalite_override`, `plateforme_nom`, etc.) absents du schéma Zod backend. Certains ont des valeurs invalides pour le schéma (ex. arrays, objets imbriqués), ce qui provoque une "Validation error" même si le champ modifié est correct.
+
+**How to apply:** Toujours construire le payload explicitement avec uniquement les champs attendus par le schéma Zod de la route (voir `backend/src/routes/investissements.js` — `const Schema = z.object({...})`). Utiliser `|| undefined` pour les champs optionnels string et `?? null` pour les champs nullable.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reinvest_adjustsimul.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reinvest_adjustsimul.md
new file mode 100644
index 0000000..7030bee
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reinvest_adjustsimul.md
@@ -0,0 +1,14 @@
+---
+name: adjustSimulForActuals doit tenir compte des réinvestissements
+description: Bug constaté — après saisie d'un remboursement, la projection ignorait les réinvestissements et revenait au capital initial
+type: feedback
+originSessionId: 680bfd99-577f-454e-b5b8-ea58460277f9
+---
+Ne jamais oublier qu'`adjustSimulForActuals` est le point central déclenché après chaque remboursement. Si une nouvelle notion modifie le capital effectif d'un prêt, cette fonction doit en être informée.
+
+**Why:** En mai 2026, après l'ajout des réinvestissements, la projection s'affichait correctement juste après un réinvestissement (car `generateSimulWithReinvestissements` était appelée). Mais dès qu'un remboursement était saisi, `adjustSimulForActuals` recalculait sur `montant_investi` seul, effaçant l'impact du réinvestissement.
+
+**How to apply:** À chaque fois qu'une nouvelle fonctionnalité modifie le capital réel d'un prêt (réinvestissement, abondement, etc.), vérifier et adapter les trois points suivants :
+1. `adjustSimulForActuals` dans `schedule.js`
+2. `syncInvestissementStatut` dans `remboursements.js`
+3. Le bouton ↺ (recalcul manuel) dans `simul.js` — il passe par `adjustSimulForActuals`, donc hérite automatiquement des corrections.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_remb_methode_fallback.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_remb_methode_fallback.md
new file mode 100644
index 0000000..1c08e4f
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_remb_methode_fallback.md
@@ -0,0 +1,21 @@
+---
+name: feedback_remb_methode_fallback
+description: openRembFromSimul et openNewRemb doivent utiliser inv.methode_remboursement comme fallback quand la plateforme est choix_investisseur
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 5682d634-7345-40a8-8184-f617d1dd8444
+---
+
+Quand `plat.methode_remboursement === 'choix_investisseur'`, les fonctions `openRembFromSimul` et `openNewRemb` dans **InvestissementDetail.jsx** doivent utiliser `inv.methode_remboursement` comme valeur par défaut — pas `'portefeuille'`.
+
+Pattern correct :
+```js
+const methode = plat && plat.methode_remboursement !== 'choix_investisseur'
+ ? plat.methode_remboursement
+ : (inv?.methode_remboursement || 'portefeuille');
+```
+
+**Why:** Sans ce correctif, tous les remboursements saisis depuis la fiche détail d'un investissement en `compte_courant` étaient enregistrés en `portefeuille`, ce qui empêchait la création du retrait automatique dans `depots_retraits`.
+
+**How to apply:** Vérifier ce pattern à chaque fois qu'on modifie ou crée une fonction d'ouverture du formulaire de remboursement. Voir [[project_methode_remboursement_investissement]].
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reprocess_adjustsimul.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reprocess_adjustsimul.md
new file mode 100644
index 0000000..981983c
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_reprocess_adjustsimul.md
@@ -0,0 +1,14 @@
+---
+name: feedback_reprocess_adjustsimul
+description: La route /reprocess doit appeler adjustSimulForActuals après la transaction fiscale
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 054dabdd-2751-4898-95ba-26cc36f7e7e6
+---
+
+La route `POST /api/remboursements/reprocess` doit appeler `adjustSimulForActuals(db, invId)` pour chaque `investissement_id` distinct touché, après la transaction de mise à jour fiscale.
+
+**Why:** Sans cet appel, recalculer les champs fiscaux en masse ne mettait pas à jour les tableaux de projection — les remboursements de capital n'étaient donc plus reflétés dans les échéanciers après un reprocess.
+
+**How to apply:** La requête SQL du reprocess doit sélectionner `r.investissement_id`. Collecter les IDs dans un `Set` pendant la transaction, puis itérer avec `adjustSimulForActuals` après le `transaction()()`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_result_banner.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_result_banner.md
new file mode 100644
index 0000000..cfaaa6e
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_result_banner.md
@@ -0,0 +1,28 @@
+---
+name: feedback-result-banner
+description: Toute bannière succès/erreur doit utiliser ResultBanner avec × à droite et auto-dismiss 4s
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
+---
+
+Utiliser le composant `ResultBanner` (`frontend/src/components/ResultBanner.jsx`) pour **toute** bannière de résultat succès/erreur dans l'app.
+
+**Règle** : jamais de div inline avec `result.ok ? 'rgba...'`. Toujours ` setResult(null)} />`.
+
+**Comportement garanti par le composant** :
+- × positionné à l'extrême droite (`justifyContent: space-between`)
+- Auto-dismiss après 4 secondes (paramètre `delay` configurable)
+- Callback `onDismiss` appelé à la fermeture manuelle ET automatique
+
+**Format du state `result`** : toujours `{ ok: boolean, msg: string }`. Convertir les structures complexes (ex: résultats d'import `{ inserted, total, skipped, errors }`) en message texte au moment du `setResult`.
+
+**Redirect après dismiss** : si la section doit rediriger après fermeture, passer le navigate dans `onDismiss` :
+```jsx
+onDismiss={() => { setResult(null); navigate('/admin/plateformes?section=referentiel'); }}
+```
+
+**Why:** Demande explicite d'Olivier — cohérence visuelle sur toute l'app (× à droite, disparition automatique).
+
+**How to apply:** Vérifier systématiquement `setResult` + affichage résultat dans tout nouveau composant. Si la structure du result n'est pas `{ ok, msg }`, normaliser au `setResult`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_safari_mobile_viewport.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_safari_mobile_viewport.md
new file mode 100644
index 0000000..ff92cac
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_safari_mobile_viewport.md
@@ -0,0 +1,35 @@
+---
+name: feedback-safari-mobile-viewport
+description: "Safari mobile — utiliser 100dvh et visualViewport pour éviter les décalages liés à la barre d'adresse"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 21fbd9de-5545-4f5d-8809-a41863f7cdb6
+---
+
+Sur Safari iOS (iPhone/iPad), `100vh` inclut la barre d'adresse du navigateur, ce qui pousse les éléments en bas de page (sidebar, UserMenu) hors de la zone visible.
+
+**Why:** Safari mobile calcule `100vh` par rapport à la hauteur totale incluant le chrome du navigateur, pas la zone réellement visible. `window.innerHeight` a le même problème pour les calculs JS.
+
+**How to apply — CSS :**
+Toujours doubler les déclarations de hauteur plein-écran :
+```css
+height: 100vh;
+height: 100dvh; /* remplace 100vh sur Safari 15.4+ */
+min-height: 100vh;
+min-height: 100dvh;
+```
+`100dvh` (dynamic viewport height) s'ajuste dynamiquement quand la barre d'adresse apparaît/disparaît.
+
+**How to apply — JS (calculs de position type `position: fixed`) :**
+Remplacer `window.innerHeight` par `window.visualViewport.height` :
+```js
+const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight;
+```
+Et écouter les événements du visualViewport si le menu doit rester calé pendant le scroll :
+```js
+window.visualViewport.addEventListener('resize', computePosition);
+window.visualViewport.addEventListener('scroll', computePosition);
+```
+
+Appliqué sur : `.sidebar` (height), `.app-shell` (min-height), `UserMenu.computePosition`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sed_truncation.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sed_truncation.md
new file mode 100644
index 0000000..dda3320
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sed_truncation.md
@@ -0,0 +1,14 @@
+---
+name: feedback-sed-truncation
+description: Ne jamais utiliser sed pour remplacer des couleurs dans les fichiers JSX — risque de troncature
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 0922bde7-84ed-45dc-b233-50bf0be5ad3e
+---
+
+Ne jamais utiliser `sed` pour des remplacements de couleurs (ou autres chaînes) dans les fichiers JSX volumineux.
+
+**Why:** En session 2026-06-01, un `sed -i` de remplacement de couleurs hex a tronqué `Cerfa2561Preview.jsx` à 271 lignes (sur ~443), coupant le fichier en plein milieu d'une chaîne JSX. Il n'y avait pas de git pour récupérer, ce qui a nécessité une reconstruction manuelle depuis l'historique de la conversation.
+
+**How to apply:** Pour les remplacements en masse dans les fichiers JSX, toujours utiliser `python3` via bash avec lecture/écriture du fichier complet, ou l'outil `Edit` avec `replace_all: true` pour des remplacements ciblés. Vérifier systématiquement `wc -l` et `tail | xxd` après tout remplacement en masse.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sidebar_expand_overlay.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sidebar_expand_overlay.md
new file mode 100644
index 0000000..f1a81e2
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sidebar_expand_overlay.md
@@ -0,0 +1,21 @@
+---
+name: feedback-sidebar-expand-overlay
+description: Fix couleur parasite au survol du logo en sidebar réduite — utiliser background:transparent !important sur .sidebar-expand-overlay
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 21fbd9de-5545-4f5d-8809-a41863f7cdb6
+---
+
+Le bouton `.sidebar-expand-overlay` (position: absolute; inset: 0) provoque une couleur parasite au survol même avec `background: none`, car le navigateur applique ses propres styles par défaut sur `:hover`.
+
+**Why:** Le bouton couvre toute la zone `.sidebar-brand` en mode collapsed. Les styles par défaut du navigateur s'appliquent sur le `:hover` du `` malgré `background: none`.
+
+**How to apply:** Toujours ajouter sur `.sidebar-expand-overlay` :
+```css
+background: transparent !important;
+-webkit-appearance: none; appearance: none;
+outline: none;
+```
+Et les mêmes règles sur `:hover`, `:focus`, `:active`.
+L'effet visuel passe uniquement par `filter: brightness(1.18)` sur `.sidebar-brand-logo` au hover.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sqlite_vacuum_into.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sqlite_vacuum_into.md
new file mode 100644
index 0000000..e71e79a
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_sqlite_vacuum_into.md
@@ -0,0 +1,39 @@
+---
+name: feedback_sqlite_vacuum_into
+description: Ne jamais utiliser db.backup() pour les exports — toujours VACUUM INTO ; valider avec integrity_check avant pending-restore
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 5cf6a868-7a3f-44e5-a0f8-0499f84b1afc
+---
+
+Ne jamais utiliser `db.backup()` pour créer une copie de la DB destinée à être lue comme fichier brut.
+
+**Why:** `db.backup()` (better-sqlite3) copie les pages SQLite, y compris le header qui indique "WAL mode". Le fichier résultant est techniquement en WAL mode mais sans fichier WAL — il est incomplet et provoque `SQLITE_CORRUPT` lors de la réouverture. Incident prod constaté le 2026-06-16 : serveur en boucle de crash après restauration.
+
+**How to apply:**
+
+Pour tout export (route export-full, job autoExport, backup de sécurité dans la route restore) :
+```js
+// ✅ Correct — produit un fichier DELETE-mode propre, sans WAL
+db.exec(`VACUUM INTO '${tmpDb.replace(/'/g, "''")}'`);
+const dbData = fs.readFileSync(tmpDb);
+```
+
+Avant d'écrire le fichier `.pending-restore`, toujours valider :
+```js
+import Database from 'better-sqlite3';
+const tmpValidate = path.join(os.tmpdir(), `cl-validate-${Date.now()}.db`);
+let testDb;
+try {
+ fs.writeFileSync(tmpValidate, dbEntry.data);
+ testDb = new Database(tmpValidate, { readonly: true });
+ const check = testDb.pragma('integrity_check');
+ if (!check || check[0]?.integrity_check !== 'ok') throw new HttpError(500, '...');
+} finally {
+ if (testDb) try { testDb.close(); } catch {}
+ if (fs.existsSync(tmpValidate)) fs.unlinkSync(tmpValidate);
+}
+```
+
+Cette validation est déjà en place dans `backend/src/routes/admin.js` (route restore).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_timezone_lastday.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_timezone_lastday.md
new file mode 100644
index 0000000..2d3a780
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_timezone_lastday.md
@@ -0,0 +1,25 @@
+---
+name: feedback-timezone-lastday
+description: "Ne jamais utiliser .toISOString().split('T')[0] pour calculer une date locale — provoque un décalage d'un jour en UTC+1"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 2aaa93e3-eec2-479c-892a-d5ce8371a99f
+---
+
+Ne jamais calculer le dernier jour d'un mois avec `.toISOString().split('T')[0]`.
+
+**Why:** `new Date(annee, m, 0).toISOString()` convertit en UTC. En France (UTC+1 en hiver), minuit local le 31 déc. = 23h le 30 déc. UTC → la chaîne retournée est `"2024-12-30"` au lieu de `"2024-12-31"`. Les investissements souscrits le dernier jour du mois sont alors exclus à tort des calculs de capital mensuel dans le dashboard.
+
+**How to apply:** Toujours utiliser les méthodes locales pour construire une chaîne de date :
+
+```js
+// ❌ À éviter
+const lastDay = new Date(annee, m, 0).toISOString().split('T')[0];
+
+// ✅ Correct
+const _d = new Date(annee, m, 0);
+const lastDay = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, '0')}-${String(_d.getDate()).padStart(2, '0')}`;
+```
+
+Appliquer ce pattern partout où une date locale est formatée en chaîne `YYYY-MM-DD` à partir d'un objet `Date`. Déjà corrigé dans `backend/src/routes/dashboard.js` (capitalMensuel de `/api/dashboard/interets-par-plateforme`).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_tip_td_closed.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_tip_td_closed.md
new file mode 100644
index 0000000..7cd497f
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_tip_td_closed.md
@@ -0,0 +1,41 @@
+---
+name: feedback_tip_td_closed
+description: Règle de grisage des cellules hors-période dans les tableaux mensuels tip-table
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: b8b23026-381b-4ec2-b150-02080d5f4720
+---
+
+Toute nouvelle page ou composant utilisant un tableau `tip-table` avec des données par investissement doit griser les cellules hors-période via la classe `.tip-td-closed`.
+
+**Règle :** Rendre ` ` (sans contenu, sans tiret) quand :
+- Le mois est **avant** le début de l'investissement (date_souscription ou premier paiement)
+- Le mois est **après** le dernier remboursement pour les prêts `statut === 'rembourse'`
+
+**Pattern de détection :**
+```js
+// Avant (via date_souscription)
+const subYear = Number(inv.date_souscription?.slice(0, 4));
+const subMo = Number(inv.date_souscription?.slice(5, 7)) - 1;
+const isBefore = subYear > displayYear || (subYear === displayYear && mi < subMo);
+
+// Après (via lastRembDateMap)
+const isAfter = inv.statut === 'rembourse' && lastDate && (() => {
+ const lastYear = Number(lastDate.slice(0, 4));
+ const lastMo = Number(lastDate.slice(5, 7)) - 1;
+ if (lastYear < displayYear) return true;
+ if (lastYear === displayYear) return mi > lastMo;
+ return false;
+})();
+```
+
+**CSS défini dans styles.css :**
+```css
+.tip-td-closed { background: var(--surface-2) !important; }
+.tip-td-closed.tip-col-current { background: rgba(109,40,217,0.045) !important; }
+```
+La surbrillance du mois courant reste prioritaire grâce au double sélecteur.
+
+**Why:** Demande utilisateur pour indiquer visuellement qu'il n'y a rien à attendre sur ces mois.
+**How to apply:** Appliquer dans tout nouveau tableau mensuel tip-table dès sa création. Déjà présent dans Plateformes.jsx (onglets Remboursements + Dépôts/Retraits) et InvMensuelTable.jsx.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_tiptable_padding.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_tiptable_padding.md
new file mode 100644
index 0000000..ac89898
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_tiptable_padding.md
@@ -0,0 +1,20 @@
+---
+name: feedback-tiptable-padding
+description: "Le composant TableauInteretsPlateforme doit être enveloppé dans un div padding 0 24px dans chaque page qui l'utilise"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 752b3578-1999-4431-9dd9-73297a9baaf4
+---
+
+`TableauInteretsPlateforme` ne gère pas son propre padding horizontal. Chaque page qui l'intègre doit l'envelopper :
+
+```jsx
+
+```
+
+**Why:** Le composant touche les bords sans ce wrapper. Corrigé dans `Dashboard.jsx` et `Remboursements.jsx` (session 2026-05-31).
+
+**How to apply:** Si le tableau est intégré dans une nouvelle page, ajouter systématiquement ce wrapper.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_usermenu_portal.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_usermenu_portal.md
new file mode 100644
index 0000000..704e5b8
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/feedback_usermenu_portal.md
@@ -0,0 +1,14 @@
+---
+name: feedback-usermenu-portal
+description: "UserMenu popup et sous-panel doivent être rendus via createPortal(…, document.body) pour éviter les problèmes de stacking context avec la sidebar sticky"
+metadata:
+ node_type: memory
+ type: feedback
+ originSessionId: 752b3578-1999-4431-9dd9-73297a9baaf4
+---
+
+Le popup `.user-menu-popup` et le sous-panel `.user-menu-subpanel` de `UserMenu.jsx` sont rendus via `createPortal(…, document.body)`.
+
+**Why:** La sidebar a `position: sticky` qui crée un stacking context. Même avec `position: fixed` et `z-index: 200`, les popups restaient derrière les tableaux `.tip-table` dont les cellules ont `position: sticky; z-index: 1/2`. Le portail sort complètement du DOM de la sidebar, éliminant tout conflit de stacking context.
+
+**How to apply:** Toujours utiliser `createPortal` (importé depuis `react-dom`) pour tout popup/dropdown rendu dans la sidebar ou dans un conteneur avec `overflow` ou `position` non-statique.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_2778sd.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_2778sd.md
new file mode 100644
index 0000000..ef6ca76
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_2778sd.md
@@ -0,0 +1,57 @@
+---
+name: project_2778sd
+description: "Fonctionnalité déclaration mensuelle 2778-SD (PFO sur revenus étrangers) — matrice, simulation CERFA, report 2042, taux dynamiques, paramètre pfoAssujetti"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 984f7812-9585-45f1-be5b-5037de201a05
+---
+
+Onglet **2778-SD** dans la page Fiscalité (TaxReport), conditionné par la préférence `pfoAssujetti`.
+
+**Architecture :**
+- Backend : `GET /api/taxreport/2778` — matrice mensuelle par plateforme étrangère (domiciliation ≠ france), base = `interets_bruts_avant_local` si > 0 sinon `interets_bruts`
+- Frontend : `Cerfa2778Preview.jsx` — deux sous-vues : Matrice mensuelle + Simulation CERFA
+- TaxReport.jsx : onglet 2778-SD visible uniquement si `pfoAssujetti === true`
+
+**Taux dynamiques :**
+- Table `taux_pfu` enrichie : colonnes `csg`, `crds`, `solidarite` (migration dans index.js)
+- Taux 2021-2025 : CSG 9,2 % | CRDS 0,5 % | Solidarité 7,5 % → PS total 17,2 % → PFU 30 %
+- Taux 2026+ : CSG 10,6 % | CRDS 0,5 % | Solidarité 7,5 % → PS total 18,6 % → PFU 31,4 %
+- `pfu.js` calcule `prelev_sociaux` et `pfu_total` automatiquement depuis les 3 composantes
+- `getRatesForYear(annee, pfuList)` dans Cerfa2778Preview : trouve l'année exacte ou la plus proche
+
+**Calcul cases CERFA (notre cas = placement revenu fixe) :**
+- BA = intérêts bruts totaux (arrondi à l'euro)
+- IA = BA × 12,8 % (PFO, acompte non libératoire)
+- PQ = BA × CSG%, PV = BA × CRDS%, PF1 = PQ+PV, PG1 = BA × solidarité%
+- PU = PF1, PK = PG1, QR = IA + PU + PK (montant à payer)
+
+**Report annuel 2042 :**
+- Case 2TR = total BA annuel (intérêts bruts)
+- Case 2BH = idem 2TR (éviter double imposition PS)
+- Case 2CK = total IA annuel (acompte déjà versé, imputable sur IR définitif)
+
+**Paramètre pfoAssujetti :**
+- Stocké dans `user_preferences` (clé `pfo_assujetti`) et `localStorage` (`cl_pfo_assujetti`)
+- Exposé via `UiContext` : `pfoAssujetti`, `setPfoAssujetti`
+- UI dans Settings > Flat Tax (PFU) : section "Fiscalité des plateformes étrangères", bloc accordéon avec toggle (masqué par défaut)
+- Seuils : 25 000 € (célibataire) / 50 000 € (couple) de RFR de l'avant-dernière année
+
+**Sélection plateformes persistée :** `localStorage` clé `cl_2778_excluded_plats` (array d'IDs exclus)
+
+**Structure tableau CerfaBlock (Cerfa2778Preview.jsx) :**
+- Grille 6 colonnes : `1fr 90px 90px 90px 90px 90px` → Libellé | Base imposable (BA) | Taux | Case | Montant | (sans titre, colonne Case SUP)
+- Composant `Row` : props `label, code, value, highlight, note, caseSup, base, taux, noUnit` — `noUnit` supprime le symbole €, `whiteSpace: pre-line` sur le label pour les sauts de ligne
+- Prop `mois` et `annee` passées à CerfaBlock depuis le parent pour afficher "Mai 2026" dans Montant
+- Sections : Page de garde (page 1) | Prélèvement forfaitaire obligatoire non libératoire (page 2) | Prélèvements sociaux (page 3) | Totaux à reporter (page 4)
+- Ligne BA+IA fusionnée : libellé sur 2 lignes (séparateur `\n`), code=IA, value=IA, taux=pfo%
+- Ligne "Mois concerné" : code vide, value=`${MOIS_LABELS[mois]} ${annee}`, noUnit=true
+
+**Settings > Flat Tax (PFU) :**
+- Table : 7 colonnes (Année, PFU total, IR, PS total, └> dont CSG, └> dont CRDS, └> dont Solidarité)
+- Fond indigo léger `rgba(99,102,241,0.06)` sur les 3 colonnes de décomposition
+- Formulaires ajout/édition : saisir IR + CSG + CRDS + Solidarité → PFU total calculé automatiquement
+
+**Why:** Obligation légale déclaration mensuelle PFO pour revenus d'intérêts de plateformes étrangères.
+**How to apply:** Toujours vérifier `pfoAssujetti` avant d'afficher quoi que ce soit lié à la 2778-SD. Les taux viennent de `/api/pfu`, jamais hardcodés.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_2fa.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_2fa.md
new file mode 100644
index 0000000..ae10163
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_2fa.md
@@ -0,0 +1,32 @@
+---
+name: project_2fa
+description: "Implémentation complète du 2FA (TOTP + email OTP) — DB, backend, frontend Login et MonCompte"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: e9aff1a4-9c1c-4cab-b7ec-90146b832fb0
+---
+
+Fonctionnalité 2FA ajoutée en session juin 2026.
+
+**DB (db/index.js)** — colonnes sur `users` : `totp_secret TEXT`, `totp_enabled INTEGER DEFAULT 0`. Tables : `two_fa_sessions` (session temporaire entre /login et /2fa/verify, 5 min), `two_fa_email_codes` (OTP email 6 chiffres, 5 min), `two_fa_trusted_devices` (appareils de confiance 30 jours, avec `user_agent` et `ip_address`).
+
+**Backend (auth.js)** — routes :
+- `POST /login` : si `totp_enabled` → vérifie `deviceToken` (header `X-Device-Token`), sinon retourne `{ requires2FA: true, sessionToken }`
+- `GET /2fa/setup` : génère secret TOTP + QR code base64 (lib `otplib` API fonctionnelle)
+- `POST /2fa/confirm-setup` : vérifie code, active 2FA
+- `POST /2fa/disable` : désactive (mot de passe requis), efface tous les appareils de confiance
+- `POST /2fa/send-email-code` : envoie OTP 6 chiffres par email (validité 5 min)
+- `POST /2fa/verify` : vérifie code (TOTP ou email), émet JWT, crée `two_fa_trusted_devices` si `trustDevice=true`
+- `GET /trusted-devices` : liste appareils de confiance actifs
+- `DELETE /trusted-devices/:id` : révoque un appareil
+- `DELETE /trusted-devices` : révoque tous
+
+**Frontend** :
+- `AuthContext.jsx` : `login(email, password, deviceToken)` retourne `{ requires2FA, sessionToken }` si 2FA actif ; nouvelle fonction `completeLogin(token, user)` appelée après vérification du code
+- `Login.jsx` : machine à 3 états (`form` → `method` → `code`) ; countdown 5 min pour OTP email ; checkbox "Faire confiance 30 jours" ; `cl_device_token` stocké dans localStorage
+- `MonCompte.jsx` > Sécurité : composant `TwoFASection` (QR code setup, clé manuelle, confirmation code, désactivation) + composant `TrustedDevicesSection` (liste appareils, badge APPAREIL ACTUEL, déconnexion individuelle ou tout)
+- `api.js` : `authHeaders()` envoie automatiquement `X-Device-Token` si `cl_device_token` présent
+
+**Why:** Sécurité renforcée, demande utilisateur.
+**How to apply:** Toujours envoyer `X-Device-Token` depuis `authHeaders()` (déjà en place). Ne pas exposer les tokens bruts dans les réponses API.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_admin_sous_pages.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_admin_sous_pages.md
new file mode 100644
index 0000000..fe75deb
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_admin_sous_pages.md
@@ -0,0 +1,25 @@
+---
+name: project_admin_sous_pages
+description: "Architecture des sous-pages Admin — AdminPlateformes, AdminFiscalite et leurs sections"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: da8e6c45-04c2-4db9-8e7f-189d65e7cf99
+---
+
+Trois sous-pages Admin existent sous `/admin/*`, chacune suivant le pattern `account-layout` / `account-sidebar` / `account-content` avec un bouton "← Administration" pour revenir.
+
+**`/admin/plateformes` — AdminPlateformes.jsx**
+NAV : Référentiel | Logos des plateformes | Tags suggérés | Types de garanties | Notation
+- `GarantiesSection` : CRUD garanties (import CSV/JSON, export dropdown)
+- `NotationRefSection` : critères de notation liés aux plateformes du **référentiel** (`/api/referentiel/:id/notation`) — pas aux plateformes utilisateur. Routes CRUD dans `referentiel.js`.
+
+**`/admin/fiscalite` — AdminFiscalite.jsx**
+NAV : Flat Tax (PFU) | 2047 — Crédit d'impôts
+Section par défaut : `pfu`
+- `PfuSection` : composant autonome (state + handlers + modals intégrés), charge `/api/pfu`
+- `TauxCreditImpotSection` : taux crédit d'impôt 2047 (124 pays), charge `/api/taux-credit-impot`
+
+**Why:** Les sections PFU, garanties et notation ont été déplacées hors de Settings car elles relèvent de la configuration administrative et non des préférences utilisateur.
+
+**How to apply:** Toute nouvelle section de référentiel admin → ajouter dans AdminPlateformes ou AdminFiscalite selon la nature. Ne pas recreer ces sections dans Settings.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_aide.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_aide.md
new file mode 100644
index 0000000..73cfbfb
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_aide.md
@@ -0,0 +1,22 @@
+---
+name: Page Centre d'aide
+description: Page /aide avec layout Settings, section FAQ accordéon, bouton dans UserMenu
+type: project
+originSessionId: c7a8bfe3-1a9f-476a-be50-c4f412629322
+---
+Page d'aide accessible via le menu utilisateur (sous "Paramètres").
+
+**Fichiers créés / modifiés :**
+- `frontend/src/pages/Aide.jsx` — page créée (layout `account-layout`, même pattern que Settings)
+- `frontend/src/App.jsx` — route `/aide` ajoutée
+- `frontend/src/components/UserMenu.jsx` — bouton "Aide" + `IconAide` ajoutés sous "Paramètres"
+
+**Structure de la page :**
+- Titre sidebar : "Centre d'aide"
+- Navigation par `?section=` (défaut : `faq`)
+- Section FAQ avec composant `FaqItem` (accordéon dépliable, chevron animé)
+
+**FAQ existante (section `faq`) :**
+1. "Comment est calculé le solde du porte-monnaie d'une plateforme ?" — décrit la formule complète : dépôts − retraits manuels + remboursements (selon fiscalité plateforme) + bonus − capital investi + corrections.
+
+**How to apply :** Pour ajouter une nouvelle entrée FAQ, ajouter un `` dans la section `faq` de `Aide.jsx`. Pour ajouter une nouvelle section, ajouter une entrée dans le tableau `NAV` et un bloc conditionnel `{section === 'xxx' && ...}` dans le contenu.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_auth_email.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_auth_email.md
new file mode 100644
index 0000000..0e64dce
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_auth_email.md
@@ -0,0 +1,40 @@
+---
+name: project_auth_email
+description: "Vérification email, mot de passe oublié, pages auth — architecture complète"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: e9aff1a4-9c1c-4cab-b7ec-90146b832fb0
+---
+
+Fonctionnalités auth ajoutées (session juin 2026, avant le 2FA).
+
+**DB** — migrations dans `db/index.js` :
+- `users.email_verified INTEGER NOT NULL DEFAULT 1` (DEFAULT 1 pour ne pas bloquer les comptes existants ; 0 pour les nouveaux si SMTP actif)
+- Table `email_verification_tokens` (token, expires_at 24h, used)
+- Table `password_reset_tokens` (token, expires_at 1h, used)
+- Table `smtp_config` (id=1, enabled, host, port, secure, email, username, password, allow_unauth, app_name, app_url)
+
+**Backend (auth.js)** — routes :
+- `POST /register` : premier user auto-vérifié ; si SMTP actif → envoie email bienvenue + `{ requiresVerification: true }`
+- `POST /login` : bloque si `email_verified=0`, code `EMAIL_NOT_VERIFIED`
+- `GET /verify-email?token=` : valide le token, set `email_verified=1`
+- `POST /resend-verification` : invalide anciens tokens, envoie nouveau (toujours 200 pour éviter enumeration)
+- `PUT /me` : si email change → `email_verified=0` + envoie nouveau lien de vérification
+- `POST /forgot-password` / `POST /reset-password` : tokens 1h, toujours 200
+
+**Backend (admin.js)** :
+- `GET /admin/users` retourne `email_verified`
+- `PATCH /admin/users/:id/verify-email` : vérification manuelle par admin
+
+**Pages frontend** :
+- `Login.jsx` : gère `EMAIL_NOT_VERIFIED` → bouton "Renvoyer l'email"
+- `Register.jsx` : état `verifyEmail` si `requiresVerification`
+- `ForgotPassword.jsx` : layout 2 colonnes, état idle → sent
+- `ResetPassword.jsx` : layout 2 colonnes, lit `?token=`, redirect /login après 3s
+- `VerifyEmail.jsx` : layout 2 colonnes, appel GET au montage, états loading/ok/error
+- `MonCompte.jsx` : badge vérifié/non-vérifié sur email, `EmailChangeForm`, `EmailResendBlock`
+- `App.jsx` : routes `/forgot-password`, `/reset-password`, `/verify-email`
+
+**Why:** Sécurité compte, demande utilisateur.
+**How to apply:** `getSmtpConfig()` est la source de vérité SMTP. `smtpReady = cfg.enabled && cfg.host && cfg.email`. Toujours wrap les `sendMail` dans try/catch pour ne pas bloquer l'inscription.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_auto_reinvest.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_auto_reinvest.md
new file mode 100644
index 0000000..cacb571
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_auto_reinvest.md
@@ -0,0 +1,30 @@
+---
+name: Fonctionnalité réinvestissement automatique des intérêts
+description: auto_reinvest par investissement — déclenché automatiquement après chaque remboursement, brut ou net selon la fiscalité de la plateforme
+type: project
+originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
+---
+Fonctionnalité ajoutée : réinvestissement automatique des intérêts après chaque remboursement.
+
+**DB (migrations dans `db/index.js`) :**
+- `investissements.auto_reinvest` INTEGER DEFAULT 0
+- `reinvestissements.source` TEXT DEFAULT 'manuel' ('manuel' | 'auto')
+- Migration `fiscalite_locale` (interets_bruts_avant_local, taxe_locale sur remboursements) était tronquée — réparée dans la même session
+
+**Backend :**
+- Route `PUT /api/investissements/:id/auto-reinvest { active: bool }` dans `investissements.js`
+- `GET /api/investissements/:id` retourne maintenant `plateforme_fiscalite` (JOIN sur plateformes)
+- `POST /api/remboursements` : si `auto_reinvest=1`, crée un reinvestissement avec `source='auto'`
+ - Plateforme `flat_tax` (France) → montant = `interets_nets`
+ - Autres plateformes → montant = `interets_bruts`
+
+**Frontend (`InvestissementDetail.jsx`) :**
+- Modal "Ajouter un réinvestissement" : sélecteur tabs "Manuel" / "Automatique"
+ - Tab Manuel = formulaire existant
+ - Tab Automatique = activation/désactivation + description brut/net selon plateforme
+ - État `reinvTab` reset à 'manuel' à chaque ouverture via `openReinvModal(tab='manuel')`
+- Bloc "Réinvestissements complémentaires" visible dès que `auto_reinvest=1` (même sans reinvest existant)
+- Badge `auto` sur les lignes créées automatiquement
+- Menu ⋮ carte "Informations du projet" : entrée "Désactiver le réinvestissement auto" visible si actif
+
+**Why:** Permettre de réinvestir automatiquement les intérêts perçus pour capitaliser sans saisie manuelle.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_capital_mensuel_table.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_capital_mensuel_table.md
new file mode 100644
index 0000000..aeb9747
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_capital_mensuel_table.md
@@ -0,0 +1,28 @@
+---
+name: project_capital_mensuel_table
+description: "Composant CapitalMensuelTable.jsx — onglet Vision mensuelle dans Investissements, capital encours par plateforme par mois"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: b72654be-0979-4e85-a9d7-0fd12a61d170
+---
+
+Nouvel onglet **"Vision mensuelle"** ajouté dans `Investissements.jsx` entre les onglets "Plateformes" et "Investissements".
+
+**Composant** : `frontend/src/components/CapitalMensuelTable.jsx`
+
+**Calcul 100% frontend** — utilise les données déjà chargées dans la page (`allRows`, `allRembs`, `allReinvests`, `plats`). Pas de nouvel endpoint backend.
+
+**Logique capital encours par mois** : pour chaque mois M de l'année :
+- L'investissement est actif si `date_souscription ≤ fin_M` ET (statut actif aujourd'hui OU `date_fin ≥ début_M`)
+- `capital = montant_investi + reinvests_≤_finM − capital_remboursé_≤_finM`
+- Groupé par `plateforme_id`
+
+**Structure du tableau** : classes `tip-table` / `tip-th-*` / `tip-td-*` réutilisées depuis TableauInteretsPlateforme. Colonnes : Plateforme | 12 mois | Moyenne | Poids (barre + %).
+
+**Sélecteur d'années** : boutons ‹ › + années + bouton TOUT. Le bouton TOUT ramène sur l'année courante (pas de mode global distinct).
+
+**Multi-détenteur** : détenteur_nom affiché si `new Set(grid.map(p => p.investisseur_id)).size > 1` — cohérent avec [[feedback_multi_detenteur]].
+
+**Why:** Demande d'Olivier pour visualiser la répartition mensuelle du capital déployé par plateforme.
+**How to apply:** Props à passer depuis Investissements.jsx : `allRows`, `allRembs`, `allReinvests`, `plats`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_card_menu_investissement.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_card_menu_investissement.md
new file mode 100644
index 0000000..fd40229
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_card_menu_investissement.md
@@ -0,0 +1,19 @@
+---
+name: Menu ⋮ carte Informations du projet — InvestissementDetail
+description: Bouton contextuel ⋮ en haut à droite du bloc "Informations du projet" avec Modifier, Réinvestir, Exporter, Supprimer (+ Désactiver auto-reinvest si actif)
+type: project
+originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
+---
+Ajout d'un menu contextuel sur le bloc "Informations du projet" dans `InvestissementDetail.jsx` :
+
+- État `cardMenu: { x, y } | null` (distinct de `openMenu` qui sert aux tableaux)
+- Bouton ⋮ (30×30px, border var(--border), 3 cercles SVG) en haut à droite du header de la card
+- Menu (position: fixed + backdrop) avec :
+ - **Modifier** → `openEdit()`
+ - **Réinvestir** → `openReinvModal()`
+ - **Exporter** → `exportDossier()`
+ - Séparateur
+ - **Désactiver le réinvestissement auto** (visible uniquement si `autoReinvActive`) → PUT auto-reinvest false
+ - **Supprimer** (rouge) → `setRowDeleteConfirm(...)` via ConfirmModal (pas `setConfirmingInvDelete` qui est dans le modal d'édition)
+
+**Why:** `setConfirmingInvDelete` est dans le footer du modal d'édition et ne s'affiche pas sans l'ouvrir ; il faut utiliser `rowDeleteConfirm` / `ConfirmModal` pour les suppressions depuis le menu carte.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_inv.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_inv.md
new file mode 100644
index 0000000..4ef5c0c
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_inv.md
@@ -0,0 +1,42 @@
+---
+name: project-categories-secteurs-inv
+description: "Tables categories_inv et secteurs_inv, gestion admin dans AdminPlateformes, fusion, filtre référentiel"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 2fef7a75-e0f3-4f7d-9c80-79c3be16af03
+---
+
+Fusion de `type_investissement` (champ enum mono-valeur) et des catégories référentiel en deux tables multi-valeurs gérées globalement par l'admin.
+
+**Tables DB :**
+- `categories_inv (id, nom, created_at)` — liste globale des catégories d'investissement
+- `referentiel_categories_inv (referentiel_id, categorie_id)` — junction many-to-many
+- `secteurs_inv (id, nom, created_at)` — liste globale des secteurs d'investissement
+- `referentiel_secteurs_inv (referentiel_id, secteur_id)` — junction many-to-many
+
+**Backend :**
+- `/api/ref-categories` (GET/POST/PUT/:id/DELETE/:id/POST/:id/merge) — protégé requireAdmin
+- `/api/ref-secteurs` (GET/POST/PUT/:id/DELETE/:id/POST/:id/merge) — protégé requireAdmin
+- `referentiel.js` : helpers `attachCatsInv` + `attachSecteursInv`, `categories_inv_ids` et `secteurs_inv_ids` dans saveRelations
+- Seeding au démarrage depuis `type_investissement` et `secteur` existants
+
+**Frontend — AdminPlateformes (/admin/plateformes) :**
+- Section "Catégories d'investissement" : RefListSection avec apiPath="/ref-categories"
+- Section "Secteurs d'investissement" : RefListSection avec apiPath="/ref-secteurs"
+- Menu ⋮ par ligne : Renommer / Fusionner avec… / Supprimer
+- Fusion : POST /:id/merge → ajoute cible aux référentiels qui n'ont pas déjà la cible, supprime source
+- Clic sur un nom de catégorie → navigue vers referentiel avec `?filterCat=` pré-rempli
+
+**Frontend — PlatformeProfile.jsx :**
+- Sélecteurs chips violet (categories_inv) et vert (secteurs_inv) en mode édition
+- Vue : badges par catégorie/secteur
+- Prompt IA : `{{CATEGORIES_INV_LIST}}` et `{{SECTEURS_INV_LIST}}` interpolés depuis les listes chargées
+- `type_investissement` et `secteur` envoyés à null au save (dépréciation)
+
+**Frontend — AdminPlateformes — RefModal (création/édition référentiel) :**
+- Charge `/ref-categories` et `/ref-secteurs` au montage
+- Chips toggle violet/vert pour sélectionner les catégories et secteurs
+- Envoie `categories_inv_ids` et `secteurs_inv_ids` dans le payload
+
+**Why:** Permettre plusieurs catégories et secteurs par plateforme, gérés centralement par l'admin avec propagation automatique via renommage.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_inv_heritage.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_inv_heritage.md
new file mode 100644
index 0000000..031533b
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_inv_heritage.md
@@ -0,0 +1,61 @@
+---
+name: project-categories-secteurs-inv-heritage
+description: "Héritage merge (fusion) des catégories/secteurs d'investissement depuis le référentiel vers les plateformes utilisateur"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: cad3a9c0-cf8e-4204-ac6e-e22c03811abe
+---
+
+# Héritage catégories/secteurs d'investissement (merge)
+
+Les catégories et secteurs d'investissement définis dans le référentiel sont **fusionnés** (merge) dans les plateformes liées. Ce mécanisme est distinct de l'héritage scalaire (`overridden_fields`).
+
+**Why:** L'user doit toujours voir les tags du référentiel comme base, sans pouvoir les retirer, mais peut ajouter les siens en plus.
+
+**How to apply:** Toute évolution des tables jonction plateforme↔cats/sects doit préserver cette logique merge. Ne jamais écraser les inherited IDs lors d'un PUT.
+
+---
+
+## Mécanisme
+
+### Backend — `attachCategoriesInv` / `attachSecteursInv` (plateformes.js)
+
+Au moment de lire les plateformes, les tags sont mergés :
+1. Charger les propres tags de la plateforme (`plateforme_categories_inv`)
+2. Si la plateforme a un `referentiel_id` : charger les tags du référentiel (`referentiel_categories_inv`)
+3. Pour chaque tag du référentiel : s'il est déjà dans les propres tags → marquer `is_inherited: true` ; sinon l'ajouter avec `is_inherited: true`
+4. Tags propres non dans le référentiel → `is_inherited: false`
+
+### Backend — `PUT /plateformes/:id/categories-inv` (associations-inv.js)
+
+Avant de sauvegarder, les IDs hérités du référentiel sont re-ajoutés à `allIds` (on ne peut pas les retirer).
+
+### Frontend — `InvSelect.jsx`
+
+Prop `inheritedIds={[...]}` :
+- Items hérités : toujours cochés + disabled + badge "Réf" (fond accent, 10px)
+- `toggle()` ignore les IDs hérités
+- CSS : `.cat-select-item.inherited` (cursor: default)
+
+### Frontend — `Settings.jsx`
+
+`openEditPlat()` stocke dans l'état :
+```js
+inherited_cat_ids: platCatsInv.filter(c => c.is_inherited).map(c => c.id),
+inherited_sect_ids: platSectsInv.filter(s => s.is_inherited).map(s => s.id),
+```
+
+InvSelect reçoit `inheritedIds={editPlat.inherited_cat_ids || []}`.
+
+Bandeau héritage : affiche "+ catégories & secteurs" si `hasCatInherit || hasSectInherit`.
+
+### Frontend — `PlatformeProfile.jsx`
+
+Chips en lecture : badge "Réf" sur les items `is_inherited: true`.
+
+---
+
+## Note importante
+
+L'héritage catégories/secteurs est **distinct** des champs scalaires (`HERITABLE_FIELDS` + `overridden_fields`). Le compteur `inheritedCount` dans le bandeau Settings ne compte que les scalaires ; cats/sects sont signalés séparément via `hasCatInherit`/`hasSectInherit`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_utilisateur.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_utilisateur.md
new file mode 100644
index 0000000..c7e3e58
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_categories_secteurs_utilisateur.md
@@ -0,0 +1,36 @@
+---
+name: project-categories-secteurs-utilisateur
+description: "Catégories/secteurs d'investissement en deux niveaux : globales admin (héritées) + privées utilisateur, avec associations plateformes et investissements"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
+---
+
+Extension du système categories_inv / secteurs_inv pour permettre un modèle à deux niveaux.
+
+**DB — nouvelles colonnes et tables :**
+- `categories_inv.user_id` et `secteurs_inv.user_id` — NULL = global admin, sinon = privé utilisateur
+- `plateforme_categories_inv (plateforme_id, categorie_id)` — associations plateforme↔catégorie utilisateur
+- `plateforme_secteurs_inv (plateforme_id, secteur_id)`
+- `investissement_categories_inv (investissement_id, categorie_id)`
+- `investissement_secteurs_inv (investissement_id, secteur_id)`
+
+**Backend :**
+- `/api/categories-inv` et `/api/secteurs-inv` — CRUD user-facing (globales en lecture seule, privées éditables)
+- `/api/plateformes/:id/categories-inv` et `secteurs-inv` — GET/PUT associations plateforme
+- `/api/investissements/:id/categories-inv` et `secteurs-inv` — GET/PUT associations investissement
+- GET `/api/investissements` et `/:id` enrichis : `categories_inv` et `secteurs_inv` arrays inline
+- Push référentiel étendu : pousse aussi catégories/secteurs vers plateformes liées (mode doux = INSERT OR IGNORE, fort = DELETE+INSERT) avec sync en cascade sur investissements actifs
+
+**Sync automatique investissements :**
+Quand les catégories/secteurs d'une plateforme sont mis à jour (PUT ou push), les investissements `en_cours`/`en_retard`/`procedure` de cette plateforme sont resynchronisés automatiquement.
+
+**Frontend :**
+- Settings > "Catégories d'investissement" et "Secteurs d'investissement" : liste globales (badge Héritée, non éditables) + privées (CRUD inline)
+- Settings > Plateformes : formulaire d'édition avec chips violet (catégories) et vert (secteurs) ; panneau détail affiche les associations
+- Investissements : chips dans le formulaire (pré-remplis depuis la plateforme choisie), badges dans InvestissementDetail, filtres Catégorie et Secteur dans la liste (ET logique)
+
+**Why:** Permettre à chaque utilisateur d'organiser ses investissements selon ses propres critères tout en héritant d'une taxonomie commune définie par l'admin.
+
+**How to apply:** Pour toute nouvelle page affichant les investissements, charger `/api/categories-inv` et `/api/secteurs-inv` pour les filtres. Les données `categories_inv` et `secteurs_inv` sont déjà incluses dans les réponses GET investissements.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_cerfa2561.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_cerfa2561.md
new file mode 100644
index 0000000..8b20ede
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_cerfa2561.md
@@ -0,0 +1,40 @@
+---
+name: project-cerfa2561
+description: Simulation CERFA 2561 par plateforme dans la page Fiscalité (TaxReport)
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 0922bde7-84ed-45dc-b233-50bf0be5ad3e
+---
+
+Composant `frontend/src/components/Cerfa2561Preview.jsx` — simulation HTML du formulaire CERFA 2561, accessible via l'onglet "CERFA 2561" de la page TaxReport.
+
+**Architecture :**
+- Backend : `GET /api/taxreport/cerfa2561` — données agrégées par plateforme × investisseur
+- Backend : `GET /api/taxreport/cerfa2561/remboursements` — détail des remboursements pour un formulaire
+- Backend : `GET /api/taxreport/years` — années disponibles
+- Frontend : `Cerfa2561Preview.jsx` — mode inline (onglet) avec prop `inline`, `expanded`, `onToggleExpand`
+
+**Cases CERFA remplies :**
+- `2TT/KR` ou `2TR/AR` selon `plateforme.type_produit_fiscal` ('2TT' défaut, '2TR' configurable dans Settings)
+- `2BH/DQ` = interets_bruts si flat-tax FR (PS déjà prélevés)
+- `2CK/AD` = prelev_forfaitaire si flat-tax FR (crédit d'impôt PFNL)
+- `2TY/KS` = pertes en capital
+- Montants arrondis à l'entier (sans décimales), récapitulatif fiscal en décimales
+
+**Champ `type_produit_fiscal` sur plateformes :**
+- Migration DB : `ALTER TABLE plateformes ADD COLUMN type_produit_fiscal TEXT NOT NULL DEFAULT '2TT'`
+- Visible dans Settings > Plateformes uniquement pour les plateformes françaises
+- Affiché dans le bloc Détail de la plateforme
+
+**Ergonomie :**
+- Sélecteur de plateforme (220px) + bouton agrandir + bouton imprimer dans la toolbar
+- Impression via `window.open` → `print-color-adjust: exact` pour conserver les fonds colorés
+- Titre de la fenêtre = `CERFA 2561 — {annee} — {plateforme_nom}` (proposé comme nom de fichier PDF)
+- Panneau dépliable "N remboursements pris en compte" — tableau détail avec cache (pas de re-fetch)
+- Mode sombre : `colorScheme: 'light'` + `background: '#fff'` forcé sur le formulaire
+
+**Palette couleurs :** violet/indigo aligné sur YearSelector (`#5b21b6`, `#7c3aed`, `#f5f3ff`, `#ede9fe`)
+
+**Why:** Permettre à l'utilisateur de vérifier/simuler ce que chaque plateforme aurait dû déclarer, et de générer un PDF de référence par plateforme.
+**How to apply:** Fichier unique `Cerfa2561Preview.jsx`. Pas de sed sur ce fichier (risque de troncature). Utiliser python3 pour les remplacements de masse.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_cerfa_fiscal.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_cerfa_fiscal.md
new file mode 100644
index 0000000..d1e2b70
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_cerfa_fiscal.md
@@ -0,0 +1,65 @@
+---
+name: project_cerfa_fiscal
+description: "Architecture complète des 3 onglets fiscaux TaxReport : CERFA 2042, CERFA 2561 (IFU), CERFA 2778-SD — composants, données, vues"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: cd6fedee-a8a5-422b-912c-bf927e5e7fdd
+---
+
+## Onglets TaxReport (TaxReport.jsx)
+
+Ordre : **CERFA 2042** | **CERFA 2561 (IFU)** | **CERFA 2778-SD** (si pfoAssujetti)
+
+`activeTab` par défaut : `'2042'` — les onglets Récapitulatif et Détail par projet ont été supprimés.
+
+**KPI synthèse** : bloc "Cases fiscales 2042 — synthèse {annee}" affiché en haut de page (avant les onglets), calculé en fetchant `/taxreport/cerfa2561` + `/taxreport/2778` dans `TaxReport.jsx` (state `data2042`). Cases affichées : 2TT (si > 0), 2TR (si > 0), 2BH, 2CK (vert), 2TY (rouge, si > 0). Le fetch exclut les plateformes étrangères via `cl_2778_excluded_plats` localStorage.
+
+---
+
+## CERFA 2561 (IFU) — `Cerfa2561Preview.jsx`
+
+Sidebar 3 vues :
+- **Suivi mensuel** : tableau plateformes françaises × 12 mois (interets_bruts), avec Total intérêt brut / Taux prélevé / Total intérêt net en bas
+- **Données 2561** : sélecteur plateforme+détenteur (FR uniquement, sans doublon prénom) + formulaire CERFA 2561Form
+- **Report 2042** : `Report2042Block2561` — cases 2TT/2TR/2BH/2CK/2TY agrégées, breakdown par plateforme, badge BADGE_AUTO
+
+**Données** : `GET /taxreport/cerfa2561` retourne `lignes[]` avec `mois[mm] = { interets_bruts, prelev_sociaux, prelev_forfaitaire }` (breakdown mensuel ajouté cette session)
+
+**Helper nom** : `fullName(l)` — évite doublon si `nom` commence déjà par `prenom`
+
+---
+
+## CERFA 2778-SD — `Cerfa2778Preview.jsx`
+
+Sidebar 3 vues :
+- **Suivi mensuel** : tableau plateformes étrangères × 12 mois, avec checkboxes inclusion/exclusion persistées `cl_2778_excluded_plats`
+- **Données 2778-SD** : sélecteur mois → `CerfaBlock` avec cases BA/IA/PQ/PV/PF1/PG1/PK/QR
+- **Report 2042** : `Report2042Block` — cases 2TR/2BH/2CK pour plateformes étrangères
+
+**Données** : `GET /taxreport/2778` retourne `plateformes[] = { id, nom, mois: { mm: number } }` (route reconstruite cette session après troncature)
+
+**CerfaBlock** : grille 6 col `1fr 90px 90px 90px 90px 90px` — Libellé | Base imposable (BA) | Taux | Case | Montant | (sans titre). Sections : Page de garde (page 1) / PFO (page 2) / PS (page 3) / Totaux (page 4). Props `mois`, `annee` passées pour afficher "Mai 2026" dans Montant sans unité (`noUnit`).
+
+**Wrapper** : le composant retourne un `` depuis cette session.
+
+---
+
+## CERFA 2042 — `Cerfa2042Preview.jsx` (nouveau cette session)
+
+Sidebar 2 vues :
+- **Suivi mensuel** : tableau combiné FR (vert, déclaration automatique) + étranger (rouge, à déclarer) ; Total Plateformes françaises dans tbody avant groupe étranger ; Total Plateformes étrangères + Total général + Taux prélevé + Total intérêt net dans tfoot
+- **Données 2042** : cases mixtes groupées par code (2TT/2TR/2BH/2CK/2TY) ; chaque ligne de breakdown porte son badge `BADGE_AUTO` ou `BADGE_DECL` ; sélecteur filtre "Tout / Automatique / À déclarer" en haut à droite
+
+**Données** : fetche **toujours** les deux endpoints `/taxreport/cerfa2561` + `/taxreport/2778` (inconditionnellement, indépendant de `pfoAssujetti`) ; lit `cl_2778_excluded_plats` depuis localStorage
+
+**Badges** :
+- `BADGE_AUTO` = { text: 'déclaration automatique', bg: rgba(22,163,74,0.1), color: #16a34a }
+- `BADGE_DECL` = { text: 'à déclarer', bg: rgba(239,68,68,0.1), color: #dc2626 }
+
+**Calcul net étranger** : `brut × (1 − totalTaxRate)` (estimation depuis taux PFU de l'année)
+
+**Noms de mois** : `MOIS_LABELS` utilise les noms complets ('Janvier'…'Décembre') dans les 3 composants.
+
+**Why:** Vue de synthèse pour la déclaration 2042, fusionnant automatique (IFU FR) et manuel (2778-SD étranger).
+**How to apply:** `pfoAssujetti` est passé en prop mais n'influe plus sur le fetch (toujours chargé). La prop reste utile pour d'autres contextes. Fichiers sensibles aux troncatures : Cerfa2778Preview.jsx, Cerfa2561Preview.jsx, Cerfa2042Preview.jsx — toujours reconstruire la fin par index de lignes Python.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_chart_colors.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_chart_colors.md
new file mode 100644
index 0000000..4d7f5f7
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_chart_colors.md
@@ -0,0 +1,45 @@
+---
+name: project-chart-colors
+description: "Couleurs personnalisables des graphiques — UiContext, Settings Apparence, palette MD2, variante Reçu/Projeté"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: d7532bd8-8eb4-4b40-8c39-8f569dcf432f
+---
+
+Fonctionnalité de personnalisation des couleurs des graphiques ajoutée dans Settings > Apparence.
+
+**Fichiers modifiés :** `frontend/src/context/UiContext.jsx`, `frontend/src/pages/Settings.jsx`, `frontend/src/styles.css`
+
+## UiContext
+3 nouvelles clés localStorage et états :
+- `cl_chart_interets` → `chartInterets` / `setChartInterets` — défaut `#2196f3` (Bleu 500)
+- `cl_chart_capital` → `chartCapital` / `setChartCapital` — défaut `#4caf50` (Vert 500)
+- `cl_chart_cashback` → `chartCashback` / `setChartCashback` — défaut `#ffc107` (Ambre 500)
+
+Tous exposés dans `useUi()`.
+
+## Settings.jsx — composants ajoutés
+- `lightenColor(hex, factor=0.72)` — mélange avec blanc, utilisé pour la variante "Projeté"
+- `MD_PALETTE` — palette Material Design 2 complète (19 familles × shades 50→900 + accents A100/A200/A400/A700)
+- `isLightColor(hex)` — détecte si une couleur est claire (pour adapter la couleur du texte)
+- `findInPalette(hex)` — retrouve `{ family, shade }` d'un hex dans MD_PALETTE
+- `SUGGESTED_PALETTES` — 10 palettes prédéfinies (Classique, Indigo & Teal, Nuit, Nature, Coucher de soleil, Violet, Frais, Contraste, Pastel, Moderne)
+- `PaletteSelector` — grille 5×2 de cartes avec 3 pastilles colorées ; sélection applique les 3 couleurs
+- `ChartColorPicker` — sélecteur en 2 étapes : (1) `
` famille + indicateur couleur, (2) grille de teintes ; useEffect synchro famille quand value change depuis l'extérieur
+- Icônes SVG inline : `IconInterets`, `IconCapital`, `IconCashback` (taille 22px, fill="currentColor")
+
+## Preview Reçu / Projeté
+Chaque picker affiche deux barres :
+- **Reçu** — couleur sélectionnée (pleine)
+- **Projeté** — version éclaircie à 72% (`lightenColor`)
+
+## CSS (styles.css)
+Classes ajoutées : `.chart-color-row`, `.chart-color-item`, `.chart-color-picker`, `.color-family-select-row`, `.color-family-indicator`, `.color-shade-grid`, `.color-shade-btn`, `.color-shade-label`, `.color-preview-pair`, `.color-preview-bar`, `.color-preview-label`, `.color-preview-hex`, `.palette-grid`, `.palette-card`, `.palette-swatches`, `.palette-swatch`, `.palette-name`
+
+**Attention CSS :** `.palette-swatch` et `.color-shade-label` sont des `` — ils nécessitent `display: block` pour que width/height et opacity fonctionnent correctement.
+
+## Prochaine étape
+Brancher `chartInterets`, `chartCapital`, `chartCashback` (via `useUi()`) et `lightenColor` dans les composants de graphiques : `InteretsChart.jsx`, `InteretsMensuelsChart.jsx`, `SoldeChart.jsx`, `InteretsDistributionChart.jsx`.
+**Why:** L'utilisateur veut que les couleurs choisies dans Settings s'appliquent effectivement aux graphiques.
+**How to apply:** Importer `useUi`, déstructurer les 3 couleurs, remplacer les constantes couleur hardcodées par les valeurs dynamiques. Utiliser `lightenColor` importé ou redéfini localement pour les séries projetées.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_communication.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_communication.md
new file mode 100644
index 0000000..914cad8
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_communication.md
@@ -0,0 +1,62 @@
+---
+name: project-communication
+description: "Page Communication — tickets support + notifications : architecture, comportements UI, jobs backend"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 19a3ce4d-7c1a-418b-9ca2-65b6157249db
+---
+
+# Page Communication (`frontend/src/pages/Communication.jsx`)
+
+## Architecture générale
+3 volets : sidebar (volet 1), liste (volet 2), détail (volet 3).
+Onglets : `support` (tickets) | `notifications`.
+
+## Filtres chips (volet 2)
+- **Tickets** : multi-select via `useState(new Set(['open', 'pending']))` ; si aucune chip active → liste vide (pas "tout afficher")
+- **Notifications** : multi-select `useState(new Set(['unread']))` au démarrage ; même règle liste vide si 0 chip active
+- Chips actives = fond `var(--primary)` + checkmark SVG blanc ; cumulatives
+
+## Comportements notifications
+- Pas de présélection automatique au chargement ni au changement d'onglet
+- La notification sélectionnée reste jusqu'à clic explicite
+- Menu ⋮ dans le volet 3 : "Marquer comme lu" / "Tout considérer comme lu" / "Tout supprimer"
+
+## Dropdowns portal (pattern obligatoire)
+Tous les dropdowns custom (TagSelect, TypeDropdown, RecipientDropdown) utilisent `createPortal(content, document.body)` + `position: fixed` + `getBoundingClientRect()` pour échapper aux stacking contexts.
+
+## Composant RecipientDropdown
+Dropdown searchable pour les broadcasts. Trois niveaux :
+- "Tous les utilisateurs" (value `''`)
+- "Tous les administrateurs" (value `'admins'`) — routé vers `WHERE role='admin'` côté backend
+- Utilisateurs individuels filtrés par `display_name` / `email` avec avatars initiales
+
+## Broadcast payload
+```js
+payload.user_id = bcUserId === 'admins' ? 'admins' : Number(bcUserId);
+```
+
+## Ticket detail header (volet 3)
+Structure colonne :
+- Ligne 1 : chip Type + titre (flex:1) + StatusBadge
+- Ligne 2 : "Ouvert par **Nom** · date [— assigné à **Nom**]" à gauche + chips pages à droite
+- `margin-top: 25px` entre les deux lignes
+
+## Droits
+- Utilisateur : voit uniquement ses tickets et ses notifications
+- Admin : voit tous les tickets ; voit ses notifs + notifs `notifyAdmins`
+- Actions admin-only : Assigner à, Mettre en attente, Réouvrir, Clôturer
+- "Modifier la catégorisation" : accessible à tous (owner + admin)
+- "Fermer" (panneau) : accessible à tous
+
+## Pièges React connus
+- `{msg.is_admin && }` → si `is_admin = 0` (SQLite), affiche "0". Toujours `{!!msg.is_admin && ...}`
+- Images dans messages : classe `msg-inline-img` + wrapper `msg-img-wrap` ajouté par useEffect. Margin `7px` sur `.msg-img-wrap` ET `.msg-inline-img` pour couvrir les deux cas.
+
+## Jobs backend
+- `autoTicketStatus.js` : open→pending (2j sans réponse), pending→resolved (5j sans réponse créateur) — toutes les heures
+- `autoCleanNotifs.js` : supprime notifications >7 jours sauf type `security` — toutes les heures
+
+**Why:** Nettoyage automatique pour éviter l'accumulation infinie de notifications lues.
+**How to apply:** Si on ajoute un nouveau type de notification critique à ne jamais purger, l'ajouter à la clause `type != 'security'` dans `autoCleanNotifs.js`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_compte_id_remboursements.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_compte_id_remboursements.md
new file mode 100644
index 0000000..a0a451f
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_compte_id_remboursements.md
@@ -0,0 +1,38 @@
+---
+name: project_compte_id_remboursements
+description: FK compte_id sur investissements et remboursements — liaison aux comptes bancaires du détenteur
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: bf0e3a7b-9629-4ce8-81a7-96943a703cfc
+---
+
+Colonne `compte_id` (FK nullable → `comptes.id`, ON DELETE SET NULL) ajoutée sur `investissements` et `remboursements`.
+
+**DB** : migrations dans `backend/src/db/index.js` (guard PRAGMA table_info).
+
+**Backend `investissements.js`** : Schema Zod, GET (join comptes → compte_id/compte_nom/compte_type), POST INSERT, PUT UPDATE. Route `GET /investissements/comptes-par-investisseur/:investisseur_id` : liste les comptes d'un investisseur (pour les selects frontend).
+
+**Backend `remboursements.js`** : Schema Zod, INSERT et UPDATE (nullifié si methode ≠ compte_courant). GET / retourne `compte_nom` via LEFT JOIN comptes. Route `POST /remboursements/backfill-comptes` : lie les remboursements sans compte_id à l'investissement ou au premier compte_courant du détenteur (accessible depuis MonCompte → Nettoyage).
+
+**Cascade compte_id** : PUT investissements met à jour les remboursements avec `methode=compte_courant AND compte_id IS NOT NULL`. Les remboursements redirigés manuellement vers portefeuille sont protégés.
+
+**Frontend — 4 formulaires mis à jour :**
+- `Investissements.jsx` openEdit + openNew : `resolvedMethode` depuis la plateforme si méthode fixée
+- `InvestissementDetail.jsx` openEdit investissement : idem
+- `InvestissementDetail.jsx` formulaire remboursement : `comptesRembInvestisseur` chargé après déclaration useState (éviter TDZ), méthode toujours éditable (override possible vers portefeuille)
+- `Remboursements.jsx` : comptes chargés au changement d'investissement_id, hérite `inv.compte_id` en priorité
+
+**Règles UX :**
+- Méthode dans formulaire remboursement = toujours éditable (pas de champ readOnly) — permet forçage hors cascade
+- Auto-sélection : `inv.compte_id` en priorité, sinon premier compte de type `compte_courant`
+- `resolvedMethode` dans openEdit : lire la methode de la plateforme si ≠ `choix_investisseur`
+
+**Page Remboursements — colonne Versement :**
+- Remplace colonne "Type" ; affiche nom du compte courant (avec tooltip) ou "Porte-monnaie" (grisé)
+- Deux boutons icônes (porte-monnaie + compte-courant) dans le header du bloc pour filtrer ; dépendances useMemo incluses
+- Corrections de solde masquées quand filtre porte-monnaie désactivé
+
+**Why:** Permettre de tracer sur quel compte bancaire arrivent les remboursements, pour rapprochement bancaire futur.
+
+**How to apply:** Tout nouveau formulaire affichant methode_remboursement doit suivre ce même pattern. Voir [[project_methode_remboursement_investissement]].
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_comptes_exoneration.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_comptes_exoneration.md
new file mode 100644
index 0000000..dde9579
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_comptes_exoneration.md
@@ -0,0 +1,22 @@
+---
+name: project_comptes_exoneration
+description: "Champ exoneration_fiscale sur la table comptes — migration DB, backend, frontend Settings"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: bf0e3a7b-9629-4ce8-81a7-96943a703cfc
+---
+
+Colonne `exoneration_fiscale` ajoutée à la table `comptes`.
+
+**Valeurs :** `'aucune'` (défaut) | `'pfnl_5ans'` (Exonération IR PFNL si détention 5 ans)
+
+**Migration DB :** `ALTER TABLE comptes ADD COLUMN exoneration_fiscale TEXT NOT NULL DEFAULT 'aucune'` dans `backend/src/db/index.js` (avec guard PRAGMA table_info).
+
+**Backend `comptes.js` :** champ ajouté au Schema Zod, au SELECT, INSERT et UPDATE.
+
+**Frontend `Settings.jsx` :** select dans `CompteFormFields`, `EMPTY_COMPTE` mis à jour, payloads création/édition complétés. Colonne dans le tableau avec "Oui" (vert, title=libellé) / "Non" (grisé). `EXONERATION_LABELS` map définie à côté de `TYPE_COMPTE_LABELS`.
+
+**Why:** Permettre de distinguer les enveloppes fiscalement avantageuses (PEA-PME, etc.) pour les calculs fiscaux futurs.
+
+**How to apply:** Si un nouveau type d'exonération est ajouté, mettre à jour l'enum Zod backend ET `EXONERATION_LABELS` frontend.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_confirm_modal.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_confirm_modal.md
new file mode 100644
index 0000000..a9ceff9
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_confirm_modal.md
@@ -0,0 +1,36 @@
+---
+name: Composant ConfirmModal et suppressions
+description: ConfirmModal.jsx créé ; tous les confirm() natifs remplacés dans les pages
+type: project
+originSessionId: c8c8964c-17e5-40a2-8e72-52261af1d3e9
+---
+Un composant `ConfirmModal` générique a été créé dans `frontend/src/components/ConfirmModal.jsx`.
+Props : `open`, `title` (défaut "Confirmer la suppression"), `message`, `confirmLabel` (défaut "Supprimer"), `onConfirm`, `onCancel`.
+Il s'appuie sur `Modal.jsx` existant, largeur 440px, bouton ghost "Annuler" + bouton danger.
+
+**Why:** Remplacer les 12 `confirm()` natifs (popup navigateur) par des modales React ergonomiques.
+
+**How to apply:** Toute nouvelle suppression doit utiliser `ConfirmModal` avec un état local `const [deleteConfirm, setDeleteConfirm] = useState(null)`.
+Pattern standard :
+```js
+const del = (item) => {
+ setDeleteConfirm({
+ message: `Supprimer "${item.nom}" ?`,
+ onConfirm: async () => {
+ await api.del(`/route/${item.id}`);
+ setDeleteConfirm(null);
+ reload();
+ },
+ });
+};
+```
+Et dans le JSX, avant la fermeture du return :
+```jsx
+ setDeleteConfirm(null)}
+/>
+```
+Fichiers déjà migrés : DepotsRetraits, Admin (UsersSection), Investissements, Remboursements, Settings (Settings + GarantiesPanel + NotationPanel), FamilleEntreprises.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_corrections_solde.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_corrections_solde.md
new file mode 100644
index 0000000..27c22a6
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_corrections_solde.md
@@ -0,0 +1,28 @@
+---
+name: Fonctionnalité corrections de solde
+description: Table corrections_solde, route /api/corrections, intégration dashboard/fiscal/remboursements pour réconcilier les micro-écarts de flat-tax
+type: project
+originSessionId: 94c97348-4f1d-4ff7-ba4b-e94dda6b32ea
+---
+Nouvelle fonctionnalité implémentée pour corriger les micro-écarts (0,01 €) de calcul de flat-tax sur les plateformes françaises.
+
+**Modèle :** table `corrections_solde` (id, investisseur_id, plateforme_id, date, montant, notes, created_at). Séparée de `remboursements` pour éviter de rendre `investissement_id` nullable (migration SQLite complexe).
+
+**Déclenchement :** après chaque dépôt/retrait sur une plateforme `fiscalite = 'flat_tax'`, une `CorrectionModal` s'ouvre et demande le solde réel constaté. Si |écart| ≥ 0,005 €, une correction est créée automatiquement via POST `/api/corrections`.
+
+**Solde attendu proposé par défaut :**
+- Retrait → 0 € (cas typique : vider le porte-monnaie)
+- Dépôt → solde calculé avant l'opération + montant du dépôt
+
+**Impact :**
+- `dashboard.js` : `correctionsPerPlat` s'ajoute au `walletMap` par plateforme ; route `GET /solde-historique` ajoutée (calcul du solde à une date passée, filtre `date <= :date` sur les 5 composantes)
+- `fiscal2778.js` : corrections ajoutées à `net_recu`, ligne dédiée dans le récapitulatif, tableau séparé, incluses dans l'export CSV
+- `Remboursements.jsx` : corrections mergées dans `tableRows` avec flag `_is_correction: true`, badge "CORRECTION" ambré, supprimables via menu ⋮
+- `DepotsRetraits.jsx` : corrections affichées comme nouveau type de mouvement dans l'onglet Mouvements (`normalizedCorrections` via useMemo, `type: 'correction'`, id préfixé `corr_${id}`). Menu ⋮ sur chaque ligne (Modifier / Correction de solde / Supprimer). Option "Corrections de solde" dans le filtre type.
+
+**Route :** `/api/corrections` (GET filtré par scope, POST avec validation Zod, DELETE avec vérification ownership)
+**Route solde historique :** `GET /api/dashboard/solde-historique?plateforme_id=X&date=Y`
+
+**Why:** Les arrondi de prélèvements sociaux/IR créent des écarts cumulatifs entre le solde calculé et le solde réel de la plateforme.
+
+**How to apply:** Ne pas modifier `remboursements` pour ajouter ces corrections — toujours passer par la table dédiée.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_dashboard_kpis.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_dashboard_kpis.md
new file mode 100644
index 0000000..6afbab9
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_dashboard_kpis.md
@@ -0,0 +1,34 @@
+---
+name: project-dashboard-kpis
+description: "KPIs Dashboard — TrendBadge, logique M vs M-1, capital investi et en risque avec comparaison mensuelle"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 752b3578-1999-4431-9dd9-73297a9baaf4
+---
+
+6 KPIs dans l'ordre : Capital investi · Capital en risque · Performance annualisée · Intérêts bruts/nets · Capital remboursé · Cashback reçu.
+
+**Composants dans Dashboard.jsx :**
+- `TrendBadge({ current, prev, invert })` — badge ↗/↘ vert/rouge, `invert=true` pour Capital en risque (hausse = mauvais)
+- `KpiCard({ title, value, badge, refValue })` — carte label uppercase, valeur large, badge inline, ref grise dessous
+- `DashboardKpis({ portfolio, netMode, pfuRates, capitalMensuelData })` — enfant de `InteretsChartProvider`
+
+**Logique comparaison selon sélecteur d'année :**
+- Année courante (N) : valeur = mois en cours, badge = M vs M-1, ref = `X € en avr. 2026`
+- Année passée (N-x) : valeur = total annuel, badge = N vs N-1, ref = `X € en 2024`
+- modeGlobal : cumul sans badge
+
+**Capital investi et Capital en risque — comparaison mensuelle (2026-05-31) :**
+- Backend `dashboard.js` boucle `capitalMensuel` : chaque entrée contient maintenant `{ mois, capital, en_defaut }` — `en_defaut` = somme capital des prêts `en_retard`/`procédure`
+- Frontend : `capitalCurrent/capitalPrev` et `enDefautCurrent/enDefautPrev` extraits de `capitalMensuelData` dans le `useMemo` de `thisMonthRow`
+- Badge ne s'affiche que si `isCurrentYear` et `prev > 0`
+- Approximation : statut non historisé → valeur mensuelle `en_defaut` reflète le statut actuel projeté rétrospectivement
+
+**Données source :**
+- `rawData.rembourses` (mensuel) — `{ mois, interets_bruts, interets_nets, capital, cashback }`
+- `rawDataGlobal.rembourses` (annuel) — `{ annee, interets_bruts, interets_nets, capital, cashback }`
+- `capitalMensuelData` — prop passée depuis Dashboard parent via `onCapitalMensuel` de `TableauInteretsPlateforme`
+- `currentYear`, `currentMonth`, `rawData`, `rawDataGlobal` exposés depuis `InteretsChartContext`
+
+**How to apply:** Respecter la distinction `isCurrentYear` pour choisir entre granularité mensuelle (N) et annuelle (N-x). Tout nouveau KPI affichant des intérêts doit utiliser `netMode` pour basculer brut/net.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_depots_mensuel_table.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_depots_mensuel_table.md
new file mode 100644
index 0000000..886b339
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_depots_mensuel_table.md
@@ -0,0 +1,35 @@
+---
+name: project_depots_mensuel_table
+description: "Composant DepotsMensuelTable.jsx — onglet Vision mensuelle dans DepotsRetraits, dépôts/retraits par plateforme par mois avec filtres icônes"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: b72654be-0979-4e85-a9d7-0fd12a61d170
+---
+
+Nouvel onglet **"Vision mensuelle"** ajouté dans `DepotsRetraits.jsx` entre les onglets "Plateformes" et "Mouvements".
+
+**Composant** : `frontend/src/components/DepotsMensuelTable.jsx`
+
+**Calcul 100% frontend** — props : `allRows` (dépôts/retraits), `plats`.
+
+**Filtres icônes** : deux boutons dans le header (pattern identique à TableauInteretsPlateforme) :
+- Icône `"depot"` (default ON, couleur `#22c55e`) : inclure les dépôts
+- Icône `"retrait"` (default OFF, couleur `#ef4444`) : inclure les retraits
+- Quand les deux sont actifs → valeur = dépôts − retraits (net), valeurs négatives en rouge
+- Quand un seul actif → somme positive du type sélectionné
+
+**Icônes** : chargées depuis `/api/icons` → `libIcons = { name: filename }` → ` `. Composant `AppIcon` interne avec fallback span. Noms dans la bibliothèque : `"depot"` et `"retrait"`.
+
+**Agrégation** : par `plateforme_id`, colonnes `depots[]` et `retraits[]` séparées (12 mois), calculées depuis `r.type === 'depot'` / `r.type === 'retrait'`.
+
+**Structure du tableau** : classes `tip-table` réutilisées. Colonnes : Plateforme | 12 mois | Total | Moy. mensuelle.
+
+**Sélecteur d'années** : boutons ‹ › + années + bouton TOUT = `setAnnee(currentYear)`.
+
+**Multi-détenteur** : `detenteur_nom` affiché si `new Set(byPlat.map(p => p.investisseur_id)).size > 1` — cohérent avec [[feedback_multi_detenteur]].
+
+**hexToRgba** : helper local pour teinter les boutons actifs.
+
+**Why:** Demande d'Olivier pour visualiser la répartition mensuelle des flux de trésorerie (dépôts/retraits) par plateforme.
+**How to apply:** Props à passer depuis DepotsRetraits.jsx : `allRows`, `plats`. Onglet tab `activeTab === 'vision-mensuelle'`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_detail_panel_depots.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_detail_panel_depots.md
new file mode 100644
index 0000000..5d4b7f7
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_detail_panel_depots.md
@@ -0,0 +1,18 @@
+---
+name: Panneau détail unifié — DepotsRetraits
+description: DetailPanel unifié pour dépôts/retraits et corrections de solde ; boutons Correction de solde et Supprimer retirés du panel
+type: project
+originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
+---
+Le composant `DetailPanel` dans `DepotsRetraits.jsx` a été unifié :
+
+- Format identique pour tous les types (dépôt, retrait, correction de solde) sous le titre "Détails du mouvement"
+- Champ "Type de mouvement" affiche : "Dépôt", "Retrait" ou "Correction de solde"
+- Montant sans couleur verte/rouge (neutre pour tous les types)
+- **Footer selon le type :**
+ - `auto_remboursement` → mention "Modifiable via la page Remboursements"
+ - `correction` → bouton "Supprimer la correction" (style `dr-detail-edit-btn`)
+ - `depot` → bouton "Modifier le dépôt"
+ - `retrait` → bouton "Modifier le retrait"
+- Boutons "Correction de solde" et "Supprimer" retirés du panel (disponibles via menu ⋮)
+- Props du composant : `{ row, onEdit, onDeleteCorrection }` (onCorrection supprimé)
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_docker_deployment.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_docker_deployment.md
new file mode 100644
index 0000000..c12cc92
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_docker_deployment.md
@@ -0,0 +1,30 @@
+---
+name: project-docker-deployment
+description: "Architecture de déploiement Docker (Traefik, nginx, volumes, icônes seed, DATA_DIR) — pièges et corrections appliquées"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 8f54f673-57ca-4a21-b205-0857456e2ed0
+---
+
+Déploiement Docker sur `crowdlending.croguennec.net` — corrections appliquées en session 2026-06-13.
+
+**Stack :** Traefik (reverse proxy) → crowdlending-frontend (nginx) → crowdlending-backend (Node/Express). Volumes montés sur `~/volumes/crowdlending/data` et `~/volumes/crowdlending/uploads`.
+
+**Pièges résolus :**
+
+1. **Nom du container nginx** : dans `frontend/nginx.conf`, l'upstream doit être `crowdlending-backend` (nom du container Docker), pas `backend`.
+
+2. **Traefik middleware provider** : les middlewares de fichier doivent s'écrire `ipwhitelist-all@file`, pas `ipwhitelist-all@docker`.
+
+3. **`express.static` + `fallthrough`** : sans `{ fallthrough: false }`, un logo manquant appelle `next()` qui tombe sur `app.use('/api', requireAuth)` → 401. Toujours ajouter `fallthrough: false` sur les middlewares statiques publics (logos, icons-files).
+
+4. **Chemin `DATA_DIR`** : les chemins relatifs depuis `__dirname` diffèrent entre dev Windows (`backend/src/routes` → 3 niveaux → projet root/data) et Docker (`/app/src/routes` → 3 niveaux → `/data`). Solution : variable d'env `DATA_DIR=/app/data` dans `docker-compose.yml` ; fallback `../../../data` pour dev. Utilisé dans `referentiel.js` et `server.js`.
+
+5. **Volume masque les fichiers seed** : monter un volume sur `/app/data` masque les fichiers copiés dans l'image. Solution : dossier `backend/icons_seed/` + `docker-entrypoint.sh` qui copie les fichiers manquants dans `/app/data/icons/` au démarrage (copie idempotente, ne remplace pas l'existant).
+
+6. **Sauvegarde DB** : `sqlite3` n'est pas installé dans le container. Utiliser `docker cp $CONTAINER:/app/data/crowdlending.db $BACKUP_PATH` dans `deploy.sh`.
+
+7. **deploy.sh** : `git pull --ff-only` échoue si les branches ont divergé. Utiliser `git pull origin main`.
+
+**How to apply :** Avant tout déploiement depuis zéro, vérifier que `DATA_DIR` est dans `docker-compose.yml`, que `icons_seed/` est peuplé avec les 16 icônes, et que `docker-entrypoint.sh` est exécutable dans le Dockerfile.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_domiciliation_iso.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_domiciliation_iso.md
new file mode 100644
index 0000000..63188f3
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_domiciliation_iso.md
@@ -0,0 +1,42 @@
+---
+name: project-domiciliation-iso
+description: Migration du champ domiciliation des plateformes vers codes ISO 3166-1 alpha-2 avec sélecteur pays à drapeaux
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 62e4d5e0-01c1-41c5-a24b-23f6ec59b5fb
+---
+
+Migration complète du champ `domiciliation` (plateformes + référentiel) de valeurs texte vers codes ISO 3166-1 alpha-2.
+
+**Ce qui a été fait :**
+
+1. **Zod backend** — `z.enum(['france','zone_europeenne','hors_zone_europeenne'])` remplacé par `z.string().min(1).max(100)` dans `plateformes.js` et `referentiel.js`
+
+2. **Migration DB** (`db/index.js`) — trois blocs ajoutés en fin de fichier :
+ - `'france'` → `'FR'`, `'zone_europeenne'`/`'hors_zone_europeenne'` → `'EU'` provisoire sur les deux tables
+ - `pays_siege` (nom français) → `domiciliation` (ISO) sur `plateformes_referentiel` via mapping NAME_TO_ISO complet
+
+3. **Sentinel** — tous les `=== 'france'` / `!= 'france'` remplacés par `=== 'FR'` / `!= 'FR'` dans : `taxreport.js`, `plateformes.js`, `Settings.jsx`, `AdminPlateformes.jsx`, `TaxReport.jsx`, `Cerfa2561Preview.jsx`, `CerfaRecapTable.jsx`, `Cerfa2042Preview.jsx`
+
+4. **UI** — `CountrySelect` (composant existant, codes ISO, drapeaux) intégré dans :
+ - `Settings.jsx` : formulaire création + formulaire édition plateforme
+ - `AdminPlateformes.jsx` : RefModal
+ - `PlatformeProfile.jsx` : section Géographie (lecture + édition), remplace `pays_siege`
+
+5. **DOMICILIATION_LABELS** supprimé dans les 3 fichiers, remplacé par `countryLabel(code)` via `COUNTRIES` de `CountrySelect.jsx`
+
+6. **Filtre référentiel** rendu dynamique (valeurs distinctes en base) dans `AdminPlateformes.jsx`
+
+7. **Badges "hérité"** ajoutés aux 4 champs manquants dans la modale édition Settings : `fiscalite`, `type_produit_fiscal`, `taux_fiscalite_locale`, `logo_filename`
+
+8. **Réinitialiser au référentiel** — ajout ConfirmModal avant exécution + message de succès vert dans le formulaire après reset
+
+**État DB :**
+- `plateformes.domiciliation` : valeurs ISO (`'FR'`, `'EU'` provisoire pour ex zone_europeenne)
+- `plateformes_referentiel.domiciliation` : valeurs ISO, dont 3 migrées depuis `pays_siege`
+- `pays_siege` : champ conservé en DB mais retiré de l'UI (PlatformeProfile)
+
+**Why:** Prépare l'affichage avec drapeaux et ouvre la porte à une granularité pays plutôt que zone géographique, utile pour la fiscalité (crédit d'impôt 2047 par pays).
+
+**How to apply:** Le sentinel clé est `=== 'FR'` (pas `=== 'france'`). Les plateformes encore en `'EU'` sont à corriger manuellement via le sélecteur pays.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_donut_interactif.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_donut_interactif.md
new file mode 100644
index 0000000..5481935
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_donut_interactif.md
@@ -0,0 +1,38 @@
+---
+name: project-donut-interactif
+description: "Fonctionnalités interactives du donut Dashboard — tooltip, clic, mémoire, légende cliquable, labels nets/bruts"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 75df8125-7b0d-44b0-a3e5-9e68fb07b541
+---
+
+Évolutions sur `InteretsDonutChart.jsx`, `InteretsMensuelsChart.jsx` et `InteretsChartContext.jsx`.
+
+**Tooltip au survol (donut)** : chaque segment SVG a `onMouseEnter`/`onMouseLeave`. Le tooltip HTML est positionné via `position: absolute` sur un wrapper `position: relative` autour du SVG (maxWidth 260). Coordonnées calculées depuis le midpoint de l'arc en % du viewBox 240×240.
+
+**Clic → filtrage du bar chart** :
+- Mode multi-types : clic sur un segment isole ce type (`setInclureCapital/Cashback/Interets`)
+- Mode type unique (Reçu/Projeté) : clic appelle `selectActualOnly()` ou `selectProjectedOnly()`
+- `handleArcClick` compare `item.label === 'Intérêts nets' || item.label === 'Intérêts bruts'` (pas 'Intérêts' seul — label dynamique)
+
+**Mémoire / retour arrière** : avant chaque clic, l'état complet est sauvegardé dans `prevStateRef` (useRef) + `hasPrev` (useState). Cliquer sur le centre du donut restaure l'état via `setActualProjected(actual, projected)`. Indicateur visuel : ↩ SVG au-dessus du label central, fond bleu au hover.
+
+**Contexte — exports ajoutés** : `selectActualOnly`, `selectProjectedOnly`, `setActualProjected`.
+
+**Légende donut** :
+- Ordre fixe : Intérêts → Capital → Cashback (dans `donutData`, donc aussi dans le SVG)
+- Chaque ligne est cliquable (même comportement que le quartier)
+- Format compact une ligne : label (muted) + montant (bold) + détail inline `dont X reçus · Y projetés` (11px muted)
+- La fonction `makeItem(label, color, opacity, actualAmt, projectedAmt)` centralise la construction
+- `getLegendDetail` vérifie `label === 'Intérêts nets' || 'Intérêts bruts'` (pas 'Intérêts' seul)
+
+**Labels nets/bruts** :
+- Donut : le label Intérêts devient `netMode ? 'Intérêts nets' : 'Intérêts bruts'` dans `makeItem` (mode multi-types). `netMode` est destructuré depuis `useInteretsChart()`.
+- Bar chart légende : `activeTypes` utilise `label: netMode?'Intérêts nets':'Intérêts bruts'`
+- Bar chart tooltip bas : dernière ligne affiche toujours "Total" (pas "Projeté" pour les futurs)
+
+**En-tête bar chart** : remplacé le texte plat par des badges colorés inline — un par type actif, fond teinté `hexToRgba(color, 0.12)`, point coloré 7px, label 13px dans la couleur du type, suivi de `· année` en muted. La variable `headerLabel` a été supprimée.
+
+**Why:** Améliorer la lisibilité et la cohérence du dashboard.
+**How to apply:** Toujours vérifier les deux variantes du label Intérêts (`'Intérêts nets'` et `'Intérêts bruts'`) dans toute logique qui compare `arc.label` ou `d.label`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_drillcell_filtrage_zero.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_drillcell_filtrage_zero.md
new file mode 100644
index 0000000..a86c926
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_drillcell_filtrage_zero.md
@@ -0,0 +1,14 @@
+---
+name: project-drillcell-filtrage-zero
+description: DrillCellPanel filtre les lignes à 0 selon les critères actifs (intérêts/capital/cashback)
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 43bfd5e1-0e82-496d-bdd2-437e92339323
+---
+
+Les lignes REÇUS et PROJETÉS dont le total est 0 selon les critères actifs (intérêts seuls, capital, cashback) sont masquées dans DrillCellPanel.jsx.
+
+**Why:** Un prêt différé avec uniquement un cashback enregistré apparaissait avec 0,00 € dans la colonne Intérêts — ce qui était correct mais visuellement trompeur en mode "Intérêts uniquement".
+
+**How to apply:** Les listes filtrées `recusVisible` et `projetesVisible` sont calculées via `.filter(r => recuRowValue(r) !== 0)`. Le compteur d'en-tête affiche "Reçus (X/Y)" si des lignes sont cachées (X visible, Y total), sinon "Reçus (X)". Message alternatif si recusVisible.length === 0 mais recus.length > 0 : "Aucun montant à afficher avec les critères sélectionnés". Le bouton "Valider en masse" reste conditionné sur `projetes.length` (total réel, pas filtré).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_drillcellpanel_dashboard.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_drillcellpanel_dashboard.md
new file mode 100644
index 0000000..68b2fb7
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_drillcellpanel_dashboard.md
@@ -0,0 +1,36 @@
+---
+name: project_drillcellpanel_dashboard
+description: DrillCellPanel permanent sur Dashboard + navigation croisée Dashboard↔Remboursements
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 8667511a-3e20-485e-b8ed-c038934d7ec3
+---
+
+## DrillCellPanel permanent sur Dashboard
+
+- Remplace le tableau "Échéances prévues du mois en cours" (supprimé)
+- Toujours visible en bas du Dashboard (`alwaysOpen=true`), initialisé sur le mois courant, toutes plateformes
+- Cliquer une cellule du TIP (TableauInteretsPlateforme) met à jour le DrillCellPanel via `setDrillCell`
+- Props passées : `cell`, `alwaysOpen`, `pfuRates`, `activeView`, `activeId`, `plateformes`, `investissements=[]`, `onBulkDone`, `onEditRecu`, `onEditProjet`
+
+**Why:** Remplace un tableau statique par un panneau drill-down interactif, cohérent avec la page Remboursements.
+
+## Navigation Dashboard → Remboursements → retour
+
+### Dashboard (`onEditRecu` / `onEditProjet`)
+- Reçu cliqué : `navigate('/remboursements?edit-remb=ID&from=dashboard&drill-annee=X&drill-mois=Y[&drill-plat=Z]')`
+- Projeté cliqué : `navigate('/remboursements?open-simul=INV_ID&simul-date=DATE&simul-capital=C&simul-interets=I&from=dashboard&drill-annee=X&drill-mois=Y[&drill-plat=Z]')`
+- Restauration drillCell au retour : `useSearchParams` lit `drill-annee`, `drill-mois`, `drill-plat` dans `_initDrillCell()`
+- Les params URL sont nettoyés au premier rendu (`setSearchParams({}, { replace: true })`)
+
+### Remboursements (auto-ouverture)
+- `autoOpenRef = useRef(false)` — ouverture une seule fois
+- `backToDashboardRef = useRef(null)` — mémorise l'URL de retour `/?drill-annee=X&drill-mois=Y[&drill-plat=Z]`
+- `useEffect([allRows.length, investissements.length, plateformes.length])` — attend que les données soient chargées
+ - `edit-remb` → cherche dans `allRows`, appelle `openEdit(remb)`
+ - `open-simul` → reconstruit l'objet simul depuis les params, appelle `openFromSimul({...})`
+ - Nettoie l'URL avec `navigate('/remboursements', { replace: true })`
+- `close()` modifié : si `backToDashboardRef.current`, navigate vers cette URL puis réinitialise la ref
+
+**How to apply:** Tout nouveau lien "ouvrir modal depuis une autre page" doit suivre ce pattern URL params + autoOpenRef + backRef.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_export_restore.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_export_restore.md
new file mode 100644
index 0000000..09b1f0e
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_export_restore.md
@@ -0,0 +1,53 @@
+---
+name: project_export_restore
+description: "Fonctionnalité export complet prod→dev — ZIP DB+assets, stockage serveur, restauration pending-restore, job 3h00"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 5cf6a868-7a3f-44e5-a0f8-0499f84b1afc
+---
+
+# Export / Restore prod → dev
+
+Fonctionnalité permettant à un admin d'extraire l'état complet d'un environnement (DB + logos + icônes) et de le restaurer ailleurs.
+
+## Fichiers concernés
+
+- `backend/src/routes/admin.js` — routes export/restore
+- `backend/src/jobs/autoExport.js` — job automatique 3h00
+- `frontend/src/pages/admin/ExportSection.jsx` — UI complète
+- `frontend/src/pages/admin/JobLogsSection.jsx` — KNOWN_JOBS inclut `auto_export`
+- `backend/src/db/index.js` — bloc pending-restore au démarrage
+
+## Routes backend (`/api/admin/`)
+
+| Route | Rôle |
+|---|---|
+| `GET /export-full` | Génère un ZIP (VACUUM INTO + assets), sauvegarde sur disque, retourne au navigateur |
+| `GET /exports` | Liste les exports stockés sur le serveur |
+| `POST /exports/upload` | Upload d'un ZIP externe, validation manifest, sauvegarde |
+| `GET /exports/:filename` | Téléchargement d'un export stocké |
+| `DELETE /exports/:filename` | Suppression d'un export stocké |
+| `POST /exports/:filename/restore` | Restauration : backup sécurité + assets + pending-restore + process.exit(0) |
+
+## Mécanisme pending-restore
+
+1. La route restore écrit `{DB_PATH}.pending-restore`
+2. `process.exit(0)` → Docker redémarre le container
+3. Au démarrage, `db/index.js` détecte le fichier `.pending-restore`, le copie vers `DB_PATH`, supprime les fichiers WAL/SHM, puis supprime le `.pending-restore`
+
+## Stockage
+
+- `DATA_DIR/exports/` — max 10 fichiers ZIP, auto-purge des plus anciens
+- Les exports générés automatiquement et manuellement partagent le même dossier
+- Les backups de sécurité pré-restore portent le préfixe `pre-restore-backup-`
+
+## Job automatique
+
+- `startAutoExportJob()` — planifié à 3h00 locale chaque jour (sans dérive)
+- Tracé dans `job_logs` (job_name = `auto_export`)
+- Démarré dans `server.js` aux côtés de `startAutoStatutJob()`
+
+## Incident SQLITE_CORRUPT (2026-06-16)
+
+Voir [[feedback_sqlite_vacuum_into]] — root cause : `db.backup()` produisait un fichier WAL-mode incomplet. Fix : `VACUUM INTO`. Le serveur a été récupéré manuellement via container temporaire node:20-alpine avec bind mount sur `/root/volumes/crowdlending/data`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fichiers_tronques.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fichiers_tronques.md
new file mode 100644
index 0000000..e0ec532
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fichiers_tronques.md
@@ -0,0 +1,25 @@
+---
+name: project_fichiers_tronques
+description: Fichiers JSX/JS tronqués en production — dashboard.js et Investissements.jsx reconstruits
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: b72654be-0979-4e85-a9d7-0fd12a61d170
+---
+
+Fichiers tronqués (octets nuls `\x00` ou fin abrupte) — cause probable : écriture Windows interrompue.
+
+**Reconstruits session 2026-06-01 :**
+- `backend/src/server.js` — 2 octets nuls strippés
+- `backend/src/routes/plateformes.js` — route DELETE `/:id/logo` reconstruite
+- `backend/src/routes/dashboard.js` — fin handler `GET /interets-par-plateforme` reconstruite (796 lignes)
+- `backend/src/routes/taxreport.js` — route `GET /years` reconstruite + routes `GET /cerfa2561` et `GET /cerfa2561/remboursements` reconstruites + `round2` helper manquant ajouté
+- `backend/src/db/index.js` — fin seed `app_icons` reconstruite (960 lignes)
+- `backend/src/routes/simul.js` — `export default router;` manquant ajouté, doublon supprimé
+- `frontend/src/pages/Settings.jsx` — section `NotationPanel` tronquée ligne 2951 (`{fo`) reconstruite
+
+**Reconstruits session 2026-05-29 :**
+- `frontend/src/pages/Investissements.jsx` ✅
+
+**Why:** Troncatures causées par des opérations Edit/Write ou flush incomplet sur Windows (voir [[feedback_file_truncation]]).
+**How to apply:** Si le backend refuse de démarrer avec "Unexpected end of input", vérifier avec `xxd | tail -4` pour détecter octets nuls ou troncature, puis reconstruire avec python3.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fiscalite_locale_remboursements.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fiscalite_locale_remboursements.md
new file mode 100644
index 0000000..6410944
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fiscalite_locale_remboursements.md
@@ -0,0 +1,39 @@
+---
+name: Fiscalité locale sur remboursements
+description: Nouveaux champs interets_bruts_avant_local et taxe_locale pour les plateformes avec retenue à la source locale
+type: project
+originSessionId: c7a8bfe3-1a9f-476a-be50-c4f412629322
+---
+Ajout de la prise en charge de la fiscalité locale (retenue à la source) dans les remboursements, pour les plateformes configurées avec `fiscalite = 'avec_fiscalite_locale'`.
+
+**Modèle financier :**
+- `interets_bruts_avant_local` = montant saisi par l'utilisateur (brut avant retenue)
+- `taxe_locale` = `interets_bruts_avant_local × taux_fiscalite_locale / 100` (auto-calculé, éditable)
+- `interets_bruts` = `interets_bruts_avant_local − taxe_locale` (auto-calculé, lecture seule)
+- PFU français s'applique sur `interets_bruts` (après retenue locale)
+
+**Why:** Les plateformes hors zone française peuvent prélever une taxe locale avant reversement ; celle-ci doit être stockée séparément pour la réconciliation fiscale.
+
+**How to apply:** Toute future page ou export affichant des intérêts sur ces plateformes doit distinguer `interets_bruts_avant_local` (brut plateforme) de `interets_bruts` (base PFU).
+
+**Migrations DB (backend/src/db/index.js) :**
+- `ALTER TABLE remboursements ADD COLUMN interets_bruts_avant_local REAL NOT NULL DEFAULT 0`
+- `ALTER TABLE remboursements ADD COLUMN taxe_locale REAL NOT NULL DEFAULT 0`
+
+**Backend (backend/src/routes/remboursements.js) :**
+- Les deux champs ajoutés au schéma Zod, INSERT et UPDATE (normal type uniquement, pas les bonus)
+
+**Frontend — formulaires modifiés :**
+- `Remboursements.jsx` : champs conditionnels dans le formulaire modal (visible si `avec_fiscalite_locale`)
+- `InvestissementDetail.jsx` : idem dans la modale remboursement + 2 colonnes conditionnelles dans le tableau des remboursements enregistrés ("Int. bruts Plateforme" et "Taxe Locale")
+
+**Logique setField / setRembField :**
+1. Saisie `interets_bruts_avant_local` → calcule `taxe_locale` et `interets_bruts`, puis PFU
+2. Correction manuelle `taxe_locale` → recalcule `interets_bruts` (`avant_local − taxe_locale`), puis PFU
+3. `interets_bruts` est en lecture seule quand `hasLocalTax` est actif
+
+**openRembFromSimul :** si plateforme avec retenue locale, les `interets_prevus` de la projection sont traités comme `interets_bruts_avant_local` et la taxe est pré-calculée.
+
+**Calcul solde porte-monnaie — règle définitive (corrigé dans dashboard.js et DepotsRetraits.jsx) :**
+- `flat_tax` → `net_recu` (PFU prélevé à la source)
+- Hors France (`sans_fiscalite_locale` ou `avec_fiscalite_locale`) → `capital + cashback + interets_bruts` (pas de PFU à la source ; l'investisseur déclare le PFU séparément ; `interets_bruts` est déjà net de la retenue locale)
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fiscalite_override.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fiscalite_override.md
new file mode 100644
index 0000000..f290b09
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_fiscalite_override.md
@@ -0,0 +1,38 @@
+---
+name: project_fiscalite_override
+description: Override de fiscalité par investissement — exonération flat tax pour PEA-PME ou comptes similaires
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 5682d634-7345-40a8-8184-f617d1dd8444
+---
+
+## Champ ajouté sur `investissements`
+
+| Colonne | Type | Valeurs |
+|---|---|---|
+| `fiscalite_override` | TEXT nullable | `null` (suit la plateforme) \| `'exonere'` (traité comme sans_fiscalite_locale) |
+
+## Route backend
+
+`PUT /api/investissements/:id/fiscalite-override { override: 'exonere' | null }` — bascule l'état, vérifie ownership.
+
+## Comportement quand `fiscalite_override = 'exonere'`
+
+- `exonere` est un cas de `isTaxIndicatif = true` (voir [[project_istaxindicatif]]).
+- Les prelev sont **calculés automatiquement** (indicatif) depuis les taux PFU, mais les champs sont en **lecture seule** et labellisés "— indicatif".
+- `net_recu` = capital + cashback + interets_bruts (pas de déduction des prelev indicatifs).
+- Seuls les **futurs** remboursements sont affectés — les existants gardent leurs valeurs (sauf recalcul via reprocess).
+
+## UI dans la fiche détail
+
+- **Badge** vert "Exonéré flat tax" affiché à côté du titre "Informations du projet" quand actif.
+- **Menu ⋮** de la carte : option "Exonérer de la flat tax" / "Rétablir la flat tax" (libellé adapté à l'état courant) → ouvre une modale de confirmation (`fiscaliteOverrideConfirm` state) avec explication et bouton Confirmer.
+- La modale est rendue juste avant le bloc `{cardMenu && ...}`.
+
+## Auto-reinvest et fiscalite_override
+
+Dans `remboursements.js` (POST), le montant auto-réinvesti utilise `inv.fiscalite === 'flat_tax'` pour choisir entre `interets_nets` et `interets_bruts`. Si l'investissement est exonéré mais la plateforme reste `flat_tax`, ce calcul n'est pas encore ajusté — à surveiller si l'auto-reinvest est activé sur un investissement exonéré.
+
+**Why:** Investissements logés en PEA-PME (ex. Baltis) ne subissent pas la retenue à la source ; les intérêts sont versés bruts.
+**How to apply:** Vérifier `inv.fiscalite_override === 'exonere'` partout où `plateforme_fiscalite === 'flat_tax'` conditionne un calcul.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_formulaires_remboursement.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_formulaires_remboursement.md
new file mode 100644
index 0000000..c481ff6
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_formulaires_remboursement.md
@@ -0,0 +1,33 @@
+---
+name: formulaires-remboursement-refacto
+description: "Refactorisation des modales de saisie de remboursement dans InvestissementDetail et Remboursements — grille 3 colonnes, sections, règles isIndicatif"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 9f8f19b9-5456-41ce-b666-455acf9ebccb
+---
+
+Les deux modales de remboursement ont été refactorisées pour adopter le même format.
+
+**Structure commune (grille 3 colonnes, width=900) :**
+- Section **Remboursement** → Capital | Intérêts bruts | Cashback (titre AVANT les champs hasLocalTax)
+- Section **Imposition** (+ "— indicatif" si applicable) → Prélèvements sociaux | IR | **Total prélèvements** (readonly grisé = PS + IR)
+ - Ligne texte : "Montant des intérêts après imposition : xxx €" (pas de cadre, texte simple)
+- Section **Versement** → Date | Montant versé (readonly calculé)
+ - Ligne commentaire : explication du calcul selon isIndicatif (voir formulations ci-dessous)
+
+**Champs supprimés des deux formulaires :** Statut, Notes, Mode de remboursement, Intérêts nets (remplacé par Total prélèvements + ligne commentaire).
+
+**Règle isIndicatif (alignée entre les deux pages) :**
+- `isExonere = currentInv?.fiscalite_override === 'exonere'`
+- `isIndicatif = isExonere || currentPlat?.fiscalite !== 'flat_tax'`
+- Si indicatif : prélèvements grisés en lecture seule, titre "Imposition — indicatif"
+- Si indicatif : `netRecu = capital + cashback + interets_bruts` (pas nets)
+
+**Formulations commentaire Montant versé :**
+- Indicatif : "L'imposition n'est pas déduite du montant versé (fiscalité appliquée à titre indicatif)."
+- Flat tax : "L'imposition est déduite du montant versé (capital + cashback + intérêts nets)."
+
+**Why:** Harmonisation UX + application correcte de la règle d'exonération flat tax (voir [[project_istaxindicatif]]).
+
+**How to apply:** Toute nouvelle page avec formulaire de remboursement doit reproduire ce pattern de grille et la logique isIndicatif.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_formulaires_ux.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_formulaires_ux.md
new file mode 100644
index 0000000..daedd92
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_formulaires_ux.md
@@ -0,0 +1,32 @@
+---
+name: project-formulaires-ux
+description: Améliorations UX des formulaires Nouvel investissement et Nouveau remboursement (session 2026-05-30)
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: c75c80b6-ddc2-47f8-8f5d-1b772f53c3b5
+---
+
+## Formulaire "Nouveau remboursement" (Remboursements.jsx)
+
+- **Investissement select** : les bonus (🎁 Bonus Parrainage, 🏆 Bonus Plateforme) apparaissent en tête de liste, avant le séparateur et les investissements
+- **Investissement select** : les investissements sont préfixés du nom de plateforme (`Indemo — LV0000109486 (64,69 €)`) et triés alphabétiquement par plateforme puis nom projet
+- **Ouverture** : aucun investissement présélectionné (valeur `''` → tiret)
+- **Bonus — champ "Investisseur bénéficiaire" supprimé** : `bonus_investisseur_id` est auto-déduit de `plateforme.investisseur_id` lors de la sélection de la plateforme dans le select bonus
+- **Bonus — layout** : ligne 1 = Investissement (span 1) + Plateforme (span 2) ; ligne 2 = Date (span 1) + Montant (span 2)
+
+**Why:** UX — éviter la saisie redondante du détenteur déjà porté par la plateforme ; meilleure lisibilité de la liste d'investissements.
+
+**How to apply:** Si un futur formulaire bonus est ajouté, appliquer le même pattern : pas de champ détenteur visible, dériver depuis la plateforme sélectionnée.
+
+---
+
+## Formulaire "Nouvel investissement" (Investissements.jsx)
+
+- **Plateforme** : aucune plateforme présélectionnée à l'ouverture (tiret par défaut) — corrigé dans `openNew()`, et les deux `useEffect` (`?new=1` et `pendingNew`)
+- **Champ "Détenteur" supprimé** : remplacé par ` ` ; `investisseur_id` est auto-alimenté via `investisseurForPlat(platId)` dès qu'une plateforme est choisie
+- **Champs obligatoires ajoutés** : Taux annuel (`required`), Durée en mois (`required`), Date 1ère échéance (`required`) — validation navigateur uniquement, pas de contrainte DB
+
+**Why:** Simplifier la saisie et éviter des investissements sans données financières essentielles.
+
+**How to apply:** Ne pas recréer le select Détenteur dans ce formulaire ; la logique `investisseurForPlat` dans le onChange de la plateforme suffit.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_icons_library.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_icons_library.md
new file mode 100644
index 0000000..8ea9ff5
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_icons_library.md
@@ -0,0 +1,62 @@
+---
+name: project-icons-library
+description: "Bibliothèque d'icônes admin — table app_icons, route /api/icons, section Admin, nettoyage fond SVG, composant AppIcon dans Settings"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: d7532bd8-8eb4-4b40-8c39-8f569dcf432f
+---
+
+Système de gestion d'icônes applicatives accessible via `/admin?section=icons`.
+
+## Tables (backend/src/db/index.js)
+```sql
+CREATE TABLE IF NOT EXISTS app_icons (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE,
+ filename TEXT NOT NULL,
+ description TEXT,
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now'))
+)
+CREATE TABLE IF NOT EXISTS app_icons_history (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ icon_id INTEGER NOT NULL REFERENCES app_icons(id) ON DELETE CASCADE,
+ filename TEXT NOT NULL,
+ replaced_at TEXT NOT NULL
+)
+```
+10 icônes seedées au premier démarrage (guard count=0).
+
+## Route (backend/src/routes/icons.js)
+- `GET /api/icons` — liste toutes les icônes
+- `GET /api/icons/:name` — détail
+- `GET /api/icons/:name/history` — historique (requireAdmin)
+- `POST /api/icons` — création (upload multer, requireAdmin)
+- `PUT /api/icons/:name` — remplacement avec archivage (max 10 versions)
+- `PATCH /api/icons/:name` — modifier description uniquement
+- Fichiers servis en statique : `GET /api/icons-files/:filename`
+- Dossier physique : `data/icons/` à la racine du projet (pas dans backend/)
+
+## Nettoyage fond SVG à l'upload (stripSvgBackground)
+Supprime automatiquement les fonds blancs/gris dans les SVGs uploadés :
+1. `background-color` dans `style=""` sur ``
+2. Attribut `enable-background` (artefact Illustrator)
+3. `` blanc/quasi-blanc couvrant le canvas entier
+4. `` blanc/quasi-blanc couvrant le canvas — **patterns raster-trace** (ex: `fill="#FEFEFE"`) détectés via `isNearWhite()` (seuil ≥ #F0F0F0) + `pathCoversCanvas()` (coordonnées proches de 0 ET de vbW/vbH)
+- Rasters PNG/JPG/WebP : fond blanc → transparent via sharp (seuil R,G,B > 240)
+- Fichiers `design/Icones/svg/*.svg` nettoyés rétroactivement
+
+## Composant AppIcon (frontend/src/pages/Settings.jsx)
+Remplace les SVGs inline dans le bloc "Couleurs des graphiques" :
+```jsx
+const ICONS_BASE = '/api/icons-files/';
+function AppIcon({ filename, size = 22 }) { ... }
+```
+- `AppearanceSection` charge `GET /api/icons` au montage → dictionnaire `{ name: filename }`
+- Utilisé avec `libIcons.interets`, `libIcons.capital`, `libIcons.cashback` à taille 33px
+- CSS `.app-lib-icon` + `[data-theme="dark"] .app-lib-icon { filter: invert(1) }`
+- Labels associés : `font-size: 1.1rem`, `font-weight: 600`
+
+**Why:** Source unique — remplacer l'icône dans l'admin met à jour automatiquement tous les endroits qui l'affichent.
+**How to apply:** Pour afficher une icône de la bibliothèque ailleurs dans l'app, charger `GET /api/icons` et utiliser ` `.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_icons_sidebar_titles.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_icons_sidebar_titles.md
new file mode 100644
index 0000000..dd5ab52
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_icons_sidebar_titles.md
@@ -0,0 +1,23 @@
+---
+name: project-icons-sidebar-titles
+description: Icônes de la bibliothèque dans la sidebar nav et titres de pages
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 743b204f-7d6b-45ed-b91c-387060b5941e
+---
+
+Icônes de la bibliothèque intégrées dans l'interface à deux endroits.
+
+**Sidebar (`Layout.jsx`)** — composant `NavIcon` : affiche l'icône bibliothèque (` `) si disponible, sinon fallback SVG inline. Icônes chargées via `api.get('/icons')` au montage du Layout, stockées dans `navIcons` state. Taille : 24px. CSS : `.nav-lib-icon` avec `filter: invert(1)` permanent (sidebar toujours sombre), opacity 0.85 au repos / 1 sur lien actif.
+
+Mapping noms bibliothèque → pages :
+- `dashboard` → Tableau de bord
+- `investissement` → Investissements
+- `depots-retraits` → Dépôts / Retraits
+- `remboursement` → Remboursements
+- `tax` → Fiscalité
+
+**Titres de pages** — composant `PageIcon` (`frontend/src/components/PageIcon.jsx`) : ` ` inline dans le `h2` du topbar. Cache module-level partagé (un seul appel API). Taille : 40px. Rendu inline (`display: inline; verticalAlign: middle; marginRight: 10`). Importé dans Dashboard, Investissements, DepotsRetraits, Remboursements, Fiscal2778.
+
+**Why:** Ne jamais wrapper dans un div ou modifier `display` du h2 — voir [[feedback-pageicon-topbar]].
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_interets_mensuels_chart.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_interets_mensuels_chart.md
new file mode 100644
index 0000000..e5d4ee2
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_interets_mensuels_chart.md
@@ -0,0 +1,114 @@
+---
+name: project-interets-mensuels-chart
+description: InteretsMensuelsChart + InteretsDonutChart + InteretsChartContext — barres empilées + donut + mode TOUT (vue annuelle)
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 9d8a4dc8-0caa-4a53-bf00-040dfe2afe4a
+---
+
+## Architecture (3 fichiers)
+
+- `frontend/src/context/InteretsChartContext.jsx` — état partagé entre les deux graphiques
+- `frontend/src/components/InteretsMensuelsChart.jsx` — graphique barres empilées
+- `frontend/src/components/InteretsDonutChart.jsx` — donut de répartition
+
+Utilisés dans `Dashboard.jsx` via `` wrapper, layout flex 2/3 + 1/3 avec `alignItems: 'stretch'`.
+
+## InteretsChartContext
+
+Centralise : `annee`, `inclureInterets/Capital/Cashback`, `selectedMonth`, `rawData`, `months` (useMemo), `annualTotal`, `availableYears`, couleurs UiContext (`chartInterets/Capital/Cashback`).
+
+Reçoit en props depuis Dashboard : `netMode`, `pfuRates`, `activeView`, `activeId`.
+
+**Deux fetches :**
+- `GET /api/dashboard/interets-mensuels?annee=&scope=` → `rawData` — retourne `{ rembourses, projections, annees }`. LEFT JOINs pour bonus (`bonus_investisseur_id`).
+- `GET /api/dashboard/interets-annuels?scope=` → `rawDataGlobal` — retourne `{ rembourses, projections, annees }` groupés par année. Projections filtrées `>= date('now', 'start of month')`.
+
+**Mode global (TOUT) :**
+- `modeGlobal` (bool, false par défaut) + `toggleModeGlobal`
+- `years` useMemo avec fenêtrage : max 8 années passées, année courante ≤ position 9, toujours ≥ 3 années projetées, max 12 barres
+- `selectedYear` (number | null) + `setSelectedYear`
+- `globalTotal` = somme de tous les `years[].total`
+
+Algorithme fenêtrage `years` :
+```js
+pastYears = allSorted.filter(y < currentYear)
+futureYears = allSorted.filter(y > currentYear)
+pastCount = min(pastYears.length, 8)
+pastSlice = pastYears.slice(-pastCount)
+usedSlots = pastCount + 1
+projCount = max(3, 12 - usedSlots)
+futureSlice = futureYears.slice(0, projCount)
+visibleYears = [...pastSlice, currentYear, ...futureSlice]
+```
+
+Chaque objet `year` dans `years[]` a les mêmes champs qu'un `month` (capitalAmt, cashbackAmt, interetsAmt, capitalProjAmt, interetsProjAmt, actual, projected, total, isCurrent, isFuture, label).
+
+## InteretsMensuelsChart — barres empilées
+
+- 3 toggles avec icônes de la bibliothèque (`GET /api/icons`, ` `, opacity active/inactive)
+- `ICONS_BASE = '/api/icons-files/'` ; noms : `interets`, `capital`, `cashback`
+- Segments empilés : Capital → Cashback → Intérêts (bottom to top)
+- Couche `actual` (opaque) + `projected` (`hexToRgba(color, 0.28)`)
+- Sélecteur année : fenêtre glissante ±1 an, `windowStart` state local
+- **Bouton TOUT** : après les chevrons années, style `.solde-range-btn / .active` ; en mode TOUT les chevrons et le sélecteur d'années sont masqués
+- En mode TOUT : `items = years`, `barCount = years.length`, gap=10, labels = années, clic → `setSelectedYear(toggle)`
+- En mode mensuel : `items = months`, `barCount = 12`, gap=8, labels = mois, clic → `setSelectedMonth(toggle)`
+- `filteredTotal(item)` unifié pour les deux modes → `rawMax`, `totalH`, label barre, grand total en-tête
+- Légende bas : swatches reçu/projeté + sélecteur Reçu/Projeté
+
+## InteretsDonutChart — donut de répartition
+
+- Asservi à `InteretsChartContext` : mêmes toggles + `selectedMonth` (mode mensuel) ou `selectedYear` (mode TOUT)
+- Mode mensuel : vue annuelle → totaux 12 mois ; vue mensuelle → données du mois cliqué
+- Mode TOUT : vue globale → totaux toutes années ; vue annuelle → données de l'année cliquée
+- Clic sur le sous-titre en légende → reset sélection (`setSelectedMonth(null)` ou `setSelectedYear(null)`)
+- **Caps arrondis radiaux** : `roundedArcPath()` — path SVG `M outerS A outerArc A capR capR 0 0 1 innerE A innerArc A capR capR 0 0 0 outerS Z`
+ - Le cap est un semicercle reliant outer→inner à l'angle exact du segment, sans débordement angulaire
+ - Cercle complet (1 segment, GAP=0) : deux demi-cercles + `fillRule="evenodd"`
+- GAP = 4° entre segments ; GAP = 0 si segment unique
+- Constantes : `CX=CY=120`, `OUTER_R=108`, `INNER_R=72`, `capR=18`
+- Anneau de fond : ``
+- Texte central : label (mois/année/Total) + montant
+- Titre "Répartition" : une ligne EN BAS dans la zone légende (`justifyContent: space-between`, séparé par `borderBottom`)
+- SVG : `viewBox="0 0 240 240"`, `maxWidth: 260`, `height: auto`
+
+## Layout Dashboard
+
+```jsx
+
+
+
+```
+
+`alignItems:'stretch'` + `height:'100%'` sur la carte donut → même hauteur. `maxWidth:260` sur le SVG évite que le donut explose en hauteur.
+
+## Sélecteur Reçu / Projeté
+
+`showActual` + `showProjected` dans le contexte (+ `toggleActual` / `toggleProjected`).
+
+Règle "force reçu" : si `showProjected` est déjà false, `toggleActual` ne peut pas désactiver `showActual`.
+
+Sélecteur UI dans la ligne légende du bar chart :
+- Fond `#f0f0f0`, borderRadius 8, padding 3
+- Pill actif : fond blanc, boxShadow, fontWeight 600, couleur `#1a1a2e`
+- Pill inactif : transparent, couleur `#9ca3af`
+
+Impacts :
+- `buildSegments` filtre `actual[]` si `!showActual`, `projected[]` si `!showProjected`
+- `filteredTotal(item)` = somme conditionnelle → utilisé pour `rawMax`, `totalH`, label valeur, grand total en-tête
+- `totalH` doit utiliser `filteredTotal` (pas `item.total`) sinon le label sort du viewBox quand un filtre est actif
+- Donut : `sumA(key)` / `sumP(key)` retournent 0 si le filtre correspondant est off
+
+## Fichiers backend associés
+
+`backend/src/routes/dashboard.js` — deux endpoints :
+- `/interets-mensuels` : groupé par mois pour une année donnée
+- `/interets-annuels` : groupé par année, toutes années, projections >= début mois courant
+
+**Why:** Visualiser répartition mensuelle/annuelle/multi-années par type (intérêts/capital/cashback), reçu vs projeté.
+**How to apply:** Pour ajouter un 3e graphique lié, consommer `useInteretsChart()`. Toujours passer par python3/bash pour éditer ces fichiers (jamais Edit/Write direct sur les grands composants SVG).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_interets_nets_investissements.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_interets_nets_investissements.md
new file mode 100644
index 0000000..3892e9b
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_interets_nets_investissements.md
@@ -0,0 +1,15 @@
+---
+name: Correction intérêts nets — page Investissements
+description: net_recu_total remplacé par interets_nets_total pour afficher les vrais intérêts nets
+type: project
+originSessionId: c8c8964c-17e5-40a2-8e72-52261af1d3e9
+---
+Bug corrigé : la colonne "Intérêts (Net)" du tableau par plateforme dans Investissements.jsx affichait `net_recu_total` (= capital + cashback + intérêts nets) au lieu des seuls intérêts nets.
+
+**Fix backend** (`backend/src/routes/investissements.js`) : ajout d'une sous-requête `SUM(r.interets_nets)` retournée sous le nom `interets_nets_total`, en plus du `net_recu_total` conservé.
+
+**Fix frontend** (`frontend/src/pages/Investissements.jsx`) : tous les usages de `net_recu_total` pour afficher des intérêts nets remplacés par `interets_nets_total`.
+
+**Why:** `net_recu = capital + cashback + interets_nets` — utiliser ce champ pour afficher des "intérêts" gonflait artificiellement la valeur du capital remboursé.
+
+**How to apply:** Pour tout calcul d'intérêts nets sur les investissements, utiliser `interets_nets_total` (SUM des interets_nets des remboursements). `net_recu_total` reste disponible si on veut afficher le flux total reçu.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_invchart_capital_encours.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_invchart_capital_encours.md
new file mode 100644
index 0000000..c2e8a14
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_invchart_capital_encours.md
@@ -0,0 +1,109 @@
+---
+name: project_invchart_capital_encours
+description: "Formule unifiée capital investi (Dashboard + Investissements) : montant_investi + reinvests(≤date) − capital_remboursé(≤date)"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 09de8d8d-fa6d-4d2f-93bf-3920d2b93158
+---
+
+## Formule canonique — capital restant engagé à une date D
+
+```
+capital_restant(D) = montant_investi
+ + SUM(reinvestissements où date_reinvestissement ≤ D)
+ − SUM(remboursements.capital où date_remb ≤ D AND type='normal')
+```
+
+Cette formule doit être appliquée identiquement sur toutes les pages et tous les composants.
+
+**Why:** Des divergences existaient car certains endroits utilisaient `capital_total` (tous reinvests sans filtre date) ou `montant_investi` brut (sans soustraire le capital remboursé). Corrigé en session 2026-05-29.
+
+---
+
+## Dashboard — backend (dashboard.js)
+
+### portfolio.encours / portfolio.en_defaut
+
+La requête SQL utilise des sous-requêtes corrélées :
+```sql
+SELECT i.id, ...
+ COALESCE(SUM(CASE WHEN i.statut='en_cours' THEN
+ i.montant_investi
+ + COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
+ - COALESCE((SELECT SUM(rb.capital) FROM remboursements rb WHERE rb.investissement_id = i.id AND rb.type='normal'), 0)
+ END), 0) AS encours,
+ -- idem pour en_defaut (en_retard + procedure)
+FROM investissements i WHERE ...
+```
+
+### capitalMensuel (interets-par-plateforme)
+
+Boucle sur les 12 mois de l'année demandée, pour chaque investissement :
+```js
+capital_restant = montant_investi
+ + reinvestsCumul(≤ lastDay) // depuis reinvestMap
+ - capitalRembCumul(≤ lastDay) // depuis capitalRembMap (type='normal')
+ - capitalSimulCumul // depuis simul_remboursements :
+ // date_prevue > today
+ // ET date_prevue > last_remb_date
+ // ET date_prevue ≤ lastDay
+```
+`capitalSimulCumul` permet aux années futures (2027+) de décrémenter le capital selon les projections de remboursement.
+
+### KPI "Capital investi" Dashboard (Dashboard.jsx — DashboardKpis)
+
+- **Mode global / année courante** → `portfolio.encours + portfolio.en_defaut`
+- **Année passée** → dernier mois de `capitalMensuelData` pour cette année (ex. décembre 2025 = 24 345,13 €)
+- `capitalMensuelData` est remonté via callback `onCapitalMensuel` depuis `TableauInteretsPlateforme`
+- `prevCapitalAnnee` (référence N-1 pour TrendBadge) utilise encore `capitalParAnneeMap` (capital souscrit par année) faute de données N-1 disponibles
+
+---
+
+## Page Investissements (Investissements.jsx)
+
+### Données chargées
+
+- `allRembs` : tous les remboursements (scope courant)
+- `allReinvests` : tous les réinvestissements via `GET /api/reinvestissements?scope=all`
+
+### Memos de coupure (cutoff = platYear ? `${platYear}-12-31` : null)
+
+- `rembParInv` → `{ [inv_id]: { capital, interets_bruts, interets_nets } }` filtrés par cutoff
+- `capRembParInv` → alias `{ [inv_id]: capital }` pour rétrocompatibilité
+- `reinvestCumulParInv` → `{ [inv_id]: montant_cumulé }` filtrés par cutoff
+- `lastRembDateMap` → `{ [inv_id]: date_dernière_remb }` calculé depuis `allRembs`
+
+### Filtre année (platYear) — sémantique "actif au 31/12"
+
+`isActiveAtEndOfYear(r)` : investissement inclus si :
+1. `date_souscription ≤ ${platYear}-12-31`
+2. ET statut en [`en_cours`, `en_retard`, `procedure`] (actif aujourd'hui)
+ OU `date_cible ?? lastRembDateMap[r.id] >= ${platYear}-12-01` (clôturé en décembre ou après)
+
+Remplace l'ancien filtre `date_souscription.slice(0,4) === platYear` dans `kpiRows`, `platData`, et `chartRows`.
+
+**Why:** Le filtre par année de souscription excluait les prêts souscrits en 2024 encore actifs en 2025, créant une divergence avec le Dashboard (24 345 € vs 16 118 €).
+
+### Calcul capInv dans totals / platData / invDistRows
+
+```js
+const capInv = r.montant_investi + (reinvestCumulParInv[r.id] || 0);
+const capRemb = capRembParInv[r.id] || 0;
+const capRestant = Math.max(0, capInv - capRemb);
+```
+
+### InvChart.jsx
+
+Props : `rows`, `remboursements`, `reinvestissements`, `platYear`
+
+Deltas chronologiques :
+- `+montant_investi` à `date_souscription` (plus `capital_total` — supprimé)
+- `+reinvestissement.montant` à `date_reinvestissement` (nouveau)
+- `−remboursement.capital` à `date_remb`
+
+---
+
+## GET /api/reinvestissements — mode scope=all
+
+Route `reinvestissements.js` étendue pour supporter `?scope=all` (tous les reinvestissements de l'utilisateur, via JOIN investissements + investisseurs). Utilisé par la page Investissements au chargement.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_investissement_detail_features.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_investissement_detail_features.md
new file mode 100644
index 0000000..a984042
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_investissement_detail_features.md
@@ -0,0 +1,27 @@
+---
+name: project-investissement-detail-features
+description: "Fonctionnalités ajoutées à InvestissementDetail — modifier DPE, traitement en masse remboursements, correction ConfirmModal"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 06bf2c9d-6350-45bd-a65f-4399f1218408
+---
+
+## Fonctionnalités du menu ⋮ bloc Projections (simulMenu)
+
+Trois items dans le menu `simulMenu` de la carte "Projections de remboursements", dans cet ordre :
+1. **Modifier la date de la 1ère échéance** — ouvre `editDpeModal` ; champ date pré-rempli avec `inv.date_premiere_echeance` ; sauvegarde via `PUT /investissements/:id` (payload minimal, pas de spread `inv`) puis `POST /simul/recalculate` → `load()`.
+2. **Traitement en masse des remboursements** — ouvre `bulkRembModal` ; calcule les échéances simul antérieures à aujourd'hui sans remboursement correspondant (match exact date ou mois, sans fallback capRestant), affiche tableau récap + totaux, crée les remboursements séquentiellement via `POST /remboursements` avec barre de progression, écran succès final.
+3. **Régénérer l'échéancier** — existant.
+
+## États ajoutés à InvestissementDetail
+
+- `editDpeModal`, `editDpeValue`, `editDpeErr`, `editDpeSaving`
+- `bulkRembModal`, `bulkRembItems`, `bulkRembProcessing`, `bulkRembProgress`, `bulkRembDone`
+
+## Bug corrigé — ConfirmModal manquant
+
+Le state `rowDeleteConfirm` (suppression remboursement via menu ⋮ du tableau) était alimenté mais le composant `` n'était jamais monté dans le JSX. Ajouté en fin de template.
+
+**Why:** Oubli lors d'une précédente session — le state existait mais le point de montage manquait.
+**How to apply:** Toujours vérifier que chaque état de type `*Confirm` ou `*Modal` a bien son composant `` ou `` correspondant dans le return.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_istaxindicatif.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_istaxindicatif.md
new file mode 100644
index 0000000..6bf018a
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_istaxindicatif.md
@@ -0,0 +1,75 @@
+---
+name: project_istaxindicatif
+description: Règle isTaxIndicatif — prelev indicatifs vs réels, net_recu brut vs net ; route reprocess en masse
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 5682d634-7345-40a8-8184-f617d1dd8444
+---
+
+## Règle centrale `isTaxIndicatif`
+
+```js
+isTaxIndicatif = (fiscalite !== 'flat_tax') OR (fiscalite_override === 'exonere')
+```
+
+Cas couverts :
+- Plateforme `sans_fiscalite_locale` ou `avec_fiscalite_locale` (étrangères) → indicatif
+- Plateforme `flat_tax` mais investissement `exonere` (PEA-PME) → indicatif
+- Plateforme `flat_tax` sans override → prelev réels
+
+## Comportement selon la règle
+
+| `isTaxIndicatif` | Prelev calculés ? | Champs prelev | `net_recu` |
+|---|---|---|---|
+| `false` | Oui, réels | Modifiables | capital + cashback + interets_nets |
+| `true` | Oui, indicatifs | Lecture seule, label "indicatif" | capital + cashback + interets_bruts |
+
+**Pourquoi** : les plateformes étrangères et les PEA-PME ne prélèvent pas la flat tax à la source. Le montant versé à l'investisseur est le brut. Les prelev sont stockés à titre informatif pour la déclaration fiscale personnelle.
+
+## Backend — `remboursements.js`
+
+### Helper `getTaxIndicatif(investissementId)`
+```js
+function getTaxIndicatif(investissementId) {
+ const row = db.prepare(`
+ SELECT p.fiscalite, i.fiscalite_override
+ FROM investissements i JOIN plateformes p ON p.id = i.plateforme_id
+ WHERE i.id = ?
+ `).get(investissementId);
+ return row.fiscalite !== 'flat_tax' || row.fiscalite_override === 'exonere';
+}
+```
+
+### `computeChamps(body, isTaxIndicatif)`
+```js
+const interets_nets = round2(body.interets_bruts - body.prelev_sociaux - body.prelev_forfaitaire);
+const net_recu = isTaxIndicatif
+ ? round2(capital + cashback + body.interets_bruts)
+ : round2(capital + cashback + interets_nets);
+```
+
+Utilisé dans POST et PUT handlers.
+
+## Frontend — `InvestissementDetail.jsx`
+
+```js
+const isExonere = inv?.fiscalite_override === 'exonere';
+const isIndicatif = isExonere || currentPlat?.fiscalite !== 'flat_tax';
+```
+
+- Prelev + interets_nets : `readOnly={isIndicatif}`, style grisé (`var(--surface-2)`), label "— indicatif"
+- Recalcul auto des prelev depuis taux PFU dans `updateRembField` : toujours calculé, même en mode indicatif
+- Montant remboursé affiché = capital + cashback + interets_bruts si indicatif
+
+## Route reprocess en masse
+
+`POST /api/remboursements/reprocess` — recalcule tous les remboursements `type='normal'` de l'utilisateur connecté (tous ses investisseurs).
+
+Champs recalculés : `taxe_locale`, `interets_bruts`, `prelev_sociaux`, `prelev_forfaitaire`, `interets_nets`, `net_recu` + montant du retrait auto associé (`depots_retraits` avec `source='auto_remboursement'`).
+
+Logique : join investissements + plateformes + investisseurs, taux PFU de l'année du remboursement, `isTaxIndicatif` appliqué à `net_recu`.
+
+Déclenchement UI : bouton dans `Settings.jsx` section `nettoyage` (groupe "Mes données") → modale de confirmation → appel `api.post('/remboursements/reprocess', {})`. ⚠️ Anciennement dans `MonCompte.jsx` — déplacé en Settings lors de la réorganisation NAV (juin 2026).
+
+**How to apply** : toute nouvelle route ou formulaire manipulant `prelev_sociaux`, `prelev_forfaitaire`, `interets_nets`, `net_recu` doit appliquer cette règle. Checker `getTaxIndicatif()` (backend) ou `isIndicatif` (frontend).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_logo_topbar_detail.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_logo_topbar_detail.md
new file mode 100644
index 0000000..0a0efce
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_logo_topbar_detail.md
@@ -0,0 +1,17 @@
+---
+name: Logo plateforme dans la topbar d'InvestissementDetail
+description: Si la plateforme a un logo, il remplace le nom textuel dans la topbar ; dark mode via .logo-plateforme CSS
+type: project
+originSessionId: f8882910-b8d1-4a87-ab0b-2962aa6aad7b
+---
+Dans `InvestissementDetail.jsx`, la topbar affiche le logo de la plateforme si disponible, sinon le nom textuel.
+
+**Backend :** `GET /api/investissements/:id` retourne `plateforme_logo` (alias de `p.logo_filename`) via le JOIN sur plateformes. Aussi `plateforme_fiscalite` pour la logique brut/net du réinvestissement auto.
+
+**Frontend :**
+- URL du logo : `` `${(import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '')}/api/logos/${inv.plateforme_logo}` ``
+- Classe CSS `logo-plateforme` (définie dans `styles.css`) gère le mode sombre : `filter: invert(1) hue-rotate(180deg)` sur `[data-theme="dark"]`
+- Taille : `height: 38px, maxWidth: 140px, objectFit: contain`
+- Le nom de la plateforme reste en `title` sur l'image pour le survol
+
+**Bouton retour :** "← Investissements" déplacé à droite de la topbar (à la place des 3 anciens boutons Exporter/Réinvestir/Modifier supprimés).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_logos_referentiel.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_logos_referentiel.md
new file mode 100644
index 0000000..4898a9f
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_logos_referentiel.md
@@ -0,0 +1,33 @@
+---
+name: project_logos_referentiel
+description: "Bibliothèque de logos dans Admin > Référentiels — upload PNG/SVG par plateforme du référentiel, auto-push vers plateformes liées"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 009410f3-ba29-4d50-9909-840d1411ff1a
+---
+
+Section "Logos des plateformes" ajoutée dans Admin > Référentiels (section id `logos-ref`).
+
+**Backend (`backend/src/routes/referentiel.js`) :**
+- `POST /api/referentiel/:id/logo` — upload multer (data/logos/), traitement fond blanc (sharp + SVG cleanup), update `logo_filename` dans `plateformes_referentiel`, auto-push vers plateformes liées non-overridées
+- `DELETE /api/referentiel/:id/logo` — supprime fichier + NULL logo_filename + propagation
+- Routes protégées par requireAdmin (via mount dans server.js)
+- Fichiers nommés `ref_{id}_{timestamp}.{ext}`
+
+**Frontend (`Admin.jsx`) :**
+- Composant `LogosBibliothequeSection` : grille de cards, une par entrée référentiel
+- Carte avec logo ou croix placeholder SVG + texte "Logo manquant" si `logo_filename` null
+- Un seul ` ` caché partagé via `useRef` (pas `React.useRef` — React n'est pas importé en default dans Admin.jsx)
+- Upload via `fetch` multipart (pas `api.post` car FormData)
+- Bouton "Remplacer" ou "Importer" selon présence du logo ; bouton "×" pour suppression
+- Dans `ReferentielSection` : miniature 32×32 ou `×` pointillé en 1ère colonne du tableau
+
+**Héritage :**
+- `logo_filename` est dans `HERITABLE_FIELDS` de plateformes.js → héritage via push/reset
+- Auto-push immédiat lors de l'upload logo référentiel
+- `InvestissementDetail` affiche déjà `plateforme_logo` (champ retourné par investissements.js)
+
+**Why:** Permettre à l'admin de gérer les logos de toutes les plateformes en un seul endroit, avec propagation automatique vers les comptes utilisateurs et jusqu'aux fiches d'investissement.
+
+**How to apply:** Toute nouvelle plateforme ajoutée au référentiel apparaît automatiquement dans la bibliothèque avec le placeholder croix — l'admin sait qu'il doit importer un logo. Après upload, le logo se propage via logo_filename aux plateformes liées.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_menu_actions_tables.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_menu_actions_tables.md
new file mode 100644
index 0000000..f3cb1b2
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_menu_actions_tables.md
@@ -0,0 +1,86 @@
+---
+name: menus-actions-sur-les-tableaux-et-blocs
+description: "Pattern menu contextuel fixe (position: fixed + backdrop + icônes SVG) sur tableaux et headers de blocs ; voir aussi [[feedback_menu_actions_format]]"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 2fef7a75-e0f3-4f7d-9c80-79c3be16af03
+---
+
+Menus d'action ⋮ sur les tableaux :
+
+| Page | Tableau | Actions disponibles |
+|---|---|---|
+| DepotsRetraits | Mouvements | Modifier / Correction de solde / Supprimer |
+| Remboursements | Remboursements | Modifier / Supprimer |
+| Investissements | Investissements | Voir le détail / Modifier / Supprimer |
+| InvestissementDetail | Remboursements enregistrés | Modifier / Supprimer |
+| InvestissementDetail | Réinvestissements complémentaires | Supprimer |
+| AdminPlateformes — ReferentielSection | Référentiel plateformes | Modifier / Pousser (douce) / Pousser (forcée) / Supprimer |
+| AdminPlateformes — RefListSection | Catégories d'investissement | Renommer / Fusionner avec… / Supprimer |
+| AdminPlateformes — RefListSection | Secteurs d'investissement | Renommer / Fusionner avec… / Supprimer |
+
+Menus ⋮ sur les **headers de blocs** (InvestissementDetail) — bouton 30×30 px, border var(--border), 3 cercles SVG, état séparé de `openMenu` :
+
+| Bloc | État | Actions |
+|---|---|---|
+| Informations du projet | `cardMenu` | Modifier / Réinvestir / Exporter / Désactiver auto-reinvest (si actif) / Supprimer |
+| Remboursements enregistrés | `rembMenu` | Ajouter un remboursement / Supprimer tous les remboursements |
+| Réinvestissements complémentaires | `reinvMenu` | Ajouter un réinvestissement / Supprimer tous les réinvestissements |
+| Projections de remboursements | `simulMenu` | Régénérer l'échéancier |
+
+**Fermeture au scroll :** `useEffect` avec `window.addEventListener('scroll', closeAll, true)` — implémenté sur InvestissementDetail, DepotsRetraits, Investissements, Remboursements, AdminPlateformes. À reproduire sur toute nouvelle page avec menu contextuel.
+
+**Pattern technique (version avec icônes — format actuel de référence) :**
+```jsx
+// Icônes SVG locales (13×13 px)
+function IconEdit() { return ; }
+function IconTrash() { return ; }
+
+// État
+const [openMenu, setOpenMenu] = useState(null); // { row, x, y }
+
+// Fermeture scroll
+useEffect(() => {
+ if (!openMenu) return;
+ const close = () => setOpenMenu(null);
+ window.addEventListener('scroll', close, true);
+ return () => window.removeEventListener('scroll', close, true);
+}, [openMenu]);
+
+// Ouverture
+const handleMenuOpen = (e, row) => {
+ e.stopPropagation();
+ const rect = e.currentTarget.getBoundingClientRect();
+ setOpenMenu({ row, x: rect.right, y: rect.bottom + 4 });
+};
+
+// Rendu
+{openMenu && (
+ <>
+ setOpenMenu(null)} />
+
+ {[
+ { icon: , label: 'Modifier', onClick: () => { ... } },
+ { icon: , label: 'Supprimer', color: 'var(--danger)', onClick: () => { ... } },
+ ].map(({ icon, label, onClick, disabled, color }) => (
+ { if (!disabled) e.currentTarget.style.background = 'var(--surface-2)'; }}
+ onMouseLeave={e => e.currentTarget.style.background = 'none'}
+ onClick={disabled ? undefined : onClick}>
+ {icon}
+ {label}
+
+ ))}
+
+ >
+)}
+```
+
+**Why:** Actions inline remplacées par menus contextuels compacts pour garder les tableaux lisibles. Les icônes ont été ajoutées suite à la demande d'Olivier pour aligner avec le style de l'image de référence.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_methode_remboursement_investissement.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_methode_remboursement_investissement.md
new file mode 100644
index 0000000..99cba35
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_methode_remboursement_investissement.md
@@ -0,0 +1,36 @@
+---
+name: project_methode_remboursement_investissement
+description: "Méthode de remboursement et nom du compte courant sur les investissements — champs DB, formulaires, retrait automatique"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 5682d634-7345-40a8-8184-f617d1dd8444
+---
+
+## Champs ajoutés sur `investissements`
+
+| Colonne | Type | Rôle |
+|---|---|---|
+| `methode_remboursement` | TEXT nullable | `'portefeuille'` \| `'compte_courant'` \| null |
+| `nom_compte_courant` | TEXT nullable | Nom du compte bancaire quand methode = compte_courant |
+
+## Règle d'affichage dans les formulaires
+
+Le select "Méthode de remboursement" n'apparaît que si la plateforme sélectionnée a `methode_remboursement = 'choix_investisseur'`. Implémenté dans **Investissements.jsx** et **InvestissementDetail.jsx** (formulaire d'édition).
+
+Le champ "Nom du compte courant" (obligatoire) apparaît quand `methode_remboursement === 'compte_courant'`. Il utilise un `
` avec `
` peuplé depuis `GET /api/investissements/comptes-courants` (retourne les paires `{investisseur_id, nom_compte_courant}` distinctes, filtré côté client par `form.investisseur_id`).
+
+## Retrait automatique dans dépôts/retraits
+
+Quand un remboursement est enregistré avec `methode_remboursement = 'compte_courant'`, la fonction `syncAutoRetrait()` dans `remboursements.js` crée automatiquement un retrait dans `depots_retraits` avec `source = 'auto_remboursement'` et `remboursement_id = rembId`. Ce retrait est supprimé/recréé à chaque PUT et supprimé au DELETE du remboursement.
+
+Ces retraits automatiques sont **exclus** du calcul du solde porte-monnaie (filtre `source !== 'auto_remboursement'`).
+
+## Affichage fiche détail
+
+`inv.methode_remboursement` affiché dans "Informations du projet" :
+- `'portefeuille'` → "Porte-monnaie de la plateforme"
+- `'compte_courant'` → "Compte courant de l'investisseur (nom_compte_courant)"
+
+**Why:** Baltis et d'autres plateformes françaises laissent le choix à l'investisseur entre porte-monnaie et virement bancaire direct.
+**How to apply:** Toujours vérifier que les deux formulaires (liste + détail) et le pré-remplissage du formulaire remboursement sont cohérents. Voir [[feedback_remb_methode_fallback]].
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_notifications.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_notifications.md
new file mode 100644
index 0000000..d77b8da
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_notifications.md
@@ -0,0 +1,55 @@
+---
+name: project_notifications
+description: "Système de notifications in-app — table DB, routes backend, cloche topbar, page /notifications"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: dc9d0717-dc15-48a7-a013-2988b2e0ad4a
+---
+
+## Architecture
+
+**Table SQLite** : `notifications` (id, user_id FK, type, title, body, link, read, created_at). Les notifications sont strictement personnelles (filtrées par `user_id` via JWT sur toutes les routes).
+
+**Routes backend** `backend/src/routes/notifications.js` :
+- `GET /` — liste paginée (params: limit, offset, unread_only, type)
+- `GET /count` — nombre non lus (polling)
+- `PATCH /read-all` — tout marquer lu
+- `PATCH /:id/read` — marquer un lu
+- `DELETE /:id` — supprimer une notif
+- `DELETE /` — tout supprimer
+- `POST /seed` — générer des notifs de test (requireAdmin)
+
+⚠️ Routes statiques (`/count`, `/read-all`, `/seed`) déclarées **avant** `/:id` pour éviter le conflit Express.
+
+**Composants frontend** :
+- `NotifTypeAvatar.jsx` — source unique des icônes SVG (paths Lucide) et couleurs par type ; exporte `TYPE_META` et le composant par défaut
+- `NotificationBell.jsx` — cloche dans la topbar, polling 30s via `fetchCount`, écoute `window` event `notif:refresh` pour rechargement immédiat
+- `pages/Notifications.jsx` — page `/notifications`, layout fragment + `.topbar` hidden, bloc card 65% centré, bloc Simulation admin en dessous
+
+**Événement cross-composant** : `window.dispatchEvent(new CustomEvent('notif:refresh'))` à dispatcher après toute création de notification pour forcer le rechargement immédiat du compteur cloche.
+
+## Types de notifications
+
+| Type | Label | Icône Lucide | Couleur |
+|---|---|---|---|
+| `system` | Système | Radio | Gris |
+| `ticket_reply` | Ticket | FileText | Bleu |
+| `info` | Info | Info | Bleu ciel |
+| `team` | Équipe | Users | Violet |
+| `success` | Succès | Check | Vert |
+| `warning` | Avertissement | AlertTriangle | Ambre |
+| `security` | Sécurité | ShieldAlert | Rouge |
+| `announcement` | Annonce | Megaphone | Rose |
+
+Les icônes sont des SVG inline (paths Lucide) car `lucide-react` n'est pas installé dans le projet.
+
+## CSS notable
+
+- `.notif-bell-btn` : sans fond, sans bordure, sans effet hover — juste l'icône et le badge
+- `.notif-row-unread` / `.notif-item-unread` : `border-left: 3px solid var(--primary)` + fond primaire à 4-5% d'opacité
+- `.notif-dropdown-list` : scrollbar fine (4px) via `scrollbar-width: thin`
+- Layout page : fragment root + `.topbar` hidden → wrapper `.notif-center-wrap` → `.card.notif-block` (max-width 65%, margin auto)
+
+**Why:** Notifications personnelles par user_id ; `notif:refresh` évite d'attendre le poll de 30s après une action.
+**How to apply:** Toute future source de notifications doit dispatcher `notif:refresh` après insertion. Nouveaux types à ajouter dans `TYPE_META` de `NotifTypeAvatar.jsx`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_pagination.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_pagination.md
new file mode 100644
index 0000000..1481999
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_pagination.md
@@ -0,0 +1,27 @@
+---
+name: project_pagination
+description: Pagination côté client sur les 3 pages de listes — hook usePagination + composant Pagination
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: f2be746c-47f1-426a-ae80-3c71059fa38c
+---
+
+Pagination implémentée sur Investissements, DepotsRetraits et Remboursements.
+
+- Hook `src/hooks/usePagination.js` : tailles 15/25/50/100, défaut 15, localStorage par page, reset page 1 sur changement de filtres
+- Composant `src/components/Pagination.jsx` : compteur "X–Y sur Z", sélecteur taille, boutons nav
+- Clés localStorage : `cl_pagesize_inv`, `cl_pagesize_dr`, `cl_pagesize_remb`
+- Les exports CSV/XLS/JSON visent toujours la liste filtrée complète, pas la page affichée
+
+**Why:** Listes sans limite pouvant devenir longues.
+**How to apply:** Toute nouvelle page avec liste longue doit utiliser `usePagination` avec une clé `cl_pagesize_` dédiée.
+
+## Mode focus (liste agrandie)
+
+Chaque page a un état `listFocused` qui masque les graphiques, KPIs et onglets pour se concentrer sur la liste.
+
+- Bouton expand/collapse (icône maximize/minimize) placé avant le bouton export dans l'en-tête de chaque section liste
+- En mode focus : page size passe automatiquement à 25 ; au retour : 15
+- Sur les onglets Vision mensuelle : bouton passé en prop `expandButton` aux composants `CapitalMensuelTable`, `DepotsMensuelTable`, `TableauInteretsPlateforme` — s'affiche après le bouton TOUT
+- Le bouton expand et l'export doivent toujours être dans un wrapper `` pour éviter un problème d'alignement centré
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateforme_detenteur_logo.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateforme_detenteur_logo.md
new file mode 100644
index 0000000..ecb450a
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateforme_detenteur_logo.md
@@ -0,0 +1,21 @@
+---
+name: Modèle plateforme — détenteur & logo
+description: Évolutions récentes du modèle Plateforme : investisseur_id (détenteur), date_ouverture, logo_filename, contrainte UNIQUE modifiée
+type: project
+originSessionId: b87fd892-4ff2-4b8b-b3e6-f0f92e303c8e
+---
+Trois champs ajoutés à la table `plateformes` via migrations dans `backend/src/db/index.js` :
+
+- `investisseur_id` INTEGER FK → investisseurs (détenteur de la plateforme) — backfillé sur le compte principal
+- `date_ouverture` TEXT (ISO)
+- `logo_filename` TEXT — fichier stocké dans `data/logos/`, servi via `GET /api/logos/:filename` (statique, sans auth)
+
+**Contrainte UNIQUE modifiée** : `UNIQUE(user_id, nom)` → `UNIQUE(user_id, nom, investisseur_id)` pour permettre la même plateforme détenue par deux investisseurs différents. Migration par recréation de table dans `index.js` (SQLite ne supporte pas DROP CONSTRAINT).
+
+**Logo** : nommage `logo_{nom_sanitisé}_{timestamp}.{ext}` (timestamp pour éviter le cache navigateur). Upload via `POST /api/plateformes/:id/logo` (multer), suppression via `DELETE /api/plateformes/:id/logo`. Helmet configuré avec `crossOriginResourcePolicy: cross-origin`.
+
+**Frontend** : `LOGO_BASE = (VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/logos/'`. Helper `platInvestisseur(p)` reconstruit l'objet investisseur depuis les colonnes dénormalisées du JOIN.
+
+**Why:** Permettre à plusieurs membres de la famille/entreprise de détenir chacun un compte sur la même plateforme (ex. deux comptes Tantiem).
+
+**How to apply:** Toute requête SQL sur `plateformes` doit faire un `LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id` et exposer `inv.nom AS investisseur_nom` etc. Les selects de plateforme dans les formulaires affichent `{p.nom} — {p.investisseur_nom}` pour disambiguïser.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateformes_page.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateformes_page.md
new file mode 100644
index 0000000..81f53e3
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_plateformes_page.md
@@ -0,0 +1,85 @@
+---
+name: project_plateformes_page
+description: "Page /plateformes — vue par plateforme avec onglets Remboursements, Dépôts/Retraits, Capital investi, Investissements"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: b8b23026-381b-4ec2-b150-02080d5f4720
+---
+
+Page `frontend/src/pages/Plateformes.jsx` créée pour une vision centrée plateforme.
+
+## Structure générale
+
+- Sélecteur Plateforme (carte violette), Sélecteur Détenteur (carte teal, visible si multi-détenteur), Sélecteur Année
+- 5 KPIs avec TrendBadge (Capital encours, À risque, Investissements total, Capital remboursé, Intérêts brut/net)
+- 4 onglets : **Remboursements** | **Dépôts / Retraits** | **Capital investi** | **Investissements**
+
+## Onglet Remboursements
+
+- Tableau mensuel par investissement (tip-table), même sélecteurs que `TableauInteretsPlateforme` :
+ - 3 boutons icônes (Intérêts/Capital/Cashback) depuis `/api/icons`
+ - Sélecteur années `‹ 2024 2025 ›` + TOUT
+ - Toggle Détaillé/Consolidé (localStorage `cl_remb_plat_detaille`)
+ - Boutons Reçu / Projeté
+- Données réelles depuis `allRembs`, projections depuis `GET /api/simul/all` (nouvel endpoint)
+- `buildCellValue` : mois futur → projection seule ; mois courant → réel si dispo, sinon projection (jamais les deux) ; mois passé → réel seul
+- Grisage `.tip-td-closed` : avant `firstNonNull` de la ligne, après `lastRembDate` pour statut `rembourse`
+
+## Onglet Dépôts / Retraits
+
+- Tableau mensuel Dépôts / Retraits / Corrections / Net par mois
+- Filtre plateforme multi-sélect (custom dropdown, `plateforme_ids[]`) dans DepotsRetraits.jsx
+- Clic cellule → navigation `/depots-retraits?tab=mouvements&type=...&plat_ids=...`
+- Toggle Détaillé/Consolidé (localStorage `cl_dr_plat_detaille`) groupant par détenteur
+- Sélecteur années partagé avec les autres onglets
+- Grisage `.tip-td-closed` : avant date souscription, après dernier remb (prêts remboursés)
+
+## Onglet Capital investi
+
+- Composant `InvMensuelTable.jsx` : capital encours par investissement par mois
+- Grisage : avant `date_souscription` de l'investissement, après dernier remboursement pour statut `rembourse`
+
+## Backend — nouvel endpoint
+
+`GET /api/simul/all` dans `backend/src/routes/simul.js`, inséré AVANT `router.use(requireInvestisseur)` :
+- Supporte `scope=all` → filtre par `user_id`
+- Retourne `simul_remboursements` avec `plateforme_id` et `investisseur_id` joints
+
+## CSS
+
+`.tip-td-closed` : `background: var(--surface-2)` avec override pour `.tip-col-current` (fond violet prioritaire).
+
+## Comportements UX
+
+- Changer de plateforme conserve l'onglet actif (pas de reset sur `setActiveTab`)
+- `rembShowProjected && proj && real === 0` : la projection n'est ajoutée au mois courant que si aucun réel n'existe (anti-doublon). Même correction dans `TableauInteretsPlateforme.jsx`
+
+## Corrections et évolutions (2026-06-18)
+
+### CSS en-têtes tableaux
+- `.tip-th-name` (colonne PLATEFORME/Investissement) passe au dégradé violet comme `.tip-th-year` — texte blanc, border `rgba(255,255,255,.2)`
+- `.tip-td-avg` reçoit le même fond subtil que `.tip-td-total`
+- Colonne STATUT dans l'onglet Remboursements et dans `InvMensuelTable` : `
` (plus d'inline styles) ; ` ` STATUT utilise `tip-th-name`
+- `STATUT_BG` / `STATUT_FG` supprimés de `InvMensuelTable.jsx`
+- Titres "Mouvements de trésorerie" et "Capital investi par investissement" → `` avec span muted pour l'année (aligné sur "Investissements")
+
+### UX / navigation
+- Onglet par défaut : `'remboursements'` (était `'depots-retraits'`)
+- Bouton Agrandir/Réduire ajouté sur les onglets Dépôts/Retraits et Investissements (les deux onglets Remboursements et Capital investi l'avaient déjà)
+- Onglet Investissements : pagination via `usePagination` (clé `cl_pagesize_plat_inv`), tri par date_souscription dans `sortedChartRows`
+
+### Persistance des critères (localStorage)
+- `cl_plat_selected` → plateforme sélectionnée (préférée à `platOptions[0]` à l'init)
+- `cl_plat_tab` → onglet actif
+- `cl_plat_year` → année sélectionnée
+- `cl_plat_detenteur` → détenteur sélectionné (restauré sur la même plateforme ; remis à `all` si l'utilisateur change de plateforme, via `isFirstPlatChange` ref qui skip la restauration initiale)
+
+### Onglet Investissements — colonnes tableau (2026-06-18)
+- En-tête corrigé : "Émetteur" → "Détenteur" dans le ``
+- "Date" → "Date de souscription"
+- Colonne "Date cible" ajoutée après "Date de souscription" (thead + tbody alignés, 8 colonnes)
+- Thead final : Projet | Détenteur | Date de souscription | Date cible | Montant | Capital restant | Intérêts (Brut/Net) | Statut
+
+**Why:** Page demandée pour avoir une vision centrée plateforme en complément des vues globales.
+**How to apply:** Plateformes.jsx fait ~1564 lignes — toujours utiliser Python pour les éditions, jamais Edit/Write direct (risque de troncature).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_profil_import_ia_tags.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_profil_import_ia_tags.md
new file mode 100644
index 0000000..d0ae775
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_profil_import_ia_tags.md
@@ -0,0 +1,29 @@
+---
+name: project-profil-import-ia-tags
+description: ProfilImportBlock — import IA avec résolution noms→IDs et création de tags inconnus
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
+---
+
+L'import IA dans `PlatformeProfile.jsx` (composant `ProfilImportBlock`) gère maintenant correctement `categories_inv` et `secteurs_inv`.
+
+**Flow en 3 étapes** (state `step` : `'json'` → `'creation'` → `'diff'`) :
+1. **JSON** : saisie + "Analyser les changements →"
+2. **Création** (seulement si tags inconnus) : panneau ambre listant les nouveaux tags avec cases à cocher (toutes cochées par défaut). L'utilisateur valide ou décoche → "Continuer vers le diff →"
+3. **Diff** : tableau normal + bouton "Appliquer"
+
+**`resolveTags(aiNames, knownList, currentItems)`** : helper inline dans le composant
+- Normalise les noms (lowercase + accents supprimés)
+- Similarité : inclusion OU Levenshtein ≤ 2
+- Retourne `{ resolvedIds, resolvedNames, unknownNames, currentNames, hasChange }`
+
+**`apply()`** :
+- Pour chaque tag inconnu approuvé (`toCreate[nom] === true`) : crée via `POST /ref-categories` ou `POST /ref-secteurs`, récupère l'ID
+- Inclut `categories_inv_ids` et `secteurs_inv_ids` dans le payload PUT `/referentiel/:id`
+- Corrige le bug ancien où ces champs n'étaient jamais sauvegardés
+
+**Prompt IA** : `categories_inv` et `secteurs_inv` encouragent l'IA à utiliser les valeurs existantes mais autorisent les noms nouveaux si la plateforme y appartient clairement.
+
+**Props** : `ProfilImportBlock` reçoit maintenant `catsInv` et `secteursInv` (chargés depuis `/ref-categories` et `/ref-secteurs` dans le composant parent).
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_projections_display.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_projections_display.md
new file mode 100644
index 0000000..1152d59
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_projections_display.md
@@ -0,0 +1,23 @@
+---
+name: project-projections-display
+description: "Amélioration affichage projections InvestissementDetail — montants barrés, échéances non tenues"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 2824251c-280e-4f17-8623-a78ad9304896
+---
+
+Améliorations apportées à la table "Projections de remboursements" dans `InvestissementDetail.jsx`.
+
+**Montants réels vs prévus (barré + réel) :**
+- Quand un remboursement est payé mais le montant diffère de la projection : affiche le montant prévu barré en gris + le montant réel en gras.
+- Appliqué aux colonnes Intérêts ET Total.
+- Utilise `matchRembsAll` qui agrège **tous** les remboursements du mois (pas de priorité date exacte) pour gérer les rattrapages multi-versements.
+
+**Why:** Un prêt peut avoir plusieurs versements dans le même mois (rattrapage de mois manqués), ou le capital peut être remboursé en plusieurs tranches à des dates différentes dans le mois. La `Map` standard n'en garde qu'un — il faut `rembsArrayByMonth` (Map) et sommer.
+
+**Échéances passées non payées :**
+- Si `date_prevue < today` et aucun remboursement correspondant : affiche barré + 0,00 € dans les colonnes montants, et statut **✗ Échéance non tenue** en rouge.
+- Les projections futures restent affichées normalement avec statut "En attente".
+
+**How to apply:** Pour tout futur travail sur InvestissementDetail, ne pas toucher `matchRemb` (utilisé pour le clic/ouverture modale) mais utiliser `matchRembsAll` pour les comparaisons de montants.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_plateformes.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_plateformes.md
new file mode 100644
index 0000000..229e0a2
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_plateformes.md
@@ -0,0 +1,98 @@
+---
+name: project-referentiel-plateformes
+description: "Système référentiel commun pour les plateformes — architecture, tables, champs héritables, mécanismes de sync admin→user"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 7f63e6b8-eac0-45e1-bbd4-3fad774e7e8d
+---
+
+# Référentiel des plateformes
+
+Fonctionnalité permettant à l'admin de gérer un référentiel commun (`plateformes_referentiel`) dont les utilisateurs héritent lors de la création de leurs plateformes.
+
+**Why:** Centraliser les données communes (domiciliation, fiscalité, catégories) pour éviter la saisie répétée et garantir la cohérence entre tous les comptes utilisateurs.
+
+**How to apply:** Toujours maintenir la cohérence entre les deux tables lors d'évolutions de schéma. Voir aussi la règle dans CLAUDE.md §7.
+
+---
+
+## Tables DB
+
+### `plateformes_referentiel`
+id, nom (UNIQUE), url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, logo_filename, **methode_remboursement**, **type_pret_defaut**, **freq_interets_defaut**, description, created_at, updated_at
+
+**Note :** `type_investissement` et `secteur` existent encore en DB mais ne sont plus utilisés dans le code (remplacés par categories_inv/secteurs_inv). `duree_defaut` et `taux_defaut` de `plateformes` ont aussi été supprimés du code (colonnes DB conservées).
+
+### `referentiel_categories`
+referentiel_id FK + categorie_nom TEXT (string, pas FK vers categories_plateforme — les catégories user sont scoped)
+
+### `referentiel_notation`
+id, referentiel_id FK, nom, type, valeurs (JSON), min_val, max_val, description, ordre, created_at
+
+### Colonnes ajoutées à `plateformes`
+- `referentiel_id INTEGER REFERENCES plateformes_referentiel(id) ON DELETE SET NULL`
+- `overridden_fields TEXT NOT NULL DEFAULT '[]'` — JSON array des champs modifiés par l'user
+
+---
+
+## Champs héritables scalaires (HERITABLE_FIELDS)
+
+```js
+['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'logo_filename', 'icone_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut']
+```
+
+Définis dans `backend/src/routes/plateformes.js`. Identiques dans `referentiel.js` (PUSHABLE, sans `icone_filename`).
+
+**Héritage tags (catégories/secteurs d'investissement) :** mécanisme séparé via merge — voir [[project-categories-secteurs-inv-heritage]].
+
+---
+
+## Routes backend
+
+### Admin — `/api/referentiel` (requireAdmin)
+- `GET /` — liste avec nb_plateformes_liees
+- `POST /` — créer une entrée
+- `PUT /:id` — modifier
+- `DELETE /:id` — supprimer (délie les plateformes)
+- `GET /:id/plateformes` — plateformes liées
+- `POST /:id/push` — pousse les champs vers toutes les plateformes liées ; deux modes :
+ - **Douce** (défaut, `force` absent) : respecte `overridden_fields`, ignore les champs modifiés par l'user
+ - **Forcée** (`{ force: true }`) : écrase tous les champs et réinitialise `overridden_fields = '[]'` sur chaque plateforme
+- `GET /export` — export ZIP (json + images)
+- `POST /import-zip` — import ZIP avec résolution des tags par nom
+- **⚠️ Routes statiques (/export, /import-zip) TOUJOURS avant `/:id`**
+
+### Admin — `/api/admin`
+- `GET /plateformes-orphelines` — plateformes sans referentiel_id, enrichies d'une suggestion de liaison (Levenshtein ≥ 80%)
+- `POST /plateformes-orphelines/:id/importer` — crée une entrée référentiel depuis la plateforme
+- `POST /plateformes-orphelines/:id/lier` — liaison directe à un référentiel existant sans import
+
+### User — `/api/plateformes`
+- `GET /referentiel-list` — lecture seule du référentiel (requireAuth, pas admin)
+- `POST /:id/reset` — réinitialise les champs héritables depuis le référentiel, vide overridden_fields
+
+---
+
+## Logique frontend
+
+### Settings.jsx
+- Dropdown "Basé sur le référentiel" en tête du formulaire de création → pré-remplit champs scalaires
+- `referentiel_id` transmis au POST
+- Formulaire d'édition : bandeau indigo si lié + badge "hérité" par champ + bouton "Réinitialiser au référentiel"
+- `openEditPlat()` charge `referentiel_id`, `referentiel_nom`, `overridden_fields`, `inherited_cat_ids`, `inherited_sect_ids`
+- Bandeau affiche : `{N} champ(s) hérité(s)` + `+ catégories & secteurs` si applicable
+- `inheritedCount` basé sur les 10 champs scalaires heritables
+- InvSelect catégories/secteurs reçoit `inheritedIds` → items hérités toujours cochés + désactivés + badge "Réf"
+
+### Admin.jsx (AdminPlateformes)
+- Section `referentiel` : CRUD + menu ⋮ (Modifier / Pousser douce / Pousser forcée / Supprimer)
+- Formulaire référentiel inclut : méthode de remboursement, type de prêt défaut, périodicité intérêts défaut
+- Section `orphelines` : badge similarité ambre + bouton "Lier" / "Importer"
+
+---
+
+## Algorithme similarité noms (admin.js)
+
+Levenshtein normalisé, insensible à la casse et aux accents, seuil 80%.
+`similarity(a, b) = 1 - levenshtein(normalize(a), normalize(b)) / max(len(a), len(b))`
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_search_filter.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_search_filter.md
new file mode 100644
index 0000000..6b47492
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_referentiel_search_filter.md
@@ -0,0 +1,16 @@
+---
+name: project_referentiel_search_filter
+description: "Recherche, filtres et pagination ajoutés à ReferentielSection dans Admin.jsx"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: f7c8b8d5-8019-47f1-a337-6c144cef302c
+---
+
+Recherche par nom (pleine largeur), filtre domiciliation et filtre catégorie ajoutés à `ReferentielSection` dans `Admin.jsx`.
+
+**Pourquoi :** Amélioration UX pour naviguer dans un référentiel croissant.
+
+**How to apply :** Les catégories disponibles sont dérivées dynamiquement avec `useMemo` depuis les données chargées. Pagination via `usePagination` avec clé `cl_pagesize_referentiel`. Bouton "Effacer les filtres" conditionnel. Layout : champ recherche pleine largeur, puis ligne domiciliation + catégorie côte à côte.
+
+**Icône vs logo dans la colonne image :** `icone_filename` (icône bibliothèque Admin) est prioritaire sur `logo_filename` (logo uploadé). Les deux sont stockés dans `data/logos/` et servis via `/api/logos/` — ne jamais utiliser `/api/icons-files/` pour `icone_filename` du référentiel.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_reinvestissements.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_reinvestissements.md
new file mode 100644
index 0000000..15239b9
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_reinvestissements.md
@@ -0,0 +1,49 @@
+---
+name: Fonctionnalité réinvestissements
+description: Ajout de capital complémentaire sur un prêt en cours — architecture complète DB/backend/frontend
+type: project
+originSessionId: 680bfd99-577f-454e-b5b8-ea58460277f9
+---
+## Fonctionnalité : réinvestissements sur un investissement existant
+
+Implémentée en mai 2026. Permet d'ajouter une ou plusieurs sommes complémentaires à un prêt en cours.
+
+### Base de données
+- Nouvelle table `reinvestissements` : `id`, `investissement_id` (FK cascade), `montant`, `date_reinvestissement`, `note`, `created_at`
+- Index : `idx_reinv_inv ON reinvestissements(investissement_id, date_reinvestissement)`
+- Migration dans `backend/src/db/index.js` (pattern habituel `CREATE TABLE IF NOT EXISTS`)
+- `montant_investi` dans `investissements` reste l'initial — ne jamais le modifier pour un réinvestissement
+
+### Backend
+- Route : `backend/src/routes/reinvestissements.js` — GET (liste par investissement_id), POST, DELETE
+- Montée dans `server.js` : `app.use('/api/reinvestissements', requireAuth, reinvestissementsRouter)`
+- GET `/api/investissements/:id` retourne maintenant `reinvestissements`, `reinvestissements_total`, `capital_total`
+- GET `/api/investissements/` retourne `reinvestissements_total` et `capital_total` (subqueries SQL)
+
+### Logique de simulation (`backend/src/utils/schedule.js`)
+- Nouvelle fonction exportée : `generateSimulWithReinvestissements(db, investissementId)`
+ - Génère le planning complet en appliquant le capital cumulatif période par période
+ - Appelée à chaque POST/DELETE sur `/api/reinvestissements`
+- `adjustSimulForActuals` **modifiée** pour tenir compte des réinvestissements :
+ - Calcule `capitalAtLastDate = montant_investi + reinvestissements jusqu'à la date du dernier remb`
+ - Les réinvestissements futurs (après dernière date remb) gonflent progressivement les périodes suivantes
+ - Si aucun capital remboursé → bifurque vers `generateSimulWithReinvestissements` si des reinvests existent
+
+**Why:** `adjustSimulForActuals` est appelée après chaque saisie de remboursement. Sans cette modification, elle recalculait la projection sur `montant_investi` seul, ignorant les réinvestissements.
+
+### `remboursements.js` — `syncInvestissementStatut`
+- Corrigé pour utiliser `capitalTotal = montant_investi + SUM(reinvestissements)` au lieu de `montant_investi` seul
+- Évite de passer le statut à "rembourse" trop tôt quand du capital réinvesti reste dû
+
+### Frontend (`InvestissementDetail.jsx`)
+- Bouton "Réinvestir" dans la topbar (même style qu'Exporter)
+- Modal de saisie : montant + date + note optionnelle
+- Tableau récapitulatif conditionnel (affiché uniquement si reinvests > 0) avec colonne "capital cumulé" et suppression inline
+- KPI "Montant investi" → "Capital total investi" si reinvests, avec sous-libellé "Initial X + Y réinvesti"
+- XIRR : chaque réinvestissement est un flux sortant supplémentaire `{ amount: -montant, date }`
+
+### `Investissements.jsx` (liste)
+- Remplace `r.montant_investi` par `(r.capital_total ?? r.montant_investi)` dans les agrégats KPI, par-plateforme, tableau et export Excel
+- Rétro-compatible : fallback sur `montant_investi` si pas de reinvests
+
+**How to apply:** Toute future page affichant le capital d'un investissement doit utiliser `capital_total` (depuis l'API) plutôt que `montant_investi` brut.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_admin_refacto.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_admin_refacto.md
new file mode 100644
index 0000000..1643f16
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_admin_refacto.md
@@ -0,0 +1,47 @@
+---
+name: project_settings_admin_refacto
+description: Refactorisation Settings.jsx (3070 lignes) et Admin.jsx en composants autonomes — structure finale et imports manquants corrigés
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: ab8b89ba-75a8-4b6e-a0b4-5180e6c20e4a
+---
+
+Settings.jsx et Admin.jsx ont été découpés en composants autonomes (chacun avec son propre fetch, sans props drilling depuis le parent).
+
+**Structure finale :**
+
+- `pages/Settings.jsx` → shell 97 lignes, routing `?section=` uniquement
+- `pages/Admin.jsx` → shell 74 lignes, routing `?section=` uniquement
+- `pages/admin/adminHelpers.jsx` — `fmt`, `Badge`, `StatusBadge`
+- `pages/admin/UsersSection.jsx` — fetch `/admin/users`
+- `pages/admin/CreateUserSection.jsx` — POST `/admin/users`
+- `pages/admin/JobLogsSection.jsx` — fetch `/admin/job-logs`
+- `pages/admin/IconsSection.jsx` — fetch `/icons`
+- `pages/settings/AppearanceSection.jsx` — thème, police, couleurs graphiques
+- `pages/settings/PlateformesSection.jsx` — CRUD plateformes, export CSV/XLS, PlatDetailPanel
+- `pages/settings/CategoriesInvSection.jsx` — CRUD catégories d'investissement
+- `pages/settings/SecteursInvSection.jsx` — CRUD secteurs d'investissement
+- `pages/settings/ComptesSection.jsx` — CRUD comptes courants/PEA-PME
+- `pages/settings/MaFiscaliteSection.jsx` — toggle PFO
+- `pages/settings/DataCleanupSection.jsx` — nettoyage données
+- `pages/settings/ImportsSection.jsx` — import CSV/XLS/ZIP
+
+**Imports à ne pas oublier dans les sections (erreurs rencontrées) :**
+
+- `PlateformesSection` : `useNavigate` (react-router-dom), `memberLabel` + `fmtDate` (format.js), `ResultBanner`
+- `ComptesSection` : `Modal`, `memberLabel` (format.js), `TYPE_COMPTE_LABELS` et `EXONERATION_LABELS` (constantes locales)
+- `ImportsSection` : `ResultBanner`, `fmtDate` (format.js)
+- `DataCleanupSection` : ne pas laisser traîner le bloc NAV de Settings (bug d'extraction)
+
+**Bugs d'extraction corrigés :**
+- `platsToXLS` tronquée → compléter à la main avec Python
+- `\n` littéral dans `.join('\n')` → remplacer par séquence escape
+- `platImportRef` déclarée deux fois → supprimer le doublon
+- Bloc NAV de Settings collé à la fin de DataCleanupSection → couper au marqueur `/* ── Nav`
+- Extra `);` en fin de CategoriesInvSection et SecteursInvSection → supprimer
+- `saveCatInv` sans signature `async` → insérer l'entête manquant
+
+**Why:** Settings.jsx à 3070 lignes causait des troncatures et des bugs à chaque modification. Découpage en composants autonomes pour éviter ces problèmes.
+
+**How to apply:** Pour toute nouvelle section Settings ou Admin, créer un fichier autonome dans `pages/settings/` ou `pages/admin/`, avec ses propres imports et son propre `useEffect`/fetch. Ne jamais remonter l'état au parent.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_nav.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_nav.md
new file mode 100644
index 0000000..f89ff6a
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_settings_nav.md
@@ -0,0 +1,52 @@
+---
+name: project_settings_nav
+description: "Structure NAV de Settings.jsx — groupes, labels et sections disponibles (état juin 2026)"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: da8e6c45-04c2-4db9-8e7f-189d65e7cf99
+---
+
+La NAV de Settings est organisée en 4 groupes :
+
+```
+Interface
+ └ Apparence (id: apparence) — thème, police, langue, devise, couleurs graphiques
+
+Mon paramétrage
+ ├ Mes membres & entreprises (id: membres) — FamilleEntreprises.jsx (anciennement MonCompte?section=famille)
+ ├ Mes plateformes (id: plateformes)
+ ├ Mes comptes courants (id: comptes) — ComptesSection inline dans Settings.jsx
+ └ Ma fiscalité (id: ma-fiscalite) — toggle PFO/2778-SD (pfoAssujetti via UiContext)
+
+Mes tags
+ ├ Mes catégories d'investissement (id: categories-inv) — 2 accordéons : Global (replié) / Privé (déplié)
+ └ Mes secteurs d'investissement (id: secteurs-inv) — idem
+
+Mes données
+ ├ Nettoyage de données (id: nettoyage) — DataCleanupSection (anciennement MonCompte?section=nettoyage)
+ └ Importation de données (id: imports)
+```
+
+**Sections supprimées/déplacées :**
+- `pfu` → `/admin/fiscalite?section=pfu`
+- `garanties` → `/admin/plateformes?section=garanties`
+- `notation` → `/admin/plateformes?section=notation`
+- Groupe "Qualité" supprimé
+- Groupe "Importation" renommé "Mes données"
+- `famille` retiré de MonCompte → Settings `membres`
+- `nettoyage` retiré de MonCompte → Settings `nettoyage`
+
+**MonCompte** ne contient plus que 2 sections : `profil` et `securite`.
+
+**Section `ma-fiscalite` :** toggle PFO/2778-SD (`pfoAssujetti` via UiContext), texte explicatif dépliable (`showPfoDetail` local).
+
+**Sections `categories-inv` et `secteurs-inv` :** affichées en 2 accordéons —
+- "Globalement définis" replié par défaut (`catGlobalOpen=false`, `sectGlobalOpen=false`)
+- "Mes propres" déplié par défaut (`catPrivateOpen=true`, `sectPrivateOpen=true`)
+États dans le composant Settings principal.
+
+**Composants inline dans Settings.jsx** (définis en bas du fichier, après Settings) :
+- `EMPTY_COMPTE`, `CompteFormFields`, `ComptesSection`
+
+**How to apply:** Toute nouvelle section utilisateur va dans "Mon paramétrage". Toute section admin (référentiels, taux) va dans AdminPlateformes ou AdminFiscalite. DataCleanup et famille sont dans Settings, pas dans MonCompte.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_solde_portefeuille.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_solde_portefeuille.md
new file mode 100644
index 0000000..45013ce
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_solde_portefeuille.md
@@ -0,0 +1,40 @@
+---
+name: Calcul solde porte-monnaie
+description: Formule correcte du solde porte-monnaie plateforme — inclut la soustraction du capital investi non remboursé
+type: project
+originSessionId: 680bfd99-577f-454e-b5b8-ea58460277f9
+---
+## Formule solde porte-monnaie par plateforme
+
+```
+solde = dépôts − retraits_manuels + net_recu_portefeuille + bonus_wallet − capital_total_investi
+```
+
+- **retraits_manuels** : retraits hors `source='auto_remboursement'` (les auto-retraits sont générés par les remboursements compte_courant et ne représentent pas un mouvement du porte-monnaie)
+- **remb_wallet** : montant crédité au porte-monnaie selon la fiscalité de la plateforme :
+ - `flat_tax` → `net_recu` (PFU 30% prélevé à la source par la plateforme française)
+ - `sans_fiscalite_locale` ou `avec_fiscalite_locale` → `capital + cashback + interets_bruts` (pas de PFU prélevé à la source ; l'investisseur déclare le PFU séparément ; `interets_bruts` est déjà net de la retenue locale si applicable)
+ - Uniquement pour `methode_remboursement = 'portefeuille'`
+- **bonus_wallet** : cashback des remboursements `type IN ('bonus_parrainage', 'bonus_plateforme')`
+- **capital_total_investi** : `montant_investi + SUM(reinvestissements)` par investissement souscrit, groupé par plateforme
+
+Le capital revient progressivement via `net_recu` au fil des remboursements — il n'est pas nécessaire de le déduire partiellement.
+
+## Implémentations
+
+### Backend — `dashboard.js` (vue "Toutes les années")
+Quatre requêtes SQL fusionnées dans `walletMap` :
+1. `drManuelPerPlat` — dépôts − retraits manuels
+2. `rembWalletPerPlat` — net_recu portefeuille (fiscalité ajustée selon plateforme)
+3. `bonusWalletPerPlat` — cashback bonus
+4. `capitalInvestiPerPlat` — capital_total soustrait
+
+### Frontend — `DepotsRetraits.jsx` (vue filtrée par année)
+Calcul cumulatif jusqu'à `yearEnd = YYYY-12-31` dans `drPlatData` (useMemo) et `kpiSoldePortefeuille` :
+1. Boucle sur `allRows` : exclure `auto_remboursement`, cumuler dépôts/retraits
+2. Boucle sur `allRemb` : ajouter `net_recu` si `methode_remboursement === 'portefeuille'`
+3. Boucle sur `allInv` (chargé via `/investissements`) : soustraire `capital_total ?? montant_investi`
+
+**Why:** Sans la soustraction du capital investi, le porte-monnaie affichait les dépôts comme disponibles même quand le capital était engagé dans des prêts actifs.
+
+**How to apply:** Toute nouvelle vue ou export affichant un "solde porte-monnaie" doit appliquer cette formule complète, y compris la soustraction du capital.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_suppression_detenteur.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_suppression_detenteur.md
new file mode 100644
index 0000000..e62f725
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_suppression_detenteur.md
@@ -0,0 +1,14 @@
+---
+name: project_suppression_detenteur
+description: "Suppression d'un détenteur — réassignation au principal avant delete, pas de cascade"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 94928e14-ffe7-4c4d-b139-748259d26134
+---
+
+La suppression d'un investisseur (détenteur) réassigne toutes les données liées au compte principal avant de supprimer l'enregistrement. Les tables `investissements`, `depots_retraits`, `plateformes` et `comptes` sont mises à jour via une transaction.
+
+**Why:** Le schéma SQL a `ON DELETE CASCADE` sur `investisseur_id` dans `investissements` et `depots_retraits`, ce qui supprimait les données métier à tort. Seule la suppression d'une plateforme dans Paramètres doit supprimer les données associées.
+
+**How to apply:** La logique est dans `DELETE /:id` de `backend/src/routes/investisseurs.js`. Toute nouvelle table avec `investisseur_id NOT NULL` et `ON DELETE CASCADE` devra être ajoutée à cette transaction de réassignation.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_interets_plateforme.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_interets_plateforme.md
new file mode 100644
index 0000000..9bd9cbf
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_interets_plateforme.md
@@ -0,0 +1,43 @@
+---
+name: project_tableau_interets_plateforme
+description: "Tableau mensuel intérêts par plateforme dans le Dashboard, synchronisé avec InteretsChartContext"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: e5c19875-c3d7-42c1-ab67-bf3ba23dbda3
+---
+
+Composant `TableauInteretsPlateforme.jsx` ajouté dans le Dashboard, juste après les graphiques, à l'intérieur du `InteretsChartProvider`.
+
+**Fichiers modifiés :**
+- `frontend/src/components/TableauInteretsPlateforme.jsx` — composant créé
+- `frontend/src/pages/Dashboard.jsx` — import + intégration
+- `backend/src/routes/dashboard.js` — endpoint `GET /api/dashboard/interets-par-plateforme`
+- `frontend/src/styles.css` — classes `tip-*`
+
+**Endpoint backend :** `GET /api/dashboard/interets-par-plateforme?annee=&scope=all`
+- Retourne : `{ plateformes: [{ id, nom, rembourses: { 'YYYY-MM': { interets_bruts, interets_nets, cashback, capital } }, projections: { 'YYYY-MM': { interets_prevus, capital_prevu } } }], capitalMensuel: [{ mois, capital }] }`
+- Capital mensuel calculé via itération JS sur les investissements (date_souscription / date_cible / statut)
+
+**Logique frontend :**
+- Synchronisé avec `InteretsChartContext` : année, netMode, inclureInterets/Capital/Cashback, showActual/showProjected, modeGlobal
+- `buildValue(plat, mIdx, { withCapital })` : fonction centrale — withCapital=true pour l'affichage, false pour la performance
+- Mois passés = données réelles (si showActual) ; mois courant = réel + projeté ; mois futurs = projections (si showProjected)
+- Valeurs projetées affichées en italique/muted
+
+**Lignes du tableau :**
+- Une ligne par plateforme (avec Total + Moy. mensuelle)
+- "Toutes les plateformes" : somme mensuelle incluant capital si toggle actif
+- "Capital investi" : snapshot mensuel, colonne Total = dernier mois non nul
+- "Performance nette/brute mensuelle" : perfMonthTotals[i] / capitalValues[i] ; Total = perfAnnTotale/12
+- "Performance nette/brute annualisée" : perf_mensuelle × 12 ; Total = perfGrandTotal / lastCapital
+
+**Why:** Les 3 dernières lignes (capital investi + 2 perfs) utilisent `perfMonthTotals` (sans capital) même quand le toggle capital est actif — le capital gonfle les montants mais ne doit pas biaiser la performance.
+
+**Visuel :**
+- Couleur thème : violet/indigo (`#7c3aed → #4f46e5`) alignée sur le YearSelectorKpi
+- Colonne mois courant : barre indigo en haut du header + fond légèrement teinté
+- Header "PLATEFORME" et "TOTAL" : même gradient indigo
+- Cellules vides fin de ligne (avg pour les 3 dernières lignes) : `tip-td-void` avec `border: 1px hidden transparent` pour effacer les lignes en border-collapse
+- Sélecteur Reçu/Projeté en bas du tableau (même style pill que bar chart)
+- En mode global ("Depuis le début") : tableau masqué (retourne null)
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_remboursements_detail.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_remboursements_detail.md
new file mode 100644
index 0000000..7efeec7
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tableau_remboursements_detail.md
@@ -0,0 +1,49 @@
+---
+name: tableau-remboursements-detail
+description: "Évolutions du tableau 'Remboursements enregistrés' dans InvestissementDetail — colonnes fusionnées, tooltip, CSS, clic ligne, retour dashboard"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: cad3a9c0-cf8e-4204-ac6e-e22c03811abe
+---
+
+Tableau "Remboursements enregistrés" dans InvestissementDetail.jsx — évolutions UX.
+
+**Colonnes actuelles (sans fiscalité locale) :**
+Date | Capital | Intérêts (Brut/Net) | Cashback | Imposition | Montant versé | ⋮
+
+**Colonne "Imposition" (fusion PS + IR) :**
+- Remplace les deux colonnes "Prélèv. sociaux" et "Impôt revenu"
+- Affiche la somme `prelev_sociaux + prelev_forfaitaire`
+- Titre suffixé "— indicatif" si `inv.fiscalite_override === 'exonere' || tblPlat.fiscalite !== 'flat_tax'`
+- Tooltip CSS instantané (classe `.cell-tooltip`) : "Prélèvements sociaux : x,xx €\nImpôt sur le revenu : x,xx €"
+
+**Tooltip CSS (styles.css) :**
+- Classe `.cell-tooltip` avec `::after { content: attr(data-tooltip); white-space: pre-line; opacity: 0 → 1 au hover }`
+- Background : `var(--surface)` (pas de couleur hardcodée)
+- Pas de transition = apparition instantanée
+
+**CSS tableau — classe `remb-table` :**
+- `table-layout: fixed; width: 100%` → largeurs égales entre colonnes numériques
+- `text-align: center` sur toutes les cellules
+- Première colonne (Date) : `text-align: left; width: 92px`
+- Dernière colonne (⋮) : `width: 36px`
+
+**Commentaire sous le titre de section :**
+- Même formulation que dans la modale : isIndicatif ou flat tax
+- Calculé via IIFE avec `_plat` et `_isIndicatif` locaux (pas de state dédié)
+
+**Clic sur ligne → modale modification :**
+- `onClick={() => openEditRemb(r)}` sur le ``
+- `cursor: pointer` sur le ` `
+- Bouton ⋮ stoppe la propagation : `e.stopPropagation()` avant `openRowMenu`
+
+**Ouverture depuis le tableau de bord (remb-date + from=dashboard dans l'URL) :**
+- Le useEffect qui traite `remb-date` attend que `pfuRates.length > 0` (guard + dépendance) pour calculer les prélèvements correctement
+- `fromRef.current` est capturé EN PREMIER (avant l'ouverture de la modale) et les params URL nettoyés immédiatement
+- Après save (`submitRemb`) ou suppression (`deleteRemb`) : `closeRembModal` lit `fromRef.current` et navigue vers `/` si `'dashboard'`
+- Bug corrigé : sans le guard pfuRates, les prélèvements étaient 0 car les taux n'étaient pas encore chargés
+
+**Why:** Meilleure lisibilité du tableau, cohérence avec les modales, accès rapide à la modification, UX fluide depuis le dashboard.
+
+**How to apply:** Si un autre tableau similaire est créé, reproduire `.remb-table`, `.cell-tooltip`, et le pattern clic ligne + stopPropagation sur le bouton ⋮. Pour tout flux "ouvrir modale depuis URL param", capturer `fromRef` avant d'ouvrir la modale et attendre que les données de référence (pfuRates) soient chargées.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tags_suggeres_admin.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tags_suggeres_admin.md
new file mode 100644
index 0000000..0dc5d17
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tags_suggeres_admin.md
@@ -0,0 +1,27 @@
+---
+name: project-tags-suggeres-admin
+description: "Système de suggestions admin pour catégories/secteurs d'investissement créés par les utilisateurs"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
+---
+
+Système de validation admin des tags (categories_inv / secteurs_inv) créés par les utilisateurs (user_id IS NOT NULL).
+
+**Emplacement** : section `inv-suggestions` dans `/admin/plateformes` (AdminPlateformes.jsx), pas dans Admin.jsx.
+
+**Backend (admin.js)** — 6 routes ajoutées :
+- `GET /api/admin/inv-suggestions-count` → `{ cats, sects, total }`
+- `GET /api/admin/inv-suggestions` → `{ categories: [...], secteurs: [...] }` avec user info + nb_plateformes + nb_investissements
+- `POST /api/admin/inv-suggestions/categories/:id/promouvoir` → SET user_id = NULL (rend global)
+- `DELETE /api/admin/inv-suggestions/categories/:id`
+- `POST /api/admin/inv-suggestions/secteurs/:id/promouvoir`
+- `DELETE /api/admin/inv-suggestions/secteurs/:id`
+
+**Frontend (AdminPlateformes.jsx)** :
+- `InvSuggestionsSection` : onglets Catégories/Secteurs, groupé par utilisateur, actions Promouvoir/Supprimer via ConfirmModal + ResultBanner
+- Bouton compteur dans `RefListSection` (catégories ET secteurs) : gris si 0, rouge si > 0, navigue vers `?section=inv-suggestions`
+- Bouton "← Retour aux catégories" dans la section
+
+**How to apply:** Quand un utilisateur crée une catégorie/secteur privé, il apparaît ici. L'admin peut le promouvoir (global = visible par tous) ou le supprimer.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taux_credit_impot.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taux_credit_impot.md
new file mode 100644
index 0000000..e0133b2
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taux_credit_impot.md
@@ -0,0 +1,26 @@
+---
+name: project_taux_credit_impot
+description: "Référentiel taux crédit d'impôt 2047 — table DB, route backend, section Admin avec import IA"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 6ab3db95-bde5-486d-8d53-7e58134a13ca
+---
+
+Table `taux_credit_impot` seedée avec 124 pays (notice DGFiP 2047). Route `/api/taux-credit-impot` (GET auth, POST/PUT/DELETE admin). Section Admin sous groupe "Référentiels" → id `taux-ci`.
+
+**Schéma :** `nom_pays, code_pays (ISO alpha-2), div_taux, div_taux_alt, div_taux_alt_label, div_exclusif_residence, int_taux, int_taux_alt, int_taux_alt_label, int_exclusif_residence, notice, statut_convention (active/suspendue/caduque), date_suspension, ref_boi`
+
+**Frontend Admin.jsx :**
+- `TciModal` — modale create/edit avec `CountrySelect showCode` pour le code pays (auto-remplit `nom_pays` si vide)
+- `TauxCreditImpotSection` — tableau avec filtre tabs + recherche, menu ⋮ (Modifier/Supprimer), icône ℹ à 10px du badge convention
+- `TciImportBlock` — import JSON IA : coller réponse → diff avant/après par champ → appliquer sélectivement
+- `TciPromptBlock` — prompt IA éditable (localStorage `cl_tci_prompt`), bouton copier remplace `{{CURRENT_DATA}}` en live
+- Légende "excl. résidence" dans le pied du tableau
+
+**Piège fieldset CSS :** `fieldset` a `min-width: min-content` par défaut → ajouter `minWidth: 0` pour éviter le débordement de contenu.
+
+**Piège checkbox CSS :** `input { width: 100% }` global force les checkboxes sur toute la largeur → toujours ajouter `width: auto` sur les `input[type="checkbox"]`.
+
+**Why:** Notice DGFiP 2047 mise à jour annuellement ; le bloc IA permet de comparer et patcher les taux sans retouche manuelle du code.
+**How to apply:** Futur champ `code_pays` sur la table `plateformes` permettra une jointure avec ce référentiel pour afficher le taux applicable par plateforme.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taxreport.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taxreport.md
new file mode 100644
index 0000000..81e6ca7
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_taxreport.md
@@ -0,0 +1,32 @@
+---
+name: project-taxreport
+description: "Refonte de la page Fiscalité — renommage, onglets, pagination, sélecteur d'année"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 0922bde7-84ed-45dc-b233-50bf0be5ad3e
+---
+
+Page Fiscalité entièrement refactorée en session 2026-05-31.
+
+**Renommage complet :**
+- Fichier : `Fiscal2778.jsx` → `TaxReport.jsx` ; `fiscal2778.js` → `taxreport.js`
+- Route frontend : `/2778-sd` → `/taxreport` (redirect conservée pour l'ancienne URL)
+- Route backend : `/api/fiscal-2778` → `/api/taxreport`
+- Composant : `Fiscal2778()` → `TaxReport()`
+- 5 fichiers mis à jour : App.jsx, Layout.jsx, server.js, TaxReport.jsx, taxreport.js
+
+**Structure de la page :**
+- Bloc "Cases fiscales calculées" toujours visible en haut
+- Onglets `dr-tabs` (pattern Investissements) : **Récapitulatif** | **Détail par projet**
+- Récapitulatif : tableau récap + corrections de solde + pertes en capital (conditionnels)
+- Détail par projet : tableau avec pagination (`cl_pagesize_fiscal_detail`), bouton agrandir/réduire, ExportDropdown (CSV + JSON)
+
+**Sélecteur d'année :**
+- Composant `YearSelector` autonome, clone stylisé du `YearSelectorKpi` du Dashboard (card violet/indigo)
+- Pas de mode "Depuis le début" (fiscal = toujours par année)
+- Années disponibles chargées via `GET /api/taxreport/years` (années ayant des remboursements réels, desc)
+- Endpoint backend ajouté dans `taxreport.js` avec même logique de scope investisseur
+
+**Why:** Cohérence visuelle avec le Dashboard ; meilleure ergonomie que l'input number.
+**How to apply:** Pour toute future modif de la page fiscalité, le fichier est `frontend/src/pages/TaxReport.jsx` et la route backend `backend/src/routes/taxreport.js`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tip_css_drillcell_consolidation.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tip_css_drillcell_consolidation.md
new file mode 100644
index 0000000..b557454
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_tip_css_drillcell_consolidation.md
@@ -0,0 +1,27 @@
+---
+name: project_tip_css_drillcell_consolidation
+description: Correctifs CSS en-têtes tip-table et bug DrillCellPanel en mode consolidé
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 18505460-8577-4ae3-848c-a569e3dd2b75
+---
+
+## Correctifs CSS tip-table (2026-06-18)
+
+`.tip-th-name` (colonne PLATEFORME) reçoit maintenant le même dégradé violet `linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)` que `.tip-th-year`, `.tip-th-total` et `.tip-th-avg`. Texte blanc, border-right `rgba(255,255,255,.2)`.
+
+`.tip-td-avg` reçoit le même fond subtil que `.tip-td-total` (`rgba(109,40,217,.04)` + `border-left rgba(109,40,217,.1)`), avec override dark mode amber cohérent.
+
+## Bug DrillCellPanel en mode consolidé (2026-06-18)
+
+**Cause** : en mode consolidé (`groupByNom`), `plat.id` était remplacé par `plat.nom` (chaîne). `Number("BienPrêter") = NaN` → filtre `plateforme_id` ignoré → toutes les plateformes retournées.
+
+**Fix en 4 fichiers** :
+
+- `TableauInteretsPlateforme.jsx` : lors de la fusion, stocker `_ids: [id1, id2, ...]` (vrais IDs numériques). `onCellClick` passe `platId: plat._ids[0]` (numérique) + `platIds: plat._ids` (tableau complet).
+- `Dashboard.jsx` : destructure et propage `platIds` dans `drillCell`.
+- `DrillCellPanel.jsx` : si `cell.platIds.length > 1` et filtre non modifié manuellement, envoie `plateforme_ids=1,2,3` au lieu de `plateforme_id`.
+- `dashboard.js` (backend) : route `/detail-cellule` accepte `plateforme_ids` (CSV) → `IN (?, ?, ?)` dynamique ; rétrocompatible avec `plateforme_id` seul.
+
+**Why:** `Remboursements.jsx` passe `cellInfo` entier dans `setDrillCell`, donc `platIds` est automatiquement propagé sans modification.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_toggle_consolidation.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_toggle_consolidation.md
new file mode 100644
index 0000000..8b0234f
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_toggle_consolidation.md
@@ -0,0 +1,24 @@
+---
+name: project-toggle-consolidation
+description: Toggle Détaillé/Consolidé dans tous les tableaux mensuels — regroupe les plateformes par nom quelque soit le détenteur
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 752b3578-1999-4431-9dd9-73297a9baaf4
+---
+
+Fonctionnalité ajoutée sur les 4 tableaux "Vision mensuelle" :
+
+- `TableauInteretsPlateforme.jsx` (Dashboard + Remboursements)
+- `CapitalMensuelTable.jsx` (Investissements)
+- `DepotsMensuelTable.jsx` (Dépôts/Retraits)
+
+**Ce qui a été implémenté :**
+- Bouton `[Détaillé ▾]` / `[Consolidé ▲]` dans le `th` "Plateforme", visible uniquement si `multiDetenteur`
+- En mode Consolidé : lignes fusionnées par `nom` de plateforme, données sommées, poids recalculé sur total consolidé
+- Clé localStorage partagée : `cl_tip_group_by_nom` — changer le mode dans un tableau le change partout
+- `useMemo` de consolidation placé AVANT le `return null` conditionnel (règle des hooks React)
+
+**Helper fusion pour TableauInteretsPlateforme :** fonction `mergeMaps(mapA, mapB)` qui somme les champs `interets_bruts`, `interets_nets`, `cashback`, `capital`, `interets_prevus`, `capital_prevu` par clé de mois.
+
+**Why:** Olivier voulait pouvoir voir les données consolidées par plateforme indépendamment du détenteur (ex. "Enky" au lieu de "Enky Olivier" + "Enky Capucine").
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_depots_remb.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_depots_remb.md
new file mode 100644
index 0000000..b14bb9c
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_depots_remb.md
@@ -0,0 +1,31 @@
+---
+name: project-trendbadge-depots-remb
+description: "TrendBadge déployé sur DepotsRetraits (4 KPIs) et Remboursements (3 KPIs) — même pattern qu'Investissements"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: d05ae334-ab61-467e-9700-1d87d90f940a
+---
+
+TrendBadge ajouté aux KPIs de DepotsRetraits.jsx et Remboursements.jsx en session 2026-05-31.
+
+**DepotsRetraits — 4 KPIs :**
+- Dépôts, Retraits (invert=true), Diff. Dépôts vs Retraits, Porte-monnaie
+- `drPlatYear` initialisé à `String(new Date().getFullYear())`
+- `prevTotals` : filter allRows sur prevYear + même filtre plateforme
+- `prevKpiSoldePortefeuille` : même calcul que kpiSoldePortefeuille avec cutoff 31/12 N-1
+- `prevYear = String(Number(drPlatYear || new Date().getFullYear()) - 1)`
+
+**Remboursements — 3 KPIs :**
+- Capital remboursé, Cashback, Intérêts (Brut/Net)
+- `rembPlatYear` initialisé à `String(new Date().getFullYear())`
+- `prevTotals` : allRows filtrés sur rembPrevYear + filterPlatId + corrections N-1
+- `rembPrevYear = String(Number(rembPlatYear || new Date().getFullYear()) - 1)`
+
+**Pattern commun ([[project-trendbadge-investissements]]) :**
+- En vue "Toutes les années" : effectiveYear = année courante → badges vs N-1 toujours visibles
+- Valeur de référence N-1 en sous-titre de chaque KPI
+
+**Why:** Cohérence avec le Dashboard et la page Investissements.
+
+**How to apply:** Pour toute nouvelle page avec filtre année + KPIs, initialiser le state année à `String(new Date().getFullYear())` et calculer prevTotals avec `effectiveYear = year || currentYear`.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_investissements.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_investissements.md
new file mode 100644
index 0000000..4dfca70
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_trendbadge_investissements.md
@@ -0,0 +1,25 @@
+---
+name: project-trendbadge-investissements
+description: TrendBadge ajouté aux 5 KPIs de la page Investissements — comparaison N vs N-1
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: d05ae334-ab61-467e-9700-1d87d90f940a
+---
+
+TrendBadge (copié depuis Dashboard) ajouté aux 5 KPIs de Investissements.jsx : Capital investi, À risque, Depuis le début, Remboursés, Intérêts perçus.
+
+**Implémentation :**
+- `TrendBadge` défini en haut du fichier (avant le composant principal)
+- `isActiveAtEndOfYear(r, yr = platYear)` : paramètre `yr` optionnel pour réutilisation avec N-1
+- `prevTotals` memo : recalcule les 5 agrégats pour l'année N-1 (rembs + reinvests filtrés au 31/12 N-1)
+- `platYear` initialisé à `String(new Date().getFullYear())` pour afficher les badges dès le chargement
+
+**Comportement "Toutes les années" (`platYear = ''`) :**
+- `effectiveYear = platYear || String(new Date().getFullYear())`
+- Compare le total cumulé actuel vs le total cumulé au 31/12 de l'année précédente
+- Badges toujours visibles, même sans filtre année
+
+**Why:** Sans année sélectionnée, `prevTotals` retournait null → badges absents au chargement et en vue globale.
+
+**How to apply:** Si d'autres pages ont un filtre année + KPIs, appliquer le même pattern `effectiveYear` pour que les badges ne disparaissent pas en vue globale.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_user_preferences.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_user_preferences.md
new file mode 100644
index 0000000..7f5961e
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_user_preferences.md
@@ -0,0 +1,41 @@
+---
+name: project-user-preferences
+description: "Table user_preferences — stockage DB des préférences UI par utilisateur, route /api/preferences, sync UiContext"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: d7532bd8-8eb4-4b40-8c39-8f569dcf432f
+---
+
+Système de persistance des préférences UI en base de données, par utilisateur.
+
+## Table (backend/src/db/index.js)
+```sql
+CREATE TABLE IF NOT EXISTS user_preferences (
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ key TEXT NOT NULL,
+ value TEXT NOT NULL,
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
+ PRIMARY KEY (user_id, key)
+)
+```
+Table générique clé/valeur — extensible à toutes les futures prefs sans migration.
+
+## Route (backend/src/routes/preferences.js)
+- `GET /api/preferences` — retourne `{ key: value, ... }` pour l'utilisateur connecté
+- `PATCH /api/preferences` — upsert d'un ou plusieurs couples ; liste blanche des clés autorisées
+- Clés actuellement autorisées : `chart_interets`, `chart_capital`, `chart_cashback`
+- Pour ajouter une nouvelle pref : l'ajouter dans le Set `ALLOWED_KEYS` de la route
+
+## Stratégie de sync dans UiContext
+- **Au montage** : `api.get('/preferences')` → valeurs DB écrasent localStorage (DB fait foi). Silencieux si token absent.
+- **À chaque changement** : localStorage mis à jour immédiatement (feedback instantané) + `api.patch('/preferences', { key: value })` asynchrone silencieux en cas d'erreur réseau.
+
+## Extension future
+Pour persister d'autres prefs (thème, font_scale, langue, devise, display_mode) :
+1. Ajouter la clé dans `ALLOWED_KEYS` (preferences.js)
+2. Lire la valeur dans le `useEffect` de montage (UiContext)
+3. Appeler `persistPref(key, value)` dans le setter correspondant
+
+**Why:** L'utilisateur veut que ses préférences de couleurs soient liées à son compte et non au navigateur.
+**How to apply:** Toute nouvelle préférence UI candidate à la persistance multi-device doit passer par ce système.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_xlsx_export.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_xlsx_export.md
new file mode 100644
index 0000000..bf04013
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_xlsx_export.md
@@ -0,0 +1,18 @@
+---
+name: project_xlsx_export
+description: Export Excel migré vers SheetJS (xlsx) — vrais fichiers .xlsx sans avertissement
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: f2be746c-47f1-426a-ae80-3c71059fa38c
+---
+
+Les exports Excel des 3 pages utilisent SheetJS (`import * as XLSX from 'xlsx'`).
+
+- `xlsx` installé dans `frontend/` via npm
+- Pattern : `XLSX.utils.json_to_sheet(data)` → `XLSX.utils.book_append_sheet` → `XLSX.write(wb, { type: 'array', bookType: 'xlsx' })`
+- `dlBlob` reçoit un `Uint8Array`, MIME type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
+- Extension `.xlsx` (remplace l'ancien `.xml` / `.xls` SpreadsheetML qui déclenchait l'avertissement Excel)
+
+**Why:** L'ancien format SpreadsheetML XML avec extension `.xls` déclenchait un avertissement de sécurité dans Excel.
+**How to apply:** Toute nouvelle fonction d'export Excel doit suivre ce pattern SheetJS, pas le XML SpreadsheetML manuel.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_year_selector_kpi.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_year_selector_kpi.md
new file mode 100644
index 0000000..6016fcc
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_year_selector_kpi.md
@@ -0,0 +1,69 @@
+---
+name: project-year-selector-kpi
+description: "YearSelectorKpi — card sélecteur d'année violet/indigo sur la ligne KPI du Dashboard, synchronisé avec InteretsChartContext"
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 9d8a4dc8-0caa-4a53-bf00-040dfe2afe4a
+---
+
+## Emplacement
+
+Composant `YearSelectorKpi` défini dans `frontend/src/pages/Dashboard.jsx`, avant la fonction `Dashboard`. Doit être enfant de `InteretsChartProvider` pour accéder au contexte.
+
+## Layout Dashboard modifié
+
+Le `InteretsChartProvider` remonte pour englober à la fois la grille KPI et les graphiques :
+
+```jsx
+
+ {/* KPI + sélecteur */}
+
+
+ {/* 6 KPIs existants */}
+
+
+
+ {/* Graphiques */}
+
+ +
+
+
+```
+
+`alignItems:'stretch'` + `marginBottom:0` sur le kpi-grid (override CSS) → la card année a la même hauteur que la grille.
+
+## YearSelectorKpi
+
+- **Couleur** : `linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)` — violet/indigo, ombre `rgba(109,40,217,0.30)` qui s'intensifie à l'ouverture
+- **Texte blanc**, label "PÉRIODE" en xs uppercase, chevron SVG animé (rotate 180° à l'ouverture)
+- **Année en 2rem / bold** ; si modeGlobal actif → affiche "Depuis le début" en 1.1rem
+- `width: 200px`, `flexShrink: 0`
+
+## Dropdown custom
+
+- Options : `{ value:'all', label:'Depuis le début' }` en tête, puis `availableYears` en ordre **croissant** (pas de `.reverse()`)
+- Item actif : fond `rgba(109,40,217,0.08)`, couleur `#7c3aed`, bold, coche SVG ✓ à droite
+- Hover : `var(--surface-2)`
+- Séparateurs `borderBottom` entre items sauf le dernier
+- `position: absolute; top: calc(100% + 6px); right: 0; z-index: 200`
+- Fermeture sur clic extérieur via `useEffect` sur `mousedown`
+
+## Synchronisation bidirectionnelle
+
+```js
+const handleSelect = (value) => {
+ if (value === 'all') {
+ if (!modeGlobal) toggleModeGlobal(); // active TOUT
+ } else {
+ if (modeGlobal) toggleModeGlobal(); // désactive TOUT
+ setAnnee(value);
+ }
+};
+```
+
+- Changer l'année dans le bar chart → `annee` dans contexte → YearSelectorKpi se met à jour
+- Activer TOUT dans le bar chart → `modeGlobal` dans contexte → affiche "Depuis le début"
+
+**Why:** L'utilisateur doit voir l'année active de façon très visible et pouvoir la changer directement depuis le dashboard.
+**How to apply:** Toujours garder YearSelectorKpi à l'intérieur de InteretsChartProvider. Si on déplace les graphiques, déplacer le Provider en même temps.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_zip_backup_referentiel.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_zip_backup_referentiel.md
new file mode 100644
index 0000000..e442e10
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/project_zip_backup_referentiel.md
@@ -0,0 +1,41 @@
+---
+name: project-zip-backup-referentiel
+description: Système de sauvegarde ZIP pour le référentiel plateformes — export/import avec manifest + data + logos
+metadata:
+ node_type: memory
+ type: project
+ originSessionId: 264d200a-fea4-4903-b7c5-20dcc9ebfbd2
+---
+
+Système complet d'export/import ZIP pour `plateformes_referentiel`.
+
+**Fichiers créés/modifiés :**
+- `backend/src/utils/zip.js` — helper ZIP pur Node.js (pas de dépendance externe). `createZip(entries)` + `readZip(buffer)`. Utilise `zlib.deflateRawSync` / `inflateRawSync` + CRC-32 maison.
+- `backend/src/routes/referentiel.js` — 3 nouvelles routes (positionnées AVANT `/:id`) :
+ - `GET /api/referentiel/export` → télécharge tout le référentiel en ZIP
+ - `GET /api/referentiel/:id/export` → télécharge une seule entrée en ZIP
+ - `POST /api/referentiel/import-zip` → importe un ZIP (multipart `file`), écrase en cas de conflit de `nom`
+- `frontend/src/api.js` — méthode `api.blob(path)` ajoutée pour les téléchargements binaires authentifiés
+- `frontend/src/pages/AdminPlateformes.jsx` — `ReferentielSection` :
+ - Boutons "Exporter tout" + "Importer" dans la barre d'outils
+ - Entrée "Exporter" dans le menu ⋮ par ligne
+ - `importResult` banner via `ResultBanner`
+
+**Structure ZIP (version 1.1) :**
+```
+manifest.json { version: '1.1', app, exported_at, count, type }
+data.json [ { nom, url, domiciliation, ..., categories_inv: [noms], secteurs_inv: [noms], logo_filename, icone_filename, notation: [...] } ]
+garanties.json [ { libelle, description, ordre } ] — types de garanties du user exporteur
+logos/ fichiers image logo/icône (si présents sur le serveur)
+```
+
+**Import — comportement :**
+- Conflit `nom` → UPDATE (écrase tout)
+- `categories_inv` / `secteurs_inv` par noms → crée les tags globaux manquants (`user_id = NULL`)
+- Images dans le ZIP → écrites dans `logosDir` (= `DATA_DIR/logos`)
+- `notation` par plateforme → DELETE + re-INSERT dans `referentiel_notation`
+- `garanties.json` → upsert dans `garantie_types` pour le user qui importe
+
+**Why:** Demande explicite — sauvegarde et migration du référentiel entre instances.
+
+**How to apply:** Toute évolution du schéma `plateformes_referentiel` doit répercuter les nouveaux champs dans les routes export (liste `fields`) et dans la logique d'UPDATE/INSERT de l'import.
diff --git a/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/user_profile.md b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/user_profile.md
new file mode 100644
index 0000000..c01ed35
--- /dev/null
+++ b/.cowork/memory/ef8f4a3f-abbf-422c-8382-f0bf9d3a9cdf/memory/user_profile.md
@@ -0,0 +1,11 @@
+---
+name: Profil utilisateur
+description: Olivier, propriétaire et développeur de l'app crowdlending, pilote les évolutions fonctionnelles
+type: user
+originSessionId: b87fd892-4ff2-4b8b-b3e6-f0f92e303c8e
+---
+Olivier (olivier@croguennec.net) est le propriétaire de l'application crowdlending-app. Il pilote directement les évolutions fonctionnelles et techniques, formule des demandes précises, et valide les résultats visuellement dans l'interface.
+
+Il est à l'aise avec le code mais délègue l'implémentation. Il préfère des réponses concises et des recommandations directes (pas d'options multiples sauf demande explicite).
+
+L'application est un tracker de portefeuille de prêts participatifs, multi-investisseurs (famille + entreprises), usage personnel/familial.
diff --git a/.cowork/registertask.ps1 b/.cowork/registertask.ps1
new file mode 100644
index 0000000..6c7d549
--- /dev/null
+++ b/.cowork/registertask.ps1
@@ -0,0 +1,6 @@
+# Crée la tâche planifiée (toutes les heures)
+$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NonInteractive -File C:\dev\crowdlending-app\.cowork\sync-cowork-memory.ps1"
+$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Hours 1) -Once -At (Get-Date)
+$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
+
+Register-ScheduledTask -TaskName "CoworkMemorySync-crowdlending" -Action $action -Trigger $trigger -Settings $settings -RunLevel Highest -Force
\ No newline at end of file
diff --git a/.cowork/sync-cowork-memory.ps1 b/.cowork/sync-cowork-memory.ps1
new file mode 100644
index 0000000..7a1867b
--- /dev/null
+++ b/.cowork/sync-cowork-memory.ps1
@@ -0,0 +1,18 @@
+# sync-cowork-memory.ps1
+$memorySource = "C:\Users\olivi\AppData\Roaming\Claude\local-agent-mode-sessions\cad3a9c0-cf8e-4204-ac6e-e22c03811abe\7707aa40-4613-41ca-918f-a32d6df19bce\spaces"
+$memoryDest = "C:\dev\crowdlending-app\.cowork\memory"
+$repoPath = "C:\dev\crowdlending-app"
+
+# Sync des fichiers mémoire
+robocopy $memorySource $memoryDest *.md /E /XO /NFL /NDL
+
+# Commit Git si des changements existent
+Set-Location $repoPath
+$status = git status --porcelain .cowork
+if ($status) {
+ git add .cowork
+ git commit -m "chore: sync cowork memory $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
+ Write-Host "Mémoire Cowork synchronisée et commitée." -ForegroundColor Green
+} else {
+ Write-Host "Aucun changement détecté." -ForegroundColor Gray
+}
\ No newline at end of file