## Project Memory

> Auto-genere par /memorize le 2026-02-18 — **refonte Solution B appliquee le 2026-06-12** (voir `.memory/` + `.insights/brainstorm-vitrine-refonte-2026-06-12.md` + `.insights/vitrine-feature-truth-matrix.md` a la racine www) — maj /memorize-update le 2026-07-31.
> Documentation complete dans `.memory/`

- **Stack** : Laravel 9 + Vite 6 + Tailwind CSS 4 + Alpine.js 3 + GSAP 3.12
- **Fichiers source** : 146
- **Couverture** : 100% (146/146) — les 27 `config/*.php` standards et le boilerplate `tests/` sont hors perimetre (documente dans `architecture.md`)

### Architecture (post-refonte 2026-06-12)
- **CGV, article 7 (2026-10-31, soir)** : `pages/legal/cgv.blade.php` - 7.3 accès immédiat demandé ET perte du droit de rétractation reconnue une fois le service entièrement fourni (L221-25), prorata (durée de la période, ou crédits utilisés pour une recharge), remboursement sous 14 jours ; 7.4 recharge entièrement utilisée = plus de rétractation (L221-28 1°), abonnement = rétractation possible au prorata ; formulaire en 7.5 ; « Dernière mise à jour : 31 octobre 2026 » (= `payment.terms_version` du core, enregistré avec chaque paiement). ⚠ Jamais de renonciation pure au droit de rétractation pour un abonnement (service numérique, CJUE C-234/25). Manques : fonction « Renoncer au contrat ici » (L221-21, D221-5, obligatoire depuis le 19/06/2026), médiateur de la consommation. Détail : `rs_infoparcelle_application/.insights/checkout-v2-2026-10-31/README.md`. Déploiement : `git pull` + `view:clear`.
- Site vitrine SaaS B2B Infoparcelle — positionnement « Donnees foncieres + CRM immobilier » (plus seulement data)
- **Plus de pages generiques config-driven** : les 52 pages dispatchers (27 features + 11 clients + 14 use-cases) ont ete SUPPRIMEES sans redirection (site jamais en prod ; module redirections core/admin si besoin un jour)
- **11 pages piliers** dediees dans `pages/produit/` : carte-cadastrale, localiser-une-annonce (differenciateur top 10 parcelles), estimation, crm-immobilier, documents-et-conformite, portail-client, widgets, **avis-clients** (ajoute depuis : avis adosses a une transaction reelle, etoiles Google via schema.org, page publique `/avis/p/{slug}`, conformite decret 2017-1436 + principes ISO 20488), gestion-agence, api, **mcp (page pilier complete depuis le 2026-08-20 — le serveur est en production)**
- ⚠ **URLs a la racine** : `/estimation`, `/widgets`, `/avis-clients`… — seul le chemin des VUES porte le prefixe `produit/`. 32 routes web (31 GET + 1 POST contact)
- ⚠ **Autoload PSR-4 : l'override `Rootsud\Core\ → ../core/src/` est declare dans `composer.json` mais N'EST PAS ACTIF** (mesure par Reflexion le 2026-08-20 : `ApiAccessController` et les vues `core::` sont resolus depuis `vendor/rootsud/core`, sur les 3 projets - vitrine, application, administration). Un `composer update` l'a retire. **Toute modification du core doit donc etre recopiee dans les `vendor/` pour etre testee en dev**, et seulement vers les projets a la MEME version (`composer.lock`) : vitrine / application / administration sont en **1.0.200**, captimmo et rootsud en **1.0.189** (ne pas leur copier).
- **Images marketing** : source commitee `resources/images/` (42 captures webp + og-default), copiee vers `public/img/` au build par `vite-plugin-static-copy` — ne jamais editer `public/img/` a la main
- **7 pages metiers** dans `pages/metiers/` : index + agent-immobilier, agence-reseau, investisseur, notaire, prestataire, **particulier (ajoute 2026-06-12** — vendre/louer sans agence : claims sources sur `config/lead_capabilities.php` tier particulier = rental.docs 6 baux/rental.edl/keys.pv/adverts.publish/matching/offers/rental.applications/docs.request/estimation.avis/portal ; PAS de Hoguet/honoraires ; bail particulier = **signature papier forcee** → ne jamais promettre l'e-sign du bail ; compromis → notaire). Present dans : nav mega-menu metiers (icone `home`, deja tree-shakee), footer, hub /metiers (6 cartes), grille home, sitemap, route+PageController
- `config/vitrine.php` REDUIT (~420 lignes) : navigation (3 mega-menus), footer (5 col), stats, pricing ids, comparison_features (quotas REELS verifies en BDD billing_plan_extras), faq, faq_trial, trust_badges, how_it_works, client_logos. Clés supprimees : features/clients/use_cases/mcp_tools/testimonials/feature_categories
- **10 composants de section** (+ `inline-cta`, 2026-10-01) dans `components/section/` : heading, hero-pillar, screenshot (capture reelle + fallback mockup via file_exists), feature-split, steps, faq (+JSON-LD), feature-grid, comparison, cross-links — utilises via @include (PAS x-components)
- Mega-dropdown generique unique `partials/mega-dropdown/menu.blade.php` (groups|items + badge « Bientot »)
- **Audit anti-oubli fonctionnel (2026-06-12)** : croisement exhaustif app+cores vs vitrine → 4 ajouts : (1) **multiparcelle** (section feature-split + FAQ sur carte-cadastrale, item estimation, FAQ localiser-une-annonce, slot `multiparcelle.webp` = vraie fiche 2 parcelles EI0410+EI0411 — claims sources : Fr_parcel_multiple, $is_multiparcel card.blade, multiparcel_id partout) ; (2) **webhooks sortants** (section sur /api : Zapier/Make/HubSpot/Brevo/Salesforce/URL custom — source config/pipeline_crm_integrations.php) ; (3) **notifications temps reel + PWA navigateur** et (4) **recherche universelle (omnibar) + mobile/terrain** (3 items grid crm-immobilier). Le reste etait deja couvert (verifie par grep concept par concept : agenda/GCal, analyse de marche, secteurs, historique, recherches sauvegardees, textes IA, avis clients/NPS, loi Climat, PV cles, lettre prospection+QR, filiation, extrait cadastral…). Volontairement non couverts (mineurs) : export CSV leads, magic link, 2FA compte
- Captures reelles : **39 webp (38 slots + multiparcelle), plus AUCUN mockup fallback** dans `resources/images/screenshots/` (SOURCE commitable) copiees vers `public/img/screenshots/` au build via vite-plugin-static-copy — produites avec le compte demo Agence Horizon (cf. memoire demo-accounts-infoparcelle), harness CDP + headless jetable. **Passe QA complete 2026-06-12** : 37/38 refaites apres revue 1 par 1 (donnees demo enrichies : estimation 663 285k, registre fees+client+bien, agenda 12 RDV, baremes honoraires agence, civilites Mme/Mr, profil conseiller complet). carte-cadastrale = **3D photorealiste** parcelle 66136000EI0410 (HEADED Chrome requis pour gmp-map-3d) ; exports-plans = vrai plan cadastral PDF EI0410 ; avis-valeur-pdf = vrai PDF regenere (pages 1+2 cote a cote, pdftoppm) ; widget-hosted/widget-estimation = page hostee avec carte de fond (apparait seulement apres selection d'adresse) ; widget-embed = URL ngrok retouchee en app.infoparcelle.fr ; scoring-investissement = detail annonce score 34 + radar (distinct de pige-annonces) ; portail-claim = « Bienvenue chez Agence Horizon » (fix reel resolvePortalAgencyName dans ClientPortalPublicController de l'app)
- `public/img/og-default.png` genere (GD 1200x630)
- **`localiser-annonce.webp` refaite le 2026-09-30** (1440x1320, 287 Ko) : annonce 228172539 (Mougins, maison avec piscine), les 2 derniers `.row` de `.advert-small-content` retires (titre/prix/description + caracteristiques) pour faire apparaitre le **top 10** sous les photos, 1re parcelle (06085000AI0004, score 82) cliquee = contour orange sur la carte satellite. Capturee avec le **compte 2** : le compte demo 214295 n'existe pas dans `core.users` de la base preprod 5430 qui porte desormais `pgsql_city_user` (FK `lead_statuses_user_id_foreign` → la fiche annonce tombe en 500).

### Plan SEO de novembre 2026 (2026-11-03, lots 1 à 8)

Plan et bilan : `.insights/plan-seo-novembre-2026-11-03.md`. Rien commité, branche inchangée.
- **Outils à l'adresse racine** : clé `path` de `config/outils.php` (`/trouver-proprietaire-parcelle`, `/plan-de-situation`,
  `/dpe-parcelle`, `/cadastre-inverse`), assistant unique **`App\Services\Outils\OutilUrl`** (`path`, `to`, `available`,
  `all`, `slugForPath`) : toute adresse d'outil d'une vue, du plan du site, de `/llms.txt` ou d'une page commune passe
  par lui ; `/outils/{slug}` redirige en 301 (paramètres conservés). ⚠ Un lien vers un outil depuis une page du site doit
  être gardé par `OutilUrl::available()` (outils coupés en production = 404). ⚠ Une directive Blade collée à un mot
  (`relais@if`) n'est pas compilée et casse la page : passer par `{!! cond ? ... : '' !!}`.
- **Trois outils de plus à l'adresse racine (2026-11-03, suite)** : `/reference-cadastrale` (« référence cadastrale »
  3 600 recherches par mois), `/zone-tendue` (4 400), `/prix-m2` (outil `prix-immobilier`) ; clé `path` de
  `config/outils.php`, anciennes adresses `/outils/{slug}` en 301 avec leurs paramètres. Restent sous `/outils/` :
  `localiser-annonce` et `plu`.
- **Outil « Cadastre inversé »** (`Tools\CadastreInverse`, champs `surface` + `tolerance` 0/1/2/5/10 %, BAN en
  `type=municipality`, commune + arrondissements, 30 parcelles au plus, cache `v1`) ; résultat lié à l'outil Référence.
- **Pages nouvelles** : `/pige-immobiliere`, `/etude-precision-localisation` (Article, chiffres de `locate_benchmark`),
  `/api/{cadastre|dvf|dpe}` (`config/api_datasets.php`, réponses réelles de l'API, coûts lus dans `useQuota`, ⚠ colonnes
  en `min-w-0` sinon le bloc de code déborde à 390 px), `/historique-parcelle` et `/extrait-cadastral` (`pages/guides/`),
  `/application-mobile` (Android seul, capture `application-mobile-fiche.webp` recadrée sans adresse),
  `/metiers/promoteur` (filtres relevés dans la table `filters` du site), `/comparatif/{apex-notaires|parcellaire}`
  (`config/comparatifs.php` : colonne concurrente masquée, `noindex` et hors plan du site tant que `checked_at` est vide ;
  remplis le 2026-11-03, voir plus bas).
- **Titres et H1** de 6 piliers (tableau 2.2 du plan), exemple d'avis de valeur (`exemple-avis-de-valeur-p{2,4,8}.webp`),
  guide Hoguet sur `/documents-et-conformite`, section portails sur `/localiser-une-annonce`.
- **Communes** : `communes.no_dvf_departments` (57, 67, 68, 976) = population seule dans `communes:publish` et
  `CommunePresenter::noDvf()` (mention à la place des prix, question de FAQ, pages commune et département).
- **Maillage** : Ressources en 3 groupes (Guides, Développeurs, Entreprise, `w-[760px]`), Pige dans Produit, Promoteur
  dans Métiers ; pied de page à colonnes comptées (`lg:grid-cols-5|6|7`, marque sur une ligne pleine largeur), colonnes
  Guides, Cadastre (si communes publiées) et Développeurs.
- Compteurs écrits par le serveur (`countUp`), `sameAs` Google Play, index de sitemaps inconditionnel, redirections
  `.insights/seed-redirections-seo-2026-11.php` (à rejouer en prod). `crawl.py` n'exclut plus que `/api/v1`.
- **Suite du même jour** : une section animée par page nouvelle, composants `components/anim/` (`pige-flow`,
  `api-flow`, `history-timeline`, `cerfa-fill`, `phone-day`, `land-funnel`, `data-orbit`) + aperçu
  `pages/outils/previews/cadastre-inverse`, styles `an-*` en fin d'`app.css` (déclenchés par `inView` → `is-in`,
  figés sous `prefers-reduced-motion`). ⚠ Un élément agrandi ou décalé avant son apparition (tampon `an-stamp`) fait
  déborder à 390 px : `overflow-x-clip` sur le conteneur. ⚠ L'orbite place ses étiquettes sur deux rayons (`--r`,
  `--r2`, choisi par `--rr`) dans un ordre court / long mesuré sans chevauchement : changer l'ordre ou les rayons
  impose de remesurer sur un tour complet. Guide Hoguet sourcé (loi 70-9 art. 3, décret 72-678 art. 72,
  Légifrance), extrait cadastral = Premium et CRM, DVF depuis le 1er janvier 2014 (dernière vente 31/12/2025),
  30 portails relevés par l'utilisateur sur `/localiser-une-annonce` (PAP compris).
- **Page `/comparatif`** (`PageController::comparatifs`, vue `pages/comparatifs`, animation `components/anim/tools-merge`) :
  liste les comparatifs publiés (`checked_at` renseigné) en deux groupes (`category` `marche` | `public`, tri par
  `order`, carte = `card_kind` + `card_title` + `card`), méthode et lien de correction ; seul lien du pied de page
  (colonne Guides), dans le plan du site et `/llms.txt`. Route `/comparatif/{slug}` en `[a-z0-9-]+` (404 si absent de
  la config). Liens croisés d'une page vers un outil gardés par `OutilUrl::available()`. Bloc « Autres comparatifs »
  en bas de chaque page (même groupe d'abord) : sans lui, chaque comparatif n'avait qu'un lien entrant.
- **10 comparatifs (2026-11-03)** : marché = Telescop, La Loupe Immo, Pappers Immobilier, KelFoncier, Parcellaire,
  SurfaceParcelle ; services publics et professionnels = cadastre.gouv.fr, Géoportail (cartes.gouv.fr), Géofoncier,
  Apex (SPDC). Choix : demande mesurée dans la Search Console (SurfaceParcelle ~650 impressions en 6 mois, Apex ~340,
  Parcellai.re ~200, cadastre.gouv.fr, Géofoncier) + concurrents directs de l'étude tarifaire de juin. 🟥 Parcelle.com
  (175 impressions) n'est qu'un nom de domaine à vendre : pas de page. 🟥 Le Géoportail a fermé en septembre 2026,
  son successeur est cartes.gouv.fr (IGN). Colonne Infoparcelle relue sur la grille de production (`/tarifs` en ligne).
- 🟥 **Colonne Infoparcelle = toujours une valeur du tableau `$IP` en tête de `config/comparatifs.php`** (retour
  utilisateur du 2026-11-03 : « Annonces de plusieurs portails » face à « Annonces des particuliers et des
  professionnels, baisses de prix » faisait croire qu'Infoparcelle en faisait moins). Chaque valeur décrit TOUT ce que
  fait l'application (pige : vente et location, particuliers et pros, plus de 30 portails, filtres ; suivi : historique,
  alerte de baisse de prix, recherches sauvegardées ; 52 critères de recherche de la table `filters`, DPE d'avant 2021…),
  relue sur la grille de production, la matrice de vérité et la base. Une nouvelle fonction se déclare une fois dans
  `$IP` et apparaît sur les 10 pages.
- **Structure d'une page comparative = celle de « Qonto ou Pennylane » de Qonto (relevée le 2026-11-03)** :
  en-tête (preuve « Plus de 1 000 professionnels », boutons « Essayer Infoparcelle » et « Voir le tableau »), logos
  clients (pas de notes : aucune vérifiée), tableau « ce qu'il faut retenir » en 5 groupes (par début du libellé de la
  ligne, `$groupDefs` de la vue) avec coche / tiret, boutons sous le tableau, « plus qu'une carte cadastrale » (4 cartes
  + orbite animée), 3 blocs à capture réelle + appel, « Découvrez les offres » (forfaits mensuels publics, prix lus en
  base avec la promotion en cours : `PageController::comparatifOffers`, cache 10 min), « Comparez Infoparcelle à
  d'autres solutions », bandeau, « En savoir plus » (`about` du concurrent + bloc Infoparcelle), « Pourquoi choisir
  Infoparcelle plutôt que X ? » (`reasons`, 5 raisons tirées du tableau ; « en plus de » pour un service public),
  « Qui utilise… » (`who` + métiers + mobile), FAQ (FAQPage, générée), « Pour aller plus loin », méthode et
  correction. Aucun score ni « gagnant » écrit (L122-1) ; `:prix` dans une raison = prix d'appel lu en base.
- **Audit SEO des comparatifs (2026-11-03)** : titres et H1 sur les recherches réelles (« apex cadastre » 248
  impressions en 6 mois, « parcellai.re » 184, « comparatif et prix » pour les logiciels du marché), descriptions avec
  les mots cherchés (nom du propriétaire, prix de vente, historique, surfaceparcelle.com), `dateModified` = date du
  relevé, `ItemList` sur `/comparatif`, `th scope`. Contenu commun : chaque page choisit 3 des 9 blocs de `$blockLib`
  (`blocks` en config, jamais les 3 mêmes que la page la plus proche) et a 2 questions de FAQ propres (`faq`) ; mesuré
  (Jaccard 5-grammes, hors menu et pied) : 41 % en moyenne entre deux comparatifs (maximum 48 %), dont 8 à 11 % de
  gabarit commun à tout le site. Le reste vient de la colonne Infoparcelle, identique à dessein.
