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>
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)} % | |
| 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 {fr ? "mois" : "mo"} | |
| 269 | + <b className="vp-display text-[14px]"> | |
| 270 | + <Pct v={p.pct12m} /> | |
| 188 | 271 | </b> |
| 189 | − </p> | |
| 190 | − )} | |
| 272 | + </span> | |
| 273 | + <span> | |
| 274 | + 2021→ | |
| 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 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 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'é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'é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'é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 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 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