Refonte editoriale v2 de l accueil — direction artistique premium
Moins de boites, plus de typographie : le contenu structure la page. - Hero : composition typographique geante (clamp 44-88px), surlignage orange irregulier en trait SVG sous chez-vous., plus de gros boutons rectangulaires - Donnees vivantes : ligne mono LIVE avec compteur anime (count-up 0,9s), derniere synchro reelle qui tictaque a la seconde (max last_sync des connecteurs), sources et loyer moyen — separes par filets, zero chip - Recherche au coeur : grand champ Ou voulez-vous habiter ? souligne 2px (focus accent), criteres inline Ville/Quartier/Loyer separes par filets verticaux, bouton texte Tous les criteres avec badge — la boite filterbar disparait de l accueil - Barre de resultats sticky : N logements · Liste|Carte (onglets textes a soulignement anime scaleX) · Filtres (mobile) — remplace le FAB flottant - Fiches editoriales : plus de carte-boite — l image domine (radius 10, zoom 1,025 au survol), prix 22px niveau 1, adresse niveau 2, quartier/ville en mono uppercase niveau 3, source en micro-mono niveau 4 - Grille a rythme magazine : 3 colonnes, 1 grand format panoramique 16/9 toutes les 7 fiches, apparition en decale par colonne, align-items start - Pagination nue : chiffres sans boites, page courante soulignee accent - Nav mobile : tabbar flottante detachee des bords (radius 18, blur, ombre portee), etat actif = trait accent sous l icone ; widget KA Agent remonte - Primitives locales v2 : rayons 0/6/10/18, durees 150/240/350ms, hairlines - Feuille mobile : resume des criteres actifs sous la recherche (remplace le FAB), poignee, application sticky - Classes partagees avec CourtTerme (.hero, .filterbar, .stat-chip…) intactes : la refonte vit dans de nouvelles classes + une couche finale
2 changed files +439 −131
modified
frontend/src/pages/Home.tsx
+129 −119
@@ -15,7 +15,7 @@ import { | |||||
| 15 | import ListingCard from "../components/ListingCard"; | 15 | import ListingCard from "../components/ListingCard"; | ||
| 16 | import Pager from "../components/Pager"; | 16 | import Pager from "../components/Pager"; | ||
| 17 | import { | 17 | import { | ||
| 18 | − IcoAlert, IcoBolt, IcoHeart, IcoHouse, IcoList, IcoMap, IcoPaw, IcoPin, | 18 | + IcoAlert, IcoBolt, IcoList, IcoMap, IcoPaw, | ||
| 19 | IcoSearch, IcoSliders, IcoSofa, | 19 | IcoSearch, IcoSliders, IcoSofa, | ||
| 20 | } from "../components/Icons"; | 20 | } from "../components/Icons"; | ||
| 21 | 21 | ||||
@@ -34,6 +34,41 @@ function useDebounced<T>(value: T, delayMs = 300): T { | |||||
| 34 | }, [value, delayMs]); | 34 | }, [value, delayMs]); | ||
| 35 | return debounced; | 35 | return debounced; | ||
| 36 | } | 36 | } | ||
| 37 | + | ||||
| 38 | +/** Compteur animé du héro (~0,9 s, easing cubique) — l'impression d'un vrai | ||||
| 39 | + moteur qui indexe. Respecte prefers-reduced-motion (valeur directe). */ | ||||
| 40 | +function useCountUp(target: number | null | undefined, ms = 900): string | null { | ||||
| 41 | + const [v, setV] = useState<number | null>(null); | ||||
| 42 | + useEffect(() => { | ||||
| 43 | + if (target == null) return; | ||||
| 44 | + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setV(target); return; } | ||||
| 45 | + let raf = 0; | ||||
| 46 | + const t0 = performance.now(); | ||||
| 47 | + const step = (t: number) => { | ||||
| 48 | + const p = Math.min(1, (t - t0) / ms); | ||||
| 49 | + setV(Math.round(target * (1 - Math.pow(1 - p, 3)))); | ||||
| 50 | + if (p < 1) raf = requestAnimationFrame(step); | ||||
| 51 | + }; | ||||
| 52 | + raf = requestAnimationFrame(step); | ||||
| 53 | + return () => cancelAnimationFrame(raf); | ||||
| 54 | + }, [target, ms]); | ||||
| 55 | + return v == null ? null : v.toLocaleString("fr-CA"); | ||||
| 56 | +} | ||||
| 57 | + | ||||
| 58 | +/** « il y a X s » vivant — re-rendu chaque seconde tant que le ts existe. */ | ||||
| 59 | +function useAgo(ts: number | null): string | null { | ||||
| 60 | + const [, tick] = useState(0); | ||||
| 61 | + useEffect(() => { | ||||
| 62 | + if (ts == null) return; | ||||
| 63 | + const id = setInterval(() => tick((x) => x + 1), 1000); | ||||
| 64 | + return () => clearInterval(id); | ||||
| 65 | + }, [ts]); | ||||
| 66 | + if (ts == null) return null; | ||||
| 67 | + const s = Math.max(0, Math.floor(Date.now() / 1000 - ts)); | ||||
| 68 | + if (s < 90) return `il y a ${s} s`; | ||||
| 69 | + if (s < 5400) return `il y a ${Math.round(s / 60)} min`; | ||||
| 70 | + return `il y a ${Math.round(s / 3600)} h`; | ||||
| 71 | +} | ||||
| 37 | const UNIT_TYPES = ["1½", "2½", "3½", "4½", "5½", "Loft", "Studio"]; | 72 | const UNIT_TYPES = ["1½", "2½", "3½", "4½", "5½", "Loft", "Studio"]; | ||
| 38 | const PRICE_STEPS = [600, 800, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000]; | 73 | const PRICE_STEPS = [600, 800, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000]; | ||
| 39 | const AREA_STEPS = [400, 600, 800, 1000, 1200]; | 74 | const AREA_STEPS = [400, 600, 800, 1000, 1200]; | ||
@@ -112,11 +147,19 @@ export default function Home() { | |||||
| 112 | // compteur partagé de la vue carte (remonté par MapSearch : liste = carte) | 147 | // compteur partagé de la vue carte (remonté par MapSearch : liste = carte) | ||
| 113 | const [mapTotal, setMapTotal] = useState<number | null>(null); | 148 | const [mapTotal, setMapTotal] = useState<number | null>(null); | ||
| 114 | 149 | ||||
| 150 | + // données vivantes du héro : dernière synchro réelle (max des connecteurs) | ||||
| 151 | + const [lastSync, setLastSync] = useState<number | null>(null); | ||||
| 115 | useEffect(() => { | 152 | useEffect(() => { | ||
| 116 | − fetchSources().then((r) => registerSourceNames(r.sources)).catch(() => {}); | 153 | + fetchSources().then((r) => { | ||
| 154 | + registerSourceNames(r.sources); | ||||
| 155 | + const ts = Math.max(0, ...r.sources.map((s) => s.last_sync ?? 0)); | ||||
| 156 | + if (ts > 0) setLastSync(ts > 1e12 ? Math.round(ts / 1000) : ts); | ||||
| 157 | + }).catch(() => {}); | ||||
| 117 | fetchFacets().then(setFacets).catch(() => {}); | 158 | fetchFacets().then(setFacets).catch(() => {}); | ||
| 118 | fetchStats().then(setStats).catch(() => {}); | 159 | fetchStats().then(setStats).catch(() => {}); | ||
| 119 | }, []); | 160 | }, []); | ||
| 161 | + const totalLive = useCountUp(stats?.total); | ||||
| 162 | + const syncAgo = useAgo(lastSync); | ||||
| 120 | 163 | ||||
| 121 | // Géolocalisation d'accueil : à l'arrivée sur le site (URL nue, une fois | 164 | // Géolocalisation d'accueil : à l'arrivée sur le site (URL nue, une fois | ||
| 122 | // par session), demander la position pour afficher directement les | 165 | // par session), demander la position pour afficher directement les | ||
@@ -246,71 +289,26 @@ export default function Home() { | |||||
| 246 | return ( | 289 | return ( | ||
| 247 | <div className="container"> | 290 | <div className="container"> | ||
| 248 | <section className="hero"> | 291 | <section className="hero"> | ||
| 249 | − <span className="kicker">La porte d'entrée vers votre prochain chez-vous.</span> | 292 | + <span className="kicker">Location — tout le Québec, un seul endroit</span> | ||
| 250 | − <h1> | 293 | + <h1 className="hero-display"> | ||
| 251 | − Trouvez votre prochain <span className="hl">chez-vous.</span> | 294 | + Trouvez votre prochain<br /> | ||
| 295 | + <span className="brush">chez-vous.</span> | ||||
| 252 | </h1> | 296 | </h1> | ||
| 253 | <p className="lede"> | 297 | <p className="lede"> | ||
| 254 | − Tous les logements à louer au Québec, regroupés au même endroit — mis à jour | 298 | + Tous les logements à louer au Québec, agrégés en continu — photos complètes, | ||
| 255 | − automatiquement, avec toutes les photos et un lien direct vers l'annonce originale. | 299 | + juste valeur estimée, lien direct vers l'annonce originale. | ||
| 256 | </p> | 300 | </p> | ||
| 257 | − <div className="hero-cta"> | 301 | + <div className="live-line" aria-label="Données en direct"> | ||
| 258 | − <button | 302 | + <span className="live-flag"><span className="live-dot" /> live</span> | ||
| 259 | − className="btn btn-primary btn-lg" | 303 | + {totalLive && ( | ||
| 260 | − onClick={() => { | 304 | + <span className="live-item"><b>{totalLive}</b> logements indexés</span> | ||
| 261 | − setView("liste"); | 305 | + )} | ||
| 262 | − resultsRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); | 306 | + {stats && stats.sources > 0 && ( | ||
| 263 | − }} | 307 | + <span className="live-item"><b>{stats.sources}</b> sources</span> | ||
| 264 | − > | 308 | + )} | ||
| 265 | − <IcoSearch size={16} /> Explorer les logements | 309 | + {syncAgo && <span className="live-item">synchro {syncAgo}</span>} | ||
| 266 | − </button> | 310 | + {stats?.avg_price != null && ( | ||
| 267 | − <button | 311 | + <span className="live-item">loyer moyen <b>{Math.round(stats.avg_price)} $</b></span> | ||
| 268 | − className="btn btn-lg" | ||||
| 269 | − onClick={() => { | ||||
| 270 | − setView("carte"); | ||||
| 271 | − resultsRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); | ||||
| 272 | − }} | ||||
| 273 | − > | ||||
| 274 | − <IcoMap size={16} /> Voir la carte | ||||
| 275 | − </button> | ||||
| 276 | − </div> | ||||
| 277 | − <div className="benefits" aria-label="Avantages"> | ||||
| 278 | − <div className="benefit"> | ||||
| 279 | − <span className="benefit-ico"><IcoHouse size={20} /></span> | ||||
| 280 | − <span className="benefit-label">Nouveaux logements</span> | ||||
| 281 | − </div> | ||||
| 282 | − <div className="benefit"> | ||||
| 283 | − <span className="benefit-ico"><IcoSearch size={20} /></span> | ||||
| 284 | − <span className="benefit-label">Recherche facile</span> | ||||
| 285 | − </div> | ||||
| 286 | − <div className="benefit"> | ||||
| 287 | − <span className="benefit-ico"><IcoHeart size={20} /></span> | ||||
| 288 | − <span className="benefit-label">Favoris et alertes</span> | ||||
| 289 | − </div> | ||||
| 290 | − <div className="benefit"> | ||||
| 291 | − <span className="benefit-ico blue"><IcoPin size={20} /></span> | ||||
| 292 | − <span className="benefit-label">Partout au Québec</span> | ||||
| 293 | − </div> | ||||
| 294 | − </div> | ||||
| 295 | − <div className="stat-row"> | ||||
| 296 | − <span className="stat-chip"><span className="pulse" /> Données synchronisées en continu</span> | ||||
| 297 | − {stats && ( | ||||
| 298 | − <> | ||||
| 299 | − <span className="stat-chip"><b>{stats.total}</b> logements actifs</span> | ||||
| 300 | − <span className="stat-chip"><b>{stats.quebec ?? 0}</b> à Québec</span> | ||||
| 301 | − <span className="stat-chip"><b>{stats.levis ?? 0}</b> à Lévis</span> | ||||
| 302 | − {(stats.montreal ?? 0) > 0 && ( | ||||
| 303 | − <span className="stat-chip"><b>{stats.montreal}</b> Grand Montréal</span> | ||||
| 304 | − )} | ||||
| 305 | − {(stats.autres ?? 0) > 0 && ( | ||||
| 306 | − <span className="stat-chip"><b>{stats.autres}</b> ailleurs au Québec</span> | ||||
| 307 | − )} | ||||
| 308 | − {stats.avg_price != null && ( | ||||
| 309 | − <span className="stat-chip"> | ||||
| 310 | − loyer moyen <b>{Math.round(stats.avg_price)} $</b> | ||||
| 311 | − </span> | ||||
| 312 | − )} | ||||
| 313 | − </> | ||||
| 314 | )} | 312 | )} | ||
| 315 | </div> | 313 | </div> | ||
| 316 | </section> | 314 | </section> | ||
@@ -318,29 +316,31 @@ export default function Home() { | |||||
| 318 | {sheetOpen && ( | 316 | {sheetOpen && ( | ||
| 319 | <div className="sheet-backdrop" onClick={() => setSheetOpen(false)} aria-hidden="true" /> | 317 | <div className="sheet-backdrop" onClick={() => setSheetOpen(false)} aria-hidden="true" /> | ||
| 320 | )} | 318 | )} | ||
| 321 | − <section className={`filterbar ${sheetOpen ? "open" : ""}`} aria-label="Filtres"> | 319 | + <section className={`search-zone ${sheetOpen ? "open" : ""}`} aria-label="Recherche et filtres"> | ||
| 322 | <div className="sheet-handle" aria-hidden="true" /> | 320 | <div className="sheet-handle" aria-hidden="true" /> | ||
| 323 | <div className="sheet-head"> | 321 | <div className="sheet-head"> | ||
| 324 | − <span>Filtres</span> | 322 | + <span>Affiner la recherche</span> | ||
| 325 | <button className="sheet-close" onClick={() => setSheetOpen(false)} aria-label="Fermer les filtres"> | 323 | <button className="sheet-close" onClick={() => setSheetOpen(false)} aria-label="Fermer les filtres"> | ||
| 326 | ✕ | 324 | ✕ | ||
| 327 | </button> | 325 | </button> | ||
| 328 | </div> | 326 | </div> | ||
| 329 | 327 | ||||
| 330 | − {/* — rangée principale : recherche, ville, quartier, loyer, + filtres — */} | 328 | + {/* — la recherche, au cœur du produit : un grand champ souligné — */} | ||
| 331 | − <div className="f-primary"> | 329 | + <div className="q-big"> | ||
| 332 | − <div className="f-search"> | 330 | + <IcoSearch size={22} strokeWidth={2} /> | ||
| 333 | − <IcoSearch size={15} strokeWidth={2.4} /> | 331 | + <input | ||
| 334 | − <input | 332 | + id="f-q" placeholder="Où voulez-vous habiter ?" value={q} | ||
| 335 | − id="f-q" placeholder="Ville, quartier ou code postal" value={q} | 333 | + onChange={(e) => setQ(e.target.value)} | ||
| 336 | − onChange={(e) => setQ(e.target.value)} | 334 | + aria-label="Recherche — ville, quartier ou code postal" | ||
| 337 | − aria-label="Recherche" | 335 | + /> | ||
| 338 | − /> | 336 | + {q && ( | ||
| 339 | − {q && ( | 337 | + <button className="f-clear" onClick={() => setQ("")} aria-label="Effacer la recherche">✕</button> | ||
| 340 | − <button className="f-clear" onClick={() => setQ("")} aria-label="Effacer la recherche">✕</button> | 338 | + )} | ||
| 341 | − )} | 339 | + </div> | ||
| 342 | − </div> | 340 | + | ||
| 343 | − <label className="f-ctl"> | 341 | + {/* — critères inline, séparés par filets (pas de boîtes) — */} | ||
| 342 | + <div className="crit-line"> | ||||
| 343 | + <label className="crit"> | ||||
| 344 | <span>Ville</span> | 344 | <span>Ville</span> | ||
| 345 | <select value={city} onChange={(e) => { setCity(e.target.value); setSector(""); }}> | 345 | <select value={city} onChange={(e) => { setCity(e.target.value); setSector(""); }}> | ||
| 346 | <option value="">Toutes</option> | 346 | <option value="">Toutes</option> | ||
@@ -349,7 +349,7 @@ export default function Home() { | |||||
| 349 | ))} | 349 | ))} | ||
| 350 | </select> | 350 | </select> | ||
| 351 | </label> | 351 | </label> | ||
| 352 | − <label className="f-ctl"> | 352 | + <label className="crit"> | ||
| 353 | <span>Quartier</span> | 353 | <span>Quartier</span> | ||
| 354 | <select value={sector} onChange={(e) => setSector(e.target.value)}> | 354 | <select value={sector} onChange={(e) => setSector(e.target.value)}> | ||
| 355 | <option value="">Tous</option> | 355 | <option value="">Tous</option> | ||
@@ -358,7 +358,7 @@ export default function Home() { | |||||
| 358 | ))} | 358 | ))} | ||
| 359 | </select> | 359 | </select> | ||
| 360 | </label> | 360 | </label> | ||
| 361 | − <div className="f-ctl"> | 361 | + <div className="crit"> | ||
| 362 | <span>Loyer</span> | 362 | <span>Loyer</span> | ||
| 363 | <div className="range-pair"> | 363 | <div className="range-pair"> | ||
| 364 | <select aria-label="Loyer minimum" value={priceMin} | 364 | <select aria-label="Loyer minimum" value={priceMin} | ||
@@ -383,12 +383,12 @@ export default function Home() { | |||||
| 383 | </div> | 383 | </div> | ||
| 384 | </div> | 384 | </div> | ||
| 385 | <button | 385 | <button | ||
| 386 | − className={`f-more ${advOpen || advCount > 0 ? "on" : ""}`} | 386 | + className={`crit-more ${advOpen || advCount > 0 ? "on" : ""}`} | ||
| 387 | onClick={() => setAdvOpen(!advOpen)} | 387 | onClick={() => setAdvOpen(!advOpen)} | ||
| 388 | aria-expanded={advOpen} | 388 | aria-expanded={advOpen} | ||
| 389 | > | 389 | > | ||
| 390 | − <IcoSliders size={14} /> Plus de filtres | 390 | + Tous les critères | ||
| 391 | − {advCount > 0 && <span className="f-more-badge">{advCount}</span>} | 391 | + {advCount > 0 && <span className="crit-badge">{advCount}</span>} | ||
| 392 | <span className={`f-chev${advOpen ? " up" : ""}`} aria-hidden="true" /> | 392 | <span className={`f-chev${advOpen ? " up" : ""}`} aria-hidden="true" /> | ||
| 393 | </button> | 393 | </button> | ||
| 394 | </div> | 394 | </div> | ||
@@ -478,10 +478,21 @@ export default function Home() { | |||||
| 478 | )} | 478 | )} | ||
| 479 | 479 | ||||
| 480 | <button className="btn btn-primary sheet-apply" onClick={() => setSheetOpen(false)}> | 480 | <button className="btn btn-primary sheet-apply" onClick={() => setSheetOpen(false)}> | ||
| 481 | − Voir les résultats {listings ? `(${total})` : ""} | 481 | + Voir {listings ? `les ${total.toLocaleString("fr-CA")} logements` : "les résultats"} | ||
| 482 | </button> | 482 | </button> | ||
| 483 | </section> | 483 | </section> | ||
| 484 | 484 | ||||
| 485 | + {/* mobile : résumé des critères — ouvre la feuille (recherche intégrée, pas de FAB) */} | ||||
| 486 | + <button className="crit-summary" onClick={() => setSheetOpen(true)}> | ||||
| 487 | + <IcoSliders size={14} /> | ||||
| 488 | + <span className="cs-txt"> | ||||
| 489 | + {pills.length > 0 | ||||
| 490 | + ? pills.slice(0, 3).map((p) => p.label).join(" · ") + (pills.length > 3 ? ` · +${pills.length - 3}` : "") | ||||
| 491 | + : "Ville, quartier, budget, critères…"} | ||||
| 492 | + </span> | ||||
| 493 | + <span className="f-chev" aria-hidden="true" /> | ||||
| 494 | + </button> | ||||
| 495 | + | ||||
| 485 | <div className="chips" role="group" aria-label="Filtres rapides"> | 496 | <div className="chips" role="group" aria-label="Filtres rapides"> | ||
| 486 | {availableTypes.map((t) => ( | 497 | {availableTypes.map((t) => ( | ||
| 487 | <button | 498 | <button | ||
@@ -534,30 +545,37 @@ export default function Home() { | |||||
| 534 | </div> | 545 | </div> | ||
| 535 | )} | 546 | )} | ||
| 536 | 547 | ||||
| 537 | − <div className="results-head" ref={resultsRef}> | 548 | + <div className="results-bar" ref={resultsRef}> | ||
| 538 | − <h2>Logements disponibles</h2> | 549 | + <h2 className="rb-count"> | ||
| 539 | − <div className="results-tools"> | ||||
| 540 | {view === "carte" | 550 | {view === "carte" | ||
| 541 | − ? mapTotal != null && ( | 551 | + ? (mapTotal != null | ||
| 542 | − <span>{mapTotal.toLocaleString("fr-CA")} résultat{mapTotal > 1 ? "s" : ""}</span> | 552 | + ? <><b>{mapTotal.toLocaleString("fr-CA")}</b> logement{mapTotal > 1 ? "s" : ""}</> | ||
| 543 | − ) | 553 | + : "Logements") | ||
| 544 | − : listings && <span>{total} résultat{total > 1 ? "s" : ""}</span>} | 554 | + : (listings | ||
| 545 | − <div className="view-toggle" role="tablist" aria-label="Mode d'affichage"> | 555 | + ? <><b>{total.toLocaleString("fr-CA")}</b> logement{total > 1 ? "s" : ""}</> | ||
| 556 | + : "Logements")} | ||||
| 557 | + </h2> | ||||
| 558 | + <div className="rb-tools"> | ||||
| 559 | + <div className="rb-tabs" role="tablist" aria-label="Mode d'affichage"> | ||||
| 546 | <button | 560 | <button | ||
| 547 | role="tab" aria-selected={view === "liste"} | 561 | role="tab" aria-selected={view === "liste"} | ||
| 548 | − className={view === "liste" ? "on" : ""} | 562 | + className={`rb-tab ${view === "liste" ? "on" : ""}`} | ||
| 549 | onClick={() => setView("liste")} | 563 | onClick={() => setView("liste")} | ||
| 550 | > | 564 | > | ||
| 551 | <IcoList size={13} /> Liste | 565 | <IcoList size={13} /> Liste | ||
| 552 | </button> | 566 | </button> | ||
| 553 | <button | 567 | <button | ||
| 554 | role="tab" aria-selected={view === "carte"} | 568 | role="tab" aria-selected={view === "carte"} | ||
| 555 | − className={view === "carte" ? "on" : ""} | 569 | + className={`rb-tab ${view === "carte" ? "on" : ""}`} | ||
| 556 | onClick={() => setView("carte")} | 570 | onClick={() => setView("carte")} | ||
| 557 | > | 571 | > | ||
| 558 | <IcoMap size={13} /> Carte | 572 | <IcoMap size={13} /> Carte | ||
| 559 | </button> | 573 | </button> | ||
| 560 | </div> | 574 | </div> | ||
| 575 | + <button className="rb-filters" onClick={() => setSheetOpen(true)}> | ||||
| 576 | + <IcoSliders size={13} /> Filtres | ||||
| 577 | + {activeFilters > 0 && <span className="rb-badge">{activeFilters}</span>} | ||||
| 578 | + </button> | ||||
| 561 | </div> | 579 | </div> | ||
| 562 | </div> | 580 | </div> | ||
| 563 | 581 | ||||
@@ -573,7 +591,7 @@ export default function Home() { | |||||
| 573 | )} | 591 | )} | ||
| 574 | 592 | ||||
| 575 | {!error && view === "liste" && listings === null && ( | 593 | {!error && view === "liste" && listings === null && ( | ||
| 576 | − <div className="grid" aria-busy="true"> | 594 | + <div className="grid grid-edito" aria-busy="true"> | ||
| 577 | {Array.from({ length: 8 }).map((_, i) => ( | 595 | {Array.from({ length: 8 }).map((_, i) => ( | ||
| 578 | <div className="skel" key={i}> | 596 | <div className="skel" key={i}> | ||
| 579 | <div className="sk-img" /> | 597 | <div className="sk-img" /> | ||
@@ -598,37 +616,29 @@ export default function Home() { | |||||
| 598 | )} | 616 | )} | ||
| 599 | 617 | ||||
| 600 | {!error && view === "carte" && ( | 618 | {!error && view === "carte" && ( | ||
| 601 | − <Suspense | 619 | + <div className="view-pane" key="carte"> | ||
| 602 | − fallback={ | 620 | + <Suspense | ||
| 603 | − <div className="map-split"> | 621 | + fallback={ | ||
| 604 | − <div className="mapview map-loading">Chargement de la carte…</div> | 622 | + <div className="map-split"> | ||
| 605 | − </div> | 623 | + <div className="mapview map-loading">Chargement de la carte…</div> | ||
| 606 | − } | 624 | + </div> | ||
| 607 | − > | 625 | + } | ||
| 608 | − <MapSearch filters={filters} onTotal={setMapTotal} /> | 626 | + > | ||
| 609 | − </Suspense> | 627 | + <MapSearch filters={filters} onTotal={setMapTotal} /> | ||
| 628 | + </Suspense> | ||||
| 629 | + </div> | ||||
| 610 | )} | 630 | )} | ||
| 611 | 631 | ||||
| 612 | {!error && view === "liste" && listings !== null && listings.length > 0 && ( | 632 | {!error && view === "liste" && listings !== null && listings.length > 0 && ( | ||
| 613 | − <> | 633 | + <div className="view-pane" key="liste"> | ||
| 614 | − <div className="grid"> | 634 | + <div className="grid grid-edito"> | ||
| 615 | {listings.map((l) => ( | 635 | {listings.map((l) => ( | ||
| 616 | <ListingCard key={l.uid} l={l} /> | 636 | <ListingCard key={l.uid} l={l} /> | ||
| 617 | ))} | 637 | ))} | ||
| 618 | </div> | 638 | </div> | ||
| 619 | <Pager page={page} pageSize={PAGE_SIZE} total={total} onPage={goToPage} /> | 639 | <Pager page={page} pageSize={PAGE_SIZE} total={total} onPage={goToPage} /> | ||
| 620 | − </> | 640 | + </div> | ||
| 621 | )} | 641 | )} | ||
| 622 | − | ||||
| 623 | − {/* Bouton flottant mobile — ouvre la feuille de filtres */} | ||||
| 624 | − <button | ||||
| 625 | − className={`fab${view === "carte" ? " fab-carte" : ""}`} | ||||
| 626 | − onClick={() => setSheetOpen(true)} | ||||
| 627 | − aria-label="Ouvrir les filtres" | ||||
| 628 | − > | ||||
| 629 | − <IcoSliders size={15} /> Filtres | ||||
| 630 | − {activeFilters > 0 && <span className="fab-badge">{activeFilters}</span>} | ||||
| 631 | − </button> | ||||
| 632 | </div> | 642 | </div> | ||
| 633 | ); | 643 | ); | ||
| 634 | } | 644 | } | ||
modified
frontend/src/styles.css
+310 −12
@@ -110,18 +110,6 @@ html, body { overflow-x: clip; } | |||||
| 110 | .hero h1 .outline { color: var(--navy); } | 110 | .hero h1 .outline { color: var(--navy); } | ||
| 111 | .hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; } | 111 | .hero p.lede { color: var(--ink-2); font-size: 17px; max-width: 640px; margin: 18px 0 0; } | ||
| 112 | 112 | ||||
| 113 | −/* — CTA du héro : accès direct aux résultats / à la carte — */ | ||||
| 114 | −.hero-cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; } | ||||
| 115 | −.btn-lg { min-height: 52px; padding: 14px 26px; font-size: 15px; border-radius: 12px; } | ||||
| 116 | −.btn-lg svg { flex: none; } | ||||
| 117 | −.hero-cta .btn-primary { box-shadow: 5px 5px 0 var(--accent); } | ||||
| 118 | −.hero-cta .btn-primary:hover { transform: translate(-2px, -2px); box-shadow: 7px 7px 0 var(--accent); } | ||||
| 119 | −.hero-cta .btn-primary:active { transform: none; box-shadow: 3px 3px 0 var(--accent); } | ||||
| 120 | −@media (max-width: 640px) { | ||||
| 121 | − .hero-cta { gap: 10px; } | ||||
| 122 | − .hero-cta .btn { flex: 1 1 45%; } | ||||
| 123 | −} | ||||
| 124 | − | ||||
| 125 | .stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; } | 113 | .stat-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; } | ||
| 126 | .stat-chip { | 114 | .stat-chip { | ||
| 127 | background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill); | 115 | background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-pill); | ||
@@ -2407,3 +2395,313 @@ html { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */ | |||||
| 2407 | .hd-crit { min-width: 86px; font-size: 10px; } | 2395 | .hd-crit { min-width: 86px; font-size: 10px; } | ||
| 2408 | .hiver-detail li { font-size: 12.5px; } | 2396 | .hiver-detail li { font-size: 12.5px; } | ||
| 2409 | } | 2397 | } | ||
| 2398 | + | ||||
| 2399 | +/* ============================================================================= | ||||
| 2400 | + REFONTE ÉDITORIALE v2 (2026-08-25) — accueil « produit premium » | ||||
| 2401 | + Direction : moins de boîtes, hiérarchie typographique, hairlines, données | ||||
| 2402 | + vivantes, image dominante. Primitives locales : rayons 0/6/10/18, | ||||
| 2403 | + durées 150/240/350 ms. Couche posée en FIN de fichier : elle a le dernier | ||||
| 2404 | + mot sur la cascade (y compris sur la couche « harmonisation » ci-dessus). | ||||
| 2405 | +============================================================================= */ | ||||
| 2406 | +:root { | ||||
| 2407 | + --r-0: 0px; --r-1: 6px; --r-2: 10px; --r-3: 18px; | ||||
| 2408 | + --dur-1: 150ms; --dur-2: 240ms; --dur-3: 350ms; | ||||
| 2409 | + --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1); | ||||
| 2410 | + --hairline: rgba(20, 24, 20, 0.14); | ||||
| 2411 | + --hairline-strong: rgba(20, 24, 20, 0.6); | ||||
| 2412 | +} | ||||
| 2413 | + | ||||
| 2414 | +/* ---- Héro : composition typographique, zéro boîte ---- */ | ||||
| 2415 | +.hero h1.hero-display { | ||||
| 2416 | + font-size: clamp(44px, 7.2vw, 88px); | ||||
| 2417 | + line-height: 1.0; letter-spacing: -0.045em; max-width: 1020px; margin-top: 18px; | ||||
| 2418 | +} | ||||
| 2419 | +.brush { position: relative; display: inline-block; padding: 0 0.06em; } | ||||
| 2420 | +.brush::after { /* surlignage orange irrégulier (trait SVG) — pas un rectangle */ | ||||
| 2421 | + content: ""; position: absolute; left: -3%; right: -3%; bottom: 0.03em; height: 0.34em; | ||||
| 2422 | + z-index: -1; transform: rotate(-0.6deg); | ||||
| 2423 | + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 26' preserveAspectRatio='none'%3E%3Cpath d='M5 17 C 60 8, 118 21, 168 13 S 258 10, 295 15' fill='none' stroke='%23ff6a00' stroke-width='11' stroke-linecap='round' opacity='0.92'/%3E%3C/svg%3E") no-repeat center / 100% 100%; | ||||
| 2424 | +} | ||||
| 2425 | +.hero p.lede { font-size: 16.5px; max-width: 580px; } | ||||
| 2426 | +.hero::before { opacity: 0.8; } | ||||
| 2427 | +.hero::after { opacity: 0.55; } | ||||
| 2428 | + | ||||
| 2429 | +/* ---- Ligne de données vivantes (mono + filets — plus de chips) ---- */ | ||||
| 2430 | +.live-line { | ||||
| 2431 | + display: flex; flex-wrap: wrap; align-items: baseline; | ||||
| 2432 | + margin-top: 34px; font-family: var(--font-mono); font-size: 12px; | ||||
| 2433 | + color: var(--ink-2); letter-spacing: 0.02em; row-gap: 10px; | ||||
| 2434 | +} | ||||
| 2435 | +.live-flag { | ||||
| 2436 | + display: inline-flex; align-items: center; gap: 7px; | ||||
| 2437 | + font-weight: 700; font-size: 10.5px; letter-spacing: 0.18em; | ||||
| 2438 | + text-transform: uppercase; color: var(--accent-deep); | ||||
| 2439 | + padding-right: 14px; margin-right: 14px; border-right: 1px solid var(--hairline-strong); | ||||
| 2440 | +} | ||||
| 2441 | +.live-dot { | ||||
| 2442 | + width: 7px; height: 7px; border-radius: 50%; background: var(--accent); | ||||
| 2443 | + animation: pulse 2.4s ease infinite; | ||||
| 2444 | +} | ||||
| 2445 | +.live-item { padding-right: 14px; margin-right: 14px; border-right: 1px solid var(--hairline); } | ||||
| 2446 | +.live-item:last-child { border-right: 0; padding-right: 0; margin-right: 0; } | ||||
| 2447 | +.live-item b { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; } | ||||
| 2448 | +@media (max-width: 640px) { | ||||
| 2449 | + .live-line { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; | ||||
| 2450 | + white-space: nowrap; margin-right: -16px; padding-right: 16px; } | ||||
| 2451 | + .live-line::-webkit-scrollbar { display: none; } | ||||
| 2452 | +} | ||||
| 2453 | + | ||||
| 2454 | +/* ---- Recherche au cœur : grand champ souligné + critères en filets ---- */ | ||||
| 2455 | +.search-zone { margin: 42px 0 0; } | ||||
| 2456 | +.q-big { | ||||
| 2457 | + display: flex; align-items: center; gap: 14px; | ||||
| 2458 | + border: 0; border-bottom: 2px solid var(--ink); border-radius: 0; | ||||
| 2459 | + background: transparent; padding: 4px 2px 14px; color: var(--ink-3); | ||||
| 2460 | + transition: border-color var(--dur-2) var(--ease-out), color var(--dur-2) ease; | ||||
| 2461 | +} | ||||
| 2462 | +.q-big:focus-within { border-color: var(--accent); color: var(--accent-deep); } | ||||
| 2463 | +.q-big svg { flex: none; } | ||||
| 2464 | +.q-big input { | ||||
| 2465 | + flex: 1; min-width: 0; border: 0; background: none; outline: none; padding: 0; | ||||
| 2466 | + font-family: var(--font-display); font-weight: 600; | ||||
| 2467 | + font-size: clamp(19px, 2.8vw, 28px); letter-spacing: -0.02em; color: var(--ink); | ||||
| 2468 | +} | ||||
| 2469 | +.q-big input::placeholder { color: var(--ink-3); font-weight: 500; } | ||||
| 2470 | +.crit-line { display: flex; align-items: stretch; flex-wrap: wrap; } | ||||
| 2471 | +.crit { | ||||
| 2472 | + display: flex; flex-direction: column; justify-content: center; gap: 1px; | ||||
| 2473 | + padding: 12px 26px 12px 0; margin-right: 26px; | ||||
| 2474 | + border-right: 1px solid var(--hairline); min-width: 0; cursor: pointer; | ||||
| 2475 | +} | ||||
| 2476 | +.crit > span { | ||||
| 2477 | + font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; | ||||
| 2478 | + text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-3); | ||||
| 2479 | +} | ||||
| 2480 | +.crit select { | ||||
| 2481 | + border: 0; background: transparent; outline: none; cursor: pointer; | ||||
| 2482 | + font-family: var(--font-display); font-weight: 700; font-size: 15.5px; | ||||
| 2483 | + color: var(--ink); appearance: none; -webkit-appearance: none; | ||||
| 2484 | + padding: 2px 18px 2px 0; min-width: 0; max-width: 190px; text-overflow: ellipsis; | ||||
| 2485 | + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='5'%3E%3Cpath d='M0 0l4.5 5L9 0z' fill='%238b928c'/%3E%3C/svg%3E"); | ||||
| 2486 | + background-repeat: no-repeat; background-position: right center; | ||||
| 2487 | +} | ||||
| 2488 | +.crit:hover select { color: var(--accent-deep); } | ||||
| 2489 | +.crit .range-pair select { max-width: 88px; } | ||||
| 2490 | +.crit-more { | ||||
| 2491 | + display: inline-flex; align-items: center; gap: 9px; align-self: center; | ||||
| 2492 | + border: 0; background: none; padding: 12px 0; cursor: pointer; | ||||
| 2493 | + font-family: var(--font-display); font-weight: 700; font-size: 14.5px; color: var(--ink); | ||||
| 2494 | + transition: color var(--dur-1) ease; | ||||
| 2495 | +} | ||||
| 2496 | +.crit-more:hover, .crit-more.on { color: var(--accent-deep); } | ||||
| 2497 | +.crit-badge, .rb-badge { | ||||
| 2498 | + display: inline-grid; place-items: center; min-width: 19px; height: 19px; padding: 0 5px; | ||||
| 2499 | + border-radius: var(--r-pill); background: var(--accent); color: var(--on-accent); | ||||
| 2500 | + font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; | ||||
| 2501 | +} | ||||
| 2502 | +/* panneau avancé : filet, pas de boîte (réutilise .f-adv/.f-group/.seg) */ | ||||
| 2503 | +.search-zone .f-adv { border-top: 1px solid var(--hairline); margin-top: 2px; padding-top: 18px; } | ||||
| 2504 | +.search-zone .seg button { | ||||
| 2505 | + border: 1px solid var(--hairline-strong); background: transparent; color: var(--ink-2); | ||||
| 2506 | + min-height: 38px; font-weight: 600; margin-left: -1px; | ||||
| 2507 | +} | ||||
| 2508 | +.search-zone .seg button:first-child { border-radius: var(--r-1) 0 0 var(--r-1); } | ||||
| 2509 | +.search-zone .seg button:last-child { border-radius: 0 var(--r-1) var(--r-1) 0; } | ||||
| 2510 | +.search-zone .seg button:hover { background: var(--accent-soft); color: var(--accent-deep); } | ||||
| 2511 | +.search-zone .seg button.on { background: var(--ink); border-color: var(--ink); color: var(--paper); box-shadow: none; } | ||||
| 2512 | +.search-zone .f-native { border: 1px solid var(--hairline-strong); background: transparent; } | ||||
| 2513 | + | ||||
| 2514 | +/* résumé mobile des critères — remplace le FAB (voir media 640 plus bas) */ | ||||
| 2515 | +.crit-summary { display: none; } | ||||
| 2516 | + | ||||
| 2517 | +/* ---- Barre de résultats sticky : N logements · Liste|Carte · Filtres ---- */ | ||||
| 2518 | +.results-bar { | ||||
| 2519 | + position: sticky; top: 64px; z-index: var(--z-sticky, 300); | ||||
| 2520 | + display: flex; align-items: center; gap: 16px; | ||||
| 2521 | + margin: 30px 0 22px; padding: 12px 0 11px; | ||||
| 2522 | + border-bottom: 1px solid var(--hairline-strong); | ||||
| 2523 | + background: color-mix(in srgb, var(--paper) 86%, transparent); | ||||
| 2524 | + backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); | ||||
| 2525 | +} | ||||
| 2526 | +@supports not (background: color-mix(in srgb, red 50%, blue)) { | ||||
| 2527 | + .results-bar { background: rgba(245, 243, 238, 0.92); } | ||||
| 2528 | +} | ||||
| 2529 | +.rb-count { margin: 0; font-size: clamp(16px, 2vw, 20px); letter-spacing: -0.02em; color: var(--ink); } | ||||
| 2530 | +.rb-count b { font-variant-numeric: tabular-nums; } | ||||
| 2531 | +.rb-tools { margin-left: auto; display: flex; align-items: center; gap: 20px; } | ||||
| 2532 | +.rb-tabs { display: flex; gap: 18px; } | ||||
| 2533 | +.rb-tab { | ||||
| 2534 | + position: relative; display: inline-flex; align-items: center; gap: 6px; | ||||
| 2535 | + border: 0; background: none; padding: 8px 0; cursor: pointer; | ||||
| 2536 | + font-family: var(--font-display); font-weight: 700; font-size: 13.5px; color: var(--ink-3); | ||||
| 2537 | + transition: color var(--dur-1) ease; | ||||
| 2538 | +} | ||||
| 2539 | +.rb-tab:hover, .rb-tab.on { color: var(--ink); } | ||||
| 2540 | +.rb-tab::after { | ||||
| 2541 | + content: ""; position: absolute; left: 0; right: 0; bottom: -12px; height: 2.5px; | ||||
| 2542 | + background: var(--accent); transform: scaleX(0); transform-origin: left; | ||||
| 2543 | + transition: transform var(--dur-2) var(--ease-out); | ||||
| 2544 | +} | ||||
| 2545 | +.rb-tab.on::after { transform: scaleX(1); } | ||||
| 2546 | +.rb-filters { | ||||
| 2547 | + display: inline-flex; align-items: center; gap: 7px; | ||||
| 2548 | + border: 0; background: none; padding: 8px 0; cursor: pointer; | ||||
| 2549 | + font-family: var(--font-display); font-weight: 700; font-size: 13.5px; color: var(--ink); | ||||
| 2550 | +} | ||||
| 2551 | +.rb-filters:hover { color: var(--accent-deep); } | ||||
| 2552 | +@media (min-width: 641px) { .rb-filters { display: none; } } | ||||
| 2553 | + | ||||
| 2554 | +/* ---- Transition douce liste ↔ carte ---- */ | ||||
| 2555 | +.view-pane { animation: pane-in var(--dur-2) var(--ease-out); } | ||||
| 2556 | +@keyframes pane-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } | ||||
| 2557 | + | ||||
| 2558 | +/* ---- Chips rapides : plus légères (hairline, texte d'abord) ---- */ | ||||
| 2559 | +.chip { | ||||
| 2560 | + border: 1px solid var(--hairline-strong); background: transparent; | ||||
| 2561 | + min-height: 38px; padding: 7px 15px; font-size: 12.5px; box-shadow: none; | ||||
| 2562 | +} | ||||
| 2563 | +.chip:hover { transform: none; box-shadow: none; background: var(--accent-soft); color: var(--accent-deep); } | ||||
| 2564 | +.chip.on, .chip.chip-deal.on { background: var(--ink); border-color: var(--ink); color: var(--paper); box-shadow: none; } | ||||
| 2565 | +.chips { margin: 20px 0 0; } | ||||
| 2566 | + | ||||
| 2567 | +/* ---- Pastilles actives : discrètes, survol = retrait ---- */ | ||||
| 2568 | +.pills { margin: 14px 0 0; } | ||||
| 2569 | +.pills .pill { border: 1px solid var(--hairline-strong); background: transparent; box-shadow: none; padding: 5px 12px; font-size: 12px; } | ||||
| 2570 | +.pills .pill:hover { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); } | ||||
| 2571 | + | ||||
| 2572 | +/* ---- Cartes éditoriales : l'image domine, plus de boîte ---- */ | ||||
| 2573 | +.card, .card:hover { background: transparent; border: 0; border-radius: 0; box-shadow: none; transform: none; } | ||||
| 2574 | +.card-img { border-radius: var(--r-2); aspect-ratio: 4/3; } | ||||
| 2575 | +.card-img img { transition: transform var(--dur-3) var(--ease-out); } | ||||
| 2576 | +@media (hover: hover) { | ||||
| 2577 | + .card:hover .card-img img { transform: scale(1.025); } | ||||
| 2578 | + .card:hover .card-price { color: var(--accent-deep); } | ||||
| 2579 | +} | ||||
| 2580 | +.card:focus-visible { outline: 2px solid var(--green); outline-offset: 4px; border-radius: var(--r-2); } | ||||
| 2581 | +.card-body { padding: 13px 2px 0; gap: 3px; } | ||||
| 2582 | +.card-price { font-size: 22px; letter-spacing: -0.03em; color: var(--ink); transition: color var(--dur-1) ease; } | ||||
| 2583 | +.card-title { font-size: 14px; font-weight: 500; color: var(--ink-2); } | ||||
| 2584 | +.card-meta { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); } | ||||
| 2585 | +.card-meta .sep { width: 3px; height: 3px; border-radius: 50%; background: var(--accent); } | ||||
| 2586 | +.card-foot { border-top: 0; padding-top: 8px; } | ||||
| 2587 | +.source-tag { background: none; border: 0; border-radius: 0; padding: 0; color: var(--ink-3); font-size: 10px; letter-spacing: 0.08em; max-width: 62%; } | ||||
| 2588 | +.avail { font-size: 11px; color: var(--ink-3); } | ||||
| 2589 | +.badge { border: 0; border-radius: 4px; background: rgba(20, 24, 20, 0.78); color: var(--paper); | ||||
| 2590 | + backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); | ||||
| 2591 | + font-size: 9.5px; padding: 4px 9px; letter-spacing: 0.08em; } | ||||
| 2592 | +.badge.type { background: var(--accent); color: var(--on-accent); } | ||||
| 2593 | +.badge.right { background: rgba(20, 24, 20, 0.55); } | ||||
| 2594 | + | ||||
| 2595 | +/* ---- Grille à rythme éditorial (accueil) : grand / petits / panoramique ---- */ | ||||
| 2596 | +.grid.grid-edito { grid-template-columns: repeat(3, 1fr); gap: 36px 26px; } | ||||
| 2597 | +.grid-edito > *:nth-child(7n+1) { grid-column: span 2; } | ||||
| 2598 | +.grid-edito > *:nth-child(7n+1) .card-img, | ||||
| 2599 | +.grid-edito > .skel:nth-child(7n+1) .sk-img { aspect-ratio: 16/9; } | ||||
| 2600 | +.grid-edito > *:nth-child(7n+1) .card-price { font-size: 27px; } | ||||
| 2601 | +.grid-edito > *:nth-child(7n+1) .card-title { font-size: 15px; } | ||||
| 2602 | +@media (max-width: 980px) { | ||||
| 2603 | + .grid.grid-edito { grid-template-columns: 1fr 1fr; gap: 30px 20px; } | ||||
| 2604 | +} | ||||
| 2605 | +@media (max-width: 640px) { | ||||
| 2606 | + .grid.grid-edito { grid-template-columns: 1fr; gap: 32px; } | ||||
| 2607 | + .grid-edito > *:nth-child(7n+1) { grid-column: auto; } | ||||
| 2608 | +} | ||||
| 2609 | +@media (prefers-reduced-motion: no-preference) { | ||||
| 2610 | + .grid-edito > * { animation: rise 0.5s var(--ease-out) backwards; } | ||||
| 2611 | + .grid-edito > *:nth-child(3n+2) { animation-delay: 60ms; } | ||||
| 2612 | + .grid-edito > *:nth-child(3n) { animation-delay: 120ms; } | ||||
| 2613 | +} | ||||
| 2614 | +@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } | ||||
| 2615 | + | ||||
| 2616 | +/* ---- Squelettes sans boîte ---- */ | ||||
| 2617 | +.skel { border: 0; border-radius: 0; background: transparent; } | ||||
| 2618 | +.skel .sk-img { aspect-ratio: 4/3; border-radius: var(--r-2); } | ||||
| 2619 | +.skel .sk-line { margin: 12px 2px; } | ||||
| 2620 | + | ||||
| 2621 | +/* ---- Pagination éditoriale : chiffres nus, page courante soulignée ---- */ | ||||
| 2622 | +.pager { border-top: 1px solid var(--hairline); padding-top: 16px; } | ||||
| 2623 | +.pager-info { font-family: var(--font-mono); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); } | ||||
| 2624 | +.pager-btn { | ||||
| 2625 | + min-width: 34px; height: 38px; padding: 0 8px; border: 0; border-radius: var(--r-1); | ||||
| 2626 | + background: none; color: var(--ink-2); | ||||
| 2627 | + font-family: var(--font-display); font-size: 13.5px; font-weight: 600; position: relative; | ||||
| 2628 | +} | ||||
| 2629 | +.pager-btn:hover:not(:disabled):not(.on) { background: var(--accent-soft); color: var(--accent-deep); border: 0; } | ||||
| 2630 | +.pager-btn.on { background: none; border: 0; color: var(--ink); font-weight: 700; } | ||||
| 2631 | +.pager-btn.on::after { content: ""; position: absolute; left: 9px; right: 9px; bottom: 3px; height: 2.5px; background: var(--accent); } | ||||
| 2632 | +.pager-arrow { border: 1px solid var(--hairline-strong); } | ||||
| 2633 | + | ||||
| 2634 | +/* ---- Navigation mobile flottante (pilule détachée du bord) ---- */ | ||||
| 2635 | +@media (max-width: 760px) { | ||||
| 2636 | + .tabbar { | ||||
| 2637 | + left: 12px; right: 12px; bottom: calc(10px + env(safe-area-inset-bottom)); | ||||
| 2638 | + border: 1px solid var(--hairline-strong); border-radius: var(--r-3); | ||||
| 2639 | + background: rgba(250, 249, 245, 0.88); | ||||
| 2640 | + box-shadow: 0 14px 34px rgba(20, 24, 20, 0.16); | ||||
| 2641 | + padding: 6px; | ||||
| 2642 | + } | ||||
| 2643 | + .tabbar a { border-radius: 12px; font-size: 10px; color: var(--ink-3); gap: 2px; } | ||||
| 2644 | + .tabbar a.active { color: var(--ink); } | ||||
| 2645 | + .tabbar a.active::after { content: ""; width: 16px; height: 2.5px; border-radius: 2px; background: var(--accent); } | ||||
| 2646 | + body { padding-bottom: calc(80px + env(safe-area-inset-bottom)); } | ||||
| 2647 | + .footer { padding-bottom: calc(86px + env(safe-area-inset-bottom)); } | ||||
| 2648 | +} | ||||
| 2649 | + | ||||
| 2650 | +/* ---- Mobile : recherche condensée + feuille de critères premium ---- */ | ||||
| 2651 | +@media (max-width: 640px) { | ||||
| 2652 | + .results-bar { top: 56px; gap: 10px; } | ||||
| 2653 | + .search-zone { margin: 30px 0 0; } | ||||
| 2654 | + .crit-line, .search-zone .f-adv, .search-zone .sheet-apply { display: none; } | ||||
| 2655 | + .crit-summary { | ||||
| 2656 | + display: flex; align-items: center; gap: 10px; width: 100%; | ||||
| 2657 | + border: 0; border-bottom: 1px solid var(--hairline); background: none; | ||||
| 2658 | + padding: 14px 2px; margin: 0; cursor: pointer; text-align: left; | ||||
| 2659 | + font-family: var(--font-mono); font-size: 12px; color: var(--ink-2); | ||||
| 2660 | + } | ||||
| 2661 | + .crit-summary svg { flex: none; color: var(--ink); } | ||||
| 2662 | + .crit-summary .cs-txt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | ||||
| 2663 | + .crit-summary .f-chev { margin: 0 2px 0 0; transform: rotate(45deg); } | ||||
| 2664 | + | ||||
| 2665 | + .search-zone.open { | ||||
| 2666 | + display: flex; flex-direction: column; gap: 12px; | ||||
| 2667 | + position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal, 900); | ||||
| 2668 | + margin: 0; border-radius: var(--r-3) var(--r-3) 0 0; background: var(--surface); | ||||
| 2669 | + max-height: 86dvh; overflow-y: auto; -webkit-overflow-scrolling: touch; | ||||
| 2670 | + padding: 10px 18px calc(16px + env(safe-area-inset-bottom)); | ||||
| 2671 | + box-shadow: 0 -18px 50px rgba(20, 24, 20, 0.3); | ||||
| 2672 | + animation: sheet-up 0.26s var(--ease-out); | ||||
| 2673 | + } | ||||
| 2674 | + .search-zone.open .sheet-handle { | ||||
| 2675 | + display: block; flex: none; width: 40px; height: 4.5px; | ||||
| 2676 | + border-radius: 999px; background: var(--line); margin: 0 auto; | ||||
| 2677 | + } | ||||
| 2678 | + .search-zone.open .sheet-head { | ||||
| 2679 | + display: flex; justify-content: space-between; align-items: center; | ||||
| 2680 | + font-family: var(--font-display); font-weight: 700; font-size: 17px; | ||||
| 2681 | + letter-spacing: -0.01em; color: var(--ink); | ||||
| 2682 | + position: sticky; top: -10px; background: var(--surface); padding: 6px 0 8px; | ||||
| 2683 | + border-bottom: 1px solid var(--hairline); z-index: 1; | ||||
| 2684 | + } | ||||
| 2685 | + .search-zone.open .crit-summary { display: none; } | ||||
| 2686 | + .search-zone.open .q-big { padding: 2px 0 10px; } | ||||
| 2687 | + .search-zone.open .q-big input { font-size: 17px; } | ||||
| 2688 | + .search-zone.open .crit-line { display: flex; flex-direction: column; } | ||||
| 2689 | + .search-zone.open .crit { border-right: 0; border-bottom: 1px solid var(--hairline); margin: 0; padding: 10px 0; } | ||||
| 2690 | + .search-zone.open .crit select { max-width: none; width: 100%; font-size: 16px; } | ||||
| 2691 | + .search-zone.open .crit .range-pair select { width: auto; flex: 1; } | ||||
| 2692 | + .search-zone.open .crit-more { display: none; } | ||||
| 2693 | + .search-zone.open .f-adv { display: grid; border-top: 0; padding-top: 4px; margin-top: 0; } | ||||
| 2694 | + .search-zone.open .sheet-apply { | ||||
| 2695 | + display: flex; position: sticky; bottom: 0; z-index: 1; width: 100%; min-height: 50px; | ||||
| 2696 | + box-shadow: 0 -14px 18px -12px rgba(20, 24, 20, 0.35); | ||||
| 2697 | + } | ||||
| 2698 | +} | ||||
| 2699 | + | ||||
| 2700 | +/* ---- Widget KA Agent : dégagé de la tabbar flottante au doigt ---- */ | ||||
| 2701 | +@media (max-width: 760px) { | ||||
| 2702 | + .kaa-btn.kaa-btn { bottom: calc(86px + env(safe-area-inset-bottom, 0px)); right: 14px; } | ||||
| 2703 | + .kaa-panel.kaa-panel:not(.kaa-full) { bottom: calc(152px + env(safe-area-inset-bottom, 0px)); } | ||||
| 2704 | +} | ||||
| 2705 | + | ||||
| 2706 | +/* les fiches s'alignent en haut : pas d'étirement vide sous les petites */ | ||||
| 2707 | +.grid.grid-edito { align-items: start; } | ||||
| 2410 | 2708 | ||||