- **Similarité réduite (2026-11-03, suite)** : mesurée par Jaccard 5-grammes hors menu et pied sur 12 communes du 66
  (palier 3 publié le temps de la mesure, fichier remis à l'identique) et les 10 comparatifs. Communes 39 → 28 % en
  moyenne, comparatifs 41 → 34 %. Communes : `CommunePresenter::facts()` (maison face à appartement, rang parmi les
  voisines, profil du PLU, sections de A à Z et la plus fournie, DPE et logements autorisés pour 1 000 habitants, part des
  terrains constructibles, lots par copropriété) remplace les phrases d'explication identiques ; `abcText()` (une phrase
  par zone ABC) ; FAQ avec maison / appartement, voisines, permis, copropriétés, zone tendue courte, sans les phrases
  génériques ; documents, « Pour aller plus loin » et sources rédigés avec les chiffres de la commune ; « Autres
  communes » = les 9 publiées les plus proches puis 3 des plus peuplées (elles étaient les 12 mêmes partout).
  Comparatifs : clé `offer` [forfait, phrase] par page (forfait mis en avant « Conseillé face à X », prix du service en
  sous-titre des offres), points des forfaits choisis d'après les `blocks` de la page, 4 cartes de « plus qu'une carte
  cadastrale » tirées d'une réserve de 9 sans reprendre les blocs, essai dans la question du prix, « Qui utilise » d'après
  `audience`. Ce qui reste commun : la structure et la colonne Infoparcelle du tableau, identique à dessein.
  Puis contenu propre ajouté : **« L'essentiel sur X »** sous les chiffres clés des communes
  (`CommunePresenter::essentials`, instantanés du département déjà en cache : rang de prix parmi les communes d'au moins
  30 ventes, part des DPE F ou G et des annonces de particuliers face au département, logements autorisés pour 1 000
  habitants face au département, année de ventes la plus active, écart entre quartiers, densité tirée de
  `fr_insees.superficie` en m², exposée par `CommuneData::contour()` en `area_km2`, cache `contour.v2`) ; section
  **« lequel choisir »** des comparatifs (clé `choose` : `them` / `us`, trois cas chacun tirés des lignes relevées,
  « Choisissez X si vous » / « Utilisez X pour » pour un service public, aucun gagnant) + question de FAQ. Comparatifs
  32 % en moyenne ; communes inchangées à 28 % (gabarits où seuls les chiffres changent : la mesure en 5-grammes ne
  bouge plus, l'information propre à chaque page augmente).
- **Descriptions des pages départementales** : elles commençaient par « Le département compte… », sans le nom du
  département. Désormais « Cadastre de l'Allier (03) : plan cadastral, PLU, prix au m² et annonces des 317 communes. »
  puis les chiffres tant qu'elles tiennent en 160 caractères (pas de prix au m² en Alsace-Moselle et à Mayotte) ;
  101 pages entre 120 et 160 caractères.
- **Comparatifs remplis le 2026-11-03** (`checked_at`, sources sous le tableau, pages indexées). 🟥 **Apex = portail
  de la DGFiP vers le SPDC** (serveur cadastral officiel réservé aux notaires et géomètres habilités, seul à délivrer
  l'extrait « modèle 1 ») : jamais présenté comme un concurrent remplacé par Infoparcelle ; page en complémentarité,
  liée depuis `/metiers/notaire`. Parcellaire : éditeur Karpentech, offre gratuite ou 10 € TTC par mois. « Non indiqué »
  = absent des pages consultées, jamais « non ». Relever de nouveau avant de changer une valeur.
- Déploiement : `git pull` + `npm run build` + `view:clear` + `route:clear` + `config:clear`, puis le script de redirections.
- **Titres de l'accueil (2026-11-04, solution C du brainstorm, validée par l'utilisateur)** : titre « Infoparcelle :
  cadastre, parcelles et CRM des pros de l'immobilier » (576 px, l'ancien faisait 635 px et était coupé), description
  « 94 millions de parcelles cadastrales : … », **H1 FIXE « Le cadastre des pros / de l'immobilier. »** dans
  `ui/home-hero` (plus de `x-text` ni de `data-h1` / `data-h2` ; taille `clamp(1.75rem,9vw,2.25rem)` sur téléphone
  pour 2 lignes jusqu'à 320 px), pastille « Cadastre + CRM immobilier », sous-titre qui porte l'action de l'onglet
  (« Collez une annonce : nous trouvons les parcelles cadastrales les plus probables… », « Tapez une adresse : … »),
  même H1 sur le hero de repli, H2 descriptifs (« De l'annonce localisée au mandat signé », « Cadastre, estimation,
  CRM : toute la plateforme », « Pour chaque métier de l'immobilier », « Outils cadastre gratuits : parcelle, plans,
  prix, DPE ») et `how-it-works` inclus sans section titrée autour (deux H2 à la suite avant). Raison mesurée :
  mai-juillet (titre « outils cadastral », H1 « Le cadastre innovant et performant ») = position 13,5 et 196 clics par
  trimestre sur les requêtes cadastre / parcelle propres à l'accueil ; août-octobre (« Données foncières & CRM »)
  = 25,1 et 94. 🟥 Ne pas remettre de mots clés des pages dédiées dans le titre de l'accueil (cadastre inversé,
  référence cadastrale, prix m², DPE, CRM immobilier ont leur page) ; « annonce immobilière » (9 900 / mois) = intention
  « consulter des annonces », jamais dans le titre ni le H1. Note, mesures et suivi :
  `.insights/audit-seo-2026-11-03/accueil-titres-2026-11-04.md` + `compare-accueil.py` (références Search Console
  mai-juillet et août-octobre dans le même dossier). Build Vite nécessaire (classe `clamp` nouvelle). Déploiement :
  `git pull` + `npm run build` + `view:clear`, puis « Demander une indexation » de l'accueil dans la Search Console.
  **Déployé le 2026-11-04** (contrôlé en production : titre, description, H1 fixe unique, classe `clamp` compilée ;
  titre déjà repris par Google sur « infoparcelle »). Suite le même jour : la question de l'onglet Annonce devient
  « Quelle est la parcelle de cette annonce ? » (`config/outils.php`, vaut pour tous les heros de l'outil). Les 4
  questions tiennent alors sur une ligne dès 347 px de large (la plus longue, « Quelle parcelle se trouve à cette
  adresse ? », mesure 307 px en 15 px) : la réservation de 2 lignes posée le matin (`min-h-[2.75em]`) laissait un
  vide au-dessus de la question et a été retirée (classes d'origine). Formulaire au même endroit sur les 4 onglets à
  360 et 375 px de largeur utile. ⚠ Garder chaque question sous 307 px, sinon le formulaire se décale d'un onglet
  à l'autre sur téléphone.
- **Deux pages sur « localiser une annonce » (2026-11-04)** : Google ne montrait que `/localiser-une-annonce` (3 392
  impressions en 12 mois, 7e sur « localiser annonce immobilière », 12e sur « trouver adresse annonce immobilière ») ;
  `/outils/localiser-annonce`, indexée le 2026-11-04, portait un titre commençant pareil. Son `meta_title`
  (`config/outils.php`) devient « Trouver la parcelle d'une annonce immobilière : outil gratuit » ; la page produit
  garde « Localiser une annonce immobilière et trouver son adresse ». À contrôler fin novembre : si la Search Console
  montre les deux pages sur les mêmes recherches, canonique de la page d'outil vers la page produit. Déploiement :
  `git pull` + `config:clear`.

### Fenêtres de connexion et d'inscription : saisie automatique (2026-11-01, audit mobile, point 14)

`partials/login-modal.blade.php` et `partials/signup-modal.blade.php` : `autocomplete="email"`, `autocapitalize="off"`,
`spellcheck="false"` sur l'adresse, `autocomplete="current-password"` sur le mot de passe de connexion (celui de
l'inscription portait déjà `new-password`). Le gestionnaire de mots de passe du téléphone propose enfin l'adresse et
enregistre le mot de passe. Les pages `/login`, `/register`, `/forgot`, `/reset` viennent du core (mêmes attributs,
œil du mot de passe réparé). Le reste de la vague 1 (page de paiement, fenêtre des forfaits, déconnexion) arrive par
le tag core. Déploiement : `git pull` + `view:clear`. Compte rendu : `/home/julio/www/.insights/mobile-audit-2026-11-01/VAGUE1-README.md`.

### Lien de confirmation d'e-mail : il connecte la personne (2026-11-01, audit mobile, point 3)

Ouvert sans session (Chrome depuis la messagerie), un lien valide qui confirme l'adresse connecte la personne et mène à l'application (session partagée) ; rouvert dans un autre navigateur ensuite : connexion, « déjà confirmée ». Bancs alignés : `.insights/tunnel-b7-b12-2026-10-26.sh` 24/24 et `.insights/activation-vitrine-2026-10-23.sh` 6/6 (il utilisait l'ancien lien non signé, refusé depuis le 26/10). Rien à déployer sur la vitrine en dehors du tag core.

### Page `/localiser-une-annonce` refondue sur les chiffres du banc (2026-09-30)

- **Source unique des chiffres** : `config('vitrine.locate_benchmark')`. 🟥 **Les annonces de référence du banc
  (5 789) sont plus faciles que la réalité** : 72 % ont un meilleur score > 80 contre **34 %** des annonces en ligne,
  et 51 % des annonces en ligne sont sous 70. Afficher 82/93 (toutes références) ou 73/88 (références
  indépendantes) surévaluait nettement le ressenti dans l'application (retour utilisateur). Méthode retenue : 600
  annonces en ligne tirées au hasard rejouées (`rs_infoparcelle_application/.insights/advert-locate-benchmark/locate-score/rand600`),
  justesse par tranche de score appliquée à leurs scores → 51-64 % en tête, 72-84 % dans le top 10 ; valeurs
  publiées choisies par l'utilisateur : **57 / 78** (grille 57 / 21 / 22). Tranches : > 80 → 85 % (34 % des
  annonces), 70-80 → 57 % (14 %), < 70 → 35 % (51 %), justesse des références hors empreinte DPE. Titre : « La
  bonne parcelle dans le top 10 78 fois sur 100 ». Mentions ailleurs : « près de 8 fois sur 10 » dans le top 10.
  Refaire le tirage au hasard après chaque évolution du moteur avant de toucher ces valeurs.
- **Encart « Un moteur qui progresse en continu »** sous les graphiques (pastille « Mise à jour : <mois> », lue sur
  `measured_at`) : affirme qu'une évolution n'est mise en ligne que si elle bat la précédente (règle de bascule du banc).
  ⚠ Mettre `measured_at` à jour à chaque nouvelle mesure, sinon la pastille vieillit.
- **Composant** `components/section/locate-proof.blade.php` (retravaillé le 2026-09-30 sur retour utilisateur : 2
  visuels au lieu de 4) : grille « sur 100 annonces » (73 en tête / 15 aux rangs 2 à 10 / 12 hors top 10, carrés
  animés, survol d'une légende = groupe mis en avant) et justesse de la 1re parcelle par tranche de score. Avant /
  aujourd'hui et maisons / terrains retirés. Section « Dans l'autre sens » : « environ 3 fois sur 4 » (`reverse_label`),
  testé sur des parcelles portant une annonce connue (banc inverse : 59 / 81 sur 3 348 parcelles, 85 % = liens d'agents
  seuls, jugé non représentatif). Composant Alpine `locateProof`
  (`shown`, `focus`), transitions `.lp-cell/.lp-bar/.lp-fade` dans `app.css`, coupées sous `prefers-reduced-motion`.
  Couleurs validées (dataviz) : ip-700 / ip-400 / gray-300, valeurs toujours écrites. Ton : jamais « sans arrondi
  flatteur » ni formule défensive, titre « La bonne parcelle en tête 73 fois sur 100 ».
- **CTA de mi-page (2026-10-01)** : composant `components/section/inline-cta` en fin de section « Sous le capot » (« Testez-le sur votre prochaine annonce »), mêmes boutons que le hero et le bandeau final, sans fond pleine largeur pour garder l'alternance blanc / gris. Réutilisable sur les autres piliers.
- ⚠ Un `<table class="sr-only">` DÉBORDE (un tableau ignore `width: 1px`, +97 px en 390 px) : envelopper dans
  `<div class="sr-only">`.
- Nouvelles sections : preuves chiffrées, « Sous le capot » (6 signaux), sens inverse (onglet Annonces de la fiche
  parcelle), opportunités « Annonce localisée ». 2 captures ajoutées : `fiche-parcelle-annonces.webp`
  (AI0004 Mougins, annonce vraie en tête à 92) et `opportunites-annonce-localisee.webp` (liste, x2). Icônes
  `Trees`, `Waves`, `ArrowLeftRight` ajoutées aux deux blocs d'`app.js`.
- Mentions alignées : accueil, `/pourquoi-infoparcelle`, `/fonctionnalites`, `/metiers/agent-immobilier`,
  `/carte-cadastrale`, méga-menu, FAQ globale (« près de 3 fois sur 4 »). Matrice de vérité : lignes 1b à 1e.
- ⚠ La section opportunités décrit une fonctionnalité livrée le 2026-09-30 : **ne publier qu'après son déploiement**.

### Tunnel de vente (2026-08-02)

- **La vitrine n'est pas un site statique** : le `RouteServiceProvider` du core y injecte
  `/login`, `/authenticate`, `/register`, `/api/v1/register`, `/autologin`, `/paiement/{id}`,
  `/paiement-edit/{id}/{planId?}`, `/trial/{id}`, `/compte/*`. Vitrine et application partagent
  `APP_KEY` + `APP_PROJECT=infoparcelle` + la table `sessions` (`pgsql_root`), donc le cookie
  `infoparcelle_session` pose sur le domaine parent vaut pour les deux. Se connecter sur la
  vitrine connecte aussi l'app. ⚠ **En prod, `SESSION_DOMAIN` doit valoir `.infoparcelle.fr`
  des deux cotes** (en dev l'app est sur ngrok, donc seul le sens vitrine -> app est verifiable).
- **`/tarifs` est personnalise** : anonyme -> grille publique derivee de
  `billing_plan_options.lock_visibility` ; connecte -> `HasPlan::getUserPlan()`, qui substitue
  les tarifs negocies (reseau, enseigne) a la grille publique via `cleanUserPlan()`.
- **CTA contextuels** : connecte -> `getSubscriptionLink()` (paiement / essai / changement
  d'abonnement, « Abonnement Actuel » non cliquable) ; anonyme -> `$dispatch('choose-plan')`
  qui memorise `url.intended = /paiement/{id}` avant d'ouvrir l'inscription. Apres connexion
  OU inscription, l'utilisateur atterrit sur le paiement du plan choisi.
- 🟥 **`x-data` sur le lien lui-même (corrigé 2026-10-01)** : ni `<body>` ni `<main>` ne portent de `x-data`, donc un `@click.prevent="$dispatch(...)"` hors composant Alpine est IGNORÉ et le lien part sur `/register` au lieu d'ouvrir la modale. C'était le cas du hero des 11 piliers, de la home et du bandeau `cta-banner`. `cta-signup` et `cta-banner` posent désormais `x-data` sur l'ancre. Même correctif sur le bouton des cartes tarifaires (`pricing-card`, les cartes API vivent hors de `pricingToggle`) et sur les deux « Se connecter » de `/tarifs` (bandeau partenaire de `pages/pricing`, encart membres de `pricing-promo`). Tout nouveau déclencheur d'évènement Alpine doit porter son `x-data` s'il n'est pas DANS un composant. Audit rejouable sur tout le sitemap : `docker exec docker-rs-1 python3 /var/www/rs_infoparcelle_vitrine/.insights/audit-alpine-orphans-2026-10-01.py` (seuls restent les `x-show` inertes des cartes API, voulus).
- **TOUT CTA d'inscription passe par `partials/cta-signup.blade.php`** (jamais un
  `open-signup` ecrit a la main) : les modales d'auth n'etant rendues que hors connexion,
  un bouton qui annule la navigation pour ouvrir une modale absente est **inerte** pour un
  abonne. C'est ce qui avait neutralise le CTA principal de la home et des 11 piliers.
- **L'essai gratuit ne memorise pas `/trial/{id}` avant une inscription** : l'essai est
  deja cree par `checkAndCreateUser`, donc la route jugerait l'utilisateur non eligible et
  le renverrait sur la home de la vitrine. L'intention pointe directement sur l'application.
- **Le comparatif (vue Tableau) est DYNAMIQUE** (2026-08-02) : colonnes = les plans affiches,
  lignes = `project_extras` marques `show_in_table` + `function` (groupes par `category`,
  tries par `order`), cellules = `billing_plan_extras`. Meme logique que le comparatif du
  CMS core. La cle `comparison_features` de `config/vitrine.php` a ete **supprimee** : cette
  liste editoriale figee divergeait en silence des que la grille evoluait en base, et ne
  pouvait pas etre juste a la fois en dev et en prod (3 incoherences constatees).
  ⚠ **Les libelles et descriptions se modifient desormais dans /administration**
  (`short_description` ou `title` pour le libelle, `tooltip` pour la description revelee
  par le bouton « ? »), plus dans le code. Un extra qui doit apparaitre dans le tableau a
  besoin de `show_in_table=1` ET `function=1`.
  22 descriptions redigees le 2026-08-02 (script rejouable en prod :
  `.insights/seed-extra-descriptions-2026-08-02.php`, `--dry-run` / `--force`), toutes
  sourcees sur `.insights/vitrine-feature-truth-matrix.md`. **39/39 lignes couvertes.**
- **Une ligne dont aucun plan affiche ne beneficie est masquee** (elle n'alignerait que des
  tirets), et les categories devenues vides disparaissent avec elle - aucun titre orphelin.
  ⚠ `PricingController::comparisonCellIsEmpty()` est le miroir exact des branches de rendu
  du composant : faire evoluer l'un sans l'autre masquerait des lignes utiles.
- ⚠ **Les labels `YOUSIGN_*` ne designent PAS Yousign** : `YOUSIGN_LEVEL2` est **notre**
  signature avec verification d'identite du signataire (code a usage unique SMS/email) sur
  les documents sensibles type compromis - reelle, incluse au plan CRM. Ne jamais la nommer
  Youtrust/Yousign (matrice ligne 143). `YOUSIGN_CREDIT` vaut 0 partout et disparait tout
  seul par la regle de masquage.
  Le comparatif s'affiche aussi pour les **partenaires** (il montre leurs offres) et la
  colonne « Essai » disparait d'elle-meme quand l'essai a ete consomme.
- **GTM** : le layout pousse `session('gtm_event')` dans `dataLayer` (le core y depose
  login / sign_up). Sans ce push, les connexions ne remontaient pas.
- **Cartes tarifaires en flex centre**, jamais en grille a N colonnes : le nombre de cartes
  visibles varie cote client (frequence mensuelle/annuelle) et selon le profil (essai deja
  consomme, grille partenaire a 1 plan). Conteneur `flex flex-wrap justify-center`, largeur
  par carte via `$cardClass`. ⚠ `lg:basis-52` (et non `basis-72`) : c'est le seuil de passage
  a la ligne, 4 cartes a `basis-72` depassent de 8px et la 4e wrappe.
- **Modales d'auth natives** (`partials/login-modal.blade.php`, `partials/signup-modal.blade.php`,
  rendues seulement hors connexion) : le login est un vrai POST same-origin vers `/authenticate`,
  l'inscription poste `/api/v1/register` de la vitrine puis rejoue `/autologin` **localement**
  (le `redirect_url` du core pointe vers l'app : on n'en garde que le token).
- **1 ligne modifiee dans le core** : `AuthController::autologin` fait `redirect()->intended('/')`
  au lieu de `redirect('/')`. ⚠ La vitrine charge le core en PSR-4 **live**, mais
  `rs_infoparcelle_application` et `administration` utilisent `vendor/rootsud/core` :
  **tag core + `composer update` requis chez eux avant la prod** (changement neutre pour eux).

### Serveur MCP (lancement commercial 2026-08-20, page refondue 2026-09-10)

- **Le serveur est en production** : `https://mcp.infoparcelle.fr` repond, 14 outils,
  OAuth avec enregistrement dynamique (RFC 7591). Verifie de bout en bout par un **appel
  d'outil reel** avec un jeton OAuth, pas seulement par un `/health`.
- ⚠ **L'URL a communiquer est la RACINE `https://mcp.infoparcelle.fr`**, sans `/mcp`.
  Elle repond **401** a une requete non authentifiee : c'est normal, ce n'est pas une page.
  Les deux URL vivent dans `config/vitrine.php` (**`mcp_server_url`**, **`chatgpt_app_url`**),
  source unique de la page.
- **App ChatGPT officielle (2026-09-10)** : le MCP est **valide par OpenAI** et publie dans
  le repertoire des apps : `https://chatgpt.com/plugins/plugin_asdk_app_6a86e894071c8191839bf93ff4393053`.
  **Parcours RELEVE A L'ECRAN** (fiche ouverte dans le Chrome de l'utilisateur, compte deja
  connecte) : fiche « Infoparcelle - Cadastre et prix immobiliers », bouton **« Essayer dans le
  chat »**, menu « … » = Gerer / Deconnecter / Gerer pour l'espace de travail, entree
  **« Plugins »** dans la barre laterale de ChatGPT (libelle FR observe ; « Apps » ailleurs),
  appel par **`@Infoparcelle`** en tete de message, 3 prompts d'exemple publies sur la fiche
  (page de presentation 12 rue Oberkampf avec plan cote / logements F-G > 80 m² a Perpignan /
  prix reel des appartements vendus a Montpellier depuis 2024). Le libelle du bouton pour un
  compte NON connecte n'a pas ete observe (l'auteur ne deconnecte pas le compte de
  l'utilisateur) : la page dit « connectez votre compte » sans nommer ce bouton.
  ⚠ La fiche ChatGPT annonce « France metropolitaine » alors que les 5 DOM sont couverts
  (soumission Infoparcelle-MCP a corriger, hors vitrine).
- **Page `/mcp` refondue pour une campagne sponsorisee** : hero sombre (`hero_dark`) avec
  **demo de conversation batie sur des DONNEES REELLES** renvoyees par le serveur le
  2026-09-10 (geocoder_search + parcelle_get sur `66136000BY0124` : 306 m², maison 1968,
  2 niveaux, 223 m², DPE C 138 kWh, PLU UB 9-12 m, DVF 09/2017 319 900 €) - **adresse et
  numero de parcelle masques** (decision utilisateur : ne pas exposer le bien d'un
  particulier). Ne jamais « ameliorer » ces chiffres. Ordre du tutoriel : **ChatGPT en
  premier** (app officielle, rien a copier) → Claude (Parametres → Personnaliser →
  Connecteurs → Ajouter → Nom `Infoparcelle` + URL → Continuer → laisser les autres options
  telles quelles → valider → Connecter) → autres clients (JSON + doc). CTA selon la session :
  anonyme = `cta-signup` « Creer mon compte » + « Voir l'app dans ChatGPT » ; connecte =
  « Ouvrir dans ChatGPT » + « Voir mes credits API » (`/compte/api` de l'app).
- **Mesure de la campagne** : chaque CTA pousse `dataLayer` `{event:'mcp_cta', mcp_action}`
  (`chatgpt_open` + `mcp_place` hero/bandeau/tutoriel, `tab` + `mcp_tab`, `copy_url`,
  `copy_prompt` + `mcp_copy`). A brancher dans GTM comme conversions. Verifie : 4 pushes
  reels a l'E2E.
- ⚠ **Le bouton « Copier » a un repli `execCommand`** : `navigator.clipboard` n'existe
  qu'en contexte securise, il serait mort en http (mesure : `isSecureContext = false` en dev).
  Les prompts sont portes par un attribut **`data-prompt`** lu via `$el.dataset` : un `@js()`
  dans un `@click="…"` casserait l'attribut (guillemets doubles).
- ⚠ **Scopes** : les 14 outils tapent `apiscope:cadastre` (13) et `apiscope:person`
  (`occupants`), en refus par defaut. Une **cle API** sans ces scopes recoit un **403**.
  La voie **OAuth** n'est jamais scopee - d'ou le choix editorial de ne JAMAIS pousser la
  cle API pour brancher une IA.
- ⚠ **Credits (grille V14 du 2026-09-10, verifiee en base le 2026-09-14)** : application
  Basique 9,90 €/mois 2 000, Premium 39,90 5 000, CRM 79,90 10 000 ; plans API **Starter 25 €
  5 000 (324), Business 100 € 25 000 (389), Pro 200 € 50 000 (390)** = les seuls en
  libre-service en prod ; **Enterprise 391 / Scale 392 existent mais sont VERROUILLES et
  proposes au cas par cas : le tableau de `/mcp` les rend « Volumes superieurs : sur devis »
  SANS prix** (decision utilisateur 2026-09-14). Essai API 396 (14 j, 2 000, ligne gatee par
  `trial_enabled_api`), packs 393-395 (9,90 / 39,90 / 69,90 € HT, 12 mois) cites en note.
  Une analyse complete = 40 a 60 credits, Premium = une centaine d'analyses. Un appel coute 1 a 20 credits, une analyse
  complete d'adresse ~30. 🟥 **614 abonnements actifs sur 629 sont sur des plans
  legacy/partenaires SANS credits API** (`434 quota_unavailable`) : ne jamais annoncer le
  MCP comme « inclus dans tous les abonnements ». `trial_enabled` est **false** : la page
  ne parle pas d'essai.
- Icones ajoutees a `app.js` (les DEUX blocs) : `Copy`, `Plug`, **`ExternalLink`**.
- **Recette 2026-09-10** : 200, 0 erreur console, 142 icones rendues / 0 manquante, 3 JSON-LD
  valides (graph, BreadcrumbList, FAQPage 9 questions), onglets et 3 boutons Copier pilotes
  par script, mobile 390 px sans debordement (iframe de sonde), rendu connecte verifie par
  curl + autologin, 0 « — », 0 ligne de log, sitemap et 5 pages temoins en 200.

### Performance de chargement (2026-10-02)

Audit de production (`curl` + Chrome headless CDP à froid) puis corrections, **0 migration, 0 route, build Vite** :

- 🟥 **La police Inter répondait 404 en production** depuis la refonte : `public/fonts/inter/` est dans `public/`
  (gitignoré, jamais déployé). Le site s'affichait en police système, et le `<link rel=preload>` visait le 404.
  Désormais sous-ensemble **latin** (`resources/fonts/inter/*-latin.woff2`, 70 Ko au lieu de 352 Ko, généré par
  `pyftsubset` en gardant `cv02/cv03/cv04/cv11/tnum`), référencé en relatif dans `app.css` donc **hashé par Vite**
  (cache immuable d'un an) et préchargé via `\Illuminate\Support\Facades\Vite::asset(...)` (⚠ pas d'alias `Vite` dans ce projet → 500).
  `i[data-lucide] { font-style: normal }` : les `<i>` d'icônes, italiques par défaut avant leur remplacement par
  un SVG, faisaient télécharger l'italique (77 Ko) sur toutes les pages.
- 🟥 **Le JS partait non compressé** (105 Ko au lieu de 30) : Apache sert le `.js` en `text/javascript`, absent de
  la ligne `DEFLATE` de `public/.htaccess` (suivi par git, déployé par `git pull`).
- 🟥 **Contenu du hero invisible jusqu'à GSAP** : tous les `data-gsap` (titre H1 = LCP compris) étaient en
  `opacity:0` jusqu'au chargement différé de GSAP (115 Ko, `requestIdleCallback` jusqu'à 2 s), et pour toujours si
  le chunk échouait. `app.js` ne masque plus que les éléments **sous la ligne de flottaison** (`data-gsap-pending`)
  et révèle tout si l'import échoue.
- **Favicons** : `partials/favicons.blade.php` remplace `favicon()->render()` du core (≈ 20 balises vers
  public.rootsud.fr, dont 10 `apple-touch-startup-image`, tuiles Windows, manifeste Yandex et un manifeste nommé
  « rootsud »). 6 icônes locales `resources/images/icons/` → `/img/icons/`, manifeste « Infoparcelle ».
- **Plus aucun média sur public.rootsud.fr** (30/30 pages du sitemap) : logo de la navbar local
  (`resources/images/logo-infoparcelle.svg`, supprime aussi une requête SQL par page), **logos clients en WebP**
  (`resources/images/clients/`, 104 Ko → 24 Ko, hauteur 80 px = 2×, `width`/`height` en config, 2e série du
  défilement en `aria-hidden`). Seul le logo du JSON-LD reste sur `STORAGE_PUBLIC_URL` (lu par Google, jamais
  téléchargé par le navigateur) : donc **pas de `preconnect` vers public.rootsud.fr**, il n'aurait plus d'usage.
- **Préconnexions** : celles de GTM, Cookiebot et Clarity portaient `crossorigin` alors que ces scripts sont chargés
  sans CORS → connexion jamais réutilisée. Une seule `preconnect` (chargeur GTM, posée **avant** son script), le
  reste en `dns-prefetch`.
- **Captures** (`components/section/screenshot`) : `width`/`height` réels (`getimagesize`), variantes
  `*-800w.webp` + `srcset` (3,4 Mo → 1,4 Mo pour les 44 variantes), et `eager` (fetchpriority high, sans fondu)
  pour la capture du hero des pages piliers, qui était l'élément LCP en `loading="lazy"`. ⚠ Toute nouvelle capture
  doit recevoir sa variante 800 px (sinon `srcset` absent, rien ne casse).
- Mesuré à froid, accueil en mobile : 19 → 15 requêtes du domaine, plus aucune connexion à public.rootsud.fr,
  police Inter réellement affichée, 0 erreur de log, 30/30 pages en 200.

Déploiement : `git pull` + **`npm run build`** + `view:clear` + `config:clear`. Aucun package touché.

### Favicons : le fond vert venait de l'apple-touch-icon (2026-08-20)

- 🟥 **Symptome** : logo sur fond VERT dans les resultats Google et dans le connecteur
  Claude, qui affiche `google.com/s2/favicons?domain=infoparcelle.fr&sz=64`.
- **Cause** : toutes les icones sont correctement transparentes **sauf les 6 variantes
  d'`apple-touch-icon`** (`.png`, `-60x60`, `-76x76`, `-120x120`, `-152x152`, `-180x180`),
  qui portaient **68 a 71 % de #327500** et **zero pixel transparent**. Le service de Google retient la **plus grande** icone disponible : il
  servait donc celle-la. Mesure : le PNG 64x64 rendu par Google a exactement les memes
  couleurs dominantes (#327500 puis #3f6ad8).
- **Source** : `config/favicon-generator.php`, `ios.background_color` et
  `windows.background_color` valaient **#327500**, la couleur PAR DEFAUT du generateur,
  jamais personnalisee. ⚠ `administration` avait deja ete corrige (#136497), pas les deux
  projets Infoparcelle.
- **Livre** : les **2 valeurs iOS** (`ios.background_color`, `ios.startup_image.background_color`)
  passent a **#ffffff** dans la vitrine ET l'application, + **les 6 variantes**
  d'`apple-touch-icon` recomposees (voir plus bas), + `/favicon.ico` a la racine des deux sites.
- 🟥 **`windows.background_color` reste DELIBEREMENT a #136497.** Avec
  `picture_aspect: white_silhouette`, les `mstile-*` de production sont des silhouettes
  **blanches sur fond transparent**, et cette couleur devient le `<TileColor>` du
  `browserconfig.xml` : la passer a #ffffff donnerait une **tuile invisible**. Erreur
  commise puis rattrapee en inspectant les PNG reels ; un commentaire le rappelle dans
  les deux configs.
- Les **10 `apple-touch-startup-image-*`** (98-99 % de vert) sont refaits eux aussi.
  Methode : pour chaque fichier de production, mesure de la **boite englobante du logo**
  hors fond, puis recomposition a la meme position et a la meme taille sur fond blanc.
  ⚠ **3 portent le logo pivote a -90°** (`748x1024`, `1182x2208`, `1496x2048`, splash
  paysage). Le sens a ete **mesure, pas suppose** : IoU **0,960** a -90° contre **0,323**
  a +90°. Concordance finale avec la production : **IoU 0,89 a 0,98** sur les 10
  (les valeurs basses sont les petits formats, ou le logo fait 46x64 px et l'anticrenelage
  domine le masque).
- ⚠ **Le liseré clair autour des formes fait partie du DESSIN** (6,7 % de pixels blancs
  opaques dans la source 512 px), ce n'est pas de l'anticrenelage sur le vert. Sur fond
  blanc il disparait en peripherie - sans dommage, la silhouette etant portee par le bleu -
  mais il reste visible la ou il separe deux zones bleues (pin / cercle, toit de la maison),
  ce qui preserve la lisibilite du logo.
- ⚠ **Le blanc est un choix de CONTRASTE, pas d'esthetique** : le logo est bleu #3f6ad8
  a 84 %. Rapports mesures : blanc **4,92:1**, #227beb 1,20:1, #136497 1,29:1,
  #327500 1,16:1. **Sur un fond bleu - y compris la couleur de marque - le logo
  disparait.** Ne pas « harmoniser » ces valeurs avec la charte.
- ⚠ Ces icones ne peuvent PAS etre transparentes : iOS rend la transparence en noir sur
  l'ecran d'accueil. Un fond opaque est obligatoire.
- ⚠ **L'icone a ete RECOMPOSEE, pas repeinte** : remplacer le vert par du blanc aurait
  laisse des franges vertes sur les bords adoucis. Elle est reconstruite depuis une
  source transparente en 512 px, avec la marge exacte de l'originale (**12 px, 6,7 %**,
  mesuree sur la boite englobante du logo).
- ⚠ **Changer la config ne suffit pas** : les PNG deja publies sur `public.rootsud.fr`
  ne sont pas regeneres. Le fichier a ecraser est
  `<STORAGE_PUBLIC_PATH>/icones/infoparcelle/apple-touch-icon.png`.
- ⚠ **Google met son cache a jour lentement** : plusieurs jours a quelques semaines avant
  que l'icone change dans les resultats, donc dans Claude.
- **`/favicon.ico` renvoyait 404** sur www ET sur app (toutes les icones vivent sur
  `public.rootsud.fr`), alors que c'est le premier chemin teste par les crawlers.
  Corrige sur la vitrine : source commitee `resources/images/favicon.ico`, copiee vers
  `public/` au build par `vite-plugin-static-copy` (`dest: '..'`, les chemins etant
  relatifs a `public/build`). 16/32/48 px, transparente, generee depuis le 512 px : plus
  nette que l'ancienne (variance du laplacien a 32 px : 5949 contre 4308).
  **L'application a recu le meme `.ico`** (`public/favicon.ico`, verifie **200
  `image/vnd.microsoft.icon`**).
- 🟥 **Le `.gitignore` de l'application excluait `/public` en entier** : git **ne peut pas
  re-inclure un fichier situe dans un dossier exclu**, donc les **5 exceptions**
  `!/public/index.php`, `!/public/robots.txt`, `!/public/web.config`, `!/public/.htaccess`
  etaient **mortes depuis toujours** - ces fichiers ne survivent que parce qu'ils sont
  **suivis depuis l'origine**. ⚠ **`git check-ignore` seul ne le revele PAS** : il consulte
  l'index et repond « non ignore » pour un fichier deja suivi ; c'est **`--no-index`** qui
  montre que les 5 sont bel et bien ignores par les patterns. Corrige en **`/public/*`**,
  qui rend les exceptions reelles sans rien laisser fuir (verifie : `js/`, `css/`, `build/`,
  `images/`, `mix-manifest.json`, `sw.js` toujours ignores ; seul `favicon.ico` remonte).
- ⚠ **Jeu d'icones moderne jamais deploye** : `administration/storage/app/public/icones/infoparcelle/`
  contient un set date du **2025-04-13** (favicon.svg, web-app-manifest-192/512), entierement
  transparent, que la production n'a jamais recu. Meme logo a **0,3 %** pres (comparaison
  des silhouettes alpha) - c'est lui qui a servi de source. Basculer dessus changerait la
  liste des fichiers et le `site.webmanifest`, donc les balises `<link>` : decision a part.

### Sources de données : ce qui est réellement ingéré (rectifié le 2026-08-20)

- **18 sources** alimentent l'ETL (liste donnée par l'utilisateur, `core-city/src/Tasks/`) :
  DVF+ **Cerema**, cadastre **DGFiP PCI**, **BAN**, DPE **ADEME**, découpage **geo.api.gouv.fr**,
  PLU **IGN GeoPF**, RNU **APIcarto IGN**, fiscalité locale **data.economie.gouv.fr**,
  permis **SDES** (Sitadel), établissements publics **CIF/SIP/SIE**, **IGN Geoservices**,
  historique des communes **INSEE**, **RNI copropriétés**, personnes morales, décès INSEE,
  **DFI** (filiation), zones Pinel/ALUR, **BDNB**.
