SPB Git

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

refonte v2 « terminal éditorial » : hero encre pleine largeur, recherche centrale CTA rouge, graphique marché multi-séries interactif (palette CVD validée), flux de ventes prix-d'abord, rapport de valeur recomposé (grands chiffres, sections à filets), méthodologie article + chaîne de calcul, header compact, zéro ombre décalée/carte superflue

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 25, 2026) parent 8279c57 Browse files

19 changed files +1,398 −803

modified src/app/HomeClient.tsx +327 −207
@@ -1,9 +1,10 @@
1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai 1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 "use client"; 2 "use client";
3+import { useState } from "react";
3 import Link from "next/link"; 4 import Link from "next/link";
4 import SearchBox from "@/components/SearchBox"; 5 import SearchBox from "@/components/SearchBox";
5 import EstimateForm from "@/components/EstimateForm"; 6 import EstimateForm from "@/components/EstimateForm";
6−import { IndexSpark, locNum } from "@/components/MetricViz"; 7+import { locNum, MarketPulseChart } from "@/components/MetricViz";
7 import { useLang } from "@/components/LangContext"; 8 import { useLang } from "@/components/LangContext";
8 9
9 /** Indice de marché d'un grand type — série + variations (calculé côté serveur). */ 10 /** Indice de marché d'un grand type — série + variations (calculé côté serveur). */
@@ -31,21 +32,57 @@ const PULSE_LABELS: Record<PulseItem["type"], { fr: string; en: string }> = {
31 condo: { fr: "Condo", en: "Condo" }, 32 condo: { fr: "Condo", en: "Condo" },
32 plex: { fr: "Plex", en: "Plex" }, 33 plex: { fr: "Plex", en: "Plex" },
33 }; 34 };
35+/* attribution FIXE des couleurs de série (jamais recyclée) */
36+const PULSE_COLORS: Record<PulseItem["type"], string> = {
37+ unifamilial: "var(--chart-1)",
38+ condo: "var(--chart-2)",
39+ plex: "var(--chart-3)",
40+};
34 41
35−function TrendBadge({ pct, label }: { pct: number | null; label: string }) { 42+/** Variation signée — l'encre porte le positif, le rouge danger le négatif. */
36− if (pct == null) return null; 43+function Pct({ v, digits = 1 }: { v: number | null; digits?: number }) {
44+ if (v == null) return <span className="text-ink-3">—</span>;
37 return ( 45 return (
38− <span 46+ <span className={v >= 0 ? "text-ink" : "text-[var(--danger)]"}>
39− className={`vp-mono rounded-[4px] border border-ink px-1.5 py-0.5 text-[10px] font-bold ${ 47+ {v >= 0 ? "+" : "−"}
40− pct >= 0 ? "bg-lime text-ink" : "bg-danger text-white" 48+ {Math.abs(v).toFixed(digits)}&nbsp;%
41− }`}
42− >
43− {label} {pct >= 0 ? "+" : ""}
44− {pct.toFixed(1)} %
45 </span> 49 </span>
46 ); 50 );
47 } 51 }
48 52
53+/** En-tête de section éditorial : filet fort, numéro mono rouge, titre display. */
54+function SectionHead({
55+ num,
56+ kicker,
57+ title,
58+ lede,
59+}: {
60+ num: string;
61+ kicker: string;
62+ title: string;
63+ lede?: string;
64+}) {
65+ return (
66+ <header className="sec">
67+ <div className="flex flex-wrap items-baseline gap-x-4 gap-y-1">
68+ <span className="sec-num">
69+ {num} — {kicker}
70+ </span>
71+ </div>
72+ <h2 className="vp-display mt-2 max-w-3xl text-[clamp(24px,3.6vw,36px)] font-bold uppercase leading-[1.02] tracking-[-0.03em]">
73+ {title}
74+ </h2>
75+ {lede && <p className="mt-3 max-w-2xl text-[14.5px] leading-relaxed text-ink-2">{lede}</p>}
76+ </header>
77+ );
78+}
79+
80+const PERIODS = [
81+ { key: "12", months: 12, fr: "12 mois", en: "12 mo" },
82+ { key: "24", months: 24, fr: "24 mois", en: "24 mo" },
83+ { key: "all", months: null as number | null, fr: "Depuis 2021", en: "Since 2021" },
84+] as const;
85+
49 export default function HomeClient({ 86 export default function HomeClient({
50 pulse, 87 pulse,
51 recent, 88 recent,
@@ -57,6 +94,7 @@ export default function HomeClient({
57 }) { 94 }) {
58 const { lang, t } = useLang(); 95 const { lang, t } = useLang();
59 const fr = lang === "fr"; 96 const fr = lang === "fr";
97+ const [period, setPeriod] = useState<(typeof PERIODS)[number]["key"]>("24");
60 const money = (v: number) => 98 const money = (v: number) =>
61 new Intl.NumberFormat(fr ? "fr-CA" : "en-CA", { 99 new Intl.NumberFormat(fr ? "fr-CA" : "en-CA", {
62 style: "currency", 100 style: "currency",
@@ -69,233 +107,315 @@ export default function HomeClient({
69 month: "short", 107 month: "short",
70 }); 108 });
71 109
72− return ( 110+ const months = PERIODS.find((p) => p.key === period)?.months ?? 24;
73− <div className="pb-6 pt-10 sm:pt-14"> 111+ const chartData = pulse.map((p) => ({
74− {/* ---- Héros ---- */} 112+ key: p.type,
75− <section> 113+ label: PULSE_LABELS[p.type][lang],
76− <span className="kicker"> 114+ color: PULSE_COLORS[p.type],
77− {lang === "fr" ? "Québec · 2021-2026" : "Québec · 2021-2026"} 115+ series: p.series,
78− </span> 116+ }));
79− <h1 className="vp-display mt-3 max-w-4xl text-[clamp(34px,6.4vw,72px)] font-bold uppercase leading-[0.98] tracking-[-0.035em]">
80− {lang === "fr" ? (
81− <>
82− La <span className="outline-txt">vraie</span> valeur.
83− <br />
84− <span className="hl">Sans boîte noire.</span>
85− </>
86− ) : (
87− <>
88− The <span className="outline-txt">true</span> value.
89− <br />
90− <span className="hl">No black box.</span>
91− </>
92− )}
93− </h1>
94− <p className="mt-5 max-w-xl text-[16.5px] text-ink-2">{t("sub")}</p>
95 117
96− <div className="mt-7 flex gap-2.5 overflow-x-auto pb-1.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> 118+ const proofs: [string, string][] = [
97− <span className="stat-chip"> 119+ ["3 747 008", fr ? "propriétés estimées" : "estimated properties"],
98− <span className="pulse" /> 120+ ["745 119", fr ? "ventes réelles 2021-2026" : "real sales 2021-2026"],
99− <b>3 747 008</b> {lang === "fr" ? "propriétés" : "properties"} 121+ [locNum(salesLast30d, lang, { maxFrac: 0 }), fr ? "ventes captées / 30 jours" : "sales captured / 30 days"],
100− </span> 122+ ["1 100", fr ? "municipalités couvertes" : "municipalities covered"],
101− <span className="stat-chip"> 123+ ];
102− <b>745 119</b> {lang === "fr" ? "ventes réelles" : "real sales"}
103− </span>
104− <span className="stat-chip">
105− <b>{locNum(salesLast30d, lang, { maxFrac: 0 })}</b>{" "}
106− {lang === "fr" ? "ventes captées / 30 j" : "sales captured / 30 d"}
107− </span>
108− <span className="stat-chip">
109− <b>1 100</b> {lang === "fr" ? "municipalités" : "municipalities"}
110− </span>
111− <span className="stat-chip">
112− MdAPE <b>11 %</b>
113− </span>
114− </div>
115− </section>
116 124
117− {/* ---- Recherche ---- */} 125+ return (
118− <section className="mt-9"> 126+ <div className="pb-6">
119− <div className="vp-card p-4 sm:p-5"> 127+ {/* ================= HÉRO — terminal encre pleine largeur ================= */}
120− <p className="klabel mb-2 pl-0.5"> 128+ <section className="bleed vp-dark">
121− {lang === "fr" ? "Adresse de la propriété" : "Property address"} 129+ <div className="mx-auto max-w-6xl px-4 pb-10 pt-12 sm:px-6 sm:pb-14 sm:pt-16">
130+ <span className="kicker !text-[var(--accent-bright)] [&::before]:!bg-[var(--accent-bright)]">
131+ {fr
132+ ? "Terminal public de valeur immobilière — Québec"
133+ : "Public real-estate value terminal — Québec"}
134+ </span>
135+ <h1 className="vp-display mt-4 text-[clamp(38px,8vw,92px)] font-bold uppercase leading-[0.95] tracking-[-0.035em]">
136+ {fr ? (
137+ <>
138+ La <span className="outline-txt">vraie</span> valeur.
139+ <br />
140+ <span className="text-[var(--accent-bright)]">Sans boîte noire.</span>
141+ </>
142+ ) : (
143+ <>
144+ The <span className="outline-txt">true</span> value.
145+ <br />
146+ <span className="text-[var(--accent-bright)]">No black box.</span>
147+ </>
148+ )}
149+ </h1>
150+ <p className="mt-5 max-w-xl text-[15.5px] leading-relaxed text-[rgba(245,243,238,0.72)]">
151+ {t("sub")}
122 </p> 152 </p>
123− <SearchBox /> 153+
154+ {/* -------- la recherche : l'action centrale de la page -------- */}
155+ <div className="mt-9 max-w-3xl">
156+ <p className="vp-mono mb-1 text-[10.5px] font-medium uppercase tracking-[0.16em] text-[rgba(245,243,238,0.55)]">
157+ {fr ? "Entrez une adresse au Québec" : "Enter a Québec address"}
158+ </p>
159+ <SearchBox variant="hero" />
160+ <p className="vp-mono mt-2.5 text-[10px] uppercase tracking-[0.06em] text-[rgba(245,243,238,0.4)]">
161+ {t("dataNote")}
162+ </p>
163+ </div>
164+
165+ {/* -------- preuves de profondeur de données, intégrées -------- */}
166+ <div className="mt-10 grid grid-cols-2 gap-x-6 gap-y-5 border-t border-[rgba(245,243,238,0.16)] pt-6 sm:grid-cols-4">
167+ {proofs.map(([num, label], i) => (
168+ <div key={label}>
169+ <p className="vp-display flex items-baseline gap-2 text-[clamp(20px,2.6vw,28px)] font-bold leading-none tracking-[-0.02em] text-[var(--paper-on-ink)]">
170+ {num}
171+ {i === 2 && (
172+ <span
173+ aria-hidden="true"
174+ className="inline-block h-[7px] w-[7px] rounded-full bg-[var(--accent-bright)] [animation:pulse_2.4s_ease_infinite]"
175+ />
176+ )}
177+ </p>
178+ <p className="vp-mono mt-1.5 text-[9.5px] uppercase tracking-[0.12em] text-[rgba(245,243,238,0.5)]">
179+ {label}
180+ </p>
181+ </div>
182+ ))}
183+ </div>
124 </div> 184 </div>
125− <p className="vp-mono mt-2.5 pl-1 text-[11px] uppercase tracking-[0.06em] text-ink-3">
126− {t("dataNote")}
127− </p>
128 </section> 185 </section>
129 186
130− {/* ---- Comment ça marche (bande compacte) ---- */} 187+ {/* ============ le principe, en une ligne (pas de boîtes) ============ */}
131− <section className="mt-10 grid gap-3 sm:grid-cols-3"> 188+ <section className="border-b border-[var(--line)] py-6">
132− {(fr 189+ <div className="grid gap-x-8 gap-y-4 sm:grid-cols-3">
133− ? [ 190+ {(fr
134− ["01", "Cherchez l'adresse", "3,7 M d'unités d'évaluation, recherche tolérante aux fautes."], 191+ ? [
135− ["02", "Le moteur croise deux sources", "Modèle hédonique (690 k ventes) + comparables réels ajustés en dollars."], 192+ ["01", "Cherchez l'adresse", "3,7 M d'unités d'évaluation, recherche tolérante aux fautes."],
136− ["03", "Fourchette + confiance", "P10-P90, indice A-D, rapport PDF — le calcul montré en entier."], 193+ ["02", "Deux sources croisées", "Modèle hédonique (690 k ventes) + comparables réels ajustés en dollars."],
137− ] 194+ ["03", "Fourchette + confiance", "P10-P90, indice A-D — le calcul montré en entier."],
138− : [ 195+ ]
139− ["01", "Search the address", "3.7M assessment units, typo-tolerant search."], 196+ : [
140− ["02", "Two sources, crossed", "Hedonic model (690k sales) + real comparables adjusted in dollars."], 197+ ["01", "Search the address", "3.7M assessment units, typo-tolerant search."],
141− ["03", "Range + confidence", "P10-P90, A-D index, PDF report — the full math, shown."], 198+ ["02", "Two sources, crossed", "Hedonic model (690k sales) + real comparables adjusted in dollars."],
142− ] 199+ ["03", "Range + confidence", "P10-P90, A-D index — the full math, shown."],
143− ).map(([num, title, body], i) => ( 200+ ]
144− <div key={num} className="flex items-start gap-3 rounded-[10px] border-[1.5px] border-dashed border-[rgba(20,24,20,0.3)] bg-surface-2 p-4"> 201+ ).map(([num, title, body]) => (
145− <span className="vp-mono flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-[1.5px] border-ink bg-ink text-[12px] font-bold text-lime"> 202+ <div key={num} className="flex items-start gap-3">
146− {num} 203+ <span className="vp-mono pt-[3px] text-[11px] font-bold text-accent">{num}</span>
147− </span> 204+ <p className="text-[13px] leading-snug text-ink-2">
148− <div> 205+ <span className="vp-display block text-[14px] font-bold uppercase tracking-[-0.01em] text-ink">
149− <p className="vp-display text-[14.5px] font-bold uppercase tracking-[-0.01em]"> 206+ {title}
150− {title} 207+ </span>
151− {i < 2 && <span className="ml-2 text-ink-3">→</span>} 208+ {body}
152 </p> 209 </p>
153− <p className="mt-1 text-[12.5px] leading-snug text-ink-2">{body}</p>
154 </div> 210 </div>
155− </div> 211+ ))}
156− ))} 212+ </div>
157 </section> 213 </section>
158 214
159− {/* ---- Pouls du marché ---- */} 215+ {/* ================= MARCHÉ — data visualization éditoriale ================= */}
160− <section className="mt-14"> 216+ <section className="mt-12">
161− <span className="kicker">{fr ? "Indice Vrai-Prix" : "Vrai-Prix index"}</span> 217+ <SectionHead
162− <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]"> 218+ num="01"
163− {fr ? "Le pouls du marché québécois" : "The pulse of the Québec market"} 219+ kicker={fr ? "Indice Vrai-Prix" : "Vrai-Prix index"}
164− </h2> 220+ title={fr ? "Le pouls du marché québécois" : "The pulse of the Québec market"}
165− <p className="mt-2 max-w-2xl text-[14px] text-ink-2"> 221+ lede={
166− {fr 222+ fr
167− ? "L'indice mensuel calculé sur les ventes réelles — celui-là même qui ajuste les comparables de chaque estimation. Base 100 il y a 24 mois." 223+ ? "L'indice mensuel calculé sur les ventes réelles — celui-là même qui ajuste les comparables de chaque estimation. Trois grands types, une même échelle : base 100 au début de la période."
168− : "The monthly index built from real sales — the same one adjusting every estimate's comparables. Base 100 twenty-four months ago."} 224+ : "The monthly index built from real sales — the same one adjusting every estimate's comparables. Three major types, one scale: base 100 at the period start."
169− </p> 225+ }
170− <div className="mt-4 grid gap-4 sm:grid-cols-3"> 226+ />
227+
228+ {/* contrôles : période (une rangée, au-dessus du graphique) */}
229+ <div className="mt-5 flex flex-wrap items-end justify-between gap-x-6 gap-y-2 border-b border-[var(--line)]">
230+ <div className="flex gap-5">
231+ {PERIODS.map((p) => (
232+ <button
233+ key={p.key}
234+ type="button"
235+ className="vp-tab"
236+ aria-pressed={period === p.key}
237+ onClick={() => setPeriod(p.key)}
238+ >
239+ {p[lang]}
240+ </button>
241+ ))}
242+ </div>
243+ {/* légende — l'identité ne repose jamais sur la couleur seule */}
244+ <div className="flex flex-wrap gap-x-4 gap-y-1 pb-2">
245+ {chartData.map((s) => (
246+ <span key={s.key} className="vp-mono flex items-center gap-1.5 text-[10px] uppercase tracking-[0.08em] text-ink-2">
247+ <span className="inline-block h-[2px] w-[14px]" style={{ background: s.color }} />
248+ {s.label}
249+ </span>
250+ ))}
251+ </div>
252+ </div>
253+
254+ <div className="mt-4">
255+ <MarketPulseChart data={chartData} lang={lang} months={months} />
256+ </div>
257+
258+ {/* lecture par type : variations en colonnes de données */}
259+ <div className="mt-5 grid gap-x-8 gap-y-3 border-t border-[var(--line)] pt-4 sm:grid-cols-3">
171 {pulse.map((p) => ( 260 {pulse.map((p) => (
172− <div key={p.type} className="vp-card vp-card-hover p-5"> 261+ <div key={p.type} className="flex items-baseline justify-between gap-4 sm:block">
173− <div className="flex items-center justify-between gap-2"> 262+ <p className="vp-mono flex items-center gap-1.5 text-[10.5px] font-bold uppercase tracking-[0.08em] text-ink">
174− <p className="vp-display text-[16px] font-bold uppercase tracking-[-0.01em]"> 263+ <span className="inline-block h-[2px] w-[12px]" style={{ background: PULSE_COLORS[p.type] }} />
175− {PULSE_LABELS[p.type][lang]} 264+ {PULSE_LABELS[p.type][lang]}
176− </p> 265+ </p>
177− <TrendBadge pct={p.pct12m} label={fr ? "12 mois" : "12 mo"} /> 266+ <p className="vp-mono mt-1 flex gap-4 text-[11px] text-ink-2">
178− </div> 267+ <span>
179− <div className="mt-3"> 268+ 12&nbsp;{fr ? "mois" : "mo"}&nbsp;
180− <IndexSpark series={p.series} /> 269+ <b className="vp-display text-[14px]">
181− </div> 270+ <Pct v={p.pct12m} />
182− {p.since2021 != null && (
183− <p className="vp-mono mt-2 text-[10.5px] uppercase tracking-[0.05em] text-ink-3">
184− {fr ? "Depuis 2021" : "Since 2021"} :{" "}
185− <b className="text-ink">
186− {p.since2021 >= 0 ? "+" : ""}
187− {p.since2021.toFixed(0)} %
188 </b> 271 </b>
189− </p> 272+ </span>
190− )} 273+ <span>
274+ 2021→&nbsp;
275+ <b className="vp-display text-[14px]">
276+ <Pct v={p.since2021} digits={0} />
277+ </b>
278+ </span>
279+ </p>
191 </div> 280 </div>
192 ))} 281 ))}
193 </div> 282 </div>
194 </section> 283 </section>
195 284
196− {/* ---- Dernières ventes captées ---- */} 285+ {/* ================= VENTES RÉELLES — flux prix-d'abord ================= */}
197 {recent.length > 0 && ( 286 {recent.length > 0 && (
198 <section className="mt-14"> 287 <section className="mt-14">
199− <span className="kicker"> 288+ <SectionHead
200− <span className="pulse" style={{ width: 8, height: 8 }} /> 289+ num="02"
201− {fr ? "Flux en continu" : "Live feed"} 290+ kicker={fr ? "Flux en continu" : "Live feed"}
202− </span> 291+ title={fr ? "Dernières ventes réelles captées" : "Latest real sales captured"}
203− <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]"> 292+ lede={
204− {fr ? "Dernières ventes réelles captées" : "Latest real sales captured"} 293+ fr
205− </h2> 294+ ? "Transactions publiées, appariées automatiquement à leur unité d'évaluation. Chaque vente mène à l'estimation complète de la propriété."
206− <p className="mt-2 max-w-2xl text-[14px] text-ink-2"> 295+ : "Published transactions, automatically matched to their assessment unit. Each sale leads to the property's full estimate."
207− {fr 296+ }
208− ? "Transactions publiées, appariées automatiquement à leur unité d'évaluation. Cliquez pour voir l'estimation complète de la propriété vendue." 297+ />
209− : "Published transactions, automatically matched to their assessment unit. Click for the sold property's full estimate."} 298+ <div className="mt-5">
210− </p> 299+ {recent.map((s) => {
211− <div className="src-wrap mt-4"> 300+ const inner = (
212− <table className="src-table"> 301+ <div className="flex flex-wrap items-baseline gap-x-4 gap-y-1 sm:flex-nowrap">
213− <thead> 302+ {/* le prix d'abord — la donnée mène */}
214− <tr> 303+ <p className="vp-display w-full text-[22px] font-bold leading-none tracking-[-0.02em] sm:w-[168px] sm:shrink-0 sm:text-[20px]">
215− <th>{fr ? "Date" : "Date"}</th> 304+ {money(s.amount)}
216− <th>{fr ? "Propriété" : "Property"}</th> 305+ </p>
217− <th className="text-right">{fr ? "Prix de vente" : "Sale price"}</th> 306+ <div className="min-w-0 flex-1">
218− <th className="text-right"></th> 307+ <p className="truncate text-[14.5px] font-semibold leading-snug">
219− </tr> 308+ {s.street}
220− </thead> 309+ {s.city ? `, ${s.city}` : ""}
221− <tbody> 310+ </p>
222− {recent.map((s) => ( 311+ <p className="vp-mono mt-0.5 text-[10.5px] uppercase tracking-[0.05em] text-ink-3">
223− <tr key={s.id}>
224− <td className="vp-mono whitespace-nowrap text-[12px] font-bold uppercase">
225 {dateFmt(s.date)} 312 {dateFmt(s.date)}
226− </td> 313+ {s.propertyType && s.propertyType !== "indéterminé" ? ` · ${s.propertyType}` : ""}
227− <td>
228− <p className="font-semibold leading-tight">
229− {s.street}
230− {s.city ? `, ${s.city}` : ""}
231− </p>
232− {s.propertyType && s.propertyType !== "indéterminé" && (
233− <p className="vp-mono mt-0.5 text-[10.5px] uppercase tracking-[0.04em] text-ink-3">
234− {s.propertyType}
235− </p>
236− )}
237− </td>
238− <td className="text-right">
239− <span className="vp-display font-bold">{money(s.amount)}</span>
240 {s.vsRolePct != null && Math.abs(s.vsRolePct) < 400 && ( 314 {s.vsRolePct != null && Math.abs(s.vsRolePct) < 400 && (
241− <p className="vp-mono mt-0.5 text-[10px] text-ink-3"> 315+ <>
242− {s.vsRolePct >= 0 ? "+" : "−"} 316+ {" · "}
243− {Math.abs(s.vsRolePct).toFixed(0)} %{" "} 317+ <span className={s.vsRolePct >= 0 ? "text-ink-2" : "text-[var(--danger)]"}>
244− {fr ? "vs rôle" : "vs roll"} 318+ {s.vsRolePct >= 0 ? "+" : "−"}
245− </p> 319+ {Math.abs(s.vsRolePct).toFixed(0)} % {fr ? "vs rôle" : "vs roll"}
246− )} 320+ </span>
247− </td> 321+ </>
248− <td className="text-right">
249− {s.idProvinc && (
250− <Link
251− href={`/estimation/${encodeURIComponent(s.idProvinc)}`}
252− className="vp-mono whitespace-nowrap rounded-[5px] border-[1.5px] border-ink bg-surface-2 px-2.5 py-1.5 text-[11px] font-bold uppercase tracking-[0.04em] transition-colors hover:bg-lime"
253− >
254− {fr ? "Estimer" : "Estimate"} →
255− </Link>
256 )} 322 )}
257− </td> 323+ </p>
258− </tr> 324+ </div>
259− ))} 325+ {s.idProvinc && (
260− </tbody> 326+ <p className="vp-mono shrink-0 text-[10.5px] font-bold uppercase tracking-[0.08em] text-accent">
261− </table> 327+ {fr ? "Voir l'estimation" : "See the estimate"} →
328+ </p>
329+ )}
330+ </div>
331+ );
332+ return s.idProvinc ? (
333+ <Link key={s.id} href={`/estimation/${encodeURIComponent(s.idProvinc)}`} className="feed-row">
334+ {inner}
335+ </Link>
336+ ) : (
337+ <div key={s.id} className="feed-row">
338+ {inner}
339+ </div>
340+ );
341+ })}
262 </div> 342 </div>
263 </section> 343 </section>
264 )} 344 )}
265 345
266− {/* ---- Formulaire manuel ---- */} 346+ {/* ================= LE CALCUL, MONTRÉ — promesse centrale ================= */}
267 <section className="mt-14"> 347 <section className="mt-14">
268− <span className="kicker">{lang === "fr" ? "Plan B" : "Plan B"}</span> 348+ <SectionHead
269− <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]"> 349+ num="03"
270− {t("manualTitle")} 350+ kicker={fr ? "Sans boîte noire" : "No black box"}
271− </h2> 351+ title={fr ? "Voici exactement comment le prix est calculé" : "Here is exactly how the price is computed"}
272− <div className="vp-card mt-4 p-4 sm:p-6"> 352+ />
273− <EstimateForm /> 353+ <div className="mt-6 grid gap-10 lg:grid-cols-[1.2fr_1fr]">
354+ <ol className="tl">
355+ {(fr
356+ ? [
357+ ["Propriété identifiée", "3,7 M d'unités d'évaluation géoréférencées (MAMH)."],
358+ ["Rôle d'évaluation récupéré", "Valeurs terrain + bâtiment, six millésimes 2021-2026."],
359+ ["Ventes comparables sélectionnées", "Rayon adaptatif, même famille de type, aire ±20 %."],
360+ ["Caractéristiques ajustées", "Marché, superficie, âge — chaque ajustement affiché en dollars."],
361+ ["Estimation calculée", "65 % modèle hédonique + 35 % comparables pondérés."],
362+ ["Intervalle de confiance produit", "Fourchette P10-P90 et indice A-D, toujours affichés."],
363+ ]
364+ : [
365+ ["Property identified", "3.7M georeferenced assessment units (MAMH)."],
366+ ["Assessment roll retrieved", "Land + building values, six vintages 2021-2026."],
367+ ["Comparable sales selected", "Adaptive radius, same type family, area ±20%."],
368+ ["Characteristics adjusted", "Market, area, age — every adjustment shown in dollars."],
369+ ["Estimate computed", "65% hedonic model + 35% weighted comparables."],
370+ ["Confidence interval produced", "P10-P90 range and A-D index, always shown."],
371+ ]
372+ ).map(([title, body], i, arr) => (
373+ <li key={title} className="tl-step" data-accent={i === arr.length - 1 ? "true" : undefined}>
374+ <p className="vp-display flex items-baseline gap-2.5 text-[15px] font-bold uppercase tracking-[-0.01em]">
375+ <span className="vp-mono text-[10.5px] font-bold text-accent">0{i + 1}</span>
376+ {title}
377+ </p>
378+ <p className="mt-1 text-[13px] leading-snug text-ink-2">{body}</p>
379+ </li>
380+ ))}
381+ </ol>
382+ <div>
383+ {(lang === "fr"
384+ ? [
385+ ["Le calcul, montré", "Chaque ajustement de chaque comparable est affiché en dollars. Pas de score magique."],
386+ ["Fourchette honnête", "Un prix unique est un mensonge statistique. On donne P10-P90 et un indice de confiance A-D."],
387+ ["Données réelles", "Rôles fonciers officiels (MAMH) et ventes publiées — locales et récentes en priorité."],
388+ ]
389+ : [
390+ ["The math, shown", "Every adjustment on every comparable is displayed in dollars. No magic score."],
391+ ["Honest range", "A single price is a statistical lie. We give P10-P90 and an A-D confidence index."],
392+ ["Real data", "Official assessment rolls (MAMH) and published sales — local and recent first."],
393+ ]
394+ ).map(([title, body]) => (
395+ <div key={title} className="border-t border-[var(--line)] py-4 first:border-t-0 first:pt-0">
396+ <h3 className="vp-display text-[15px] font-bold uppercase tracking-[-0.01em]">{title}</h3>
397+ <p className="mt-1.5 text-[13px] leading-snug text-ink-2">{body}</p>
398+ </div>
399+ ))}
400+ <p className="vp-mono mt-2 border-t border-[var(--line)] pt-4 text-[11px] uppercase tracking-[0.06em]">
401+ <Link href="/methodologie" className="border-b border-accent pb-px text-accent-deep hover:text-accent">
402+ {fr ? "Méthodologie complète — niveau rapport technique" : "Full methodology — technical-report depth"} →
403+ </Link>
404+ </p>
405+ </div>
274 </div> 406 </div>
275 </section> 407 </section>
276 408
277− {/* ---- Manifeste transparence ---- */} 409+ {/* ================= PLAN B — estimation sans adresse ================= */}
278− <section className="mt-14 grid gap-4 sm:grid-cols-3"> 410+ <section className="mt-14">
279− {(lang === "fr" 411+ <SectionHead
280− ? [ 412+ num="04"
281− ["01", "Le calcul, montré", "Chaque ajustement de chaque comparable est affiché en dollars. Pas de score magique."], 413+ kicker={fr ? "Plan B" : "Plan B"}
282− ["02", "Fourchette honnête", "Un prix unique est un mensonge statistique. On donne P10-P90 et un indice de confiance A-D."], 414+ title={t("manualTitle")}
283− ["03", "Données réelles", "Rôles fonciers officiels (MAMH) et ventes publiées — locales et récentes en priorité."], 415+ />
284− ] 416+ <div className="mt-6 max-w-3xl">
285− : [ 417+ <EstimateForm />
286− ["01", "The math, shown", "Every adjustment on every comparable is displayed in dollars. No magic score."], 418+ </div>
287− ["02", "Honest range", "A single price is a statistical lie. We give P10-P90 and an A-D confidence index."],
288− ["03", "Real data", "Official assessment rolls (MAMH) and published sales — local and recent first."],
289− ]
290− ).map(([num, title, body]) => (
291− <div key={num} className="vp-card vp-card-hover p-5">
292− <p className="vp-mono text-[11px] font-bold text-green">{num}</p>
293− <h3 className="vp-display mt-1.5 text-[17px] font-bold uppercase tracking-[-0.01em]">
294− {title}
295− </h3>
296− <p className="mt-2 text-[13.5px] text-ink-2">{body}</p>
297− </div>
298− ))}
299 </section> 419 </section>
300 </div> 420 </div>
301 ); 421 );
modified src/app/compte/CompteClient.tsx +1 −1
@@ -345,7 +345,7 @@ export default function CompteClient({
345 href={socialUrl(net, v)} 345 href={socialUrl(net, v)}
346 target="_blank" 346 target="_blank"
347 rel="noopener noreferrer" 347 rel="noopener noreferrer"
348− className="stat-chip hover:bg-lime-soft" 348+ className="stat-chip transition-colors hover:text-accent-deep"
349 > 349 >
350 <b>{SOCIAL_LABELS[net]}</b> {v} 350 <b>{SOCIAL_LABELS[net]}</b> {v}
351 </a> 351 </a>
modified src/app/globals.css +180 −88
@@ -1,10 +1,11 @@
1 /* ----------------------------------------------------------------------------- 1 /* -----------------------------------------------------------------------------
2 Vrai-Prix — estimation immobilière transparente (Québec) 2 Vrai-Prix — estimation immobilière transparente (Québec)
3 Auteur : Simon-Pierre Boucher — contact@spboucher.ai 3 Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4− Système de design « éditorial sharp » — identité ROUGE distincte de Lou-Ka : 4+ Système de design « terminal éditorial » v2 — Bloomberg × FT data × institutionnel :
5 · Typo display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono 5 · Typo display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono
6− · Signature : bordures encre + ombres décalées (néo-brutalisme raffiné) 6+ · Signature : filets fins (hairlines), grands chiffres, compositions asymétriques,
7− · Accent rouge vif #ff5148 + bordeaux #9e2a25 sur encre 7+ encre profonde + rouge signal réservé à l'information importante
8+ · Zéro ombre décalée, zéro pilule, radius 0-2 px — la donnée fait le décor
8 ----------------------------------------------------------------------------- */ 9 ----------------------------------------------------------------------------- */
9 @import "tailwindcss"; 10 @import "tailwindcss";
10 /* ka-ui (Groupe KA) — tokens + composants communs (.gk-badge, .ka-footer, .card…). 11 /* ka-ui (Groupe KA) — tokens + composants communs (.gk-badge, .ka-footer, .card…).
@@ -16,7 +17,8 @@
16 17
17 :root { 18 :root {
18 /* ---- Accent Vrai-Prix (tokens canoniques ka-ui) — identité rouge ---- */ 19 /* ---- Accent Vrai-Prix (tokens canoniques ka-ui) — identité rouge ---- */
19− --accent: #ff5148; 20+ --accent: #d8372e; /* rouge signal éditorial (lisible sur papier) */
21+ --accent-bright: #ff5148; /* rouge vif — marqueurs, points de donnée sur encre */
20 --accent-soft: #ffe3e0; 22 --accent-soft: #ffe3e0;
21 --accent-deep: #9e2a25; 23 --accent-deep: #9e2a25;
22 --on-accent: #ffffff; 24 --on-accent: #ffffff;
@@ -28,6 +30,7 @@
28 --ink-2: #4d5551; 30 --ink-2: #4d5551;
29 --ink-3: #8b928c; 31 --ink-3: #8b928c;
30 --line: rgba(20, 24, 20, 0.14); 32 --line: rgba(20, 24, 20, 0.14);
33+ --line-soft: rgba(20, 24, 20, 0.08);
31 --line-strong: rgba(20, 24, 20, 0.85); 34 --line-strong: rgba(20, 24, 20, 0.85);
32 --green: #9e2a25; 35 --green: #9e2a25;
33 --green-deep: #771f1b; 36 --green-deep: #771f1b;
@@ -37,11 +40,20 @@
37 --amber-soft: #fdf3e2; 40 --amber-soft: #fdf3e2;
38 --danger: #b3423a; 41 --danger: #b3423a;
39 --danger-soft: #fbe9e7; 42 --danger-soft: #fbe9e7;
40− --r-card: 10px; 43+ /* série de graphiques (palette validée : CVD 28,6 · contraste ≥3:1) */
41− --r-ctl: 6px; 44+ --chart-1: #141814; /* unifamiliale */
45+ --chart-2: #d8372e; /* condo */
46+ --chart-3: #6b736d; /* plex */
47+ --r-card: 2px;
48+ --r-ctl: 2px;
42 --shadow-flat: 0 1px 2px rgba(20, 24, 20, 0.05); 49 --shadow-flat: 0 1px 2px rgba(20, 24, 20, 0.05);
43− --shadow-off: 6px 6px 0 var(--ink); 50+ /* ombres décalées héritées — aplaties : le v2 ne porte aucune ombre dure */
44− --shadow-off-soft: 8px 8px 0 rgba(20, 24, 20, 0.08); 51+ --shadow-off: 0 1px 2px rgba(20, 24, 20, 0.08);
52+ --shadow-off-soft: none;
53+ --shadow-off-mid: 0 1px 2px rgba(20, 24, 20, 0.08);
54+ /* encre légèrement plus profonde pour les aplats pleine largeur */
55+ --ink-deep: #101410;
56+ --paper-on-ink: #edeae2;
45 } 57 }
46 58
47 @theme inline { 59 @theme inline {
@@ -58,6 +70,9 @@
58 --color-amber: var(--amber); 70 --color-amber: var(--amber);
59 --color-amber-soft: var(--amber-soft); 71 --color-amber-soft: var(--amber-soft);
60 --color-danger: var(--danger); 72 --color-danger: var(--danger);
73+ --color-accent: var(--accent);
74+ --color-accent-deep: var(--accent-deep);
75+ --color-accent-bright: var(--accent-bright);
61 --font-display: var(--font-space-grotesk), system-ui, sans-serif; 76 --font-display: var(--font-space-grotesk), system-ui, sans-serif;
62 --font-body: var(--font-inter), system-ui, sans-serif; 77 --font-body: var(--font-inter), system-ui, sans-serif;
63 --font-mono: var(--font-jetbrains), ui-monospace, monospace; 78 --font-mono: var(--font-jetbrains), ui-monospace, monospace;
@@ -80,10 +95,10 @@ body {
80 -webkit-font-smoothing: antialiased; 95 -webkit-font-smoothing: antialiased;
81 padding-bottom: env(safe-area-inset-bottom); 96 padding-bottom: env(safe-area-inset-bottom);
82 } 97 }
83−/* grain subtil — texture signature */ 98+/* grain subtil — texture signature (discret, presque imperceptible) */
84 body::before { 99 body::before {
85 content: ""; 100 content: "";
86− position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0.35; 101+ position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0.22;
87 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E"); 102 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
88 } 103 }
89 /* au-dessus du grain (::before positionné, premier dans l'ordre de peinture) ; 104 /* au-dessus du grain (::before positionné, premier dans l'ordre de peinture) ;
@@ -95,146 +110,223 @@ h1, h2, h3, h4 {
95 font-family: var(--font-space-grotesk), system-ui, sans-serif; 110 font-family: var(--font-space-grotesk), system-ui, sans-serif;
96 letter-spacing: -0.03em; 111 letter-spacing: -0.03em;
97 } 112 }
98−::selection { background: var(--lime); color: var(--ink); } 113+::selection { background: var(--ink); color: var(--paper); }
99 114
100 /* ---------- primitives signature ---------- */ 115 /* ---------- primitives signature ---------- */
101 .vp-display { font-family: var(--font-space-grotesk), system-ui, sans-serif; } 116 .vp-display { font-family: var(--font-space-grotesk), system-ui, sans-serif; }
102 .vp-mono { font-family: var(--font-jetbrains), ui-monospace, monospace; } 117 .vp-mono { font-family: var(--font-jetbrains), ui-monospace, monospace; }
103 118
119+/* pleine largeur depuis un conteneur centré (héro, bandes encre) */
120+.bleed { width: 100vw; margin-left: calc(50% - 50vw); }
121+
122+/* aplat encre : héro, blocs terminal */
123+.vp-dark { background: var(--ink-deep); color: var(--paper-on-ink); }
124+.vp-dark ::selection { background: var(--accent-bright); color: var(--ink); }
125+
104 .kicker { 126 .kicker {
105 font-family: var(--font-jetbrains), ui-monospace, monospace; 127 font-family: var(--font-jetbrains), ui-monospace, monospace;
106− font-size: 11.5px; font-weight: 500; text-transform: uppercase; 128+ font-size: 11px; font-weight: 500; text-transform: uppercase;
107− letter-spacing: 0.14em; color: var(--green); 129+ letter-spacing: 0.16em; color: var(--accent);
108− display: inline-flex; align-items: center; gap: 8px; 130+ display: inline-flex; align-items: center; gap: 9px;
109 } 131 }
110−.kicker::before { content: ""; width: 22px; height: 2px; background: var(--green); } 132+.kicker::before { content: ""; width: 20px; height: 1.5px; background: var(--accent); }
111 133
112 .klabel { 134 .klabel {
113 font-family: var(--font-jetbrains), ui-monospace, monospace; 135 font-family: var(--font-jetbrains), ui-monospace, monospace;
114− font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; 136+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em;
115− color: var(--ink-3); font-weight: 700; 137+ color: var(--ink-3); font-weight: 500;
138+}
139+
140+/* en-tête de section éditorial : filet fort + numéro mono */
141+.sec {
142+ border-top: 2px solid var(--ink);
143+ padding-top: 14px;
144+}
145+.sec-num {
146+ font-family: var(--font-jetbrains), ui-monospace, monospace;
147+ font-size: 11px; font-weight: 700; color: var(--accent);
148+ letter-spacing: 0.1em;
116 } 149 }
117 150
151+/* carte héritée — aplatie : surface + hairline, aucune ombre */
118 .vp-card { 152 .vp-card {
119 background: var(--surface); 153 background: var(--surface);
120− border: 1.5px solid var(--ink); 154+ border: 1px solid var(--line);
121 border-radius: var(--r-card); 155 border-radius: var(--r-card);
122− box-shadow: var(--shadow-off-soft); 156+ box-shadow: none;
123 } 157 }
124−.vp-card-hover { transition: transform 0.16s ease, box-shadow 0.16s ease; } 158+.vp-card-hover { transition: border-color 0.15s ease; }
125−.vp-card-hover:hover { transform: translate(-3px, -3px); box-shadow: var(--shadow-off); } 159+.vp-card-hover:hover { transform: none; box-shadow: none; border-color: var(--line-strong); }
126 160
161+/* champ : soulignement éditorial (le cadre disparaît) */
127 .vp-input { 162 .vp-input {
128− border: 1.5px solid var(--line); background: var(--surface-2); 163+ border: 0; border-bottom: 1px solid var(--line-strong); background: transparent;
129− border-radius: var(--r-ctl); padding: 11px 12px; font-size: 15px; 164+ border-radius: 0; padding: 10px 2px; font-size: 15px;
130 color: var(--ink); outline: none; font-family: inherit; width: 100%; 165 color: var(--ink); outline: none; font-family: inherit; width: 100%;
131 transition: border-color 0.15s ease, box-shadow 0.15s ease; min-height: 44px; 166 transition: border-color 0.15s ease, box-shadow 0.15s ease; min-height: 44px;
132 appearance: none; -webkit-appearance: none; 167 appearance: none; -webkit-appearance: none;
133 } 168 }
134−.vp-input:focus { border-color: var(--ink); box-shadow: 3px 3px 0 var(--lime); } 169+.vp-input::placeholder { color: var(--ink-3); }
170+.vp-input:focus { border-color: var(--accent); box-shadow: 0 1px 0 var(--accent); }
135 select.vp-input { 171 select.vp-input {
136 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E"); 172 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23141814'/%3E%3C/svg%3E");
137− background-repeat: no-repeat; background-position: right 12px center; padding-right: 30px; 173+ background-repeat: no-repeat; background-position: right 6px center; padding-right: 26px;
138 } 174 }
139 @media (max-width: 900px) { .vp-input { font-size: 16px; } } /* anti-zoom iOS */ 175 @media (max-width: 900px) { .vp-input { font-size: 16px; } } /* anti-zoom iOS */
140 176
141 .btn { 177 .btn {
142− border: 1.5px solid var(--ink); border-radius: var(--r-ctl); padding: 11px 20px; 178+ border: 1px solid var(--ink); border-radius: var(--r-ctl); padding: 11px 22px;
143− font-weight: 700; font-size: 14px; cursor: pointer; min-height: 44px; 179+ font-weight: 600; font-size: 13.5px; cursor: pointer; min-height: 44px;
144− transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.15s ease; 180+ transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
145− font-family: var(--font-space-grotesk), system-ui, sans-serif; letter-spacing: 0.01em; 181+ font-family: var(--font-space-grotesk), system-ui, sans-serif; letter-spacing: 0.02em;
146 display: inline-flex; align-items: center; justify-content: center; gap: 8px; 182 display: inline-flex; align-items: center; justify-content: center; gap: 8px;
147 } 183 }
148−.btn:active { transform: translate(2px, 2px); box-shadow: none !important; } 184+.btn:active { opacity: 0.85; }
149−.btn-primary { background: var(--ink); color: var(--lime); box-shadow: 4px 4px 0 rgba(20,24,20,0.25); } 185+.btn-primary { background: var(--ink); color: var(--paper); }
150−.btn-primary:hover { background: var(--green-deep); } 186+.btn-primary:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
151−.btn-ghost { background: transparent; color: var(--ink); border-color: var(--ink); } 187+.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-strong); }
152−.btn-ghost:hover { background: var(--lime); box-shadow: 4px 4px 0 rgba(20,24,20,0.2); } 188+.btn-ghost:hover { border-color: var(--accent); color: var(--accent-deep); }
189+.btn-accent { background: var(--accent); border-color: var(--accent); color: #fff; }
190+.btn-accent:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
153 191
192+/* preuve de données : chiffre fort + libellé mono, séparés par filets */
154 .stat-chip { 193 .stat-chip {
155− background: var(--surface); border: 1.5px solid var(--ink); border-radius: 999px; 194+ background: transparent; border: 0; border-left: 1px solid var(--line);
156− padding: 8px 16px; font-family: var(--font-jetbrains), ui-monospace, monospace; 195+ border-radius: 0; padding: 2px 0 2px 14px;
157− font-size: 12px; color: var(--ink-2); display: inline-flex; gap: 8px; align-items: center; 196+ font-family: var(--font-jetbrains), ui-monospace, monospace;
158− box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.12); white-space: nowrap; 197+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
198+ color: var(--ink-2); display: inline-flex; gap: 7px; align-items: baseline;
199+ box-shadow: none; white-space: nowrap;
200+}
201+.stat-chip:first-child { border-left: 0; padding-left: 0; }
202+.stat-chip b {
203+ color: var(--ink); font-weight: 700; font-size: 15px;
204+ font-family: var(--font-space-grotesk), system-ui, sans-serif; letter-spacing: -0.01em;
159 } 205 }
160−.stat-chip b { color: var(--ink); font-weight: 700; }
161 .stat-chip .pulse { 206 .stat-chip .pulse {
162− width: 8px; height: 8px; border-radius: 50%; background: var(--green); 207+ width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
163− box-shadow: 0 0 0 4px var(--lime-soft); animation: pulse 2.4s ease infinite; 208+ align-self: center; animation: pulse 2.4s ease infinite;
164 } 209 }
165−@keyframes pulse { 50% { box-shadow: 0 0 0 7px rgba(255, 81, 72, 0.4); } } 210+@keyframes pulse { 50% { box-shadow: 0 0 0 5px rgba(216, 55, 46, 0.18); } }
166 211
212+/* surligné signature : marqueur rouge sous le texte (plus de boîte inclinée) */
167 .hl { 213 .hl {
168− background: var(--lime); padding: 0 10px; border-radius: 8px; display: inline-block; 214+ background: linear-gradient(transparent 64%, rgba(216, 55, 46, 0.3) 64%);
169− transform: rotate(-1deg); 215+ padding: 0 2px; border-radius: 0; display: inline; transform: none;
170 } 216 }
171−.outline-txt { color: transparent; -webkit-text-stroke: 2px var(--ink); } 217+.outline-txt { color: transparent; -webkit-text-stroke: 1.5px var(--ink); }
218+.vp-dark .outline-txt { -webkit-text-stroke: 1.5px var(--paper-on-ink); }
172 219
173−/* ticker */ 220+/* ticker — bande de cotation (signature terminal) */
174 .ticker { 221 .ticker {
175− background: var(--ink); color: var(--lime); overflow: hidden; 222+ background: var(--ink-deep); color: var(--paper-on-ink); overflow: hidden;
176 font-family: var(--font-jetbrains), ui-monospace, monospace; 223 font-family: var(--font-jetbrains), ui-monospace, monospace;
177− font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase; 224+ font-size: 10.5px; letter-spacing: 0.09em; text-transform: uppercase;
178− padding: 7px 0; white-space: nowrap; 225+ padding: 6px 0; white-space: nowrap;
179− border-bottom: 1px solid rgba(255, 81, 72, 0.25);
180 } 226 }
181−.ticker-track { display: inline-flex; animation: ticker 44s linear infinite; will-change: transform; } 227+.ticker-track { display: inline-flex; animation: ticker 48s linear infinite; will-change: transform; }
182−.ticker span { padding: 0 26px; position: relative; } 228+.ticker span { padding: 0 24px; position: relative; opacity: 0.85; }
183−.ticker span::after { content: "◆"; position: absolute; right: -6px; opacity: 0.5; font-size: 8px; top: 3px; } 229+.ticker span::after { content: ""; position: absolute; right: -2px; top: 50%; width: 4px; height: 4px; transform: translateY(-50%) rotate(45deg); background: var(--accent-bright); opacity: 0.8; }
184 @keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } } 230 @keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
185 @media (prefers-reduced-motion: reduce) { 231 @media (prefers-reduced-motion: reduce) {
186 .ticker-track { animation: none; } 232 .ticker-track { animation: none; }
187 * { transition-duration: 0.01ms !important; } 233 * { transition-duration: 0.01ms !important; }
188 } 234 }
189 235
190−/* kv cells (fiche) */ 236+/* kv cells (fiche) — cellule de données éditoriale : filet haut, pas de boîte */
191 .kv-cell { 237 .kv-cell {
192− background: var(--surface-2); border: 1.5px solid var(--line); 238+ background: transparent; border: 0; border-top: 1px solid var(--line);
193− border-radius: var(--r-ctl); padding: 10px 13px; 239+ border-radius: 0; padding: 8px 2px 2px;
194 } 240 }
195 .kv-cell .k { 241 .kv-cell .k {
196 font-family: var(--font-jetbrains), ui-monospace, monospace; 242 font-family: var(--font-jetbrains), ui-monospace, monospace;
197 font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em; 243 font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.1em;
198− color: var(--ink-3); font-weight: 700; 244+ color: var(--ink-3); font-weight: 500;
199 } 245 }
200 .kv-cell .v { 246 .kv-cell .v {
201 font-weight: 700; font-size: 14.5px; margin-top: 2px; 247 font-weight: 700; font-size: 14.5px; margin-top: 2px;
202 font-family: var(--font-space-grotesk), system-ui, sans-serif; 248 font-family: var(--font-space-grotesk), system-ui, sans-serif;
203 } 249 }
204 250
205−/* pills */ 251+/* pills (indice de confiance) — carrés, fins */
206 .pill { 252 .pill {
207− display: inline-block; border-radius: 4px; padding: 3px 10px; 253+ display: inline-block; border-radius: 2px; padding: 3px 9px;
208 font-family: var(--font-jetbrains), ui-monospace, monospace; 254 font-family: var(--font-jetbrains), ui-monospace, monospace;
209 font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; 255 font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
210 } 256 }
211−.pill-conf-A { background: var(--lime); color: var(--ink); border: 1px solid var(--ink); } 257+.pill-conf-A { background: var(--ink); color: var(--paper); border: 1px solid var(--ink); }
212−.pill-conf-B { background: var(--lime-soft); color: var(--green-deep); border: 1px solid var(--green); } 258+.pill-conf-B { background: transparent; color: var(--ink); border: 1px solid var(--line-strong); }
213 .pill-conf-C { background: var(--amber-soft); color: #8a5a12; border: 1px solid var(--amber); } 259 .pill-conf-C { background: var(--amber-soft); color: #8a5a12; border: 1px solid var(--amber); }
214 .pill-conf-D { background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger); } 260 .pill-conf-D { background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger); }
215 261
216−/* tableau éditorial */ 262+/* tableau éditorial — filets fins, en-tête discret, zéro cadre épais */
217 .src-wrap { 263 .src-wrap {
218− overflow-x: auto; border: 1.5px solid var(--ink); border-radius: var(--r-card); 264+ overflow-x: auto; border: 0; border-top: 2px solid var(--ink); border-radius: 0;
219− box-shadow: var(--shadow-off-soft); background: var(--surface); 265+ box-shadow: none; background: transparent;
220 } 266 }
221 .src-table { width: 100%; border-collapse: separate; border-spacing: 0; } 267 .src-table { width: 100%; border-collapse: separate; border-spacing: 0; }
222 .src-table th { 268 .src-table th {
223 text-align: left; font-family: var(--font-jetbrains), ui-monospace, monospace; 269 text-align: left; font-family: var(--font-jetbrains), ui-monospace, monospace;
224 font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); 270 font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3);
225− padding: 12px 14px; border-bottom: 1.5px solid var(--ink); background: var(--surface-2); 271+ padding: 10px 12px 8px; border-bottom: 1px solid var(--line-strong); background: transparent;
226− white-space: nowrap; 272+ white-space: nowrap; font-weight: 500;
227 } 273 }
228−.src-table td { padding: 11px 14px; border-bottom: 1px solid var(--line); font-size: 13.5px; vertical-align: top; } 274+.src-table td { padding: 12px; border-bottom: 1px solid var(--line); font-size: 13.5px; vertical-align: top; }
229 .src-table tr:last-child td { border-bottom: none; } 275 .src-table tr:last-child td { border-bottom: none; }
230 .src-table tr:hover td { background: var(--surface-2); } 276 .src-table tr:hover td { background: var(--surface-2); }
231 277
278+/* flux de ventes (accueil) — rangée prix-d'abord */
279+.feed-row {
280+ display: block; border-bottom: 1px solid var(--line); padding: 14px 2px;
281+ transition: background 0.12s ease;
282+}
283+.feed-row:hover { background: var(--surface-2); }
284+
285+/* onglets de contrôle (période / catégorie) */
286+.vp-tab {
287+ font-family: var(--font-jetbrains), ui-monospace, monospace;
288+ font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
289+ color: var(--ink-3); padding: 8px 2px 6px; border: 0; background: transparent;
290+ border-bottom: 2px solid transparent; cursor: pointer; min-height: 34px;
291+ transition: color 0.12s ease, border-color 0.12s ease;
292+}
293+.vp-tab:hover { color: var(--ink); }
294+.vp-tab[aria-pressed="true"], .vp-tab[aria-selected="true"] {
295+ color: var(--ink); border-bottom-color: var(--accent);
296+}
297+
298+/* infobulle de graphique */
299+.vv-tip {
300+ position: absolute; pointer-events: none; z-index: 5;
301+ background: var(--ink-deep); color: var(--paper-on-ink);
302+ border-radius: 2px; padding: 8px 10px; min-width: 130px;
303+ font-family: var(--font-jetbrains), ui-monospace, monospace; font-size: 10.5px;
304+ box-shadow: 0 2px 10px rgba(20, 24, 20, 0.25);
305+}
306+.vv-tip .row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 1.5px 0; }
307+.vv-tip .key { display: inline-block; width: 10px; height: 2px; margin-right: 6px; vertical-align: middle; }
308+.vv-tip .val { font-weight: 700; color: #fff; }
309+
310+/* timeline méthodologie */
311+.tl { position: relative; padding-left: 26px; }
312+.tl::before {
313+ content: ""; position: absolute; left: 7px; top: 6px; bottom: 6px;
314+ width: 1px; background: var(--line-strong);
315+}
316+.tl-step { position: relative; padding: 0 0 26px; }
317+.tl-step:last-child { padding-bottom: 0; }
318+.tl-step::before {
319+ content: ""; position: absolute; left: -24px; top: 5px; width: 9px; height: 9px;
320+ background: var(--paper); border: 1.5px solid var(--ink); border-radius: 50%;
321+}
322+.tl-step[data-accent="true"]::before { background: var(--accent); border-color: var(--accent); }
323+
232 /* animations de révélation */ 324 /* animations de révélation */
233 @keyframes rise { 325 @keyframes rise {
234− from { opacity: 0; transform: translateY(12px); } 326+ from { opacity: 0; transform: translateY(10px); }
235 to { opacity: 1; transform: none; } 327 to { opacity: 1; transform: none; }
236 } 328 }
237−.rise { animation: rise 0.55s cubic-bezier(0.2, 0.8, 0.2, 1) both; } 329+.rise { animation: rise 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
238 @keyframes growX { from { transform: scaleX(0); } } 330 @keyframes growX { from { transform: scaleX(0); } }
239 .grow-x { transform-origin: left; animation: growX 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) both; } 331 .grow-x { transform-origin: left; animation: growX 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
240 @keyframes winPop { from { opacity: 0; } } 332 @keyframes winPop { from { opacity: 0; } }
@@ -243,13 +335,13 @@ select.vp-input {
243 .rise, .grow-x, .vv-win { animation: none; } 335 .rise, .grow-x, .vv-win { animation: none; }
244 } 336 }
245 337
246−/* barres horizontales (historique) */ 338+/* barres horizontales (historique) — fines, encre, millésime courant en rouge */
247−.hbar-track { background: var(--surface-2); border-radius: 0 4px 4px 0; height: 18px; overflow: hidden; } 339+.hbar-track { background: var(--line-soft); border-radius: 0; height: 12px; overflow: hidden; }
248 .hbar-fill { 340 .hbar-fill {
249− display: block; height: 100%; background: var(--green); border-radius: 0 4px 4px 0; 341+ display: block; height: 100%; background: var(--ink); border-radius: 0;
250 min-width: 2px; transition: background 0.12s ease; 342 min-width: 2px; transition: background 0.12s ease;
251 } 343 }
252−.hbar-row:hover .hbar-fill { background: var(--ink); box-shadow: inset 0 0 0 2px var(--lime); } 344+.hbar-row:hover .hbar-fill { background: var(--accent); }
253 345
254 346
255 /* Vrai-Prix Maps (framework Ka Maps par Groupe Ka) — recherche par carte */ 347 /* Vrai-Prix Maps (framework Ka Maps par Groupe Ka) — recherche par carte */
@@ -258,20 +350,20 @@ select.vp-input {
258 height: 62vh; 350 height: 62vh;
259 min-height: 420px; 351 min-height: 420px;
260 width: 100%; 352 width: 100%;
261− border: 1.5px solid var(--ink); 353+ border: 1px solid var(--line-strong);
262− border-radius: 16px; 354+ border-radius: 2px;
263 overflow: hidden; 355 overflow: hidden;
264− box-shadow: 6px 6px 0 rgba(20, 24, 20, 0.22); 356+ box-shadow: none;
265 z-index: 0; 357 z-index: 0;
266 } 358 }
267 .vpm-wrap .ka-map { 359 .vpm-wrap .ka-map {
268− --ka-accent: var(--lime); 360+ --ka-accent: var(--accent);
269 --ka-on-accent: #ffffff; 361 --ka-on-accent: #ffffff;
270 --ka-surface: var(--surface); 362 --ka-surface: var(--surface);
271 --ka-ink: var(--ink); 363 --ka-ink: var(--ink);
272− --ka-line: var(--ink); 364+ --ka-line: var(--line-strong);
273− --ka-radius: 10px; 365+ --ka-radius: 2px;
274− --ka-shadow: 4px 4px 0 rgba(20, 24, 20, 0.2); 366+ --ka-shadow: 0 2px 10px rgba(20, 24, 20, 0.18);
275 --ka-font: var(--font-inter), system-ui, sans-serif; 367 --ka-font: var(--font-inter), system-ui, sans-serif;
276 position: absolute; 368 position: absolute;
277 inset: 0; 369 inset: 0;
@@ -281,10 +373,10 @@ select.vp-input {
281 bottom: 12px; 373 bottom: 12px;
282 left: 12px; 374 left: 12px;
283 z-index: 9; 375 z-index: 9;
284− background: var(--surface); 376+ background: var(--ink-deep);
285− color: var(--ink); 377+ color: var(--paper-on-ink);
286− border: 1.5px solid var(--ink); 378+ border: 0;
287− border-radius: 999px; 379+ border-radius: 2px;
288 padding: 6px 12px; 380 padding: 6px 12px;
289 font-family: var(--font-jetbrains), monospace; 381 font-family: var(--font-jetbrains), monospace;
290 font-size: 10.5px; 382 font-size: 10.5px;
@@ -306,11 +398,11 @@ select.vp-input {
306 margin-top: 12px; 398 margin-top: 12px;
307 padding: 9px 14px; 399 padding: 9px 14px;
308 background: var(--ink); 400 background: var(--ink);
309− color: var(--lime); 401+ color: var(--paper);
310− border: 1.5px solid var(--ink); 402+ border: 1px solid var(--ink);
311− border-radius: 999px; 403+ border-radius: 2px;
312 font-size: 12.5px; 404 font-size: 12.5px;
313 font-weight: 600; 405 font-weight: 600;
314 cursor: pointer; 406 cursor: pointer;
315 } 407 }
316−.vpm-pop-cta:active { transform: translate(2px, 2px); } 408+.vpm-pop-cta:active { opacity: 0.85; }
modified src/app/layout.tsx +20 −14
@@ -111,25 +111,30 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
111 dangerouslySetInnerHTML={{ __html: JSON.stringify(ORG_JSONLD) }} 111 dangerouslySetInnerHTML={{ __html: JSON.stringify(ORG_JSONLD) }}
112 /> 112 />
113 <LangProvider> 113 <LangProvider>
114− {/* header opaque (standard header/menu KA 2026-08-22) */} 114+ {/* header opaque compact (standard header/menu KA 2026-08-22) */}
115− <header className="sticky top-0 z-[var(--z-header,500)] border-b-2 border-ink bg-paper"> 115+ <header className="sticky top-0 z-[var(--z-header,500)] border-b border-[var(--line-strong)] bg-paper">
116− <div className="relative mx-auto flex h-16 max-w-6xl items-center gap-5 px-4 sm:px-6"> 116+ <div className="relative mx-auto flex h-14 max-w-6xl items-center gap-6 px-4 sm:px-6">
117 <Link 117 <Link
118 href="/" 118 href="/"
119− className="vp-display group relative flex items-center text-[22px] font-bold leading-none tracking-[-0.04em] after:absolute after:-inset-2 after:content-[''] sm:text-[26px]" 119+ className="vp-display relative flex items-baseline gap-1.5 text-[19px] font-bold leading-none tracking-[-0.04em] after:absolute after:-inset-2 after:content-['']"
120 > 120 >
121− Vrai 121+ Vrai&nbsp;Prix
122− <span className="ml-1 rotate-[-2deg] rounded-md bg-ink px-[7px] pb-1 pt-[2px] text-lime transition-transform duration-200 group-hover:rotate-0"> 122+ <span
123− Prix 123+ aria-hidden="true"
124− </span> 124+ className="inline-block h-[7px] w-[7px] bg-accent"
125+ />
125 </Link> 126 </Link>
126 {/* badge Groupe KA — desktop seulement (l'espace manque sous xl ; 127 {/* badge Groupe KA — desktop seulement (l'espace manque sous xl ;
127 le menu mobile porte sa propre entrée) */} 128 le menu mobile porte sa propre entrée) */}
128 <span className="hidden xl:inline-flex"> 129 <span className="hidden xl:inline-flex">
129 <GroupeKaBadge /> 130 <GroupeKaBadge />
130 </span> 131 </span>
131− <div className="ml-auto flex items-center gap-2"> 132+ <div className="ml-auto flex items-center gap-3 sm:gap-4">
132 <HeaderNav /> 133 <HeaderNav />
134+ <span
135+ aria-hidden="true"
136+ className="hidden h-4 w-px bg-[var(--line)] lg:inline-block"
137+ />
133 <AccountButton /> 138 <AccountButton />
134 <LangToggle /> 139 <LangToggle />
135 </div> 140 </div>
@@ -147,11 +152,12 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
147 puis le bloc encre final = KaFooter (commun Groupe KA) */} 152 puis le bloc encre final = KaFooter (commun Groupe KA) */}
148 <div className="mx-auto max-w-6xl px-4 sm:px-6"> 153 <div className="mx-auto max-w-6xl px-4 sm:px-6">
149 <div className="border-t-2 border-ink pt-7 text-[13px] text-ink-2"> 154 <div className="border-t-2 border-ink pt-7 text-[13px] text-ink-2">
150− <p className="vp-display mb-3 text-[26px] font-bold tracking-[-0.04em] text-ink"> 155+ <p className="vp-display mb-3 flex items-baseline gap-2 text-[24px] font-bold tracking-[-0.04em] text-ink">
151− Vrai 156+ Vrai&nbsp;Prix
152− <span className="ml-1 inline-block rotate-[-2deg] rounded-md bg-ink px-[6px] pb-[3px] pt-[1px] text-lime"> 157+ <span
153− Prix 158+ aria-hidden="true"
154− </span> 159+ className="inline-block h-[8px] w-[8px] bg-accent"
160+ />
155 </p> 161 </p>
156 <p className="max-w-2xl"> 162 <p className="max-w-2xl">
157 Estimations bâties sur les rôles d&apos;évaluation foncière du 163 Estimations bâties sur les rôles d&apos;évaluation foncière du
modified src/app/methodologie/MethodologieClient.tsx +70 −46
@@ -192,71 +192,95 @@ export default function MethodologieClient() {
192 <span>{fr ? "Méthodologie" : "Methodology"}</span> 192 <span>{fr ? "Méthodologie" : "Methodology"}</span>
193 </nav> 193 </nav>
194 <span className="kicker">{fr ? "Le calcul, en entier" : "The math, in full"}</span> 194 <span className="kicker">{fr ? "Le calcul, en entier" : "The math, in full"}</span>
195− <h1 className="vp-display mt-3 max-w-3xl text-[clamp(30px,5vw,52px)] font-bold uppercase leading-[1] tracking-[-0.03em]"> 195+ <h1 className="vp-display mt-3 max-w-3xl text-[clamp(32px,5.5vw,58px)] font-bold uppercase leading-[0.98] tracking-[-0.03em]">
196 {fr ? ( 196 {fr ? (
197 <> 197 <>
198− Méthodologie <span className="hl">ultra-détaillée</span> 198+ Voici exactement
199+ <br />
200+ <span className="hl">comment le prix est calculé</span>
199 </> 201 </>
200 ) : ( 202 ) : (
201 <> 203 <>
202− The <span className="hl">full</span> methodology 204+ Here is exactly
205+ <br />
206+ <span className="hl">how the price is computed</span>
203 </> 207 </>
204 )} 208 )}
205 </h1> 209 </h1>
206− <p className="mt-4 max-w-2xl text-[16px] text-ink-2"> 210+ <p className="mt-4 max-w-2xl text-[15.5px] leading-relaxed text-ink-2">
207 {fr 211 {fr
208 ? "Notre différenciateur est la transparence totale. Cette page documente chaque étape — données, fusion, modèle, validation, moteur — au niveau de détail d'un rapport technique." 212 ? "Notre différenciateur est la transparence totale. Cette page documente chaque étape — données, fusion, modèle, validation, moteur — au niveau de détail d'un rapport technique."
209 : "Our differentiator is total transparency. This page documents every step — data, merge, model, validation, engine — at technical-report depth."} 213 : "Our differentiator is total transparency. This page documents every step — data, merge, model, validation, engine — at technical-report depth."}
210 </p> 214 </p>
211 215
212− <div className="mt-10 space-y-8"> 216+ {/* la chaîne de calcul, d'un coup d'œil */}
217+ <ol className="mt-9 grid gap-x-6 gap-y-4 border-y border-[var(--line)] py-5 sm:grid-cols-3 lg:grid-cols-6">
218+ {(fr
219+ ? ["Propriété identifiée", "Rôle d'évaluation récupéré", "Ventes comparables sélectionnées", "Caractéristiques ajustées", "Estimation calculée", "Intervalle de confiance produit"]
220+ : ["Property identified", "Assessment roll retrieved", "Comparable sales selected", "Characteristics adjusted", "Estimate computed", "Confidence interval produced"]
221+ ).map((step, i, arr) => (
222+ <li key={step} className="relative flex items-start gap-2.5 lg:block">
223+ <span className={`vp-mono text-[11px] font-bold ${i === arr.length - 1 ? "text-accent" : "text-ink-3"}`}>
224+ 0{i + 1}
225+ </span>
226+ <p className="vp-display text-[13px] font-bold uppercase leading-tight tracking-[-0.01em] lg:mt-1.5">
227+ {step}
228+ {i < arr.length - 1 && <span className="ml-1.5 text-ink-3" aria-hidden="true">→</span>}
229+ </p>
230+ </li>
231+ ))}
232+ </ol>
233+
234+ <div className="mt-12 space-y-12">
213 {sections.map((sct) => ( 235 {sections.map((sct) => (
214− <section key={sct.num} className="vp-card p-6 sm:p-8"> 236+ <section key={sct.num} className="sec grid gap-x-10 gap-y-3 lg:grid-cols-[110px_1fr]">
215− <div className="flex items-baseline gap-4"> 237+ <span className="vp-display text-[clamp(26px,3vw,36px)] font-bold leading-none text-accent lg:pt-1">
216− <span className="vp-mono text-[13px] font-bold text-green">{sct.num}</span> 238+ {sct.num}
217− <h2 className="vp-display text-[20px] font-bold uppercase tracking-[-0.02em]"> 239+ </span>
240+ <div>
241+ <h2 className="vp-display text-[clamp(19px,2.4vw,24px)] font-bold uppercase tracking-[-0.02em]">
218 {sct.title} 242 {sct.title}
219 </h2> 243 </h2>
220− </div> 244+ <div className="mt-4 space-y-3.5">
221− <div className="mt-4 space-y-3.5"> 245+ {sct.paras.map((p, i) => (
222− {sct.paras.map((p, i) => ( 246+ <p key={i} className="max-w-3xl text-[14.5px] leading-relaxed text-ink-2">
223− <p key={i} className="max-w-3xl text-[14.5px] leading-relaxed text-ink-2"> 247+ {p}
224− {p} 248+ </p>
225− </p>
226− ))}
227− </div>
228− {sct.kv && (
229− <div className="mt-5 grid grid-cols-2 gap-2.5 sm:grid-cols-3">
230− {sct.kv.map(([k, v]) => (
231− <div key={k} className="kv-cell">
232− <p className="k">{k}</p>
233− <p className="v">{v}</p>
234− </div>
235 ))} 249 ))}
236 </div> 250 </div>
237− )} 251+ {sct.kv && (
238− {sct.table && ( 252+ <div className="mt-5 grid grid-cols-2 gap-x-6 gap-y-2 sm:grid-cols-3">
239− <div className="src-wrap mt-5"> 253+ {sct.kv.map(([k, v]) => (
240− <table className="src-table"> 254+ <div key={k} className="kv-cell">
241− <thead> 255+ <p className="k">{k}</p>
242− <tr> 256+ <p className="v">{v}</p>
243− {sct.table.head.map((h) => ( 257+ </div>
244− <th key={h}>{h}</th> 258+ ))}
245− ))} 259+ </div>
246− </tr> 260+ )}
247− </thead> 261+ {sct.table && (
248− <tbody> 262+ <div className="src-wrap mt-6 max-w-3xl">
249− {sct.table.rows.map(([a, b, c]) => ( 263+ <table className="src-table">
250− <tr key={a}> 264+ <thead>
251− <td className="font-semibold">{a}</td> 265+ <tr>
252− <td className="vp-mono text-[12.5px]">{b}</td> 266+ {sct.table.head.map((h) => (
253− <td className="vp-mono text-[12.5px]">{c}</td> 267+ <th key={h}>{h}</th>
268+ ))}
254 </tr> 269 </tr>
255− ))} 270+ </thead>
256− </tbody> 271+ <tbody>
257− </table> 272+ {sct.table.rows.map(([a, b, c]) => (
258− </div> 273+ <tr key={a}>
259− )} 274+ <td className="font-semibold">{a}</td>
275+ <td className="vp-mono text-[12.5px]">{b}</td>
276+ <td className="vp-mono text-[12.5px]">{c}</td>
277+ </tr>
278+ ))}
279+ </tbody>
280+ </table>
281+ </div>
282+ )}
283+ </div>
260 </section> 284 </section>
261 ))} 285 ))}
262 </div> 286 </div>
modified src/app/municipalite/[slug]/[type]/page.tsx +2 −2
@@ -164,7 +164,7 @@ export default async function MunTypePage({
164 <li key={x.type}> 164 <li key={x.type}>
165 <Link 165 <Link
166 href={`/municipalite/${slug}/${TYPE_SLUGS[x.type] ?? x.type}`} 166 href={`/municipalite/${slug}/${TYPE_SLUGS[x.type] ?? x.type}`}
167− className="stat-chip hover:bg-lime" 167+ className="stat-chip transition-colors hover:text-accent-deep"
168 > 168 >
169 {TYPE_LABELS[x.type]?.pluriel ?? x.type} 169 {TYPE_LABELS[x.type]?.pluriel ?? x.type}
170 <b>{numFr(x.n)}</b> 170 <b>{numFr(x.n)}</b>
@@ -172,7 +172,7 @@ export default async function MunTypePage({
172 </li> 172 </li>
173 ))} 173 ))}
174 <li> 174 <li>
175− <Link href={`/municipalite/${slug}`} className="stat-chip hover:bg-lime"> 175+ <Link href={`/municipalite/${slug}`} className="stat-chip transition-colors hover:text-accent-deep">
176 ← Tout {mun.nom} 176 ← Tout {mun.nom}
177 </Link> 177 </Link>
178 </li> 178 </li>
modified src/app/municipalite/[slug]/page.tsx +1 −1
@@ -219,7 +219,7 @@ export default async function MunicipalitePage({
219 <li key={v.slug}> 219 <li key={v.slug}>
220 <Link 220 <Link
221 href={`/municipalite/${v.slug}`} 221 href={`/municipalite/${v.slug}`}
222− className="stat-chip hover:bg-lime" 222+ className="stat-chip transition-colors hover:text-accent-deep"
223 > 223 >
224 {v.nom} 224 {v.nom}
225 </Link> 225 </Link>
modified src/app/page.tsx +21 −16
@@ -36,11 +36,11 @@ const TOP_VILLES = (
36 stats as { par_ville: { ville: string; n: number; mediane: number | null }[] } 36 stats as { par_ville: { ville: string; n: number; mediane: number | null }[] }
37 ).par_ville.slice(0, 24); 37 ).par_ville.slice(0, 24);
38 38
39−/** Séries d'indice par grand type — 25 derniers mois, avec variations. */ 39+/** Séries d'indice par grand type — historique complet (le client fenêtre 12/24/tout). */
40 function buildPulse(): PulseItem[] { 40 function buildPulse(): PulseItem[] {
41 return (["unifamilial", "condo", "plex"] as const).map((type) => { 41 return (["unifamilial", "condo", "plex"] as const).map((type) => {
42 const full = getMarketIndex(type); 42 const full = getMarketIndex(type);
43− const series = full.slice(-25); 43+ const series = full;
44 const last = full.length ? full[full.length - 1].idx : null; 44 const last = full.length ? full[full.length - 1].idx : null;
45 const yearAgo = full.length >= 13 ? full[full.length - 13].idx : null; 45 const yearAgo = full.length >= 13 ? full[full.length - 13].idx : null;
46 const first = full.length >= 2 ? full[0].idx : null; 46 const first = full.length >= 2 ? full[0].idx : null;
@@ -89,33 +89,38 @@ export default function Home() {
89 /> 89 />
90 {/* ---- Explorer par municipalité (maillage interne indexable) ---- */} 90 {/* ---- Explorer par municipalité (maillage interne indexable) ---- */}
91 <section className="mt-14"> 91 <section className="mt-14">
92− <span className="kicker">Explorer par municipalité</span> 92+ <header className="sec">
93− <h2 className="vp-display mt-3 text-[clamp(22px,3.6vw,34px)] font-bold uppercase leading-[1.05] tracking-[-0.03em]"> 93+ <span className="sec-num">05 — Explorer par municipalité</span>
94− La valeur des propriétés, ville par ville 94+ <h2 className="vp-display mt-2 max-w-3xl text-[clamp(24px,3.6vw,36px)] font-bold uppercase leading-[1.02] tracking-[-0.03em]">
95− </h2> 95+ La valeur des propriétés, ville par ville
96− <p className="mt-3 max-w-2xl text-[15px] text-ink-2"> 96+ </h2>
97− Statistiques d&apos;évaluation, valeur médiane et propriétés estimées 97+ <p className="mt-3 max-w-2xl text-[14.5px] leading-relaxed text-ink-2">
98− pour chacune des 1 097 municipalités du Québec. 98+ Statistiques d&apos;évaluation, valeur médiane et propriétés estimées
99− </p> 99+ pour chacune des 1 097 municipalités du Québec.
100− <ul className="mt-6 grid grid-cols-2 gap-x-6 gap-y-2 sm:grid-cols-3 lg:grid-cols-4"> 100+ </p>
101+ </header>
102+ <ul className="mt-6 grid grid-cols-2 gap-x-8 sm:grid-cols-3 lg:grid-cols-4">
101 {TOP_VILLES.map((v) => ( 103 {TOP_VILLES.map((v) => (
102 <li key={v.ville}> 104 <li key={v.ville}>
103 <Link 105 <Link
104 href={`/municipalite/${slugify(v.ville)}`} 106 href={`/municipalite/${slugify(v.ville)}`}
105− className="group flex items-baseline justify-between gap-2 border-b border-[rgba(20,24,20,0.12)] py-1.5 text-[14px]" 107+ className="group flex items-baseline justify-between gap-2 border-b border-[rgba(20,24,20,0.1)] py-2 text-[13.5px]"
106 > 108 >
107− <span className="font-medium group-hover:underline"> 109+ <span className="font-medium group-hover:text-accent-deep">
108 {v.ville} 110 {v.ville}
109 </span> 111 </span>
110− <span className="vp-mono text-[11px] text-ink-2"> 112+ <span className="vp-mono text-[10.5px] text-ink-3">
111 {v.mediane != null ? moneyFr(v.mediane) : ""} 113 {v.mediane != null ? moneyFr(v.mediane) : ""}
112 </span> 114 </span>
113 </Link> 115 </Link>
114 </li> 116 </li>
115 ))} 117 ))}
116 </ul> 118 </ul>
117− <p className="mt-5"> 119+ <p className="mt-6">
118− <Link href="/municipalites" className="btn btn-ghost"> 120+ <Link
121+ href="/municipalites"
122+ className="vp-mono border-b border-accent pb-px text-[11px] uppercase tracking-[0.08em] text-accent-deep hover:text-accent"
123+ >
119 Toutes les municipalités → 124 Toutes les municipalités →
120 </Link> 125 </Link>
121 </p> 126 </p>
modified src/components/AccountButton.tsx +5 −5
@@ -45,7 +45,7 @@ export default function AccountButton() {
45 return ( 45 return (
46 <a 46 <a
47 href={`/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`} 47 href={`/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`}
48− className="vp-mono inline-flex min-h-[36px] items-center rounded-full border-[1.5px] border-ink bg-ink px-4 py-1.5 text-[12px] font-bold uppercase tracking-[0.08em] text-lime transition-all hover:bg-green-deep" 48+ className="vp-mono inline-flex min-h-[36px] items-center rounded-[2px] bg-ink px-3.5 py-1.5 text-[11px] font-medium uppercase tracking-[0.1em] text-paper transition-colors hover:bg-accent"
49 title={fr ? "Se connecter avec KA ID" : "Sign in with KA ID"} 49 title={fr ? "Se connecter avec KA ID" : "Sign in with KA ID"}
50 > 50 >
51 KA&nbsp;ID 51 KA&nbsp;ID
@@ -57,7 +57,7 @@ export default function AccountButton() {
57 return ( 57 return (
58 <Link 58 <Link
59 href="/compte" 59 href="/compte"
60− className="group inline-flex min-h-[36px] items-center gap-2 rounded-full border-[1.5px] border-ink bg-surface py-1 pl-1 pr-3 transition-all hover:bg-lime-soft" 60+ className="group inline-flex min-h-[36px] items-center gap-2 py-1"
61 title={fr ? "Mon compte — carte de membre Groupe KA" : "My account — Groupe KA member card"} 61 title={fr ? "Mon compte — carte de membre Groupe KA" : "My account — Groupe KA member card"}
62 > 62 >
63 {user.picture ? ( 63 {user.picture ? (
@@ -66,14 +66,14 @@ export default function AccountButton() {
66 src={user.picture} 66 src={user.picture}
67 alt="" 67 alt=""
68 referrerPolicy="no-referrer" 68 referrerPolicy="no-referrer"
69− className="h-7 w-7 rounded-full border border-ink object-cover" 69+ className="h-7 w-7 rounded-full object-cover grayscale transition-[filter] group-hover:grayscale-0"
70 /> 70 />
71 ) : ( 71 ) : (
72− <span className="vp-display flex h-7 w-7 items-center justify-center rounded-full bg-ink text-[13px] font-bold text-lime"> 72+ <span className="vp-display flex h-7 w-7 items-center justify-center rounded-full bg-ink text-[13px] font-bold text-paper">
73 {prenom.charAt(0).toUpperCase()} 73 {prenom.charAt(0).toUpperCase()}
74 </span> 74 </span>
75 )} 75 )}
76− <span className="vp-display max-w-[92px] truncate text-[13px] font-bold text-ink"> 76+ <span className="vp-display max-w-[92px] truncate border-b-2 border-transparent text-[13px] font-bold text-ink group-hover:border-accent">
77 {prenom} 77 {prenom}
78 </span> 78 </span>
79 </Link> 79 </Link>
modified src/components/CookieConsent.tsx +5 −5
@@ -164,8 +164,8 @@ export default function CookieConsent() {
164 <> 164 <>
165 {showBanner && ( 165 {showBanner && (
166 <div className="fixed inset-x-0 bottom-0 z-[var(--z-toast,950)] p-3 pb-[calc(12px+env(safe-area-inset-bottom))] sm:p-5 sm:pb-[calc(20px+env(safe-area-inset-bottom))]"> 166 <div className="fixed inset-x-0 bottom-0 z-[var(--z-toast,950)] p-3 pb-[calc(12px+env(safe-area-inset-bottom))] sm:p-5 sm:pb-[calc(20px+env(safe-area-inset-bottom))]">
167− <div className="mx-auto max-w-3xl rounded-[10px] border-[1.5px] border-ink bg-ink p-5 text-paper shadow-[0_-8px_32px_rgba(16,18,16,0.35),6px_6px_0_rgba(20,24,20,0.3)]"> 167+ <div className="mx-auto max-w-3xl rounded-[2px] bg-[var(--ink-deep)] p-5 text-paper shadow-[0_-8px_32px_rgba(16,18,16,0.35)]">
168− <p className="klabel !text-lime">🍪 {t.prefsTitle}</p> 168+ <p className="klabel !text-[var(--accent-bright)]">🍪 {t.prefsTitle}</p>
169 <p className="mt-2 text-[13.5px] leading-relaxed text-[rgba(245,243,238,0.85)]"> 169 <p className="mt-2 text-[13.5px] leading-relaxed text-[rgba(245,243,238,0.85)]">
170 {t.banner}{" "} 170 {t.banner}{" "}
171 <a href="/confidentialite" className="border-b border-lime text-lime"> 171 <a href="/confidentialite" className="border-b border-lime text-lime">
@@ -173,7 +173,7 @@ export default function CookieConsent() {
173 </a> 173 </a>
174 </p> 174 </p>
175 <div className="mt-4 flex flex-wrap gap-2.5"> 175 <div className="mt-4 flex flex-wrap gap-2.5">
176− <button className="btn border-lime bg-lime text-ink" onClick={() => persist(true, true)}> 176+ <button className="btn btn-accent" onClick={() => persist(true, true)}>
177 {t.acceptAll} 177 {t.acceptAll}
178 </button> 178 </button>
179 <button 179 <button
@@ -199,7 +199,7 @@ export default function CookieConsent() {
199 onClick={() => setPanel(false)} 199 onClick={() => setPanel(false)}
200 > 200 >
201 <div 201 <div
202− className="w-full max-w-lg rounded-[10px] border-[1.5px] border-ink bg-surface p-6 shadow-[8px_8px_0_rgba(20,24,20,0.3)]" 202+ className="w-full max-w-lg rounded-[2px] border border-[var(--line-strong)] bg-surface p-6 shadow-[0_12px_40px_rgba(20,24,20,0.3)]"
203 onClick={(e) => e.stopPropagation()} 203 onClick={(e) => e.stopPropagation()}
204 role="dialog" 204 role="dialog"
205 aria-modal="true" 205 aria-modal="true"
@@ -214,7 +214,7 @@ export default function CookieConsent() {
214 { k: t.prefs, d: t.prefsDesc, on: prefs, lock: false, set: setPrefs }, 214 { k: t.prefs, d: t.prefsDesc, on: prefs, lock: false, set: setPrefs },
215 { k: t.analytics, d: t.analyticsDesc, on: analytics, lock: false, set: setAnalytics }, 215 { k: t.analytics, d: t.analyticsDesc, on: analytics, lock: false, set: setAnalytics },
216 ].map((row) => ( 216 ].map((row) => (
217− <div key={row.k} className="flex items-start justify-between gap-4 rounded-lg border-[1.5px] border-[rgba(20,24,20,0.14)] bg-surface-2 p-4"> 217+ <div key={row.k} className="flex items-start justify-between gap-4 border-t border-[var(--line)] p-4 pl-0">
218 <div> 218 <div>
219 <p className="vp-display text-[14px] font-bold"> 219 <p className="vp-display text-[14px] font-bold">
220 {row.k} 220 {row.k}
modified src/components/HeaderNav.tsx +34 −31
@@ -40,16 +40,16 @@ export default function HeaderNav() {
40 40
41 return ( 41 return (
42 <> 42 <>
43− {/* nav pilule — desktop */} 43+ {/* nav texte — desktop : soulignement rouge = page courante */}
44− <nav className="hidden items-center gap-1 lg:flex"> 44+ <nav className="hidden items-center gap-5 lg:flex">
45 {items.map((it) => ( 45 {items.map((it) => (
46 <Link 46 <Link
47 key={it.href} 47 key={it.href}
48 href={it.href} 48 href={it.href}
49− className={`vp-display inline-flex min-h-[38px] items-center rounded-full border-[1.5px] px-3.5 py-1.5 text-[13.5px] font-bold transition-all ${ 49+ className={`vp-mono inline-flex min-h-[38px] items-center border-b-2 pt-[2px] text-[11px] font-medium uppercase tracking-[0.1em] transition-colors ${
50 isActive(it.href) 50 isActive(it.href)
51− ? "border-ink bg-ink text-lime" 51+ ? "border-accent text-ink"
52− : "border-transparent text-ink-2 hover:border-ink hover:text-ink" 52+ : "border-transparent text-ink-2 hover:text-ink"
53 }`} 53 }`}
54 > 54 >
55 {it.label} 55 {it.label}
@@ -71,39 +71,36 @@ export default function HeaderNav() {
71 <label 71 <label
72 htmlFor="vp-menu" 72 htmlFor="vp-menu"
73 aria-hidden="true" 73 aria-hidden="true"
74− className="flex h-11 w-11 cursor-pointer flex-col items-center justify-center gap-[5px] rounded-full border-[1.5px] border-ink bg-surface active:bg-lime-soft" 74+ className="flex h-11 w-11 cursor-pointer flex-col items-center justify-center gap-[5px]"
75 > 75 >
76− <span className="h-[2px] w-[18px] bg-ink" /> 76+ <span className="h-px w-[20px] bg-ink" />
77− <span className="h-[2px] w-[18px] bg-ink" /> 77+ <span className="h-px w-[20px] bg-ink" />
78− <span className="h-[2px] w-[18px] bg-ink" /> 78+ <span className="h-px w-[12px] self-center ml-[8px] bg-accent" />
79 </label> 79 </label>
80 80
81− {/* panneau plein écran — sibling du checkbox, affiché via peer-checked */} 81+ {/* panneau plein écran encre — sibling du checkbox, affiché via peer-checked */}
82 <div 82 <div
83− className="fixed inset-0 z-[var(--z-modal,900)] hidden flex-col overflow-y-auto peer-checked:flex" 83+ className="vp-dark fixed inset-0 z-[var(--z-modal,900)] hidden flex-col overflow-y-auto peer-checked:flex"
84− style={{ background: "var(--paper)" }}
85 role="dialog" 84 role="dialog"
86 aria-modal="true" 85 aria-modal="true"
87 aria-label="Menu" 86 aria-label="Menu"
88 > 87 >
89 {/* barre du haut : marque + fermer */} 88 {/* barre du haut : marque + fermer */}
90− <div 89+ <div className="flex h-14 shrink-0 items-center justify-between border-b border-[rgba(245,243,238,0.18)] px-4">
91− className="flex h-16 shrink-0 items-center justify-between border-b-2 border-ink px-4"
92− style={{ background: "var(--paper)" }}
93− >
94 <Link 90 <Link
95 href="/" 91 href="/"
96− className="vp-display flex items-center text-[22px] font-bold leading-none tracking-[-0.04em]" 92+ className="vp-display flex items-baseline gap-1.5 text-[19px] font-bold leading-none tracking-[-0.04em] text-[var(--paper-on-ink)]"
97 > 93 >
98− Vrai 94+ Vrai&nbsp;Prix
99− <span className="ml-1 rotate-[-2deg] rounded-md bg-ink px-[7px] pb-1 pt-[2px] text-lime"> 95+ <span
100− Prix 96+ aria-hidden="true"
101− </span> 97+ className="inline-block h-[7px] w-[7px] bg-[var(--accent-bright)]"
98+ />
102 </Link> 99 </Link>
103 <label 100 <label
104 htmlFor="vp-menu" 101 htmlFor="vp-menu"
105 aria-label={fr ? "Fermer le menu" : "Close menu"} 102 aria-label={fr ? "Fermer le menu" : "Close menu"}
106− className="vp-display flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border-[1.5px] border-ink bg-ink text-[17px] font-bold text-lime" 103+ className="vp-mono flex h-11 w-11 cursor-pointer items-center justify-center text-[15px] text-[var(--paper-on-ink)]"
107 > 104 >
108 ✕ 105 ✕
109 </label> 106 </label>
@@ -115,37 +112,43 @@ export default function HeaderNav() {
115 <Link 112 <Link
116 key={it.href} 113 key={it.href}
117 href={it.href} 114 href={it.href}
118− className="group flex items-baseline gap-4 border-b-[1.5px] border-dashed border-[rgba(20,24,20,0.18)] py-5 last:border-0" 115+ className="group flex items-baseline gap-4 border-b border-[rgba(245,243,238,0.12)] py-[18px] last:border-0"
119 > 116 >
120− <span className="vp-mono text-[12px] font-bold text-green"> 117+ <span className="vp-mono text-[11px] font-medium text-[var(--accent-bright)]">
121 0{i + 1} 118 0{i + 1}
122 </span> 119 </span>
123 <span 120 <span
124− className={`vp-display text-[29px] font-bold uppercase leading-none tracking-[-0.03em] ${ 121+ className={`vp-display text-[28px] font-bold uppercase leading-none tracking-[-0.03em] ${
125 isActive(it.href) 122 isActive(it.href)
126− ? "rounded-md bg-ink px-2.5 py-1 text-lime" 123+ ? "text-[var(--accent-bright)]"
127− : "text-ink group-active:text-green-deep" 124+ : "text-[var(--paper-on-ink)] group-active:text-[var(--accent-bright)]"
128 }`} 125 }`}
129 > 126 >
130 {it.label} 127 {it.label}
131 </span> 128 </span>
129+ {isActive(it.href) && (
130+ <span
131+ aria-hidden="true"
132+ className="mb-[2px] inline-block h-[6px] w-[6px] self-center bg-[var(--accent-bright)]"
133+ />
134+ )}
132 </Link> 135 </Link>
133 ))} 136 ))}
134 </nav> 137 </nav>
135 138
136 {/* pied du menu */} 139 {/* pied du menu */}
137− <div className="shrink-0 border-t-2 border-ink px-6 pb-[max(20px,env(safe-area-inset-bottom))] pt-4"> 140+ <div className="shrink-0 border-t border-[rgba(245,243,238,0.18)] px-6 pb-[max(20px,env(safe-area-inset-bottom))] pt-4">
138 {/* entrée « Un service Groupe KA » du menu mobile */} 141 {/* entrée « Un service Groupe KA » du menu mobile */}
139− <div className="mb-3"> 142+ <div className="mb-4">
140 <GroupeKaBadge /> 143 <GroupeKaBadge />
141 </div> 144 </div>
142 <button 145 <button
143 onClick={() => setLang(fr ? "en" : "fr")} 146 onClick={() => setLang(fr ? "en" : "fr")}
144− className="vp-mono mb-3 rounded-full border-[1.5px] border-ink bg-surface px-4 py-2 text-[12px] font-bold uppercase tracking-[0.08em]" 147+ className="vp-mono mb-4 border-b border-[var(--paper-on-ink)] pb-1 text-[12px] font-medium uppercase tracking-[0.1em] text-[var(--paper-on-ink)]"
145 > 148 >
146 {fr ? "English version" : "Version française"} 149 {fr ? "English version" : "Version française"}
147 </button> 150 </button>
148− <div className="vp-mono flex flex-wrap gap-x-5 gap-y-1.5 text-[10.5px] uppercase tracking-[0.06em] text-ink-3"> 151+ <div className="vp-mono flex flex-wrap gap-x-5 gap-y-1.5 text-[10.5px] uppercase tracking-[0.06em] text-[rgba(245,243,238,0.55)]">
149 <Link href="/conditions">{fr ? "Conditions" : "Terms"}</Link> 152 <Link href="/conditions">{fr ? "Conditions" : "Terms"}</Link>
150 <Link href="/confidentialite"> 153 <Link href="/confidentialite">
151 {fr ? "Confidentialité" : "Privacy"} 154 {fr ? "Confidentialité" : "Privacy"}
modified src/components/KaChat.tsx +5 −5
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/LangContext.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/LeadForm.tsx +7 −5
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/MetricViz.tsx +428 −184
Large diff (915 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/ResultView.tsx +185 −158
Large diff (549 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/SearchBox.tsx +101 −29
Large diff (165 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/StatsDashboard.tsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified src/components/StatsView.tsx +4 −4
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme