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 1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 2 "use client";
3 +import { useState } from "react";
3 4 import Link from "next/link";
4 5 import SearchBox from "@/components/SearchBox";
5 6 import EstimateForm from "@/components/EstimateForm";
6 −import { IndexSpark, locNum } from "@/components/MetricViz";
7 +import { locNum, MarketPulseChart } from "@/components/MetricViz";
7 8 import { useLang } from "@/components/LangContext";
8 9
9 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 32 condo: { fr: "Condo", en: "Condo" },
32 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 }) {
36 − if (pct == null) return null;
42 +/** Variation signée — l'encre porte le positif, le rouge danger le négatif. */
43 +function Pct({ v, digits = 1 }: { v: number | null; digits?: number }) {
44 + if (v == null) return <span className="text-ink-3">—</span>;
37 45 return (
38 − <span
39 − className={`vp-mono rounded-[4px] border border-ink px-1.5 py-0.5 text-[10px] font-bold ${
40 − pct >= 0 ? "bg-lime text-ink" : "bg-danger text-white"
41 − }`}
42 − >
43 − {label} {pct >= 0 ? "+" : ""}
44 − {pct.toFixed(1)} %
46 + <span className={v >= 0 ? "text-ink" : "text-[var(--danger)]"}>
47 + {v >= 0 ? "+" : "−"}
48 + {Math.abs(v).toFixed(digits)}&nbsp;%
45 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 86 export default function HomeClient({
50 87 pulse,
51 88 recent,
@@ -57,6 +94,7 @@ export default function HomeClient({
57 94 }) {
58 95 const { lang, t } = useLang();
59 96 const fr = lang === "fr";
97 + const [period, setPeriod] = useState<(typeof PERIODS)[number]["key"]>("24");
60 98 const money = (v: number) =>
61 99 new Intl.NumberFormat(fr ? "fr-CA" : "en-CA", {
62 100 style: "currency",
@@ -69,233 +107,315 @@ export default function HomeClient({
69 107 month: "short",
70 108 });
71 109
72 − return (
73 − <div className="pb-6 pt-10 sm:pt-14">
74 − {/* ---- Héros ---- */}
75 − <section>
76 − <span className="kicker">
77 − {lang === "fr" ? "Québec · 2021-2026" : "Québec · 2021-2026"}
78 − </span>
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>
110 + const months = PERIODS.find((p) => p.key === period)?.months ?? 24;
111 + const chartData = pulse.map((p) => ({
112 + key: p.type,
113 + label: PULSE_LABELS[p.type][lang],
114 + color: PULSE_COLORS[p.type],
115 + series: p.series,
116 + }));
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">
97 − <span className="stat-chip">
98 − <span className="pulse" />
99 − <b>3 747 008</b> {lang === "fr" ? "propriétés" : "properties"}
100 − </span>
101 − <span className="stat-chip">
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>
118 + const proofs: [string, string][] = [
119 + ["3 747 008", fr ? "propriétés estimées" : "estimated properties"],
120 + ["745 119", fr ? "ventes réelles 2021-2026" : "real sales 2021-2026"],
121 + [locNum(salesLast30d, lang, { maxFrac: 0 }), fr ? "ventes captées / 30 jours" : "sales captured / 30 days"],
122 + ["1 100", fr ? "municipalités couvertes" : "municipalities covered"],
123 + ];
116 124
117 − {/* ---- Recherche ---- */}
118 − <section className="mt-9">
119 − <div className="vp-card p-4 sm:p-5">
120 − <p className="klabel mb-2 pl-0.5">
121 − {lang === "fr" ? "Adresse de la propriété" : "Property address"}
125 + return (
126 + <div className="pb-6">
127 + {/* ================= HÉRO — terminal encre pleine largeur ================= */}
128 + <section className="bleed vp-dark">
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 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 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 185 </section>
129 186
130 − {/* ---- Comment ça marche (bande compacte) ---- */}
131 − <section className="mt-10 grid gap-3 sm:grid-cols-3">
132 − {(fr
133 − ? [
134 − ["01", "Cherchez l'adresse", "3,7 M d'unités d'évaluation, recherche tolérante aux fautes."],
135 − ["02", "Le moteur croise deux sources", "Modèle hédonique (690 k ventes) + comparables réels ajustés en dollars."],
136 − ["03", "Fourchette + confiance", "P10-P90, indice A-D, rapport PDF — le calcul montré en entier."],
137 − ]
138 − : [
139 − ["01", "Search the address", "3.7M assessment units, typo-tolerant search."],
140 − ["02", "Two sources, crossed", "Hedonic model (690k sales) + real comparables adjusted in dollars."],
141 − ["03", "Range + confidence", "P10-P90, A-D index, PDF report — the full math, shown."],
142 − ]
143 − ).map(([num, title, body], i) => (
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">
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">
146 − {num}
147 − </span>
148 − <div>
149 − <p className="vp-display text-[14.5px] font-bold uppercase tracking-[-0.01em]">
150 − {title}
151 − {i < 2 && <span className="ml-2 text-ink-3">→</span>}
187 + {/* ============ le principe, en une ligne (pas de boîtes) ============ */}
188 + <section className="border-b border-[var(--line)] py-6">
189 + <div className="grid gap-x-8 gap-y-4 sm:grid-cols-3">
190 + {(fr
191 + ? [
192 + ["01", "Cherchez l'adresse", "3,7 M d'unités d'évaluation, recherche tolérante aux fautes."],
193 + ["02", "Deux sources croisées", "Modèle hédonique (690 k ventes) + comparables réels ajustés en dollars."],
194 + ["03", "Fourchette + confiance", "P10-P90, indice A-D — le calcul montré en entier."],
195 + ]
196 + : [
197 + ["01", "Search the address", "3.7M assessment units, typo-tolerant search."],
198 + ["02", "Two sources, crossed", "Hedonic model (690k sales) + real comparables adjusted in dollars."],
199 + ["03", "Range + confidence", "P10-P90, A-D index — the full math, shown."],
200 + ]
201 + ).map(([num, title, body]) => (
202 + <div key={num} className="flex items-start gap-3">
203 + <span className="vp-mono pt-[3px] text-[11px] font-bold text-accent">{num}</span>
204 + <p className="text-[13px] leading-snug text-ink-2">
205 + <span className="vp-display block text-[14px] font-bold uppercase tracking-[-0.01em] text-ink">
206 + {title}
207 + </span>
208 + {body}
152 209 </p>
153 − <p className="mt-1 text-[12.5px] leading-snug text-ink-2">{body}</p>
154 210 </div>
155 − </div>
156 − ))}
211 + ))}
212 + </div>
157 213 </section>
158 214
159 − {/* ---- Pouls du marché ---- */}
160 − <section className="mt-14">
161 − <span className="kicker">{fr ? "Indice Vrai-Prix" : "Vrai-Prix index"}</span>
162 − <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]">
163 − {fr ? "Le pouls du marché québécois" : "The pulse of the Québec market"}
164 − </h2>
165 − <p className="mt-2 max-w-2xl text-[14px] text-ink-2">
166 − {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."
168 − : "The monthly index built from real sales — the same one adjusting every estimate's comparables. Base 100 twenty-four months ago."}
169 − </p>
170 − <div className="mt-4 grid gap-4 sm:grid-cols-3">
215 + {/* ================= MARCHÉ — data visualization éditoriale ================= */}
216 + <section className="mt-12">
217 + <SectionHead
218 + num="01"
219 + kicker={fr ? "Indice Vrai-Prix" : "Vrai-Prix index"}
220 + title={fr ? "Le pouls du marché québécois" : "The pulse of the Québec market"}
221 + lede={
222 + fr
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."
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."
225 + }
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 260 {pulse.map((p) => (
172 − <div key={p.type} className="vp-card vp-card-hover p-5">
173 − <div className="flex items-center justify-between gap-2">
174 − <p className="vp-display text-[16px] font-bold uppercase tracking-[-0.01em]">
175 − {PULSE_LABELS[p.type][lang]}
176 − </p>
177 − <TrendBadge pct={p.pct12m} label={fr ? "12 mois" : "12 mo"} />
178 − </div>
179 − <div className="mt-3">
180 − <IndexSpark series={p.series} />
181 − </div>
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)} %
261 + <div key={p.type} className="flex items-baseline justify-between gap-4 sm:block">
262 + <p className="vp-mono flex items-center gap-1.5 text-[10.5px] font-bold uppercase tracking-[0.08em] text-ink">
263 + <span className="inline-block h-[2px] w-[12px]" style={{ background: PULSE_COLORS[p.type] }} />
264 + {PULSE_LABELS[p.type][lang]}
265 + </p>
266 + <p className="vp-mono mt-1 flex gap-4 text-[11px] text-ink-2">
267 + <span>
268 + 12&nbsp;{fr ? "mois" : "mo"}&nbsp;
269 + <b className="vp-display text-[14px]">
270 + <Pct v={p.pct12m} />
188 271 </b>
189 − </p>
190 − )}
272 + </span>
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 280 </div>
192 281 ))}
193 282 </div>
194 283 </section>
195 284
196 − {/* ---- Dernières ventes captées ---- */}
285 + {/* ================= VENTES RÉELLES — flux prix-d'abord ================= */}
197 286 {recent.length > 0 && (
198 287 <section className="mt-14">
199 − <span className="kicker">
200 − <span className="pulse" style={{ width: 8, height: 8 }} />
201 − {fr ? "Flux en continu" : "Live feed"}
202 − </span>
203 − <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]">
204 − {fr ? "Dernières ventes réelles captées" : "Latest real sales captured"}
205 − </h2>
206 − <p className="mt-2 max-w-2xl text-[14px] text-ink-2">
207 − {fr
208 − ? "Transactions publiées, appariées automatiquement à leur unité d'évaluation. Cliquez pour voir l'estimation complète de la propriété vendue."
209 − : "Published transactions, automatically matched to their assessment unit. Click for the sold property's full estimate."}
210 − </p>
211 − <div className="src-wrap mt-4">
212 − <table className="src-table">
213 − <thead>
214 − <tr>
215 − <th>{fr ? "Date" : "Date"}</th>
216 − <th>{fr ? "Propriété" : "Property"}</th>
217 − <th className="text-right">{fr ? "Prix de vente" : "Sale price"}</th>
218 − <th className="text-right"></th>
219 − </tr>
220 − </thead>
221 − <tbody>
222 − {recent.map((s) => (
223 − <tr key={s.id}>
224 − <td className="vp-mono whitespace-nowrap text-[12px] font-bold uppercase">
288 + <SectionHead
289 + num="02"
290 + kicker={fr ? "Flux en continu" : "Live feed"}
291 + title={fr ? "Dernières ventes réelles captées" : "Latest real sales captured"}
292 + lede={
293 + fr
294 + ? "Transactions publiées, appariées automatiquement à leur unité d'évaluation. Chaque vente mène à l'estimation complète de la propriété."
295 + : "Published transactions, automatically matched to their assessment unit. Each sale leads to the property's full estimate."
296 + }
297 + />
298 + <div className="mt-5">
299 + {recent.map((s) => {
300 + const inner = (
301 + <div className="flex flex-wrap items-baseline gap-x-4 gap-y-1 sm:flex-nowrap">
302 + {/* le prix d'abord — la donnée mène */}
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]">
304 + {money(s.amount)}
305 + </p>
306 + <div className="min-w-0 flex-1">
307 + <p className="truncate text-[14.5px] font-semibold leading-snug">
308 + {s.street}
309 + {s.city ? `, ${s.city}` : ""}
310 + </p>
311 + <p className="vp-mono mt-0.5 text-[10.5px] uppercase tracking-[0.05em] text-ink-3">
225 312 {dateFmt(s.date)}
226 − </td>
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>
313 + {s.propertyType && s.propertyType !== "indéterminé" ? ` · ${s.propertyType}` : ""}
240 314 {s.vsRolePct != null && Math.abs(s.vsRolePct) < 400 && (
241 − <p className="vp-mono mt-0.5 text-[10px] text-ink-3">
242 − {s.vsRolePct >= 0 ? "+" : "−"}
243 − {Math.abs(s.vsRolePct).toFixed(0)} %{" "}
244 − {fr ? "vs rôle" : "vs roll"}
245 − </p>
246 − )}
247 − </td>
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>
315 + <>
316 + {" · "}
317 + <span className={s.vsRolePct >= 0 ? "text-ink-2" : "text-[var(--danger)]"}>
318 + {s.vsRolePct >= 0 ? "+" : "−"}
319 + {Math.abs(s.vsRolePct).toFixed(0)} % {fr ? "vs rôle" : "vs roll"}
320 + </span>
321 + </>
256 322 )}
257 − </td>
258 − </tr>
259 − ))}
260 − </tbody>
261 − </table>
323 + </p>
324 + </div>
325 + {s.idProvinc && (
326 + <p className="vp-mono shrink-0 text-[10.5px] font-bold uppercase tracking-[0.08em] text-accent">
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 342 </div>
263 343 </section>
264 344 )}
265 345
266 − {/* ---- Formulaire manuel ---- */}
346 + {/* ================= LE CALCUL, MONTRÉ — promesse centrale ================= */}
267 347 <section className="mt-14">
268 − <span className="kicker">{lang === "fr" ? "Plan B" : "Plan B"}</span>
269 − <h2 className="vp-display mt-2 text-[22px] font-bold uppercase tracking-[-0.02em]">
270 − {t("manualTitle")}
271 − </h2>
272 − <div className="vp-card mt-4 p-4 sm:p-6">
273 − <EstimateForm />
348 + <SectionHead
349 + num="03"
350 + kicker={fr ? "Sans boîte noire" : "No black box"}
351 + title={fr ? "Voici exactement comment le prix est calculé" : "Here is exactly how the price is computed"}
352 + />
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 406 </div>
275 407 </section>
276 408
277 − {/* ---- Manifeste transparence ---- */}
278 − <section className="mt-14 grid gap-4 sm:grid-cols-3">
279 − {(lang === "fr"
280 − ? [
281 − ["01", "Le calcul, montré", "Chaque ajustement de chaque comparable est affiché en dollars. Pas de score magique."],
282 − ["02", "Fourchette honnête", "Un prix unique est un mensonge statistique. On donne P10-P90 et un indice de confiance A-D."],
283 − ["03", "Données réelles", "Rôles fonciers officiels (MAMH) et ventes publiées — locales et récentes en priorité."],
284 − ]
285 − : [
286 − ["01", "The math, shown", "Every adjustment on every comparable is displayed in dollars. No magic score."],
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 − ))}
409 + {/* ================= PLAN B — estimation sans adresse ================= */}
410 + <section className="mt-14">
411 + <SectionHead
412 + num="04"
413 + kicker={fr ? "Plan B" : "Plan B"}
414 + title={t("manualTitle")}
415 + />
416 + <div className="mt-6 max-w-3xl">
417 + <EstimateForm />
418 + </div>
299 419 </section>
300 420 </div>
301 421 );
modified src/app/compte/CompteClient.tsx +1 −1
@@ -345,7 +345,7 @@ export default function CompteClient({
345 345 href={socialUrl(net, v)}
346 346 target="_blank"
347 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 350 <b>{SOCIAL_LABELS[net]}</b> {v}
351 351 </a>
modified src/app/globals.css +180 −88
@@ -1,10 +1,11 @@
1 1 /* -----------------------------------------------------------------------------
2 2 Vrai-Prix — estimation immobilière transparente (Québec)
3 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 5 · Typo display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono
6 − · Signature : bordures encre + ombres décalées (néo-brutalisme raffiné)
7 − · Accent rouge vif #ff5148 + bordeaux #9e2a25 sur encre
6 + · Signature : filets fins (hairlines), grands chiffres, compositions asymétriques,
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 10 @import "tailwindcss";
10 11 /* ka-ui (Groupe KA) — tokens + composants communs (.gk-badge, .ka-footer, .card…).
@@ -16,7 +17,8 @@
16 17
17 18 :root {
18 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 22 --accent-soft: #ffe3e0;
21 23 --accent-deep: #9e2a25;
22 24 --on-accent: #ffffff;
@@ -28,6 +30,7 @@
28 30 --ink-2: #4d5551;
29 31 --ink-3: #8b928c;
30 32 --line: rgba(20, 24, 20, 0.14);
33 + --line-soft: rgba(20, 24, 20, 0.08);
31 34 --line-strong: rgba(20, 24, 20, 0.85);
32 35 --green: #9e2a25;
33 36 --green-deep: #771f1b;
@@ -37,11 +40,20 @@
37 40 --amber-soft: #fdf3e2;
38 41 --danger: #b3423a;
39 42 --danger-soft: #fbe9e7;
40 − --r-card: 10px;
41 − --r-ctl: 6px;
43 + /* série de graphiques (palette validée : CVD 28,6 · contraste ≥3:1) */
44 + --chart-1: #141814; /* unifamiliale */
45 + --chart-2: #d8372e; /* condo */
46 + --chart-3: #6b736d; /* plex */
47 + --r-card: 2px;
48 + --r-ctl: 2px;
42 49 --shadow-flat: 0 1px 2px rgba(20, 24, 20, 0.05);
43 − --shadow-off: 6px 6px 0 var(--ink);
44 − --shadow-off-soft: 8px 8px 0 rgba(20, 24, 20, 0.08);
50 + /* ombres décalées héritées — aplaties : le v2 ne porte aucune ombre dure */
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 59 @theme inline {
@@ -58,6 +70,9 @@
58 70 --color-amber: var(--amber);
59 71 --color-amber-soft: var(--amber-soft);
60 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 76 --font-display: var(--font-space-grotesk), system-ui, sans-serif;
62 77 --font-body: var(--font-inter), system-ui, sans-serif;
63 78 --font-mono: var(--font-jetbrains), ui-monospace, monospace;
@@ -80,10 +95,10 @@ body {
80 95 -webkit-font-smoothing: antialiased;
81 96 padding-bottom: env(safe-area-inset-bottom);
82 97 }
83 −/* grain subtil — texture signature */
98 +/* grain subtil — texture signature (discret, presque imperceptible) */
84 99 body::before {
85 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 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 104 /* au-dessus du grain (::before positionné, premier dans l'ordre de peinture) ;
@@ -95,146 +110,223 @@ h1, h2, h3, h4 {
95 110 font-family: var(--font-space-grotesk), system-ui, sans-serif;
96 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 115 /* ---------- primitives signature ---------- */
101 116 .vp-display { font-family: var(--font-space-grotesk), system-ui, sans-serif; }
102 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 126 .kicker {
105 127 font-family: var(--font-jetbrains), ui-monospace, monospace;
106 − font-size: 11.5px; font-weight: 500; text-transform: uppercase;
107 − letter-spacing: 0.14em; color: var(--green);
108 − display: inline-flex; align-items: center; gap: 8px;
128 + font-size: 11px; font-weight: 500; text-transform: uppercase;
129 + letter-spacing: 0.16em; color: var(--accent);
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 134 .klabel {
113 135 font-family: var(--font-jetbrains), ui-monospace, monospace;
114 − font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em;
115 − color: var(--ink-3); font-weight: 700;
136 + font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em;
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 152 .vp-card {
119 153 background: var(--surface);
120 − border: 1.5px solid var(--ink);
154 + border: 1px solid var(--line);
121 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; }
125 −.vp-card-hover:hover { transform: translate(-3px, -3px); box-shadow: var(--shadow-off); }
158 +.vp-card-hover { transition: border-color 0.15s ease; }
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 162 .vp-input {
128 − border: 1.5px solid var(--line); background: var(--surface-2);
129 − border-radius: var(--r-ctl); padding: 11px 12px; font-size: 15px;
163 + border: 0; border-bottom: 1px solid var(--line-strong); background: transparent;
164 + border-radius: 0; padding: 10px 2px; font-size: 15px;
130 165 color: var(--ink); outline: none; font-family: inherit; width: 100%;
131 166 transition: border-color 0.15s ease, box-shadow 0.15s ease; min-height: 44px;
132 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 171 select.vp-input {
136 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 175 @media (max-width: 900px) { .vp-input { font-size: 16px; } } /* anti-zoom iOS */
140 176
141 177 .btn {
142 − border: 1.5px solid var(--ink); border-radius: var(--r-ctl); padding: 11px 20px;
143 − font-weight: 700; font-size: 14px; cursor: pointer; min-height: 44px;
144 − transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.15s ease;
145 − font-family: var(--font-space-grotesk), system-ui, sans-serif; letter-spacing: 0.01em;
178 + border: 1px solid var(--ink); border-radius: var(--r-ctl); padding: 11px 22px;
179 + font-weight: 600; font-size: 13.5px; cursor: pointer; min-height: 44px;
180 + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
181 + font-family: var(--font-space-grotesk), system-ui, sans-serif; letter-spacing: 0.02em;
146 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; }
149 −.btn-primary { background: var(--ink); color: var(--lime); box-shadow: 4px 4px 0 rgba(20,24,20,0.25); }
150 −.btn-primary:hover { background: var(--green-deep); }
151 −.btn-ghost { background: transparent; color: var(--ink); border-color: var(--ink); }
152 −.btn-ghost:hover { background: var(--lime); box-shadow: 4px 4px 0 rgba(20,24,20,0.2); }
184 +.btn:active { opacity: 0.85; }
185 +.btn-primary { background: var(--ink); color: var(--paper); }
186 +.btn-primary:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
187 +.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-strong); }
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 193 .stat-chip {
155 − background: var(--surface); border: 1.5px solid var(--ink); border-radius: 999px;
156 − padding: 8px 16px; font-family: var(--font-jetbrains), ui-monospace, monospace;
157 − font-size: 12px; color: var(--ink-2); display: inline-flex; gap: 8px; align-items: center;
158 − box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.12); white-space: nowrap;
194 + background: transparent; border: 0; border-left: 1px solid var(--line);
195 + border-radius: 0; padding: 2px 0 2px 14px;
196 + font-family: var(--font-jetbrains), ui-monospace, monospace;
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 206 .stat-chip .pulse {
162 − width: 8px; height: 8px; border-radius: 50%; background: var(--green);
163 − box-shadow: 0 0 0 4px var(--lime-soft); animation: pulse 2.4s ease infinite;
207 + width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
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 213 .hl {
168 − background: var(--lime); padding: 0 10px; border-radius: 8px; display: inline-block;
169 − transform: rotate(-1deg);
214 + background: linear-gradient(transparent 64%, rgba(216, 55, 46, 0.3) 64%);
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 221 .ticker {
175 − background: var(--ink); color: var(--lime); overflow: hidden;
222 + background: var(--ink-deep); color: var(--paper-on-ink); overflow: hidden;
176 223 font-family: var(--font-jetbrains), ui-monospace, monospace;
177 − font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase;
178 − padding: 7px 0; white-space: nowrap;
179 − border-bottom: 1px solid rgba(255, 81, 72, 0.25);
224 + font-size: 10.5px; letter-spacing: 0.09em; text-transform: uppercase;
225 + padding: 6px 0; white-space: nowrap;
180 226 }
181 −.ticker-track { display: inline-flex; animation: ticker 44s linear infinite; will-change: transform; }
182 −.ticker span { padding: 0 26px; position: relative; }
183 −.ticker span::after { content: "◆"; position: absolute; right: -6px; opacity: 0.5; font-size: 8px; top: 3px; }
227 +.ticker-track { display: inline-flex; animation: ticker 48s linear infinite; will-change: transform; }
228 +.ticker span { padding: 0 24px; position: relative; opacity: 0.85; }
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 230 @keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
185 231 @media (prefers-reduced-motion: reduce) {
186 232 .ticker-track { animation: none; }
187 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 237 .kv-cell {
192 − background: var(--surface-2); border: 1.5px solid var(--line);
193 − border-radius: var(--r-ctl); padding: 10px 13px;
238 + background: transparent; border: 0; border-top: 1px solid var(--line);
239 + border-radius: 0; padding: 8px 2px 2px;
194 240 }
195 241 .kv-cell .k {
196 242 font-family: var(--font-jetbrains), ui-monospace, monospace;
197 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 246 .kv-cell .v {
201 247 font-weight: 700; font-size: 14.5px; margin-top: 2px;
202 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 252 .pill {
207 − display: inline-block; border-radius: 4px; padding: 3px 10px;
253 + display: inline-block; border-radius: 2px; padding: 3px 9px;
208 254 font-family: var(--font-jetbrains), ui-monospace, monospace;
209 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); }
212 −.pill-conf-B { background: var(--lime-soft); color: var(--green-deep); border: 1px solid var(--green); }
257 +.pill-conf-A { background: var(--ink); color: var(--paper); border: 1px solid var(--ink); }
258 +.pill-conf-B { background: transparent; color: var(--ink); border: 1px solid var(--line-strong); }
213 259 .pill-conf-C { background: var(--amber-soft); color: #8a5a12; border: 1px solid var(--amber); }
214 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 263 .src-wrap {
218 − overflow-x: auto; border: 1.5px solid var(--ink); border-radius: var(--r-card);
219 − box-shadow: var(--shadow-off-soft); background: var(--surface);
264 + overflow-x: auto; border: 0; border-top: 2px solid var(--ink); border-radius: 0;
265 + box-shadow: none; background: transparent;
220 266 }
221 267 .src-table { width: 100%; border-collapse: separate; border-spacing: 0; }
222 268 .src-table th {
223 269 text-align: left; font-family: var(--font-jetbrains), ui-monospace, monospace;
224 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);
226 − white-space: nowrap;
271 + padding: 10px 12px 8px; border-bottom: 1px solid var(--line-strong); background: transparent;
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 275 .src-table tr:last-child td { border-bottom: none; }
230 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 324 /* animations de révélation */
233 325 @keyframes rise {
234 − from { opacity: 0; transform: translateY(12px); }
326 + from { opacity: 0; transform: translateY(10px); }
235 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 330 @keyframes growX { from { transform: scaleX(0); } }
239 331 .grow-x { transform-origin: left; animation: growX 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
240 332 @keyframes winPop { from { opacity: 0; } }
@@ -243,13 +335,13 @@ select.vp-input {
243 335 .rise, .grow-x, .vv-win { animation: none; }
244 336 }
245 337
246 −/* barres horizontales (historique) */
247 −.hbar-track { background: var(--surface-2); border-radius: 0 4px 4px 0; height: 18px; overflow: hidden; }
338 +/* barres horizontales (historique) — fines, encre, millésime courant en rouge */
339 +.hbar-track { background: var(--line-soft); border-radius: 0; height: 12px; overflow: hidden; }
248 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 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 347 /* Vrai-Prix Maps (framework Ka Maps par Groupe Ka) — recherche par carte */
@@ -258,20 +350,20 @@ select.vp-input {
258 350 height: 62vh;
259 351 min-height: 420px;
260 352 width: 100%;
261 − border: 1.5px solid var(--ink);
262 − border-radius: 16px;
353 + border: 1px solid var(--line-strong);
354 + border-radius: 2px;
263 355 overflow: hidden;
264 − box-shadow: 6px 6px 0 rgba(20, 24, 20, 0.22);
356 + box-shadow: none;
265 357 z-index: 0;
266 358 }
267 359 .vpm-wrap .ka-map {
268 − --ka-accent: var(--lime);
360 + --ka-accent: var(--accent);
269 361 --ka-on-accent: #ffffff;
270 362 --ka-surface: var(--surface);
271 363 --ka-ink: var(--ink);
272 − --ka-line: var(--ink);
273 − --ka-radius: 10px;
274 − --ka-shadow: 4px 4px 0 rgba(20, 24, 20, 0.2);
364 + --ka-line: var(--line-strong);
365 + --ka-radius: 2px;
366 + --ka-shadow: 0 2px 10px rgba(20, 24, 20, 0.18);
275 367 --ka-font: var(--font-inter), system-ui, sans-serif;
276 368 position: absolute;
277 369 inset: 0;
@@ -281,10 +373,10 @@ select.vp-input {
281 373 bottom: 12px;
282 374 left: 12px;
283 375 z-index: 9;
284 − background: var(--surface);
285 − color: var(--ink);
286 − border: 1.5px solid var(--ink);
287 − border-radius: 999px;
376 + background: var(--ink-deep);
377 + color: var(--paper-on-ink);
378 + border: 0;
379 + border-radius: 2px;
288 380 padding: 6px 12px;
289 381 font-family: var(--font-jetbrains), monospace;
290 382 font-size: 10.5px;
@@ -306,11 +398,11 @@ select.vp-input {
306 398 margin-top: 12px;
307 399 padding: 9px 14px;
308 400 background: var(--ink);
309 − color: var(--lime);
310 − border: 1.5px solid var(--ink);
311 − border-radius: 999px;
401 + color: var(--paper);
402 + border: 1px solid var(--ink);
403 + border-radius: 2px;
312 404 font-size: 12.5px;
313 405 font-weight: 600;
314 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 111 dangerouslySetInnerHTML={{ __html: JSON.stringify(ORG_JSONLD) }}
112 112 />
113 113 <LangProvider>
114 − {/* header opaque (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">
116 − <div className="relative mx-auto flex h-16 max-w-6xl items-center gap-5 px-4 sm:px-6">
114 + {/* header opaque compact (standard header/menu KA 2026-08-22) */}
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-14 max-w-6xl items-center gap-6 px-4 sm:px-6">
117 117 <Link
118 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
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">
123 − Prix
124 − </span>
121 + Vrai&nbsp;Prix
122 + <span
123 + aria-hidden="true"
124 + className="inline-block h-[7px] w-[7px] bg-accent"
125 + />
125 126 </Link>
126 127 {/* badge Groupe KA — desktop seulement (l'espace manque sous xl ;
127 128 le menu mobile porte sa propre entrée) */}
128 129 <span className="hidden xl:inline-flex">
129 130 <GroupeKaBadge />
130 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 133 <HeaderNav />
134 + <span
135 + aria-hidden="true"
136 + className="hidden h-4 w-px bg-[var(--line)] lg:inline-block"
137 + />
133 138 <AccountButton />
134 139 <LangToggle />
135 140 </div>
@@ -147,11 +152,12 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
147 152 puis le bloc encre final = KaFooter (commun Groupe KA) */}
148 153 <div className="mx-auto max-w-6xl px-4 sm:px-6">
149 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">
151 − Vrai
152 − <span className="ml-1 inline-block rotate-[-2deg] rounded-md bg-ink px-[6px] pb-[3px] pt-[1px] text-lime">
153 − Prix
154 − </span>
155 + <p className="vp-display mb-3 flex items-baseline gap-2 text-[24px] font-bold tracking-[-0.04em] text-ink">
156 + Vrai&nbsp;Prix
157 + <span
158 + aria-hidden="true"
159 + className="inline-block h-[8px] w-[8px] bg-accent"
160 + />
155 161 </p>
156 162 <p className="max-w-2xl">
157 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 192 <span>{fr ? "Méthodologie" : "Methodology"}</span>
193 193 </nav>
194 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 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 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 211 {fr
208 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 213 : "Our differentiator is total transparency. This page documents every step — data, merge, model, validation, engine — at technical-report depth."}
210 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 235 {sections.map((sct) => (
214 − <section key={sct.num} className="vp-card p-6 sm:p-8">
215 − <div className="flex items-baseline gap-4">
216 − <span className="vp-mono text-[13px] font-bold text-green">{sct.num}</span>
217 − <h2 className="vp-display text-[20px] font-bold uppercase tracking-[-0.02em]">
236 + <section key={sct.num} className="sec grid gap-x-10 gap-y-3 lg:grid-cols-[110px_1fr]">
237 + <span className="vp-display text-[clamp(26px,3vw,36px)] font-bold leading-none text-accent lg:pt-1">
238 + {sct.num}
239 + </span>
240 + <div>
241 + <h2 className="vp-display text-[clamp(19px,2.4vw,24px)] font-bold uppercase tracking-[-0.02em]">
218 242 {sct.title}
219 243 </h2>
220 − </div>
221 − <div className="mt-4 space-y-3.5">
222 − {sct.paras.map((p, i) => (
223 − <p key={i} className="max-w-3xl text-[14.5px] leading-relaxed text-ink-2">
224 − {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>
244 + <div className="mt-4 space-y-3.5">
245 + {sct.paras.map((p, i) => (
246 + <p key={i} className="max-w-3xl text-[14.5px] leading-relaxed text-ink-2">
247 + {p}
248 + </p>
235 249 ))}
236 250 </div>
237 − )}
238 − {sct.table && (
239 − <div className="src-wrap mt-5">
240 − <table className="src-table">
241 − <thead>
242 − <tr>
243 − {sct.table.head.map((h) => (
244 − <th key={h}>{h}</th>
245 − ))}
246 − </tr>
247 − </thead>
248 − <tbody>
249 − {sct.table.rows.map(([a, b, c]) => (
250 − <tr key={a}>
251 − <td className="font-semibold">{a}</td>
252 − <td className="vp-mono text-[12.5px]">{b}</td>
253 − <td className="vp-mono text-[12.5px]">{c}</td>
251 + {sct.kv && (
252 + <div className="mt-5 grid grid-cols-2 gap-x-6 gap-y-2 sm:grid-cols-3">
253 + {sct.kv.map(([k, v]) => (
254 + <div key={k} className="kv-cell">
255 + <p className="k">{k}</p>
256 + <p className="v">{v}</p>
257 + </div>
258 + ))}
259 + </div>
260 + )}
261 + {sct.table && (
262 + <div className="src-wrap mt-6 max-w-3xl">
263 + <table className="src-table">
264 + <thead>
265 + <tr>
266 + {sct.table.head.map((h) => (
267 + <th key={h}>{h}</th>
268 + ))}
254 269 </tr>
255 − ))}
256 − </tbody>
257 − </table>
258 − </div>
259 − )}
270 + </thead>
271 + <tbody>
272 + {sct.table.rows.map(([a, b, c]) => (
273 + <tr key={a}>
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 284 </section>
261 285 ))}
262 286 </div>
modified src/app/municipalite/[slug]/[type]/page.tsx +2 −2
@@ -164,7 +164,7 @@ export default async function MunTypePage({
164 164 <li key={x.type}>
165 165 <Link
166 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 169 {TYPE_LABELS[x.type]?.pluriel ?? x.type}
170 170 <b>{numFr(x.n)}</b>
@@ -172,7 +172,7 @@ export default async function MunTypePage({
172 172 </li>
173 173 ))}
174 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 176 ← Tout {mun.nom}
177 177 </Link>
178 178 </li>
modified src/app/municipalite/[slug]/page.tsx +1 −1
@@ -219,7 +219,7 @@ export default async function MunicipalitePage({
219 219 <li key={v.slug}>
220 220 <Link
221 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 224 {v.nom}
225 225 </Link>
modified src/app/page.tsx +21 −16
@@ -36,11 +36,11 @@ const TOP_VILLES = (
36 36 stats as { par_ville: { ville: string; n: number; mediane: number | null }[] }
37 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 40 function buildPulse(): PulseItem[] {
41 41 return (["unifamilial", "condo", "plex"] as const).map((type) => {
42 42 const full = getMarketIndex(type);
43 − const series = full.slice(-25);
43 + const series = full;
44 44 const last = full.length ? full[full.length - 1].idx : null;
45 45 const yearAgo = full.length >= 13 ? full[full.length - 13].idx : null;
46 46 const first = full.length >= 2 ? full[0].idx : null;
@@ -89,33 +89,38 @@ export default function Home() {
89 89 />
90 90 {/* ---- Explorer par municipalité (maillage interne indexable) ---- */}
91 91 <section className="mt-14">
92 − <span className="kicker">Explorer par municipalité</span>
93 − <h2 className="vp-display mt-3 text-[clamp(22px,3.6vw,34px)] font-bold uppercase leading-[1.05] tracking-[-0.03em]">
94 − La valeur des propriétés, ville par ville
95 − </h2>
96 − <p className="mt-3 max-w-2xl text-[15px] text-ink-2">
97 − Statistiques d&apos;évaluation, valeur médiane et propriétés estimées
98 − pour chacune des 1 097 municipalités du Québec.
99 − </p>
100 − <ul className="mt-6 grid grid-cols-2 gap-x-6 gap-y-2 sm:grid-cols-3 lg:grid-cols-4">
92 + <header className="sec">
93 + <span className="sec-num">05 — Explorer par municipalité</span>
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 + La valeur des propriétés, ville par ville
96 + </h2>
97 + <p className="mt-3 max-w-2xl text-[14.5px] leading-relaxed text-ink-2">
98 + Statistiques d&apos;évaluation, valeur médiane et propriétés estimées
99 + pour chacune des 1 097 municipalités du Québec.
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 103 {TOP_VILLES.map((v) => (
102 104 <li key={v.ville}>
103 105 <Link
104 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 110 {v.ville}
109 111 </span>
110 − <span className="vp-mono text-[11px] text-ink-2">
112 + <span className="vp-mono text-[10.5px] text-ink-3">
111 113 {v.mediane != null ? moneyFr(v.mediane) : ""}
112 114 </span>
113 115 </Link>
114 116 </li>
115 117 ))}
116 118 </ul>
117 − <p className="mt-5">
118 − <Link href="/municipalites" className="btn btn-ghost">
119 + <p className="mt-6">
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 124 Toutes les municipalités →
120 125 </Link>
121 126 </p>
modified src/components/AccountButton.tsx +5 −5
@@ -45,7 +45,7 @@ export default function AccountButton() {
45 45 return (
46 46 <a
47 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 49 title={fr ? "Se connecter avec KA ID" : "Sign in with KA ID"}
50 50 >
51 51 KA&nbsp;ID
@@ -57,7 +57,7 @@ export default function AccountButton() {
57 57 return (
58 58 <Link
59 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 61 title={fr ? "Mon compte — carte de membre Groupe KA" : "My account — Groupe KA member card"}
62 62 >
63 63 {user.picture ? (
@@ -66,14 +66,14 @@ export default function AccountButton() {
66 66 src={user.picture}
67 67 alt=""
68 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 73 {prenom.charAt(0).toUpperCase()}
74 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 77 {prenom}
78 78 </span>
79 79 </Link>
modified src/components/CookieConsent.tsx +5 −5
@@ -164,8 +164,8 @@ export default function CookieConsent() {
164 164 <>
165 165 {showBanner && (
166 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)]">
168 − <p className="klabel !text-lime">🍪 {t.prefsTitle}</p>
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-[var(--accent-bright)]">🍪 {t.prefsTitle}</p>
169 169 <p className="mt-2 text-[13.5px] leading-relaxed text-[rgba(245,243,238,0.85)]">
170 170 {t.banner}{" "}
171 171 <a href="/confidentialite" className="border-b border-lime text-lime">
@@ -173,7 +173,7 @@ export default function CookieConsent() {
173 173 </a>
174 174 </p>
175 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 177 {t.acceptAll}
178 178 </button>
179 179 <button
@@ -199,7 +199,7 @@ export default function CookieConsent() {
199 199 onClick={() => setPanel(false)}
200 200 >
201 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 203 onClick={(e) => e.stopPropagation()}
204 204 role="dialog"
205 205 aria-modal="true"
@@ -214,7 +214,7 @@ export default function CookieConsent() {
214 214 { k: t.prefs, d: t.prefsDesc, on: prefs, lock: false, set: setPrefs },
215 215 { k: t.analytics, d: t.analyticsDesc, on: analytics, lock: false, set: setAnalytics },
216 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 218 <div>
219 219 <p className="vp-display text-[14px] font-bold">
220 220 {row.k}
modified src/components/HeaderNav.tsx +34 −31
@@ -40,16 +40,16 @@ export default function HeaderNav() {
40 40
41 41 return (
42 42 <>
43 − {/* nav pilule — desktop */}
44 − <nav className="hidden items-center gap-1 lg:flex">
43 + {/* nav texte — desktop : soulignement rouge = page courante */}
44 + <nav className="hidden items-center gap-5 lg:flex">
45 45 {items.map((it) => (
46 46 <Link
47 47 key={it.href}
48 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 50 isActive(it.href)
51 − ? "border-ink bg-ink text-lime"
52 − : "border-transparent text-ink-2 hover:border-ink hover:text-ink"
51 + ? "border-accent text-ink"
52 + : "border-transparent text-ink-2 hover:text-ink"
53 53 }`}
54 54 >
55 55 {it.label}
@@ -71,39 +71,36 @@ export default function HeaderNav() {
71 71 <label
72 72 htmlFor="vp-menu"
73 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" />
77 − <span className="h-[2px] w-[18px] bg-ink" />
78 − <span className="h-[2px] w-[18px] bg-ink" />
76 + <span className="h-px w-[20px] bg-ink" />
77 + <span className="h-px w-[20px] bg-ink" />
78 + <span className="h-px w-[12px] self-center ml-[8px] bg-accent" />
79 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 82 <div
83 − className="fixed inset-0 z-[var(--z-modal,900)] hidden flex-col overflow-y-auto peer-checked:flex"
84 − style={{ background: "var(--paper)" }}
83 + className="vp-dark fixed inset-0 z-[var(--z-modal,900)] hidden flex-col overflow-y-auto peer-checked:flex"
85 84 role="dialog"
86 85 aria-modal="true"
87 86 aria-label="Menu"
88 87 >
89 88 {/* barre du haut : marque + fermer */}
90 − <div
91 − className="flex h-16 shrink-0 items-center justify-between border-b-2 border-ink px-4"
92 − style={{ background: "var(--paper)" }}
93 − >
89 + <div className="flex h-14 shrink-0 items-center justify-between border-b border-[rgba(245,243,238,0.18)] px-4">
94 90 <Link
95 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
99 − <span className="ml-1 rotate-[-2deg] rounded-md bg-ink px-[7px] pb-1 pt-[2px] text-lime">
100 − Prix
101 − </span>
94 + Vrai&nbsp;Prix
95 + <span
96 + aria-hidden="true"
97 + className="inline-block h-[7px] w-[7px] bg-[var(--accent-bright)]"
98 + />
102 99 </Link>
103 100 <label
104 101 htmlFor="vp-menu"
105 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 106 </label>
@@ -115,37 +112,43 @@ export default function HeaderNav() {
115 112 <Link
116 113 key={it.href}
117 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 118 0{i + 1}
122 119 </span>
123 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 122 isActive(it.href)
126 − ? "rounded-md bg-ink px-2.5 py-1 text-lime"
127 − : "text-ink group-active:text-green-deep"
123 + ? "text-[var(--accent-bright)]"
124 + : "text-[var(--paper-on-ink)] group-active:text-[var(--accent-bright)]"
128 125 }`}
129 126 >
130 127 {it.label}
131 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 135 </Link>
133 136 ))}
134 137 </nav>
135 138
136 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 141 {/* entrée « Un service Groupe KA » du menu mobile */}
139 − <div className="mb-3">
142 + <div className="mb-4">
140 143 <GroupeKaBadge />
141 144 </div>
142 145 <button
143 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 149 {fr ? "English version" : "Version française"}
147 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 152 <Link href="/conditions">{fr ? "Conditions" : "Terms"}</Link>
150 153 <Link href="/confidentialite">
151 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