- 🟥 **`INPI` était cité comme source dans 5 fichiers de documentation : il n'apparaît NULLE
  PART dans le code** (0 occurrence, vérifié sur tous les cores). Retiré, glossaire compris.
- ⚠ **`Géorisques` est une COUCHE CARTOGRAPHIQUE (WMS), pas une donnée ingérée.**
  `OverlayerSeeder` configure **8 couches** pointant `georisques.gouv.fr/services` : cavités
  souterraines, mouvements de terrain, retrait-gonflement des argiles, anciens sites
  industriels, séismes historiques, gaz/hydrocarbures/produits chimiques, SIS, potentiel radon.
  → **Légitime sur la vitrine** (la carte les affiche vraiment), **faux dans la doc de l'API**
  (rien n'en est exposé par l'API ni le MCP). La doc annonçait même « Géorisques : Permis de
  construire », doublement faux : les permis viennent du **SDES**.
- 🟥 **La page /partenaires promettait « inondation, sismicité, argiles, SEVESO »** :
  **aucune couche inondation ni SEVESO n'existe** (vérifié sur les 8 couches configurées).
  Description réécrite sur la liste réelle.
- ⚠ **Le champ `risques` de l'API ne contient QUE `argile` et `radon`**, tous deux issus de
  la **BDNB** (`rel_batiment_groupe_parcelle.batiment_groupe_radon`), et non de Géorisques.
  Ne jamais promettre d'autres risques via l'API.
- ⚠ **DVF vient de Cerema (DVF+)**, version enrichie des valeurs foncières DGFiP : citer
  Cerema, pas seulement la DGFiP.

### Périmètre géographique : métropole **et** DOM (rectifié le 2026-08-20)

- 🟥 **Le marketing affirmait « France métropolitaine », c'était FAUX.** Mesuré en base :
  **1 053 787 parcelles outre-mer** (971 Guadeloupe 243 760 · 972 Martinique 221 208 ·
  973 Guyane 79 344 · 974 La Réunion 427 615 · 976 Mayotte 81 860). Total :
  **96 départements métropolitains + 5 DOM = 101**, **37 533 communes**.
- ⚠ **La documentation technique disait deja vrai** (`docs/resources/faq.md`,
  `docs/guides/dvf/search-transactions.md` : « France métropolitaine + DOM ») : seule la
  vitrine et la truth matrix se trompaient. Une FAQ publiée répondait même **« Non »** à
  la question « La couverture inclut-elle les DOM-TOM ? ».
- ⚠ **Dire « France métropolitaine et DOM », jamais « DOM-TOM »** : seuls 5 départements
  sont couverts. Saint-Pierre-et-Miquelon, Saint-Martin, Saint-Barthélemy, Wallis,
  Polynésie et Nouvelle-Calédonie ne le sont pas.
- 🟥 **Trou DVF jamais documenté avant, découvert par mesure** : **aucune transaction DVF
  en Moselle (57), Bas-Rhin (67), Haut-Rhin (68) ni à Mayotte (976)**. L'Alsace-Moselle
  relève du **livre foncier**, régime distinct qui n'alimente pas le jeu de données public.
  Vérifié par l'API : Strasbourg (67482), Metz (57463) et Mamoudzou (97611) renvoient
  **0 mutation**, alors que Guadeloupe, Martinique, Guyane et La Réunion en renvoient de
  récentes. ⚠ **Le cadastre, lui, est bien présent** en Alsace-Moselle (4,15 M parcelles) :
  un résultat DVF vide y signifie « donnée non publiée », **jamais** « aucune vente ».
  Documenté dans la doc technique et dans la skill MCP.
- Corrigé sur la vitrine en 5 endroits : FAQ `carte-cadastrale`, 2 FAQ de `config/vitrine.php`,
  sous-titre `/api`, modale d'inscription, page `/a-propos`.

### Documentation technique : `docs.infoparcelle.fr`, JAMAIS `/api/guide` (2026-08-20)

- 🟥 **`app.infoparcelle.fr/api/guide/...` sert la doc SANS AUCUN STYLE.** Le build
  Docusaurus deploye a ete fait avec `baseUrl = /` (pour le domaine dedie), donc son HTML
  reference `/assets/css/...` : sur `app.infoparcelle.fr` ce chemin est un **404** alors que
  le vrai fichier est sous `/api/guide/assets/css/...` (les deux mesures faites). La valeur
  par defaut de `docusaurus.config.js` (`/api/guide/`) ne correspond pas au build livre.
  **Ne jamais lier ce chemin depuis une page publique.**
- **La bonne URL est le domaine dedie** : **`https://docs.infoparcelle.fr`** en prod
  (verifie : `/`, `/api-reference`, `/sdks/mcp` en 200, CSS resolu), **`http://docs.devinfoparcelle.fr`**
  en dev (vhost Apache `devinfoparcelle.fr.conf:167`).
- Elle est portee par **`config('vitrine.documentation_url')`** (`DOCUMENTATION_URL` dans
  `.env`, defaut = prod). ⚠ Toujours passer par cette cle : trois liens la consommaient
  deja de facon incoherente avant.
- ⚠ **« Documentation API » pointait vers `/api`**, c'est-a-dire la page **marketing** dont
  l'utilisateur venait - dans le CTA `cta-banner` variante `developer` **et** dans le footer.
  Corrige : le footer distingue desormais « API REST » (page marketing) et « Documentation
  technique » (doc, lien externe) ; `cta-banner` pointe la doc.
- Le footer supporte `'external' => true` (ajoute `target="_blank" rel="noopener"`).
  ⚠ `url()` laisse passer une URL absolue telle quelle (verifie), donc une entree de footer
  peut porter une URL complete.

### Redirections des anciennes URLs (audit 2026-08-02, avant bascule prod)

- Le module `/redirection` de **/administration** ecrit dans `core.redirections` (+ pivot
  `core.redirection_site`). `RedirectionMiddleware` est dans le `$middleware` **global** du
  `Rootsud\Core\Http\Kernel` - et `bootstrap/app.php` de la vitrine bind bien ce Kernel-la
  (le `App\Http\Kernel` local est ignore), donc les redirections s'appliquent ici.
- ⚠ **Une redirection n'est pas seulement un confort SEO** : les pages de l'ancien site
  vivent toujours dans `core.project_site_pages` (site_id 1). Sans redirection, l'URL n'est
  pas un 404 - elle est servie par la **route CMS du core**, donc avec du contenu GrapeJS
  perime. Le middleware etant global, il intercepte AVANT le routage : c'est lui qui protege.
  (Avant le correctif `vendor.blade.php`, ces pages tombaient en 500 ; elles s'affichent
  desormais, ce qui rend la couverture des redirections d'autant plus necessaire.)
- **Couverture verifiee : 18/18 URLs de production** (liste `project_site_pages` prod) :
  8 conservees a l'identique (`/`, `/cgu`, `/cgv`, `/cookies`, `/confidentialite`,
  `/contact`, `/pourquoi-infoparcelle`, `/tarifs`) + 10 redirigees en 301.
- Script **idempotent et rejouable en prod** : `.insights/seed-redirections-vitrine-2026-08-02.php`
  (option `--dry-run`). Il ajoute les 5 pages oubliees, toutes **vivantes en prod** au moment
  de l'audit : `/saint-valentin`, `/noel`, `/black-friday` (landings promo -> `/tarifs`),
  `/presentation_keller_williams` (-> `/partenaires`) et `/nouveauté-interface-recherche-quartier`
  (-> `/carte-cadastrale`).
- **Sitemap servi a DEUX URLs** : `/sitemap.xml` (canonique, declaree dans `robots.txt`) et
  `/storage/sitemap.xml` (**URL historique** que le CMS de /administration generait, connue
  de la Search Console). Meme contenu dynamique, une seule source de verite.
- 🟥 **Le reglage « sitemap » du site doit rester DESACTIVE dans /administration**
  (`project_site_setups.sitemap`). Tant qu'il vaut `SITEMAP_ENABLED`, `HasSitemap::generate_sitemap()`
  ecrit un **fichier physique** `public/storage/sitemap.xml` a partir des pages du CMS -
  qu'Apache sert alors en priorite, masquant totalement la route Laravel. Symptome constate
  en prod le 2026-08-02 : `/storage/sitemap.xml` renvoyait 7 URLs (l'ancien CMS, fichier
  date du 16/02) alors que `/sitemap.xml` en renvoyait 30. **Non reproductible en dev**, ou
  le fichier n'a jamais existe. Diagnostic en 2 secondes : `curl -I` sur l'URL - un
  `x-powered-by: PHP` = la route repond ; un `last-modified` + `etag` sans `x-powered-by` =
  Apache sert un fichier statique. Le desactiver declenche `delete_sitemap()`, qui supprime
  le fichier et rend la main a la route.
- ⚠ Meme cause, meme effet : ne PAS lancer `php artisan storage:link` sur la vitrine.
- ⚠ `RedirectionMiddleware` ignore `storage/*`, donc le module `/redirection` ne peut pas
  couvrir cette URL - la route est obligatoire.
- ⚠ La liste du `SitemapController` est **ecrite a la main** : toute nouvelle page publique
  doit y etre ajoutee (`/avis-clients` y manquait depuis sa creation, corrige le 2026-08-02).
- **`public/robots.txt` bloque les pages techniques du core** (`/login`, `/register`,
  `/forgot`, `/compte`, `/paiement*`, `/trial/`, `/autologin`, `/oauth/`, `/tarifs/connexion`).
  Consequence du tunnel same-origin : ces pages sont desormais servies par le domaine
  vitrine ET liees depuis des pages publiques (les replis sans JavaScript des CTA en
  exposent 16 sur la seule page `/tarifs`, dont les redirections OAuth `/login/{provider}`).
  Avant, ces liens pointaient vers l'application, donc un autre domaine. ⚠ `Disallow: /tarifs/connexion`
  ne bloque PAS `/tarifs` (prefixe plus long) - verifie.
