/* ============================================================ SF Logements — /proprietes-v2 Onglet CSS de Slater · reconstruction 2026-08-04 PHASE 1 : barre d'outils Le DESIGN vit dans les classes jsw-* du Designer Webflow (créées par API, éditables dans le panneau Style). Ici : uniquement les états dynamiques que le Designer ne sait pas exprimer + l'habillage des éléments natifs Finsweet. Zéro !important. ============================================================ */ /* Les boutons créés par l'API sont rendus en <a> */ .jsw-btn, .jsw-view-btn { text-decoration: none; } /* ---------- Pastilles de région ---------- */ /* état coché — on cible :has(input:checked) ET la classe is-list-active que Finsweet pose sur le label sélectionné. (L'ancien sélecteur ciblait .jsw-region-check:checked, mais l'input a perdu cette classe → highlight qui ne s'appliquait jamais. 2026-08-05.) */ /* .jsw-selected est posée par le JS d'après la CONDITION Finsweet (fiable) ; :has(input:checked) et is-list-active sont des filets (l'input perd son checked au re-render CMS, donc .jsw-selected est la source de vérité). */ .jsw-region-pill.jsw-selected, .jsw-region-pill:has(input:checked), .jsw-region-pill.is-list-active { background-color: #002474; border-color: #002474; color: #fff; } .jsw-region-pill.jsw-selected .jsw-region-name, .jsw-region-pill:has(input:checked) .jsw-region-name, .jsw-region-pill.is-list-active .jsw-region-name { color: #fff; } .jsw-region-pill.jsw-selected .jsw-region-count, .jsw-region-pill:has(input:checked) .jsw-region-count, .jsw-region-pill.is-list-active .jsw-region-count { color: rgba(255, 255, 255, 0.72); } /* régions sans logement : on ne les montre pas */ .jsw-region-item:has([fs-list-emptyfacet]), .jsw-region-item.jsw-is-empty { display: none; } /* ---------- Pastilles de filtres actifs (natives Finsweet) ---------- */ /* la pastille aligne tous ses enfants (champ, valeur, ✕) sur le même axe central horizontal. */ .jsw-chip { display: inline-flex; align-items: center; } .jsw-chip [fs-list-element="tag-operator"] { display: none; } .jsw-chip [fs-list-element="tag-field"] { color: #8A8D96; } /* ⚠️ la vieille classe filter_tags_remove le mettait en absolute → l'icône partait hors écran. Et à 12px/45% elle restait invisible. Elle lui collait aussi un margin-top:8px qui le poussait sous la ligne du texte (le ✕ n'était pas centré) → on remet les marges verticales à 0 et on force l'auto-centrage flex. 2026-08-05. */ .jsw-chip img[fs-list-element="tag-remove"] { display: block; position: static; align-self: center; vertical-align: middle; width: 10px; height: 10px; filter: brightness(0); opacity: 0.75; box-sizing: content-box; padding: 5px; margin: 0 0 0 2px; border-radius: 300px; cursor: pointer; } .jsw-chip img[fs-list-element="tag-remove"]:hover { opacity: 1; background: rgba(20, 22, 28, 0.08); } /* anti-flash : au chargement, le gabarit brut « Tag Operator Value » est dans le DOM jusqu'à ce que Finsweet le consomme */ .jsw-chips { visibility: hidden; } body.jsw-fs-ready .jsw-chips { visibility: visible; } /* une région dans les pastilles se distingue (elle vient du haut) */ .jsw-chip.jsw-chip-region { background: rgba(0, 36, 116, 0.08); color: #002474; } /* ---------- Menu de tri (dropdown natif Webflow) ---------- */ .jsw-sort-menu { top: calc(100% + 6px); z-index: 20; } .jsw-btn .w-icon-dropdown-toggle { position: static; margin: 0; font-size: 12px; opacity: 0.6; } /* ---------- Sélecteur de vues ---------- */ .jsw-view-btn[data-disabled] { opacity: 0.45; cursor: default; } /* ---------- Bouton Filtres ---------- */ .jsw-filters-count[hidden] { display: none; } /* ---------- Tiroir de filtres (structure NATIVE Webflow) ---------- La coquille, les sections, les segments et le pied sont de vrais éléments avec les classes jsw-drawer*. Ici : seulement les états. */ body.jsw-drawer-locked { overflow: hidden; } /* segment coché (Grandeur, Chambres, Salles de bain) :has(input:checked) — l'input a perdu sa classe .jsw-seg-check, on cible donc l'input générique + is-list-active. 2026-08-05. */ .jsw-seg:has(input:checked), .jsw-seg.is-list-active { background-color: #002474; border-color: #002474; color: #fff; } .jsw-seg:has(input:checked) .jsw-seg-label, .jsw-seg.is-list-active .jsw-seg-label { color: #fff; } .jsw-seg:has(input:checked) .jsw-seg-count, .jsw-seg.is-list-active .jsw-seg-count { color: rgba(255, 255, 255, 0.72); } /* ORDRE label → compteur dans les segments. Le DOM sort le wrapper (jsw-seg-inner, qui porte le compteur) AVANT le span du label → « 5 1 » au lieu de « 1 · 5 ». On force l'ordre visuel : d'abord le label (la valeur), ensuite le compteur. 2026-08-05. */ .jsw-seg { display: inline-flex; align-items: center; gap: 6px; } .jsw-seg .jsw-seg-label { order: 1; } .jsw-seg .jsw-seg-inner, .jsw-seg .jsw-seg-count { order: 2; } /* ---------- Options à 0 dans les segments : ESTOMPÉES, jamais masquées ---------- Décision de Simon (2026-08-05) : une grandeur/chambre/sdb sans logement reste visible mais grisée et non-cliquable (« aucun Studio en ce moment » informe l'usager). Le JS (section C2) pose `jsw-is-empty` d'après le compteur LIVE à chaque render. On estompe aussi la classe native `is-list-emptyfacet` de Finsweet, au cas où une version future la pose. */ .jsw-seg.jsw-is-empty, .jsw-seg.is-list-emptyfacet, .jsw-seg-item:has(.is-list-emptyfacet) { opacity: 0.35; pointer-events: none; } /* ⚠️ jsw-is-dead N'EST PLUS UTILISÉ (retiré le 2026-08-05 avec la logique d'instantané qui masquait à tort chambres/sdb/6½). Aucune règle ne doit remasquer un segment — sinon on reproduit le bogue. */ /* ---------- Avis chambres / salles de bain (posé par le JS) ---------- */ .jsw-seg-note { margin: 8px 0 0; font-size: 12px; line-height: 1.4; color: #8a8d96; } /* commodités : la case reste visible mais grisée (liste courte, on garde le repère visuel plutôt que de faire sauter les lignes) */ .jsw-fcheck.is-list-emptyfacet { opacity: 0.35; pointer-events: none; } /* ---------- Note promo de la carte : 2 lignes MAX + « … » ---------- Demande de Simon (2026-08-05) : les rabais longs (« 250 $/mois pendant 12 mois et 125 $/mois la 2e année… ») ne doivent JAMAIS grossir la carte. Plafond : 2 lignes, ellipse, le détail complet se lit en cliquant la carte (fiche du logement). -webkit-line-clamp refusé par l'API Webflow → vit ici. HOMOGÉNÉITÉ DES AINES (2e demande) : la zone note RÉSERVE toujours 2 lignes (2.7em), pleine, courte ou VIDE — ainsi prix, note, ville, rue·région et faits tombent sur les mêmes lignes sur TOUTES les cartes, promo ou non (mesuré avant fix : ville à 960 vs 981, 21px d'écart). L'espace négatif absorbe, les infos restent alignées. La note vide est en .w-dyn-bind-empty (display:none Webflow) → la règle à 2 classes la réaffiche pour qu'elle réserve sa place. */ .jsw-price-note { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; height: 2.7em; /* exactement 2 lignes (line-height 1.35) */ } /* ⚠️ le display:none de Webflow sur .w-dyn-bind-empty est !important (prouvé : la règle à 2 classes seule perdait) → seul cas légitime de !important du fichier : surcharger une utilité !important de Webflow. */ .jsw-price-note.w-dyn-bind-empty { display: -webkit-box !important; } /* séparateur « rue · région » en pseudo-élément : l'ancien embed HTML s'alignait 15px trop bas en flex baseline (point « orphelin » vu par Simon). Un ::before sur la région est aligné au texte par construction. */ .jsw-card-sub > [fs-list-field="Régions"]::before { content: "· "; } /* ---------- Interrupteur « Logements en promotion » ---------- Habillage de la case native fs-list-field="En Promotion", déplacée par le JS (section C4) juste sous Prix mensuel. On masque la case Webflow d'origine et on dessine un vrai toggle à droite. Le mécanisme Finsweet reste intact. */ .jsw-promo { display: flex; align-items: center; gap: 12px; margin: 14px 0 4px; padding: 12px 14px; border: 1px solid #e4e5e9; border-radius: 12px; cursor: pointer; transition: border-color 0.15s, background-color 0.15s; } .jsw-promo:hover { border-color: #002474; } /* la case native Webflow (carré) est masquée : on ne garde que le toggle */ .jsw-promo .jsw-fcheck-box { display: none; } .jsw-promo-label { flex: 1 1 auto; font-size: 14px; font-weight: 500; color: #14161c; } .jsw-promo-count { font-size: 12.5px; color: #8a8d96; } /* piste + bouton de l'interrupteur */ .jsw-promo-track { flex: none; position: relative; width: 40px; height: 22px; border-radius: 300px; background: #d4d6dc; transition: background-color 0.18s; } .jsw-promo-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(20, 22, 28, 0.25); transition: transform 0.18s; } /* état activé — piloté par la case Finsweet cochée */ .jsw-promo:has(input:checked) { border-color: #002474; background: rgba(0, 36, 116, 0.05); } .jsw-promo:has(input:checked) .jsw-promo-track { background: #002474; } .jsw-promo:has(input:checked) .jsw-promo-knob { transform: translateX(18px); } /* ---------- Chargement de l'inventaire ---------- Finsweet reconstitue les 552 logements en 24 requêtes (~2 à 9 s). Tant que ce n'est pas fini, la barre d'outils est inerte : agir pendant cette fenêtre filtrait un inventaire partiel. */ .jsw-toolbar.is-loading { opacity: 0.45; pointer-events: none; } /* pastilles : le nom du champ est réaffiché (« Chambre », « Salle de bain ») — sans lui, « 1 » ne veut rien dire. Seul l'opérateur reste masqué. */ .jsw-chip [fs-list-element="tag-field"]::after { content: " :"; } /* cases à cocher aux couleurs du site */ .jsw-fcheck-box { accent-color: #002474; } /* ---------- Autocomplete de la barre de recherche ---------- */ .jsw-autocomplete { display: none; background: #fff; border: 1px solid #e4e5e9; border-radius: 12px; box-shadow: 0 12px 32px rgba(20, 22, 28, 0.1); margin-top: 6px; overflow: hidden; } .jsw-autocomplete.is-open { display: block; } .jsw-ac-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 16px; font-size: 14px; color: #14161c; cursor: pointer; } .jsw-ac-item:hover, .jsw-ac-item.is-hl { background: #fafafb; } .jsw-ac-meta { color: #8a8d96; font-size: 12.5px; flex: none; } /* ---------- VUE LISTE : fiches horizontales en COLONNES FIXES (phase 3) ---------- Une seule liste CMS, plusieurs habillages : mêmes éléments que la grille, recomposés quand .jsw-main porte data-jsw-view="list" (JS section F). Principe (demande Simon 2026-08-06, même règle que les aines de la grille) : chaque zone a une LARGEUR FIXE — lieu flex · faits 120 · note 250 · prix 175 — et la note vide reste EN PLACE (colonne réservée) : promo ou non, tout tombe au même x. Hauteur de fiche verrouillée à 125px (sinon le ratio naturel de chaque photo l'imposait — mesuré : 109/125/145). Ellipse sur ville et rue pour qu'aucun libellé long ne fasse plier une fiche. La vue tableau n'existe qu'au-dessus de 1100px : en dessous les colonnes ne rentrent plus (mesuré à 835px : lieu écrasé à 66px) → la vue liste retombe sur les cartes de la grille et le bouton Liste est retiré. */ @media (min-width: 1100px) { .jsw-main[data-jsw-view="list"] .cms_list { display: flex; flex-direction: column; gap: 10px; } /* la classe héritée cms_list-item plafonne à 640px (invisible en grille, bridant en liste) : on libère la pleine largeur */ .jsw-main[data-jsw-view="list"] .cms_list-item { max-width: none; width: 100%; } .jsw-main[data-jsw-view="list"] .jsw-card { flex-direction: row; align-items: stretch; height: 125px; } .jsw-main[data-jsw-view="list"] .jsw-card-media { width: 190px; flex: 0 0 190px; aspect-ratio: auto; height: 100%; } .jsw-main[data-jsw-view="list"] .jsw-card-media img { width: 100%; height: 100%; object-fit: cover; } /* Corps en GRID 3 colonnes × 2 rangées (redesign 2026-08-06, demande Simon : « les rabais ne sont pas à la bonne place ») — même hiérarchie que la carte de grille validée : le RABAIS va SOUS LE PRIX, colonne de droite, au lieu de flotter au milieu. Faits élargis à 190px en nowrap (« 4½ · 3 ch. · 1 sdb » tenait plus dans 120px et pliait). Vérifié par mesures sur 10 fiches mixtes : h=125×10, faits même x, prix même y (33px), avec et sans promo. */ .jsw-main[data-jsw-view="list"] .jsw-card-body { display: grid; grid-template-columns: minmax(0, 1fr) 190px 240px; grid-template-rows: auto auto; align-content: center; column-gap: 24px; row-gap: 3px; padding: 14px 20px; } .jsw-main[data-jsw-view="list"] .jsw-card-place { grid-column: 1; grid-row: 1 / 3; align-self: center; margin-top: 0; margin-bottom: 0; } .jsw-main[data-jsw-view="list"] .jsw-card-city { font-size: 16.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .jsw-main[data-jsw-view="list"] .jsw-card-sub { overflow: hidden; white-space: nowrap; flex-wrap: nowrap; } .jsw-main[data-jsw-view="list"] .jsw-card-facts { grid-column: 2; grid-row: 1 / 3; align-self: center; white-space: nowrap; justify-content: flex-start; margin-top: 0; padding-top: 0; border-top: 0; gap: 14px; } .jsw-main[data-jsw-view="list"] .jsw-card-facts * { white-space: nowrap; } .jsw-main[data-jsw-view="list"] .jsw-card-price { grid-column: 3; grid-row: 1; justify-self: end; align-self: end; margin-top: 0; } .jsw-main[data-jsw-view="list"] .jsw-price-note { grid-column: 3; grid-row: 2; justify-self: end; text-align: right; max-width: 240px; height: 2.7em; /* réserve 2 lignes, note ou pas : le prix ne bouge jamais */ margin-top: 0; } /* la note vide garde sa place (surcharge l'utilité !important de Webflow) */ .jsw-main[data-jsw-view="list"] .jsw-price-note.w-dyn-bind-empty { display: -webkit-box !important; } .jsw-main[data-jsw-view="list"] .jsw-price-main { font-size: 19px; } .jsw-main[data-jsw-view="list"] .jsw-price-amt-promo { font-size: 19px; } } @media (max-width: 1099px) { /* La vue LISTE (tableau) n'existe pas sous 1100px (colonnes trop serrées) — la grille la remplace. La vue CARTE, elle, RESTE dispo sur mobile mais en PLEIN ÉCRAN (patron Airbnb/Centris : pas de split possible sur mobile), pilotée par un BOUTON FLOTTANT — donc on retire Liste ET Carte du sélecteur de vues du haut (le sélecteur ne garderait qu'une option). */ .jsw-view-btn[data-jsw-view="list"], .jsw-view-btn[data-jsw-view="map"] { display: none; } /* le sélecteur ne montre plus que « Grille » → autant le masquer sur mobile, la bascule carte se fait par le bouton flottant */ .jsw-views { display: none; } /* carte plein écran quand la vue carte est active */ .jsw-main[data-jsw-view="map"] .jsw-map { display: block; position: fixed; inset: 0; width: 100vw; height: 100dvh; z-index: 100000; margin: 0; border: 0; border-radius: 0; } /* en carte mobile, la liste est cachée (on bascule vers elle par le bouton) */ .jsw-main[data-jsw-view="map"] .cms_list-wrapper { display: none; } /* cacher la navbar du site quand la carte est en plein écran (demande Simon) : la classe jsw-map-fs est posée sur <body> par le JS à l'entrée en carte mobile. La nav `.navigation` est z-index 999999 (au-dessus de la carte) → seule solution propre : la masquer. Restaurée à la sortie. */ body.jsw-map-fs .navigation { display: none !important; } /* bouton flottant de bascule liste ⇄ carte (créé par le JS, section F quater) */ .jsw-map-toggle { display: inline-flex; align-items: center; gap: 8px; position: fixed; left: 50%; transform: translateX(-50%); bottom: 24px; z-index: 100001; background: #14161C; color: #fff; border: 0; border-radius: 300px; padding: 13px 22px; font-size: 14px; font-weight: 600; font-family: inherit; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28); cursor: pointer; } } /* le bouton flottant n'existe QUE sur mobile */ @media (min-width: 1100px) { .jsw-map-toggle { display: none !important; } } /* ---------- VUE CARTE : split Mapbox + colonne compacte (phase 3) ---------- La carte est un FILTRE (maquette validée l.202-231) : pins pilules par ville, clic = cocher la facette Ville Finsweet. Le conteneur .jsw-map est un élément Webflow (classe Designer, display:none de base) ; tout ce qui dépend de [data-jsw-view] vit ici. Desktop seulement (≥1100). */ @media (min-width: 1100px) { /* Disposition Airbnb/Centris (Simon 2026-08-06) : carte rétrécie à gauche, colonne élargie à droite en DEUX cartes VERTICALES par rangée. Les fiches horizontales étroites « squishaient » l'info (ville sur 2 lignes) → on réutilise l'anatomie de la grille (photo en haut, infos dessous). */ .jsw-main[data-jsw-view="map"] { display: grid; grid-template-columns: minmax(0, 1fr) 500px; column-gap: 16px; align-items: start; } .jsw-main[data-jsw-view="map"] .jsw-toolbar, .jsw-main[data-jsw-view="map"] .jsw-chips, .jsw-main[data-jsw-view="map"] .filter_empty { grid-column: 1 / -1; } /* rangée 3 EXPLICITE pour la paire carte/liste : l'embed vidé (enfant auto-placé de jsw-main) ne peut ainsi pas les désaligner */ .jsw-main[data-jsw-view="map"] .jsw-map { display: block; grid-column: 1; grid-row: 3; position: sticky; top: 16px; } .jsw-main[data-jsw-view="map"] .cms_list-wrapper { grid-column: 2; grid-row: 3; max-height: 640px; overflow-y: auto; padding-right: 4px; } /* colonne = grille de 2 cartes verticales */ .jsw-main[data-jsw-view="map"] .cms_list { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .jsw-main[data-jsw-view="map"] .cms_list-item { max-width: none; width: 100%; } /* fiche VERTICALE, comme la grille : photo pleine largeur en tête, corps dessous. On annule tout ce qui restait de l'ancienne fiche horizontale. */ .jsw-main[data-jsw-view="map"] .jsw-card { flex-direction: column; align-items: stretch; height: 100%; } .jsw-main[data-jsw-view="map"] .jsw-card-media { width: 100%; flex: 0 0 auto; aspect-ratio: 4 / 2.8; height: auto; } .jsw-main[data-jsw-view="map"] .jsw-card-media img { width: 100%; height: 100%; object-fit: cover; } .jsw-main[data-jsw-view="map"] .jsw-card-body { padding: 12px 13px; flex-direction: column; align-items: flex-start; gap: 0; } /* la ville tient sur une ligne (fini le passage sur 2 lignes signalé par Simon) */ .jsw-main[data-jsw-view="map"] .jsw-card-city { font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } .jsw-main[data-jsw-view="map"] .jsw-card-facts { margin-top: 8px; padding-top: 8px; gap: 10px; font-size: 12px; } /* la note promo (longue) reste masquée ; le badge Promotion revient (la fiche verticale a la place, comme la grille) */ .jsw-main[data-jsw-view="map"] .jsw-price-note { display: none !important; } .jsw-main[data-jsw-view="map"] .jsw-price-main { font-size: 18px; } .jsw-main[data-jsw-view="map"] .jsw-price-amt-promo { font-size: 18px; } .jsw-main[data-jsw-view="map"] .jsw-card-place { margin-top: 6px; } /* masquer la pagination native en vue carte (le scroll infini prend le relais) */ .jsw-main[data-jsw-view="map"] .w-pagination-wrapper { display: none; } } /* Les pastilles/clusters de la carte sont dessinés par Mapbox en couches natives (circle + symbol, voir JS section F bis) — pas de marker HTML. On ne stylise ici que le conteneur des « cooperative gestures » de Mapbox pour rester cohérent avec la police du site. */ .jsw-map .mapboxgl-cooperative-gesture-screen { font-family: inherit; } /* ---------- Tiroir en plein écran sur mobile ---------- Demande de Simon (2026-08-05) : sur mobile le tiroir doit prendre TOUTE la largeur et passer PAR-DESSUS la nav bar (qui le recouvrait). Cause mesurée : la nav du site `.navigation` est à `z-index:999999` alors que le tiroir n'était qu'à 1001 → la nav gagnait. On remonte le tiroir + le backdrop au-dessus, on force la pleine largeur, et on gère nous-mêmes la glissière (fermé = hors écran à droite, ouvert = translateX 0) pour ne dépendre d'aucune combo transform du Designer (qui est calée sur la largeur bureau). Bureau/tablette inchangés (tiroir latéral à droite au-dessus de 767px). */ @media screen and (max-width: 767px) { .jsw-drawer { width: 100vw; max-width: none; left: 0; right: 0; z-index: 1000001; /* > .navigation (999999) */ transform: translateX(100%); /* fermé : entièrement hors écran à droite */ } .jsw-drawer.jsw-open { transform: translateX(0); /* ouvert : plein écran */ } .jsw-backdrop { z-index: 1000000; /* juste sous le tiroir, au-dessus de la nav */ } } /* ---------- HERO PREMIUM (Variante C — panneau latéral) ---------- Le hero de /proprietes-v2 a été DÉTACHÉ de son composant (2026-08-06) et restylé par API avec des classes neuves jsw-hero* (dégradé latéral gauche, alignement gauche, eyebrow filet). L'ancienne pilule « Tous nos logements » ET le lien de l'eyebrow ont été retirés par SIMON dans le Designer (l'eyebrow est maintenant du texte pur, plus un <a>) → aucune neutralisation de lien nécessaire. Ce bloc ne garde donc que les correctifs responsive (le titre 72px débordait sur tablette/mobile — mesuré). */ /* le titre garde sa taille desktop mais respire un peu moins large en panneau */ .jsw-hero-title { max-width: 12ch; } /* ---- Tablette : réduire le titre pour qu'il tienne dans le panneau ---- */ @media (max-width: 991px) { .jsw-hero-title { font-size: 56px; max-width: none; } } /* ---- Mobile : panneau pleine largeur, dégradé vertical, titre fluide ---- Cause du débordement (mesurée) : titre 72px sur ~295px de panneau + padding 56px hérité. On réduit le padding, on élargit le panneau, et le titre passe en clamp() pour ne JAMAIS déborder quelle que soit la largeur. */ @media (max-width: 767px) { .jsw-hero-card { background-image: linear-gradient(180deg, rgba(8,11,20,.32) 0%, rgba(8,11,20,.78) 100%), url("https://cdn.prod.website-files.com/68dc4240b81b16f1ec5583cc/694a01c5882bb55e9a214c8f_Image-1.avif"); padding-left: 24px; padding-right: 24px; } .jsw-hero-panel { max-width: none; width: 100%; } .jsw-hero-title { max-width: none; font-size: clamp(34px, 11vw, 46px); line-height: 1.06; letter-spacing: -0.02em; } .jsw-hero-sub { font-size: 15px; } } /* ============================================================ SKELETON LOADER (2026-08-06) ------------------------------------------------------------ Pendant que Finsweet reconstitue l'inventaire (2 à 9 s au 1er accès), on montre des fantômes qui shimmerent — PAS d'écran vide ni le « 24 » trompeur. Règle : cache chaud → apparition directe, AUCUN skeleton (piloté côté JS par une course « prêt < 300 ms »). Ici, uniquement l'habillage. Zones (révélées ENSEMBLE via body.jsw-skeleton-on) : • grille : 8 jsw-skel-card (média + 3 barres) • pastilles région : 4 jsw-skel-pill • compteur : 1 jsw-skel-count (barre courte) Les fantômes sont fabriqués en JS (zéro élément Webflow) et insérés dans jsw-main ; ce CSS leur donne forme + mouvement. ============================================================ */ /* --- Conteneurs skeleton : cachés par défaut, montrés seulement quand le JS pose body.jsw-skeleton-on --- */ .jsw-skel-grid, .jsw-skel-pills, .jsw-skel-count { display: none; } body.jsw-skeleton-on .jsw-skel-grid { display: grid; } /* grille calquée en JS */ body.jsw-skeleton-on .jsw-skel-pills { display: flex; flex-wrap: wrap; gap: 8px; } body.jsw-skeleton-on .jsw-skel-count { display: block; } /* Quand le skeleton est actif, on masque les vraies zones dynamiques. (le JS pose data-jsw-skel-hide="1" sur chaque vraie zone jumelée) */ body.jsw-skeleton-on [data-jsw-skel-hide="1"] { display: none !important; } /* --- Le bloc shimmer : brique de base de tous les fantômes --- */ .jsw-skel { position: relative; overflow: hidden; background: #ECEEF1; border-radius: 8px; } .jsw-skel::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient( 90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.65) 50%, rgba(255,255,255,0) 100% ); animation: jsw-shimmer 1.4s ease-in-out infinite; } @keyframes jsw-shimmer { 100% { transform: translateX(100%); } } /* --- Carte fantôme : épouse la vraie carte (mesurée 288×399) --- */ .jsw-skel-card { display: flex; flex-direction: column; gap: 12px; } .jsw-skel-card .jsw-skel-media { width: 100%; aspect-ratio: 4 / 2.9; /* même ratio que la photo réelle */ border-radius: 12px; /* jeton Cards du site */ } .jsw-skel-card .jsw-skel-line { height: 13px; border-radius: 6px; } .jsw-skel-card .jsw-skel-price { height: 20px; width: 42%; border-radius: 6px; } /* prix dominant */ .jsw-skel-card .jsw-skel-city { width: 68%; } .jsw-skel-card .jsw-skel-facts { width: 84%; margin-top: 4px; } /* --- Pastille région fantôme : mesurée 153×37 --- */ .jsw-skel-pill { width: 148px; height: 37px; border-radius: 300px; /* pilule, comme les vraies */ } /* --- Barre du compteur fantôme : remplace « Résultats 551 » --- */ .jsw-skel-count { width: 128px; height: 15px; border-radius: 6px; } /* --- Révélation douce : les vraies zones fondent en entrant --- */ .cms_list-wrapper, .jsw-regions-inner, .jsw-results-text { transition: opacity .25s ease; } /* --- Accessibilité : pas d'animation si l'utilisateur la refuse --- */ @media (prefers-reduced-motion: reduce) { .jsw-skel::after { animation: none; display: none; } .jsw-skel { background: #E9EBEE; } } /* --- Annonce lecteur d'écran, visuellement masquée --- */ .jsw-skel-status { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }