Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.
TypeScript 90.2%
JavaScript 3.5%
Python 3.4%
CSS 1.9%
HTML 0.6%
1// Vrai-Prix — méthode du coût : page d'atterrissage /cout (héro, KPI « Coût aujourd'hui », indices,2// explorateur de coûts unitaires, observatoire des matériaux, assemblages populaires, régions, méthode, sources, limites).3"use client";4import { useEffect, useRef, useState } from "react";5import Link from "next/link";6import { useRouter } from "next/navigation";7import { useLang } from "@/components/LangContext";8import SearchBox from "@/components/SearchBox";9import type { CostOverview, PriceKind } from "@/lib/cost/types";10import { UNIT_LABELS } from "@/lib/cost/units";11import { categoryLabel } from "@/lib/cost/taxonomy";12import { money, num, pct } from "@/components/avendre/fmt";13import { IndexLines, Sparkline } from "./Charts";14import { fmtDate, KindIcon, KindLegend } from "./Provenance";15import { Drawer, PriceSourceCard } from "./SourceDrawer";1617interface ItemRow { code: string; nameFr: string; nameEn: string; unit: string; category: string; price: number | null; low: number | null; high: number | null; kind: PriceKind | null; sourceCount: number; observedAt: string | null; confidence: number | null; note: string | null; sources: { source: string; price: number; url: string | null; observedAt: string; sourceUnit: string; conversionFactor: number; regular: boolean }[] }1819export function Sect({ num, kicker, title, lede, id }: { num: string; kicker: string; title: string; lede?: string; id?: string }) {20 return (21 <div className="sec" id={id}>22 <div className="flex items-baseline gap-3">23 <span className="sec-num">{num}</span>24 <span className="kicker">{kicker}</span>25 </div>26 <h2 className="vp-display mt-2 text-[clamp(20px,3vw,28px)] font-bold uppercase leading-[1.05] tracking-[-0.025em]">{title}</h2>27 {lede && <p className="mt-2 max-w-3xl text-[14px] leading-relaxed text-ink-2">{lede}</p>}28 </div>29 );30}3132function Kpi({ k, v, sub, na }: { k: string; v: string | null; sub?: string | null; na: string }) {33 return (34 <div className="border-t border-[var(--line)] pt-3">35 <p className="klabel">{k}</p>36 <p className={`vp-display mt-1 text-[clamp(20px,2.6vw,28px)] font-bold tracking-[-0.02em] ${v ? "" : "text-ink-3"}`}>{v ?? na}</p>37 {sub && <p className="vp-mono mt-0.5 text-[10.5px] uppercase tracking-[0.05em] text-ink-2">{sub}</p>}38 </div>39 );40}4142export default function Landing({ overview }: { overview: CostOverview | null }) {43 const { lang } = useLang();44 const fr = lang === "fr";45 const router = useRouter();46 const na = fr ? "Donnée non disponible" : "Data not available";47 const k = overview?.kpis;48 const [q, setQ] = useState("");49 const [items, setItems] = useState<ItemRow[] | null>(null);50 const [searching, setSearching] = useState<string | null>(null);51 const [drawer, setDrawer] = useState<ItemRow | null>(null);52 const timer = useRef<ReturnType<typeof setTimeout> | null>(null);5354 useEffect(() => {55 if (timer.current) clearTimeout(timer.current);56 const query = q.trim();57 if (query.length < 2) return;58 timer.current = setTimeout(async () => {59 try {60 const r = await fetch(`/api/cost/items?q=${encodeURIComponent(query)}`);61 const d = (await r.json()) as { items: ItemRow[] };62 setItems(d.items ?? []);63 setSearching(query);64 } catch { setItems([]); setSearching(query); }65 }, 250);66 return () => { if (timer.current) clearTimeout(timer.current); };67 }, [q]);68 const loading = q.trim().length >= 2 && searching !== q.trim();6970 const indexSeries = (overview?.indices ?? [])71 .filter((s) => ["statcan:18100289:10:1:1", "statcan:18100289:9:1:1", "statcan:18100289:10:5:1", "statcan:18100289:10:1:8", "statcan:18100289:10:1:19"].includes(s.code))72 .map((s) => ({ label: `${s.geography.split(",")[0]} — ${s.division === "aggregate" || s.division.startsWith("Agr") ? s.buildingType : s.division}`.slice(0, 40), points: s.points.map((p) => ({ period: p.period, value: p.value })) }));73 const unitL = (u: string) => UNIT_LABELS[u as keyof typeof UNIT_LABELS]?.[fr ? "fr" : "en"] ?? u;7475 return (76 <div className="space-y-14 py-8">77 {/* ================= héro ================= */}78 <section className="rise">79 <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 border-b-2 border-ink pb-2.5">80 <nav className="vp-mono flex flex-wrap items-center gap-2 text-[10.5px] uppercase tracking-[0.08em] text-ink-3">81 <Link href="/" className="text-ink-2 hover:text-accent-deep">Vrai Prix</Link>82 <span aria-hidden="true">/</span>83 <span>{fr ? "Coût" : "Cost"}</span>84 </nav>85 <span className="vp-mono text-[10px] uppercase tracking-[0.08em] text-accent">{fr ? "Troisième lecture de la valeur" : "Third reading of value"}</span>86 </div>87 <div className="mt-6 grid gap-x-12 gap-y-8 lg:grid-cols-[1.5fr_1fr]">88 <div>89 <span className="kicker">{fr ? "Méthode du coût" : "Cost approach"}</span>90 <h1 className="vp-display mt-2 text-[clamp(30px,5vw,54px)] font-bold uppercase leading-[0.98] tracking-[-0.03em]">91 {fr ? <>Combien coûterait ce bâtiment<br /><span className="hl">à reconstruire aujourd'hui ?</span></> : <>What would this building cost<br /><span className="hl">to rebuild today?</span></>}92 </h1>93 <p className="mt-4 max-w-2xl text-[15.5px] leading-relaxed text-ink-2">94 {fr ? "Décomposez le coût de remplacement à neuf, composante par composante, puis mesurez la dépréciation et ajoutez la valeur du terrain. Chaque hypothèse et chaque source sont visibles." : "Break the replacement cost new down component by component, then measure depreciation and add the land value. Every assumption and every source is visible."}95 </p>96 <div className="mt-6 max-w-xl">97 <p className="klabel mb-1">{fr ? "Mode propriété — une adresse du rôle d'évaluation" : "Property mode — an address from the assessment roll"}</p>98 <SearchBox onPick={(s) => router.push(`/cout?property=${encodeURIComponent(s.id)}`)} placeholder={fr ? "Entrez une adresse (ex. 35 rue Servantes, Gatineau)" : "Enter an address (e.g. 35 rue Servantes, Gatineau)"} />99 <div className="mt-3 flex flex-wrap gap-2.5">100 <Link href="/cout?mode=construction" className="btn btn-ghost">{fr ? "Mode construction (sans adresse)" : "Construction mode (no address)"}</Link>101 <Link href="/a-vendre" className="btn btn-ghost">{fr ? "Depuis une annonce à vendre" : "From a listing for sale"} →</Link>102 </div>103 </div>104 <p className="vp-mono mt-6 max-w-2xl border-l-2 border-accent pl-4 text-[11px] uppercase leading-relaxed tracking-[0.05em] text-ink-2">105 {fr ? "Estimation indicative — les coûts présentés sont des estimations statistiques et ne constituent ni une soumission d'entrepreneur ni une évaluation professionnelle certifiée (OEAQ)." : "Indicative estimate — the costs shown are statistical estimates and constitute neither a contractor's bid nor a certified professional appraisal (OEAQ)."}106 </p>107 </div>108 <aside className="lg:border-l lg:border-[var(--line)] lg:pl-10">109 <p className="kicker">{fr ? "Des photos à la méthode du coût." : "From photos to the cost approach."}</p>110 <p className="mt-3 text-[14px] leading-relaxed text-ink-2">111 {fr ? "L'IA transforme une annonce réelle en profil technique de bâtiment. Les coûts actuels du marché sont ensuite appliqués composante par composante — chaque inférence est étiquetée, chaque prix est sourcé, chaque calcul est déterministe." : "AI turns a real listing into a technical building profile. Current market costs are then applied component by component — every inference is labelled, every price is sourced, every computation is deterministic."}112 </p>113 <Link href="/a-vendre" className="vp-mono mt-3 inline-block text-[11px] font-bold uppercase tracking-[0.08em] text-accent">{fr ? "Choisir une annonce" : "Pick a listing"} →</Link>114 <div className="mt-6 border-t border-[var(--line)] pt-4">115 <p className="klabel">{fr ? "La formule, jamais masquée" : "The formula, never hidden"}</p>116 <pre className="vp-mono mt-2 whitespace-pre-wrap text-[12px] leading-relaxed text-ink">{`V_coût = V_terrain + RCN − D117D = D_physique + D_fonctionnelle + D_externe118RCN = Directs + Indirects + Frais généraux + Profit + Contingence`}</pre>119 </div>120 </aside>121 </div>122 </section>123124 {/* ================= KPI ================= */}125 <section>126 <Sect num="01" kicker={fr ? "Coût aujourd'hui" : "Cost today"} title={fr ? "Indice des coûts de construction — Québec, aujourd'hui" : "Construction cost index — Québec, today"} lede={fr ? "Données synchronisées en arrière-plan (Statistique Canada, APCHQ, détaillants publics). L'interface ne déclenche jamais de collecte : elle lit la base locale." : "Data synchronised in the background (Statistics Canada, APCHQ, public retailers). The interface never triggers a crawl: it reads the local database."} />127 <div className="mt-6 grid grid-cols-2 gap-x-8 gap-y-6 md:grid-cols-3 lg:grid-cols-5">128 <Kpi k={fr ? "Coût résidentiel (indice)" : "Residential cost (index)"} v={k?.residentialIndex ? num(k.residentialIndex.value, lang, 1) : null} sub={k?.residentialIndex ? `${k.residentialIndex.pctYoy != null ? `${pct(k.residentialIndex.pctYoy, lang, 1)} / 12 ${fr ? "mois" : "months"} · ` : ""}${k.residentialIndex.geography.split(",")[0]} · ${k.residentialIndex.period.slice(0, 7)}` : (fr ? "StatCan 18-10-0289" : "StatCan 18-10-0289")} na={na} />129 <Kpi k={fr ? "Matériaux (proxy bois)" : "Materials (wood proxy)"} v={k?.materialsProxy ? num(k.materialsProxy.value, lang, 1) : null} sub={k?.materialsProxy ? `${k.materialsProxy.pctYoy != null ? `${pct(k.materialsProxy.pctYoy, lang, 1)} / 12 ${fr ? "mois" : "months"} · ` : ""}${k.materialsProxy.label}` : null} na={na} />130 <Kpi k={fr ? "Main-d'œuvre (charpentier)" : "Labour (carpenter)"} v={k?.labour ? `${num(k.labour.totalEmployerCost, lang, 2)} $/h` : null} sub={k?.labour ? `${k.labour.pctYoy != null ? `${pct(k.labour.pctYoy, lang, 1)} / 12 ${fr ? "mois" : "months"} · ` : ""}${fr ? "coût employeur" : "employer cost"} · ${k.labour.source}` : null} na={na} />131 <Kpi k={fr ? "Observations de prix" : "Price observations"} v={k ? num(k.observations, lang) : null} sub={k ? `${k.itemsObserved} / ${k.itemsTotal} ${fr ? "articles observés (120 j)" : "items observed (120 d)"}` : null} na={na} />132 <Kpi k={fr ? "Dernière mise à jour" : "Last update"} v={k?.lastSync ? fmtDate(k.lastSync, fr) : null} sub={fr ? "synchronisation des sources" : "source synchronisation"} na={na} />133 </div>134 <div className="mt-8">135 <p className="klabel mb-2">{fr ? "Indices des prix de la construction de bâtiments — 2021 → présent (StatCan 18-10-0289-01, base 2017 = 100)" : "Building construction price indexes — 2021 → present (StatCan 18-10-0289-01, 2017 = 100)"}</p>136 {indexSeries.length ? <IndexLines series={indexSeries} lang={lang} /> : <p className="vp-mono border border-dashed border-[var(--line-strong)] p-6 text-center text-[11px] uppercase tracking-[0.06em] text-ink-3">{na}</p>}137 </div>138 </section>139140 {/* ================= explorateur ================= */}141 <section>142 <Sect num="02" kicker={fr ? "Explorer" : "Explore"} title={fr ? "Explorer les coûts unitaires" : "Explore unit costs"} lede={fr ? "Chaque article a un prix canonique (médiane robuste multi-source), une provenance et une date. Cliquez une ligne pour ouvrir la fiche source." : "Every item has a canonical price (robust multi-source median), a provenance and a date. Click a row to open the source card."} />143 <div className="mt-5 max-w-xl">144 <input value={q} onChange={(e) => setQ(e.target.value)} placeholder={fr ? "Rechercher : gypse, 2 x 6, bardeau, R24, fenêtre…" : "Search: drywall, 2 x 6, shingle, R24, window…"} className="vp-input text-base" aria-label={fr ? "Rechercher un coût unitaire" : "Search a unit cost"} />145 </div>146 <div className="mt-3"><KindLegend fr={fr} /></div>147 {q.trim().length >= 2 && (148 <div className="src-wrap mt-4">149 <table className="src-table">150 <thead><tr><th>{fr ? "Article" : "Item"}</th><th className="text-right">{fr ? "Prix" : "Price"}</th><th>{fr ? "Unité" : "Unit"}</th><th>{fr ? "Prov." : "Prov."}</th><th className="text-right">{fr ? "Sources" : "Sources"}</th><th>{fr ? "Mise à jour" : "Updated"}</th></tr></thead>151 <tbody>152 {loading && <tr><td colSpan={6} className="vp-mono text-[11px] uppercase text-ink-3">{fr ? "Recherche…" : "Searching…"}</td></tr>}153 {!loading && items?.length === 0 && <tr><td colSpan={6} className="text-ink-2">{fr ? "Aucun article." : "No item."}</td></tr>}154 {!loading && items?.map((it) => (155 <tr key={it.code} className="cursor-pointer" onClick={() => setDrawer(it)}>156 <td><span className="font-semibold">{fr ? it.nameFr : it.nameEn}</span><br /><span className="vp-mono text-[10px] uppercase text-ink-3">{it.code} · {categoryLabel(it.category as never, fr)}</span></td>157 <td className={`vp-mono text-right ${it.price == null ? "text-ink-3" : ""}`}>{it.price != null ? `${num(it.price, lang, 2)} $` : na}</td>158 <td className="vp-mono text-[12px]">/ {unitL(it.unit)}</td>159 <td><KindIcon kind={it.kind} fr={fr} source={it.sources[0]?.source} observedAt={it.observedAt} note={it.note} /></td>160 <td className="vp-mono text-right">{it.sourceCount}</td>161 <td className="vp-mono text-[12px]">{it.observedAt ?? "—"}</td>162 </tr>163 ))}164 </tbody>165 </table>166 </div>167 )}168 <Drawer open={!!drawer} onClose={() => setDrawer(null)} title={fr ? "Source du prix" : "Price source"} fr={fr}>169 {drawer && (170 <PriceSourceCard fr={fr} itemName={fr ? drawer.nameFr : drawer.nameEn} unit={drawer.unit} lastSync={k?.lastSync ?? null}171 price={drawer.price != null ? { itemCode: drawer.code, unit: drawer.unit as never, price: drawer.price, low: drawer.low ?? drawer.price, high: drawer.high ?? drawer.price, kind: drawer.kind ?? "reference", sourceCount: drawer.sourceCount, observedAt: drawer.observedAt, sources: drawer.sources, confidence: drawer.confidence ?? 0, indexed: null, note: drawer.note } : null} />172 )}173 {drawer && <Link href={`/cout/assemblage?item=${encodeURIComponent(drawer.code)}`} className="vp-mono mt-4 inline-block text-[11px] font-bold uppercase tracking-[0.08em] text-accent">{fr ? "Voir les assemblages qui l'utilisent" : "See assemblies using it"} →</Link>}174 </Drawer>175 </section>176177 {/* ================= observatoire matériaux ================= */}178 <section>179 <Sect num="03" kicker={fr ? "Observatoire" : "Observatory"} title={fr ? "Prix des matériaux" : "Material prices"} lede={fr ? "Séries historiques des matériaux courants (médiane par date des observations détaillants). Un article sans observation affiche sa référence interne, étiquetée hypothèse." : "Historical series of common materials (median per date of retail observations). An item without observation shows its internal reference, labelled as an assumption."} />180 <div className="mt-6 grid gap-x-8 gap-y-6 sm:grid-cols-2 lg:grid-cols-4">181 {(overview?.materials ?? []).map((m) => (182 <Link key={m.itemCode} href={`/cout/assemblage?item=${encodeURIComponent(m.itemCode)}`} className="group border-t border-[var(--line)] pt-3">183 <p className="text-[13px] font-semibold leading-snug group-hover:text-accent-deep">{fr ? m.nameFr : m.nameEn}</p>184 <p className="vp-display mt-1 flex items-baseline gap-2 text-[22px] font-bold tracking-[-0.02em]">185 {m.price != null ? `${num(m.price, lang, 2)} $` : <span className="text-[14px] text-ink-3">{na}</span>}186 <span className="vp-mono text-[10px] font-medium text-ink-3">/ {unitL(m.unit)}</span>187 <KindIcon kind={m.kind} fr={fr} observedAt={m.observedAt} />188 </p>189 <div className="mt-2"><Sparkline points={m.history} lang={lang} /></div>190 <p className="vp-mono mt-1 text-[10px] uppercase tracking-[0.05em] text-ink-3">{m.sourceCount} {fr ? "source(s)" : "source(s)"} · {m.observedAt ?? (fr ? "référence" : "reference")}</p>191 </Link>192 ))}193 {!overview?.materials?.length && <p className="text-ink-2">{na}</p>}194 </div>195 </section>196197 {/* ================= assemblages populaires ================= */}198 <section>199 <Sect num="04" kicker={fr ? "Assemblages" : "Assemblies"} title={fr ? "Assemblages populaires" : "Popular assemblies"} lede={fr ? "Un assemblage est une composante réelle de construction : matériaux (avec pertes) + heures de main-d'œuvre (taux APCHQ/CCQ) + équipement. Coût direct à Montréal, avant facteur régional." : "An assembly is a real construction component: materials (with waste) + labour hours (APCHQ/CCQ rates) + equipment. Direct cost in Montréal, before regional factor."} />200 <div className="mt-6 grid gap-x-8 gap-y-6 sm:grid-cols-2 lg:grid-cols-3">201 {(overview?.popularAssemblies ?? []).map((a) => (202 <Link key={a.code} href={`/cout/assemblage/${encodeURIComponent(a.code)}`} className="vp-card vp-card-hover flex flex-col p-4">203 <p className="vp-mono text-[10px] uppercase tracking-[0.08em] text-ink-3">{a.code}</p>204 <p className="vp-display mt-1 text-[15px] font-bold leading-tight">{fr ? a.nameFr : a.nameEn}</p>205 <p className="vp-display mt-3 text-[24px] font-bold tracking-[-0.02em]">{num(a.direct, lang, 2)} $ <span className="vp-mono text-[10px] font-medium text-ink-3">/ {unitL(a.unit)}</span></p>206 <div className="vp-mono mt-2 grid grid-cols-3 gap-2 text-[10px] uppercase tracking-[0.04em] text-ink-2">207 <span>{fr ? "Matériaux" : "Materials"}<br /><b className="text-ink">{num(a.material, lang, 2)}</b></span>208 <span>{fr ? "Main-d'œuvre" : "Labour"}<br /><b className="text-ink">{num(a.labour, lang, 2)}</b></span>209 <span>{fr ? "Équip." : "Equip."}<br /><b className="text-ink">{num(a.equipment, lang, 2)}</b></span>210 </div>211 <p className="vp-mono mt-3 text-[10px] uppercase tracking-[0.05em] text-ink-3">{fr ? "Part observée" : "Observed share"} : {Math.round(a.observedShare * 100)} %</p>212 </Link>213 ))}214 </div>215 <Link href="/cout/assemblage" className="vp-mono mt-5 inline-block text-[11px] font-bold uppercase tracking-[0.08em] text-accent">{fr ? "Tout le catalogue d'assemblages" : "Full assembly catalogue"} →</Link>216 </section>217218 {/* ================= régions ================= */}219 <section>220 <Sect num="05" kicker={fr ? "Localisation" : "Location"} title={fr ? "Coûts par région" : "Costs by region"} lede={fr ? "Les conventions collectives de la construction (Loi R-20, CCQ) fixent des taux uniformes pour tout le Québec : le facteur main-d'œuvre vaut 1,00 sauf régions éloignées. Les facteurs matériaux et équipement traduisent le transport — hypothèses Vrai-Prix, modifiables." : "Construction collective agreements (Act R-20, CCQ) set uniform wages across Québec: the labour factor is 1.00 except remote regions. Material and equipment factors reflect transport — Vrai-Prix assumptions, editable."} />221 <div className="src-wrap mt-5">222 <table className="src-table">223 <thead><tr><th>{fr ? "Région" : "Region"}</th><th className="text-right">{fr ? "Matériaux" : "Materials"}</th><th className="text-right">{fr ? "Main-d'œuvre" : "Labour"}</th><th className="text-right">{fr ? "Équipement" : "Equipment"}</th><th className="text-right">{fr ? "Global" : "Overall"}</th><th>{fr ? "Confiance" : "Confidence"}</th></tr></thead>224 <tbody>225 {(overview?.locations ?? []).map((l) => (226 <tr key={l.code}>227 <td><span className="font-semibold">{fr ? l.nameFr : l.nameEn}</span> <span className="vp-mono text-[10px] text-ink-3">{l.code}</span></td>228 <td className="vp-mono text-right">× {num(l.materialFactor, lang, 2)}</td>229 <td className="vp-mono text-right">× {num(l.labourFactor, lang, 2)}</td>230 <td className="vp-mono text-right">× {num(l.equipmentFactor, lang, 2)}</td>231 <td className="vp-mono text-right font-bold">× {num(l.overallFactor, lang, 3)}</td>232 <td className="vp-mono text-[11px]">{l.confidence} / 100</td>233 </tr>234 ))}235 </tbody>236 </table>237 </div>238 </section>239240 {/* ================= méthode en 4 étapes ================= */}241 <section>242 <Sect num="06" kicker={fr ? "La méthode" : "The method"} title={fr ? "La méthode du coût en 4 étapes" : "The cost approach in 4 steps"} />243 <ol className="mt-6 grid gap-x-8 gap-y-6 sm:grid-cols-2 lg:grid-cols-4">244 {(fr245 ? [["Décrire le bâtiment", "Type, aire, étages, sous-sol, garage, revêtements, toiture, fenêtres, mécanique, cuisine, salles de bain — chaque caractéristique porte son origine (rôle, saisie, IA, hypothèse)."], ["Quantifier et chiffrer à neuf", "Le moteur de géométrie dérive les quantités d'assemblages (formules visibles) ; chaque assemblage = matériaux + heures × taux + équipement, localisés, puis indirects, frais généraux, profit, contingence → RCN."], ["Déprécier", "Physique (âge-vie ou composante par composante selon la condition), fonctionnelle (curable / incurable), externe. Rien n'est calculé sans preuve : l'externe vaut 0 par défaut."], ["Ajouter le terrain, comparer", "Valeur du terrain au rôle (ou saisie) + bâtiment déprécié = indication par le coût. Confrontée aux lectures hédonique et comparables, aux benchmarks, jamais moyennée."]]246 : [["Describe the building", "Type, area, storeys, basement, garage, cladding, roof, windows, mechanical, kitchen, bathrooms — each attribute carries its origin (roll, entered, AI, assumption)."], ["Quantify and price new", "The geometry engine derives assembly quantities (visible formulas); each assembly = materials + hours × rate + equipment, localised, then indirects, overhead, profit, contingency → RCN."], ["Depreciate", "Physical (age-life or component by component by condition), functional (curable / incurable), external. Nothing is computed without evidence: external defaults to 0."], ["Add land, compare", "Land value from the roll (or entered) + depreciated building = cost indication. Confronted with the hedonic and comparables readings and benchmarks, never averaged."]]247 ).map(([t, b], i) => (248 <li key={t} className="border-t border-[var(--line)] pt-3">249 <span className="vp-mono text-[11px] font-bold text-accent">0{i + 1}</span>250 <p className="vp-display mt-1 text-[15px] font-bold uppercase tracking-[-0.01em]">{t}</p>251 <p className="mt-1.5 text-[13px] leading-snug text-ink-2">{b}</p>252 </li>253 ))}254 </ol>255 </section>256257 {/* ================= sources + limites ================= */}258 <section className="grid gap-x-12 gap-y-10 lg:grid-cols-2">259 <div>260 <Sect num="07" kicker={fr ? "Sources" : "Sources"} title={fr ? "D'où viennent les nombres" : "Where the numbers come from"} />261 <ul className="mt-5 space-y-2.5">262 {(overview?.sources ?? []).filter((s) => s.active || s.observations > 0).map((s) => (263 <li key={s.name} className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 border-b border-[var(--line)] pb-2 text-[13px]">264 <span className="font-semibold">{s.url ? <a href={s.url} target="_blank" rel="noopener noreferrer nofollow" className="hover:text-accent-deep">{s.name} ↗</a> : s.name}</span>265 <span className="vp-mono text-[10px] uppercase tracking-[0.05em] text-ink-3">{s.type} · {s.license.replace("_", " ")} · {s.observations} obs. · {s.lastSync ? fmtDate(s.lastSync, fr) : (fr ? "jamais synchronisé" : "never synced")}</span>266 </li>267 ))}268 </ul>269 <p className="vp-mono mt-4 text-[10px] uppercase leading-relaxed tracking-[0.05em] text-ink-3">270 {fr ? "CCQ · APCHQ · Statistique Canada · Détaillants publics (Canac, BMR, Patrick Morin) · Altus Group (import licencié) · Sources autorisées. Une source « unknown » n'est jamais redistribuée avant validation ; Firecrawl n'est qu'un outil d'extraction et ne confère aucun droit de réutilisation." : "CCQ · APCHQ · Statistics Canada · Public retailers (Canac, BMR, Patrick Morin) · Altus Group (licensed import) · Authorised sources. An 'unknown' source is never redistributed before validation; Firecrawl is only an extraction tool and grants no reuse right."}271 </p>272 </div>273 <div>274 <Sect num="08" kicker={fr ? "Limites" : "Limits"} title={fr ? "Ce que la méthode ne dit pas" : "What the method does not say"} />275 <ul className="mt-5 space-y-2 text-[13.5px] leading-relaxed text-ink-2">276 {(fr277 ? ["Coût de remplacement (composantes modernes équivalentes), pas de reproduction à l'identique : un bâtiment ancien est chiffré avec un 2 × 6 R24, pas avec ses matériaux d'origine.", "Les quantités d'assemblages découlent d'hypothèses de prise de quantités (rapport de forme 1,5, hauteur d'étage 9 pi, 16 po c/c) : vous pouvez les remplacer une à une.", "Les prix de référence internes (béton livré, fermes, fenêtres sur mesure…) sont des hypothèses documentées ; ils abaissent la couverture observée et l'indice de confiance.", "La valeur du terrain au rôle n'est pas la valeur marchande du terrain : elle date de 18 à 24 mois avant l'entrée en vigueur du rôle.", "La dépréciation par composante dépend de la condition saisie ou observée ; sans information, l'âge chronologique plafonné est utilisé.", "Aucune moyenne des trois approches : la comparaison est l'objet de l'analyse, pas un chiffre unique."]278 : ["Replacement cost (modern equivalent components), not identical reproduction: an old building is priced with a 2 × 6 R24 wall, not with its original materials.", "Assembly quantities follow take-off assumptions (aspect ratio 1.5, 9 ft storey height, 16 in o.c.): you can replace them one by one.", "Internal reference prices (delivered concrete, trusses, custom windows…) are documented assumptions; they lower observed coverage and the confidence index.", "The land value on the roll is not the land's market value: it dates from 18 to 24 months before the roll took effect.", "Component depreciation depends on entered or observed condition; without information, capped chronological age is used.", "No averaging of the three approaches: the comparison is the object of the analysis, not a single number."]279 ).map((t) => (<li key={t} className="border-l-2 border-[var(--line-strong)] pl-3">{t}</li>))}280 </ul>281 </div>282 </section>283 </div>284 );285}286287export { money };288