- ⚠ **URL accentuee** : `Request::path()` ne decode PAS les `%XX`. Derriere Apache, c'est la
  variante **percent-encodee** (`/nouveaut%C3%A9-...`) qui matche - un navigateur encode
  toujours les caracteres non-ASCII. La variante a accent brut est conservee en base (c'est
  elle qui figure dans `project_site_pages` et qu'un admin recopiera naturellement) mais
  elle reste inerte : ne pas la retirer en croyant qu'elle suffit.

### Essai gratuit : un seul interrupteur, `config('vitrine.trial_enabled')`

- Il pilote deja les CTA (navbar, `cta-signup`, `cta-banner`, `hero-pillar`, modale
  d'inscription), le sous-titre et les cartes de `/tarifs`, le sitemap, et la **redirection
  301 de `/essai-gratuit` vers `/tarifs`** (`PageController::freeTrial`).
- **2026-08-27** : le **pied de page** y obeit enfin. Il restait le seul endroit a annoncer
  « Essai gratuit » avec l'essai desactive - et son lien menait a la redirection 301, donc
  a la page des tarifs.
- **Mecanisme** : cle **`'requires_trial' => true`** sur le lien dans
  `config/vitrine.php → footer_navigation`, filtree dans `partials/footer.blade.php`
  **avant** le rendu de la colonne (une colonne videe n'est pas rendue - pas de titre
  orphelin). Declaratif : un futur lien lie a l'essai se masque en ajoutant la cle.
- ⚠ **`/essai-gratuit` reste une route valide** (redirigee), donc rien ne casse pour un
  visiteur arrivant d'un ancien lien ou d'un e-mail. Ne pas supprimer la route.
- Verifie : A/B (code d'origine -> **1** lien rendu ; corrige -> **0**), contre-epreuve
  `trial_enabled = true` -> le lien **revient** et les 5 colonnes restent, **29/29 URLs du
  sitemap en 200**, 0 residu `essai-gratuit` dans le HTML, 0 ligne de log.

### Contenu réel de l'essai : promesses corrigées (2026-10-26)

- 🟥 **La vitrine promettait dans l'essai ce qu'il ne contient pas en production** : `/essai-gratuit` (CRM et
  portail client), la FAQ de l'essai (`config/vitrine.php`, opportunités et analyse de marché), et les FAQ de 4
  piliers (« inclus dans tous les plans dès l'essai gratuit » pour le CRM, l'espace client, les widgets et les avis
  clients). Source : le tableau de `www.infoparcelle.fr/tarifs`, rendu depuis la base de production. ⚠ **La base de
  dev diffère** (plan 286 avec opportunités, analyse de marché et 1 000 crédits API) : ne jamais rédiger l'essai
  d'après la dev. Matrice à jour : section « MAJ 2026-10-26 » de `.insights/vitrine-feature-truth-matrix.md`.
- **Hero des piliers** : paramètre `plan_note` de `components/section/hero-pillar` (plan qui inclut le module),
  posé sur crm-immobilier, portail-client, widgets, avis-clients, documents-et-conformite et gestion-agence. Sans
  lui, « 7 jours d'essai gratuit » sous le bouton laissait croire que le module s'essaie gratuitement.
- **Comparatif** : l'essai est enregistré en fréquence annuelle, ses quotas étaient divisés par 12 (100 fiches
  affichées « 8 », 1 export « 0 »). `comparison-table` ne divise plus pour un plan `trial` et affiche
  « pendant 7 jours » sous l'en-tête.
- Déploiement : `git pull` + `view:clear` + `config:clear`. Pas de build (aucune classe CSS ni icône nouvelle).

### Mise en avant des promotions (2026-09-17)

- **Avant** : une pastille « -40 % » en coin de carte sur `/tarifs`, prix deja remise SANS prix
  de reference barre, rien ailleurs sur le site.
- **Source unique** : `App\Services\ActivePromotion`. `planOffer()` calcule le prix d'UN plan
  (catalogue, remise, economie, echeance) et sert **a la fois** la carte tarifaire et la barre
  d'annonce : les deux ne peuvent pas diverger. `current()` rend la meilleure promotion
  **reellement applicable au visiteur courant**, memoisee par requete ; `memberHint()` dit s'il
  faut inviter un anonyme a se connecter. `App\Services\PricingCatalog` porte la selection des
  plans (deplacee de `PricingController`, comportement inchange : HTML de `/tarifs` identique).
- **Eligibilite = core uniquement** (`getPriceInfoPromotionOffer` + `Billing_plan::getPromotions`) :
  visibilite, inclusions/exclusions de groupe, dates, quotas. Aucune logique parallele.
- **Exposition** : `View::composer` dans `AppServiceProvider` sur une liste EXPLICITE de vues
  (`layouts.vitrine`, `partials.promo-bar`, `partials.cta-banner`, `components.section.hero-pillar`,
  `components.pricing-promo`, `pages.pricing`). ⚠ Un composer sur le seul layout ne suffit pas :
  Blade rend les sections de la page AVANT le layout. Toute nouvelle vue qui lit `$activePromo`
  doit etre ajoutee a cette liste.
- **Ce qui s'affiche** : barre d'annonce sur toutes les pages (`partials/promo-bar`, composant
  Alpine `promoBar`), encart en tete de `/tarifs` (`components/pricing-promo`), carte refaite
  (ruban « Offre en cours · -X % » qui se COMBINE avec « Le plus populaire », prix catalogue
  barre, « Vous economisez X € », prix annuel barre), pastille promo dans `cta-banner` et
  `hero-pillar`. Les cartes portent `id="plan-{id}"` (cible des liens « Voir l'offre »).
- 🟥 **Zero hallucination** : compte a rebours et « jusqu'au JJ/MM/AAAA inclus » UNIQUEMENT si
  l'option de la promotion porte `end = 1` (fin = fin du jour `end_at`, comme `verifyVisibility`) ;
  prix barre = prix catalogue reel ; pas de fausse rarete (`quota_limit` non exploite).
- 🟥 **AUCUNE promesse de duree n'est derivee de `frequency_limit`.** Au renouvellement le core
  appelle `getPriceInfo($isRefresh)` avec `$subscriber = null` : la promotion est REEVALUEE selon
  sa visibilite du moment. Une promotion suspendue ou passee plus tard ne s'applique donc plus
  aux abonnes : ecrire « sur toute la duree de votre abonnement » serait faux. Seule une note
  editoriale assumee peut etre affichee : `config('vitrine.promotions.{id}.duration_note')`.
- 🟥 **Promotion sur PLUSIEURS plans (retour utilisateur du 2026-09-17, Premium ajoute a la promo 121)** :
  l'encart affichait « Plan Premium, CRM : 23,94 € HT / mois », qui se lit comme le prix des DEUX
  plans. `current()` expose desormais `plan_prices` (un prix par offre commerciale, reference
  mensuelle, du moins cher au plus cher - l'ancre du lien est le premier) et `plans_label`
  (« le plan CRM » / « les plans Premium et CRM ») : ne jamais accoler un prix unique a une liste de
  plans, ni ecrire « le plan » en dur. Libelle et accroche de `config('vitrine.promotions')` ne
  doivent pas nommer un plan (les plans concernes sont derives de la base) : « Offre de lancement ».
- **Grille ouverte sur le MENSUEL, avec passerelle vers l'annuel (decision 2026-09-17)** : mesure en
  base, la nouvelle grille compte 13 abonnes mensuels et 0 annuel (l'annuel ne se vendait que sur les
  anciens plans a 199/299 € d'un coup) ; ouvrir sur l'annuel afficherait « 39,50 € / mois » pour
  474 € a payer tout de suite, et contredirait « sans engagement ». Chaque carte mensuelle porte
  donc un bouton `data-yearly-nudge` (« ou X € HT / mois en annuel, soit Y € d'economie par an »)
  qui appelle `setFrequency('YEAR')`. Jumeau annuel = meme `name_public` + `quantity_min`, parmi
  les `$plans` de la page ; economie = 12 x prix mensuel CATALOGUE - prix annuel du visiteur
  (Premium en promo : 478,80 - 239,40 = 239,40 €), et la base de comparaison est ECRITE dans la
  ligne (« par rapport au mensuel a 39,90 € ») car le prix affiche au-dessus peut etre remise.
  Aucune mention de duree de la remise (retiree sur demande). Plans API exclus.
- **Carte ANNUELLE : economie mesuree contre 12 mois du meme plan en mensuel catalogue** (retour
  utilisateur du 2026-09-17). Le prix annuel catalogue contient deja les mois offerts : « annuel
  catalogue - annuel remise » affichait 159,60 € pour le Premium alors que la carte mensuelle
  annoncait 239,40 €. Le bloc affiche le total + « par rapport au mensuel a 39,90 € » + le detail
  quand les deux composantes existent (« 2 mois offerts 79,80 € » / « Offre -40 % 159,60 € »).
  « N mois offerts » seulement si le compte tombe juste, sinon « Tarif annuel » (CRM : 790 € contre
  958,80 € = 2,11 mois). Une carte annuelle SANS promotion affiche aussi son economie (bloc bleu).
  Jumeau mensuel = meme `name_public` + `quantity_min` parmi les `$plans` de la page ; sans jumeau
  (grille partenaire a un seul plan), repli sur « catalogue - remise ». **Prix barre de la carte
  annuelle = prix du MENSUEL (39,90 €)**, et la pastille a cote donne l'ecart TOTAL entre les deux
  prix affiches (Premium -50 %, CRM -51 %, Basique sans promo -17 %) : jamais « -40 % » a cote de
  39,90 -> 19,95. Meme reference dans la pastille « Facturation annuelle » : 12 x mensuel barre
  (~~478,80 €~~ 239,40 € HT/an), jamais l'annuel catalogue (399 €). Le ruban continue de nommer la promotion (« Offre -40 % ») et le detail du bloc
  d'economie fait le lien entre les deux.
- 🟥 **Carte ALLEGEE (2026-09-18, retour sur captures de production) - fait foi sur les puces
  ci-dessus pour tout ce qui concerne le CONTENU de la carte.** La carte portait jusqu'a 11
  chiffres (annuel) et disait trois fois la meme chose ; le bouton descendait de 140 px sur les
  plans en promotion. Regle : le prix a payer, UNE economie, l'echeance. (1) Pastille annuelle =
  « Facture 279,30 € HT par an », SANS montant barre (12 x mensuel barre repetait le bloc
  d'economie). (2) Bloc d'economie sur 2 lignes : « Economisez 199,50 € / an » (`whitespace-nowrap`,
  sans « HT » : il est sous le prix) puis, en petit, la composition SANS montants et l'echeance
  (« 2 mois offerts + offre -30 % · jusqu'au 18/09 », annee affichee seulement si elle differe de
  l'annee courante) - c'est cette ligne qui relie la pastille (-42 %) au ruban (-30 %). (3) Prix
  barre d'une carte annuelle suivi du libelle visible « en mensuel » (`aria-hidden`, le lecteur
  d'ecran a deja « Prix en mensuel : »). (4) Passerelle vers l'annuel = UNE ligne sous le bouton
  (« ou 23,28 € HT / mois en annuel → »), sans second chiffre : le bouton remonte vers le prix et
  le « par an par rapport » qui se lisait comme un begaiement disparait. Densite mesuree par le
  harness, ruban compris : 5 chiffres en mensuel, 7 en annuel. Ne pas re-enrichir la carte sans
  retirer autre chose.
- **Bascule mensuel / annuel sans saut de page (2026-09-17)** : les cartes n'ont PLUS de
  `x-transition`. Avec lui, les cartes sortantes restaient dans le flux pendant leur fondu alors
  que les entrantes arrivaient deja : 7 cartes un instant, retour a la ligne, page qui saute.
  Desormais `x-show` seul (sortie instantanee) + classe CSS `plan-card-in` (`app.css`) : une
  animation CSS rejoue d'elle-meme quand l'element repasse de `display:none` a visible, avec une
  cascade `--plan-delay` (70 ms par rang, rang calcule dans `pages/pricing` parmi les cartes
  visibles ENSEMBLE). Coupee sous `prefers-reduced-motion`. Les cartes annuelles portent `x-cloak`
  (plus d'apparition avant l'initialisation d'Alpine) - ⚠ jamais sur les plans API, qui vivent
  hors composant Alpine : l'attribut n'y serait jamais retire. Selecteur : pastille blanche
  glissante (`Alpine.data('frequencyThumb')`, `resources/js/alpine/frequency-thumb.js`, imbrique
  dans `pricingToggle` dont il lit `frequency`), replacee au resize et apres chargement des polices.
- 🟥 **Remise « a vie » : vrai seulement tant que la promotion reste ACTIVE et SANS date de fin
  depassee.** Mesure le 2026-09-17 sur un abonne CRM reel ayant la promo 121 dans son historique
  (`getPriceInfo(..., $isRefresh = true)` = `PayplugTrait::refreshPayment`, transaction annulee) :
  sans date de fin ou date future -> remise appliquee au renouvellement ; date de fin DEPASSEE ->
  renouvellement au plein tarif (`hasPromotionOffer = false`). Une promotion « offre » avec date
  de fin ne prolonge donc PAS la remise des abonnes apres cette date. Non corrige (core, decision
  produit) : ne rien promettre sur la duree tant que ce n'est pas tranche.
- **Promo « Membre » + visiteur anonyme** : rien de chiffre (on ignore s'il sera eligible), mais un
  encart « Des offres reservees aux membres sont en cours » en tete de `/tarifs` + une ligne dans
  le bandeau partenaire. Jamais pour une promotion a liste d'INCLUSION nominative (`lock_type` 1).
- **Textes** : `config/vitrine.php` → `promo_bar_enabled` + `promotions[id]` (`label`, `tagline`,
  `duration_note`). Le `code` en base est technique : sans entree, libelle « Offre en cours ».
- **Cache** : 5 min pour un anonyme (`vitrine.active_promotion.{site}`), JAMAIS pour un connecte ;
  `/tarifs` resout sans cache a partir de ses propres plans (`prime()`). Apres un changement de
  promotion dans /administration, la barre des autres pages suit sous 5 minutes (`cache:clear`
  pour l'immediat).
- **Navbar** : elle est `fixed top-0` ; la barre vit dans le flux au-dessus. Un script inline du
  layout cale `nav.style.top` sur le bas de la barre (36 px → 0 au defilement), sans
  `requestAnimationFrame` (suspendu en onglet cache) et en retirant `top` de la `transition-all`
  de la navbar. Fermeture memorisee en `localStorage` (`ip_promo_dismissed` = id de la promo :
  une NOUVELLE promotion reapparait), retiree avant le premier rendu par un script inline.
- ⚠ Valeurs passees au composant Alpine par `data-promo-id` / `data-ends-at`, jamais par un
  litteral dans `x-data="…"` (meme piege que `@js()` dans un `@click`).
- **Mesure** : `dataLayer` `{event:'promo_bar', promo_action: view|click|dismiss, promo_id}`.
- ⚠ **Envoi après le consentement (2026-11-04)** : `promo_bar` « view » et `tool_view`, poussés au chargement,
  arrivaient AVANT la mise à jour du consentement par Cookiebot (même pour un visiteur ayant déjà accepté) : les
  balises GA4 exigent `analytics_storage` et ne réessaient pas, l'événement était perdu (0 `tool_view` en production
  depuis la mise en ligne, `tool_switch` après un clic bien reçu). Tous les envois de `tool-form.js` et
  `promo-bar.js` passent par `resources/js/consent-ready.js` (`pushAfterConsent`) : file jusqu'à
  `cookie_consent_update` dans le dataLayer, ordre conservé, envoi direct sans GTM sur la page, repli à 10 s sans
  Cookiebot, abandon à 30 min. Banc `.insights/consent-ready-2026-11-04/harness.mjs` 12/12 ; en dev, `tool_view`
  (outil et accueil) et `promo_bar` partent vers sgtm (503 : le serveur de mesure refuse le domaine de dev, comme
  pour `page_view`). Tout nouvel événement poussé au chargement doit passer par `pushAfterConsent`.
- 🟥 **Conteneur serveur (2026-11-04)** : `GTM-NMRBQS3J` (sgtm.infoparcelle.fr) ne transmet à GA4 que les événements
  du déclencheur 28 (« Infoparcelle - Custom Events », balise 5) en plus de `page_view`. Il ne retenait que
  `login|sign_up|search|view_item|begin_checkout` : les événements des outils, de la barre de promotion, de `/mcp`, de
  la carte commune et `app_download` n'avaient JAMAIS atteint GA4. Étendu (version 17, publiée le 2026-11-04, `tool_view` vu dans le temps réel de GA4). Tout nouvel
  événement GA4 doit être ajouté à la fois au conteneur web ET à ce déclencheur.
- **Verifie** : harness `.insights/active-promotion-2026-09-17.php` **28/28** dans une transaction
  ANNULEE (Public, echeance reelle, promo echue, Membre + exclusion du groupe 97 : anonyme rien +
  invitation / user 2 promo / user 14401 rien, inclusion nominative, carte populaire + promo,
  79,90 → 47,94 et 790 → 474, plan sans promo inchange) ; sans promotion eligible le HTML de `/`
  et d'une page pilier est identique a l'origine (espaces pres) ; navigateur : barre, encart,
  cartes, hero sombre `/mcp`, compte a rebours avec une date fabriquee puis restauree, fermeture
  memorisee, 3 evenements `dataLayer`, mobile 390 px sans debordement (barre 56 px), 30/30 URLs
  du sitemap en 200, 0 message console, 0 ligne de log.
- **Deploiement** : `git pull` + `npm run build` + `view:clear` + `config:clear` + `cache:clear`.
  Aucun tag, aucune migration.

### Points d'attention
- **Zoom des champs sur iPhone (2026-11-05)** : fin de `resources/css/app.css`, texte des champs à 16 px sur iOS seulement (fenêtres d'inscription et de connexion à 14 px) ; même règle que le core. ⚠ Banc navigateur en http : Chrome passe les adresses en https (`HttpsUpgrades`) et la vitrine tombe sur l'application, lancer Chrome avec `--disable-features=HttpsUpgrades`.
- **Bloc remonté sous `components/page-header` (`-mt-*`) : `relative z-10` obligatoire (2026-11-03).** L'en-tête est une section `relative` : le navigateur la dessine après les blocs non positionnés qui la suivent, donc son fond recouvre la partie remontée (haut du bouton Google Play de `/application-mobile` et des badges de `/api` coupés).
- **🟥 `components/pricing-card.blade.php` évaluait la promotion avec `$user = null` même pour un connecté (corrigé 2026-09-08)** : une promotion en visibilité « Membre » n'apparaissait JAMAIS sur `/tarifs`, même à un membre éligible (seules les promos Public passaient). La carte passe désormais `auth()->user()` + `$userGroupList` / `$is_subscribed` du contrôleur à `getPriceInfoPromotionOffer` ; le prix catalogue reste calculé sans utilisateur. Vérifié avec l'ANCIEN core dans le vendor (1.0.208 = prod) : membre hors groupe exclu → 2 cartes à -40 %, membre du groupe exclu → 0, anonyme → 0.
- **`vitrine.is_tarifs_auth` (2026-09-08)** : `VITRINE_TARIFS_AUTH=true` dans `.env` pose le middleware `auth` sur `GET /tarifs` (anonyme -> 302 `/login`, retour sur `/tarifs` apres connexion par `redirect()->intended` du core ; `/tarifs/intention` et `/tarifs/connexion` restent publics ; sitemap inchange). Sert de contournement tant que le correctif core `HasVisibility` (exclusions ignorees en Public) n'est pas deploye : promotion en « Membre + Exclure », visible des connectes. ⚠ Lu a l'enregistrement des routes : `php artisan route:clear` apres changement (obligatoire si `route:cache`). Defaut `false`.
- **Site monolingue : aucun prefixe de langue dans les URLs (2026-08-07)** - `config/app.php` declare **`'supported_locales' => []`**, lu par `Rootsud\Core\Providers\Traits\HasI18n`. Consequence : **`/fr/tarifs`, `/en/tarifs`, `/nl` renvoient 404** ; seule la forme sans prefixe existe (`/tarifs`). La vitrine etant integralement redigee en francais, la gestion i18n du core n'a plus lieu de s'appliquer ici. ⚠ **Un tableau vide n'est PAS la meme chose que retirer la cle** : la cle absente reactiverait `fr` / `en` / `nl`. ⚠ **Decision propre a ce site** : les applications et `administration` gardent leur i18n (verifie : `/fr` y repond toujours). Contexte : jusqu'au 2026-08-07, **tout** segment de 2 caracteres etait pris pour un code langue par le core - `/zz/tarifs` repondait 200 (miroir indexable) et `/",` faisait tomber le boot en **500** (voir `core/CLAUDE.md`). **Aucune URL reelle perdue** : 0 page CMS et 0 redirection a prefixe langue en base, et le sitemap n'a jamais contenu de prefixe. Verifie : **30/30 URLs du sitemap** et **32/33 routes GET** en 200 (`/tarifs/connexion` = 302 attendu), `lang="fr"`, 0 lien prefixe dans le DOM, 0 message console, 0 ligne de log.
- **Zero hallucination** : tout claim publie doit etre source dans `/home/julio/www/.insights/vitrine-feature-truth-matrix.md`. Interdits : DOM-TOM, 80+ filtres, 4.8/5, 500+ (→ 1000+), favoris, alertes baisse de prix, outil de mesure. ⚠ **MCP « disponible » n'est PLUS interdit depuis le 2026-08-20** (serveur en production, verifie par un appel d'outil reel) - mais il reste interdit de decrire un **chemin de menu ChatGPT non observe a l'ecran**, et de laisser croire qu'un client OAuth doit etre cree
- Chiffres verrouilles : 94 M parcelles, 36 000 communes, 1000+ professionnels, 50+ filtres, 12 onglets, 60+ endpoints API, France metropolitaine
- `config/vitrine.php` etait NON TRACKE en git et a ete reecrit — l'ancien (1482 l.) est perdu mais documente dans `.memory/vitrine-content.md` (ancienne version) ; backup complet pre-refonte : `/home/julio/www/.insights/vitrine-backup/vitrine-pre-refonte-2026-06-12.tar.gz`
- Logos clients : URLs reelles recuperees depuis `.audit/snapshots/home-*.txt` (format `public.rootsud.fr/{id}/responsive-images/...`)
- Icones Lucide tree-shakees : toute nouvelle icone (y compris via `'icon' => '...'` dynamique en config/views) DOIT etre ajoutee aux DEUX blocs de `resources/js/app.js` (import + usedIcons) puis `npm run build`
- Classes Tailwind dynamiques (`grid-cols-{{ $n }}`) ne compilent pas → mapper vers des classes litterales
- **PIEGE `$title` herite (fixe 2026-06-12)** : `@include` herite du scope parent, et `vitrineData()` expose `$title` (= titre du site core, « Application ») → tout composant avec `$title ?? 'defaut'` affichait « Application » au lieu du defaut. Corrige dans `section/faq` (`$faq_title`) et `section/cross-links` (`$block_title`) — pour tout NOUVEAU composant, prefixer les params optionnels (jamais `$title`/`$subtitle` nus avec un defaut)
- `@section('hero_dark', true)` AVANT `@section('content')` ; signup via `$dispatch('open-signup')`, connexion via `$dispatch('open-login')`, choix de plan via `$dispatch('choose-plan', { plan: id })` ; `rtrim()` trailing slash dans vitrineData()
- **`billing_plan_options` a des DOUBLONS et `Billing_plan::option()` est un `morphOne` sans `orderBy`** : le plan 245 porte une ligne `lock_visibility=0` ET deux `lock_visibility=1` (idem 259, 266, 277, 279). L'election depend de l'ordre physique PostgreSQL, donc ne jamais conclure « plan public » depuis `option->lock_visibility` seul - la derivation exige l'ABSENCE de toute ligne verrouillee (`whereDoesntHave`), sinon un doublon legacy a 39,90 reapparait a cote du Premium. Controle rejouable : `.insights/verify-public-plans-derivation-2026-08-02.php`
- Les vues heritees du core rendues sur la vitrine passent par `resources/views/vendor.blade.php` : il doit charger l'asset **Vite**, pas `mix()` (c'etait la cause du 500 « Mix manifest not found » sur `/compte`)
- `comparison-table` supporte les lignes de groupe `['group' => 'Titre']`
- `public/` appartient a www-data → creer les dossiers via `docker exec`, copier les assets via `docker cp`
- Le slot `$screenshot` des composants section verifie `file_exists(public_path(...))` → fallback mockup CSS automatique si la capture manque
- **Composants Alpine vestiges** : `featureFilter` et `featureTabs` sont toujours enregistres dans `app.js` mais plus consommes par aucune vue depuis la refonte (les pages config-driven qu'ils pilotaient ont ete supprimees) — ne pas s'appuyer dessus pour une nouvelle page
- **115 icones Lucide** importees explicitement, dans les DEUX blocs d'`app.js` (import + `usedIcons`)

### Fichiers memory detailles
- [architecture.md](.memory/architecture.md)
- [controllers.md](.memory/controllers.md)
- [views.md](.memory/views.md)
- [frontend.md](.memory/frontend.md)
- [vitrine-content.md](.memory/vitrine-content.md)
- [marketing.md](.memory/marketing.md)

### Grille API V14 sur /tarifs, /api et /mcp (2026-09-10)

- **`config('vitrine.trial_enabled_api')`** (renommee le 2026-09-14 depuis `api_trial_enabled`, valeur en dur comme `trial_enabled`, consommee aussi par la ligne « Essai API » de /api et /mcp, le CTA developpeur et les deux FAQ) : interrupteur **distinct** de `trial_enabled` (application, a `false`). `PricingController::trialEnabled($isApi)` le lit pour `publicPlans` et `userPlans` : l'essai API (plan `is_api` + `trial`, 14 jours, 2 000 credits) apparait dans la section « Plans API » de `/tarifs` alors que l'essai de l'application reste masque. `pricing.blade.php` passe `isTrial` aux cartes API ; `pricing-card.blade.php` derive `$trialEnabled`/`$trialDuration` du plan (« pendant 14 jours » au lieu du « 7 jours » code en dur) et affiche « Credits API inclus, tous les endpoints » pour un plan API.
- **`rememberIntent`** : pour un essai **API** l'intention memorisee est `/trial/{id}` (l'inscription ne cree que l'essai de l'application) ; l'essai application garde sa cible applicative.
- `/tarifs` liste desormais 4 cartes API (Essai API, Starter, Business, Pro) - les plans Enterprise/Scale sont verrouilles « Membre » donc absents (sur devis). Verifie en 390 px par iframe : 4 cartes empilees a 339 px, 0 debordement ; en session connectee, l'essai deja pris disparait (`getUserPlanTrial` scopee `is_api`).
- `/api` : tableau « Credits API par plan » complete (essai, Starter, Business, Pro, Enterprise et Scale sur devis) + paragraphe packs 9,90 / 39,90 / 69,90 € et bareme 1 / 2 / 10 / 20. `/mcp` : « 40 a 60 credits » par analyse et « une centaine d'analyses » en Premium (les « trentaine » et « 150 » etaient faux : une analyse reelle coute geocodage 2 + fiche 20 + DVF 10 + DPE 10 = 42, jusqu'a 62 avec PLU et permis) ; FAQ citant Business (25 000) et Pro (50 000).
- Build Vite rejoue (`npm run build`) + `view:clear` + `config:clear`. ⚠ Le vendor `core` de la vitrine (1.0.208) a recu les copies des fichiers du core modifies (diff vide entre 1.0.208 et 1.0.210 sur ces fichiers) : tag core + `composer update` au deploiement.

### Carte tarifaire : « Abonnement actuel » lu sur `is_current` (2026-10-13)

- `components/pricing-card.blade.php` reconnaissait le forfait en cours en comparant le LIBELLÉ du bouton rendu par le core ; il lit désormais `is_current` de `HasPlan::getSubscriptionLink` (repli sur le libellé tant que le core n'est pas à jour). Rendu de `/tarifs` identique, anonyme et connecté.
- Déploiement : `git pull` + `view:clear`, après le tag du core.

### Audit du tunnel, bloc A : promesses de l'essai et boutons (2026-10-26)

Audit complet : `/home/julio/www/.insights/audit-tunnel-essai-conversion-2026-10-26.md` (blocs A à G, traités un par un).
- Modale d'inscription : « CRM immobilier inclus » remplacé par le contenu réel de l'essai.
- `plan_note` sur les 6 pages métiers et sur estimation ; `cta-banner` accepte `cta_plan_note` (forfait écrit dans le
  sous-titre du bandeau `trial`), `cta_single` (un seul bouton), `cta_plan` (plan mémorisé avant l'inscription) et
  `cta_button` (libellé du bouton principal). Variante `demo` SUPPRIMÉE : l'équipe ne fait pas de démonstration,
  les 4 pages qui l'utilisaient affichent le bandeau `trial` à un seul bouton.
- Page /api et bandeau `developer` : les boutons d'inscription mémorisent l'essai API (`PricingCatalog::apiTrialPlanId`,
  résolu en base, exposé par un composer à `partials.cta-banner` et `pages.produit.api`). ⚠ Après l'inscription,
  `/trial/{id}` du core renvoie encore à la racine de l'application, pas à `/compte/api` (bloc B).
- Page /mcp : inscription normale. 🟥 L'essai de l'application a **1 000 crédits API en production** (requête du
  2026-10-26 : `API_CREDIT` 1 / 1000, `API_MCP` 0) ; `API_MCP` n'est lu par aucun code, le MCP fonctionne avec ces
  crédits. Textes « pas d'API dans l'essai » corrigés (FAQ de l'essai, /essai-gratuit, modale, guides du centre
  d'aide). Ligne « Essai gratuit » ajoutée au tableau des offres de /mcp.
- Carte « Essai gratuit » de /tarifs : liste les fonctionnalités réelles du plan (quotas pour toute la durée, jamais
  « / mois »). Elle suit la grille de l'environnement : en dev elle montre plus de choses qu'en production.
- JSON-LD `AggregateOffer` dérivé de la grille publique (`PricingCatalog::publicOfferSummary`, cache 1 h).
- « Sans engagement » précisé (FAQ, sous-titre de /tarifs, badge) : résiliation en fin de période payée, annuel
  réglé d'avance (CGV 6.3).
- Étape 3 de « Comment ça marche » (accueil) : avis de valeur dès l'essai, mandat et signature avec le plan CRM.
- Déploiement : `git pull` + `view:clear` + `config:clear`. Aucun build, aucun package.

### Audit du tunnel, bloc B : inscription et connexion (2026-10-26)

- **Destination après inscription** (`signup-modal.js::nextUrl`) : sans plan ou avec l'essai de l'application, on suit
  l'URL d'autologin de l'APPLICATION rendue par `createApi` (avant : retour sur l'accueil de la vitrine) ; plan payant
  ou essai API, on rejoue l'autologin sur la vitrine pour que `intended()` mène à `/paiement/{id}` ou `/trial/{id}`.
- **Intention « application »** : `POST /tarifs/intention` accepte `target=app` (modale ouverte par un CTA générique)
  et pose l'URL de l'application si la session y est partagée (`ApplicationUrl::whenSessionShared`, core), sinon
  oublie l'intention ; un plan choisi puis abandonné n'envoie donc plus au paiement. Réponse enrichie de `kind`
  (`trial`, `api_trial`, `paid`, `pricing`, `app`). Modale de connexion : intention `pricing`, `keep` (plan choisi,
  ré-affichage après échec) ou application par défaut.
- **Écran de succès** selon `kind` : paiement = « finalisez votre souscription » ; essais et sans plan = « Confirmez
  votre adresse e-mail pour démarrer vos 7 jours d'essai » + bouton du webmail (table de la page d'activation du core,
  plus de `mailto:` vers soi-même) + « Continuer ».
- `tunnel-intent.js` : `refreshCsrfToken()` (`GET /_core/csrf-token`, met à jour la balise meta et les champs `_token`)
  appelé à l'ouverture des deux modales : plus de page 419 après 2 h.
- Mot de passe : jauge « Fort » seulement à 5/5, critères manquants affichés, `autocomplete="new-password"`. Erreurs :
  liens « Se connecter · Mot de passe oublié ? » sous une adresse déjà utilisée, message distinct pour une erreur
  serveur et pour une coupure réseau. Redirection automatique annulée par « Continuer » (une seule navigation, le
  jeton d'autologin étant à usage unique). Icônes de l'écran de succès redessinées.
- Bandeau « E-mail vérifié » de la vitrine (cas où la session n'est pas partagée) : bouton « Accéder à mon espace »,
  plus de fermeture automatique.
- Vérifié : `.insights/tunnel-bloc-b-2026-10-26.sh` 17/17 (compte jetable en base, supprimé par
  `.insights/cleanup-test-user.php`), navigateur avec réponse d'inscription simulée (aucun compte créé).
- Déploiement : APRÈS le tag core (helper `ApplicationUrl`) : `git pull` + `npm run build` + `view:clear`.
- **2e passe (B7, B12)** : page contact `?sujet=activation-essai` = sujet « Activation de mon essai gratuit » et message
  présélectionnés, nom et adresse repris de la session (aucune donnée personnelle dans l'URL) ; `ContactController`
  ajoute « Compte connecté : #id (adresse) » à l'e-mail reçu par l'équipe. Fenêtre d'inscription : `trialActivated`
  (rendu par `createApi`) ; sans essai, « Confirmez votre adresse e-mail pour accéder à votre espace » au lieu de
  « 7 jours d'essai ». Lien de confirmation signé : voir `core/CLAUDE.md`. Test : `.insights/tunnel-b7-b12-2026-10-26.sh`.
  ⚠ Compiler dans le conteneur (`public/build` appartient à root) :
  `docker exec -w /var/www/rs_infoparcelle_vitrine -e HOME=/tmp -e PATH=/usr/local/nvm/versions/node/v20.19.0/bin:/usr/bin:/bin docker-rs-1 npm run build`.
- **3e passe (B8)** : fenêtre d'inscription en deux champs Prénom / Nom (`prenom` envoyé à `/api/v1/register`, focus
  sur le prénom à l'ouverture, côte à côte dès 640 px, empilés en dessous). Serveur et e-mails : voir `core/CLAUDE.md`.
- **Bloc C** : le layout pousse le flash historique `gtm_event` PUIS la file `Rootsud\Core\Helpers\GtmEvents::pull()`
  (avant le chargement de GTM) ; son push dédié d'`activation` est retiré (l'événement arrive par la file, identifiant
  `activation-{id}`). La fenêtre d'inscription reçoit `sign_up` avec `user_id` et `event_id` dans le JSON. Test :
  `.insights/tunnel-bloc-c-2026-10-26.sh` (comptes `e2e-tunnel-*@infoparcelle.fr`, nettoyés par `cleanup-test-user.php`,
  qui accepte désormais ce motif). ⚠ Les tests lancent tinker en **www-data** (`-u www-data -e HOME=/tmp/psysh-www`) :
  en root, il crée des dossiers de cache où le serveur web ne peut plus écrire (`file_put_contents ... No such file`).

### Provenance des inscrits (levier M2, 2026-10-30)

La provenance de la première visite (utm, identifiant de clic Google / Microsoft / Meta, site référent, page
d'arrivée) est retenue en session par le core (`CaptureSignupAttribution`) et écrite à l'inscription ; la fenêtre
d'inscription (`/api/v1/register`, sans session) est complétée à la connexion automatique. Aucun changement dans la
vitrine elle-même. Bancs : `.insights/provenance-m2-2026-10-30/` ; `cleanup-test-user.php` supprime aussi les lignes
de `core.user_signup_sources` (sans clé étrangère).
- **Consentement** : la provenance n'est relevée qu'avec l'accord « marketing » du bandeau Cookiebot. Le layout
  inclut `core::includes.signup_attribution` (`@includeIf`, dans le `<head>` avant GTM) : script qui relève la page
  au moment où la personne accepte, et fait oublier / effacer la provenance quand elle retire son accord.
- **Politique de confidentialité mise à jour (2026-10-30)** : `pages/legal/confidentialite` décrit la provenance de
  l'inscription, relevée seulement avec l'accord marketing (section 2), sa finalité et sa base légale
  « Consentement (cookies « marketing ») » (tableau de la section 3), l'absence de cookie supplémentaire et l'effet
  d'un refus ou d'un retrait (section 4), sa conservation avec le compte (section 7), le retrait depuis la page
  cookies (section 9 ; suppression manuelle possible : `delete from core.user_signup_sources where user_id = …`).
  À publier avec le déploiement de M2, pas avant.

### Lien « Télécharger l'application » et politique de confidentialité (2026-11-01, audit mobile C1 / C2)
- `App\Services\AndroidApp` + composant `components/android-app-link` (pied de page `pied-de-page`, page d'essai
  `essai-gratuit`) : lien Google Play `rs.infoparcelle.application.mobile` avec `referrer` (`utm_source=infoparcelle.fr`,
  `utm_medium=site`, `utm_campaign=telecharger-application`, `utm_content`=emplacement, et gclid / gbraid / wbraid de la
  session si la personne a accepté les cookies marketing), jamais sur iPhone / iPad, clic `dataLayer`
  `app_download` (`app_placement`). Config `vitrine.android_app` (`VITRINE_ANDROID_APP`). Icône Lucide `Smartphone`
  déclarée dans les deux blocs d'`app.js`. Banc `.insights/mobile-audit-2026-11-01/c2_vitrine.py` 20/20 (⚠ Chrome
  jetable : `CAPTURE_CHROME_FLAGS="--disable-features=HttpsUpgrades,HttpsFirstBalancedModeAutoEnable"`, sinon la vitrine
  de développement passe en https où elle n'est pas servie).
- `pages/legal/confidentialite` (novembre 2026) : l'identifiant d'un clic Google est gardé avec l'accord marketing et
  transmis à Google Ads avec la date, le montant et la référence d'un abonnement payé (jamais nom ni e-mail), sauf
  refus des cookies marketing au paiement ; effacé dès qu'il a servi et au plus tard 90 jours après le clic.

### Page publique de suppression du compte (2026-11-02, Google Play)
`GET /suppression-du-compte` (`PageController::accountDeletion`, vue `pages/legal/suppression-compte`) : étapes dans
l'application ou par e-mail, données effacées 30 jours après, données conservées (factures 10 ans, éléments d'une agence
qui continue). Plan du site, lien « Supprimer mon compte » dans le bas du pied de page (ligne passée en `flex-wrap` : elle
débordait de 3 px à 390 px), politique de confidentialité sections 7 et 9. Adresse à déclarer dans la Play Console.
Déploiement : `git pull` + `view:clear` + `route:clear` (aucune classe CSS nouvelle, pas de build).

### Fenêtre d'inscription : « Vous êtes » (2026-11-02, lot 1 du plan outils gratuits)

- `partials/signup-modal.blade.php` : trois choix (professionnel de l'immobilier, particulier, autre professionnel) en tête du formulaire e-mail, obligatoires avant l'envoi (`signup-modal.js`, champ `profil`, message « Indiquez votre profil pour continuer. », aucune requête sans profil) ; libellé « Adresse e-mail ». Pas de question pour Google / Facebook / LinkedIn (décision utilisateur). Le core enregistre le profil, le renvoie dans `sign_up` (`user_profile`) et la fenêtre d'accueil de l'application le reprend.
- `pages/legal/confidentialite.blade.php` : profil déclaré (section 2) ; l'identifiant de clic Google est aussi transmis à Google Ads pour une déclaration professionnelle (date seule), effacé après l'abonnement ou à 90 jours.
- `.insights/cleanup-test-user.php` supprime aussi `ads_conversion_uploads` et `user_settings` du compte.
- Bancs : `rs_infoparcelle_application/.insights/lot1-profil-2026-11-02/`. Déploiement : après le tag core, `git pull` + `npm run build` + `view:clear`.

### Outils gratuits par adresse : socle (2026-11-02, lot 2 du plan outils gratuits + pages communes)

- **core-city installé SANS fournisseur** (`composer require` fait par l'utilisateur ; pas de découverte automatique) : aucune route de core-city sur www, seuls les modèles servent.
- **Connexions `pgsql_city` et `pgsql_bdnb`** (ajoutée) avec le **rôle PostgreSQL `vitrine_ro` en lecture seule** (`default_transaction_read_only`, `statement_timeout = 15s`) : écriture refusée (25006), mesuré. En production : `.insights/sql-role-vitrine-lecture-seule-2026-11-02.sql`, puis UNE FOIS `.insights/sql-vitrine-ro-droits-automatiques-2026-11-03.sql` (2026-11-03, **installé en local le 2026-11-03, joué en production (confirmé le 2026-11-04)**) : déclencheur d'événement `vitrine_ro_grant_auto` (objet de la base, survit aux renommages et suppressions de schémas). Quand un schéma PREND le nom core_city / bdnb / core_advert (création ou `ALTER SCHEMA ... RENAME`), il accorde USAGE et SELECT sur tout son contenu, dans la commande même ; il accorde aussi SELECT à chaque table, vue, vue matérialisée ou partition créée ensuite dedans. Conçu pour la procédure d'import de l'utilisateur : en local core_city renommé `core_city_new` puis exporté, importé sous ce nom en production, puis bascule `core_city` → `core_city_old` et `core_city_new` → `core_city` (conseillé : les deux renommages dans une transaction, aucun instant sans core_city). Même procédure pour la BDNB (`bdnb_new` → `bdnb`, `bdnb` → `bdnb_old`), testée le 2026-11-03 ; aucune clé étrangère, vue ni fonction d'un autre schéma ne dépend de core_city ou de bdnb (une seule fonction, `city_count_building_type`, cite core_city par son nom : elle suit le nouveau schéma). Les noms de passage ne sont pas touchés ; retour arrière par renommage inverse. Plus rien à rejouer (sauf DROP DATABASE). Les autres schémas restent fermés. Un dump local normal emporte déjà les droits (70 + 45 + 69 GRANT, USAGE et privilèges par défaut mesurés le 2026-11-03) : ils ne se perdent qu'avec `-x` / `--no-privileges` / `--no-acl`. Vérifié sur bases jetables (`.insights/vitrine-ro-bascule-test-2026-11-03.sh`, rejouable, crée puis supprime zz_vro_local / zz_vro_prod) : témoin sans déclencheur ni droits dans le dump = 0/74 lisibles après bascule ; dump normal sans déclencheur = 74/74 ; avec déclencheur et dump sans droits = 74/74 (bascule en transaction ou en deux commandes), vue matérialisée et partition comprises, retour arrière 71/71, table créée après bascule lisible ; `pg_restore --clean --no-acl` 46/46.
- **`/outils`, `/outils/{slug}`, `POST /outils/{slug}/resultat`** (`OutilController`), derrière **`VITRINE_OUTILS_ENABLED`** (faux par défaut : 404 partout et rien dans le plan du site ; vrai en dev). Limiteur `outils` par adresse IP (10 / minute, 30 / heure, 429 en français), champ piège `site_web`, délai minimal de 2 s (horodatage chiffré), résultat en cache 24 h par parcelle, rôle en lecture seule.
- **Gabarit unique** `pages/outils/show` + composant Alpine `toolForm` (autocomplétion BAN par le navigateur avec `fetch`, combobox au clavier, `dataLayer` `tool_view` / `tool_submit` / `tool_result` / `tool_cta`) ; chaque outil = une entrée de `config/outils.php` + un résolveur `App\Services\Outils\Tools\*` + une vue de résultat + une vue de contenu. Balise `robots` pilotable par `@section('robots')`.
- **Premier outil : « Référence cadastrale et surface d'une parcelle »** (`/outils/reference-cadastrale`) : référence lisible, section, numéro, surface, préfixe, commune, arpentage (« seul un bornage fixe les limites »), parcelle la plus proche signalée si le point est sur la voie, contour SVG ; bloc réservé « fiche parcelle complète » avec essai (achat à l'unité : lot 3). Prix d'appel lu en base (`PricingCatalog` `low_paid`).
- Vérifié : parcours HTTP 14/14 (page, jetons, trop rapide, champ piège, trouvé, référence, HTML, hors zone, sans parcelle, coordonnées absentes, jeton falsifié, CSRF), limitation 429 en français, navigateur (saisie BAN réelle, résultat, 3 événements `dataLayer`, 390 px sans débordement, 0 erreur console), interrupteur coupé = 404, plan du site 33/33 en 200, 0 ligne de log.
- 🟥 **`.env.example` portait des mots de passe et une clé réels** (`DB_PASSWORD`, `DB_PASSWORD_ROOT`, `MAIL_PASSWORD`, dans l'historique git depuis le 2022-07-02) : vidés ; **à faire tourner** si ce sont des valeurs de production.
- Déploiement : `git pull` + `npm run build` + `view:clear` + `config:clear` ; rôle SQL + variables `DB_*_CITY` / `DB_*_BDNB` dans le `.env` ; laisser `VITRINE_OUTILS_ENABLED` à faux jusqu'au lot 3.
- **Refonte des pages d'outils (2026-11-02, soir, demande utilisateur : « c'est nul, pas animé, ça ne retient pas l'attention »)** : gabarit `pages/outils/show.blade.php` refait (hero sombre `hero_dark` avec recherche mise en avant et aperçu animé d'un résultat d'exemple, squelette de chargement, résultat animé, « Ce que vous obtenez », « Comment ça marche », guide propre à l'outil, exemple de document, FAQ avec JSON-LD, sources, autres outils, appel final à deux publics). Contenu de chaque page dans **`resources/outils/{slug}.php`** (chargé par `OutilController::pageData` : badge, h1_html, lead, hero_points, preview, features, steps, guide_view, sample, faq, sources) + guide visuel `pages/outils/guides/{slug}.blade.php` ; `content/*` (ancien) remplacé. Partiels `pages/outils/ui/` (section-head, doc-offers, sample-doc, tool-card, final-cta). Animations CSS en fin d'`app.css` (`tool-pop`, `tool-ring`, `tool-bar`, `tool-float`, `tool-glow`, `tool-skeleton`, coupées sous `prefers-reduced-motion`) ; chiffres comptés par `toolForm.countUp` (`data-count`). 22 icônes Lucide ajoutées aux deux blocs d'`app.js`. Page `/outils` refaite (outils groupés par besoin, documents à l'unité avec leurs exemples et prix lus en base).
- **PDF d'exemple téléchargeables** : `resources/documents/exemples/` (copiés vers `public/documents/exemples/` par `vite-plugin-static-copy`) : plan de situation, plan cadastral, fiche parcelle (19 pages), avis de valeur (10 pages), avec aperçus `.webp` ; parcelle de démonstration 66136000EI0410, compte de démonstration 990001, thème Infoparcelle, filigrane « Exemple, document non valable ». Régénération : `rs_infoparcelle_application/.insights/lot3-documents-2026-11-02/generate-samples.php` (rendu par les workers d'administration, sinon les cartes de couverture sortent noires) puis copie depuis `administration/storage/app/private/tmp/exemples-docs/` et aperçus par `pdftoppm`.
- **Nouvel outil « Localiser une annonce »** (`/outils/localiser-annonce`, `input = url`) : le lien de l'annonce est retrouvé dans `core_advert.propertie_adverts.source_url` (index ; variantes https/http, www, / final), puis le score déjà calculé `advert_locate_scores.top10` donne les 3 parcelles les plus probables (`App\Services\Outils\Tools\LocaliserAnnonce`, `OutilController::lookupByUrl`, cache 6 h par lien normalisé). 🟥 **Rien qui localise la parcelle** : ni référence, ni identifiant, ni surface, ni carte ; concordances qualitatives (surface du terrain identique ou proche, construction, piscine, année compatible, DPE du même jour) ; la référence affichée floutée est une **fausse référence** générée côté serveur (empreinte de l'annonce et du rang). Annonce connue sans score : message « pas encore analysée ». Connexion **`pgsql_advert`** ajoutée (`DB_*_ADVERT`, par défaut les valeurs de `pgsql_city`) ; le rôle `vitrine_ro` doit avoir `select` sur `core_advert` (script SQL mis à jour).
- **Les 7 outils par adresse refaits (2026-11-02, soir)** : page `resources/outils/{slug}.php`, guide visuel `guides/{slug}.blade.php`, aperçu du hero `previews/{slug}.blade.php` (clé `preview.visual`), résultat refait, résolveur enrichi (clés de cache suffixées `v2`/`v3`). Plan de situation : 3 plans (situation, cadastre, vue aérienne `ORTHOIMAGERY.ORTHOPHOTOS`), zoom adapté à la parcelle, nord et échelle graphique, références façon Cerfa, lieu-dit, zone du PLU, adresses. Référence cadastrale : décomposition interactive, contenance ha/a/ca, périmètre, voisines, bâti (nombres seuls), section, IRIS, filiation DFI, plan SVG. Propriétaire : personnes morales seules, forme juridique et groupe (`App\Services\Outils\PersonnesMorales`, nomenclatures de la notice DGFiP : 25 codes de droit, 10 groupes, 248 formes), droits sur le terrain et locaux détenus, lien annuaire seulement pour un vrai SIREN (les « U… » sont fictifs), démarches officielles sinon. Zone tendue : échelle ABC, règles à deux niveaux, communes voisines (`fr_commune_snapshots`). PLU : date de procédure tirée de l'IDURBA, plan de zonage coloré, part de la parcelle dans chaque zone, pièces du dossier, règlement ouvert à la bonne page. Prix : séries mensuelles DVF (`fr_price_evolutions`), médiane des médianes mensuelles pondérée (différente de la moyenne du cache), quartier / commune / département, prix par nombre de pièces, courbe 7 ans. DPE : kWh, CO₂, coût, validité, frise loi Climat métropole / outre-mer ; `config/climate.php` corrigé (gel des loyers au 24/08/2022, seuil 450 kWh au 01/01/2023, gel outre-mer au 01/07/2024). `TileMosaic` (`metersPerPixel`, `scaleBar`, `fitZoom`) et `ParcelSvg` (`frame`, `project`, `scene`) enrichis : ne pas changer leurs signatures (utilisées par plusieurs outils). Ancien dossier `content/` supprimé, clés `content_view` retirées. Cahier des charges : `.insights/outils-refonte-2026-11-02/BRIEF.md`. ⚠ **Zone tendue** : `fr_insees.alur` (core-city `InseeZoneTask`) mélange les deux listes du décret et force 2 pour toute commune A bis : 11 communes A bis absentes de la liste officielle s'affichent « zone tendue » (01173, 78518, 60141, 78455, 78007, 78043, 01071, 01078, 01103, 01399, 01435), à corriger dans core-city. **Corrigé le 2026-11-03** : `alur` 0 hors listes, 1 liste 1 (loi de 1989 + taxes), 2 liste 1 et A bis, 3 liste 2 (taxes seulement) ; `ZoneTendue::resolve` rend `tendue` (1, 2) et `fiscale` (3), résultat à trois cas (« Oui », « Pour les taxes », « Non ») avec le bloc « Location : s'applique / ne s'applique pas ici », badge « taxes » des communes voisines, cache `v3`. ⚠ Projet de loi « Relance logement » (location des logements F et G) : textes du DPE à revoir dès son vote.
- **Localisation d'annonce : vignettes et intégration à l'accueil (2026-11-02, soir, demande utilisateur)** : chaque parcelle candidate porte une vignette = silhouette de la parcelle (parcelles réunies si plusieurs) et de ses bâtiments (dur, léger, piscine en bleu), **sans voisine, sans texte, sans échelle**, tournée d'un angle tiré d'une empreinte (25 à 335 degrés, jamais l'orientation réelle) ; la rotation est faite dans les coordonnées par `ParcelSvg::silhouette()` (nouvelle méthode, aucune signature existante changée) : aucun `transform` à retirer dans la page ; mention « Orientation modifiée ». Clé de cache de l'outil par lien passée en `v2`. Bouton « Essayer avec un exemple » = `LocaliserAnnonce::exampleUrl()` (maison ou appartement en ligne déjà analysé avec un indice ≥ 90 et une photo, cache 6 h), `data-placement` transmis à la mesure `tool_placement` ; le bloc `locate-embed` d'origine est remplacé depuis le 2026-11-03 par la carte-outil du hero (voir plus bas) ; accueil : section « Outils gratuits » (4 cartes + lien vers `/outils`) avant la FAQ. **Tout est rendu seulement si `VITRINE_OUTILS_ENABLED` est vrai** : accueil et page produit inchangés en production tant que l'interrupteur est coupé. `toolForm` attend désormais 2,3 s après l'affichage avant d'envoyer (le serveur refuse un envoi plus rapide : un lien collé aussitôt démarrait sur « Rechargez la page »). Vérifié : 3 vignettes rendues, vraies références absentes de la réponse, accueil / page produit / outil sans débordement à 390 px, 0 erreur console.
- **Localisation d'annonce : calcul en direct d'une annonce pas encore analysée (2026-11-02, soir)** : `LocaliserAnnonce::resolveUrl` demande le calcul à l'application (`computeLive`, POST signé vers `config('outils.locate_compute.url')`, défaut `https://app.infoparcelle.fr/interne/outils/localiser-annonce`, en dev `OUTILS_LOCATE_COMPUTE_URL=http://app.devinfoparcelle.fr/...` ; HMAC-SHA256 de `localiser-annonce|{propertie_id}|{ts}` avec `OUTILS_INTERNAL_SECRET`, sinon `APP_KEY`, commune aux deux projets ; délai 40 s) puis relit `advert_locate_scores`. La vitrine reste en lecture seule et sans core-advert : c'est l'application qui exécute le job `AdvertLocateScore` sur place. États : `scored` (avec `live` = « Analyse réalisée à l'instant », jamais mis en cache), `no_result`, `offline` (annonce hors ligne ou archivée : pas de calcul), `excluded` (programme neuf), `pending` (calcul sans réponse : jamais mis en cache, la recherche suivante relance). Cache des résultats par lien en `v3`. Pendant l'analyse : étapes animées (`ui/locate-steps`, `toolForm.steps`). Mesuré : annonce jamais analysée → 3 parcelles en 0,6 s par la vitrine (calcul 0,4 à 0,9 s dans l'application). ⚠ `OUTILS_LOCATE_COMPUTE_URL=` laissé VIDE coupe le calcul en direct (d'où les lignes commentées dans `.env.example`).
- **Localisation d'annonce : ce que le compte débloque, dit clairement (2026-11-02, soir)** : chaque parcelle porte un panneau « Avec un compte Infoparcelle » (référence cadastrale = fausse référence floutée ; « Adresse postale » = barres grises SANS texte derrière, jamais une fausse adresse qui pourrait désigner une vraie maison ; pastilles « Sur la carte satellite », « Google Maps », « Itinéraire »), puis un encart sous les 3 parcelles « L'adresse exacte et la parcelle sur la carte » (illustration générique dessinée, marquée « Illustration » ; connecté = « Voir cette annonce dans l'application » vers `{url_application}/annonce/{propertie_adverts.id}` ; visiteur = inscription + « J'ai déjà un compte » ; « Localisation des annonces incluse dès X € HT par mois » avec le prix lu en base). Faits vérifiés dans l'application : fiche parcelle « Adresse postale » avec « Itinéraire » et « Voir sur Google Maps » (satellite), top 10 sur la carte de la fiche annonce, `ACCESS_ADVERT` dans l'essai et les forfaits Basique / Premium / CRM. `reserved` de l'outil à `null` (le bloc générique « Pour aller plus loin » faisait doublon) ; `OutilController::lookupByUrl` passe `appUrl` et `fromPrice` à la vue.
- **Outils intégrés DANS le hero (2026-11-03, refonte sur retour utilisateur : le bloc sous le fil d'Ariane faisait pièce rapportée)** : le hero passe en deux colonnes, message et boutons inchangés à gauche, **carte-outil** à droite (`pages/outils/ui/hero-tool-card.blade.php` : pastille « Outil gratuit · sans inscription », onglets en grille compacte, question de l'outil actif, champ pleine largeur et bouton dessous, ligne d'aide, **exemple de résultat animé** = 4 premières lignes du `preview` de `resources/outils/{slug}.php`), et le résultat s'ouvre juste sous le hero (`ui/hero-tool-results.blade.php` : « Votre résultat gratuit », bouton « Fermer » = `toolForm.closeResult()`, offres de documents et lien vers le guide de l'outil actif), avant la capture de l'application. Une seule résolution : `App\Services\Outils\HeroTools::resolve($slugs, $site)` (outils actifs, libellés, offres, prix d'appel, exemple d'annonce) + `rootAttributes()` (data-* de la racine `toolForm`, qui englobe le hero ET le résultat). Libellé court d'onglet et question par outil dans `config/outils.php` (`tab`, `question`). `hero-pillar` accepte `tools` (slugs, premier ouvert) et `tools_placement` (mesure) ; posés sur `carte-cadastrale` (depuis remplacé par `hero-layers`), `estimation`, `localiser-une-annonce` (outil seul, sans onglets) et les 6 pages métiers ; retirés de `documents-et-conformite` le 2026-11-03 (demande utilisateur : sans rapport avec la page, hero d'origine remis) ; l'accueil a le même hero (annonce, parcelle, prix, DPE). `ui/tool-embed` et `ui/locate-embed` SUPPRIMÉS. Grille d'onglets : une ligne jusqu'à 3 outils, 2 × 2 sur téléphone puis une ligne pour 4, deux lignes de 3 au-delà (une ligne défilante cachait « Plans » et « PLU ») ; hauteurs réservées (question sur 2 lignes, ligne d'aide sur 2 lignes) : la carte ne change pas de taille d'un onglet à l'autre (mesuré à 2 px près). Sans `tools` ou interrupteur coupé : hero centré d'origine (vérifié). Vérifié : 15 pages en 200, parcours réel (accueil : Prix puis Fermer ; notaire : référence puis Plans sur la même adresse ; particulier : 6 onglets), résultat placé avant la capture, 11 pages à 390 px sans débordement ni libellé coupé, 0 erreur console, 0 ligne de journal.
- **Hero de l'accueil « titre par outil + barre pilule » (maquette A2 de Claude Design, 2026-11-03)** : `pages/outils/ui/home-hero.blade.php`, inclus par `home.blade.php` quand `HeroTools::resolve()` rend des outils (annonce, parcelle, prix, DPE) ; interrupteur coupé = hero d'origine « De l'annonce immobilière au mandat signé » (gardé tel quel dans le `@else`). Titre sur deux lignes, sous-titre, question, exemple du champ, bouton, ligne d'aide et étiquettes du plan changent avec l'onglet : textes par outil dans `$hhCopy` (versions courtes pour téléphone), posés en `data-*` sur les boutons d'onglet et lus par `toolForm.tabData` (le serveur rend ceux du premier outil). ⚠ Depuis le 2026-11-04, le H1 n'est PLUS par outil : voir « Titres de l'accueil » (section Plan SEO). Onglets en pilule de verre sur ordinateur, grille de 4 (icône au-dessus, 62 px) sur téléphone ; barre de recherche en pilule blanche (bouton sous le champ sous 640 px) avec halo (`.hh-pill`) ; plan cadastral 3D en CSS pur (`resources/images/hero/cadastre.svg` copié vers `/img/hero/` au build ; classes `.hh-band`, `.hh-planewrap`, `.hh-plane`, `.hh-map`, `.hh-group`, `.hh-anchor`, `.hh-stem`, `.hh-ring`, `.hh-pop` en fin d'`app.css` : angle de `.hh-stem` = opposé exact de `.hh-plane`, 50° téléphone / 56° ordinateur, ni masque ni overflow sur `.hh-plane`, groupes d'étiquettes en `preserve-3d` car `x-show` retire le `display: contents` de la maquette) ; positions des parcelles dans le SVG : n° 1 54 % / 42,3 %, n° 2 45,1 % / 56,3 %, n° 3 69,9 % / 32,5 % (à recalculer si le SVG change). Valeurs du plan = celles des aperçus des pages d'outils, marquées « Exemple · … » (sous le plan sur téléphone, à droite au-dessus du bouton d'essai sur ordinateur). Bandeau de confiance en verre + `cta-signup` (« Essai gratuit 7 jours, sans carte bancaire » si `trial_enabled`, sinon « Créer mon compte », « Accéder à mon espace » connecté). Styles traduits dans ceux du site : Inter (pas Plus Jakarta Sans), `font-mono` de Tailwind (pas JetBrains Mono), fond `.hh-bg` en ip-900 / ip-800 / ip-700 + `tool-dots`, accent du titre ip-200, bouton ip-600, scores green-600 / ip-600 / amber-600, DPE F orange-600 ; logo et menu = ceux du site. `toolForm` : `tabData` (data-* de l'onglet actif) et `pickPlaceholder()` (`data-placeholder-short` sous 640 px) ; `HeroTools::rootAttributes()` accepte des attributs en plus. Vérifié : rendu comparé à la maquette (ordinateur et 390 px), 4 onglets sur les deux formats, recherche réelle (prix) et exemple d'annonce depuis le hero, 0 débordement, 0 erreur console, interrupteur coupé = ancien hero.
- **Hero des pages secondaires « couches de données » (maquette S1-V2 de Claude Design, 2026-11-03)** : composant `components/section/hero-layers.blade.php` (mêmes paramètres que `hero-pillar` + `tools` (1 à 4), `tools_placement`, `subtitle_short`, `badge_icon`), posé sur `/carte-cadastrale` (Parcelle, Plans, Propriétaire, PLU) ; sans outils publiés il inclut `hero-pillar` d'origine. Fond blanc, titre FIXE (sujet de la page), question / exemple du champ / bouton / couche mise en avant suivent l'onglet ; deux colonnes à partir de 1 280 px seulement (en dessous la pile passe sous le texte, borné à 600 px : à 1 024 px les étiquettes auraient été coupées) ; résultat sous le hero puis capture de l'application. Pile 3D en CSS pur (bloc `.hl-*` en fin d'`app.css`, valeurs fixes des couleurs du thème) : rotation de `.hl-labin` = inverse exacte de `.hl-stack`, ni opacity / filter / mask / overflow sur `.hl-stack` ou `.hl-layer`, couches de bas (hl-z0) en haut (hl-z3), première d'onglet en haut ; une face par outil dans `pages/outils/layers/{slug}.blade.php` (plan `resources/images/hero/cadastre-clair.svg` recadré sur la parcelle 54 % / 42,3 %) et valeur d'étiquette dans `$hlValues` (exemples des aperçus des pages d'outils ; les valeurs entre crochets de la maquette sont remplacées : DP1 · PCMI1, SCI · personne morale, Zone UB ; zones de la couche PLU UB / UA / N), mention « Données d'exemple ». Questions et exemples du champ = ceux des outils (la maquette proposait « Ex. : AB 123, Perpignan » et « À qui appartient cette parcelle ? », faux pour nos outils : propriétaires personnes morales seulement). `is-on` posé côté serveur puis piloté par `:class` en syntaxe objet (retire la classe même statique). `AppServiceProvider` : `components.section.hero-layers` ajouté au composer de l'offre en cours. Vérifié : rendu comparé à la maquette, 4 onglets (couche soulevée, étiquette allumée), recherche réelle puis bascule sur PLU, 390 et 1 100 px sans débordement, 0 erreur console, interrupteur coupé = ancien hero.
- **Référence cadastrale acceptée à la place d'une adresse (2026-11-03, demande utilisateur)**, dans tous les outils par adresse (pages d'outils, cartes des heros, accueil) : `toolForm` reconnaît pendant la frappe une référence complète avec ou sans espaces (« 66136 000 AB 0140 », « 66136000AB0140 », « 66136 AB 140 ») → suggestion « Parcelle … », ou section + numéro + commune (« AB 140 Perpignan », « A 55, 66000 ») → communes de la Base Adresse Nationale (`type=municipality`, homonymes listés avec code postal et département) ; section + numéro seuls → conseil « Ajoutez la commune » ; une saisie avec un mot de voie reste une adresse. Envoi `ref` à `POST /outils/{slug}/resultat` : `ParcelLocator::parseReference()` (normalise, section complétée à 2, numéro à 4) puis `byReference()` (identifiant exact, préfixe omis = parcelles du code commune du cadastre, puis commune INSEE et arrondissements ; plusieurs parcelles = 422 « indiquez le préfixe », aucune = 404) ; le point intérieur de la parcelle sert de point d'adresse (PLU, plans), libellé « Parcelle 66136 000 AB 0140 · Perpignan ». Question de l'outil Parcelle : « Quelle parcelle se trouve à cette adresse ? » (au lieu de « Quelle est la référence cadastrale de cette adresse ? ») ; exemple du champ des 7 outils par adresse (parcelle, plans, propriétaire, PLU, prix, DPE, zone tendue) : « Adresse ou référence cadastrale », consignes des pages d'outils (`lead`, étape « Saisissez l'adresse ou la référence », FAQ) et lignes d'aide des heros (accueil, carte-outil, `hero-layers`) alignées. Avec une référence, le DPE cherche les DPE de la parcelle (pas d'identifiant d'adresse BAN). Vérifié : les 7 routes répondent 200 à « 66136000AB0140 ». Pages qui affichent des outils hors `/outils` : `/`, `/carte-cadastrale`, `/estimation`, `/localiser-une-annonce` et les 6 pages métiers (relevé sur le plan du site ; `/documents-et-conformite` n'en a plus depuis le 2026-11-03). Vérifié : 10 saisies en base (0 à 11 ms), parcours au clavier sur le hero, relance sur l'onglet PLU, homonymes, référence inexistante, adresse normale inchangée.
- **Liste des suggestions commune (2026-11-03, retour utilisateur : la liste passait sous les boutons)** : `pages/outils/ui/suggestions.blade.php` (params `suggestId`, `suggestShow`), utilisée par la page d'outil, la carte-outil des heros, le hero de l'accueil et `hero-layers` : lignes arrondies, icône par type (adresse, parcelle par référence, conseil non cliquable), ligne active fléchée, hauteur pour 6 suggestions sans défilement, apparition en fondu. Icônes en SVG dans le gabarit (celles de Lucide ne sont pas converties dans les lignes créées par `x-for`). 🟥 Le bloc qui porte le champ doit passer au-dessus des blocs suivants : `z-30` sur le bloc texte de `hero-layers` et de `home-hero` (au même `z-10` que le bloc des boutons placé après lui, la liste passait dessous). 🟥 Et la section ne doit pas masquer le débordement vertical, sinon la liste est coupée au bas du hero : `overflow-x-clip` (et non `overflow-hidden`) sur `hero-layers` et `home-hero` (le plan 3D de l'accueil est déjà masqué par `.hh-band`), page d'outil sans masquage sur la section et halos masqués dans leur propre calque ; `body` masque déjà le débordement horizontal. Vérifié : liste entière et au-dessus de la section suivante sur 7 cas (390 et 1 440 px), 0 débordement horizontal.
- **Hero de `/localiser-une-annonce` « radar de correspondance » (maquette Annonce-radar de Claude Design, 2026-11-03)** : même composant `hero-layers` (colonne de gauche identique aux pages secondaires) avec `'scene' => 'radar'`, qui remplace la pile de couches par `pages/outils/scenes/radar.blade.php` : loupe sur le plan `resources/images/hero/cadastre-village.svg` (zoom ×1,6), 9 parcelles candidates + la cible (contours recopiés du SVG), impulsions, rangs 1 à 10, pastille « N° 1 · 96 », trait de liaison (deux tracés : téléphone 350 × 410, ordinateur 580 × 580), fiche annonce et jauge du score ; progression en 4 segments, légende `role=status`, mention EXEMPLE, bouton « Rejouer ». Animation 100 % CSS (bloc `.ar-*` en fin d'`app.css`, préfixé pour ne heurter aucune classe du site) pilotée par la classe de phase `ar-p0` à `ar-p3` ; composant Alpine `radarScene` (`resources/js/alpine/radar-scene.js`) : boucle 1,9 / 2,6 / 2,4 / 5,2 s seulement quand la scène est visible (IntersectionObserver, arrêt onglet caché), phase 3 rendue côté serveur et figée sous `prefers-reduced-motion`, événement `radar-replay` émis par « Essayer avec un exemple » (qui lance aussi la vraie recherche). Textes de la page conservés (badge, titre, sous-titre déjà en place) ; `subtitle_short`, `placeholder` (« https://www.leboncoin.fr/ad/… ») et `placeholder_short` (téléphone) ajoutés à `hero-layers` (`HeroTools::rootAttributes` : les attributs en plus remplacent ceux de l'outil). Ligne d'aide en mode lien d'annonce : « leboncoin, SeLoger, Bien'ici, iad et la plupart des portails » (téléphone : « · tous les grands portails »). ⚠ Une classe Tailwind (`hidden`) est battue par une règle `.ar-*` non stratifiée : masquer par une règle CSS (`.ar-a3`). Moins de 380 px : scène réduite à 88 % et centrée par une marge négative calculée (une marge automatique ne peut pas être négative). Vérifié : 4 phases à l'écran comparées à la maquette, « Essayer avec un exemple » (phase 0 + résultat réel), 390 et 360 px sans débordement, 0 erreur console, interrupteur coupé = ancien hero, carte cadastrale inchangée.
- **Hero simple refait sur la charpente des heros à outils (2026-11-03, retour utilisateur : on passait d'un style à l'autre dans le menu Produit)** : `components/section/hero-pillar.blade.php` (documents et conformité, CRM, espace client, avis clients, widgets, gestion d'agence, et repli des pages à outils quand `VITRINE_OUTILS_ENABLED` est coupé) reprend le fond `.hl-bg`, les marges, la grille deux colonnes, le titre aligné à gauche et extra-gras, le badge avec icône (`badge_icon`, celle du menu), les boutons (inscription en bleu, forme du bouton sombre des pages à outils ; « Voir les tarifs → » en lien), la pastille d'offre compacte et la mention d'essai de `hero-layers`. À la place de la scène : la capture de l'application à droite, en perspective légère sur grand écran (`.hp-shot`, déborde du conteneur et se redresse au survol), sous les boutons sur téléphone (dans le premier écran). Titres des 6 pages et `pillar_title` des pages à outils : accent `text-ip-600` + point final (fin du `text-gradient`). Variante « carte-outil » de `hero-pillar` et `pages/outils/ui/hero-tool-card.blade.php` supprimées (plus utilisées). Vérifié : 7 pages à 1 440 et 390 px (0 débordement), repli outils coupés (4 pages, capture à droite, titre d'origine), plan du site 40/40. Puis (même jour, retour utilisateur « juste sous le hero, encore un bloc texte à gauche et image à droite ») : les pages qui enchaînaient le hero sur un `feature-split` ouvrent désormais sur une grille de cartes (motif déjà utilisé par Documents, Widgets, Carte cadastrale, pages métiers) : CRM et Avis clients = leur grille existante remontée sous le hero ; Espace client (5 cartes), Gestion d'agence et Estimation (4 cartes) = nouvelle grille qui résume les sections et y mène (`feature-split` accepte `id`, ancres `scroll-mt-32` ; `feature-grid` accepte `columns` 1 et 5 et affiche une flèche vers le bas pour un `url` en « # » ; icône `ArrowDown` déclarée dans les deux blocs d'`app.js`). Fonds réalternés (grille blanche, blocs sur gris). **Page Widgets : les cinq widgets** (Estimation, DPE, Opportunité pour les propriétaires ; Recherche pour les acquéreurs et locataires ; Demande de devis pour les artisans et prestataires), textes tirés de `resources/views/widget/_modal_create_widget.blade.php` du site ; description, sous-titre, FAQ, bloc « Et ensuite ? », menu (`config/vitrine.php`), carte de l'accueil et FAQ du CRM alignés (la frise 2025 de À propos garde trois widgets : c'était vrai alors). Coquille « historique d\'avis » affichée sur la page Avis corrigée.
- **Hero de `/estimation` « maquette 3D côtière » (maquette page-estimation-v2 de Claude Design, 2026-11-03)** : `hero-layers` avec `'scene' => 'estimation'` (onglets Prix / DPE / Zone tendue, colonne de gauche identique aux autres pages secondaires) ; scène `pages/outils/scenes/estimation.blade.php` posée sur le plan `resources/images/hero/cadastre-cote.svg` (vérifié : ni script ni lien), une animation par onglet : 20 colonnes 3D dont la hauteur figure le prix au m² (maisons en bleu, appartements en orange, étiquettes à plat 2 640 / 1 980 €/m²), logement DPE F qui s'élève dans un halo de chaleur, 7 murs le long de la limite communale + sol hachuré + « Zone tendue OUI B1 » ; carte d'exemple de l'onglet (mêmes valeurs que les aperçus `resources/outils/{slug}.php`, classe F cohérente avec « loyer gelé, location jusqu'à fin 2027 »). Groupes affichés par `x-show` sur `slug` (repasser de display: none à visible rejoue les animations CSS) ; classe `.ec-grp` = `display: contents` en CSS (jamais en style en ligne : x-show le retirerait et casserait la 3D). Composant Alpine `sceneReplay` (`resources/js/alpine/scene-replay.js`) : « Rejouer » masque les groupes 60 ms en figeant la hauteur de la carte ; scène hors de l'écran au chargement (téléphone) = animations remises au début et en pause (`.is-wait`) jusqu'à ce qu'elle soit visible. Styles `.ec-*` en fin d'`app.css` : rendu téléphone par défaut (scène 580 × 560 réduite à 60 % dans une boîte 350 × 278, légende et « Rejouer » au-dessus, carte dessous, 16 colonnes, `.ec-dk` = ordinateur seulement), rendu ordinateur à partir de 768 px, boîte à 90 % sous 380 px ; colonnes par variables `--w` / `--h` ; couleurs ip / accent du thème. `hero-layers` inclut désormais toute scène `pages/outils/scenes/{scene}` (radar, estimation) et accepte `pillar_title` : titre d'origine rendu par `hero-pillar` quand l'interrupteur est coupé (posé sur `/carte-cadastrale`, `/localiser-une-annonce` et `/estimation`, qui retrouvent ainsi exactement leur ancien hero). Vérifié : 3 onglets comparés à la maquette, « Rejouer », recherche réelle (référence → prix), 390 / 360 / 800 / 1 100 px sans débordement, mise en pause puis lecture au défilement sur téléphone, 0 erreur console, interrupteur coupé = 3 anciens heros.
- **Heros des 6 pages métiers « scène métier » (2026-11-03)** : `hero-layers` avec `'scene' => 'metier-{slug}'` (outils de la page inchangés, jusqu'à 6 : au-delà de 4 les onglets passent sur deux lignes de trois, aussi sur ordinateur ; `pillar_title` = titre d'origine si l'interrupteur est coupé). Un plan par métier dans le langage des plans de Claude Design, généré par **`.insights/scenes-metiers-2026-11-03/generate.py`** (`python3 -I`, aléatoire figé ; espace de paramètres (s, t) projeté par une fonction propre au tissu, rues = espace entre îlots) : `resources/images/hero/metier-{slug}.svg` + géométrie des éléments animés `resources/outils/scenes/metier-{slug}.json` (même repère 0-1000, lue par la vue). ⚠ Après une régénération : `npm run build` (le plan est copié vers `/img/hero/` au build ; le JSON, lui, est lu directement, d'où un décalage plan / surlignages tant que le build n'est pas refait). Tissus et histoires (4 temps, légende, « Rejouer », mention EXEMPLE) : agent immobilier = lotissement pavillonnaire (annonce repérée → parcelle n° 1 · 96 → avis de valeur envoyé → mandat signé, signature tracée) ; agence & réseau = centre-ville en étoile (3 secteurs tracés autour de l'agence → un agent par secteur + standards hérités → leads partagés → mandats du mois par agent) ; investisseur = îlots haussmanniens, boulevard, fleuve (prix par îlot + ventes, médiane 2 640 €/m² → zone UB hachurée + zone tendue B1 → bien repéré → score sur les 4 axes locatif / marchand / vendabilité / prospection) ; notaire = bourg rural rapproché (AB 140 · 612 m² → division en AB 201 / AB 202 et filiation → zone UB → mandat de négociation notariale n° NEG-2026-0012 tamponné (format réel NEG-AAAA-NNNN), numérotation propre) ; prestataire = villas rapprochées (parcelle cotée → toiture, PLU UB, DPE E → 10 panneaux posés → « Demande de devis » qui avance dans le pipeline Prospect / Contacté / RDV technique) ; particulier = gros plan d'une rue (votre maison → ventes autour → DPE F loyer gelé + zone tendue → pack location). Valeurs d'exemple alignées sur les aperçus des outils. Vues `pages/outils/scenes/metier-{slug}.blade.php`, partiel commun `scenes/_progress.blade.php` (aussi utilisé par le radar), composant Alpine **`phaseScene`** (`resources/js/alpine/phase-scene.js` : `data-durations`, `data-captions`, boucle seulement si visible, phase 3 rendue côté serveur et figée sous `prefers-reduced-motion`). Styles `.mt-*` en fin d'`app.css` : plan incliné `rotateX(50deg) rotateZ(-30deg)` (480 px téléphone, 780 px ordinateur, fondu radial sur le plan ET les surlignages), étiquettes `.mt-a > .mt-stem + .mt-pop` (rotation inverse exacte), apparition `.mt-x` + `.mt-fN` (à partir de la phase N) / `.mt-oN` (pendant la phase N), tracés `.mt-draw` (`pathLength="1"`). 🟥 La règle d'affichage commence par `.mt-scene.mt-pN` : à poids égal, un état masqué déclaré plus bas (`.mt-scale.mt-x`) l'emportait. 🟥 `.mt-kanban > span:not(.mt-move)` : sans le `:not`, la carte qui avance héritait du style des colonnes et masquait la troisième. Plans servis compressés : 48 à 65 Ko. Vérifié : 24 temps (6 scènes × 4) à 1 440 px, 4 scènes à 390 px et l'investisseur à 360 px sans débordement, 0 erreur console, interrupteur coupé = 6 anciens heros, page radar intacte.
- **Audit de conversion des heros à outils (2026-11-03)** : (1) bouton d'inscription du hero PLEIN (`hero-layers` : `bg-gray-900`, accueil : blanc sur le bleu) au lieu du contour / verre des maquettes, l'ancien hero ayant un bouton plein ; (2) sur téléphone et tablette, la scène passe APRÈS le bloc compte / tarifs (`order-last xl:order-none`) : l'appel à créer un compte arrive juste sous le formulaire (≈ 670 px) au lieu d'être repoussé sous l'animation (≈ 1 100 px) ; (3) « Zone tendue » avait `reserved` à null : un résultat ouvert depuis un hero se terminait sans aucune proposition, désormais bloc « Louer ou vendre dans cette commune » avec avis de valeur et pack des 3 (`reserved.buy.doc = avis`) ; (4) sur la page particulier, le bloc de droite sous les résultats parle aux particuliers (`doc-offers` param `audience`, transmis par `hero-tool-results`, déduit du placement `metier-particulier`) ; (5) inscription depuis le résultat « Localiser une annonce » : la personne reprend sur CETTE annonce dans l'application (`cta-signup` param `ctaAppPath`, événement `open-signup` / `open-login` avec `{ app: 'annonce/ID' }`, `rememberTunnelIntent(planId, target, path)`, `PricingController::storeIntent` n'accepte que `^annonce/[1-9][0-9]{0,11}$`, sinon accueil de l'application ; seulement si la session est partagée) ; (6) repère « Dans l'application Infoparcelle » au-dessus de la capture qui suit le hero ; (7) légende des scènes animées sans zone `aria-live` (annoncée en boucle aux lecteurs d'écran) ; (8) scènes recalées sur le produit : numéro de mandat au format du registre `2026-00041`, mandat notarial `NEG-2026-0012` (format réel `NEG-AAAA-NNNN`), prestataire = surface, emprise au sol et plan de situation DP1 (les panneaux figurent le projet du client, l'application ne dessine pas d'installation). ⚠ **Mesure** : dans GTM, le déclencheur GA4 « Custom Events » (57) ne transmet que `login|sign_up|search|view_item` : les événements `tool_*` (et `promo_bar`, `mcp_cta`, `app_download`) n'arrivaient pas dans GA4 (0 sur 30 jours, mesuré). **Fait le 2026-11-03** : version **32** du conteneur `GTM-TPFLK9MW` créée, **à publier par l'utilisateur** (publication par l'API refusée) : 6 balises GA4, une par famille d'événements avec ses seuls paramètres (« Google Analytics - Outils - ... » 83 résultat, 84 vue / changement / exemple / fermeture, 85 recherche, 86 clic sous le résultat ; « Google Analytics - Vitrine - ... » 98 barre de promotion, 99 page MCP), déclencheurs 79 à 82, 95, 96, variables `dlv_tool*` 73 à 78, `dlv_promo_*` 88-89, `dlv_mcp_*` 90 à 93. `app_download` était déjà transmis par la balise 71 de la version 31 (publiée pendant l'audit mobile, depuis un autre espace de travail : l'espace 30 était en retard, `createVersion` rendait `syncError` jusqu'à un `sync`). `tool-form.js` (`track`) et la page `/mcp` remettent les clés facultatives à `undefined` à chaque envoi (Tag Manager garde la dernière valeur poussée : vérifié dans le modèle de données après un clic). 12 dimensions personnalisées GA4 (portée événement) déclarées le même jour : `tool`, `tool_placement`, `tool_address_type`, `tool_found`, `tool_status`, `tool_cta_type`, `promo_action`, `promo_id`, `mcp_action`, `mcp_place`, `mcp_tab`, `mcp_copy`. SEO : le H1 par outil « Collez une annonce. Trouvez la parcelle. » a été remplacé le 2026-11-04 par un H1 fixe (voir « Titres de l'accueil », section Plan SEO).
- **Relecture de code indépendante des outils et heros (2026-11-03), défauts corrigés** : 🟥 la fausse référence floutée de « Localiser une annonce » tirait un numéro de 12 à 1811 et désignait de VRAIES parcelles de tiers (texte présent sous le flou) : numéro `0000`, inexistant au cadastre (`LocaliserAnnonce::fakeReference`, cache des résultats par lien passé en `v4`) ; 🟥 une commune ou une rue choisie dans la liste donnait une parcelle quelconque présentée comme exacte (« COMMUNE DE PERPIGNAN » comme propriétaire) : pour les outils à la parcelle (tous sauf `zone-tendue` et `prix-immobilier`, constante `AREA_TOOLS` de `tool-form.js`), la liste ne garde que les numéros et lieux-dits (`housenumber`, `locality`), conseil « Ajoutez le numéro dans la rue » sinon, Entrée ne prend jamais d'office une suggestion quand la saisie n'a aucun chiffre, et un changement d'onglet après un résultat de commune ou de rue explique au lieu de relancer ; Entrée pendant les 250 ms d'attente envoyait une suggestion de la liste précédente (`pendingSearch` relancée d'abord) ; changement d'onglet = relance seulement si le champ n'a pas été modifié (`lastQueryText`) ; « D117 Perpignan », « RN 116 Prades » : références ET adresses proposées (adresses en tête pour un préfixe de route) ; cache du DPE empoisonnable par le `ban_id` envoyé par le navigateur : clé `v3` avec la parcelle calculée par le serveur ; référence sans préfixe : plus de préfixe 000 choisi d'office (tous les préfixes, ambiguïté sinon, 36 ms) ; champs `t`, `ref`, `site_web` en tableau = 500 « Array to string conversion » : lus par `OutilController::inputText` (422) ; exemple d'annonce relu à chaque affichage quand il n'y en a pas ou que la base échoue : absence et échec mis en cache (30 min / 5 min) + `PDO::ATTR_TIMEOUT` 3 s sur `pgsql_advert` ; quota par IP relevé à 20 / min et 120 / h (`VITRINE_OUTILS_PER_MINUTE`, `VITRINE_OUTILS_PER_HOUR`) ; focus clavier perdu à chaque recherche : `aria-disabled` + garde au lieu de `disabled` ; onglets au clavier (flèches, Début, Fin, tabindex mobile, `aria-controls` vers le formulaire `{id}-panel`) ; option active annoncée (`aria-activedescendant`, ids `{suggestId}-opt-N`) ; résultat annoncé (zone `aria-live` « Résultat affiché sous le formulaire. ») ; mesure `tool_cta` en double (« pro » + « tarifs ») corrigée. Reste à trancher : la vignette de « Localiser une annonce » montre la forme réelle de la parcelle (tournée) avec la commune et une surface identique à l'annonce, un visiteur motivé peut la retrouver dans le cadastre ouvert.
- **Menu et pied de page** : « Outils gratuits » dans Ressources et dans la colonne Données, seulement si `VITRINE_OUTILS_ENABLED` est vrai (filtré dans `config/vitrine.php`).
- **Achat de documents à l'unité sous le résultat (2026-11-02, lot 3)** : `reserved.buy.doc` (`plans`, `fiche`, `avis`) dans `config/outils.php` (plan de situation, référence cadastrale, prix, PLU), packs lus en base par `App\Services\Outils\DocumentPacks` (pack du document + pack des 3, prix TTC, aucun bouton sans pack). Connecté : lien `/paiement/{pack}` ; anonyme : `choose-plan` (intention mémorisée puis inscription ; `storeIntent` renvoie `pack: true`, la fenêtre dit « finalisez votre achat »). Ensuite, la personne crée le document dans l'application (sans filigrane grâce au crédit). Détail : `rs_infoparcelle_application/.insights/lot3-documents-2026-11-02/README.md`. Déploiement : `git pull` + `npm run build` + `view:clear`.


### Pages par commune (lot 5 du plan outils gratuits + pages communes, 2026-11-03)

- **Adresses** : `/cadastre` (départements), `/cadastre/{departement}` (slug du département ; `/cadastre/66` → 301), `/cadastre/{departement}/{commune}`. `CommuneController`, derrière **`VITRINE_COMMUNES_ENABLED`** (faux par défaut : 404 partout, `/sitemap.xml` inchangé, liens du menu et du pied de page absents ; vrai en dev).
- **Données** : instantané `core_city.fr_commune_snapshots` (core-city `CommuneSnapshotTask`) lu par `App\Services\Communes\CommuneData` (tables nommées avec leur schéma, connexion `config('communes.connection')` = `pgsql_city` en production ; **en dev `VITRINE_COMMUNES_CONNECTION=pgsql_root`** = base locale, la seule qui a des instantanés, seul le 66 y est complet), mises en cache 60 min. Contour simplifié (`fr_insees`), prix du département (`fr_zone_price_cache`, 3 ans), successeur d'une commune disparue (point dans une commune, puis `fr_insee_histories`).
- **2026-11-04 : palier 1 de production = 526 communes**, généré sur le serveur puis commité (`b2490ba`, « published
  communes ») après un constat : le dépôt ne contenait que Perpignan. 🟥 Toujours commiter `published.php` depuis
  l'endroit où `communes:publish` a tourné, avant tout autre `git pull` qui le toucherait (sinon pages en 404).
- **Pages publiées = fichier versionné `resources/communes/published.php`** (aucune table : core_city est remplacé à chaque mise à jour). `App\Services\Communes\PublishedCommunes` ; commande **`communes:publish --palier=N [--dry-run]`** (déclarée dans `AppServiceProvider` : le noyau console est celui du core, qui ignore `app/Console/Commands`), à lancer avec l'utilisateur du dépôt (`docker exec -u 1000:1000 -e HOME=/tmp …`, `www-data` ne peut pas écrire le fichier). Elle ajoute, sans jamais retirer ni changer une adresse, les communes de l'instantané qui passent le seuil du palier (`config/communes.php` : 1 = 20 000 hab. et 10 ventes, types 0, 1, 4 ; 2 = 5 000 hab. et 30 ventes ; 3 = 2 000 hab. et 30 ventes). Slug figé à la publication (suffixe du code INSEE en cas de collision), date de publication, point de la commune. ⚠ **Le fichier commité ne contient que Perpignan (palier 1 calculé sur la base locale)** : le vrai palier 1 se génère depuis la vitrine de dev reliée à la préproduction, une fois `fr_commune_snapshots` présente dans core_city (après le prochain import), puis se commite.
- **Commune non publiée** : pas de page (404), seulement une ligne du tableau de son département. **Commune publiée puis disparue** : 301 vers la commune actuelle si elle est publiée, sinon vers le département (vérifié sur une fusion réelle : Ruffieu → Haut Valromey).
- **Page commune** (`pages/cadastre/commune`) : synthèse chiffrée (`CommunePresenter::synthesis`, chaque phrase seulement si sa donnée existe, comparaison au département), 4 chiffres clés, **plan cadastral interactif** (`resources/js/alpine/commune-map.js` : Leaflet 1.9.4 chargé à l'approche de la section, morceau séparé de 44 Ko compressé ; fonds IGN plan / photo aérienne, parcelles `CADASTRALPARCELS.PARCELLAIRE_EXPRESS`, contour de la commune ; clic = API Carto de l'IGN, référence et surface + liens vers les outils `?ref=…&from=commune`), cadastre, PLU (parts triées : jsonb ne garde pas l'ordre des clés), prix (maison / appartement / tous biens, période réelle, « sur un an », colonne département ; jamais de vente une par une), annonces et DPE (« au {date} »), zone tendue à trois cas, zonage ABC, autorisations d'urbanisme, copropriétés, arrondissements, communes voisines, sources et dates. JSON-LD Place + BreadcrumbList. Titre « Cadastre de Perpignan (66) : … » (`CommunePresenter::deName` : du Barcarès, des Angles, d'Argelès-sur-Mer).
- **Page département** : synthèse, fiches des pages publiées (arrondissements compris), tableau de toutes les communes (types 0 et 4) filtrable (Alpine, recherche sans accents), classes portées par la table (Pas-de-Calais : 501 Ko, 39 Ko compressé).
- **Outils** : `toolForm` relit `?ref=` (référence complète) sur la page d'un outil et lance la recherche ; `from=commune` = `tool_placement` « commune ».
- **Plan du site** : avec les pages communes, `/sitemap.xml` (et `/storage/sitemap.xml`) devient un index : `/sitemap-pages.xml` (pages du site), `/sitemap-cadastre-departements.xml`, `/sitemap-cadastre-palier-{N}.xml` (un par palier : la Search Console donne le nombre de pages indexées de chaque palier). `lastmod` d'une commune = la plus récente de la date de publication, de la dernière vente DVF de la période et de l'approbation du PLU (`CommunePresenter::lastmod`).
- Vérifié en dev : pages en 200 (0,1 à 0,2 s), 404 et 301 attendus, interrupteur coupé = 404 + plan du site d'origine, 390 px sans débordement (4 pages), 0 erreur console, clic sur une parcelle puis outil ouvert sur cette parcelle avec son résultat.
- ⚠ Événement `commune_map_parcel` poussé dans `dataLayer` : non transmis à GA4 tant qu'aucune balise GTM ne le prend.
- Déploiement (avec la mise en production des pages) : `git pull` + `npm ci` (Leaflet) + `npm run build` + `view:clear` + `route:clear` ; `VITRINE_COMMUNES_ENABLED=true` seulement une fois le fichier du palier 1 généré et `fr_commune_snapshots` lisible par `vitrine_ro`.
- **Refonte du même jour (demande utilisateur : plus moderne, animé, complet, vendeur, meilleur référencement et maillage)** :
  - **Commune** : haut de page sombre avec la commune et ses voisines dessinées (`CommuneData::contextMap` + `CommuneSvg`, tracé animé, repère pulsant, 4 pastilles chiffrées), sommaire collant avec section active (`sectionNav`), 6 chiffres clés comptés (`countUp`, le chiffre reste écrit par le serveur), plan cadastral + liste des sections avec leur nombre de parcelles, prix (barres commune / département, **courbe annuelle 2018-2025** `partials/price-chart` tirée de `fr_price_evolutions`, ventes par année, tableau), **prix des quartiers IRIS** (barres, médiane de la commune en repère), **annonces localisées** (bloc sombre « Exclusif Infoparcelle », 6 cartes `partials/advert-card` : **silhouette de la parcelle la plus probable comme dans le résultat de l'outil** (`LocaliserAnnonce::shape`, rendue publique : tournée côté serveur, sans voisine ni échelle, même rotation que l'outil pour l'annonce, cache `communes.adverts.v2`), type, prix, surface, pièces, « Particulier », jauge de l'indice, **fausse référence floutée en 0000, aucune photo des portails** - elles appartiennent aux agences, pas de republication sur des pages indexées ; bouton « Voir la parcelle » qui reprend sur l'annonce dans l'application après inscription), PLU en anneau animé, **signaux de vente** pour les professionnels (DPE récents, terrains constructibles, autorisations d'urbanisme, capture des Opportunités), zone tendue / zonage ABC en échelle / copropriétés, **documents à l'unité** avec exemples PDF et prix lus en base (si les outils sont actifs), voisines avec barres de prix, « Autres communes du département » (pages publiées), **FAQ tirée des chiffres** (8 questions pour Perpignan, `CommunePresenter::communeFaq`, FAQPage JSON-LD), « Pour aller plus loin », sources. `dateModified` dans le JSON-LD WebPage du gabarit (`@section('date_modified')`).
  - **Département** : haut de page avec la **carte des prix des communes** (`partials/choropleth`, quintiles, infobulle au survol et au clavier, zones publiées = liens), sommaire, chiffres clés, pages publiées, **classements** (plus chères, plus abordables : au moins 30 ventes ; le plus d'annonces), communes en zone tendue, tableau filtrable, **départements voisins** (`adjacentDepartments`, déduits des communes voisines), FAQ.
  - **France** : carte des prix des départements avec les 5 DOM en encarts, classements des départements, départements par **région** (`fr_regions`), grandes villes publiées.
  - **Maillage** : sous chaque résultat d'outil, lien vers la page de la commune si elle est publiée (`OutilController::communeLink`, partiel `tool-commune-link` ; « Localiser une annonce » renvoie le code commune, cache `v5`) ; bandeau sur `/outils` ; liens croisés de `/carte-cadastrale` ; titres des liens en « Cadastre de X ».
  - Textes : formes « de / du / des / d' » et « à / au / aux » des communes et des départements (`deName`, `aName`, `deDep`, `inDep`, table des articles des 101 départements). Aucune incitation à appeler.
  - ⚠ Le conteneur de la carte Leaflet porte `isolation: isolate; z-index: 0` en style : Leaflet empile ses calques jusqu'à z-index 1000, la carte passait au-dessus du sommaire collant et de la barre de navigation. 🟥 Une classe Tailwind ajoutée dans une vue (ici `isolate`) n'existe pas avant le prochain `npm run build` : vérifier sa présence dans `public/build/assets/*.css`.
  - Composants Alpine `inView`, `countUp`, `sectionNav`, `choropleth` (`resources/js/alpine/cadastre.js`) ; styles `cm-*` en fin d'`app.css`, coupés sous `prefers-reduced-motion`. ⚠ Une directive Blade collée à une lettre (`jours@if(…)`) n'est pas compilée et casse la vue : passer par `{{ condition ? … : '' }}`.
  - Poids mesurés (dev) : Perpignan 302 Ko (38 Ko compressé), Pyrénées-Orientales 335 Ko (47 Ko), Pas-de-Calais 799 Ko (91 Ko), France 292 Ko (42 Ko) ; 0,06 à 0,1 s en cache ; 390 px sans débordement sur 4 pages, 0 erreur console.
  - Corrigé au passage : l'outil « Localiser une annonce » affichait « Vente » pour une location (`offer_type` vaut `OFFER_RENT`).

### Audit SEO, GEO, maillage et conversion (2026-11-03)

Scripts rejouables et base GA4 de comparaison : `.insights/audit-seo-2026-11-03/README.md`. Constat avant correctifs :
143 pages en 200, 0 lien interne cassé, tous les CTA fonctionnels en anonyme ; titres des pages déjà en production
identiques (aucun risque de perte de positionnement) ; tout le reste est un ajout (10 pages d'outils, `/cadastre`,
101 départements, communes publiées).
- 🟥 **JSON-LD global réécrit en PHP + `json_encode`** (`layouts/vitrine.blade.php`) : l'ancien bloc écrit à la main
  passait les textes par l'échappement de Blade (`localisation d&#039;annonces` dans le JSON de chaque page, et un
  guillemet dans un titre l'aurait cassé). Organization enrichie de l'identité des CGU : `legalName` Rootsud SAS,
  `foundingDate` 2020-09-02, adresse, SIREN 888759982 (`identifier`), `vatID`, téléphone du site, `sameAs` Facebook et
  LinkedIn. SoftwareApplication : description fixe du produit (la page garde la sienne dans WebPage), `Web, Android`.
- Même défaut corrigé dans `components/breadcrumb` (fil d'Ariane encodé par `json_encode`, lien Accueil avec
  `aria-label`) et `components/section/faq` (réponses : balises retirées PUIS entités décodées).
- **Titres** des nouvelles pages ramenés à 65 caractères au plus, sans « | Infoparcelle » (Google affiche le nom du site
  et coupe vers 60) et sur les formulations les plus cherchées (Planificateur, 2026-11-03 : « cadastre » 301 000,
  « plan cadastral » 49 500, « plan cadastral gratuit » 8 100, « plan de situation » 8 100, « cadastre en ligne »
  6 600, « zone tendue » 4 400, « référence cadastrale » 3 600 ; par ville « prix m² » et « PLU » devant « cadastre » :
  Bordeaux 1 600 et 1 000 contre 320). Commune : « Cadastre de Perpignan (66) : plan cadastral, PLU et prix au m² »
  (version courte si le nom est long, PLU omis en RNU) ; département : version courte selon la longueur du nom ;
  `/cadastre` : « Cadastre en ligne : … » ; outils : `meta_title` / `meta_description` de `config/outils.php`
  raccourcis (« Plan cadastral et plan de situation gratuits d'une adresse »). `/tarifs` : « Tarifs Infoparcelle :
  abonnements et plans API » ; `/metiers` raccourci. Descriptions des pages déjà en production : voir plus bas.
- **Maillage** : chaque département lie les départements de sa région (`CommuneController::sameRegionDepartments`) et
  un département d'outre-mer lie les 4 autres (ils n'étaient liés que par `/cadastre`) ; texte du lien « grandes
  villes » de `/cadastre` séparé du prix (« Perpignan1 537 € »).
- **Plan du site** : `lastmod` des pages du site = date du fichier qui porte leur contenu (`SitemapController::pageLastmod`)
  au lieu de l'heure de la requête (Google ignore un lastmod qui change à chaque lecture).
- **`/llms.txt`** (`PageController::llms`, vue `llms.blade.php`, texte brut, `X-Robots-Tag: noindex`) : résumé factuel
  (couverture, localisation des annonces, sources, limites DVF et propriétaires, pages produit, outils et pages
  cadastre si actifs, MCP, tarifs). Absent en production avant ce lot.
- Mesure : la version 32 de GTM est publiée (tous les événements `tool_*` transmis). `commune_map_parcel` (clic sur une
  parcelle du plan d'une page commune, paramètre renommé **`commune_insee`** = code de la commune de la page, jamais la
  parcelle) : balise « Google Analytics - Vitrine - Carte commune » (102), déclencheur « Vitrine - carte commune » (101),
  variable `dlv_commune_insee` (100), **version 33 publiée le 2026-11-03** (vérifié sur le gtm.js public) ; dimension
  personnalisée GA4 « Commune de la carte » (`commune_insee`, portée événement) déclarée le même jour. Vérifié dans le navigateur : clic sur une
  parcelle de Perpignan → `{event: 'commune_map_parcel', commune_insee: '66136'}`.
- **Descriptions (2026-11-03, suite)** : les 24 descriptions de plus de 160 caractères des pages déjà en production
  ramenées à 160 au plus (faits déjà publiés seulement). Deux corrigées sur le fond : `/contact` ne propose plus de
  « demande de démonstration » (l'équipe n'en fait pas), `/avis-clients` dit « Plan CRM » au lieu de « Inclus dans votre
  abonnement » (et « principes ISO 20488 », pas une conformité). Pages légales laissées courtes.
- Déploiement : `git pull` + `view:clear` + `route:clear` (pas de build nécessaire pour ce lot).

### Modernisation des sections, lot 1 : composants partagés animés (2026-11-05)

Inventaire et suite : `.insights/modernisation-animations-2026-11-05.md` (lots 2 à 4 à valider). 0 migration, 0 route,
build Vite. Aucun texte, titre, H1, FAQ ni JSON-LD changé : seule la présentation bouge (référencement neutre).
- **`components/section/steps`** (15 pages) : `<ol>` / `<li>`, composant Alpine **`stepsFlow(n)`**
  (`resources/js/alpine/sections.js`) : étape en cours `is-active` (carte blanche, pastille qui pulse, icône animée,
  barre qui se remplit en 3,2 s = durée par défaut de `stepsFlow`, à changer des deux côtés), étapes passées `is-done`
  (liens remplis), boucle seulement si la liste est à l'écran et l'onglet visible ; survol = étape tenue, toucher sur
  écran sans survol = 6 s puis reprise. Liens `.sf-link-h` (de la fin de « numéro + icône » au numéro suivant,
  `sf-noicon` sans icône ; affichés seulement quand les étapes sont sur une ligne : `sm` en 3 colonnes, `lg` en 4) et
  `.sf-link-v` (téléphone : frise verticale, `li` en grille `2.5rem / 1fr` avec la tête en `contents`). Sans JavaScript :
  étapes affichées ; sous `prefers-reduced-motion` : toutes parcourues, sans boucle ni barre.
- **`components/section/comparison`** (agent, pourquoi, localiser) : composant **`armedInView`** (pose `is-armed` puis
  `is-in` : rien n'est estompé si l'animation ne joue pas), une seule fois : points « avant » barrés un à un
  (`background-size`, le texte reste lisible) et ramenés à 0,8, flèche centrale, points « après » cochés ensuite
  (`--nb` = nombre de points « avant » pour enchaîner). Grille `md:grid-cols-[1fr_auto_1fr]`.
- **`components/section/feature-grid`** (39 pages) : classe `fg-card`, survol `motion-safe` (carte soulevée, icône
  agrandie et pivotée), reflet qui suit la souris (`initCardSpotlight`, variables `--mx` / `--my`, souris seulement).
- **`/pourquoi-infoparcelle`** : scène `anim.tools-merge` au-dessus de l'avant / après, désormais paramétrable
  (`merge_windows`, `merge_rows`, `merge_title`, `merge_aria` ; sans paramètre = contenu d'origine de `/comparatif`).
- 🟥 **GSAP écrit `translate: none; rotate: none; scale: none` en ligne** sur ce qu'il fait apparaître (`data-gsap`) :
  toute classe Tailwind 4 de survol qui déplace (`hover:-translate-y-1`, qui passe par la propriété `translate`) restait
  sans effet sur ces éléments, depuis toujours. `app.js` : `clearProps: 'transform,translate,rotate,scale'` en fin
  d'apparition (fade-up et stagger). Contrôlé : sur les 65 pages, aucun élément animé ne portait de décalage propre que
  ce nettoyage réveillerait.
- Styles `.sf-*`, `.an-cmp*`, `.fg-card` en fin d'`app.css` (avant les styles des comparatifs), bloc
  `prefers-reduced-motion` dédié. Vérifié : 65 pages × 390 et 1 440 px, 0 erreur JavaScript, 1 H1 par page, 0 débordement
  (sauf `/cookies` à 390 px : déclaration Cookiebot, préexistant, sans rapport), boucle mesurée (une étape toutes les
  3,2 s), survol mesuré (`translate: 0px -4px`).
- Déploiement : `git pull` + `npm run build` + `view:clear`.

### Modernisation des sections, lot 2 : une scène animée par page produit (2026-11-05)

Suite du lot 1 (même plan : `.insights/modernisation-animations-2026-11-05.md`). 0 migration, 0 route, build Vite.
Aucun titre, texte, FAQ ni JSON-LD retiré ou modifié : chaque scène s'ajoute entre le titre de la section et son contenu
d'origine, n'a aucun titre (`<p>` seulement), et se décrit par `role="img"` + `aria-label`.
- **Scènes en quatre temps** (moteur `phaseScene` des scènes métiers, barre `pages/outils/scenes/_progress` avec légende,
  « Rejouer » et EXEMPLE) : `components/anim/crm-pipeline` (/crm-immobilier, le dossier traverse À prospecter,
  Estimation, Mandat signé, Compromis ; une ligne par étape sur téléphone), `doc-sign` (/documents-et-conformite, mandat
  rempli, mentions, signature, ligne au registre `2026-00041`), `avis-build` (/estimation, les 4 temps de la section),
  `widget-flow` (/widgets, widget Estimation sur un site d'agence puis lead dans le CRM, liste de hauteur fixe),
  `portal-phone` (/portail-client), `listing-match` (/localiser-une-annonce, 4 indices de l'annonce confrontés à la
  parcelle, rang qui monte jusqu'à n° 1 · 96), `agency-share` (/gestion-agence), `review-flow` (/avis-clients, aucune
  note moyenne affichée), `assistant-tools` (/mcp, question publiée sur la fiche ChatGPT, l'assistant choisit Communes
  puis Ventes DVF ; l'orbite `data-orbit` a été écartée : ses données (annonces, filiation, risques) ne sont pas exposées
  par le MCP).
- **Socle `.ps-*`** (fin d'`app.css`, avant les styles des comparatifs) : racine `.ps` avec `ps-p0..3` en syntaxe
  objet (ps-p3 rendue par le serveur, gardée sous prefers-reduced-motion) ; `.ps-x` masqué, `.ps-fN` visible à partir du
  temps N, `.ps-oN` pendant le temps N, `.ps-hN` mis en avant, `.ps-draw` tracé (`pathLength="1"`), `.ps-type` + `.ps-tN`
  texte tapé pendant le temps N (`--n` caractères ; rejoué à chaque boucle par une animation, pas une transition),
  `.ps-grow.ps-gN` bloc qui s'ouvre en hauteur, `.ps-l` / `.ps-r` / `.ps-z` sens d'apparition, `--d` délai. Réglages
  propres : `.sc-crm-*`, `.sc-est-*`, `.sc-ag-*` (deux noms d'animation en alternance pour la rejouer à chaque temps),
  `.sc-mcp-dot`.
- **Onglets qui défilent seuls** : composant **`tabCycle(n, ms, autoplay)`** (`resources/js/alpine/sections.js` :
  défilement tant que le bloc est visible, arrêté au premier clic ou à la première touche, flèches / Début / Fin,
  `reveal()` ramène l'onglet actif au centre d'une barre qui défile sans faire défiler la page) ; vues
  `components/anim/parcel-tabs` (/carte-cadastrale, fiche parcelle d'exemple aux 12 onglets, deux lignes d'onglets dès
  640 px, défilement sans barre visible sur téléphone) et `pillar-journey` (/fonctionnalites, 9 piliers avec la capture
  `-800w` de chaque écran, chargée seulement à l'affichage ; l'API montre la vraie réponse de `config/api_datasets.php`).
  Styles `.sc-tabs`, `.sc-tab-*`, `.sc-panel`, `.sc-row`.
- **/api** : « Exemple d'utilisation » = scène `anim.api-flow` (réponse réelle de la fiche parcelle) + le même appel en
  onglets cURL / Python / JavaScript (`tabCycle(3, 0, false)`, sans défilement) ; sans JavaScript, les trois blocs
  restent affichés.
- 🟥 **Défaut antérieur corrigé : `data-captions="@json($captions)"` coupait l'attribut** (les guillemets structurels du
  JSON ne sont pas échappés par `@json`) : la légende des scènes des 6 pages métiers devenait vide dès que l'animation
  démarrait. Les 15 scènes écrivent désormais `data-captions="{{ json_encode($captions) }}"`. Ne jamais mettre `@json`
  dans un attribut entre guillemets doubles.
- ⚠ `data-md-skip` sur la racine de chaque scène (export Markdown des pages, `App\Services\Markdown\HtmlToMarkdown`) :
  à garder sur toute nouvelle scène. Seuls `phaseScene`, `radarScene` et `sceneReplay` sont ignorés d'office ; `tabCycle`
  ne l'est pas (il porte aussi les exemples de code de /api), une scène `tabCycle` ou `inView` doit donc porter le marqueur.
- Vérifié : 12 scènes à 1 280 px (chaque temps) et les plus denses à 390 px, 65 pages × 390 et 1 440 px : 0 erreur
  JavaScript, 1 H1 par page, 0 débordement nouveau (`/cookies` préexistant), 15 scènes avec leurs 4 légendes.
- Déploiement : `git pull` + `npm run build` + `view:clear`.

### Modernisation des sections, lot 3 : bas des pages d'outils (2026-11-05)

Même plan (`.insights/modernisation-animations-2026-11-05.md`). 0 migration, 0 route, build Vite ; aucun titre ni texte
retiré, les chiffres affichés sont ceux déjà publiés dans les guides (`pages/outils/guides/*`).
- **DPE** (`guides/dpe`) : calculateur **`dpeClass`** sous le tableau des seuils (deux curseurs énergie / climat, la classe
  retenue est la plus mauvaise des deux, la ligne du tableau s'allume : `.dpe-row.is-on`) et **`dpeValidity`** sous la
  frise des générations (date du DPE → « Valable jusqu'au … » ou « Expiré le … » : avant 2013 dix ans, 2013-2017 au
  31/12/2022, 2018 au 30/06/2021 au 31/12/2024, depuis le 1er juillet 2021 dix ans moins un jour ; refus avant le
  1er novembre 2006 et des dates à venir). Seuils et règles = ceux du guide : à changer aux deux endroits.
- **Prix au m²** (`guides/prix-immobilier`) : « Le parcours d'un prix jusqu'à DVF » piloté par `stepsFlow(5)`, ligne
  remplie (`.tl-fill`), étiquette « 285 000 € demandés » → « 268 000 € dans DVF » (exemple DVF publié plus bas).
- **Propriétaire et PLU** (`guides/proprietaire-parcelle`, `guides/plu`) : cartes d'étapes écrites à la main passées sur
  `stepsFlow` et les classes `.sf-*` du lot 1 (liens horizontaux à partir de `md`, `.sf.gap-5` pour l'écart de 1,25 rem).
- **Plan de situation** (`guides/plan-de-situation`) : frise verticale des 6 étapes pilotée par `stepsFlow(6)` (styles
  `.tl-step`, `.tl-icon`, `.tl-card`, `.tl-line-fill`).
- **Cadastre inversé** (`guides/cadastre-inverse`, section élargie à `max-w-6xl`) : carte d'exemple (tuiles dessinées par
  `mt_srand(11)`, aucune vraie parcelle ni décompte) synchronisée aux 4 étapes (`phaseScene`, étape en cours `.ps-hN`) :
  ± 2 %, bâties, photos (parcelle choisie), ± 5 %. ⚠ Specificité : la règle de la parcelle choisie doit répéter
  `.ci-range.ci-built.ci-pick`, sinon `:is(.ps-p1, .ps-p2) .ci-range.ci-built` l'emporte.
- **Surface** (`guides/surface-parcelle`) : parcelle d'exemple synchronisée aux 4 premières cartes (vendre 612 m²,
  diviser 312 + 300 m², construire emprise et distances, clôturer périmètre 104 m). Styles `.sc-su-*`.
- ⚠ Recette en onglet de navigateur caché : les transitions CSS n'avancent qu'à l'affichage d'une image (capture
  d'écran) ; un `getComputedStyle` lu sans image peut rendre l'état de départ de la transition.
- Vérifié : chaque scène à 1 280 px (temps forcés par `go(n)`), les calculateurs sur 7 dates et 2 couples de valeurs,
  rendus à 390 px ; 65 pages × 390 et 1 440 px : 0 erreur JavaScript, 0 composant Alpine non initialisé, 1 H1 par page,
  0 débordement nouveau (`/cookies` préexistant), 17 pages à scènes en quatre temps avec leurs 4 légendes.
- Déploiement : `git pull` + `npm run build` + `view:clear`.

### Image de partage par page, titres des fenêtres, Paris (2026-11-05, n° 20 et 21 de l'audit SEO)

- **`App\Services\OgImage`** : une image Open Graph 1200 × 630 par page au lieu d'`og-default.png` pour les 689 pages.
  Titre de la page (« Infoparcelle : » retiré, déjà en tête de l'image), étiquette et couleur du gabarit (déduites du nom
  de route : outil, cadastre commune / département, comparatif, métier, fonctionnalité, développeurs, guide, tarifs,
  légal), plan de parcelles tiré de l'empreinte de la page. Rendue par GD avec `resources/fonts/inter/InterVariable-latin.woff2`
  (FreeType lit le WOFF2 ; faux gras par décalage, le variable n'étant rendu qu'en graisse normale), JPEG ~80-95 Ko,
  écrite au premier affichage de la page dans `storage/app/og/{empreinte}.jpg` (~40 ms), servie par
  `GET /og/{empreinte}.jpg` (`OgImageController`, 404 si absente, cache public d'un mois). ⚠ Aucune image n'est fabriquée
  à la demande d'une adresse (pas de texte arbitraire à nos couleurs). Changer le dessin = incrémenter `OgImage::VERSION`
  (2 depuis le 05/11 : espaces insécables avant « : ; ? ! » et après un nombre, plus de ligne qui commence par « : »).
  Une page peut toujours imposer la sienne par `@section('og_image')`. Balises `og:image:width/height/alt` ajoutées ;
  l'`Article` des pages à auteur (`byline`) prend la même image.
- Fenêtres de connexion et d'inscription : titres en `<p>` (même style) au lieu de 3 H2 + 1 H3 dans chaque page.
- Département dont une commune porte le nom (Paris) : H1 « Cadastre de Paris (75), le département » (`$depSameName`).
- Vérifié : 65 pages en 200, 65 images différentes servies en 200 avec `og:image:alt`, 1 H1 par page, Paris à 390 et
  1 440 px sans débordement. Déploiement : `git pull` + `view:clear` + `route:clear` (aucun build).

### Phrase d'identité et réponses d'abord (2026-11-05, n° 3 du suivi des assistants IA)

- **`config('vitrine.identity')`** (variables `$identity` / `$identityMore` en tête de `config/vitrine.php`) : `sentence`
  « Infoparcelle est un logiciel français en ligne pour les professionnels de l'immobilier, édité par Rootsud SAS,
  société créée le 2 septembre 2020 à Perpignan. », `full`, `short` (pied de page). Reprise mot pour mot par la FAQ
  globale « Qu'est-ce qu'Infoparcelle ? » (accueil, /tarifs, FAQPage), « Qui sommes-nous ? » et la description de
  /a-propos (+ fiche d'identité en `<dl>` : éditeur, création, siège, SIREN, couverture, utilisateurs, contact), le
  pied de page, `/llms.txt` et `description` de l'Organization du JSON-LD. ⚠ Ne jamais la reformuler ailleurs : la
  même phrase partout est ce que reprennent les assistants.
- Questions ajoutées (réponse d'abord, FAQPage) : « Quel CRM pour un mandataire immobilier indépendant, et à quel prix ? »
  (/crm-immobilier : mode « Mandataire de réseau », prix lus en base) ; « Comment retrouver l'adresse d'une annonce
  Leboncoin ou SeLoger ? » et « Sur quelles données repose la localisation, et est-ce légal ? » (/localiser-une-annonce :
  données publiques en open data, aucune donnée personnelle, approche sur place ; aucune affirmation juridique).
- FAQ globale : DVF attribué au Cerema (DVF+), comme la doc.
- Vérifié : 5 pages × 390 et 1 440 px sans débordement, 1 H1, JSON-LD valide, versions `.md` et `/llms-full.txt` à
  jour, 0 erreur console. Déploiement : avec le reste de la vitrine (`git pull` + `view:clear` + `config:clear`).

### Modernisation des sections, lot 4 : pages institutionnelles et index (2026-11-05)

Dernier lot du plan `.insights/modernisation-animations-2026-11-05.md`. 0 migration, 0 route, build Vite ; aucun titre
ni texte retiré, aucun H2 ajouté.
- **/tarifs, « Quel plan pour moi ? »** (`components/plan-finder`, inclus dans `pricingView`, replié par défaut) :
  besoins à cocher → premier plan, du moins cher au plus cher, qui les couvre tous ; « Voir ce plan » repasse en vue
  cartes, fait défiler jusqu'au haut de la carte visible du même nom (mensuelle ou annuelle) et la fait briller
  (`.pf-flash`). Données : `PricingController::planFinderData()` + constante **`FINDER_NEEDS`** (icône, libellé, codes
  `project_extras.label`) ; l'inclusion passe par `comparisonCellIsEmpty()`, le test des cellules du comparatif : le
  sélecteur ne peut pas conseiller un plan qui n'a pas ce qui est coché, et suit la grille en base (publique,
  partenaire, dev ou prod : ⚠ les plans de production ne sont pas ceux du dev, ex. signature électronique dans Premium
  en dev, dans CRM seul en prod). Un besoin dont un code manque à la grille n'est pas proposé ; masqué sous 2 plans
  payants ou 3 besoins. Aucune mesure GA4 ajoutée (événement à créer dans GTM si besoin).
- **/a-propos** : frise « Notre parcours » sur `stepsFlow(5)` (`.tl-step`, `.ab-year`, `.ab-dot`, ligne remplie).
- **/essai-gratuit** : les 6 coches de « Ce qui est inclus » apparaissent une à une (`armedInView`, `.an-checks`, `.an-chk`).
- **/outils** : section sans titre sous le fil d'Ariane, `components/anim/tools-showcase` (`tabCycle`) : l'aperçu
  d'exemple de chaque outil (`resources/outils/{slug}.php`, clé `preview`, relu par `require`, 4 lignes au plus) avec le
  lien vers l'outil. Panneaux superposés et masqués par `invisible` (hauteur constante, mesurée sur les 10 outils).
- **/metiers** : `components/anim/pillar-journey` paramétré (`journey_groups` [titre|null, [[icône, libellé, url,
  capture|null, texte]]], `journey_title`, `journey_prefix` pour les ids, `journey_label`) : 7 métiers, capture `-800w`
  de chaque page, texte tiré de la grille ; `/fonctionnalites` inchangé (valeurs par défaut).
- **/partenaires** : `components/anim/data-orbit` paramétré (`orbit_items`, `orbit_center`, `orbit_aria`, angle = 360 /
  nombre d'éléments) avec les 12 sources de la grille, centre « Infoparcelle », conteneur `overflow-x-clip` (l'orbite
  fait 368 px sur téléphone). Ordre court / long vérifié par calcul sur un tour complet, degré par degré, avec les vraies
  dimensions des étiquettes : 11,9 px d'écart au plus près à 1 280 px, 16,7 px à 390 px. /comparatif inchangé.
- **Guides comparatifs** (`pages/comparatif-guide`) : « Selon votre besoin » (liste déroulante) au-dessus du tableau,
  composant **`needFocus`** : colonnes des outils de la section « lequel choisir » mises en avant (`.cg-on`), les autres
  estompées (`.cg-dim`), aucune masquée, raison affichée, défilement du tableau jusqu'à la première colonne ; bouton
  « Voir dans le tableau » sous chaque besoin (évènement `cg-need`). Un concurrent mis en avant reste mis en avant.
- ⚠ Grilles à colonne unique sur téléphone : `grid-cols-[minmax(0,1fr)]`, sinon la colonne implicite prend la largeur
  du contenu le plus long (débordement de 21 px mesuré sur /outils).
- Vérifié : sélecteur (Premium pour cadastre + couleurs + équipe, CRM pour les mandats, défilement et lueur sur la
  carte), filtre (3 colonnes en avant / 6 estompées, bouton depuis la section), hauteurs constantes (10 outils, 7
  métiers), 65 pages × 390 et 1 440 px : 0 erreur JavaScript, 0 composant Alpine non initialisé, 1 H1 par page,
  0 débordement nouveau (`/cookies` préexistant), plus 8 pages à 360 / 390 / 768 px.
- Déploiement : `git pull` + `npm run build` + `view:clear`.

### Guides comparatifs de plusieurs outils (2026-11-05, n° 5 du suivi du référencement dans les assistants IA)

- **Deux pages** : `/comparatif/localiser-une-annonce-immobiliere` (9 outils : Infoparcelle, Annonce to Adresse, La Loupe
  Immo, LaGrandeCarte, Parcellaire, Précisément, Prospec'Immo, Telescop, Yanport) et `/comparatif/crm-immobilier`
  (9 logiciels : Infoparcelle, Adapt Immo, Apimo, Hektor, HubSpot, Immo-Facile, Modelo ex-Netty, noCRM.io, Zoho CRM).
  Les assistants répondent à « quel outil pour… » en résumant des pages qui comparent plusieurs outils : c'en sont.
- **Contenu** : `config/comparatif_guides.php` (critères, une entrée par outil : valeurs, points forts, « À savoir »,
  sources, `checked_at` propre à l'outil si différent de celui du guide, `compare` = slug du comparatif « Infoparcelle
  ou X » s'il existe). Vue `pages/comparatif-guide` : réponse courte d'abord (« En bref »), sommaire des outils,
  tableau critères × outils (Infoparcelle en premier, colonne de critères collante, défilement horizontal dans le
  tableau seulement), « lequel choisir selon votre besoin » (y compris quand un autre outil convient mieux), une fiche
  par outil, FAQ (FAQPage), méthode ; ItemList en JSON-LD.
- **Même adresse que les comparatifs** : `PageController::comparatif()` sert d'abord un guide publié
  (`publishedComparatifGuides()`, `checked_at` renseigné), sinon la page « Infoparcelle ou X ». Liste `/comparatif`
  (groupe « Guides comparatifs » en tête), plan du site (priorité 0.6, lastmod = config + vue), `/llms.txt` (clé
  `llms`), liens croisés de `/crm-immobilier` et `/localiser-une-annonce`.
- **Prix d'Infoparcelle jamais écrits à la main** : jetons `:prix_basique`, `:prix_premium`, `:prix_crm` (prix catalogue
  mensuels lus en base par `comparatifOffers()`), `:prix`, `:essai` (selon `trial_enabled`). Espace insécable posé
  avant « € » et « % ».
- 🟥 **Faits relus sur le TEXTE BRUT des pages des éditeurs** (curl, ou navigateur pour les pages en JavaScript :
  tarifs de HubSpot, noCRM, Zoho), jamais sur un résumé d'outil. « Non indiqué » = absent des pages consultées. Netty
  n'existe plus (netty.fr renvoie vers Modelo de Septeo). Colonnes de La Loupe Immo et Telescop reprises de leur
  comparatif du 2026-11-03. Infoparcelle ne diffuse pas les annonces et n'a pas de site d'agence : écrit tel quel.
- Déploiement : `git pull` + `npm run build` (classes nouvelles) + `view:clear` + `config:clear` + `route:clear`.

### IndexNow (2026-11-04, n° 4 de l'audit SEO complet)

- Clé dans `INDEXNOW_KEY` (config `services.indexnow` : `key`, `site_url` = `https://www.infoparcelle.fr`, `endpoint` =
  `https://api.indexnow.org/indexnow`). Publique par nature : servie en clair à `/{clé}.txt`
  (`SitemapController::indexNowKey`, route `indexnow.key`, `[A-Za-z0-9-]{8,128}`) ; toute autre valeur ou clé vide = 404.
  La clé de dev diffère de celle de production.
- `php artisan indexnow:submit [--dry-run] [--all] [--url=…]` (déclarée dans `AppServiceProvider`, noyau console du
  core) : lit le plan du site PUBLIC (`site_url/sitemap.xml` et sous-plans, en HTTP), envoie les pages nouvelles ou dont
  le `<lastmod>` a changé depuis le dernier envoi accepté, plus les pages sorties du plan (une dernière fois).
  Mémoire `storage/app/indexnow/state.json` (non versionnée) : lancer en **www-data**. Vérifie d'abord que le fichier
  de clé est en ligne (sinon 403 du moteur) ; un sous-plan illisible arrête tout (pas d'envoi partiel qui ferait passer
  des pages pour retirées) ; 200 et 202 = acceptés, mémoire écrite après chaque lot accepté seulement.
- Google n'utilise pas IndexNow. Bing alimente Copilot et la recherche de ChatGPT.
- Banc : `.insights/audit-seo-complet-2026-11-04/indexnow-harness.php` (Http::fake, stockage temporaire) 33/33.

### Surface d'une parcelle, maisons vendues, matrice, division et bornage (2026-11-05, n° 12 à 14 de l'audit SEO complet)

- **Outil `surface-parcelle`** (`/surface-parcelle`, « surface parcelle » 5 400 recherches par mois) : même résolveur et
  même vue de résultat que `reference-cadastrale` ; `results/reference-cadastrale.blade.php` met la surface en tête
  (`$surfaceMode`, slug `surface-parcelle`) et retire sa carte des chiffres clés ; côté référence, la carte « Surface »
  porte un lien « Lire la surface » (`?ref=` de la parcelle). Page `resources/outils/surface-parcelle.php`, guide
  `guides/surface-parcelle` (trois niveaux de certitude et convertisseur DÉPLACÉS du guide de la référence, calculateur
  sur plan, usages, écart de surface), aperçu `previews/surface-parcelle`. Cache des résultats séparé par slug
  (`outils.surface-parcelle.*`).
- **Calculateur de terrain sur plan** (section `#terrain` du guide, remplace le 2026-11-05 un simple additionneur jugé
  sans intérêt) : composant Alpine `landCalculator` (`resources/js/alpine/land-calculator.js`, Leaflet chargé à
  l'approche, photo aérienne + parcelles IGN, carte et calque gardés HORS de l'état d'Alpine), clic = ajoute la parcelle
  du point, second clic = la retire, champ « adresse ou référence » (référence = ajout ; numéro de rue = ajout de la
  parcelle du point ; rue ou commune = plan centré). `POST /outils/surface-parcelle/terrain`
  (`OutilController::terrain`, plafond `outils-terrain` 60 / min et 600 / h, jeton et champ piège des outils, 30
  parcelles au plus) → `App\Services\Outils\Terrain::summary` : surface totale = somme des contenances, continuité et
  périmètre extérieur = union des contours élargis puis rétrécis de 15 cm (1 à 17 ms ; une parcelle seule retrouve son
  périmètre cadastral). La parcelle trouvée par le formulaire du haut est ajoutée d'office (événement `tool-parcel`
  émis par `tool-form.js`, qui exporte aussi `REF_FULL`). « Copier les références » : une ligne par parcelle (commune,
  préfixe, section, numéro, surface) + superficie totale. Sélection en bleu (le fond cadastral trace des limites en
  orange). Vérifié : 4 parcelles contiguës = 1 520 m², d'un seul tenant, 171 m ; parcelle éloignée = 2 ensembles ;
  second clic = retrait ; doublon refusé ; 390 px sans débordement ; 0 erreur console.
- **`/reference-cadastrale` recentrée** sur la référence (titre « Référence cadastrale et numéro de parcelle d'une
  adresse ») : les questions de surface vivent sur `/surface-parcelle`, celles du bornage sur `/historique-parcelle`
  (une seule réponse par question sur le site). Liens vers la surface : `/outils`, bouton « Surface d'une parcelle à X »
  des pages communes, plan cliquable des communes (`commune-map.js`), cadastre inversé, guide historique.
- **`/prix-m2`** : titre « Prix des maisons vendues et prix au m² autour d'une adresse », H1 « Prix des maisons vendues
  et prix au m² de votre quartier », bloc « Le prix d'une maison vendue, tel que le publie DVF » dans la section 2 du
  guide (exemple fictif, trois pièges). La page ne montre toujours que des médianes.
- **`/extrait-cadastral`** : section `#matrice` (la matrice indique quel propriétaire possède quelles parcelles, le
  relevé de propriété en est l'extrait ; formulaire « Demande d'extrait de matrice cadastrale », n° 6815-EM-SD, Cerfa
  n° 11565 ; propriétaire = toutes les informations, tiers = sans date ni lieu de naissance ni motifs d'exonération).
  **`/historique-parcelle`** : sections `#division` (géomètre-expert, DMPC, DP ou permis d'aménager, publication par le
  notaire) et `#bornage` (qui le demande, amiable d'abord, procès-verbal, tribunal, frais). Sources dans l'en-tête des
  deux vues.
- Icône `Scissors` déclarée dans les deux blocs d'`app.js`. Déploiement : `git pull` + `npm run build` + `view:clear` +
  `route:clear` + `config:clear` (nouvelle route issue de `config/outils.php`).
- Calculateur : chaque parcelle de la liste affiche « arpentée » ou « non arpentée », avec une phrase sous le résumé.
  Signalé « toutes les parcelles comptées arpentées » : aucun défaut (drapeau `fr_parcels.arpente` identique à Etalab
  sur les 43 779 parcelles de Perpignan ; un lotissement récent est presque entièrement arpenté : section IY 99 %).

### Prix des terrains constructibles (2026-11-05, n° 16 de l'audit SEO complet)

- **Page `/prix-terrain-constructible`** (`PrixTerrainController`, vue `pages/guides/prix-terrain-constructible`, route
  `guide.prix-terrain`) : carte de France des départements (`franceMap`, métropole + DOM), chiffres clés, 10 plus chers
  / 10 moins chers, tableau complet filtrable et triable, communes les plus chères et les plus abordables (au moins
  `CommuneData::LAND_MIN_SALES_RANK` = 30 ventes, liens seulement vers les communes publiées), méthode, « Ce qui fait le
  prix » (exemple mesuré sur le 66 : prix au m² selon la surface du terrain, chiffres dans `$sizeExample`), FAQ, liens.
  Plan du site, `/llms.txt`, menu Ressources et pied de page.
- **Bloc « Prix des terrains à bâtir à … »** sur chaque page commune (dès `communes.min_sales` ventes), comparé au
  département (`CommuneController` : `land`, `depLand`, `landPeriod`).
- 🟥 **Le `median_ppm2` du cache des prix était la moyenne pondérée de médianes MENSUELLES** (bati_type 16 comme
  maisons et appartements), très loin de la médiane sur une petite commune (Les Angles 1 145 €/m² au cache contre 529
  €/m² de médiane réelle sur 30 ventes). Corrigé dans core-city le 2026-11-05 (`fr_price_evolutions.ppm_values`, voir
  `core-city/CLAUDE.md`), effectif en production après recalcul et import de core_city ; la page des terrains garde son
  calcul direct, identique au cache recalculé (Perpignan 306 €/m² sur 242 ventes des deux côtés). `CommuneData::landSalesSql` calcule sur les ventes avec les filtres de core-city
  (codtypbien 21, aucun local, natures 3 / 4 / 5, prix au m² = ceil(valeur / surface du terrain) entre 1 et 50 000,
  une mutation comptée une fois), fenêtre `landWindow()` = 36 derniers mois publiés. `landNational()` = une requête
  GROUPING SETS (départements + communes) avec `SET LOCAL statement_timeout = '120s'` dans sa transaction (le rôle
  `vitrine_ro` est à 15 s), mise en cache 25 h et préparée par `communes:warm`. Contrôle et durée en production :
  `.insights/audit-seo-complet-2026-11-04/mesure-prix-terrains-prod.sql` (dev, 66 seul : 0,12 s). ⚠ `fr_insees` n'a
  pas de `code_department` : passer par `fr_departments` (`department_id`).
- 🟥 **Maisons neuves vendues comme terrain** (production, 2026-11-05) : une maison neuve vendue avant son inscription au
  cadastre apparaît dans DVF comme un terrain à bâtir sans local ; Halluin sortait 1re (1 258 €/m², 45 ventes à ~210 000 €
  pour ~140 m²), Berck 2e (1 033 €/m²). Aucune VEFA en cause (0 en France parmi ces ventes). `CommuneData::landCtes`
  (requête unique de la page et du bloc commune) écarte une vente sur trois signaux : (1) bâtiment de la parcelle
  achevé dans les 3 ans avant l'année de la vente (`fr_parcel_details.annee_construction_max`) ; (2) DPE de logement
  neuf (type 1) daté d'avant la vente (+ 90 j) ; (3) terrain de 500 m² au plus payé au moins 80 % du prix médian d'une
  maison de la commune (au moins 10 ventes de maisons) ET bâtiment au cadastre aujourd'hui (un terrain cher encore nu
  reste un terrain). Production : (1) et (2) repèrent 34 des 46 ventes d'Halluin mais 2 des 31 de Berck (29 restantes
  à 708 €/m² et 250 000 €), d'où (3). Dans le 66, (3) ajoute 122 ventes à 265 000 € en médiane pour 316 m² (Thuir :
  16 « terrains » à 290 885 € pour une maison médiane à 230 000 €) ; département 282 → 270 €/m², Perpignan 306 → 303.
  ⚠ `hm` et `s0` en `MATERIALIZED` : sinon la médiane des maisons est recalculée pour chaque vente (2,4 s → 0,28 s
  pour le 66). Validé en production le 2026-11-05 : Berck 217 €/m² sur 17 ventes (708 avant le 3e signal), Halluin
  167 €/m² sur 11 ; requête nationale 19 s, dont 14,6 s pour le prix médian des maisons de toutes les communes (dans
  la limite de 120 s ; lancer `communes:warm` juste après le déploiement, sinon le premier visiteur attend). Les permis
  (achèvement) ne départagent pas (rattachement trop large).
  **Règle 4, prix symbolique** (production : Colomiers 1 €/m², 25 ventes sur 49 à 1 € avec 6 parcelles par vente) :
  vente écartée si son prix total est sous `LAND_SYMBOLIC_PRICE` (1 000 €). ⚠ Un seuil relatif (5 % de la médiane du
  département) a été essayé puis retiré : il écartait les vrais grands terrains ruraux d'un département cher (Génolhac,
  Gard : 5 000 m² à 8 €/m², 41 500 €) et gardait les mêmes dans un département bon marché (Creuse). Les classements
  affichent la surface médiane des terrains vendus (`surface`), qui explique un prix au m² très bas.
  Bloc commune = classement national pour les 62 communes du 66 à 10 ventes ou plus : une vente qui touche deux
  communes est rattachée à une seule (DISTINCT ON), et le calcul d'une commune part de ses ventes par une CTE `cm`
  jointe (un `IN` faisait calculer toute la France : 230 ms → 50 ms pour Perpignan en dev). 66 : 277 €/m² sur
  2 432 ventes, Perpignan 305 €/m² sur 228. Script de contrôle `mesure-prix-terrains-prod-4.sql` (généré).
  Contrôle final en production (2026-11-05) : 17,5 s ; 3 186 communes avec un bloc, 753 classées ; haut Dinard 1 091,
  Sainte-Marie-de-Ré 1 085, Rennes 791 ; bas Génolhac 9 (5 036 m²), Brandivy 12 (4 306 m²), Capesterre-Belle-Eau 12
  (677 m², nature à confirmer : régularisations foncières possibles).
  ⚠ Mise en ligne : `SitemapController` porte aussi les guides comparatifs d'une autre session
  (`PageController::publishedComparatifGuides`, `config/comparatif_guides.php`) : les déployer ensemble. Saint-Estève (66) à 100 €/m² :
  série réelle de lots vendus pile à 100 €/m² en 2024-2025, pas un défaut. Contrôle en production :
  `.insights/audit-seo-complet-2026-11-04/mesure-prix-terrains-prod-2.sql`, GÉNÉRÉ depuis `landCtes` (régénérer si la
  règle change). La règle n'est PAS encore dans core-city (terrains à bâtir du cache).
- Déploiement : `git pull` + `view:clear` + `route:clear` + `config:clear` + `cache:clear` puis `communes:warm` (en
  www-data) ; « Demander une indexation » de `/prix-terrain-constructible`.

### Vraies médianes des maisons et appartements (2026-11-05)

- Les prix médians des pages communes, départements, quartiers et de la carte de France viennent du cache des prix de
  core-city, dont le « médian » était une moyenne de médianes mensuelles (maisons d'Olette 3 525 €/m² pour une médiane
  réelle de 979). Corrigé à la source (core-city, colonne `fr_price_evolutions.ppm_values`) : rien à changer dans la
  vitrine pour ces pages, il faut recalculer core_city puis l'importer.
- **Courbe annuelle des communes** (`CommuneData::yearlyPrices`) et **outil `/prix-m2`** (`PrixImmobilier::series` et
  `window`, clé de cache `v4`) lisent les prix de chaque vente quand chaque mois les porte (vraie médiane), sinon gardent
  l'ancienne règle ; `CommuneData::hasPpmValues()` teste la colonne (base sans la migration = ancienne règle, pas
  d'erreur). Vérifié en dev : courbe de Perpignan 24 points sur 24 identiques au calcul direct, outil à 0,03-0,07 s,
  chiffres de commune identiques au cache.

### /login, /register et /index.php (2026-11-04, n° 5 et 6 de l'audit SEO complet)

- `public/robots.txt` : `Allow: /login$` et `Allow: /register$` avant `Disallow: /login` / `/register` (règle la plus
  longue gagnante : la page exacte est lisible, `/login/google` et `/login?...` restent bloquées). Titre, description et
  canonique de ces pages : core (`AuthController`).
- `/index.php` → `/` en 301 par `App\Http\Middleware\RedirectIndexPhp` (ajouté en tête du noyau HTTP du core depuis
  `AppServiceProvider::boot`, hors console ; GET et HEAD). ⚠ Le .htaccess ne voit PAS `/index.php` : le vhost envoie
  les `.php` à PHP par `ProxyPassMatch` ; il ne traite que `/index.php/...` (301, répondait 404).

### Bandeau Cookiebot compact et cache des pages /cadastre (2026-11-04, n° 8 et 9 de l'audit SEO complet)

- **n° 8** : `resources/css/app.css`, fin du fichier, `@media (max-width: 1023px)` : le bandeau Cookiebot (chargé par
  Tag Manager) devient un panneau bas d'environ 200 px (titre + 2 lignes qui défilent, 3 boutons sur une ligne, en-tête
  masqué), au lieu d'une fenêtre centrée de 601 px dont le texte était le LCP (5,9-6,6 s en laboratoire). Mesuré en
  mobile simulé (Chrome jetable, profil neuf) : LCP de l'accueil 3,9 s → 1,6 s, /carte-cadastrale 4,5 s → 1,6 s, l'élément
  LCP redevient le hero ; vue « Personnaliser » utilisable (60 % de hauteur, défile) ; ordinateur inchangé. Données
  réelles déjà bonnes (Search Console du 02/11) : gain pour les premiers visiteurs lents. Tag Manager (GTM-TPFLK9MW) :
  Microsoft Clarity déclenché sur `cookie_consent_statistics` (comme Google Analytics) au lieu de Toutes les pages,
  version 35 à publier. ⚠ Si Cookiebot change ses identifiants (`#CybotCookiebotDialog...`), revérifier.
- **n° 9** : `config('communes.cache_minutes')` = 1500 (25 h, `VITRINE_COMMUNES_CACHE_MINUTES`, 60 avant).
  `php artisan communes:warm [--manquant] [--limite=N]` (déclarée dans `AppServiceProvider`) rend /cadastre, les
  départements et toutes les communes publiées en mode recalcul (`CommuneData::refreshMode` : chaque clé recalculée et
  remise en cache une fois par exécution, `flushMemo` entre deux pages) : 628 pages en 21 s en dev, contenu identique.
  À lancer en www-data chaque jour, après chaque import de core_city et après chaque déploiement. `departmentMap`
  trié par code INSEE (ordre des tracés stable d'un recalcul à l'autre). Banc :
  `.insights/audit-seo-complet-2026-11-04/communes-warm-harness.php` 8/8. ⚠ Hors requête HTTP, `$errors` n'est pas
  partagé : la commande le partage avant de rendre les vues.

### Maillage des pages communes (2026-11-04, n° 10 et 11 de l'audit SEO complet)

- `CommuneData::largestByDepartment()` : commune publiée la plus peuplée de chaque département (arrondissements exclus),
  clé de cache liée à la liste des pages publiées (`md5` des codes).
- Page commune : bloc « Grandes villes de la région X » (« Villes des autres départements d'outre-mer » pour un DOM)
  dans la section « voisines », la plus grande ville de chacun des autres départements de la région
  (`CommuneController::regionCities`, appuyé sur `sameRegionDepartments`). Une préfecture seule publiée dans son
  département (Limoges, Le Mans, Ajaccio…) n'était liée que par la page de son département.
- Pages d'outils : bloc des 20 plus grandes villes (`OutilController::bigCities`), ancre selon l'outil :
  `prix-immobilier` « Prix au m² à X », `zone-tendue` « Zone tendue à X », `plu` « PLU de X », sinon « Cadastre de X »,
  plus un lien vers /cadastre. ⚠ Le slug de l'outil des prix est `prix-immobilier` (adresse /prix-m2).
- Liens contextuels des communes : « Zone tendue à X : ce que cela change pour une location » (ancre unique par commune,
  au lieu d'une ancre identique sur 526 pages) et « Prix au m² d'une adresse à X » dans la section des prix.
- Mesure : `.insights/audit-seo-complet-2026-11-04/maillage-communes.py AVANT.json APRES.json` (explorations crawl.py).

### Page CRM immobilier refaite sur le modèle des trois premiers (2026-11-05, n° 37 de l'audit SEO complet)

- **Analyse** : `.insights/audit-seo-complet-2026-11-04/serp-crm-immobilier-2026-11-05.md` (HubSpot, monday.com,
  Capifrance) ; gains page par page : `gains-par-page-2026-11-05.md` (recette à reprendre aux n° 38 et 39).
- **Recette** : définition en deux phrases sous un H2 « Qu'est-ce qu'un … ? », H2 en questions, « comment choisir »
  honnête (encadré « préférez un autre outil si… »), FAQ des questions des internautes avant celles sur le produit,
  auteur et date visibles, un chiffre sourcé, captures, bloc « Sur le même thème ».
- **Composant `components/section/byline`** (réutilisable) : « Par l'équipe Infoparcelle · Mis à jour le … » + JSON-LD
  `Article` (auteur = organisation, lien /a-propos ; aucune personne nommée sans accord). Params `updated`, `headline`,
  `article_description`, `published`, `align`. La page pose aussi `@section('date_modified', …)` (même date).
- **`PageController::planPrices()`** : prix catalogue mensuels Basique / Premium / CRM lus en base, partagés par les
  guides comparatifs et la page CRM (jamais écrits à la main).
- ⚠ **Une vue incluse hérite des variables de la page** : une variable de page nommée comme un paramètre de composant
  (`$link` lu par `feature-split`, `$faq` fourni par `vitrineData`) le casse. Préfixer les variables de page.
- **Une page par intention** : « crm immobilier » → /crm-immobilier ; « logiciel immobilier », « meilleur crm » → le
  comparatif (titre « Logiciel immobilier : comparatif de 9 CRM (2026) ») ; « logiciel agent immobilier » et « logiciel
  agence immobilière » → les pages métiers. Les deux guides comparatifs portent désormais un JSON-LD `Article`.
- Vérifié : 65 pages du plan du site en 200, 184 blocs JSON-LD valides, 0 erreur console, 390 px sans débordement.
  Déploiement : `git pull` + `npm run build` + `view:clear`, puis « Demander une indexation » des deux pages.
- **N° 38 (même jour) : la recette sur 15 pages.** Gabarit des outils `pages/outils/show.blade.php` : auteur et date
  (`byline`), définition sous un H2 en question et « Sur le même thème », pilotés par trois clés facultatives de
  `resources/outils/{slug}.php` : `updated` (AAAA-MM-JJ, pose aussi `date_modified`), `definition` (['question',
  'answer'], texte simple) et `related` (liens vers guides et pages produit, pas vers d'autres outils, déjà listés
  plus bas). Une définition reprend les faits déjà publiés dans la FAQ et le guide de l'outil. Guides extrait cadastral,
  historique et prix des terrains : auteur, date, H2 en questions ; page « Localiser une annonce » : auteur, date,
  définition ; accueil : « Le cadastre, en deux questions » avant la FAQ (le haut de page du 2026-11-04 n'est pas
  touché). ⚠ Mettre à jour `updated` quand le contenu d'un outil change réellement (date affichée).
- **N° 39 (même jour) : la recette sur 17 pages produit, métiers et développeurs.** Composant
  **`components/section/page-intro`** (byline + H2 en question + réponse ; params `updated`, `headline`,
  `article_description`, `intro_question`, `intro_answer`, `intro_note` HTML) inclus juste après le fil d'Ariane, et
  `@section('date_modified', …)` dans chaque page. Sous-pages API : clés `updated` et `definition` dans
  `config/api_datasets.php`. Titres revus pour une page par intention (pages métiers, MCP « IA immobilier », widgets,
  gestion d'agence, application). Prestataires : section « Trois métiers, trois recherches » (le filtre « Piscine sur la
  parcelle », sa forme et sa surface existent dans la table `filters` de l'application). /api a désormais une FAQ.
- **Relecture des n° 37 à 39 (même jour)** : un seul composant `components/section/page-intro` pour toutes les
  introductions (34 pages ; sans `intro_question`, il n'affiche que la ligne d'auteur), encadré « En bref » au style
  des guides comparatifs, ligne d'auteur avec le logo (`img/icons/android-chrome-192x192.png`) sur deux lignes.
  `Article` : auteur = `#organization` avec l'url d'accueil (même valeur que l'entité du layout), image, `datePublished`
  seulement si `published` est fourni. ⚠ Une carte placée dans une grille CSS a besoin de `min-w-0` pour qu'un
  `truncate` agisse (sinon elle prend la largeur de sa plus longue ligne : débordement des classements de communes de
  /prix-terrain-constructible à 390 px, corrigé). L'encart « Pour aller plus loin » des outils est un paragraphe, pas
  un H3 (il précédait le premier H2).

### Rubrique Ressources et guides métier (2026-11-05, n° 36 de l'audit SEO complet)

- **`/ressources`** (`PageController::ressources`, vue `pages/ressources`) : tous les guides par thème (« Réglementation et mandats », « Cadastre et foncier », « Comparatifs et études » avec les guides comparatifs publiés), date de mise à jour et durée de lecture, `ItemList`. Source : **`config/ressources.php`** (`groups`, `guides` : url, group, title, card, icon, updated, minutes ; `updated` vide = date lue ailleurs, ex. `locate_benchmark.measured_at` pour l'étude). Toute nouvelle page de guide s'y déclare, puis dans le plan du site (`SitemapController`, liste + `pageLastmod`) et `/llms.txt`.
- **Gabarit `layouts/guide`** : le guide pose `$g` dans un bloc `@php` en tête de fichier puis `@extends('layouts.guide')` et `@section('guide_body')` (H2 avec `id` et `scroll-mt-32`, ids repris par `$g['toc']`). Clés : slug, title, description, h1, subtitle, subtitle_short, badge_icon, screenshot, alt, plan_note, crumb, headline, intro_question, intro_answer, published, updated, toc, product (title, text, points, url, url_label), faq, sources, related_links. Rendu : `hero-pillar`, fil d'Ariane, `page-intro` (auteur, date, `Article`), sommaire collant (masqué sur téléphone, repli `<details>`), article `prose`, encart « Dans Infoparcelle », FAQ (`FAQPage`), sources avec « Textes relus le … », guides du même thème + `related_links`, bandeau d'essai. ⚠ **Jamais de directive Blade dans un commentaire** de ce gabarit ou d'un guide : `@php` dans `{{-- --}}` ouvre un vrai bloc PHP (500 « Undefined variable $relatedLinks » rencontré).
- **4 guides** (`pages/guides/`) : `/loi-hoguet`, `/carte-professionnelle-immobiliere`, `/mandat-de-recherche`, `/mandat-simple-ou-exclusif` (une seule page pour « mandat simple » 210 et « mandat exclusif » 260 recherches, sinon elles se concurrencent ; le semi-exclusif présenté comme un usage, pas une catégorie légale). Textes relus sur Légifrance le 2026-11-05, articles cités dans l'en-tête de chaque vue ; tarifs des CCI volontairement non chiffrés (fiche service-public datée) ; aucune promesse juridique (« ne remplace pas le conseil d'un juriste »). Faits produit : guides d'aide `crm/documents-de-vente` et `crm/honoraires` de l'application, `MandateExpiryAlertCommand` (J-30, J-7).
- **Menu** : colonne « Guides métier » de Ressources (4 guides), « Guides » devient « Guides cadastre », « Développeurs et entreprise » regroupe API, MCP, application, partenaires, À propos, contact ; lien « Tous les guides et ressources ». Pied de page : « Tous les guides » et « Loi Hoguet » en tête de la colonne Guides.
- **Maillage** : section Hoguet de /documents-et-conformite (carte professionnelle + phrase vers les 4 guides), texte « Du mandat au compromis » de /crm-immobilier, 3 cartes de guides sur /metiers/agence-reseau (titre du bloc corrigé : `block_title`), bloc « Guides pour les agents immobiliers » sur /metiers/agent-immobilier.
- **Assistants IA** : les 4 guides dans `PageController::MARKDOWN_PAGES` (`/{guide}.md`, `/llms-full.txt` passe de 11 à 15 pages), lignes dans `/llms.txt`.
- **En production le 2026-11-05, vérifié** (5 pages en 200, styles compilés servis, plan du site, versions `.md`, liens depuis les 4 pages) ; indexation demandée, à recontrôler (`/ressources` indexée le jour même, les 4 guides pas encore connus de Google).
- Vérifié : 70 pages du plan du site en 200, 0 « — », JSON-LD `FAQPage` + `Article`, images de partage, 1 440 et 390 px sans débordement, 0 erreur console. Déploiement : `git pull` + `npm run build` + `view:clear` + `route:clear` + `config:clear` ; puis « Demander une indexation » de `/ressources` et des 4 guides, et IndexNow. Mesure à 4 et 8 semaines (requêtes, positions, inscriptions par page d'arrivée).

### Mentions légales (2026-11-05)

- **`/mentions-legales`** (`PageController::mentionsLegales`, vue `pages/legal/mentions-legales`) : éditeur Rootsud SAS (SAS au capital de 2 400 €, RCS Perpignan 888 759 982, SIREN, TVA FR 24 888759982, 13 Boulevard du Conflent à Perpignan), téléphone et e-mail lus dans `$site->setup` (téléphone mis au format « 09 72 16 74 63 »), directeur de la publication, hébergeur OVH SAS (adresse + site, aucun téléphone : OVH n'en publie pas dans ses mentions légales), propriété intellectuelle, renvois vers CGU, CGV, confidentialité, cookies et contact. Mêmes valeurs que l'article 2 des CGU et `rs_rootsud_application/config/rootsud.php` : toute évolution de l'identité se répercute aux trois endroits.
- **Pied de page** : lien « Mentions légales » en tête des liens légaux, et « © Infoparcelle, édité par Rootsud SAS (RCS Perpignan 888 759 982) » sur toutes les pages. Plan du site : entrée `/mentions-legales`.
- Raison : refus de la fiche La Fabrique du Net le 2026-11-05 (« votre site ne présente pas de mentions légales », raison sociale et immatriculation introuvables), alors que les CGU les portaient. Un modérateur ou un annuaire cherche une page nommée « Mentions légales » liée depuis le pied de page.
