@font-face{font-family:'Archivo';src:url('/fonts/Archivo-Variable.woff2') format('woff2');font-weight:100 900;font-display:swap}
@font-face{font-family:'Inter';src:url('/fonts/Inter-Variable.woff2') format('woff2');font-weight:100 900;font-display:swap}
:root{
--ink:#14213D;--paper:#FAF8F3;--accent:#1D4ED8;
--ink-doux:rgba(20,33,61,.72);--ink-faible:rgba(20,33,61,.62);
--trait:rgba(20,33,61,.12);--voile:rgba(20,33,61,.04);
--r-carte:14px;--r-puce:999px;
/* Les chiffres en Inter, chasse tabulaire, comme dans l'application : le zéro pointé de
Plex Mono a été retiré le 2026-09-11 (décision de Vincent). Chaque règle de nombre garde
font-variant-numeric:tabular-nums, qui aligne les colonnes. */
--police-chiffres:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
line-height:1.6;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
main{max-width:760px;margin:0 auto;padding:1.75rem 1.25rem 4rem}
/* ⚠️ UNE SEULE ÉCHELLE, FLUIDE. Les feuilles mélangeaient tailles fixes et clamp : selon la
page, un titre passait de 2rem à 1,5rem sur le même téléphone. */
h1,h2,h3{font-family:Archivo,Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-weight:800;letter-spacing:-.02em;text-wrap:balance}
h1{font-size:clamp(1.6rem,4.2vw,2.15rem);line-height:1.15;margin:.4rem 0 .5rem}
h2{font-size:clamp(1.15rem,2.4vw,1.4rem);line-height:1.25;margin:2.25rem 0 .7rem}
h3{font-size:1.05rem;line-height:1.3;margin:1.5rem 0 .4rem}
/* Le chapô : plus grand que le corps, moins appuyé que le titre. Il portait cinq traitements. */
.sub{color:var(--ink-doux);font-size:1.05rem;margin:0 0 1.25rem;max-width:62ch}
p{max-width:70ch}
a.brand{color:var(--accent);font-weight:700;text-decoration:none}
.liens{color:var(--ink-doux)}
.liens a,.sub a{color:var(--accent);text-decoration:none;border-bottom:1px solid transparent}
.liens a:hover,.sub a:hover{border-bottom-color:currentColor}
/* ⚠️ CHIFFRES TABULAIRES PARTOUT. Une colonne de nombres qui ne s'aligne pas se lit deux fois
plus lentement — et ces pages ne servent qu'à comparer des nombres. */
table{width:100%;border-collapse:collapse;margin:1.25rem 0;font-variant-numeric:tabular-nums;font-family:var(--police-chiffres)}
th,td{text-align:left;padding:.65rem .35rem;border-bottom:1px solid var(--trait)}
th[scope=row]{color:var(--ink-doux);font-weight:500;width:45%}
/* ── LES TABLEAUX LIBELLE / VALEUR SUR TELEPHONE (2026-09-06) ────────────────────────────
Mesure sur /verite a 375 px : la colonne des libelles faisait 90 px et coupait chaque phrase
en quatre lignes, la valeur en face en trois. Sous 560 px, chaque paire devient deux rangees
— le libelle au-dessus, la valeur dessous — comme les lignes de palmares plus bas. La regle
ne vise que les tableaux qui la demandent (class paires) : un tableau a quatre colonnes
avec en-tetes de ligne n'a rien a y gagner. Aucun accent grave ici : gabarit de chaine. */
@media (max-width:560px){
table.paires tr{display:block;padding:.55rem 0;border-bottom:1px solid var(--trait)}
table.paires th[scope=row],table.paires td{display:block;width:auto;border:0;padding:.1rem 0}
table.paires th[scope=row]{font-size:.85rem}
}
.cta{display:inline-block;margin:1.5rem 0;background:var(--accent);color:#fff;text-decoration:none;
padding:.8rem 1.35rem;border-radius:var(--r-puce);font-weight:600;
box-shadow:0 1px 2px rgba(20,33,61,.16)}
.cta:hover{filter:brightness(1.07)}
.note{color:var(--ink-faible);font-size:.85rem;margin-top:2rem;max-width:70ch}
/* ── LE MENU DE L'EN-TÊTE STATIQUE ────────────────────────────────────────────────────────
⚠️ IL VIT ICI, PAS DANS LE GABARIT. Un élément details a besoin d'un état ouvert et d'un
panneau positionné, que l'attribut style ne sait pas exprimer — j'ai d'abord posé une
feuille locale dans prerender.mjs, et le verrou du socle l'a refusée.
⚠️ ET AUCUN ACCENT GRAVE DANS CE COMMENTAIRE : ce socle EST un gabarit de chaîne, une paire
d'accents graves le referme. Deuxième fois en dix minutes. Il a raison : deux feuilles,
c'est deux vérités, et c'est ce défaut-là qui a rendu /qualite-loyer étrangère au reste.
⚠️ AUCUN JAVASCRIPT. Le menu s'ouvre au clic ET au clavier, se lit par un lecteur d'écran et
s'imprime déplié. C'est la raison d'être de cet en-tête : ces 582 pages sont faites pour être
légères et indexables. */
.menu-stat{position:relative;flex-shrink:0}
.menu-stat>summary{list-style:none;cursor:pointer;font-size:.86rem;color:var(--ink);
padding:.24rem .55rem;border-radius:var(--r-puce);border:1px solid var(--trait);white-space:nowrap}
.menu-stat>summary::-webkit-details-marker{display:none}
/* ⚠️ UNE QUESTION DE FAQ EST UN TITRE, ET NE DOIT PAS EN AVOIR L'AIR. La spec HTML autorise un
h1–h6 comme enfant unique d'un summary ; c'est ce qui fait entrer les questions dans le plan
de la page — sans lui, un lecteur d'écran qui parcourt les titres les saute toutes. Laissé en
bloc, il pousserait le triangle de dépliement à la ligne et grossirait le texte. On lui retire
sa mise en page, pas son rôle. Vérifié dans un navigateur : summary identique au pixel, et
l'arbre d'accessibilité passe de « generic » à « heading ». */
summary h3{display:inline;font-size:inherit;font-weight:inherit;margin:0}
.menu-stat>summary:hover,.menu-stat[open]>summary{border-color:var(--accent);color:var(--accent)}
.menu-stat>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.menu-stat>nav{position:absolute;right:0;top:calc(100% + .45rem);z-index:20;width:19rem;
max-width:92vw;background:#fff;border:1px solid var(--trait);
border-radius:var(--r-carte);padding:0 0 .5rem;box-shadow:0 12px 32px -14px rgba(20,33,61,.35)}
/* ── LE MENU A TROIS ÉTAGES, COMME CELUI DE L'APPLICATION ────────────────────────────────
⚠️ SIGNALÉ PAR VINCENT LE 2026-09-04 : « on a encore du vieil affichage de menu quand on
est dans une page du menu ». La table NAVIGATION porte un groupe et une icône par entrée
depuis le 03/09 ; le prérendu n'en lisait que le lien et le libellé, et cette feuille ne
savait dessiner qu'une pile de liens. Le contenu était pourtant déjà verrouillé — c'est la
FORME qui divergeait, et le site changeait de peau quand on suivait un lien.
⚠️ LES RÈGLES VIVENT ICI, PAS DANS UN BLOC PORTÉ PAR L'EN-TÊTE. Mon premier jet en émettait
un ; vitrineCss.test.mjs l'a refusé — « n'ouvre aucune feuille de style sans lui » — et il
avait raison : c'est ainsi qu'on se retrouve avec onze feuilles et cinq définitions de h1.
⚠️ ET AUCUN ACCENT GRAVE DANS CE COMMENTAIRE : il vit dans un gabarit de chaîne, qu'un seul
accent grave referme. Le mien a cassé le module au premier essai. */
.menu-titre{margin:0;padding:.55rem .8rem .15rem;font-size:11px;font-weight:600;
text-transform:uppercase;letter-spacing:.04em;color:var(--ink-faible)}
.menu-grille{display:grid;gap:2px;padding:0 .5rem}
.menu-ligne{display:flex;align-items:flex-start;gap:6px;padding:.35rem .5rem;border-radius:8px;
text-decoration:none;color:var(--ink);min-height:24px}
.menu-ligne:hover{background:rgba(29,78,216,.08)}
.menu-ligne>svg{flex-shrink:0;margin-top:2px;color:var(--accent)}
.menu-l{display:block;font-size:13px;font-weight:500;line-height:1.25}
.menu-h{display:block;font-size:11px;line-height:1.25;color:var(--ink-faible)}
/* Le groupe sans titre (A propos, seul) se separe des autres par un filet, pas par un mot. */
.menu-sep{border:0;border-top:1px solid var(--trait);margin:.45rem 0 .35rem}
/* Sous 560 px, « Méthode » cède la place : le menu la contient déjà. */
@media (max-width:560px){.entete-plus{display:none}}
/* Sous 560 px, « Mon classement » cède aussi : le menu le porte, et c'est lui qui faisait
sortir l'en-tête de l'écran (532 px pour 375, mesuré le 05/09). L'application, sur le même
téléphone, ne montre que la marque, la recherche et le menu — même grammaire. */
@media (max-width:560px){.entete-classement{display:none}}
/* Sous 560 px, la marque se reduit a son REGARD — comme l application, dont ui/Logo.tsx dit
« symbole seul (favicon, tuile PWA, header mobile) ». Mesure au simulateur iOS le 09/09 : le
mot cotait 70 px a un en-tete qui tronquait deja son propre champ (« Chercher une commun… »).
⚠️ CLIP ET NON display:none — le nom doit rester extractible (recherche dans la page,
copier-coller), exactement comme le oo dessine de logoSvg.mjs.
⚠️ ET AUCUN ACCENT GRAVE DANS CE COMMENTAIRE : il vit dans un gabarit de chaine, qu un seul
accent grave refermerait. Le mien a casse le module au premier essai — exactement ce que
l en-tete de prerender.mjs annonce, et que je n avais pas lu. */
@media (max-width:560px){
.marque-mot{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
}
/* ── LES LISTES SUR TÉLÉPHONE : RANG, NOM, NOTE — PUIS LIEU, PRIX (2026-09-10) ─────────────────
« Droit au but en version mobile » (Vincent). Mesuré à 375 px : une ligne de palmarès faisait
132 px sur trois étages (le nom, puis « 77 · 22 279 hab », puis la note, sa barre et un prix
cassé en « 1 944 » / « €/m² ») — cinq communes par écran, 8,9 écrans pour trente. Material 3
fixe une ligne double à 72 dp ; le Top Charts de l App Store et Niche tiennent un classement
en deux lignes. Ici : rang, nom et note sur la premiere ; département, habitants et prix sur
la seconde. La barre disparaît (le nombre la dit), les pastilles aussi (la fiche les porte).
Règle partagée par les six gabarits de lignes : palmarès régional et national, fraîcheur,
meilleures de France, pages par critère, collèges.
⚠️ html body EN TÊTE DE CHAQUE SÉLECTEUR, ET C EST UNE MESURE : ce socle passe AVANT les
feuilles de page, qui redéfinissent .row, .score et .price à spécificité égale — sans ce
préfixe, la règle perdait au premier gabarit qui écrit sa grille.
Aucun accent grave ici : gabarit de chaîne. */
@media (max-width:560px){
html body .row{grid-template-columns:2rem minmax(0,1fr) auto;column-gap:.6rem;row-gap:0;align-items:center;padding:.6rem 0}
html body .row .rank{align-self:start}
html body .row .who{grid-column:2;grid-row:1/span 2}
html body .row .score{grid-column:3;grid-row:1;justify-self:end;margin:0;gap:0}
html body .row .price,html body .row .val{grid-column:3;grid-row:2;justify-self:end;text-align:right;margin:0}
html body .row:not(:has(.score)) .price,html body .row:not(:has(.score)) .val{grid-row:1/span 2}
html body .row .bar,html body .row .pills,html body .row .why{display:none}
html body .row .price .u,html body .row .val .u{display:inline;margin-left:.2rem}
html body .row .su,html body .row .who .sub{font-size:.82rem}
html body .rbanner,html body .eyebrow{display:none}
html body .crumb,html body .bc,html body .pickhint,html body .card>.cn{font-size:.82rem}
html body .card>.sc{display:none}
html body .row .who .sub{margin:0}
html body .pad{padding:1rem 1.1rem}
html body .ft{font-size:.82rem}
}
/* ── UNE VIGNETTE PAR LIGNE, UN SEUL GABARIT DU RANG 1 AU RANG 30 (2026-09-13) ─────────────────
Decision de Vincent apres audit : Niche et Livability donnent une image a chaque commune d un
classement, au telephone comme au bureau, et ne changent jamais de gabarit apres la troisieme.
La vignette est la scene peinte au build (format carte, environ 12 ko), chargee a la demande
(loading lazy) : trente lignes ne se telechargent pas d un coup.
Les lignes .dup sont les trois premieres, deja montrees par le podium au bureau : elles ne
s affichent qu au telephone, ou le podium se masque (podium.mjs).
Aucun accent grave : gabarit de chaine. */
.row .vg{display:block;width:4.4rem;height:2.5rem;border-radius:8px;overflow:hidden;background:rgba(20,33,61,.08);align-self:center;flex:none}
.row .vg img{display:block;width:100%;height:100%;object-fit:cover;object-position:50% 100%}
.row .why .faible{color:var(--muted)}
/* La lettre A a E devant la note : la seule couleur de la ligne, et elle informe. */
.row .score .sg{display:inline-flex;align-items:center;justify-content:center;min-width:1.35rem;height:1.35rem;padding:0 .25rem;border-radius:6px;font-size:.75rem;font-weight:800;line-height:1;flex:none}
@media (min-width:561px){html body .row.dup{display:none}}
@media (max-width:560px){
html body .row:has(.vg){grid-template-columns:1.5rem 3.6rem minmax(0,1fr) auto;display:grid}
html body .row:has(.vg) .rank{grid-column:1;grid-row:1/span 2}
html body .row .vg{grid-column:2;grid-row:1/span 2;width:3.6rem;height:2.5rem}
html body .row:has(.vg) .who{grid-column:3;grid-row:1/span 2}
html body .row:has(.vg) .score{grid-column:4;grid-row:1}
html body .row:has(.vg) .price,html body .row:has(.vg) .val{grid-column:4;grid-row:2}
html body .row:has(.vg):not(:has(.score)) .price,html body .row:has(.vg):not(:has(.score)) .val{grid-row:1/span 2}
/* Le pourquoi du rang revient au telephone, en une ligne : la moitie plus faible se tait. */
html body .row .who .pills:has(.why){display:block;margin-top:.1rem}
html body .row .who .pills .why{display:inline;font-size:.8rem;color:var(--muted)}
html body .row .who .pills .why .faible{display:none}
html body .row .who .pills .why .axe{font-weight:600;color:var(--ink-doux)}
/* Les trois premieres ont le meme marqueur que les suivantes : le numero, a l encre (11/09). */
html body .row.top .rank{background:none;color:rgba(20,33,61,.45);width:auto;height:auto;line-height:1.45;font-size:1rem;justify-self:end}
html body li.row.top{background:none}
/* La note porte sa lettre de couleur au telephone aussi. */
html body .row .score .sg{display:inline-flex;margin-right:.3rem}
}
.rbanner picture{display:block}
/* Le lien retour pose par public/retour.js quand on arrive du classement ou d une fiche. */
.retour{margin:.1rem 0 .5rem;font-size:.9rem}
.retour a{display:inline-flex;align-items:center;min-height:24px;color:var(--accent);font-weight:600;text-decoration:none}
.retour a:hover{text-decoration:underline}
/* ── LE HAUT DE PAGE AU TÉLÉPHONE : LE TITRE, UNE LIGNE, ET LE RESTE SE DEMANDE (2026-09-10) ──
Mesuré à 375 px sur un classement par critère : bandeau, fil, titre, chapô de quatre lignes,
ligne de sources, phrase de synthèse, paragraphe de méthode de cinq lignes — le n°1 à 720 pt
sur 874. Divulgation progressive (Nielsen Norman Group) : ce qu on vient chercher d abord,
ce qui explique ensuite, à la demande. Le repli est ouvert par défaut, donc sur écran large
il se lit comme avant et son titre ne s affiche pas ; repli-mobile.js le ferme sous 560 px.
Sans JavaScript il reste ouvert — rien n est jamais perdu. */
details.calcul>summary{display:none}
@media (max-width:560px){
details.calcul>summary{display:flex;flex-wrap:wrap;align-items:baseline;gap:.15rem .6rem;list-style:none;cursor:pointer;font-size:.85rem;color:var(--ink-doux);margin:.1rem 0 .7rem;min-height:24px}
details.calcul>summary::-webkit-details-marker{display:none}
details.calcul>summary .lire{color:var(--accent);font-weight:600}
details.calcul>summary .lire::after{content:" +"}
details.calcul[open]>summary .lire::after{content:" −"}
details.calcul>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
}
/* ── LES PAGES LONGUES ANNONCENT LEUR PLAN (2026-09-06) ──────────────────────────────────
/trajet : 986 mots, sept sections, aucun sommaire ; /a-propos : huit. Le sommaire se pose
sous le chapo, en deux colonnes sur ecran large — la table de Linear, le « Contents » d'Our
World in Data. Aucun accent grave ici : gabarit de chaine. */
.sommaire{margin:1rem 0 1.5rem;padding:.8rem 1rem .6rem;border:1px solid var(--trait);border-radius:var(--r-carte);background:#fff}
.sommaire-t{margin:0 0 .3rem;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-faible)}
.sommaire ol{margin:0;padding-left:1.2rem;font-size:.92rem;line-height:1.75;columns:2;column-gap:2rem}
.sommaire li{break-inside:avoid}
.sommaire a{color:var(--ink);text-decoration:none;border-bottom:1px solid transparent}
.sommaire a:hover{color:var(--accent);border-bottom-color:currentColor}
@media (max-width:560px){.sommaire ol{columns:1}}
/* Les 22 zones du calcul : des jetons qu'on balaie, plus un paragraphe gris qu'on saute. */
.jetons{list-style:none;margin:.6rem 0 1rem;padding:0;display:flex;flex-wrap:wrap;gap:.4rem}
.jetons li{font-size:.85rem;line-height:1.3;padding:.3rem .7rem;border:1px solid var(--trait);border-radius:var(--r-puce);background:#fff;color:var(--ink)}
/* Le schema porte-a-porte : un SVG qui prend la largeur, une legende en dessous. */
.schema{margin:1.1rem 0 1.4rem;padding:.9rem 1rem .6rem;border:1px solid var(--trait);border-radius:var(--r-carte);background:#fff}
.schema svg{display:block;width:100%;height:auto}
.schema figcaption{margin-top:.4rem;font-size:.85rem;color:var(--ink-faible)}
/* ── LE PLANCHER DE 13 px, ENFIN APPLIQUÉ AUX PAGES PRÉRENDUES (2026-09-07) ────────────────
L'application pose ce plancher depuis le 05/09 : sous 640 px, aucun texte courant ne descend
sous 13 px. Les 563 palmarès, les 481 classements par critère et les 34 894 fiches ne l'ont
JAMAIS eu — leur feuille de style vit ici, et le verrou echelleTexteMobile ne lit que src/.
Le défaut est donc revenu sans que rien ne tombe.
Mesuré sur un palmarès à 390 px : 105 fragments sous 13 px, dont 54 entre 9,9 et 10,9 px —
les unités « /100 » et « €/m² », une paire par ligne de classement — plus douze à 11 px dans
le pied et dix-neuf à 12. Le plancher d'Apple est 11 pt (environ 15 px), celui de Material
14 sp ; 10 px pour l'unité d'un nombre est en dessous de tout.
ON RELÈVE, ON NE REDESSINE PAS. Ces règles vivent dans six gabarits et dans le prérendu à des
dizaines d'endroits : les corriger une par une aurait laissé passer la prochaine.
ET LE BLOC GAGNE PAR LA SPÉCIFICITÉ, PAS PAR L'ORDRE — c'est la subtilité qui aurait rendu ce
correctif silencieusement inopérant. Le socle passe AVANT les feuilles de page, et c'est voulu
(une page qui a une raison de diverger doit pouvoir l'écrire). À spécificité égale, le .price
.u de la page l'emporterait donc sur le mien. Le préfixe body porte chaque sélecteur d'un
cran : il gagne où qu'il soit placé, et il ne gagne QUE sous 640 px.
ET LA FICHE N'A JAMAIS EU DE DÉFAUT ICI, CONTRAIREMENT À CE QUE J'AI RAPPORTÉ LE 07/09. Mon
relevé comptait 38 fragments sous 13 px : ils appartiennent tous à la version IMPRIMABLE
(classe hidden print:block, tailles en points — 8pt vaut 10,67 px), invisible à l'écran. Le
script de mesure ne vérifiait pas la visibilité. À l'écran, la fiche en compte ZÉRO.
IL RESTE UN CAS QUE CE BLOC NE PEUT PAS PRENDRE : la ligne de preuve du pied, dont la taille
de 12 px est écrite en STYLE EN LIGNE dans le prérendu. Un style en ligne bat toute feuille,
media query comprise — seule la source peut le corriger, et c'est fait au même endroit que
ce bloc plutôt qu'ici.
ET J'AI RANGÉ LE TAMPON « ILLUSTRATION GÉNÉRÉE » DANS LES EXCEPTIONS SANS LES RELIRE. Le
verrou de l'application n'en nomme que deux — la classe DPE au centre d'une bande, le « i »
dans un cercle de 15 px — et sa définition dit : « Ce ne sont pas des textes : ce sont des
lettres employées comme dessins, à l'intérieur d'une forme qui ne peut pas grandir avec
elles. » Le tampon est une phrase de deux mots, à lire, sur un bandeau qui peut parfaitement
l'accueillir plus grande. Il monte donc à 13 px comme le reste : une mention de provenance
qu'on ne lit pas ne prouve rien.
ET LE SUR-TITRE « MÉTHODE » ÉTAIT PLUS PETIT QUE SON PROPRE TEXTE — 11,2 px pour un h2 au-
dessus d'un corps à 12,8. Les capitales espacées justifient un corps réduit, pas une
hiérarchie inversée. */
@media (max-width:640px){
body .price .u,body .val .u,body .su,body .p-note small,body .p-prix small,body .menu-titre,body .menu-h,body .sommaire-t,body .crumb,body .eyebrow,body .meta,body .method,body .method p,body .method h2,body .links a,body .rbanner .stamp,body .cap,body .p-pourquoi{font-size:13px;line-height:1.35}
}
/* Un dépli de référence : le résumé se lit comme un titre de section, le chevron dit qu'il
s'ouvre. Le contenu reste dans le DOM fermé — un details fermé est indexé. */
.deplie{margin:1.1rem 0 1.4rem;border:1px solid var(--trait);border-radius:var(--r-carte);background:#fff}
.deplie>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem;font-weight:700;font-size:1.02rem;color:var(--ink)}
.deplie>summary::-webkit-details-marker{display:none}
.deplie>summary::before{content:"";width:.45rem;height:.45rem;border-right:2px solid var(--accent);border-bottom:2px solid var(--accent);transform:rotate(-45deg);transition:transform .12s}
.deplie[open]>summary::before{transform:rotate(45deg)}
.deplie>summary:hover{color:var(--accent)}
.deplie>*:not(summary){padding:0 1rem 1rem}
/* Une cible tactile ne descend pas sous 24 px (WCAG 2.5.8, AA) — déjà corrigé côté application. */
a{min-height:24px}
@media (prefers-reduced-motion:no-preference){a{transition:color .15s,border-color .15s}}
