SPB Git

Repository Public

b04f0e8896bb41e1dffc.html 319.1 KB 4,183 lines html

1<!DOCTYPE html>2<html lang="fr">3<head>4<meta charset="UTF-8">5<meta name="google-site-verification" content="HY1yJR5rBElkmygv-FB1kKLAlnrIrcYJaCgXkpznCJM" />6<meta http-equiv="Content-Security-Policy" content="7  default-src 'self';8  script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;9  style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;10  img-src 'self' data: blob: https:;11  font-src 'self' data:;12  connect-src 'self' https://jfgkriojazaohgjezijj.supabase.co wss://jfgkriojazaohgjezijj.supabase.co https://cdn.jsdelivr.net https://nominatim.openstreetmap.org https://*.tile.openstreetmap.org;13  frame-src 'self';14  worker-src 'self' blob:;15  object-src 'none';16  base-uri 'self';17">18<meta name="viewport" content="width=device-width, initial-scale=1.0">19<title>Logements disponibles — Gestion ISR</title>20<meta name="description" content="Découvrez nos logements disponibles dans le Centre-du-Québec avec Gestion ISR.">21<!-- URL canonique : évite les doublons d'indexation (variantes de paramètres + domaine Vercel). -->22<link rel="canonical" id="link-canonical" href="https://location.gestion-isr.com/">23<meta property="og:type" content="website">24<meta property="og:title" content="Logements disponibles — Gestion ISR">25<meta property="og:description" content="Découvrez nos logements disponibles dans le Centre-du-Québec avec Gestion ISR.">26<meta property="og:url" content="https://location.gestion-isr.com/">27<meta property="og:image" content="https://location.gestion-isr.com/og-image.png?v=20">28<meta property="og:image:secure_url" content="https://location.gestion-isr.com/og-image.png?v=20">29<meta property="og:image:type" content="image/png">30<meta property="og:image:width" content="1200">31<meta property="og:image:height" content="630">32<meta name="twitter:card" content="summary_large_image">33<meta name="twitter:image" content="https://location.gestion-isr.com/og-image.png?v=20">34<meta name="theme-color" content="#FB8C00">35<link rel="manifest" href="/site.webmanifest">36<meta name="mobile-web-app-capable" content="yes">37<meta name="apple-mobile-web-app-capable" content="yes">38<meta name="apple-mobile-web-app-title" content="Gestion ISR">39<link rel="apple-touch-icon" href="/icon-isr-192.png">40<link rel="icon" type="image/png" href="data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAwADADASIAAhEBAxEB/8QAGwAAAwADAQEAAAAAAAAAAAAAAAUGAwQHAgj/xAAwEAACAQQBAgUCAwkAAAAAAAABAgMABAURBhIhBzFBUWETIghSgRQWNnFydaGxs//EABkBAAIDAQAAAAAAAAAAAAAAAAACAQMFBv/EACIRAAICAgEEAwEAAAAAAAAAAAECAwQAESESEzFBIjJx8P/aAAwDAQACEQMRAD8A+eqKKKMMKKKpOCcLzXMbyWDGIiRQruW4mJEaH0XYB7n2H86rllSJC7nQGPHG0jBUGycm6K3M1jL7DZOfG5K3e3uoG6XRv9j3B8wfWtOnVgw2PGKQQdHCqDw7wUPJeZ47C3MzxQ3Dt9RkH3dKozkD5PTrfpukSRSupdI3ZV8yFJAqh8Psq3GeY2OauLG5nS2+oWjRdMQ0bL6/LVRZZuy/bPy0dfuuMtgC9xevxsb/ADO5+Ifhnx7KYfHx2UKYyW1lht0khTe4mkClWHqfu3s9979zSSTLngHilieJ4eAHDXsNvA8DN3EjyMv1t/m8t+4GvQa8ZfxpsbiyCR8dyKuJYpV62UBuiRW1v51r9a5Zecpyt9z215TlYmmngu4ZhCAVAVGDLGvsO3z7965qjStyIUs8qAdAn2da/vWbtu1WRw8H22OQPQyt/E3/AB7Zf2uP/rLXLKc8pzeX5VmpstkS8srjSqinpiQE6VR6AbP+T5mlLxSooZ43VT5EqQDXQ0YjXrpEx5AzFtyCaZpFHBONcVnpbDGtYi1gmUtKymRd6Lqq7/TpBGtVu/vbcG6eZrO3ZZNdcTIhVtEkE7XufuI359/Opqih6FdyWZNk5C25lAAbxjux5JeWhj6URhHbG3ALN2UzfV2O/Y77b9qz2vLLq1S2hjtopYLdkZVnJcsUZ2Uk9u4LnRGuw15b3O0UPQrvvqTzgtuZdabHmJ5HPj7VLX9lgniUDaSKCrfcx77B/MRsaNYstnbnI2EdnJDDGiMrEoNF+leler30N6/qPxSiimFKASdwL8vOQbMpTo3xn//Z">41<style>42*{box-sizing:border-box;margin:0;padding:0}43html,body{overflow-x:hidden;max-width:100%}44body{font-family:system-ui,sans-serif;background:#f5f5f3;color:#1a1a1a;min-height:100vh;width:100%}45.header{background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}46.logo-row{display:flex;align-items:center;gap:10px}47.logo-circle{width:36px;height:36px;border-radius:50%;background:#FB8C00;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:12px}48.brand{font-size:18px;font-weight:600}49.hero{background:#fff;border-bottom:1px solid #e8e8e5;padding:1.5rem 2rem}50.count-badge{display:inline-block;background:#FFECCF;color:#E67700;font-size:13px;font-weight:500;padding:4px 10px;border-radius:20px;margin-bottom:0.75rem}51.hero h1{font-size:24px;font-weight:600;margin-bottom:4px}52.hero p{font-size:14px;color:#666}53.filters-wrap{background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem;display:flex;gap:1rem;flex-wrap:wrap;align-items:flex-end}54.filter-group{display:flex;flex-direction:column;gap:4px;min-width:130px}55.filter-group label{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.04em}56.filter-group select{font-size:13px;padding:7px 10px;border:1px solid #ddd;border-radius:8px;background:#fff;color:#1a1a1a;cursor:pointer}57#browse-controls .filters-wrap,#browse-controls .tags-row{display:none!important}58.price-range{display:flex;flex-direction:column;gap:4px}59.price-range label{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.04em}60.price-inputs{display:flex;align-items:center;gap:6px}61.price-inputs input{width:80px;font-size:13px;padding:7px 8px;border:1px solid #ddd;border-radius:8px;color:#1a1a1a}62.price-inputs span{font-size:13px;color:#888}63.btn-reset{font-size:13px;padding:7px 14px;border:1px solid #ddd;border-radius:8px;background:#fff;color:#666;cursor:pointer;align-self:flex-end}64.tags-row{padding:0.6rem 2rem;display:flex;gap:6px;flex-wrap:wrap;min-height:40px;align-items:center;background:#f5f5f3;border-bottom:1px solid #e8e8e5}65.tag{display:flex;align-items:center;gap:5px;background:#fff;border:1px solid #ddd;border-radius:20px;padding:3px 10px;font-size:12px;color:#444}66.tag button{background:none;border:none;color:#888;cursor:pointer;font-size:14px;line-height:1;padding:0}67.tags-empty{font-size:13px;color:#aaa}68.quick-filters{display:flex;gap:8px;flex-wrap:wrap;align-items:center}69.quick-filter-btn{font-size:12px;font-weight:600;padding:5px 12px;border-radius:20px;border:1.5px solid #ddd;background:#fff;color:#555;cursor:pointer;transition:all 0.15s;white-space:nowrap}70.quick-filter-btn:hover{border-color:#FB8C00;color:#FB8C00}71.quick-filter-btn.active{background:#FB8C00;color:#fff;border-color:#FB8C00}72.main{padding:1.5rem 2rem 3rem}73.results-line{font-size:13px;color:#888;margin-bottom:1rem}74/* ⭐ À la une — overlay premium (photo plein cadre, texte dessus) */75.feat{margin:1.25rem 0}76.feat-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin:0 2px 12px;flex-wrap:wrap}77.feat-head .ttl{font-size:19px;font-weight:800;color:#1f2420;display:flex;align-items:center;gap:8px;letter-spacing:-.01em}78.feat-head .sub{font-size:12.5px;color:#b08a4e;font-weight:700}79.feat-row{display:flex;align-items:center;gap:10px}80.feat-track{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;flex:1;padding:4px 2px 8px;scrollbar-width:none;-ms-overflow-style:none}81.feat-track::-webkit-scrollbar{display:none}82.feat-card{flex:0 0 202px;scroll-snap-align:start;position:relative;height:172px;border-radius:15px;overflow:hidden;cursor:pointer;box-shadow:0 6px 18px rgba(0,0,0,.14);transition:transform .2s ease,box-shadow .2s ease}83.feat-card:hover{transform:translateY(-3px);box-shadow:0 16px 32px -6px rgba(0,0,0,.26)}84.feat-photo{position:absolute;inset:0;background:#e9e4dc no-repeat center/cover;display:flex;align-items:center;justify-content:center;color:#c3bcb2;font-size:34px;transition:transform .4s ease}85.feat-card:hover .feat-photo{transform:scale(1.05)}86.feat-veil{position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.82),rgba(0,0,0,.1) 58%,rgba(0,0,0,0) 85%)}87.feat-badge{position:absolute;left:10px;top:10px;z-index:2;background:#FB8C00;color:#fff;font-size:8.5px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;padding:3px 9px;border-radius:999px;box-shadow:0 3px 10px rgba(0,0,0,.28)}88.feat-pcount{position:absolute;right:10px;top:10px;z-index:2;background:rgba(0,0,0,.45);color:#fff;font-size:9px;font-weight:600;padding:2px 8px;border-radius:999px;display:flex;align-items:center;gap:4px;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}89.feat-txt{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:11px 13px;color:#fff;display:flex;align-items:flex-end;justify-content:space-between;gap:10px}90.feat-info{min-width:0}91.feat-ttl{font-size:12px;font-weight:700;line-height:1.2;text-shadow:0 1px 3px rgba(0,0,0,.45)}92.feat-ville{font-size:9px;opacity:.9;text-transform:uppercase;letter-spacing:.04em;margin-top:1px}93.feat-price{font-size:14px;font-weight:800;text-shadow:0 1px 3px rgba(0,0,0,.45);flex:none;text-align:right;white-space:nowrap}94.feat-price small{font-size:9px;font-weight:600;opacity:.85}95.feat-arrow{width:38px;height:38px;border-radius:50%;border:1px solid #ecd9bd;background:#fff;color:#8a5a00;cursor:pointer;font-size:19px;line-height:1;flex:none;box-shadow:0 3px 10px rgba(0,0,0,.1)}96.feat-arrow:hover{border-color:#FB8C00;color:#FB8C00;background:#fff7ef}97@media(max-width:620px){.feat-card{flex:0 0 72%;height:168px}.feat-head .ttl{font-size:17px}.feat-arrow{display:none}}98.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}99.loading{grid-column:1/-1;text-align:center;padding:4rem;color:#888}100.loading-spinner{width:32px;height:32px;border:3px solid #e8e8e5;border-top-color:#FB8C00;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 1rem}101@keyframes spin{to{transform:rotate(360deg)}}102.card{background:#fff;border:1px solid #e8e8e5;border-radius:14px;overflow:hidden;transition:border-color 0.15s,transform 0.1s;cursor:pointer}103.card:hover{border-color:#FB8C00;transform:translateY(-2px)}104.card-promotion{border:3px solid #F97316 !important;box-shadow:0 0 0 5px rgba(249,115,22,0.25)}105.card-psl{border:3px solid #8B5CF6 !important;box-shadow:0 0 0 5px rgba(139,92,246,0.25)}106.card-derniere{border:3px solid #EF4444 !important;box-shadow:0 0 0 5px rgba(239,68,68,0.25)}107.card-coupdecoeur{border:3px solid #EF4444 !important;box-shadow:0 0 0 5px rgba(239,68,68,0.25)}108.card-img{height:180px;background:#f0f0ee;position:relative;overflow:hidden}109.card-img img{width:100%;height:100%;object-fit:cover}110.card-img-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#ccc}111.badge-row{position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:flex-end;align-items:flex-start;gap:6px}112.badge{font-size:11px;font-weight:600;padding:4px 9px;border-radius:12px;white-space:nowrap}113.badge-dispo{background:#FB8C00;color:#fff}114.badge-animal{background:#fff;color:#333;border:1px solid #ddd}115.card-heart{position:absolute;bottom:10px;left:10px;z-index:11;width:32px;height:32px;border-radius:50%;border:none;background:rgba(255,255,255,0.92);color:#bbb;font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;box-shadow:0 1px 4px rgba(0,0,0,0.2)}116.card-heart.active{color:#EF4444}117.card-heart:hover{background:#fff;color:#EF4444}118/* Bouton favori dans la fiche ouverte (détail) */119.d-fav{display:inline-flex;align-items:center;gap:7px;font-size:13.5px;font-weight:700;padding:9px 15px;border-radius:999px;border:1.5px solid #e4d8c4;background:#fff;color:#555;cursor:pointer;transition:all .12s;margin-bottom:8px;white-space:nowrap}120.d-fav .hc{font-size:16px;color:#c9c9c4;line-height:1}121.d-fav:hover{border-color:#EF4444;color:#EF4444}122.d-fav:hover .hc,.d-fav.active .hc{color:#EF4444}123.d-fav.active{border-color:#EF4444;background:#FDECEC;color:#EF4444}124.d-fav::after{content:"Ajouter aux favoris"}125.d-fav.active::after{content:"\2713 Dans vos favoris"}126.badge-etiquette{background:#F59E0B;color:#fff}127.card-body{padding:1rem 1.1rem 1.1rem}128.card-ville{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:4px}129.card-title{font-size:15px;font-weight:600;margin-bottom:10px;line-height:1.3}130.card-tags{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:12px}131.card-tag{font-size:11px;padding:3px 8px;border-radius:6px;font-weight:500}132.ct-size{background:#E6F1FB;color:#0C447C}133.ct-date{background:#FAEEDA;color:#633806}134.ct-etage{background:#EAF3DE;color:#27500A}135.ct-variantes{background:#EEEDFE;color:#3C3489;font-weight:600}136.ct-incl{background:#F1EFE8;color:#444441}137.price-from{font-size:.62em;font-weight:600;color:#777;vertical-align:middle}138/* Modèles de photos (variantes) */139.card-models-btn{display:inline-flex;align-items:center;gap:6px;margin:2px 0 10px;padding:6px 12px;border:1px solid #d9dde2;border-radius:9px;background:#fff;color:#3C3489;font-size:12.5px;font-weight:600;cursor:pointer;transition:.12s}140.card-models-btn:hover{background:#EEEDFE;border-color:#3C3489}141#d-photo-models{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px}142.d-models-box{border:1.5px solid #efbd7c;background:linear-gradient(180deg,#fff4e2,#fce6c6);border-radius:14px;padding:12px 16px;margin:2px 0 6px;box-shadow:0 2px 10px rgba(200,120,10,.1)}143.d-models-head{font-size:14px;font-weight:800;color:#b45309;margin-bottom:9px}144.d-models-head span{font-size:12.5px;font-weight:500;color:#a9743a}145.pv-model-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 14px;border:1.5px solid #edbf85;border-radius:10px;background:#fff;color:#7a4a12;font-size:14px;font-weight:700;cursor:pointer;transition:.12s;box-shadow:0 1px 3px rgba(200,120,10,.14);text-align:center}146.pv-model-btn:hover{background:#e5850f;color:#fff;border-color:#e5850f;transform:translateY(-1px)}147.pv-model-btn:hover span{color:#ffe8cf}148.pv-model-btn span{font-size:12px;font-weight:500;color:#b1936a}149.pv-model-btn.pv-model-cur{background:#e5850f;color:#fff;border-color:#e5850f;cursor:default;box-shadow:none}150.pv-model-btn.pv-model-cur span{color:#ffe8cf}151.pv-model-btn:disabled{opacity:1}152.lb-models{position:absolute;top:14px;left:50%;transform:translateX(-50%);display:none;gap:8px;flex-wrap:wrap;justify-content:center;z-index:5;max-width:92vw}153.lb-models.show{display:flex}154.lb-mbtn{padding:7px 15px;border-radius:20px;border:1px solid rgba(255,255,255,.45);background:rgba(0,0,0,.5);color:#fff;font-size:13px;font-weight:600;cursor:pointer}155.lb-mbtn:hover{background:rgba(0,0,0,.7)}156.lb-mbtn.active{background:#fff;color:#111;border-color:#fff}157/* PHASE 2 — Immeuble & unités (vue agent) */158.d-bld-meta{font-size:12.5px;color:#555;margin:0 0 10px}159.d-bld{border:2px solid #cfd6de;border-radius:12px;overflow:hidden;background:#fbfcfd;width:fit-content;max-width:100%;margin:0 auto}160.d-bld-floor{display:flex;align-items:stretch;border-bottom:1px solid #e7ebef}161.d-bld-floor:last-child{border-bottom:none}162.d-floor-lbl{flex:0 0 72px;background:#f1f5f9;font-size:9.5px;font-weight:700;color:#5b6b7c;display:flex;align-items:center;padding:6px 8px;text-transform:uppercase;letter-spacing:.01em}163.d-units{flex:1;display:flex;flex-wrap:wrap;gap:7px;padding:8px;justify-content:center}164.d-unit{flex:0 0 auto;width:104px;border:1.5px solid #ddd;border-radius:8px;overflow:hidden;cursor:pointer;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.05);transition:transform .12s,box-shadow .12s}165.d-unit:hover{transform:translateY(-2px);box-shadow:0 5px 13px rgba(0,0,0,.14)}166.d-unit-bar{display:flex;justify-content:space-between;align-items:center;gap:4px;padding:2px 7px;font-size:9px;font-weight:700}167.d-unit-body{padding:5px 7px 7px}168.d-unit-type{font-size:13px;font-weight:700;color:#1a1a1a}169.d-unit-etage{font-size:10px;color:#7a828b;margin-top:1px}170.d-unit-price{font-size:12.5px;font-weight:800;color:#E67700;margin-top:3px}171/* vue compacte : petites tuiles numérotées (comme un plan) */172.d-units.compact{gap:3px;padding:5px;align-items:stretch}173.d-units.compact.d-hasgroups{gap:8px;align-items:flex-start}174.d-posgroup{display:flex;flex-direction:column;gap:2px;border:1px dashed #cbd3da;border-radius:6px;padding:3px}175.d-poslbl{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.02em;color:#7a828b;text-align:center}176.d-postiles{display:flex;flex-wrap:wrap;gap:3px}177.d-unit.mini{width:46px;border-width:1px;border-radius:5px;padding:3px 2px;box-shadow:none;color:#1a2230;text-align:center;overflow:hidden;display:flex;flex-direction:column;justify-content:center;min-height:34px}178.d-unit.mini .mn{font-size:8.5px;font-weight:800;line-height:1.05}179.d-unit.mini .mm{font-size:6.5px;font-weight:600;color:#46525f;line-height:1.15;margin-top:1px;white-space:nowrap}180.d-unit.mini:hover{transform:translateY(-1px);box-shadow:0 2px 6px rgba(0,0,0,.16)}181.d-unit.d-unit-locked{cursor:default;opacity:.72}182.d-unit.d-unit-locked:hover{transform:none;box-shadow:none}183.d-unit.mini.d-unit-cur{outline:2px solid #E67700;outline-offset:1px;box-shadow:0 0 0 3px rgba(230,119,0,.18)}184/* ===== Tour d'habitation : plan compact « fenêtres » dans un contour d'immeuble ===== */185/* 5 styles de bâtiment (A–E) via variables CSS */186.sk-A{--wall:#7b8a99;--body1:#fbfcfd;--body2:#e9eef3;--sep:#cdd6df;--lbl:#46525f}187.sk-B{--wall:#9e5b4a;--body1:#fbf3f0;--body2:#f1ddd4;--sep:#e8cfc5;--lbl:#7a4336}188.sk-C{--wall:#3f7d8c;--body1:#eef8fb;--body2:#d8ebf1;--sep:#c6e0e8;--lbl:#2f6676}189.sk-D{--wall:#b09468;--body1:#fdfaf3;--body2:#f0e6d2;--sep:#e7dabf;--lbl:#7a6239}190.sk-E{--wall:#2d3138;--body1:#30353c;--body2:#23272d;--sep:rgba(255,255,255,.12);--lbl:#cbd0d8}191.d-bld-tour{border:none;background:none;overflow:visible;width:100%;max-width:560px;margin:0 auto}192.d-tour-roof{width:72%;max-width:340px;height:0;margin:0 auto;border-left:16px solid transparent;border-right:16px solid transparent;border-bottom:18px solid var(--wall,#7b8a99)}193.d-bld-tour.sk-C .d-tour-roof,.d-bld-tour.sk-E .d-tour-roof{height:9px;border:none;background:var(--wall,#7b8a99);border-radius:5px 5px 0 0;width:86%;max-width:430px}194.d-bld-tour.sk-D .d-tour-roof{border-left-width:34px;border-right-width:34px;border-bottom-width:26px;width:54%}195.d-tour-body{border:4px solid var(--wall,#7b8a99);border-radius:4px 4px 10px 10px;background:linear-gradient(var(--body1,#fbfcfd),var(--body2,#e9eef3));overflow:hidden}196.d-floor-tour{display:flex;align-items:center;gap:8px;padding:6px 8px;border-bottom:1px dashed var(--sep,#cdd6df)}197.d-floor-tour:last-child{border-bottom:none}198.d-floor-tour::after{content:'';flex:0 0 64px}   /* espaceur symétrique au libellé → fenêtres centrées sous le toit */199.d-flbl-tour{flex:0 0 64px;text-align:right;font-size:9.5px;font-weight:700;color:var(--lbl,#5b6b7c);text-transform:uppercase;letter-spacing:.01em}200.d-grains{flex:1;display:flex;flex-wrap:wrap;justify-content:center;gap:5px;align-content:center}201.d-grain{width:16px;height:21px;flex:0 0 auto;border-radius:2px;border:1.5px solid #ccc;background:#eee;box-sizing:border-box;cursor:pointer;transition:transform .1s,box-shadow .1s}202.d-grain:hover{transform:scale(1.4);box-shadow:0 2px 7px rgba(0,0,0,.25);position:relative;z-index:2}203.d-grain.d-unit-locked{cursor:default;opacity:.7}204.d-grain.d-unit-locked:hover{transform:none;box-shadow:none}205/* Noir chic : fenêtres éclairées façon soir (halo chaud) */206.d-bld-tour.sk-E .d-grain{box-shadow:0 0 6px 1px rgba(255,205,120,.55);filter:brightness(1.12) saturate(1.1)}207.d-bld-tour.sk-E .d-grain:hover{box-shadow:0 0 10px 2px rgba(255,205,120,.85)}208/* Infobulle au survol d'une fenêtre (prix / dispo / type / modèle) */209.d-grain-tip{position:fixed;z-index:4000;pointer-events:none;background:#1f2937;color:#fff;border-radius:9px;padding:8px 11px;font-size:12px;line-height:1.55;box-shadow:0 6px 24px rgba(0,0,0,.3);max-width:250px;opacity:0;transition:opacity .1s;display:none}210.d-grain-tip.show{opacity:1;display:block}211.d-grain-tip .gt-row{display:flex;gap:12px;justify-content:space-between;white-space:nowrap}212.d-grain-tip .gt-k{color:#9fb0c6;font-weight:600}213.d-grain-tip .gt-v{font-weight:700;text-align:right}214.d-grain-tip .gt-head{font-weight:800;font-size:13px;margin-bottom:5px;padding-bottom:5px;border-bottom:1px solid rgba(255,255,255,.18)}215/* Tour : modèles mis en avant + boutons Extérieur / Commodités */216.tbld-models{margin:4px 0 12px}217.tbld-x-lbl{font-size:12.5px;font-weight:700;color:#3a4653;margin:0 0 7px}218.tbld-x-lbl span{font-weight:400;color:#8a929b}219.tbld-models-row{display:flex;gap:10px;overflow-x:auto;padding-bottom:4px;-webkit-overflow-scrolling:touch}220.tbld-model{flex:0 0 auto;width:140px;height:92px;border-radius:12px;border:1px solid #e4dccf;background-size:cover;background-position:center;position:relative;cursor:pointer;overflow:hidden;padding:0;box-shadow:0 2px 8px rgba(0,0,0,.08);transition:transform .12s,box-shadow .12s}221.tbld-model:hover{transform:translateY(-2px);box-shadow:0 6px 16px rgba(0,0,0,.16)}222.tbld-model-cap{position:absolute;left:0;right:0;bottom:0;padding:16px 8px 6px;background:linear-gradient(transparent,rgba(0,0,0,.72));color:#fff;display:flex;flex-direction:column;align-items:flex-start;text-align:left;font-size:12px;line-height:1.25}223.tbld-model-cap b{font-size:13px}224.tbld-model-cap span{font-size:10.5px;opacity:.9}225.tbld-x-row{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}226.tbld-x-btn{display:inline-flex;align-items:center;gap:7px;background:#fff;border:1.5px solid #cfd6de;border-radius:10px;padding:9px 14px;font-size:13.5px;font-weight:700;color:#3a4653;cursor:pointer;font-family:inherit;transition:border-color .12s,background .12s}227.tbld-x-btn:hover{border-color:#FB8C00;background:#fff7ef}228.tbld-x-btn span{background:#eef2f6;color:#5b6b7c;border-radius:20px;padding:1px 8px;font-size:12px}229.d-legend-tour{display:flex;flex-wrap:wrap;gap:6px 14px;margin:2px 0 12px;font-size:11.5px;color:#46525f}230.d-legend-tour .d-leg{display:inline-flex;align-items:center;gap:5px}231.d-legend-tour .d-leg-dot{width:12px;height:13px;border-radius:2px;border:1.5px solid #ccc;display:inline-block;box-sizing:border-box}232.bld-full-body .d-bld-tour{width:100%;max-width:560px;margin:0 auto}233@media(max-width:640px){ .d-floor-tour::after{flex-basis:50px} .d-flbl-tour{flex:0 0 50px;font-size:9px} .d-grains{gap:4px} .d-grain{width:14px;height:19px} }234/* Bouton + plan plein écran (grands immeubles) */235.d-bld-expand{display:block;width:100%;margin:0 0 10px;padding:11px;border:1.5px solid #cfd6de;border-radius:10px;background:#f4f7fa;color:#3a4653;font-size:13.5px;font-weight:700;cursor:pointer}236.d-bld-expand:hover{background:#e9eef3;border-color:#9fb0c0}237@media (min-width:769px){ .d-bld-expand{display:none !important} }  /* plan plein écran : utile seulement sur mobile */238.bld-full-overlay{display:none;position:fixed;inset:0;z-index:180;background:#fff;flex-direction:column}239.bld-full-overlay.open{display:flex}240.bld-full-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 16px;border-bottom:1px solid #e8e8e5;font-weight:700;font-size:15px;flex:0 0 auto}241.bld-full-head button{background:none;border:none;font-size:28px;line-height:1;cursor:pointer;color:#888}242.bld-zoom-ctl{display:flex;align-items:center;gap:4px;flex:0 0 auto}243.bld-zoom-ctl button{width:38px;height:38px;border:1px solid #e2e2df;border-radius:10px;font-size:20px;font-weight:700;color:#444;background:#fff;display:flex;align-items:center;justify-content:center;padding:0}244.bld-zoom-ctl button:active{background:#eef2f4}245.bld-zoom-ctl .bld-zoom-close{border:none;font-size:28px;color:#888;width:36px}246.bld-full-body{flex:1;overflow:auto;padding:14px;-webkit-overflow-scrolling:touch;cursor:grab;background:#f5f7f9;overscroll-behavior:contain}247.bld-full-body.grabbing{cursor:grabbing}248.bld-full-body .d-bld{width:max-content;min-width:100%;margin:0}249.bld-full-body .d-units.compact{gap:8px;padding:10px;justify-content:flex-start}250.bld-full-body .d-floor-lbl{flex:0 0 92px;font-size:11px}251.bld-full-body .d-unit.mini{width:130px;padding:9px 8px 10px}252.bld-full-body .d-unit.mini .mn{font-size:15px}253.bld-full-body .d-unit.mini .mm{font-size:12.5px}254/* barre d'outils du visuel */255.d-bld-toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 10px}256.d-seg{display:inline-flex;border:1px solid #d5dbe2;border-radius:8px;overflow:hidden}257.d-seg button{border:none;background:#fff;color:#556;font-size:12px;font-weight:600;padding:6px 13px;cursor:pointer}258.d-seg button.active{background:#E67700;color:#fff}259/* Panneau détail d'une unité */260.unit-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:190;align-items:flex-start;justify-content:center;padding:1.5rem;overflow-y:auto}261.unit-overlay.open{display:flex}262.unit-modal{background:#fff;border-radius:16px;width:100%;max-width:460px;margin:auto;padding:20px;position:relative}263.unit-close{position:absolute;top:12px;right:14px;background:none;border:none;font-size:26px;line-height:1;color:#999;cursor:pointer}264.up-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding-right:24px}265.up-num{font-size:20px;font-weight:800;color:#1a1a1a}266.up-sub{font-size:13px;color:#7a828b;margin-top:2px}267.up-stat{font-size:11px;font-weight:700;padding:3px 10px;border-radius:12px;white-space:nowrap}268.up-stat.st-dispo{background:#FFECCF;color:#E67700}.up-stat.st-loue{background:#FCE4E4;color:#B42318}269.up-price{font-size:18px;font-weight:800;color:#E67700;margin:12px 0 4px}270.up-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;margin-top:10px}271.up-row{display:flex;justify-content:space-between;gap:10px;font-size:14px;border-bottom:1px solid #f2f4f6;padding-bottom:5px}272.up-k{color:#7a828b}.up-v{font-weight:600;color:#1a1a1a}273.up-sec{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;color:#8a8f96;margin:16px 0 6px}274.up-caract{font-size:14px;color:#333;line-height:1.5;white-space:pre-line}275.up-note{font-size:14px;color:#333;line-height:1.5;background:#fffdf3;border:1px solid #f0d98b;border-radius:10px;padding:10px 12px}276.up-photos{display:flex;gap:6px;overflow-x:auto;padding:2px 0 8px;-webkit-overflow-scrolling:touch}277.up-photos img{height:76px;width:104px;object-fit:cover;border-radius:8px;flex:0 0 auto;cursor:pointer;background:#eee}278.up-introw{display:flex;flex-direction:column;gap:1px;font-size:13.5px;background:#fffdf3;border:1px solid #f0d98b;border-radius:8px;padding:7px 10px;margin-bottom:6px}279.up-intk{font-size:11px;font-weight:700;color:#8a5a00;text-transform:uppercase;letter-spacing:.02em}280.up-intv{color:#333;line-height:1.45}281/* Matcher (association front, sans compte) */282/* Test photo en fond (repli vert d'origine si l'image manque) */283.matcher{background-color:#E67700;background-image:url('hero-matcher.jpg');background-size:cover;background-position:center 45%;background-repeat:no-repeat;padding:46px 2rem 46px}284.matcher-inner{width:fit-content;max-width:100%;margin:0 auto;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.5)}285.matcher-inner h2{margin:0 0 14px;font-size:24px;font-weight:800;text-align:center}286.matcher-inner>.matcher-inner,.matcher-inner p{margin:0 0 16px;font-size:14px;opacity:.94;text-align:center}287.matcher-grid{display:flex;flex-wrap:wrap;align-items:flex-start;gap:16px;background:#fff;border-radius:16px;padding:18px 20px;color:#333;text-shadow:none;box-shadow:0 12px 34px rgba(0,0,0,.16)}288.mx-field{flex:0 0 auto}289.mx-field:has(#mx-secteur){width:186px}290.mx-field:has(#mx-type){width:168px}291.mx-field:has(#mx-budget){width:206px}292.mx-field:has(#mx-etage){width:176px}293.mx-field:has(#mx-animaux){width:104px}294.mx-actions-inline{align-self:center;margin-left:-4px}295.mx-btn-go{display:inline-flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,#FB8C00,#E67700);color:#fff;border:none;border-radius:10px;padding:12px 26px;font-size:15px;font-weight:700;white-space:nowrap;cursor:pointer;box-shadow:0 4px 14px rgba(230,119,0,.28);transition:filter .12s,transform .12s}296.mx-btn-go:hover{filter:brightness(1.06);transform:translateY(-1px)}297.mx-btn-more{display:inline-flex;align-items:center;gap:6px;background:#fff;color:#E67700;border:1.5px solid #9ed9c3;border-radius:10px;padding:11px 15px;font-size:13.5px;font-weight:700;white-space:nowrap;cursor:pointer;margin-right:8px}298.mx-btn-more:hover{background:#FFECCF;border-color:#FB8C00}299.mx-more{display:none;flex:1 0 100%;border-top:1px dashed #e2e2df;margin-top:4px;padding-top:14px}300.mx-more.show{display:block}301.mx-more>label{display:block;font-size:12px;font-weight:700;color:#555;margin-bottom:8px;text-align:center}302.mx-field>label{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:#777;margin-bottom:6px;text-align:center}303.mx-field>input,.mx-field>select{width:100%;padding:8px 10px;border:1px solid #ddd;border-radius:8px;font-size:14px;color:#1a1a1a}304.mx-chips{display:flex;flex-wrap:wrap;gap:5px;justify-content:center}305.mx-chips label{display:inline-flex;align-items:center;gap:4px;font-size:12.5px;border:1px solid #ddd;border-radius:16px;padding:5px 11px;cursor:pointer;background:#fff;color:#333;position:relative}306.mx-chips label:has(input:checked){background:#FFECCF;border-color:#FB8C00;color:#E67700;font-weight:600}307.mx-chips input{position:absolute;opacity:0;width:0;height:0}308.matcher-actions{display:flex;align-items:center;gap:16px;margin-top:16px;flex-wrap:wrap}309.mx-btn-primary{background:#fff;color:#E67700;border:none;border-radius:10px;padding:12px 22px;font-size:15px;font-weight:700;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.18);text-shadow:none}310.mx-btn-primary:hover{background:#FFF7EF}311.mx-btn-ghost{background:transparent;color:#fff;border:none;font-size:13px;font-weight:600;cursor:pointer;opacity:.92;text-decoration:underline}312.match-bar{background:#FFECCF;border:1px solid #FB8C00;border-radius:14px;padding:14px 18px;margin:18px 2rem 0;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}313.mx-sum-btn{border:1px solid #FB8C00;background:#fff;color:#E67700;border-radius:8px;padding:7px 13px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap}314.mx-sum-btn:hover{background:#FFF7EF}315.mx-legend{display:flex;gap:14px;margin:10px 2rem 0;font-size:11.5px;color:#777;font-weight:600}316.mx-legend .lg{display:inline-flex;align-items:center;gap:5px}317.mx-legend .lg::before{content:'';width:13px;height:13px;border-radius:3px;display:inline-block}318.mx-legend .lg-p::before{background:#0B7A5E;box-shadow:0 0 0 1px #FB8C00}319.mx-legend .lg-e::before{background:#3FBF97}320.mx-legend .lg-g::before{background:#F59E0B}321.card.tier-perfect{outline:3px solid #0B7A5E;outline-offset:-3px;box-shadow:0 6px 20px rgba(11,122,94,.28)}322.card.tier-excellent{outline:2px solid #3FBF97;outline-offset:-2px}323.card.tier-good{outline:2px solid #F59E0B;outline-offset:-2px}324.mx-tier-badge{display:inline-block;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;padding:3px 9px;border-radius:6px;margin-bottom:6px}325.mx-tier-badge.tier-perfect{background:#0B7A5E;color:#fff}326.mx-tier-badge.tier-excellent{background:#CBEEDF;color:#0B6E52}327.mx-tier-badge.tier-good{background:#F3B44A;color:#5F3B00}328.mx-chips-res{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}329.mx-chip{font-size:11px;padding:2px 8px;border-radius:20px;background:#FFECCF;color:#E67700;font-weight:600}330.mx-chip.soft{background:#FAEEDA;color:#633806}331.mx-chip.miss{background:#FCE4E4;color:#B42318}332.match-group-header{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:10px;margin:20px 0 4px;padding-bottom:8px;border-bottom:2px solid var(--gc,#ddd);font-size:16px;font-weight:800;color:var(--gc,#555)}333.match-group-header:first-child{margin-top:0}334.match-group-header .mgh-count{font-size:12px;font-weight:700;color:#fff;background:var(--gc,#888);border-radius:12px;padding:2px 11px;min-width:24px;text-align:center}335.vtable{width:100%;border-collapse:collapse;font-size:13px}336.vtable th,.vtable td{padding:9px 10px;text-align:left;border-bottom:1px solid #f0f0ee}337.vtable th{font-size:11px;text-transform:uppercase;letter-spacing:.03em;color:#888;font-weight:600;white-space:nowrap}338.vtable tbody tr:hover{background:#faf9f6}339.vtable .v-label{display:inline-block;min-width:22px;text-align:center;font-weight:700;color:#3C3489;background:#EEEDFE;border-radius:6px;padding:1px 7px;margin-right:2px}340.vtable .v-price{font-weight:700;color:#E67700;white-space:nowrap}341.vtable .v-note{color:#888;font-size:12px}342.vtable-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}343.card-footer{display:flex;align-items:center;justify-content:space-between;padding-top:10px;border-top:1px solid #f0f0ee}344.price{font-size:17px;font-weight:600}345.price span{font-size:12px;font-weight:400;color:#888}346.btn-visite-card{font-size:12px;font-weight:600;padding:7px 13px;border:none;border-radius:8px;background:#FFF1E0;color:#C2540E;cursor:pointer;transition:all 0.15s}347.btn-visite-card:hover{background:#FB8C00;color:#fff}348/* MODAL DÉTAIL */349.detail-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:100;align-items:flex-start;justify-content:center;padding:1.5rem;overflow-y:auto}350.detail-overlay.open{display:flex}351/* Évite la double barre de défilement : verrouille la page derrière un modal ouvert. */352body:has(.detail-overlay.open),body:has(.unit-overlay.open),body:has(.visite-overlay.open),body:has(.lightbox-overlay.open),body:has(.bld-full-overlay.open){overflow:hidden}353.detail-modal{background:#fff;border-radius:18px;width:100%;max-width:680px;overflow:hidden;margin:auto}354.detail-close{position:absolute;top:1rem;right:1rem;background:rgba(0,0,0,0.4);border:none;color:#fff;width:32px;height:32px;border-radius:50%;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center}355.gallery{position:relative;height:280px;background:#f0f0ee;overflow:hidden}356.gallery img{width:100%;height:100%;object-fit:cover;display:none}357.gallery img.active{display:block}358.gallery-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#ccc}359.gallery-nav{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:6px}360.gallery-dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,0.5);cursor:pointer;border:none;transition:background 0.15s}361.gallery-dot.active{background:#fff}362.gallery-prev,.gallery-next{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.35);border:none;color:#fff;width:36px;height:36px;border-radius:50%;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center}363.gallery-prev{left:12px}364.gallery-next{right:12px}365.gallery-count{position:absolute;top:12px;left:12px;background:rgba(0,0,0,0.4);color:#fff;font-size:12px;padding:3px 8px;border-radius:12px}366.detail-body{padding:1.5rem}367.detail-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:1rem;gap:1rem}368.detail-ville{font-size:12px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:4px}369.detail-title{font-size:20px;font-weight:600;line-height:1.3}370.detail-price{font-size:22px;font-weight:700;color:#FB8C00;white-space:nowrap}371.detail-price span{font-size:13px;font-weight:400;color:#888}372.detail-badges{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1.25rem}373.detail-badge{font-size:12px;font-weight:600;padding:5px 12px;border-radius:20px}374.btn-staged{width:100%;margin:2px 0 1.25rem;padding:12px;border:1.5px solid #F5871F;border-radius:10px;background:linear-gradient(135deg,#FFF1E0,#FFE6CC);color:#C2410C;font-size:15px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:all .15s}375.btn-staged:hover{background:linear-gradient(135deg,#F5871F,#EA6D0B);color:#fff;border-color:#F5871F}376.staged-spark{position:absolute;top:12px;left:12px;z-index:6;width:40px;height:40px;border-radius:50%;border:none;cursor:pointer;font-size:20px;line-height:1;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#6366F1,#EC4899);box-shadow:0 2px 9px rgba(99,102,241,0.5);overflow:hidden}377.staged-spark:hover{filter:brightness(1.08);transform:scale(1.08)}378.card-staged{position:absolute;bottom:10px;right:10px;z-index:11;width:32px;height:32px;border-radius:50%;border:none;background:linear-gradient(135deg,#6366F1,#EC4899);font-size:16px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 7px rgba(99,102,241,0.45);overflow:hidden}379.card-staged:hover{filter:brightness(1.08);transform:scale(1.08)}380@keyframes stagedShine{0%{transform:translateX(-160%) rotate(20deg)}55%,100%{transform:translateX(220%) rotate(20deg)}}381.staged-spark::after,.card-staged::after{content:"";position:absolute;top:0;left:0;width:55%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.8),transparent);pointer-events:none;animation:stagedShine 2.4s ease-in-out infinite}382@media (prefers-reduced-motion:reduce){.staged-spark::after,.card-staged::after{animation:none}}383.db-dispo{background:#FFECCF;color:#085041}384.db-animal{background:#F1EFE8;color:#444}385.db-size{background:#E6F1FB;color:#0C447C}386.db-etage{background:#EAF3DE;color:#27500A}387.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}388.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}389.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}390.detail-section{margin-bottom:1.25rem}391.detail-section h3{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#888;margin-bottom:8px}392.detail-desc{font-size:14px;color:#444;line-height:1.7;white-space:pre-line}393.ddt-tabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}394.ddt-btn{font-size:12.5px;font-weight:600;padding:5px 13px;border-radius:20px;border:1px solid #e0e0dd;background:#fff;color:#666;cursor:pointer}395.ddt-btn:hover{border-color:#FB8C00;color:#FB8C00}396.ddt-btn.ddt-on{background:#FB8C00;border-color:#FB8C00;color:#fff}397.inclusions-grid{display:flex;flex-wrap:wrap;gap:6px}398.incl-tag{font-size:12px;padding:4px 10px;background:#f5f5f3;border:1px solid #e8e8e5;border-radius:20px;color:#555}399.detail-divider{border:none;border-top:1px solid #f0f0ee;margin:1.25rem 0}400/* Logements similaires */401.sim-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px}402.sim-card{background:#fff;border:1px solid #eee;border-radius:12px;overflow:hidden;cursor:pointer;transition:box-shadow .15s,transform .12s}403.sim-card:hover{box-shadow:0 8px 22px rgba(0,0,0,.1);transform:translateY(-2px)}404.sim-photo{height:100px;background:#f0f0ee no-repeat center/cover;display:flex;align-items:center;justify-content:center;color:#c4c4c4;font-size:26px;position:relative}405.sim-badge{position:absolute;left:7px;top:7px;background:#FB8C00;color:#3d2400;font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;padding:2px 7px;border-radius:999px}406.sim-b{padding:8px 10px}407.sim-t{font-size:12.5px;font-weight:600;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}408.sim-v{font-size:10.5px;color:#999;text-transform:uppercase;letter-spacing:.03em;margin:1px 0 4px}409.sim-p{font-size:13.5px;font-weight:800;color:#E67700}410/* Feuille de partage / publication */411.share-ov{position:fixed;inset:0;z-index:4000;background:rgba(0,0,0,.45);display:none;align-items:flex-end;justify-content:center}412.share-ov.open{display:flex}413.share-sheet{background:#fff;width:100%;max-width:460px;border-radius:18px 18px 0 0;padding:16px 18px 24px;box-shadow:0 -8px 30px rgba(0,0,0,.2)}414@media(min-width:560px){.share-ov{align-items:center}.share-sheet{border-radius:18px}}415.share-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}416.share-head b{font-size:16px}417.share-head button{border:none;background:#f2f2f0;width:30px;height:30px;border-radius:50%;cursor:pointer;font-size:14px;color:#666}418.share-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}419.share-btn{display:flex;align-items:center;gap:10px;padding:12px 14px;border:1px solid #eee;border-radius:12px;background:#fff;cursor:pointer;font-size:13.5px;font-weight:600;color:#333;text-align:left}420.share-btn:hover{border-color:#FB8C00;background:#fff7ef}421.share-btn .ic{font-size:18px;flex:none}422.share-note{font-size:12px;color:#888;margin-top:12px;min-height:16px;text-align:center}423424/* BOUTONS CONTACT */425.contact-btns{display:flex;gap:10px;margin-bottom:0.75rem}426.btn-call{flex:1;padding:12px;background:#F97316;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none}427.btn-call:hover{background:#EA6C0A}428.btn-email{flex:1;padding:12px;background:#3B82F6;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none}429.btn-email:hover{background:#2563EB}430.btn-visite-full{width:100%;padding:12px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;margin-bottom:0.75rem;display:flex;align-items:center;justify-content:center;gap:8px}431.btn-visite-full:hover{background:#E67700}432.btn-retour{width:100%;padding:10px;background:transparent;color:#666;border:1px solid #ddd;border-radius:10px;font-size:14px;cursor:pointer}433.btn-share{width:100%;padding:10px;background:transparent;color:#FB8C00;border:1.5px solid #FB8C00;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;margin-bottom:8px;display:flex;align-items:center;justify-content:center;gap:8px}434.btn-share:hover{background:#FFECCF}435/* NOUS JOINDRE */436#contact-section{background:#FFF7EF;color:#1a1a1a;padding:2.5rem 2rem;margin-top:1.5rem}437.contact-inner{max-width:820px;margin:0 auto;text-align:center}438.contact-inner h2{font-size:24px;font-weight:800;margin:0 0 6px}439.contact-lead{font-size:15px;color:#8a7f72;margin:0 0 1.75rem}440.contact-cards{display:flex;gap:16px;justify-content:center;flex-wrap:wrap}441.contact-card{display:flex;align-items:center;gap:14px;background:#fff;border:1px solid #f0e2d2;border-radius:14px;padding:16px 22px;text-decoration:none;color:#1a1a1a;transition:box-shadow .15s,transform .12s,border-color .15s;min-width:260px}442.contact-card:hover{box-shadow:0 8px 22px rgba(0,0,0,.07);transform:translateY(-2px);border-color:#FB8C00}443.cc-icon{display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:50%;background:#FFECCF;color:#E67700;flex-shrink:0}444.cc-text{display:flex;flex-direction:column;text-align:left}445.cc-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;color:#a89a8a;font-weight:600}446.cc-value{font-size:16px;font-weight:700;color:#C25A0C}447/* MODAL VISITE */448.visite-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:200;align-items:center;justify-content:center;padding:1rem}449.visite-overlay.open{display:flex}450.vf-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px 14px;padding:1.25rem;max-height:62vh;overflow-y:auto}451.vf-f{display:flex;flex-direction:column;gap:4px;font-size:12px;font-weight:600;color:#555}452.vf-f.vf-full{grid-column:1/-1}453.vf-f input,.vf-f select,.vf-f textarea{padding:9px 11px;border:1px solid #ddd;border-radius:8px;font-size:14px;font-weight:400;color:#1a1a1a;font-family:inherit}454.vf-submit{display:block;width:calc(100% - 2.5rem);margin:0 1.25rem 1.25rem;padding:13px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer}455.vf-submit:hover{background:#E67700}.vf-submit:disabled{opacity:.6;cursor:default}456.vf-msg{margin:1rem 1.25rem 0;font-size:13px;font-weight:600}457.vf-msg:empty{display:none;margin:0}458.vf-msg.ok{color:#E67700;background:#FFECCF;padding:10px 12px;border-radius:8px}459.vf-msg.err{color:#B42318;background:#FCE4E4;padding:10px 12px;border-radius:8px}460/* Assistant « Formulaire de location » (modal fiche) */461.vw-steps{display:flex;gap:6px;padding:14px 1.25rem 2px}462.vw-st{flex:1;display:flex;flex-direction:column;align-items:center;gap:5px}463.vw-dot{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;background:#eef0f2;color:#9aa3ad;border:2px solid #eef0f2;transition:all .2s}464.vw-st.active .vw-dot{background:linear-gradient(135deg,#FB8C00,#E67700);color:#fff;border-color:transparent;box-shadow:0 3px 9px rgba(230,119,0,.35)}465.vw-st.done .vw-dot{background:#16A34A;color:#fff;border-color:transparent}466.vw-lbl{font-size:10.5px;font-weight:700;color:#9aa3ad;text-align:center;line-height:1.1}467.vw-st.active .vw-lbl{color:#C2410C}468.vw-body{padding:14px 1.25rem;max-height:58vh;overflow-y:auto}469.vw-body section{display:flex;flex-direction:column;gap:12px}470.vw-fl{display:block;font-size:12px;font-weight:700;color:#555;margin-bottom:6px}471.vw-fl i{font-weight:400;color:#9aa3ad;font-style:normal;font-size:11px}472.vw-sub{font-size:12.5px;color:#888;margin:0}473.vw-chips{display:flex;flex-wrap:wrap;gap:7px}474.vw-chip{padding:8px 13px;border:1.5px solid #e0e2e6;border-radius:999px;background:#fff;font-size:13.5px;font-weight:600;color:#374151;cursor:pointer;user-select:none;transition:all .12s}475.vw-chip:hover{border-color:#FCD9A8;background:#FFFaf3}476.vw-chip.on{background:linear-gradient(135deg,#FB8C00,#E67700);border-color:transparent;color:#fff;box-shadow:0 2px 8px rgba(230,119,0,.3)}477.vw-crit{display:grid;grid-template-columns:1fr 1fr;gap:7px}478.vw-ci{display:flex;align-items:center;gap:8px;padding:9px 10px;border:1.5px solid #e8eaed;border-radius:10px;font-size:12.5px;font-weight:600;color:#374151;cursor:pointer;user-select:none;transition:all .12s}479.vw-ci:hover{border-color:#FCD9A8;background:#FFFaf3}480.vw-ci.on{border-color:#FB8C00;background:#FFF5E9}481.vw-ci .ck{flex:none;width:17px;height:17px;border-radius:5px;border:2px solid #cbd2d9;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:900;color:transparent}482.vw-ci.on .ck{background:#FB8C00;border-color:#FB8C00;color:#fff}483.vw-ci .cem{font-size:15px}484.vw-nav{display:flex;gap:10px;padding:12px 1.25rem 1.25rem;border-top:1px solid #f0f0ee}485.vw-btn{flex:1;padding:12px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit}486.vw-btn:hover{background:#E67700}.vw-btn:disabled{opacity:.6;cursor:default}487.vw-btn.ghost{flex:0 0 auto;background:#fff;color:#C2410C;border:1.5px solid #e6c9a8;padding:12px 16px}488.vw-btn.ghost:hover{background:#FFF7EC}489@media(max-width:440px){.vw-crit{grid-template-columns:1fr}}490.visite-modal{background:#fff;border-radius:16px;width:100%;max-width:480px;max-height:90vh;overflow-y:auto;padding:1.5rem}491.modal-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:1.25rem}492.modal-title{font-size:17px;font-weight:600}493.modal-sub{font-size:13px;color:#888;margin-top:2px}494.btn-close{background:none;border:none;font-size:22px;color:#888;cursor:pointer}495.form-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}496.form-field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}497.form-field label{font-size:12px;color:#888;font-weight:500}498.form-field input,.form-field select,.form-field textarea{font-size:14px;padding:8px 10px;border:1px solid #ddd;border-radius:8px;color:#1a1a1a;width:100%}499.form-field textarea{resize:vertical;min-height:72px}500.err{font-size:11px;color:#A32D2D;display:none;margin-top:2px}501.divider{border:none;border-top:1px solid #f0f0ee;margin:1rem 0}502.btn-submit{width:100%;padding:10px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer}503.btn-submit:hover{background:#E67700}504.success-view{display:none;text-align:center;padding:1.5rem 0}505.success-icon{font-size:52px;margin-bottom:1rem}506.success-title{font-size:18px;font-weight:600;margin-bottom:6px}507.success-msg{font-size:14px;color:#666;line-height:1.6}508/* MINI CARTE MODAL */509.detail-map{height:220px;width:100%;border-radius:10px;overflow:hidden;margin-bottom:1.25rem}510/* LIGHTBOX */511.lightbox-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.95);z-index:300;align-items:center;justify-content:center}512.lightbox-overlay.open{display:flex}513.lightbox-img{max-width:95vw;max-height:92vh;object-fit:contain;border-radius:4px}514.lightbox-close{position:absolute;top:1rem;right:1rem;background:rgba(255,255,255,0.15);border:none;color:#fff;width:40px;height:40px;border-radius:50%;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center}515.lightbox-close:hover{background:rgba(255,255,255,0.3)}516.lightbox-prev,.lightbox-next{position:absolute;top:50%;transform:translateY(-50%);background:rgba(255,255,255,0.15);border:none;color:#fff;width:44px;height:44px;border-radius:50%;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center}517.lightbox-prev:hover,.lightbox-next:hover{background:rgba(255,255,255,0.3)}518.lightbox-prev{left:1rem}519.lightbox-next{right:1rem}520.lightbox-count{position:absolute;bottom:1rem;left:50%;transform:translateX(-50%);color:rgba(255,255,255,0.7);font-size:13px}521.gallery img{cursor:zoom-in}522/* RUBAN LOUÉ */523.ribbon-loue{position:absolute;top:28px;left:-45px;width:190px;background:#EF4444;color:#fff;font-size:14px;font-weight:900;text-align:center;padding:8px 0;transform:rotate(-45deg);box-shadow:0 3px 10px rgba(0,0,0,0.4);letter-spacing:0.15em;z-index:10;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,0.2)}524.ribbon-nouveau{position:absolute;top:28px;left:-45px;width:190px;background:#16A34A;color:#fff;font-size:13px;font-weight:700;text-align:center;padding:7px 0;transform:rotate(-45deg);box-shadow:0 2px 6px rgba(0,0,0,0.18);letter-spacing:0.12em;z-index:10;pointer-events:none}525/* RUBAN LOUÉ */526.card-img{overflow:hidden;position:relative}527528/* ===================== MOBILE ===================== */529@media (max-width:768px){530  /* Marges horizontales resserrées (2rem -> 1rem) */531  .header{padding:0.7rem 1rem;flex-wrap:wrap;gap:8px}532  .brand{font-size:16px}533  .hero{padding:1.1rem 1rem}534  .hero h1{font-size:20px}535  .hero p{font-size:13px}536  .filters-wrap{padding:0.8rem 1rem;gap:0.6rem}537  .tags-row{padding:0.55rem 1rem}538  .main{padding:1.1rem 1rem 2.5rem}539  /* Grille en 1 colonne */540  .grid{grid-template-columns:1fr;gap:14px}541  /* Filtres : 2 par rangée, prix et reset pleine largeur */542  .filter-group{min-width:0;flex:1 1 44%}543  .price-range{flex:1 1 100%;min-width:0}544  .price-inputs{min-width:0}545  .price-inputs input{flex:1 1 0;min-width:0;width:auto}546  .btn-reset{align-self:stretch;flex:1 1 100%;padding:9px 14px}547  /* Matcher : pleine largeur, bouton sous les filtres */548  .matcher{padding:26px 1rem 22px}549  .matcher-inner{width:100%}550  .matcher-inner h2{font-size:20px}551  .matcher-grid{padding:14px;gap:12px}552  .mx-field:has(#mx-secteur),.mx-field:has(#mx-type),.mx-field:has(#mx-budget),.mx-field:has(#mx-etage),.mx-field:has(#mx-animaux){width:100%}553  .mx-actions-inline{width:100%;margin-left:0;align-self:stretch}554  .mx-btn-go{width:100%}555  /* Modal détail : plein écran */556  .detail-overlay{padding:0}557  .detail-modal{border-radius:0;max-width:none;min-height:100vh}558  .detail-body{padding:1.1rem}559  .gallery{height:230px}560  .detail-header{flex-direction:column;gap:.3rem}561  .detail-title{font-size:18px}562  .detail-price{font-size:20px}563  /* Modèles de photos : 1 par rangée */564  .d-models-box{padding:12px}565  #d-photo-models{grid-template-columns:1fr}566  /* Panneau de correspondances : boutons empilés pleine largeur */567  .match-bar{margin:14px 1rem 0;padding:12px 14px}568  .mx-sum-actions{width:100%;flex-direction:column}569  .mx-sum-btn{width:100%}570  .mx-legend{margin:10px 1rem 0}571  /* Visuel immeuble : pleine largeur, tuiles plus grandes et lisibles */572  .d-bld{width:100%;max-width:none}573  /* Tuiles plus petites/denses sur mobile → un maximum tiennent par rangée avant de passer à la ligne. */574  .d-floor-lbl{flex:0 0 56px;font-size:8.5px;padding:6px 5px}575  .d-units.compact{gap:4px;padding:6px}576  .d-unit.mini{width:auto;flex:1 1 54px;min-width:50px;max-width:150px;padding:6px 3px;border-radius:7px;border-width:1.5px}577  .d-unit.mini .mn{font-size:11px}578  .d-unit.mini .mm{font-size:8.5px}579  /* Panneau d'unité : quasi plein écran */580  .unit-overlay{padding:1rem}581  .unit-modal{max-width:none}582  /* Visite / lightbox */583  .vf-grid{grid-template-columns:1fr}584  /* Barre de recherche rue/ville : mise en évidence sur mobile */585  .search-wrap{padding:0.85rem 1rem;background:#FFF7EF;border-bottom:1px solid #FAD9B0}586  .search-wrap>div{max-width:none}587  .search-wrap input{border:2px solid #FB8C00!important;background:#fff;font-size:16px!important;padding:13px 12px 13px 42px!important;box-shadow:0 2px 8px rgba(251,140,0,.14)}588  .search-wrap svg{left:14px!important;color:#FB8C00!important}589  .lb-models{top:8px;gap:6px}590  .lb-mbtn{padding:6px 11px;font-size:12px}591}592@media (max-width:440px){593  .filter-group{flex:1 1 100%}594  .matcher-inner h2{font-size:18px}595  .gallery{height:200px}596}597/* Outil de partage agent — même sélecteur que la vue admin « Sélections » */598.sel-crit{background:#fff;border:1px solid #e8e8e5;border-radius:12px;padding:14px 16px}599.sel-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;align-items:start}600.sel-field>label{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.02em;color:#777;margin-bottom:6px}601.sel-field>input,.sel-field>select{width:100%;padding:7px 9px;border:1px solid #ddd;border-radius:7px;font-size:13px;background:#fff;box-sizing:border-box}602.sel-chips{display:flex;flex-wrap:wrap;gap:5px}603.sel-chips label{display:inline-flex;align-items:center;gap:4px;font-size:12px;border:1px solid #ddd;border-radius:16px;padding:4px 10px;cursor:pointer;background:#fff;color:#333}604.sel-chips label:has(input:checked){background:#FFECCF;border-color:#FB8C00;color:#E67700;font-weight:600}605.sel-chips input{margin:0}606.sel-count{font-size:13px;color:#555;margin:2px 0 10px;font-weight:600}607.sel-row{display:flex;align-items:center;gap:10px;border:1px solid #e8e8e5;border-radius:10px;padding:8px 10px;background:#fff;margin-bottom:8px;cursor:pointer}608.sel-row.off{opacity:.5;background:#fafaf8}609.sel-row input[type=checkbox]{width:18px;height:18px;flex:0 0 auto;accent-color:#FB8C00;cursor:pointer}610.sel-row .selthumb{width:54px;height:42px;border-radius:6px;background:#f0f0ee no-repeat center/cover;flex:0 0 auto;object-fit:cover;display:block}611.sel-row .selinfo{flex:1;min-width:0}612.sel-row .selinfo b{font-size:13.5px}613.sel-row .selinfo .m{font-size:12px;color:#888;margin-top:1px}614.sel-linkbox{background:#fff;border:1px solid #FB8C00;border-radius:12px;padding:14px 16px}615.sel-linkbox input.lnk{padding:8px 10px;border:1px solid #ddd;border-radius:7px;font-size:12.5px;background:#f7f7f5;color:#333}616.sel-btn{border:1px solid #d5d5d0;background:#fff;color:#333;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer}617.sel-btn.primary{background:#FB8C00;border-color:#FB8C00;color:#fff}618/* Cartes de rail (partagées avec la section « Nouveautés » vue agent) */619.fr-card{flex:0 0 208px;background:#fff;border:1px solid #e8e8e5;border-radius:12px;overflow:hidden;cursor:pointer;transition:border-color .15s}620.fr-card:hover{border-color:#FB8C00}621.fr-img{height:112px;width:100%;display:block;object-fit:cover;background:#f0f0ee}622.fr-body{padding:8px 10px 10px}623.fr-t{font-size:12.5px;font-weight:600;color:#1a1a1a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}624.fr-m{font-size:11.5px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}625.fr-p{font-size:13px;font-weight:700;color:#1a1a1a;margin-top:2px}626/* Section « Nouveautés » (vue agent) */627#agent-new-rail{margin:0 0 20px;background:#EEF6FF;border:1px solid #BFDBFE;border-radius:14px;padding:12px 14px}628#agent-new-rail .an-head{display:flex;align-items:baseline;gap:10px;margin:0 0 8px;flex-wrap:wrap}629#agent-new-rail .an-title{font-size:16px;font-weight:700;color:#1D4ED8}630#agent-new-rail .an-hint{font-size:11.5px;color:#64748b}631.an-scroll{display:flex;gap:12px;overflow-x:auto;padding-bottom:6px}632.an-scroll .fr-card{flex:0 0 208px}633.fr-days{position:absolute;top:7px;right:7px;background:#1D4ED8;color:#fff;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;box-shadow:0 1px 4px rgba(0,0,0,.25)}634/* Bascule entre immeubles liés (groupe_immeuble) — au-dessus du plan des unités */635.d-bld-switch{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;margin:0 0 12px}636.d-switch-lbl{font-size:12px;color:#8a8f96}637.d-switch-seg{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;border:none}638.d-switch-btn{display:inline-flex;align-items:center;gap:5px;font-size:12.5px;font-weight:700;padding:8px 15px;color:#E67700;background:#fff;border:1.5px solid #f0d9b8;border-radius:999px;cursor:pointer;transition:all .12s}639.d-switch-btn:hover:not(.cur){background:#fff7ef;border-color:#FB8C00}640.d-switch-btn.cur{background:#FB8C00;color:#fff;border-color:#FB8C00;cursor:default;box-shadow:0 4px 12px rgba(251,140,0,.28)}641.d-switch-btn[disabled]{opacity:1}642@media(max-width:560px){.d-switch-seg{width:100%}.d-switch-btn{flex:1 1 auto;justify-content:center;font-size:12px;padding:9px 10px}}643</style>644<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.css"645  integrity="sha384-b8ANgTJvdlAnWM5YGMpKn7Kodm+1k7NYNG9zdjTCcZcKatzYHwZ0RLdWarbJJVzU" crossorigin="anonymous"/>646<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js"647  integrity="sha384-u5N8qJeJOO2iqNjIKTdl6KeKsEikMAmCUBPc6sC6uGpgL34aPJ4VgNhuhumedpEk" crossorigin="anonymous"></script>648<style>649#map-section{display:none;background:#fff;border-bottom:1px solid #e8e8e5}650#map-section.open{display:flex;height:calc(100vh - 54px)}651#map{flex:1;height:100%;width:auto;min-height:360px}652/* En mode carte, on masque tout le contenu du bas (hero, matcher, grille de logements). */653body.map-open .hero,body.map-open #matcher-panel,body.map-open #match-summary,body.map-open #browse-controls,body.map-open #featured,body.map-open #grid,body.map-open #results-line,body.map-open .count-badge{display:none!important}654body.map-open{overflow:hidden}655/* Panneau de résultats à droite : les logements visibles dans la zone de la carte. */656#map-results{width:350px;flex:0 0 350px;overflow-y:auto;background:#f6f6f4;border-left:1px solid #e8e8e5;padding:12px 12px 24px}657#map-results .mr-head{font-size:12.5px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;color:#7a4a00;margin:2px 2px 10px;position:sticky;top:-12px;background:#f6f6f4;padding:10px 2px;z-index:2}658#map-results .mr-empty{color:#999;font-size:13px;padding:1.5rem 1rem;text-align:center;line-height:1.5}659.mr-card{display:flex;gap:10px;background:#fff;border:1px solid #e8e8e5;border-radius:12px;overflow:hidden;margin-bottom:10px;cursor:pointer;transition:box-shadow .15s,border-color .15s}660.mr-card:hover{border-color:#FB8C00;box-shadow:0 4px 14px rgba(0,0,0,.08)}661.mr-card img{width:96px;height:96px;object-fit:cover;flex:0 0 96px}662.mr-card .mr-noimg{width:96px;height:96px;flex:0 0 96px;background:#f0efea;display:flex;align-items:center;justify-content:center;font-size:26px}663.mr-b{padding:8px 10px 8px 0;min-width:0;display:flex;flex-direction:column;justify-content:center}664.mr-t{font-size:13.5px;font-weight:700;color:#222;line-height:1.25;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}665.mr-sub{font-size:12px;color:#777;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}666.mr-price{font-size:13px;font-weight:800;color:#E67700;margin-top:3px}667.mr-back-btn{position:sticky;top:0;z-index:4;display:flex;align-items:center;justify-content:center;gap:7px;width:100%;background:#FB8C00;color:#fff;border:none;border-radius:9px;padding:11px 12px;font-weight:700;font-size:14px;cursor:pointer;margin-bottom:12px;box-shadow:0 3px 10px rgba(251,140,0,.32)}668.mr-back-btn:hover{background:#E67700}669/* Dropdown caractéristiques (sélection multiple) */670.ms-chk{display:grid;grid-template-columns:1fr;gap:5px;margin-top:3px}671.ms-chk label{display:flex;align-items:center;gap:5px;font-size:12px;font-weight:500;color:#333;cursor:pointer;background:#fff;border:1px solid #e3e3e0;border-radius:8px;padding:6px 7px}672.ms-chk label:hover{border-color:#FB8C00}673#map-side .ms-chk input{width:auto!important;flex:0 0 auto;margin:0}674/* Outil rayon */675#ms-radius-row{display:flex;gap:8px;align-items:center}676#ms-radius-row select{flex:1}677#ms-radius-btn{padding:9px 10px;border:1px solid #16A34A;background:#16A34A;color:#fff;border-radius:9px;font-weight:600;cursor:pointer;white-space:nowrap;font-size:13px}678#ms-radius-btn.active{background:#DC2626;border-color:#DC2626}679#ms-radius-btn.pick{background:#E67700;border-color:#E67700}680#map-side{width:290px;flex:0 0 290px;overflow-y:auto;overflow-x:hidden;background:#fff;border-right:1px solid #e8e8e5;padding:16px;box-sizing:border-box}681#map-side .ms-h{font-size:12.5px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;color:#7a4a00;margin:0 0 4px}682#map-side .ms-count{font-size:13px;color:#555;margin:0 0 14px;font-weight:600}683#map-side .ms-f{display:flex;flex-direction:column;gap:5px;margin-bottom:12px;font-size:12px;font-weight:700;color:#555}684#map-side .ms-f select,#map-side .ms-f input{padding:9px 11px;border:1px solid #dcdce0;border-radius:9px;font-size:14px;font-weight:400;color:#1a1a1a;background:#fff;width:100%;font-family:inherit}685#map-side .btn-reset{margin-top:4px;width:100%;padding:10px;background:#f2f2f0;border:1px solid #e0e0dd;border-radius:9px;font-weight:600;cursor:pointer;color:#444}686#map-side .btn-reset:hover{border-color:#FB8C00;color:#FB8C00}687/* Boutons flottants (mobile uniquement) */688.map-fab{display:none}689.mr-sheet-close{display:none}690.mr-grip{display:none}691@media(max-width:760px){692  /* Deux zones empilées : carte (hauteur fixe, gère son propre glissement) + liste qui défile693     dans sa propre zone juste en dessous. La PAGE ne défile pas → aucun conflit avec la carte. */694  /* Ancrée en fixed entre le header (haut) et le bas RÉEL du viewport : immunisé contre695     les écarts de dvh (Samsung) qui laissaient une bande grise morte au-dessus de la barre. */696  #map-section.open{flex-direction:column;position:fixed;left:0;right:0;top:61px;bottom:0;height:auto;min-height:0;z-index:40}697  body.map-open{overflow:hidden}698  #map{height:52%;min-height:210px;flex:0 0 auto;width:100%}699  /* Filtres : tiroir glissant depuis la gauche, par-dessus l'écran */700  #map-side{position:fixed;top:0;left:0;bottom:0;height:100dvh;width:86%;max-width:330px;max-height:none;z-index:1300;transform:translateX(-103%);transition:transform .25s ease;box-shadow:4px 0 22px rgba(0,0,0,.22);border-right:1px solid #e8e8e5;border-bottom:none}701  body.ms-filters-open #map-side{transform:translateX(0)}702  body.ms-filters-open .ms-close{display:flex}703  /* Résultats : liste sous la carte, avec son propre défilement interne */704  #map-results{position:static;transform:none;width:100%;flex:1 1 auto;min-height:0;max-height:none;overflow-y:auto;border-left:none;border-top:1px solid #e4e3de;box-shadow:none;border-radius:0;padding:0 12px calc(72px + env(safe-area-inset-bottom,0px))}705  /* Poignée visible en haut du panneau : indique qu'on peut faire défiler (carte ↕) */706  .mr-grip{display:block;position:sticky;top:0;z-index:4;margin:0 -12px 6px;padding:8px 0 7px;background:#f6f6f4;box-shadow:0 1px 0 rgba(0,0,0,.06)}707  .mr-grip::before{content:"";display:block;width:42px;height:5px;margin:0 auto;border-radius:99px;background:#c6c4bd}708  /* En-tête + bouton retour NON sticky sur mobile : évite le bug Android (sticky rendu derrière le contenu) */709  #map-results .mr-head{position:static;border-radius:0;padding:4px 2px 10px}710  .mr-back-btn{position:static}711  .mr-sheet-close{display:none}712  /* Boutons flottants posés sur la carte */713  .map-fab{display:flex;position:absolute;z-index:1100;border:none;border-radius:24px;padding:11px 16px;font-size:14px;font-weight:700;box-shadow:0 3px 14px rgba(0,0,0,.28);cursor:pointer;align-items:center;gap:7px}714  #fab-filters{top:12px;left:12px;background:#fff;color:#333;border:1px solid #ddd}715  #fab-fs{top:12px;right:12px;background:#fff;color:#333;border:1px solid #ddd;padding:11px;border-radius:50%}716  #fab-results{display:none}                 /* plus besoin : les logements sont listés sous la carte */717  /* Plein écran natif : la carte occupe tout, la liste est masquée */718  #map-section:fullscreen{height:100vh;width:100vw;top:0;bottom:auto;left:0}719  #map-section:-webkit-full-screen{height:100vh;width:100vw;top:0;bottom:auto;left:0}720  #map-section:fullscreen #map,#map-section:-webkit-full-screen #map{height:100vh}721  #map-section:fullscreen #map-results,#map-section:-webkit-full-screen #map-results{display:none}722}723.ms-close{display:none;position:absolute;top:10px;right:10px;z-index:5;background:#f2f2f0;border:1px solid #e0e0dd;border-radius:50%;width:32px;height:32px;align-items:center;justify-content:center;font-size:16px;cursor:pointer;color:#555}724.map-toggle{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;padding:7px 14px;border:1px solid #ddd;border-radius:8px;background:#fff;color:#444;cursor:pointer;transition:all 0.15s}725.map-toggle:hover{border-color:#FB8C00;color:#FB8C00}726.map-toggle.active{background:#FB8C00;color:#fff;border-color:#FB8C00}727/* « Voir la carte » en vert plein pour ressortir (desktop + mobile) */728#map-toggle-btn{background:#16A34A;border-color:#16A34A;color:#fff;box-shadow:0 2px 8px rgba(22,163,74,.32)}729#map-toggle-btn:hover,#map-toggle-btn.active{background:#15803D;border-color:#15803D;color:#fff}730.leaflet-popup-content{font-family:system-ui,sans-serif;min-width:200px;margin:0;padding:0;overflow:hidden;border-radius:12px}731.leaflet-popup-content-wrapper{padding:0;border-radius:12px}732.popup-photo{width:100%;height:120px;object-fit:cover;display:block}733.popup-body{padding:10px 12px 12px}734.popup-title{font-size:13px;font-weight:600;margin-bottom:4px;line-height:1.3}735.popup-price{font-size:15px;font-weight:700;color:#FB8C00;margin-bottom:4px}736.popup-dispo{font-size:11px;color:#888;margin-bottom:8px}737.popup-btn{display:block;width:100%;padding:6px;background:#FB8C00;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;text-align:center}738.popup-btn:hover{background:#E67700}739.popup-list{display:flex;flex-direction:column;gap:6px;max-height:212px;overflow-y:auto}740.popup-row{display:flex;align-items:stretch;gap:7px;width:100%;background:#faf9f7;border:1px solid #eee;border-radius:9px;padding:6px 7px;font-family:inherit}741.popup-row:hover{border-color:#FB8C00;background:#fff}742.popup-row .pr-main{flex:1 1 auto;display:flex;align-items:center;gap:9px;min-width:0;background:none;border:none;padding:0;cursor:pointer;text-align:left;font-family:inherit}743.popup-row .pr-feat{flex:0 0 auto;align-self:center;background:#0E6E82;color:#fff;border:none;border-radius:7px;padding:6px 9px;font-size:14px;line-height:1;cursor:pointer}744.popup-row .pr-feat:hover{background:#0b5a6b}745.popup-row .pr-badge{flex:0 0 auto;color:#fff;font-size:11px;font-weight:700;border-radius:6px;padding:3px 7px;min-width:32px;text-align:center}746.popup-row .pr-txt{flex:1;display:flex;flex-direction:column;font-size:13px;color:#1a1a1a;min-width:0}747.popup-row .pr-txt b{font-size:13.5px;color:#E67700;font-weight:700}748.popup-row .pr-sub{font-size:10.5px;color:#888;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}749.popup-row .pr-go{flex:0 0 auto;color:#c4c4c0;font-size:18px;font-weight:700}750</style>751</head>752<body>753<script>/* Affiche tout de suite la barre agent si on sait déjà que c'est un staff (évite le flash de la barre candidat) */try{if(localStorage.getItem('gisr_staff')==='1')document.body.classList.add('is-staff');}catch(e){}</script>754<div class="header">755  <div class="logo-row">756    <img src="data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAEsASwDASIAAhEBAxEB/8QAHQABAAICAwEBAAAAAAAAAAAAAAYHBQgCAwQJAf/EAFAQAAEDAwEFAgQMFQIHAAAAAAABAgMEBQYRBxIhMUEIEyJRYYEUGDI1N1NxdZSztNIWFyM2QlJUVnJzdISRkpWhsbLB0dM4YhUkNENjhaL/xAAbAQEAAgMBAQAAAAAAAAAAAAAABAUCAwYHAf/EADgRAAICAQIDBAcGBwADAAAAAAABAgMEBRESITETQVFhIjI0cYGRsRQVU6HB4QYWIzNScvA1QmL/2gAMAwEAAhEDEQA/ANMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATvY7s1u20O+dzBvUtqp3ItbWq3gxPtG+N69E6c18t4bX9gNqq8chqcGpG0lyoIUYtPvcK1qeNV/wC7/u5LyXoqVuRquNj3Kmb5v8veTqdPvuqdsVyX5+41UB2VEM1NUSU9RFJDNE5WSRyNVrmORdFRUXiiovQ6yyIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABm8Kxe8ZfkEFlstMs1RKurnLwZE3q9y9EQ45pjN3xHIKiyXqmWGphXgv2Mjej2r1apr7aHH2e/pddvIz7OfBx7cvEwwANhgAAACe7G9ml12h3vuot+ltNO5PRlareDU+0Z43r4unNemvXse2eVmf5LFRd+lFbmqrqipdpqqN0VzI0X1T9FT8FF1Xoi7t4xYrVjVkprNZaRlLRU7d1jG81Xq5V5q5eaqvModY1hYi7Kr13+X7lvpmmvIfaT9VfmfmL2G1YzY6ay2WkZS0dO3RjG81Xq5y9XLzVVMoAcHKTk3KT3bOujFRWy6FNdoDY5T5lTyZBj8UcGQxM1ezg1la1E5O6I/xO68l6KmoVXT1FJVS0tVDJBPC9WSRyNVrmORdFRUXkqKfSIp3tBbIaTMqGbILMyKlyCnjVzvsWVjWp6l3ieiJwd5l4aKnSaNrTp2ovfo9z8P2+nuKPU9L7Xe2pc+9eP7mnQAO2OWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABm8Jxa8ZhkEFlstMs1RKurnLwZEzq9y9EQ/cIxW8ZjkEFlstMss8i6vevqImdXuXoiG7WyrZ/Z9n+Ptt9vaktXKiOq6tzfDmd/RqdE/qVGq6rDChwx5zfRfqyy0/T5ZUt3yiv+2Q2U7PrPs/x9tBQNSaslRHVdW5uj5nf0anRP6nRtg2dWvaFjy0lQjae5QIrqKsRvGN32q+Ni9U86cUJwDg1lXK7t+L0uu51zx6nV2W3o+B868osV0xq+VNmvFK6mrKd269q8lTo5q9WrzRTGG8e23Zlb9oVi8Du6a9UrVWjqlTn/wCN/jav7l4+NF0pvlquFku1TarpSyUtZTPVksT04tX+qdUXqh3+l6nDOr8JLqv1Xkcdn4EsSfjF9GeIn+xnZjddod63W95SWancnoyt3eXXu2a8Fev6ETivRF57FtmF02h3nX6pSWWmenoys08/ds15vVPMiLqvRF3UxyyWzHbLTWez0kdJRUzN2ONn71VeqqvFVXiqkPWNYWKnVVzn9P3JOmaY8h9pZ6v1/YoDtPUUGD41hVPijXWptvqpnUzoF0c1yNZ4SrzVV6qvPVdSbbB9sFFnVIy0Xd0VJkUTPCYngsq0ROL2f7uqt86cNdIl22fWTGvymf8AlYay0VTUUVXFV0k8lPUQvR8Usbla5jkXVFRU5KRsPT4ahp0eP1uez7+rN+TmSw82XB6vLl8EfSEFMbAts1NmEMWP5FLHT5AxukcnBrK1E6p0R/jb15p1RLnOVysWzFsddi2Z0NGRC+CnB8gee5et1T+Kd/BT0HnuXrdU/infwU0R6o3PofOAAHrR5yAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbndlSz22i2R266U1JGytuD5nVU2nhSbkz2NTXxIjU4e74y2StezD7B+P/AJz8plLKPMNRk5Zdu/8Ak/qd7hJLGr28F9AACGSQa+9six2t1lsl89CMbcX17aR9Q3g50Sse7dXx6Kiaa8uPjNgij+2J9ZNi9+WfFSFlo8nHNr2f/bEHU0niz3Lgx2zWzH7NTWez0kdJRUzN2ONifpVV6qq8VVeKqZAArpScnu+pNSUVsjXjts+smNflM/8AKw1fNoO2z6yY1+Uz/wArDV89B0D2GHx+rON1j2uXw+hzhlkgmZNDI+OWNyOY9jtHNVOKKipyU3r2D3655Lspst4vE/oitlbKySXd0V+5K9iKunXRqa+U0RN2+y/7CFh/CqflEhE/iaMfs0Zbc+L9GSdBk+3ku7b9UWYee5et1T+Kd/BT0HnuXrdU/infwU4mPVHVPofOAAHrR5yAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbu9mH2D8f/OflMpZRWvZh9g/H/zn5TKWUeX6h7Xb/s/qd9h+z1+5fQAAiEgFH9sT6ybF78s+KkLwKP7Yn1k2L35Z8VIWOk+2V+8hal7LP3F4AHTW1VNRUc1ZWTx09PAxZJZZHI1rGomqqqryRCuS35Imvka/dtn1kxr8pn/lYavlqdofadHn97go7ZFuWa2uelPI5uj53O0Rz18ScE0TnpxXnolVnpGj0Tow4QsWz5/mziNSuhdkylB7r9gbt9l/2ELD+FU/KJDSQ3b7L/sIWH8Kp+USED+JvZI/7L6Ml6F7Q/d+qLMPPcvW6p/FO/gp6CnO0Dtfo8Oopsfsr4qrIJ41a/7JlG1yeqd43qi8G+deGiLx2LjWZNqrrW7OmyL4UVuc3yNOwAepnn4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABu72YfYPx/wDOflMpZRRfZ/2iYTYtklltV3ySho62Dv8AvYZHLvN3p5HJrw6oqL5yefTb2bffhbf1l/seb52LfLKsag9uJ9z8TuMTIqVEE5Lou9eBOAQf6bezb78Lb+sv9h9NvZt9+Ft/WX+xE+x5H4b+TJH2mn/NfNE4KP7Yn1k2L35Z8VITj6bezb78Lb+sv9ipe0/nGJZLidnpbFfqSvmhurJZGROVVaxI3pvLw5aqhYaXi3Ry4OUGlv4Mh6hfVLGmlJfM2NrKmno6WWrq544KeFivllkcjWsaiaqqqvJEQ097QG1+ozWsfY7HJJBjsD+K8WurHIvB7k6MReTfOvHRE59oHbBPmdVJYbBLJDj0L/Cdxa6tci8HOTmjEXk3zrx0RKcL3RdG7Ha+9el3Lw/f6e8qNU1Ptd6qn6Pe/H9vqAAdMUQN2+y/7CFh/CqflEhpIXTadsTsV2I2fFMbcqXxyVHompVvCka6eRURuvN6oqLryRF8fKm1vFsyqYV1rnxL4cnzLPSsiGPbKc3y2/VFqdoHbNDiUUuOY3LHPf3t0lmTRzKJF8fRZPEnTmvRF1Hqp56qplqamaSaeV6vkkkcrnPcq6qqqvNVU4zSyTzPmmkfJLI5XPe92rnKvFVVV5qcCVp+n1YVfDDr3vxNGZm2ZU+KXTuQABPIYAAAAAAAAAAAAAAAAAAAAAALExPY1m+R2iG601LS0lLO1Hwuq5txZGrycjURV0XprprzIuVm4+HDjvmorzexuoxrciXDVFt+RXYLb9L5nnt1m+Eu+YYDPNlOT4XZG3i8SW91M6ZsKJBM5zt5yKqcFanDgpEp1zTr5quu6Lk+i3JFmmZdcXOdbSRAwWPimxjMsksFLeqL/h0VNVNV0ST1CtcrddNdEaumuhlPS+Z57dZvhLvmGNmv6bXNwnfFNcnzPsNKzJxUo1vZ+RUgJztA2W5LhFmhut5kt7qeWobTtSCZXu31a5ycFanDRinfh+x/Nsntcd0pKKnpaOZqOhkq5dzvU8bURFXTyqiIvQ3fe+Cqe3dseDfbfflv4GH2DJ7TsuB8XhsV+C2/S+Z57dZvhLvmEavezHJLPmNrxWrfQLX3NqOgVkyrGiaqnhLu8PUr0NdOt6ffJxrui2k317l1MrNNy61vOtru+ZCQW36XzPPbrN8Jd8wel8zz26zfCXfMNX8xaX+PH5mf3RnfhP5FSAnl+2U5PZcks9grJLetXd3ObTLHM5WIrdNd5d3hz8pgM4xa54ffnWW7Op3VLY2yKsD1c3R3LiqITqNRxchxjVNNyW6270ntv8+RGtxL6k3OLWz2fv6mCBYuG7Gs2ya3R3GGmpbfSTN3opK6RY+8b0VGta52i9FVE1M5N2d84jic9lfYZXInBjKiXVfc1jRP3kS3X9Nqm653xTXmSIaVmWR441vYp4GTyawXjGrq+13uhlo6pia7j9FRydHNVODk4c0M/XbNcppcEpszWmilts7O8VsT1dLFGvJ726aI1fGirprx0Js83HgoSlNbTe0efV+RGjjXSckovePXy95DQCV4/gV8veF3LLaJ9GlvtzpGzpJIqSKrGNeuiaaLwcnU2X5FVEVK2WybS5+L6GFVU7XtBbvqRQAkGB4ldc0vbrPZ3UzalsLplWd6tbutVEXiiLx4oZXXQog7LHtFdWfK65WSUILdsj4MvWY5dqfK5sYjp1q7nFUupu6p9X770XRd3yeUsCm2AZ/LA2SRtqgc5NVjkqlVzfd3Wqn6FIeRquFjKLutUeLmt31XiSKcHJubVcG9uvIqgFt+l8zz26zfCXfMIDjmKXfIMs+hm2xxPru8exyufusbua7zlXxcD5Rq+DkRlOq1NRW759F5n23AyanGM4NOXJeZggW36XzPPbrN8Jd8wO7PueI1VSSzuVE5JVO4/wDwRv5i0v8AHj8zb90Z34T+RUgMlkljumO3me0XildS1kCoj2KqKiovFFRU4Kip1QxpcQnGyKnB7p9GQJRcG4yWzQABkYgAAAAAAAAA2e7Sl2yKzzYlasUuFVb31r54WxU0vdb6p3LY268NETeVOenE1hNn+0taMju8+JXPFrdV18lFJPMklNF3nduVYXRuVNF57q8+HA5LXuD7zwu024f6nrdN+Fbb/EvtL4vseTwb7+h069eexDPoa7RH3Tef2zD/AJCMbSLVtYt9jjfm0l0ktrpkRvfVrJ40k0XTVGudoumuiqSf6Je0R9zXn9jQ/wCMlu2Oe9VPZ0oZ8ibK27PlgWqSSJI3I/eXm1ERE/QV1eZk42VRGyNDU5KPob8S37/ImTx6bqLHB2pxTfpbbPyOm51d+puzvh8WNVdTTXOrqaalhWCXu3PV6vRG72qIiKunNdCMfQ12iPum8/tmH/ISetpb5X9nPEqjFqeWruNDU09VEkDEkc10bn+EjV110dpqmi+UjH0S9oj7mvP7Gh/xkXC7Xazsex37Se/ab79eW23cbsjg3h2naerHbg6dCFbR12g26eks+fVlxkjeraqOnmrWzIqIrm7ybrlRF9UnnL92w0eeXe02GXZlVPSgWNXyJSVTIFc1Wt7tUc5U1bprwRfFwKA2mfTEuDqe8Z1Q3Bm4iU0M89G2BvHecjfBaiKvql8Zn8X+njjNClDZaDIaelT1ML6FJWM6+Cj2ru+bQs83DldVj3xnSpw4uT/tvfk9u/ly+JCxshVztrcbHGW3P/3W3T5mYXG+0Qia+ibz+2Yf8hXuW1ud2zJqefJaq7094pG/8vLUyO7xjdV4sd1TVV4ounMsJMl7RGv/AE15X/00P+MkXaRbLUbI8brshghgyHvokka1E1a50TllanPhqjddOGqIYYubbj5dVV8KZKxuP9PquXfv3eJnfjV20TnVKxOHP0+j923eVhh2f5tU5dZqaoyq7ywy18DJGOqnKjmrI1FRU15KhP8AtOZZktiz2hpLNfbhQU77XHI6OCdzGq5ZZUV2iddERPMU1gv17WL3yp/jWll9rn2SLf7zxfHTEzKw8da1RBVrZwny2W3cR6Mi56dbLje/FHvfmYDZhkN8v+1rF33u7VlwdDV6RLUSq/cRUXXTXlyQm+0q00187T9ptlYxslPK2ndKx3J7WNc9Wr5FRunnK12Jeyvjv5Yn8FLK2k3ansfagtNzq3tjp4m07ZXu5Ma9qsVy+REdr5iPqNbr1RxoWz7CeyXjvy2+Jtw5qeEna912kd9/DY8/aQz3JqDN3Y7Z7rV2ujo4Y1X0JKsTpHubvaq5ui6IiommunArGm2gZzT1DJ48vvrnsdvIklfI9q+61yqip5FQsztKYLklVm78itdrqrjRVcEaOWliWR0TmN3VRyN1VE0RF15cfIVbQ4TmFbUsp6bF7w+Ry6JrRvaieVVVERE8q8CVoUdMemVN8Hqrffbr377+Zo1R5n22e3F15dendsXRtvezK9hWO5hVwxsuTHRK57U01R6K16J5FcjXadNCaWXKLZimxfEq28U6zW+qgpqSo4I5GNfGurlavqk4cU8Srz5LC9trG4psJx7EK2aN9ye6JFYxdeDEVz1TyI5zU166nDal/pgxX8z+KecrDGrysXHpf9uV0lHb/Hntt8dy8ldKi+2xeuq4t+/zItty2Xx4+1MpxjSpx6qVHq2Nd9Kbe4oqKnONdeC9NUTxKsh2Rf6Z81/HVfyeIjmw3ae3H3Li+TuSpxyqRY071u+lMrufBeca6rq3prqnXW4LxilrxLY7mdNZJu8t1dFUV0Dd7eSNr4WJuo77JvgaoviVOfNZ2p5WTjQr0/M9J8cHGf8Akk11/wDpd/8AzcbCopulLLx+S4ZcUfB7d3k/+8tRS3Oyh7KEvvbL/MwqMtzsoeyhL72y/wAzDrP4i/8AF3/6sodI9uq96JLszmoIu1FkraxWJLI+rZSb3tveNXh5dxH/ALzszewbeKjLrpNaKuvdbn1L1pFprlFDGkWvgIjFeioqN0RdU568V5lb5xa75dNseRsx6jraquguE07fQjXLJGjZPVpu8U0VU4+NUJXT3/tDQQMhZBfVaxNEV9qje7zudGqr51Oduw7I2V5NM6m3XFONvdsuq28S3ryIuE6bIz2UpNOHf7zruFm7QVBSvqZ58gdGxNV7m5Mld+qx6qvmQ8XZjfLJtfZJO5zpXU1Q56u5q5U46+XUney++7bKrNqCnyWjrXWl6uSpWpt8cDWt3V4o5rGrrrpomvE/bFDQwdrS5soGxtatI58qM5JIsTFd51VdV8qqR7s6xUZOJbGvfs3Levp4bPzN1eNDtab4SntxpbT6+O6ILtXzDOaTaPfqS1368xUkFWrI44Jn7jE0TgiJy5nm2e5JtNu+b2ehbeb/AFEb6yJZ2vkerEiR6K9Xa8N3TXUnEu1qPBNomSWWrs61VHPeX1E08c2kjEdHG3g1U0XTc15prqWfleS3mqwZMh2dw0F7VzFeiPVyqreqtammrk6tXReGnPgar86zFoqplhx2nFKM21tu4rm+XL4szqxY32zsWRLeLbcUnv17ufP4IojtaviftMpGxuarmWqJsiJzR3eSrx8uip+4p89+QXS5Xq81V0u9RJUV08iume/guvLTToiImiJ0RNDwHc6VhvCw68eT3cUkcznZCycidqWybAALAiAAAAAAAAAAsXFNs2b45aIrXT1VLV00DUZClXDvujanJqORUXROmuunJCugRcrCx8yHBfBSXmjfRk248uKqTT8i3PTB557TZvgzvnkfzzatlWZ2Vtouq0MdKkqSubTwq1XKmumqqq8OJAwRKdD06ixWV0xUl0exvs1PLsi4TsbT8yaYFtOyzC6N9DaKuGSje5X+h6mPfY1y81bxRU9xF0JR6YPPPabN8Gd88qMH2/RNPyLHZbTFyfV7CrUsuqKhCxpLzJztA2pZLm9mhtV5jt7aeKobUNWCFzXbyNc1OKuXho9SR+mDzz2mzfBnfPKjB8loenyrjW6Y8Md9lt036/PYLU8uMnNWPd9fh0Lc9MHnntNm+DO+eQLNswyDMbi2tv1ctQ6NFbFG1qNjiReaNanBPd5r1UwANmLpGDiT7SmqMZeKXMxv1DJvjwWTbXvPTa6ya3XOluFOjVmpZmTR7yapvNcipr5NUMztAzG7ZveYbreW0zaiKnbTtSCNWt3Uc5ycFVeOr1I6CXLHqlarnH0lyT70n1I6tmoOtPk+4yOM3mrx+/Ud6oEiWppJO8jSRurdfKmqHszjKbnmF+ders2nbUujbGqQMVrdG8uCqpggHjVO5XOPppbb9+3gFdNV9nv6O++3mWNhu2bNsZt0duhqKW4UsTd2FldEr1janJEc1zXaJ0RVXToZyXtE5w+NzW26wRqqaI5tPLqn6ZFQp0FbboGm2zdk6ItvyJkNVzK4qMbHsjKZRkF4ya7yXW91slXVPTTedwRrejWonBqeRDNXzaDfrxhFDiFWyiS3UPd90rIlSTwGq1NV10Xgq9CIgnvDx2oJwXoc48unuIqyLVxPifpdfP3gmVr2k5Pb8ErMNjmgmttSx0aOmYrpIWO9Uxi66Ii8eaLpqumhDQZX41OQkrYqWzTW/iujPlV1lLbre262+AJBgeW3XC7268WdtM6pdC6FUnYrm7rlRV4IqceCEfBndTC+DrsW8X1RjXZKuSnB7NExx/aNkFkzS5ZbRMoluFxbI2dJIlWNEe9r10Te1Ti1OpK/TB557TZvgzvnlRgr79F0++SlbTFtJLmu5dCVVqWVUtoWNLqWrXbfM/qaZ8MclspXOTTvYaXw2+5vKqfuIZieZXrG8pfktHJHU3GRHpJJVIsm+r/VKvFFVfLqR0GdOk4NMJV11RSlyfLqvMxsz8myUZzm21059DI5NeavIL9WXqvSJKmrk7yRI26N18iaqZjAM+yTCKiZ9kqmd1On1Wnnar4nL0du6po7yoRYEmzEotp7CcE4bbbd3LoaoX2ws7WMmpeJm80ySpyq8uu1bQ2+lq5E+qupIljSVftnIqqiu8vXqYQA2VVQpgoQWyXQwnOVknKT3bAANhgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/9k=" alt="Gestion ISR" style="height:38px;width:auto">757  </div>758  <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end">759    <a class="map-toggle hide-mobile" href="/compte.html" style="text-decoration:none">760      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-3.5 3.6-6 8-6s8 2.5 8 6"/></svg>761      Mon profil762    </a>763    <button class="map-toggle" id="map-toggle-btn" onclick="toggleMap()">764      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>765      Voir la carte766    </button>767    <button class="map-toggle" id="agent-share-top" onclick="window.openAgentSel&&window.openAgentSel()" style="display:none;background:#FB8C00;color:#fff;border-color:#FB8C00">768      🔗 Créer une sélection769    </button>770  </div>771</div>772<!-- CARTE -->773<div id="map-section">774  <div id="map-side">775    <button class="ms-close" onclick="document.body.classList.remove('ms-filters-open')" aria-label="Fermer les filtres">✕</button>776    <div class="ms-h">🔎 Filtrer la carte</div>777    <div class="ms-f"><label>Mot-clé</label><input type="text" id="ms-search" placeholder="Ex. Marais, rue, ville…" autocomplete="off"></div>778    <div class="ms-count" id="ms-count"></div>779    <div class="ms-f"><label>Ville / secteur</label><select id="ms-ville"><option value="">Toutes</option></select></div>780    <div class="ms-f"><label>Type</label><select id="ms-type"><option value="">Tous</option><option>2½</option><option>3½</option><option>4½</option><option>5½</option><option>6½</option><option>Maison</option></select></div>781    <div class="ms-f"><label>Budget ($/mois)</label><div style="display:flex;gap:8px"><input id="ms-pmin" type="number" placeholder="Min" step="50"><input id="ms-pmax" type="number" placeholder="Max" step="50"></div></div>782    <div class="ms-f"><label>Étage</label><select id="ms-etage"><option value="">Tous</option><option value="demi">Demi sous-sol</option><option value="rdc">Rez-de-chaussée</option><option value="1er">1er étage</option><option value="2e">2e étage</option><option value="3e">3e étage</option></select></div>783    <div class="ms-f"><label>Animaux</label><select id="ms-animaux"><option value="">Peu importe</option><option value="Aucun">🚫 Aucun animal</option><option value="Chat">🐈 Chat accepté</option><option value="Petits">🐈🐶 Petits compagnons</option><option value="Animaux">🐈🐶 Animaux acceptés</option></select></div>784    <div class="ms-f"><label>Caractéristiques</label><div class="ms-chk" id="ms-extras"></div></div>785    <div class="ms-f"><label>Rayon autour d'un point</label>786      <div id="ms-radius-row">787        <select id="ms-radius-km"><option value="5">5 km</option><option value="10">10 km</option><option value="20" selected>20 km</option><option value="30">30 km</option><option value="50">50 km</option></select>788        <button type="button" id="ms-radius-btn">📍 Placer</button>789      </div>790      <div style="font-size:11px;font-weight:400;color:#888;margin-top:4px" id="ms-radius-hint">Clique « Placer » puis un point sur la carte.</div>791    </div>792    <button class="btn-reset" id="ms-reset">Réinitialiser</button>793  </div>794  <div id="map"></div>795  <div id="map-results"><div class="mr-grip" aria-hidden="true"></div><button class="mr-sheet-close" onclick="document.body.classList.remove('ms-results-open')" aria-label="Fermer">✕</button><div class="mr-head" id="mr-head">Logements visibles</div><div id="mr-list"></div></div>796  <button class="map-fab" id="fab-filters" onclick="document.body.classList.toggle('ms-filters-open')">🔎 Filtres</button>797  <button class="map-fab" id="fab-fs" onclick="toggleMapFullscreen()" title="Plein écran">⛶</button>798  <button class="map-fab" id="fab-results" onclick="document.body.classList.add('ms-results-open')">📋 Logements</button>799</div>800801<div class="hero">802  <div class="count-badge" id="count-badge">Chargement…</div>803  <h1>Trouvez votre prochain chez-vous</h1>804  <p>Parcourez nos logements disponibles et faites une demande de visite en quelques clics.</p>805</div>806807<!-- MATCHER : association d'appartements sans compte -->808<div id="matcher-panel" class="matcher">809  <div class="matcher-inner">810    <h2>🎯 Votre prochain logement se trouve ici !</h2>811    <div class="matcher-grid">812      <div class="mx-field"><label>Secteur visé</label><select id="mx-secteur" style="width:100%;font-size:14px;padding:9px 10px;border:1px solid #d8dce0;border-radius:8px;background:#fff;color:#333;cursor:pointer"><option value="">Tous les secteurs</option></select></div>813      <div class="mx-field"><label>Type recherché</label><div class="mx-chips" id="mx-type">814        <label><input type="checkbox" value="2½">2½</label><label><input type="checkbox" value="3½">3½</label><label><input type="checkbox" value="4½">4½</label><label><input type="checkbox" value="5½">5½</label><label><input type="checkbox" value="6½">6½</label><label><input type="checkbox" value="Maison">Maison</label>815      </div></div>816      <div class="mx-field"><label>Budget maximal ($/mois)</label><input id="mx-budget" type="number" min="0" step="1" placeholder="ex : 1200"></div>817      <div class="mx-field"><label>Étage</label><div class="mx-chips" id="mx-etage">818        <label><input type="checkbox" value="demi">Demi ss</label><label><input type="checkbox" value="rdc">RDC</label><label><input type="checkbox" value="1er">1er</label><label><input type="checkbox" value="2e">2e</label><label><input type="checkbox" value="3e">3e</label>819      </div></div>820      <div class="mx-field"><label>Animaux</label><div class="mx-chips" id="mx-animaux">821        <label><input type="checkbox" value="Chat">🐈 Chat</label><label><input type="checkbox" value="Chien">🐶 Chien</label>822      </div></div>823      <div class="mx-actions-inline">824        <button class="mx-btn-more" type="button" id="mx-more-btn" onclick="toggleMxMore()">➕ Plus de critères</button>825        <button class="mx-btn-go" onclick="runMatcher()">✨ Rechercher</button>826      </div>827      <div class="mx-more" id="mx-more">828        <label>Caractéristiques souhaitées</label>829        <div class="mx-chips" id="mx-extras">830          <label><input type="checkbox" value="meuble">🛋️ Meublé</label>831          <label><input type="checkbox" value="chauffage">🔥 Chauffage inclus</label>832          <label><input type="checkbox" value="terrain">🌿 Accès au terrain</label>833          <label><input type="checkbox" value="balcon">🔆 Balcon</label>834          <label><input type="checkbox" value="thermopompe">🍃 Thermopompe</label>835          <label><input type="checkbox" value="aspirateur">🧹 Aspirateur central</label>836        </div>837      </div>838    </div>839  </div>840</div>841<div id="match-summary" style="display:none"></div>842843<div id="browse-controls">844<div class="search-wrap" style="background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem">845  <div style="position:relative;max-width:500px">846    <svg style="position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#aaa;pointer-events:none" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>847    <input type="text" id="f-search" placeholder="Rechercher une rue, une ville…" oninput="applyFilters()" style="width:100%;font-size:14px;padding:9px 12px 9px 38px;border:1px solid #ddd;border-radius:10px;color:#1a1a1a;outline:none">848  </div>849</div>850<div class="filters-wrap">851  <div class="filter-group">852    <label>Ville</label>853    <select id="f-ville" onchange="applyFilters()"><option value="">Toutes les villes</option></select>854  </div>855  <div class="filter-group">856    <label>Grandeur</label>857    <select id="f-taille" onchange="applyFilters()"><option value="">Toutes</option><option>2½</option><option>3½</option><option>4½</option><option>5½</option><option>6½</option><option>Maison</option></select>858  </div>859  <div class="filter-group">860    <label>Étage</label>861    <select id="f-etage" onchange="applyFilters()">862      <option value="">Tous</option>863      <option value="demi">Demi sous-sol</option>864      <option value="rdc">Rez-de-chaussée</option>865      <option value="1er">1er étage</option>866      <option value="2e">2e étage</option>867      <option value="2etages-ss">2 étages + SS</option>868      <option value="3e">3e étage</option>869    </select>870  </div>871  <div class="filter-group">872    <label>Disponibilité</label>873    <select id="f-dispo" onchange="applyFilters()">874      <option value="">Toutes</option>875      <option value="maintenant">Dès maintenant</option>876      <option value="aout">Août 2026</option>877    </select>878  </div>879  <div class="filter-group">880    <label>À la une</label>881    <select id="f-etiquette" onchange="applyFilters()">882      <option value="">Toutes</option>883      <option value="Promotion">Promotion</option>884      <option value="Nouveauté">Nouveauté</option>885      <option value="PSL Accepté">PSL Accepté</option>886      <option value="Dernière unité">Dernière unité</option>887      <option value="Internet inclus">Internet inclus</option>888      <option value="Coup de coeur">Coup de coeur</option>889      <option value="Garage">Garage</option>890      <option value="Logement meublé">Logement meublé</option>891    </select>892  </div>893  <div class="filter-group">894    <label>Animaux</label>895    <select id="f-animaux" onchange="applyFilters()">896      <option value="">Peu importe</option>897      <option value="Aucun">🚫 Aucun animal</option>898      <option value="Chat">🐈 Chat accepté</option>899      <option value="Petits">🐈🐶 Petits compagnons</option>900      <option value="Animaux">🐈🐶 Animaux acceptés</option>901    </select>902  </div>903  <div class="price-range">904    <label>Prix ($/mois)</label>905    <div class="price-inputs">906      <input type="number" id="f-pmin" placeholder="Min" step="50" oninput="applyFilters()">907      <span>—</span>908      <input type="number" id="f-pmax" placeholder="Max" step="50" oninput="applyFilters()">909    </div>910  </div>911  <div class="filter-group">912    <label>Trier par</label>913    <select id="f-tri" onchange="applyFilters()">914      <option value="">Par défaut</option>915      <option value="prix-asc">Prix croissant ↑</option>916      <option value="prix-desc">Prix décroissant ↓</option>917      <option value="dispo">Disponibilité</option>918    </select>919  </div>920  <button class="btn-reset" onclick="resetFilters()">Réinitialiser</button>921</div>922<div class="tags-row" id="tags-row">923  <div class="quick-filters" id="quick-filters">924    <span style="font-size:11px;color:#aaa;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-right:4px">À la une :</span>925    <button class="quick-filter-btn" onclick="setEtiquette('Promotion')">🏷️ Promotion</button>926    <button class="quick-filter-btn" onclick="setEtiquette('Nouveauté')">✨ Nouveauté</button>927    <button class="quick-filter-btn" onclick="setEtiquette('PSL Accepté')">🔥 PSL Accepté</button>928    <button class="quick-filter-btn" onclick="setEtiquette('Dernière unité')">⚡ Dernière unité</button>929    <button class="quick-filter-btn" onclick="setEtiquette('Internet inclus')">🛜 Internet inclus</button>930    <button class="quick-filter-btn" onclick="setEtiquette('Coup de coeur')">❤️ Coup de coeur</button>931    <button class="quick-filter-btn" onclick="setEtiquette('Garage')">🚗 Garage</button>932  </div>933</div>934</div><!-- /browse-controls -->935<div class="main">936  <div id="selection-banner" style="display:none"></div>937  <!-- Nouveautés (vue agent uniquement) : annonces des 10 derniers jours + compteur de jours -->938  <section id="agent-new-rail" style="display:none">939    <div class="an-head"><span class="an-title">🆕 Nouveautés <span style="font-weight:400;color:#64748b;font-size:12px">— vue agent</span></span><span class="an-hint">annonces publiées depuis moins de 10 jours · le badge indique depuis combien de jours</span></div>940    <div class="an-scroll" id="an-scroll"></div>941  </section>942  <!-- ⭐ À la une — fiches en vedette (sous la recherche, au-dessus de la liste) -->943  <div id="featured" class="feat" style="display:none">944    <div class="feat-row">945      <button class="feat-arrow" id="feat-prev" aria-label="Précédent">‹</button>946      <div class="feat-track" id="feat-track"></div>947      <button class="feat-arrow" id="feat-next" aria-label="Suivant">›</button>948    </div>949  </div>950  <div class="results-line" id="results-line"></div>951  <div class="grid" id="grid">952    <div class="loading"><div class="loading-spinner"></div>Chargement des logements…</div>953  </div>954</div>955956<!-- NOUS JOINDRE -->957<footer id="contact-section">958  <div class="contact-inner">959    <h2 id="contact-title">Nous joindre</h2>960    <p class="contact-lead" id="contact-lead">Une question sur un logement ? Notre équipe est là pour vous répondre.</p>961    <div class="contact-cards">962      <a class="contact-card" id="cc-email" href="mailto:info@gestion-isr.com">963        <span class="cc-icon" aria-hidden="true">964          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>965        </span>966        <span class="cc-text"><span class="cc-label">Par courriel</span><span class="cc-value" id="cc-email-val">info@gestion-isr.com</span></span>967      </a>968      <a class="contact-card" id="cc-phone" href="tel:+18194721127">969        <span class="cc-icon" aria-hidden="true">970          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>971        </span>972        <span class="cc-text"><span class="cc-label">Par téléphone</span><span class="cc-value" id="cc-phone-val">819-472-1127</span></span>973      </a>974    </div>975    <div style="margin-top:22px;font-size:12.5px;opacity:.85"><a href="/confidentialite" style="color:inherit;text-decoration:underline">Politique de confidentialité</a> · <a href="/compte?tab=alertes" style="color:inherit;text-decoration:underline">Alertes nouveautés</a></div>976  </div>977</footer>978979<!-- Indicateur « accès agent » (visible seulement pour le staff connecté) -->980<div id="staff-badge" style="position:fixed;left:14px;bottom:14px;z-index:1200;background:#E67700;color:#fff;padding:9px 15px;border-radius:30px;font-size:13px;font-weight:700;box-shadow:0 6px 18px rgba(0,0,0,.28);display:none;align-items:center;gap:8px;border:2px solid #fff">👔 Accès agent · infos internes visibles</div>981<!-- Outil de partage (agent) : accessible via le bouton « Créer une sélection » de l'en-tête -->982983<div id="agent-sel-overlay" style="display:none;position:fixed;inset:0;z-index:1500;background:#f5f5f3;flex-direction:column">984  <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;background:#E67700;color:#fff;flex-shrink:0">985    <div style="font-size:16px;font-weight:800">🔗 Sélections partageables</div>986    <button onclick="window.closeAgentSel&&window.closeAgentSel()" style="background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.4);border-radius:8px;padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer">Fermer</button>987  </div>988  <div style="flex-shrink:0;background:#fff;border-bottom:1px solid #e6e6e2;padding:12px 18px">989    <div style="position:relative;max-width:1040px;margin:0 auto">990      <svg style="position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#aaa;pointer-events:none" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>991      <input id="asel-search" type="text" placeholder="Rechercher une rue, une ville, un titre…" oninput="window.agentSelSearchInput&&window.agentSelSearchInput()" style="width:100%;font-size:14px;padding:10px 12px 10px 38px;border:1px solid #d5d5d0;border-radius:10px;box-sizing:border-box;outline:none">992    </div>993  </div>994  <div style="flex:1;overflow-y:auto;padding:16px 18px 32px;max-width:1040px;width:100%;margin:0 auto;box-sizing:border-box">995    <div class="sel-crit">996      <div class="sel-grid">997        <div class="sel-field"><label>Type</label><div class="sel-chips" id="asel-type">998          <label><input type="checkbox" value="2½" onchange="window.agentSelRender&&window.agentSelRender()">2½</label><label><input type="checkbox" value="3½" onchange="window.agentSelRender&&window.agentSelRender()">3½</label><label><input type="checkbox" value="4½" onchange="window.agentSelRender&&window.agentSelRender()">4½</label><label><input type="checkbox" value="5½" onchange="window.agentSelRender&&window.agentSelRender()">5½</label><label><input type="checkbox" value="6½" onchange="window.agentSelRender&&window.agentSelRender()">6½</label><label><input type="checkbox" value="Maison" onchange="window.agentSelRender&&window.agentSelRender()">Maison</label>999        </div></div>1000        <div class="sel-field"><label>Budget maximal ($/mois)</label><input id="asel-budget" type="number" step="1" min="0" placeholder="ex : 1200" oninput="window.agentSelRender&&window.agentSelRender()"></div>1001        <div class="sel-field"><label>Préférence d'étage</label><div class="sel-chips" id="asel-etage">1002          <label><input type="checkbox" value="demi" onchange="window.agentSelRender&&window.agentSelRender()">Demi sous-sol</label><label><input type="checkbox" value="rdc" onchange="window.agentSelRender&&window.agentSelRender()">Rez-de-chaussée</label><label><input type="checkbox" value="1er" onchange="window.agentSelRender&&window.agentSelRender()">1er étage</label><label><input type="checkbox" value="2e" onchange="window.agentSelRender&&window.agentSelRender()">2e étage</label><label><input type="checkbox" value="3e" onchange="window.agentSelRender&&window.agentSelRender()">3e étage</label>1003        </div></div>1004        <div class="sel-field"><label>Animaux</label><div class="sel-chips" id="asel-animaux">1005          <label><input type="checkbox" value="Aucun" onchange="window.agentSelRender&&window.agentSelRender()">Aucun</label><label><input type="checkbox" value="Chat" onchange="window.agentSelRender&&window.agentSelRender()">Chat</label><label><input type="checkbox" value="Chien" onchange="window.agentSelRender&&window.agentSelRender()">Chien</label><label><input type="checkbox" value="Autre" onchange="window.agentSelRender&&window.agentSelRender()">Autre</label>1006        </div></div>1007      </div>1008      <div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">1009        <button class="sel-btn primary" onclick="window.agentSelAll&&window.agentSelAll()">Sélectionner tout</button>1010        <button class="sel-btn" onclick="window.agentSelNone&&window.agentSelNone()">Enlever la sélection</button>1011        <button class="sel-btn" onclick="window.agentSelResetFilters&&window.agentSelResetFilters()">Réinitialiser les filtres</button>1012      </div>1013    </div>1014    <div id="agent-sel-count" class="sel-count" style="margin-top:16px"></div>1015    <div id="agent-sel-list"></div>1016    <div id="agent-sel-share" style="margin-top:12px"></div>1017  </div>1018</div>10191020<!-- MODAL DÉTAIL -->1021<div class="detail-overlay" id="detail-overlay" onclick="closeDetailOutside(event)">1022  <div class="detail-modal" id="detail-modal">1023    <div class="gallery" id="detail-gallery"></div>1024    <div class="detail-body">1025      <div class="detail-header">1026        <div>1027          <div class="detail-ville" id="d-ville"></div>1028          <div class="detail-title" id="d-title"></div>1029        </div>1030        <div style="text-align:right">1031          <button class="d-fav" id="d-fav" type="button" title="Ajouter aux favoris" onclick="window.toggleFavorite&&window.toggleFavorite(this.getAttribute('data-app'),this)"><span class="hc">&#10084;</span></button>1032          <div class="detail-price" id="d-price"></div>1033        </div>1034      </div>1035      <div class="detail-badges" id="d-badges"></div>1036      <button class="btn-staged" id="d-staged-btn" style="display:none">🛋️ Voir meublé virtuellement</button>1037      <div id="d-photo-models-sec" class="d-models-box" style="display:none">1038        <div class="d-models-head">📷 Modèles d'appartement <span>— cliquez pour voir les photos de chaque modèle 👇</span></div>1039        <div id="d-photo-models"></div>1040      </div>1041      <div class="detail-section" id="d-variants-sec" style="display:none">1042        <h3>Modèles disponibles</h3>1043        <div id="d-variants"></div>1044      </div>1045      <div class="detail-section" id="d-building-sec" style="display:none">1046        <h3>🏢 Immeuble — unités <span style="font-size:12px;font-weight:400;color:#8a5a00">(vue agent · clique une unité)</span></h3>1047        <div id="d-building"></div>1048      </div>1049      <div class="detail-section">1050        <h3>Description</h3>1051        <div class="ddt-tabs" id="d-desc-tabs" style="display:none"></div>1052        <div class="detail-desc" id="d-desc"></div>1053      </div>1054      <hr class="detail-divider">1055      <div class="detail-section">1056        <h3>Localisation</h3>1057        <div class="detail-map" id="detail-map"></div>1058      </div>1059      <div id="d-internal-wrap" style="display:none">1060        <hr class="detail-divider">1061        <div class="detail-section">1062          <h3>🔒 Infos internes</h3>1063          <div id="d-internal"></div>1064        </div>1065      </div>1066      <hr class="detail-divider">1067      <div class="contact-btns">1068        <a class="btn-call" id="btn-call" href="tel:+18194721127">1069          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.8a19.79 19.79 0 01-3.07-8.67A2 2 0 012 .18H5a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 14.92z"/></svg>1070          Appeler1071        </a>1072        <a class="btn-email" id="btn-email" href="mailto:info@gestion-isr.com">1073          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 01-2.06 0L2 7"/></svg>1074          Courriel1075        </a>1076      </div>1077      <div id="d-owner-contact" style="display:none"></div>1078      <button class="btn-visite-full" id="btn-visite-full">Formulaire de location</button>1079      <button class="btn-share" id="btn-share" onclick="partagerFiche()">1080        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>1081        Partager1082      </button>1083      <div class="detail-section" id="d-similar-sec" style="display:none">1084        <hr class="detail-divider">1085        <h3>Logements similaires</h3>1086        <div class="sim-grid" id="d-similar"></div>1087      </div>1088      <button class="btn-retour" onclick="closeDetail()">← Retour aux logements</button>1089    </div>1090  </div>1091</div>10921093<!-- Feuille de partage / publication -->1094<div class="share-ov" id="share-overlay" onclick="if(event.target===this)closeShare()">1095  <div class="share-sheet" id="share-sheet">1096    <div class="share-head"><b>Partager</b><button onclick="closeShare()" aria-label="Fermer">✕</button></div>1097    <div class="share-grid" id="share-actions"></div>1098    <div class="share-note" id="share-note"></div>1099  </div>1100</div>11011102<!-- DÉTAIL D'UNE UNITÉ (vue agent) -->1103<div class="unit-overlay" id="unit-overlay" onclick="closeUnitPanel(event)">1104  <div class="unit-modal" id="unit-modal"></div>1105</div>11061107<!-- PLAN DE L'IMMEUBLE EN PLEIN ÉCRAN (grands immeubles) -->1108<div class="bld-full-overlay" id="bld-full-overlay">1109  <div class="bld-full-head"><span>🏢 Plan de l'immeuble <span style="font-weight:400;color:#888;font-size:12px">— glisse pour te déplacer, pince pour zoomer</span></span>1110    <div class="bld-zoom-ctl">1111      <button onclick="bldZoomBy(0.8)" title="Dézoomer" aria-label="Dézoomer">−</button>1112      <button onclick="bldZoomFit()" title="Tout voir" aria-label="Tout voir">⊡</button>1113      <button onclick="bldZoomBy(1.25)" title="Zoomer" aria-label="Zoomer">+</button>1114      <button onclick="closeBuildingFull()" title="Fermer" class="bld-zoom-close">&times;</button>1115    </div>1116  </div>1117  <div class="bld-full-body" id="bld-full-body"></div>1118</div>11191120<!-- LIGHTBOX -->1121<div class="lightbox-overlay" id="lightbox-overlay" onclick="closeLightboxOutside(event)">1122  <button class="lightbox-close" onclick="closeLightbox()">&times;</button>1123  <div class="lb-models" id="lightbox-models" onclick="event.stopPropagation()"></div>1124  <button class="lightbox-prev" id="lb-prev" onclick="lightboxNav(-1)">‹</button>1125  <img class="lightbox-img" id="lightbox-img" src="" alt="">1126  <button class="lightbox-next" id="lb-next" onclick="lightboxNav(1)">›</button>1127  <div class="lightbox-count" id="lightbox-count"></div>1128</div>11291130<!-- MODAL VISITE -->1131<div class="visite-overlay" id="visite-overlay" onclick="closeVisiteOutside(event)">1132  <div class="visite-modal" style="max-width:560px;padding:0;overflow:hidden;border-radius:16px">1133    <div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;background:#fff;border-bottom:1px solid #f0f0ee">1134      <div>1135        <div class="modal-title">Formulaire de location</div>1136        <div class="modal-sub" id="modal-apt-name"></div>1137      </div>1138      <button class="btn-close" onclick="closeVisite()">&times;</button>1139    </div>1140    <form id="visite-form">1141      <div class="vw-steps" id="vw-steps">1142        <div class="vw-st" data-st="1"><div class="vw-dot">1</div><div class="vw-lbl">Coordonnées</div></div>1143        <div class="vw-st" data-st="2"><div class="vw-dot">2</div><div class="vw-lbl">Ma recherche</div></div>1144        <div class="vw-st" data-st="3"><div class="vw-dot">3</div><div class="vw-lbl">Critères</div></div>1145      </div>1146      <div class="vf-msg" id="visite-msg"></div>1147      <div class="vw-body">1148        <section data-vstep="1">1149          <label class="vf-f vf-full"><span>Nom complet *</span><input id="vf-nom" autocomplete="name"></label>1150          <label class="vf-f vf-full"><span>Méthode de communication préférée *</span>1151            <select id="vf-moyen"><option value="" disabled selected>— Choisir —</option><option value="Téléphone">📞 Téléphone</option><option value="Courriel">✉️ Courriel</option><option value="Les deux">📞 ✉️ Les deux</option></select>1152          </label>1153          <label class="vf-f vf-full" id="vf-wrap-tel" style="display:none"><span>Téléphone *</span><input id="vf-tel" type="tel" autocomplete="tel"></label>1154          <label class="vf-f vf-full" id="vf-wrap-courriel" style="display:none"><span>Courriel *</span><input id="vf-courriel" type="email" autocomplete="email"></label>1155        </section>1156        <section data-vstep="2" style="display:none">1157          <div class="vf-f vf-full"><span class="vw-fl">Type recherché * <i>(plusieurs possibles)</i></span><div class="vw-chips" id="vf-types"></div></div>1158          <label class="vf-f vf-full"><span>Budget max ($/mois) <i style="font-weight:400;color:#9aa3ad">(facultatif)</i></span><input id="vf-budget" type="number" min="0" step="1" placeholder="ex : 1300"></label>1159          <label class="vf-f vf-full"><span>Pour quelle date ? *</span><span style="display:flex;gap:8px"><select id="vf-date-mois" style="flex:1"><option value="" disabled selected>Mois</option></select><select id="vf-date-annee" style="width:110px"><option value="" disabled selected>Année</option></select></span></label>1160          <div class="vf-f vf-full"><span class="vw-fl">Animaux *</span><div class="vw-chips" id="vf-animaux"></div></div>1161          <div class="vf-f vf-full"><span class="vw-fl">Croyez-vous passer une enquête de crédit ? *</span><div class="vw-chips" id="vf-enquete"></div></div>1162        </section>1163        <section data-vstep="3" style="display:none">1164          <p class="vw-sub">Facultatif — ça nous aide à mieux vous proposer le logement idéal.</p>1165          <div class="vf-f vf-full"><span class="vw-fl">✨ Caractéristiques souhaitées</span><div class="vw-crit" id="vf-criteres"></div></div>1166          <label class="vf-f vf-full"><span>Message / questions</span><textarea id="vf-message" rows="2" placeholder="Ta motivation, tes questions…"></textarea></label>1167        </section>1168      </div>1169      <div class="vw-nav">1170        <button type="button" class="vw-btn ghost" id="vf-prev" style="display:none">← Précédent</button>1171        <button type="button" class="vw-btn" id="vf-next">Suivant →</button>1172        <button type="submit" class="vw-btn" id="vf-submit" style="display:none">Envoyer ma demande</button>1173      </div>1174    </form>1175    <div id="visite-success" style="display:none;padding:2.5rem 1.5rem;text-align:center">1176      <div style="font-size:52px;line-height:1;margin-bottom:14px">✅</div>1177      <div style="font-size:19px;font-weight:800;color:#E67700;margin-bottom:8px">Demande envoyée&nbsp;!</div>1178      <div style="font-size:14px;color:#555;max-width:340px;margin:0 auto 22px">Merci, nous avons bien reçu ta demande. Notre équipe te contactera sous peu.</div>1179      <button onclick="closeVisite()" style="padding:12px 26px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer">Fermer</button>1180    </div>1181  </div>1182</div>11831184<script>1185// Source de données : Supabase (remplace l'ancien Google Sheet via opensheet)1186const SUPABASE_URL = 'https://jfgkriojazaohgjezijj.supabase.co';1187const SUPABASE_ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpmZ2tyaW9qYXphb2hnamV6aWpqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODEzNDE5NjQsImV4cCI6MjA5NjkxNzk2NH0.VVLfhvpkQ9WgJOhCYW3y6kRZIJbAQXTCANv6mXce1Yw';1188// On ne lit que les annonces actives, via la VUE PUBLIQUE expurgée (sans notes1189// internes : parkings/lockers/memo d'unité, share_code). Voir 145-listings-vue-publique.sql.1190const LISTINGS_URL = `${SUPABASE_URL}/rest/v1/listings_public?select=*&statut=eq.Actif`;1191// Disponibilité d'une unité : seul « Disponible » est offert au public. Les autres1192// statuts (Loué, Départ planifié, En construction, Réservé…) sont considérés non disponibles.1193const NON_DISPO = ['Loué','Départ planifié','En construction','Réservé Gestion ISR','Réservé propriétaire'];1194const U_STATUTS = ['Disponible', ...NON_DISPO];   // pour le sélecteur de statut (vue agent)1195const isAvail = u => !NON_DISPO.includes(u && u.statut);1196// Couleur de tuile par statut. Les rôles internes (staff) voient les couleurs distinctes ;1197// le public voit « vert » pour les statuts réservés / départ planifié (non révélés).1198const UNIT_COLORS_INT = {1199  'Disponible':['#DCF5EB','#16B981'], 'Loué':['#FCE4E4','#E0736C'], 'En construction':['#DCE9FF','#3B82F6'],1200  'Départ planifié':['#EEE1FB','#8B5CF6'], 'Réservé Gestion ISR':['#FFE6CC','#F5871F'], 'Réservé propriétaire':['#FEF9C3','#EAB308']1201};1202const UNIT_COLORS_PUB = {1203  'Disponible':['#DCF5EB','#16B981'], 'Loué':['#FCE4E4','#E0736C'], 'En construction':['#DCE9FF','#3B82F6'],1204  'Départ planifié':['#DCF5EB','#16B981'], 'Réservé Gestion ISR':['#DCF5EB','#16B981'], 'Réservé propriétaire':['#DCF5EB','#16B981']1205};1206function unitColors(statut, priv){ const map = priv ? UNIT_COLORS_INT : UNIT_COLORS_PUB; return map[statut] || map['Disponible']; }1207// Vue « privilégiée » (numéros d'appartement + infos internes) : le staff sur tout,1208// le propriétaire uniquement sur SES immeubles (owner_access, chargés au démarrage).1209window.__ownerPks = window.__ownerPks || new Set();1210// Infos internes RETIRÉES du site public (pour tout le monde). Elles sont gérées dans1211// l'espace agent (« Annonces ») et l'espace propriétaire. Le modal public reste 100 % public.1212function privListing(l){ return false; }1213// Infos internes : staff = table complète (RLS) ; propriétaire = RPC curatif (immeubles possédés) ; candidat = rien.1214async function fetchInternal(pk){1215  if (!pk) return null;1216  if (window.__isStaff){ try { const { data } = await _sb.from('listings_internal').select('*').eq('listing_pk', pk).maybeSingle(); return data; } catch(_){ return null; } }1217  if (window.__ownerPks && window.__ownerPks.has(String(pk))){ try { const { data } = await _sb.rpc('owner_building_internal', { p_pk: pk }); return data; } catch(_){ return null; } }1218  return null;1219}12201221let allListings = [], filtered = [], currentListing = null, currentPhotoIdx = 0;12221223// Échappement HTML — protège contre les injections (XSS) provenant des données du Sheet.1224// À utiliser pour TOUTE donnée du Sheet insérée dans innerHTML ou un attribut HTML.1225function esc(s) {1226  return String(s == null ? '' : s)1227    .replace(/&/g, '&amp;')1228    .replace(/</g, '&lt;')1229    .replace(/>/g, '&gt;')1230    .replace(/"/g, '&quot;')1231    .replace(/'/g, '&#39;');1232}12331234// Redimensionne les photos Supabase à la volée (render/image) — bien plus léger/rapide.1235// Les URLs externes (data:) sont inchangées.1236function simg(url, w, q){1237  if (!url || typeof url !== 'string') return url || '';1238  if (url.indexOf('/storage/v1/object/public/') !== -1)1239    // resize=contain : indispensable, sinon Supabase écrase la largeur sans ajuster la hauteur (image déformée/zoomée)1240    return url.replace('/storage/v1/object/public/', '/storage/v1/render/image/public/') + (url.indexOf('?') !== -1 ? '&' : '?') + 'width=' + w + '&resize=contain&quality=' + (q || 70);1241  return url;1242}1243async function getFolderPhotos(l) {1244  // Photos Supabase uniquement.1245  if (Array.isArray(l.photos) && l.photos.length){1246    // Photo principale (choisie via l'étoile dans l'admin) en tête1247    if (l.main_photo && l.photos.includes(l.main_photo)) return [l.main_photo, ...l.photos.filter(p => p !== l.main_photo)];1248    return l.photos.slice();1249  }1250  return [];1251}12521253function getEtage(l) {1254  return l['Étage'] || l['etage'] || l['étage'] || l['ETAGE'] || l['Etage'] || '';1255}12561257function normalizeEtage(e) {1258  if (!e) return '';1259  const s = e.toLowerCase().trim()1260    .replace(/é/g,'e').replace(/è/g,'e').replace(/ê/g,'e')1261    .replace(/\s+/g,' ');1262  if (1263    s.includes('2 etages + ss') ||1264    s.includes('2 etages ss') ||1265    s.includes('2 etages sous-sol') ||1266    s.includes('2 etages sous sol') ||1267    s.includes('2 etages et sous-sol') ||1268    s.includes('2 etages et sous sol') ||1269    s.includes('deux etages sous-sol') ||1270    s.includes('deux etages sous sol')1271  ) return '2etages-ss';1272  if (s.includes('demi')) return 'demi';1273  if (s.includes('sous-sol') || s.includes('sous sol')) return 'sous-sol';1274  if (s.includes('jardin')) return 'rdj';1275  if (s.includes('rez') || s.includes('chaussee')) return 'rdc';1276  if (s.includes('1er') || s.includes('premier')) return '1er';1277  if (s.includes('2e') || s.includes('deuxieme')) return '2e';1278  if (s.includes('3e') || s.includes('troisieme')) return '3e';1279  return '';1280}12811282function etageLabel(e) {1283  const map = {'sous-sol':'Sous-sol','demi':'Demi sous-sol','rdc':'Rez-de-chaussée','rdj':'Rez-de-jardins','1er':'1er étage','2e':'2e étage','2etages-ss':'2 étages + SS','3e':'3e étage'};1284  return map[e] || e;1285}12861287// ---- Variantes (modèles regroupés dans une même fiche) ----1288function hasVariants(l){ return Array.isArray(l.variants) && l.variants.length > 0; }1289function variantEffPrices(v){ const fl = variantFloors(v).filter(f => Number(f.qte) > 0); if (fl.length) return fl.map(f => { const fp = parseFloat(f.loyer); return isNaN(fp) ? parseFloat(v.loyer) : fp; }).filter(x => !isNaN(x)); const b = parseFloat(v.loyer); return isNaN(b) ? [] : [b]; }1290function variantLoyers(l){ return (l.variants || []).flatMap(variantEffPrices); }1291// Prix des unités : DISPONIBLES d'abord (repli sur toutes si tout est loué). Le prix public1292// « dès… » d'un immeuble doit refléter les unités réellement disponibles, pas les louées.1293// Unités « offertes » au public = tout SAUF « Loué » (un départ planifié / réservé / en1294// construction reste montré avec son prix). Sert aux agrégats publics (prix « dès… », modèles).1295function offeredUnits(l){ if (!Array.isArray(l.units) || !l.units.length) return []; const off = l.units.filter(u => u && u.statut !== 'Loué'); return off.length ? off : l.units; }1296// Modèles de photos réellement offerts = ceux référencés par une unité NON louée.1297function offeredModels(l){1298  const models = Array.isArray(l.photo_variants) ? l.photo_variants : [];1299  if (!models.length) return [];1300  if (!hasUnits(l)) return models;   // fiche sans unités → tous les modèles1301  const labels = new Set(offeredUnits(l).map(u => u && u.photoModel).filter(Boolean));1302  return models.filter(m => m && labels.has(m.label));   // seulement les modèles d'unités non louées1303}1304function unitLoyers(l){1305  if (!Array.isArray(l.units) || !l.units.length) return [];1306  return offeredUnits(l).map(u => parseFloat(u.prix)).filter(x => !isNaN(x) && x > 0);1307}1308function minLoyer(l){ const u = unitLoyers(l); if (u.length) return Math.min(...u); if (hasVariants(l)){ const a = variantLoyers(l); if (a.length) return Math.min(...a); } return parseFloat(l.loyer) || 0; }1309function maxLoyer(l){ const u = unitLoyers(l); if (u.length) return Math.max(...u); if (hasVariants(l)){ const a = variantLoyers(l); if (a.length) return Math.max(...a); } return parseFloat(l.loyer) || 0; }1310function loyerSet(l){ const u = unitLoyers(l); if (u.length) return u; if (hasVariants(l)){ const a = variantLoyers(l); if (a.length) return a; } const x = parseFloat(l.loyer); return isNaN(x) ? [] : [x]; }1311function variantFloors(v){ if (Array.isArray(v.floors) && v.floors.length) return v.floors.filter(f => f && f.etage); if (v.etage) return [{ etage: v.etage, qte: (v.qte != null ? v.qte : 1) }]; return []; }1312function variantQte(v){ const fl = variantFloors(v); if (fl.length) return fl.reduce((s,f) => s + (Number(f.qte) > 0 ? Number(f.qte) : 0), 0); return Number(v.qte) > 0 ? Number(v.qte) : 0; }1313function variantEtages(v){ return [...new Set(variantFloors(v).filter(f => Number(f.qte) > 0).map(f => f.etage))]; }1314// ---- Unités individuelles (modèle immeuble) ----1315function hasUnits(l){ return Array.isArray(l.units) && l.units.length > 0; }1316// Étages des unités DISPONIBLES (repli sur toutes si tout est loué). Source de vérité pour1317// la recherche + le matcher d'un immeuble : le champ « Étage » du haut devient inutile ici.1318function unitEtagesNorm(l){1319  if (!hasUnits(l)) return [];1320  const avail = l.units.filter(u => isAvail(u));1321  const src = avail.length ? avail : l.units;1322  return [...new Set(src.map(u => normalizeEtage(u.etage)).filter(Boolean))];1323}1324function totalUnits(l){ return (l.variants || []).reduce((s,v) => s + variantQte(v), 0); }1325// Nombre de logements RÉELLEMENT disponibles représentés par une carte (cohérent avec le1326// badge affiché sur la carte) : carte éclatée par modèle = son groupe d'unités dispo ;1327// immeuble à unités = unités « Disponible » ; immeuble à variantes = total des variantes ;1328// annonce simple (1 logement) = 1. Sert au décompte du bandeau « X logements disponibles ».1329function cardAvailCount(l){1330  if (l && l._virtual) return Math.max(Number(l._unitCount) || 1, 1);1331  if (hasUnits(l)) return l.units.filter(u => u && u.statut !== 'Loué').length;1332  if (hasVariants(l)) return totalUnits(l);1333  return 1;1334}1335function distinct(arr){ return [...new Set(arr.filter(x => x != null && x !== ''))]; }1336// Grandeurs/types RÉELLEMENT offerts (unités non louées). Source de vérité pour1337// l'affichage ET la recherche : un 3½ loué ne doit pas faire matcher la fiche.1338function offeredTypes(l){1339  if (hasUnits(l)) return distinct(offeredUnits(l).map(u => u.type));1340  if (hasVariants(l)) return distinct(l.variants.map(v => v.grandeur || l.grandeur));1341  return l.grandeur ? [l.grandeur] : [];1342}1343function grandeurDisplay(l){1344  const s = offeredTypes(l);1345  return s.length ? s.join(' · ') : (l.grandeur || '');1346}1347// Un logement ne doit apparaître (carte) que s'il reste au moins une disponibilité NON louée.1348// Immeuble : au moins une unité ≠ « Loué ». Appartement seul : ni étiquette « loué » ni1349// dispo_statut « Loué ». (Réservé / départ planifié restent offerts publiquement.)1350function apptStatutPub(l){ return (l.etiquette || '').toLowerCase() === 'loué' ? 'Loué' : (l.dispo_statut || 'Disponible'); }1351function listingHasAvailability(l){1352  if (hasUnits(l)) return l.units.some(u => u && u.statut !== 'Loué');1353  if (hasVariants(l)) return l.variants.some(v => v && v.statut !== 'Loué');1354  return apptStatutPub(l) !== 'Loué';1355}1356function dispoDisplay(l){1357  if (hasUnits(l)){1358    const off = offeredUnits(l);1359    const anyImm = off.some(u => u.carac && u.carac.dispoImmediate);1360    const dates = distinct(off.map(u => (u.carac && u.carac.dispo) || '').filter(Boolean));1361    if (dates.length > 1 || (anyImm && dates.length)) return 'Plusieurs disponibilités';1362    if (dates.length === 1) return dates[0];1363    if (anyImm) return 'Dès maintenant';1364    return l.date_dispo_affichage || l.date_dispo || 'Dès maintenant';1365  }1366  if (hasVariants(l)){ const s = distinct(l.variants.map(v => v.dispo)); if (s.length > 1) return 'Plusieurs disponibilités'; if (s.length === 1) return s[0]; }1367  return l.date_dispo_affichage || l.date_dispo || '';1368}1369function variantMatchesGrandeur(l, taille){ return offeredTypes(l).includes(taille); }1370function variantMatchesEtage(l, etage){1371  if (hasUnits(l)){ const es = unitEtagesNorm(l); if (es.length) return es.includes(etage); }1372  if (hasVariants(l)) return l.variants.some(v => { const es = variantEtages(v); return es.length ? es.some(e => normalizeEtage(e) === etage) : (normalizeEtage(getEtage(l)) === etage); });1373  return normalizeEtage(getEtage(l)) === etage;1374}1375function variantMatchesDispo(l, dispo){ return hasVariants(l) ? l.variants.some(v => normalizeDispo(v.dispo || l.date_dispo_affichage) === dispo) : (normalizeDispo(l.date_dispo_affichage) === dispo); }1376function dispoOrderIndex(l){ const order = ['maintenant','mars','mai','juin','juillet','aout','']; const ds = hasVariants(l) ? distinct(l.variants.map(v => v.dispo)) : [l.date_dispo_affichage]; let best = 99; (ds.length ? ds : ['']).forEach(d => { const i = order.indexOf(normalizeDispo(d)); if (i !== -1 && i < best) best = i; }); return best; }13771378// ===== MATCHER (association d'appartements, sans compte) =====1379const TIER = { perfect:{rank:0,label:'★ Parfaite',cls:'tier-perfect'}, excellent:{rank:1,label:'Très bonne',cls:'tier-excellent'}, good:{rank:2,label:'Bonne',cls:'tier-good'}, ok:{rank:3,label:'',cls:''} };1380let matchCriteria = null, matchByApp = {};1381function listingEtagesNorm(l){1382  if (hasUnits(l)){ const e = unitEtagesNorm(l); if (e.length) return e; }1383  if (hasVariants(l)){ const out=[]; l.variants.forEach(v => { const es=variantEtages(v); if (es.length) es.forEach(e=>out.push(normalizeEtage(e))); else { const n=normalizeEtage(getEtage(l)); if(n) out.push(n); } }); return [...new Set(out.filter(Boolean))]; }1384  const n=normalizeEtage(getEtage(l)); return n?[n]:[];1385}1386function petAllowed(a, pet){ if(!a) return false; if(a.includes('animaux')||a.includes('tous')) return true; if(pet==='Chat') return a.includes('chat'); if(pet==='Chien') return a.includes('chien'); return a.includes('animaux'); }1387// ---- Critères supplémentaires (caractéristiques) ----1388// `has` lit la donnée structurée d'une unité (unit.carac) ; `txt` est un repli sur le texte de1389// l'annonce, car la majorité des fiches n'ont pas encore de caractéristiques structurées.1390const EXTRA_CRIT = [1391  { key:'meuble',      label:'Meublé',             has:c => !!(c.meuble && c.meuble !== 'Non meublé'),       txt:t => /meubl/.test(t) },1392  { key:'chauffage',   label:'Chauffage inclus',   has:c => /inclus/i.test(String(c.chauffage||'')) && !/non[\s-]*inclus/i.test(String(c.chauffage||'')), txt:t => /chauffage[^.;!?\n]{0,25}inclus/.test(t) },1393  { key:'terrain',     label:'Accès au terrain',   has:c => !!c.terrain,                                     txt:t => /acc[eè]s au terrain/.test(t) },1394  { key:'balcon',      label:'Balcon',             has:c => !!c.balcon,                                      txt:t => /balcon/.test(t) },1395  { key:'thermopompe', label:'Thermopompe',        has:c => /thermopompe/i.test(c.clim || ''),               txt:t => /thermopompe/.test(t) },1396  { key:'aspirateur',  label:'Aspirateur central', has:c => !!c.aspirateur_central,                          txt:t => /aspirateur central/.test(t) },1397  { key:'ascenseur',   label:'Ascenseur',          has:c => !!c.ascenseur,                                   txt:t => /ascenseur/.test(t) },1398];1399// Texte de l'annonce en minuscules, débarrassé des tournures négatives pour éviter les faux positifs1400// (« non meublé » ne doit pas matcher « meublé »).1401function _extraHay(l){1402  const units = Array.isArray(l.units) ? l.units : [];1403  const avail = units.filter(u => isAvail(u));1404  const src = avail.length ? avail : units;1405  return [l.description || '', ...src.map(u => u.caract || '')].join(' \n ').toLowerCase()1406    .replace(/non\s+meubl\w*/g, '')1407    .replace(/chauffage\s+non\s+inclus/g, '')1408    .replace(/(pas|aucun)\s+(de\s+)?balcon/g, '')1409    .replace(/(pas|aucun)\s+(d.)?acc[eè]s au terrain/g, '');1410}1411function listingHasExtra(l, def){1412  if (l.carac && def.has(l.carac)) return true;                   // fiche virtuelle (modèle) : carac directe1413  const units = Array.isArray(l.units) ? l.units : [];1414  const avail = units.filter(u => isAvail(u));1415  const src = avail.length ? avail : units;1416  if (src.some(u => u.carac && def.has(u.carac))) return true;   // donnée structurée = source de vérité1417  return def.txt(_extraHay(l));                                   // repli : texte de l'annonce1418}1419function matchListing(c, l){1420  const chips=[], blocks=[]; let score=0, specified=0, satisfied=0, soft=false;1421  if (c.types.length){ specified++; const gs = offeredTypes(l); const hit = gs.find(g=>c.types.includes(g)); if(hit){ chips.push({txt:'✓ '+hit}); score+=3; satisfied++; } else blocks.push('type'); }1422  if (c.budget!=null && !isNaN(c.budget) && c.budget>0){ specified++; const min=minLoyer(l); if(min<=c.budget){ chips.push({txt:'✓ Dans le budget'}); score+=3; satisfied++; } else if(min<=c.budget*1.1){ chips.push({txt:'≈ Proche du budget',soft:true}); score+=1; satisfied++; soft=true; } else blocks.push('budget'); }1423  if (c.etages.length){ specified++; const es=listingEtagesNorm(l); const hit=es.find(e=>c.etages.includes(e)); if(hit){ chips.push({txt:'✓ Étage'}); score+=1; satisfied++; } else if(es.length){ blocks.push('etage'); chips.push({txt:'✗ Étage',miss:true}); } else { chips.push({txt:'Étage à confirmer',soft:true}); satisfied++; soft=true; } }1424  if (c.pets.length){ specified++; const a=(l.animaux||'').toLowerCase(); if(a.includes('aucun')) blocks.push('animaux'); else if(c.pets.some(p=>petAllowed(a,p))){ chips.push({txt:'✓ Animaux OK'}); score+=1; satisfied++; } else { chips.push({txt:'🐾 À confirmer',soft:true}); satisfied++; soft=true; } }1425  // Caractéristiques : pastille ✗ plutôt qu'un blocage — les données sont encore partielles,1426  // on ne masque donc jamais une annonce, elle est seulement moins bien classée.1427  (c.extras || []).forEach(k => {1428    const def = EXTRA_CRIT.find(x => x.key === k); if (!def) return;1429    specified++;1430    if (listingHasExtra(l, def)){ chips.push({txt:'✓ '+def.label}); score+=1; satisfied++; }1431    else chips.push({txt:'✗ '+def.label, miss:true});1432  });1433  const compatible = blocks.length===0; let tier='ok';1434  if (compatible && specified>0){ if(satisfied===specified&&!soft) tier='perfect'; else if(satisfied===specified) tier='excellent'; else if(satisfied/specified>=0.6) tier='good'; }1435  return { compatible, score, chips, tier };1436}1437function readMatcherForm(){1438  const chips = sel => [...document.querySelectorAll(sel+' input:checked')].map(i=>i.value);1439  const b = document.getElementById('mx-budget').value.trim();1440  const sect = document.getElementById('mx-secteur');1441  return { secteur: sect ? sect.value : '', types: chips('#mx-type'), budget: b===''?null:Number(b), etages: chips('#mx-etage'), pets: chips('#mx-animaux'), extras: chips('#mx-extras') };1442}1443function toggleMxMore(){1444  const p = document.getElementById('mx-more'), b = document.getElementById('mx-more-btn');1445  if (!p || !b) return;1446  const show = !p.classList.contains('show');1447  p.classList.toggle('show', show);1448  b.textContent = show ? '➖ Moins de critères' : '➕ Plus de critères';1449}1450window.toggleMxMore = toggleMxMore;1451function runMatcher(){1452  const c = readMatcherForm();1453  matchCriteria = c;1454  const avail = allListings.filter(l => (l.etiquette||'').toLowerCase() !== 'loué' && (!c.secteur || String(l.ville||'').trim() === c.secteur));1455  const scored = avail.map(l => ({ l, m: matchListing(c, l) })).filter(x => x.m.compatible)1456    .sort((a,b) => (TIER[a.m.tier].rank - TIER[b.m.tier].rank) || (b.m.score - a.m.score) || (minLoyer(a.l) - minLoyer(b.l)));1457  matchByApp = {}; scored.forEach(x => { matchByApp[x.l.ID_app || x.l.id_app] = x.m; });1458  filtered = scored.map(x => x.l);1459  document.getElementById('matcher-panel').style.display = 'none';1460  document.getElementById('browse-controls').style.display = 'none';1461  showMatchSummary(c, filtered);1462  render();1463  document.getElementById('match-summary').scrollIntoView({ behavior:'smooth', block:'start' });1464}1465function editMatcher(){1466  matchCriteria = null; matchByApp = {};1467  document.getElementById('matcher-panel').style.display = '';1468  document.getElementById('browse-controls').style.display = '';1469  document.getElementById('match-summary').style.display = 'none';1470  filtered = [...allListings]; render();1471  document.getElementById('matcher-panel').scrollIntoView({ behavior:'smooth', block:'start' });1472}1473function clearMatcher(){1474  matchCriteria = null; matchByApp = {};1475  document.getElementById('matcher-panel').style.display = '';1476  document.getElementById('browse-controls').style.display = '';1477  document.getElementById('match-summary').style.display = 'none';1478  filtered = [...allListings]; render(); renderTags();1479  window.scrollTo({ top:0, behavior:'smooth' });1480}1481function showMatchSummary(c, list){1482  const parts = [];1483  if (c.secteur) parts.push('📍 ' + c.secteur);1484  if (c.types.length) parts.push(c.types.join(', '));1485  if (c.budget) parts.push('≤ ' + c.budget + ' $');1486  if (c.etages.length) parts.push(c.etages.map(e => etageLabel(e)).join(', '));1487  if (c.pets.length) parts.push('🐾 ' + c.pets.join(', '));1488  if (c.extras && c.extras.length) parts.push(c.extras.map(k => (EXTRA_CRIT.find(x => x.key === k) || {}).label).filter(Boolean).join(', '));1489  const perfectN = list.filter(l => (matchByApp[l.ID_app||l.id_app]||{}).tier === 'perfect').length;1490  const el = document.getElementById('match-summary');1491  el.style.display = 'block';1492  el.innerHTML = `<div class="match-bar">1493    <div style="font-size:14px;color:#E67700"><b>${list.length}</b> logement${list.length>1?'s':''} correspond${list.length>1?'ent':''} à tes critères${perfectN?` — dont <b>${perfectN} parfaitement</b>`:''}${parts.length?`<div style="font-size:12px;color:#3a7d68;margin-top:2px">${esc(parts.join(' · '))}</div>`:''}</div>1494    <div class="mx-sum-actions" style="display:flex;gap:8px">1495      <button class="mx-sum-btn" onclick="editMatcher()">✏️ Modifier mes critères</button>1496      <button class="mx-sum-btn" onclick="clearMatcher()">Voir tous les logements</button>1497    </div>1498  </div>${list.length && (c.types.length||c.budget||c.etages.length||c.pets.length||(c.extras&&c.extras.length)) ? '<div class="mx-legend"><span class="lg lg-p">Parfait</span><span class="lg lg-e">Très bon</span><span class="lg lg-g">Bon</span></div>' : ''}`;1499}15001501// Sélection partagée par l'admin : /?apps=az101,az102 → n'affiche que ces logements1502function applySharedSelection() {1503  const appsParam = new URLSearchParams(location.search).get('apps');1504  if (!appsParam) return;1505  const want = new Set(appsParam.split(',').map(s => normIdApp(s)).filter(Boolean));1506  if (!want.size) return;1507  allListings = allListings.filter(l => {1508    const base = normIdApp(l.id_app || l.ID_app);1509    // cartes d'immeuble éclatées : id_app suffixé « base-N » ou « base-<modèle> »1510    return want.has(base) || [...want].some(w => base.startsWith(w + '-'));1511  });1512  showSelectionBanner(allListings.length);1513}1514function showSelectionBanner(n, title) {1515  const el = document.getElementById('selection-banner');1516  if (!el) return;1517  el.style.display = 'block';1518  const back = '<a href="/" style="font-size:13px;color:#E67700;font-weight:600;text-decoration:none;border:1px solid #FB8C00;border-radius:8px;padding:6px 12px;background:#fff;white-space:nowrap">Voir tous les logements</a>';1519  const lead = title || '📋 Sélection personnalisée';1520  el.innerHTML = `<div style="background:#FFECCF;border:1px solid #FB8C00;border-radius:14px;padding:14px 18px;margin-bottom:18px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">1521    <div style="font-size:15px;color:#E67700">${n ? `<b>${lead}</b> — ${n} logement${n>1?'s':''} disponible${n>1?'s':''}.` : 'Aucun logement disponible pour l\'instant.'}</div>${back}</div>`;1522}1523// Lien propriétaire : /?p=<token> → n'affiche que les logements ACTIFS de ce propriétaire1524async function applyOwnerSelection() {1525  const token = new URLSearchParams(location.search).get('p');1526  if (!token) return;1527  try {1528    const resp = await fetch(`${SUPABASE_URL}/rest/v1/rpc/owner_public_listings`, {1529      method: 'POST',1530      headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json' },1531      body: JSON.stringify({ p_token: token.trim() })1532    });1533    const rows = await resp.json();1534    if (!Array.isArray(rows)) return;1535    const want = new Set(rows.map(r => normIdApp(r.id_app)).filter(Boolean));1536    allListings = allListings.filter(l => {1537      const base = normIdApp(l.id_app || l.ID_app);1538      // cartes d'immeuble éclatées : id_app suffixé « base-N »1539      return want.has(base) || [...want].some(w => base.startsWith(w + '-'));1540    });1541    // Coordonnées du propriétaire (profil) → affichées sur TOUTES ses fiches via le lien.1542    try {1543      const cr = await fetch(`${SUPABASE_URL}/rest/v1/rpc/owner_public_contact`, {1544        method: 'POST',1545        headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json' },1546        body: JSON.stringify({ p_token: token.trim() })1547      });1548      const cd = await cr.json();1549      if (Array.isArray(cd) && cd[0]){ window.__ownerContact = cd[0]; applyOwnerFooter(cd[0]); }1550    } catch (_) {}1551    // Jeton CANONIQUE du propriétaire → pour rattacher les demandes (leads) à SON CRM1552    // (sinon la demande part sans owner_code et atterrit côté admin).1553    try {1554      const tr = await fetch(`${SUPABASE_URL}/rest/v1/rpc/owner_token_from_ref`, {1555        method: 'POST',1556        headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json' },1557        body: JSON.stringify({ p_ref: token.trim() })1558      });1559      const td = await tr.json();1560      window.__ownerCode = (typeof td === 'string' ? td : (Array.isArray(td) ? td[0] : null)) || token.trim();1561    } catch (_) { window.__ownerCode = token.trim(); }1562    showOwnerTopLink();   // pas de grosse bannière : juste un lien discret en haut1563  } catch (_) {}1564}1565// Pied de page « Nous joindre » : sur un lien propriétaire (?p=), on affiche SES coordonnées1566// (nom / entreprise, téléphone, courriel) à la place de celles de Gestion ISR.1567function applyOwnerFooter(oc){1568  if (!oc) return;1569  const nm=(oc.contact_name||'').trim(), ph=(oc.contact_phone||'').trim(), em=(oc.contact_email||'').trim();1570  const lead=document.getElementById('contact-lead');1571  if (lead) lead.textContent = nm1572    ? ('Une question sur un logement ? ' + nm + ' est là pour vous répondre.')1573    : 'Une question sur un logement ? Écrivez-nous, on vous répond rapidement.';1574  const eA=document.getElementById('cc-email'), eV=document.getElementById('cc-email-val');1575  if (eA && eV){ if (em){ eA.href='mailto:'+em; eV.textContent=em; eA.style.display=''; } else eA.style.display='none'; }1576  const pA=document.getElementById('cc-phone'), pV=document.getElementById('cc-phone-val');1577  if (pA && pV){ if (ph){ pA.href='tel:'+ph.replace(/[^\d+]/g,''); pV.textContent=ph; pA.style.display=''; } else pA.style.display='none'; }1578}1579// Lien discret « Voir tous les logements » en haut, quand on est sur un lien propriétaire.1580function showOwnerTopLink(){1581  const actions = document.getElementById('map-toggle-btn') && document.getElementById('map-toggle-btn').parentElement;1582  if (!actions || document.getElementById('owner-alllink')) return;1583  const a = document.createElement('a');1584  a.id = 'owner-alllink';1585  a.href = '/';1586  a.className = 'map-toggle';1587  a.title = 'Voir l’ensemble des logements du site';1588  a.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg> Voir tous les logements';1589  a.style.cssText = 'background:#FB8C00;color:#fff;border-color:#FB8C00;text-decoration:none;white-space:nowrap;order:-1';1590  actions.insertBefore(a, actions.firstChild);1591}1592// Un immeuble (fiche_type immeuble) est éclaté en UNE carte par unité DISPONIBLE : photos du1593// modèle lié (★ en tête, sinon album de l'immeuble), type/prix/étage/caractéristiques de l'unité.1594// Chaque carte est une annonce virtuelle (recherche + matcher). Les unités louées n'apparaissent pas.1595function cleanIdApp(s){ return String(s || '').replace(/\s+/g, '').replace(/½/g,'.5').replace(/¼/g,'.25').replace(/¾/g,'.75'); }1596// Normalise un id_app pour la COMPARAISON des liens partagés : minuscules, sans espaces,1597// fractions converties → un ancien lien « …-3½ » (%C2%BD) correspond au nouvel « …-3.5 ».1598function normIdApp(s){ return String(s || '').toLowerCase().replace(/\s+/g, '').replace(/½/g,'.5').replace(/¼/g,'.25').replace(/¾/g,'.75'); }1599function expandBuildingCards(list){1600  const out = [];1601  (list || []).forEach(l => {1602    const avail = Array.isArray(l.units) ? l.units.map((u, idx) => ({ u, idx })).filter(x => x.u && isAvail(x.u)) : [];1603    const models = Array.isArray(l.photo_variants) ? l.photo_variants : [];1604    // Éclatement en cartes UNIQUEMENT si au moins une unité dispo est liée à un modèle de photo.1605    // Sinon l'immeuble garde son plan (une carte + le visuel des unités par étage).1606    const anyModel = avail.some(x => x.u.photoModel && models.some(m => m && m.label === x.u.photoModel));1607    if (l.fiche_type === 'appartement' || !avail.length || !anyModel){ out.push(l); return; }1608    // Regroupe les unités disponibles PAR MODÈLE de photo : une seule carte par modèle1609    // (plusieurs unités identiques = un seul choix affiché). Les unités sans modèle1610    // gardent une carte chacune.1611    const groups = new Map();1612    avail.forEach(({ u, idx }) => {1613      const hasModel = u.photoModel && models.some(m => m && m.label === u.photoModel);1614      const key = hasModel ? ('m:' + u.photoModel) : ('u:' + idx);1615      if (!groups.has(key)) groups.set(key, { label: hasModel ? u.photoModel : '', items: [] });1616      groups.get(key).items.push({ u, idx });1617    });1618    groups.forEach(g => {1619      const items = g.items, first = items[0].u;1620      const model = g.label ? models.find(m => m && m.label === g.label) : null;1621      const mp = model ? modelPhotos(model) : [];1622      const photos = mp.length ? mp : (Array.isArray(l.photos) && l.photos.length ? l.photos : []);1623      const prices = items.map(x => parseFloat(x.u.prix)).filter(p => !isNaN(p) && p > 0);1624      const minPrix = prices.length ? Math.min(...prices) : (first.prix != null ? first.prix : l.loyer);1625      const types = [...new Set(items.map(x => x.u.type).filter(Boolean))];1626      const typeLabel = types.length === 1 ? types[0] : ((model && model.grandeur) || types.join(' / '));1627      const single = items.length === 1;1628      const vl = Object.assign({}, l);1629      vl._virtual = true; vl._parentPk = l.pk; vl._modelLabel = g.label; vl._baseIdApp = l.id_app || l.ID_app || '';1630      vl._unitIdx = items[0].idx; vl._unitId = single ? (first.id || null) : null;1631      vl._groupUnitIds = items.map(x => x.u.id).filter(Boolean);1632      vl._unitCount = items.length;1633      vl._bldUnits = l.units;   // plan complet de l'immeuble sur la fiche1634      vl.pk = (l.pk != null ? l.pk : (l.id_app || 'x')) + ':' + (g.label ? ('m' + g.label) : ('u' + items[0].idx));1635      vl.id_app = cleanIdApp((l.id_app || l.ID_app || '') + '-' + (g.label || (items[0].idx + 1)));1636      vl.ID_app = vl.id_app;1637      vl.titre = (l.titre || '') + (typeLabel ? ' — ' + typeLabel : '');1638      vl.grandeur = typeLabel || l.grandeur || '';1639      if (minPrix != null) vl.loyer = minPrix;1640      vl.etage = single ? (first.etage || '') : '';1641      vl.units = null; vl.variants = null; vl.photo_variants = null;1642      vl.photos = photos.slice(); vl.main_photo = photos[0] || null;1643      // Meublé virtuellement : rendu propre au modèle si présent, sinon celui de la fiche (repli)1644      vl.staged_photos = (model && Array.isArray(model.staged) && model.staged.length) ? model.staged.slice() : (l.staged_photos || null);1645      vl.carac = single ? (first.carac || null) : null;1646      // Description SCOPÉE aux unités de CETTE carte (le modèle / la grandeur) — jamais1647      // l'agrégat de tout l'immeuble : un 3½ ne doit pas décrire les 4½ de l'immeuble.1648      const groupDesc = (genAggregateAnnonce({ units: items.map(x => x.u), description: l.description }) || '').trim();1649      vl.description = groupDesc || (single ? (first.caract || l.description || '') : (l.description || ''));1650      vl.chambres = first.chambres; vl.sdb = first.sdb;1651      out.push(vl);1652    });1653  });1654  return out;1655}1656async function loadListings() {1657  try {1658    const resp = await fetch(LISTINGS_URL, {1659      headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON }1660    });1661    const json = await resp.json();1662    allListings = (Array.isArray(json) ? json : []).filter(l => l.pk || l.id);1663    // Compat : l'ancien code utilise l.ID_app ; en base la colonne s'appelle id_app1664    allListings.forEach(l => { if (l.ID_app === undefined) l.ID_app = l.id_app; });1665    // Sécurité : retire les entrées nulles de units/variants (évite les plantages sur .prix/.statut)1666    allListings.forEach(l => { if (Array.isArray(l.units)) l.units = l.units.filter(Boolean); if (Array.isArray(l.variants)) l.variants = l.variants.filter(Boolean); });1667    // Fiches confiées à Gestion ISR (pool ISR) → affichées sur le site public comme des annonces Gestion ISR.1668    let isrPub = new Set();1669    try { const r = await fetch(`${SUPABASE_URL}/rest/v1/rpc/public_isr_pks`, { method:'POST', headers:{ apikey:SUPABASE_ANON, Authorization:'Bearer '+SUPABASE_ANON, 'Content-Type':'application/json' }, body:'{}' }); const d = await r.json(); (Array.isArray(d)?d:[]).forEach(x => isrPub.add(typeof x==='string'?x:(x&&x.listing_pk))); } catch(_){}1670    window.__isrPks = isrPub;1671    allListings.forEach(l => { l.__isr = isrPub.has(l.pk); });   // flag conservé après expandBuildingCards1672    // Les fiches de propriétaires ne s'affichent PAS sur le site Gestion ISR, SAUF celles confiées à Gestion ISR.1673    if (!new URLSearchParams(location.search).get('p')) {1674      allListings = allListings.filter(l => !l.created_by_owner || l.__isr);1675    }1676    allListings = expandBuildingCards(allListings);   // une carte par unité disponible (immeubles)1677    applySharedSelection();1678    await applyOwnerSelection();1679    filtered = [...allListings];1680    buildVilleFilter();1681    renderAgentNewRail();1682    render();1683    renderFeatured();   // carrousel « À la une » (une fois, indépendant des filtres)1684    // Lien profond : ?fiche=<réf> ouvre directement la fiche de l'annonce1685    { const fiche = new URLSearchParams(location.search).get('fiche');1686      if (fiche){ const f = String(fiche).toLowerCase();1687        let idx = allListings.findIndex(l => String(l.id_app || l.ID_app || '').toLowerCase() === f);1688        if (idx < 0) idx = allListings.findIndex(l => String(l.id_app || l.ID_app || '').toLowerCase().split('-')[0] === f);1689        if (idx >= 0) openDetail(idx);1690      }1691    }1692  } catch(e) {1693    document.getElementById('grid').innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:4rem;color:#888">Impossible de charger les logements.</div>';1694  }1695}16961697// ⭐ À la une — fiches avec étiquette marketing + disponibles, dédupliquées par immeuble.1698function featuredListings(){1699  const seen = new Set(), out = [];1700  for (const l of (allListings || [])){1701    const et = (l.etiquette || '').trim();1702    if (!et || et.toLowerCase() === 'loué') continue;1703    if (!listingHasAvailability(l)) continue;1704    const bpk = l._virtual ? l._parentPk : l.pk;1705    const key = bpk == null ? ('x' + out.length) : String(bpk);1706    if (seen.has(key)) continue; seen.add(key);1707    out.push(l);1708  }1709  return out;1710}1711let featIdx = 0;1712function renderFeatured(){1713  const sec = document.getElementById('featured'); if (!sec) return;1714  const items = featuredListings();1715  if (!items.length){ sec.style.display = 'none'; return; }1716  sec.style.display = '';1717  const track = document.getElementById('feat-track');1718  { const sub = document.getElementById('feat-sub'); if (sub) sub.textContent = items.length + ' logement' + (items.length > 1 ? 's' : '') + ' en vedette'; }1719  const photoOf = l => { let p = l.main_photo || (Array.isArray(l.photos) && l.photos.length ? l.photos[0] : ''); if (!p && Array.isArray(l.photo_variants)){ const v = l.photo_variants.find(v => v && Array.isArray(v.photos) && v.photos.length); if (v) p = (v.main && v.photos.includes(v.main)) ? v.main : v.photos[0]; } return p; };1720  const photoCount = l => { const s = new Set(); if (l.main_photo) s.add(l.main_photo); (Array.isArray(l.photos) ? l.photos : []).forEach(p => p && s.add(p)); (Array.isArray(l.photo_variants) ? l.photo_variants : []).forEach(v => (v && v.photos || []).forEach(p => p && s.add(p))); return s.size; };1721  track.innerHTML = items.map(l => {1722    const pu = photoOf(l), idx = allListings.indexOf(l);1723    const bg = pu ? `style="background-image:url('${esc(simg(pu, 640, 62))}')"` : '';1724    const price = minLoyer(l);1725    const g = (grandeurDisplay(l) || l.grandeur || '').trim();1726    const titre = (l.titre || '').trim();1727    let dtitre = titre;1728    if (g){1729      const gl = g.toLowerCase();1730      if (titre.toLowerCase().startsWith(gl)) dtitre = titre.slice(g.length).replace(/^\s*[·\-–—]\s*/, '').trim() || titre;1731      else { const ge = g.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); dtitre = titre.replace(new RegExp('[\\s·\\-–—]+' + ge + '\\s*$', 'i'), '').trim() || titre; }1732    }1733    const pc = photoCount(l);1734    return `<div class="feat-card" data-fidx="${idx}">1735      <div class="feat-photo" ${bg}>${pu ? '' : '<span>🏠</span>'}</div>1736      <div class="feat-veil"></div>1737      <span class="feat-badge">${esc(l.etiquette)}</span>${pc > 1 ? `<span class="feat-pcount">📷 ${pc}</span>` : ''}1738      <div class="feat-txt">1739        <div class="feat-info"><div class="feat-ttl">${esc(dtitre)}</div><div class="feat-ville">${esc(l.ville || '')}</div></div>1740        <div class="feat-price">${(hasVariants(l) || hasUnits(l)) ? '<small>dès </small>' : ''}${price != null ? price.toLocaleString('fr-CA') : '—'} $<small>/mois</small></div>1741      </div></div>`;1742  }).join('');1743  const cards = [...track.children];1744  cards.forEach(c => c.addEventListener('click', () => { const idx = +c.getAttribute('data-fidx'); if (idx >= 0) openDetail(idx); }));1745  const cardStep = () => { const c = cards[0]; if (!c) return 266; const gp = parseFloat(getComputedStyle(track).columnGap || getComputedStyle(track).gap) || 14; return c.offsetWidth + gp; };1746  const prev = document.getElementById('feat-prev'), next = document.getElementById('feat-next');1747  if (prev) prev.onclick = () => track.scrollBy({ left: -cardStep() * 2, behavior: 'smooth' });1748  if (next) next.onclick = () => track.scrollBy({ left: cardStep() * 2, behavior: 'smooth' });1749  // Défilement automatique : avance d'une carte toutes les ~5 s, revient au début en fin de liste.1750  clearInterval(window.__featTimer);1751  const auto = () => {1752    const max = track.scrollWidth - track.clientWidth - 4;1753    if (track.scrollLeft >= max) track.scrollTo({ left: 0, behavior: 'smooth' });1754    else track.scrollBy({ left: cardStep(), behavior: 'smooth' });1755  };1756  const start = () => { clearInterval(window.__featTimer); window.__featTimer = setInterval(auto, 9000); };1757  start();1758  track.addEventListener('mouseenter', () => clearInterval(window.__featTimer));1759  track.addEventListener('mouseleave', start);1760  track.addEventListener('touchstart', () => clearInterval(window.__featTimer), { passive: true });1761}1762function buildVilleFilter() {1763  // Seules les villes où il reste au moins un logement DISPONIBLE (non loué) sont proposées.1764  const villes = [...new Set(allListings.filter(listingHasAvailability).map(l => l.ville).filter(Boolean))].sort();1765  const sel = document.getElementById('f-ville');1766  if (sel) villes.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; sel.appendChild(o); });1767  // Dropdown « Secteur visé » du matcher (mêmes secteurs = villes)1768  const ms = document.getElementById('mx-secteur');1769  if (ms) villes.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; ms.appendChild(o); });1770}17711772function normalizeDispo(d) {1773  if (!d) return '';1774  const s = d.toLowerCase();1775  if (s.includes('maintenant') || s.includes('dès')) return 'maintenant';1776  if (s.includes('mars')) return 'mars';1777  if (s.includes('mai')) return 'mai';1778  if (s.includes('juin')) return 'juin';1779  if (s.includes('juillet')) return 'juillet';1780  if (s.includes('août') || s.includes('aout')) return 'aout';1781  return '';1782}17831784let bedFilter = '';1785function grandeurToBedrooms(g){1786  const s = String(g || '');1787  const m = s.match(/(\d+)\s*(?:½|1\/2|\.5)?/);1788  if (m){ const n = parseInt(m[1], 10); if (!isNaN(n) && n >= 3 && n <= 12) return n - 2; }1789  return null;1790}1791function bedroomsOf(l){1792  const nums = new Set();1793  const addNum = v => { const m = String(v == null ? '' : v).match(/\d+/); if (m){ const n = parseInt(m[0], 10); if (!isNaN(n)) nums.add(n); } };1794  const addGrand = g => { const b = grandeurToBedrooms(g); if (b != null) nums.add(b); };1795  // Immeuble à unités : ne compter QUE les unités offertes (non louées).1796  if (hasUnits(l)){ offeredUnits(l).forEach(u => { if (u && u.chambres != null && u.chambres !== '') addNum(u.chambres); else if (u) addGrand(u.type); }); return nums; }1797  if (l.chambres != null && l.chambres !== '') addNum(l.chambres); else addGrand(l.grandeur);1798  if (Array.isArray(l.variants)) l.variants.forEach(v => { if (v && v.chambres != null && v.chambres !== '') addNum(v.chambres); else if (v) addGrand(v.grandeur || v.type); });1799  return nums;1800}1801function isMaison(l){1802  const has = g => /maison/i.test(String(g || ''));1803  if (hasUnits(l)) return offeredUnits(l).some(u => has(u && u.type));1804  if (has(l.grandeur)) return true;1805  if (Array.isArray(l.variants) && l.variants.some(v => has(v && (v.grandeur || v.type)))) return true;1806  return false;1807}1808function matchesBedrooms(l, sel){1809  if (!sel) return true;1810  if (sel === 'maison') return isMaison(l);1811  const nums = bedroomsOf(l);1812  if (!nums.size) return false;1813  if (sel === '4') return [...nums].some(n => n >= 4);1814  return nums.has(parseInt(sel, 10));1815}1816function setBedrooms(v){1817  bedFilter = v;1818  document.querySelectorAll('.bed-btn').forEach(b => b.classList.toggle('active', b.getAttribute('data-bed') === v));1819  applyFilters();1820}1821function etiquetteColor(e){1822  const s = String(e || '').toLowerCase();1823  if (s.includes('promo')) return '#F5871F';1824  if (s.includes('derni')) return '#DC2626';1825  if (s.includes('psl')) return '#7C3AED';1826  if (s.includes('internet')) return '#0EA5E9';1827  if (s.includes('coeur') || s.includes('cœur')) return '#DB2777';1828  if (s.includes('garage')) return '#64748B';1829  if (s.includes('meubl')) return '#0D9488';1830  return '#F59E0B';1831}1832function applyFilters() {1833  const ville = document.getElementById('f-ville').value;1834  const taille = document.getElementById('f-taille').value;1835  const etage = document.getElementById('f-etage').value;1836  const animaux = document.getElementById('f-animaux').value;1837  const dispo = document.getElementById('f-dispo').value;1838  const etiquette = document.getElementById('f-etiquette').value;1839  const search = document.getElementById('f-search').value.toLowerCase().trim();1840  const pmin = parseFloat(document.getElementById('f-pmin').value) || 0;1841  const pmax = parseFloat(document.getElementById('f-pmax').value) || Infinity;1842  filtered = allListings.filter(l => {1843    if (etiquette && (l.etiquette||'').toLowerCase() !== etiquette.toLowerCase()) return false;1844    if (search) {1845      const haystack = ((l.titre||'')+(l.ville||'')+(l.description||'')+(l.ID_app||'')).toLowerCase();1846      if (!haystack.includes(search)) return false;1847    }1848    if (ville && l.ville !== ville) return false;1849    if (bedFilter && !matchesBedrooms(l, bedFilter)) return false;1850    if (taille && !variantMatchesGrandeur(l, taille)) return false;1851    if (etage && !variantMatchesEtage(l, etage)) return false;1852    if (animaux) {1853      const a = (l.animaux || '').toLowerCase();1854      if (animaux === 'Aucun' && !a.includes('aucun')) return false;1855      if (animaux === 'Chat' && !a.includes('chat')) return false;1856      if (animaux === 'Petits' && !a.includes('petit')) return false;1857      if (animaux === 'Animaux' && (!a || a.includes('aucun'))) return false;1858    }1859    if (dispo && !variantMatchesDispo(l, dispo)) return false;1860    const loyers = loyerSet(l);1861    if (loyers.length && !loyers.some(x => x >= pmin && x <= pmax)) return false;1862    return true;1863  });1864  // Tri1865  const tri = document.getElementById('f-tri').value;1866  if (tri === 'prix-asc') {1867    filtered.sort((a,b) => minLoyer(a) - minLoyer(b));1868  } else if (tri === 'prix-desc') {1869    filtered.sort((a,b) => maxLoyer(b) - maxLoyer(a));1870  } else if (tri === 'dispo') {1871    filtered.sort((a,b) => dispoOrderIndex(a) - dispoOrderIndex(b));1872  }1873  render(); renderTags();1874}18751876function resetFilters() {1877  ['f-ville','f-taille','f-etage','f-animaux','f-dispo','f-etiquette','f-tri'].forEach(id => document.getElementById(id).value = '');1878  document.getElementById('f-pmin').value = '';1879  document.getElementById('f-pmax').value = '';1880  document.getElementById('f-search').value = '';1881  bedFilter = '';1882  document.querySelectorAll('.bed-btn').forEach(b => b.classList.toggle('active', b.getAttribute('data-bed') === ''));1883  filtered = [...allListings];1884  render(); renderTags();1885}18861887function renderTags() {1888  const tags = [];1889  const ville = document.getElementById('f-ville').value;1890  const taille = document.getElementById('f-taille').value;1891  const etage = document.getElementById('f-etage').value;1892  const animaux = document.getElementById('f-animaux').value;1893  const dispo = document.getElementById('f-dispo').value;1894  const etiquette = document.getElementById('f-etiquette').value;1895  const pmin = document.getElementById('f-pmin').value;1896  const pmax = document.getElementById('f-pmax').value;1897  const dispoLabels = {'maintenant':'Dès maintenant','mars':'Mars 2026','mai':'Mai 2026','juin':'Juin 2026','juillet':'Juillet 2026','aout':'Août 2026'};1898  if (ville) tags.push({label:'Ville : '+ville, clear:()=>{document.getElementById('f-ville').value='';applyFilters();}});1899  if (bedFilter) tags.push({label: bedFilter==='maison' ? 'Maison' : ((bedFilter==='4'?'4+':bedFilter)+' chambre'+(bedFilter==='1'?'':'s')), clear:()=>setBedrooms('')});1900  if (taille) tags.push({label:taille, clear:()=>{document.getElementById('f-taille').value='';applyFilters();}});1901  if (etage) tags.push({label:etageLabel(etage), clear:()=>{document.getElementById('f-etage').value='';applyFilters();}});1902  if (animaux) tags.push({label:animaux, clear:()=>{document.getElementById('f-animaux').value='';applyFilters();}});1903  if (etiquette) tags.push({label:'À la une : '+etiquette, clear:()=>{document.getElementById('f-etiquette').value='';applyFilters();}});1904  if (dispo) tags.push({label:'Dispo : '+(dispoLabels[dispo]||dispo), clear:()=>{document.getElementById('f-dispo').value='';applyFilters();}});1905  if (pmin||pmax) tags.push({label:'Prix : '+(pmin||'0')+' — '+(pmax||'∞')+' $', clear:()=>{document.getElementById('f-pmin').value='';document.getElementById('f-pmax').value='';applyFilters();}});1906  const row = document.getElementById('tags-row');1907  // Filtrer les tags pour exclure étiquette (géré par les chips)1908  const otherTags = tags.filter(t => !t.label.startsWith('À la une'));1909  1910  // Toujours afficher les chips1911  let html = `<div class="quick-filters" id="quick-filters">1912    <span style="font-size:11px;color:#aaa;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-right:4px">À la une :</span>1913    <button class="quick-filter-btn" onclick="setEtiquette('Promotion')">🏷️ Promotion</button>1914    <button class="quick-filter-btn" onclick="setEtiquette('Nouveauté')">✨ Nouveauté</button>1915    <button class="quick-filter-btn" onclick="setEtiquette('PSL Accepté')">🔥 PSL Accepté</button>1916    <button class="quick-filter-btn" onclick="setEtiquette('Dernière unité')">⚡ Dernière unité</button>1917    <button class="quick-filter-btn" onclick="setEtiquette('Internet inclus')">🛜 Internet inclus</button>1918    <button class="quick-filter-btn" onclick="setEtiquette('Coup de coeur')">❤️ Coup de coeur</button>1919    <button class="quick-filter-btn" onclick="setEtiquette('Garage')">🚗 Garage</button>1920  </div>`;1921  1922  if (otherTags.length) {1923    html += otherTags.map((t,i)=>`<span class="tag">${esc(t.label)}<button onclick="clearTag(${i})">&times;</button></span>`).join('');1924  }1925  1926  row.innerHTML = html;1927  window._tagClears = otherTags.map(t=>t.clear);1928  updateQuickFilters();1929}19301931function clearTag(i){window._tagClears[i]();}19321933function setEtiquette(val) {1934  const sel = document.getElementById('f-etiquette');1935  if (sel.value === val) {1936    sel.value = '';1937  } else {1938    sel.value = val;1939  }1940  applyFilters();1941}19421943function updateQuickFilters() {1944  const val = document.getElementById('f-etiquette').value;1945  document.querySelectorAll('.quick-filter-btn').forEach(btn => {1946    const btnVal = btn.getAttribute('onclick').match(/setEtiquette\('(.+?)'\)/);1947    if (btnVal) {1948      btn.classList.toggle('active', btnVal[1] === val);1949    }1950  });1951}19521953window.refreshHearts = function(){1954  document.querySelectorAll('.card-heart[data-app], .d-fav[data-app]').forEach(h => {1955    h.classList.toggle('active', !!(window.__favs && window.__favs.has(h.getAttribute('data-app'))));1956  });1957};19581959// Nouveautés (vue agent) : annonces créées depuis ≤ 10 jours, avec le nombre de jours affiché.1960function renderAgentNewRail(){1961  const sec = document.getElementById('agent-new-rail'), track = document.getElementById('an-scroll');1962  if (!sec || !track) return;1963  if (!window.__isStaff){ sec.style.display = 'none'; return; }   // agent uniquement1964  const sp = new URLSearchParams(location.search);1965  if (sp.get('apps') || sp.get('p')){ sec.style.display = 'none'; return; }1966  const now = Date.now(); const seenPk = new Set();1967  const items = (allListings || []).map(l => ({ l, days: l.created_at ? Math.floor((now - new Date(l.created_at).getTime()) / 86400000) : 999 }))1968    .filter(({ l, days }) => {1969      if (days < 0 || days > 10) return false;1970      if (String(l.etiquette || '').trim().toLowerCase() === 'loué') return false;1971      const pk = l._virtual ? l._parentPk : l.pk;1972      if (pk == null || seenPk.has(pk)) return false; seenPk.add(pk);1973      return true;1974    }).sort((a, b) => a.days - b.days).slice(0, 20);1975  if (!items.length){ sec.style.display = 'none'; return; }1976  const card = ({ l, days }) => {1977    const id = String(l.id_app || l.ID_app || '');1978    const photo = l.main_photo || (Array.isArray(l.photos) && l.photos.length ? l.photos[0] : '');1979    const mn = minLoyer(l);1980    const meta = [grandeurDisplay(l), l.ville].filter(Boolean).join(' · ');1981    const prix = mn ? ((hasVariants(l) || hasUnits(l)) ? 'dès ' : '') + Number(mn).toLocaleString('fr-CA') + ' $' : '';1982    const img = photo ? `<img class="fr-img" loading="lazy" decoding="async" src="${esc(simg(photo, 420, 62))}" alt="">` : '<div class="fr-img"></div>';1983    const badge = `<span class="fr-days">${days < 1 ? 'Nouveau' : days + 'j'}</span>`;1984    return `<div class="fr-card" data-newapp="${esc(id)}"><div style="position:relative">${img}${badge}</div><div class="fr-body"><div class="fr-t">${esc(l.titre || id)}</div><div class="fr-m">${esc(meta)}</div><div class="fr-p">${esc(prix)}</div></div></div>`;1985  };1986  track.innerHTML = items.map(card).join('');1987  sec.style.display = '';1988  track.querySelectorAll('[data-newapp]').forEach(el => el.addEventListener('click', () => {1989    const id = el.getAttribute('data-newapp');1990    const idx = (allListings || []).findIndex(x => String(x.id_app || x.ID_app || '') === id);1991    if (idx >= 0) openDetail(idx);1992  }));1993}1994async function render() {1995  const g = document.getElementById('grid');1996  const n = filtered.length;1997  if (mapVisible && map) updateMapMarkers();   // la carte suit les filtres / correspondances en direct1998  // Total réel de logements DISPONIBLES = somme des unités dispo de chaque carte1999  // (cohérent avec le badge de chaque carte ; voir cardAvailCount).2000  const units = filtered.reduce((s,l) => s + cardAvailCount(l), 0);2001  document.getElementById('count-badge').textContent = units+' logement'+(units!==1?'s':'')+' disponible'+(units!==1?'s':'');2002  document.getElementById('results-line').textContent = units+' logement'+(units!==1?'s':'')+' trouvé'+(units!==1?'s':'') + (units!==n ? ` · ${n} annonce${n!==1?'s':''}` : '');2003  { const msc = document.getElementById('ms-count'); if (msc){ const nPins = filtered.filter(listingHasAvailability).length; msc.textContent = units + ' logement' + (units!==1?'s':'') + ' · ' + nPins + ' épingle' + (nPins!==1?'s':''); } }2004  if (!n){g.innerHTML='<div style="grid-column:1/-1;text-align:center;padding:4rem;color:#888"><p style="font-size:2rem;margin-bottom:1rem">🔍</p><p>Aucun logement ne correspond.</p></div>';return;}2005  2006  // Tri par défaut prix croissant (sauf en mode correspondances, trié par pertinence)2007  if (!matchCriteria && !document.getElementById('f-tri').value) {2008    filtered.sort((a,b) => minLoyer(a) - minLoyer(b));2009  }20102011  const cards = filtered.map((l, idx) => {2012    // Vignette SYNCHRONE : photos Supabase déjà chargées → aucun appel réseau bloquant.2013    const thumb = (l.main_photo && Array.isArray(l.photos) && l.photos.includes(l.main_photo))2014      ? l.main_photo2015      : (Array.isArray(l.photos) && l.photos.length ? l.photos[0] : null);2016    const loyer = parseFloat(l.loyer)||0;2017    const etageNorm = normalizeEtage(getEtage(l));2018    const etageTag = etageNorm ? `<span class="card-tag ct-etage">${etageLabel(etageNorm)}</span>` : '';2019    // Immeuble à unités : badge « X dispo · Y unités » plutôt qu'un étage top-level trompeur.2020    const nU = hasUnits(l) ? l.units.length : 0;2021    const dispoU = hasUnits(l) ? l.units.filter(u => u && u.statut !== 'Loué').length : 0;2022    const etageCard = hasUnits(l)2023      ? `<span class="card-tag ct-variantes">${dispoU} dispo · ${nU} unité${nU>1?'s':''}</span>`2024      : (l._unitCount > 12025          ? `<span class="card-tag ct-variantes">${l._unitCount} logements disponibles</span>`2026          : (etageNorm ? `<span class="card-tag ct-etage">${etageLabel(etageNorm)}</span>` : ''));2027    const etiqLow = (l.etiquette || '').toLowerCase();2028    const cardClass = etiqLow === 'promotion' ? ' card-promotion'2029      : etiqLow === 'psl accepté' ? ' card-psl'2030      : etiqLow === 'dernière unité' ? ' card-derniere'2031      : etiqLow === 'coup de coeur' ? ' card-coupdecoeur'2032      : '';2033    const mm = matchCriteria ? (matchByApp[l.ID_app || l.id_app] || null) : null;2034    const mTier = (mm && TIER[mm.tier]) ? TIER[mm.tier] : null;2035    const tierCls = (mTier && mTier.cls) ? ' ' + mTier.cls : '';2036    const tierBadge = (mTier && mTier.label) ? `<div class="mx-tier-badge ${mTier.cls}">${mTier.label}</div>` : '';2037    const matchChips = mm ? `<div class="mx-chips-res">${mm.chips.map(c => `<span class="mx-chip ${c.miss?'miss':c.soft?'soft':''}">${esc(c.txt)}</span>`).join('')}</div>` : '';2038    return `<div class="card${cardClass}${tierCls}" onclick="openDetail(${allListings.indexOf(l)})">2039      <div class="card-img">2040        ${thumb?`<img src="${esc(simg(thumb,700,66))}" loading="lazy" alt="${esc(l.titre)}" onerror="this.parentNode.innerHTML='<div class=\\'card-img-placeholder\\'><svg viewBox=\\'0 0 24 24\\' fill=\\'none\\' stroke=\\'currentColor\\' stroke-width=\\'1.2\\'><path d=\\'M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z\\'/><path d=\\'M9 21V12h6v9\\'/></svg></div>'">`:`<div class="card-img-placeholder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"/><path d="M9 21V12h6v9"/></svg></div>`}2041        <div class="badge-row">2042          ${l.etiquette&&l.etiquette.toLowerCase()!=='loué'&&l.etiquette.toLowerCase()!=='nouveauté'?`<span class="badge badge-etiquette" style="background:${etiquetteColor(l.etiquette)}">${l.etiquette.toLowerCase()==='coup de coeur'?'Coup de coeur ❤️':esc(l.etiquette)}</span>`:''}2043        </div>2044        ${l.etiquette&&l.etiquette.toLowerCase()==='loué'?`<div class="ribbon-loue">LOUÉ</div>`:''}2045        ${l.etiquette&&l.etiquette.toLowerCase()==='nouveauté'?`<div class="ribbon-nouveau">NOUVEAUTÉ</div>`:''}2046        ${(Array.isArray(l.staged_photos)&&l.staged_photos.length)?`<button class="card-staged" onclick="event.stopPropagation();window.openStaged&&window.openStaged(${allListings.indexOf(l)})" title="✨ Voir l'appartement en aménagement virtuel">✨</button>`:''}2047        <button class="card-heart${window.__favs&&window.__favs.has(l.ID_app||l.id_app||'')?' active':''}" data-app="${esc(l.ID_app||l.id_app||'')}" onclick="event.stopPropagation();window.toggleFavorite&&window.toggleFavorite(this.getAttribute('data-app'),this)" title="Ajouter aux favoris">&#10084;</button>2048      </div>2049      <div class="card-body">2050        ${tierBadge}2051        <div class="card-ville">📍 ${esc(l.ville)}</div>2052        <div class="card-title">${esc(l.titre)}</div>2053        <div class="card-tags">2054          <span class="card-tag ct-size">${esc(grandeurDisplay(l))}</span>2055          <span class="card-tag ct-date">${esc(dispoDisplay(l))}</span>2056          ${hasVariants(l) ? `<span class="card-tag ct-variantes">${l.variants.length} modèle${l.variants.length>1?'s':''}${totalUnits(l)?` · ${totalUnits(l)} unité${totalUnits(l)>1?'s':''}`:''}</span>` : etageCard}2057        </div>2058        ${(()=>{ const am=offeredModels(l); return am.length?`<button class="card-models-btn" onclick="event.stopPropagation();openVariantPhotos(${allListings.indexOf(l)})">📷 Voir les modèles (${am.length})</button>`:''; })()}2059        ${matchChips}2060        <div class="card-footer">2061          <div class="price">${hasVariants(l)?`<span class="price-from">dès </span>`:''}${minLoyer(l).toLocaleString('fr-CA')} $<span> / mois</span></div>2062          <button class="btn-visite-card" onclick="event.stopPropagation();openVisiteFromCardIdx(${allListings.indexOf(l)})">Demander une visite</button>2063        </div>2064      </div>2065    </div>`;2066  });2067  if (matchCriteria){2068    const counts = {}; filtered.forEach(l => { const t = (matchByApp[l.ID_app||l.id_app]||{}).tier || 'ok'; counts[t] = (counts[t]||0) + 1; });2069    let out = '', last = null;2070    filtered.forEach((l, idx) => {2071      const t = (matchByApp[l.ID_app||l.id_app]||{}).tier || 'ok';2072      if (t !== last){ last = t; out += matchGroupHeader(t, counts[t]); }2073      out += cards[idx];2074    });2075    g.innerHTML = out;2076  } else {2077    g.innerHTML = cards.join('');2078  }2079}2080function matchGroupHeader(tier, n){2081  const info = { perfect:{label:'Correspondances parfaites',color:'#0B7A5E'}, excellent:{label:'Très bonnes correspondances',color:'#FB8C00'}, good:{label:'Bonnes correspondances',color:'#B45309'}, ok:{label:'Autres logements compatibles',color:'#8a909a'} }[tier] || { label:'', color:'#8a909a' };2082  return `<div class="match-group-header" style="--gc:${info.color}"><span>${esc(info.label)}</span><span class="mgh-count">${n}</span></div>`;2083}20842085function renderDetailVariants(l){2086  const sec = document.getElementById('d-variants-sec');2087  const box = document.getElementById('d-variants');2088  if (!hasVariants(l)){ sec.style.display = 'none'; box.innerHTML = ''; return; }2089  const rows = l.variants.map((v, i) => {2090    const label = v.label || String.fromCharCode(65 + i);2091    const grandeur = v.grandeur || l.grandeur || '';2092    const sup = (v.superficie != null && v.superficie !== '') ? Number(v.superficie).toLocaleString('fr-CA') + ' pi²' : '—';2093    const floors = variantFloors(v).filter(f => Number(f.qte) > 0);2094    const et = floors.length2095      ? floors.map(f => { const p = (f.loyer != null && f.loyer !== '' && !isNaN(parseFloat(f.loyer))) ? ' · ' + parseFloat(f.loyer).toLocaleString('fr-CA') + ' $' : ''; return `${etageLabel(normalizeEtage(f.etage)) || esc(f.etage)} <b>×${Number(f.qte)}</b>${p}`; }).join('<br>')2096      : '—';2097    const tot = variantQte(v);2098    const dispo = v.dispo || '—';2099    const eff = variantEffPrices(v);2100    const prix = eff.length ? (Math.min(...eff) === Math.max(...eff) ? Math.min(...eff).toLocaleString('fr-CA') + ' $' : Math.min(...eff).toLocaleString('fr-CA') + '–' + Math.max(...eff).toLocaleString('fr-CA') + ' $') : '—';2101    const st = v.statut === 'Loué' ? 'Loué' : 'Disponible';2102    const stB = `<span style="display:inline-block;font-size:10px;font-weight:700;padding:1px 7px;border-radius:10px;margin-left:6px;${st==='Loué'?'background:#FCE4E4;color:#B42318':'background:#FFECCF;color:#E67700'}">${st}</span>`;2103    return `<tr>2104      <td><span class="v-label">${esc(label)}</span>${grandeur ? esc(grandeur) : ''}${stB}${v.note ? `<div class="v-note">${esc(v.note)}</div>` : ''}</td>2105      <td>${esc(sup)}</td>2106      <td>${et}</td>2107      <td>${esc(dispo)}</td>2108      <td>${tot ? esc(tot) + (tot > 1 ? ' unités' : ' unité') : '—'}</td>2109      <td class="v-price">${esc(prix)}</td>2110    </tr>`;2111  }).join('');2112  box.innerHTML = `<div class="vtable-scroll"><table class="vtable">2113    <thead><tr><th>Modèle</th><th>Superficie</th><th>Étages disponibles</th><th>Disponibilité</th><th>Total</th><th>Loyer</th></tr></thead>2114    <tbody>${rows}</tbody></table></div>`;2115  sec.style.display = '';2116}2117// ---- PHASE 2 — Immeuble & unités (vue agent) ----2118const FLOOR_ORDER = ['Penthouse','5e étage','4e étage','3e étage','2e étage','1er étage','Rez-de-chaussée','Demi sous-sol','Sous-sol'];2119function orderFloorsTop(names){ const idx = n => { const m = String(n).match(/(\d+)\s*(?:er|re|e|ème)?\s*étage/i); if (m) return 6 - (+m[1]); const i = FLOOR_ORDER.indexOf(n); return i < 0 ? 50 : i; }; return [...names].sort((a,b) => idx(a) - idx(b)); }2120// Libellé court d'un immeuble pour les boutons de bascule : le texte entre parenthèses2121// du titre (« … (Immeuble 1635) » → « 1635 »), sinon la référence.2122function groupImmeubleLabel(s){2123  // Priorité au nom d'immeuble saisi (« Immeuble 1655 ») ; sinon repli sur l'ancien2124  // procédé (nom entre parenthèses dans le titre), puis la réf.2125  const nom = s && s.nom_immeuble && String(s.nom_immeuble).trim();2126  if (nom) return nom;2127  const t = String((s && s.titre) || '');2128  const m = t.match(/\(([^)]+)\)/);2129  if (m){ const inner = m[1].replace(/immeuble\s*/i, '').trim(); return inner || m[1].trim(); }2130  return (s && (s.id_app || s.ID_app)) || 'Immeuble';2131}2132// ---- Infobulle au survol d'une « fenêtre » (grain) de tour : prix / dispo / type / modèle ----2133function unitDispoText(l, u){2134  const c = u.carac || {};2135  if (c.dispoImmediate) return 'Dès maintenant';2136  if (c.dispo) return 'Dès ' + c.dispo;2137  return l.date_dispo_affichage || l.date_dispo || 'Dès maintenant';2138}2139function grainTipHtml(l, u){2140  const soldOut = u.statut === 'Loué';2141  const rows = [];2142  const type = [u.type || l.grandeur || '', u.chambres ? '(' + u.chambres + ' chambre' + (Number(u.chambres) > 1 ? 's' : '') + ' à coucher)' : ''].filter(Boolean).join(' ');2143  if (type) rows.push(['Type', type]);2144  if (soldOut){2145    rows.push(['Disponibilité', 'Loué']);2146  } else {2147    const prix = (u.prix != null && u.prix !== '') ? Number(u.prix).toLocaleString('fr-CA') + ' $/mois' : null;2148    if (prix) rows.push(['Prix', prix]);2149    rows.push(['Disponibilité', unitDispoText(l, u)]);2150    if (u.sdb != null && u.sdb !== '') rows.push(['Salles de bain', String(u.sdb)]);2151    if (u.superficie != null && u.superficie !== '') rows.push(['Superficie', u.superficie + ' pi²']);2152    if (u.stationnement != null && u.stationnement !== '') rows.push(['Stationnements', String(u.stationnement)]);2153  }2154  if (u.photoModel){ const m = /mod[eè]le/i.test(u.photoModel) ? u.photoModel : ('Modèle ' + u.photoModel); rows.push(['Modèle', m]); }2155  const head = u.numero ? `<div class="gt-head">🏠 Logement nº ${esc(u.numero)}</div>` : '';2156  return head + rows.map(([k, v]) => `<div class="gt-row"><span class="gt-k">${esc(k)}</span><span class="gt-v">${esc(v)}</span></div>`).join('');2157}2158let _grainTipEl = null;2159function grainTipEl(){ if (!_grainTipEl){ _grainTipEl = document.createElement('div'); _grainTipEl.className = 'd-grain-tip'; document.body.appendChild(_grainTipEl); } return _grainTipEl; }2160function showGrainTip(e, l, u){ const t = grainTipEl(); t.innerHTML = grainTipHtml(l, u); t.classList.add('show'); moveGrainTip(e); }2161function moveGrainTip(e){ const t = _grainTipEl; if (!t) return; const pad = 14; const r = t.getBoundingClientRect(); let x = e.clientX + pad, y = e.clientY + pad; if (x + r.width > window.innerWidth - 6) x = e.clientX - r.width - pad; if (y + r.height > window.innerHeight - 6) y = e.clientY - r.height - pad; t.style.left = Math.max(6, x) + 'px'; t.style.top = Math.max(6, y) + 'px'; }2162function hideGrainTip(){ if (_grainTipEl) _grainTipEl.classList.remove('show'); }2163function renderAgentBuilding(l){2164  const sec = document.getElementById('d-building-sec'), box = document.getElementById('d-building');2165  if (!sec || !box) return;2166  // Plan affiché pour une fiche IMMEUBLE ayant des unités — y compris sur une carte d'appartement2167  // (virtuelle), via les unités de l'immeuble parent (_bldUnits). Une fiche « appartement » jamais.2168  const units = (Array.isArray(l.units) && l.units.length) ? l.units : (Array.isArray(l._bldUnits) ? l._bldUnits : null);2169  if (l.fiche_type === 'appartement' || !units){ sec.style.display = 'none'; return; }2170  const virt = !!l._virtual;2171  const vs = document.getElementById('d-variants-sec'); if (vs) vs.style.display = 'none';   // le visuel remplace le tableau des modèles2172  const h3 = sec.querySelector('h3');2173  if (h3) h3.innerHTML = virt2174    ? '🏢 Appartements de l\'immeuble <span style="font-size:12px;font-weight:400;color:#8a8f96">(simple clic : changer de modèle · double-clic : détails)</span>'2175    : '🏢 Unités disponibles <span style="font-size:12px;font-weight:400;color:#8a8f96">(clique une unité pour les détails)</span>';2176  const byFloor = {}; units.forEach((u,i) => { const f = u.etage || 'Autre'; (byFloor[f] = byFloor[f] || []).push({ u, i }); });2177  const floors = orderFloorsTop(Object.keys(byFloor));2178  const dispo = units.filter(u => u && u.statut !== 'Loué').length;   // offert = tout sauf « Loué »2179  const tileFn = ({ u, i }) => {2180    const loue = !isAvail(u);2181    const [bg, bd] = unitColors(u.statut, privListing(l));2182    // Seul un logement LOUÉ masque ses infos (prix + clic). Les autres statuts — disponible,2183    // départ planifié, en construction, réservés — montrent leurs infos de location au public.2184    const soldOut = u.statut === 'Loué';2185    const prix = (!soldOut && u.prix != null && u.prix !== '') ? Number(u.prix).toLocaleString('fr-CA') + ' $' : '';2186    const staff = privListing(l);   // staff partout, OU propriétaire sur SES immeubles → numéros + couleurs internes2187    // Statut affiché en infobulle : le public ne voit pas « réservé » (montré « Disponible »).2188    const tipStatut = staff ? (u.statut || (loue ? 'Loué' : 'Disponible')) : (u.statut === 'Loué' ? 'Loué' : (u.statut === 'En construction' ? 'En construction' : 'Disponible'));2189    // Le numéro d'appartement n'est visible que pour l'agent ; le candidat voit type + prix.2190    const line1 = staff ? (u.numero || ('App ' + (i + 1))) : (u.type || l.grandeur || 'Logement');2191    const line2 = staff ? [u.type || l.grandeur || '', prix].filter(Boolean).join(' · ') : prix;2192    const tip = [staff ? (u.numero || '') : '', u.type || '', prix, tipStatut].filter(Boolean).join(' · ');2193    // Sur une carte virtuelle : seuls les appartements DISPONIBLES sont cliquables (ils ont une fiche).2194    // Le staff peut TOUJOURS cliquer (pour changer le statut, même un logement loué).2195    const locked = staff ? false : soldOut;2196    // Sur une carte virtuelle : toutes les unités du MODÈLE ouvert sont marquées « courant ».2197    const cur = virt && (l._modelLabel ? (u.photoModel === l._modelLabel) : (i === l._unitIdx));2198    return `<div class="d-unit mini${locked?' d-unit-locked':''}${cur?' d-unit-cur':''}" data-uidx="${i}" title="${esc(tip)}${u.statut === 'Loué' ? ' — déjà loué' : ''}" style="background:${bg};border-color:${bd}">2199      <div class="mn">${esc(line1)}</div>2200      <div class="mm">${line2 ? esc(line2) : '&nbsp;'}</div>2201    </div>`;2202  };2203  const POS_ORDER = ['Gauche','Avant gauche','Arrière gauche','Centre','Avant','Arrière','Droite','Avant droit','Arrière droit'];2204  const posRank = p => { const k = POS_ORDER.indexOf(p); return k < 0 ? 50 : k; };2205  const isTour = (l.batiment_style === 'tour') || (l._bldStyle === 'tour');2206  // === Tour d'habitation : plan compact « fenêtres » dans un contour d'immeuble ===2207  const grainFn = ({ u, i }) => {2208    const soldOut = u.statut === 'Loué';2209    const [bg, bd] = unitColors(u.statut, privListing(l));2210    const staff = privListing(l);2211    const tipStatut = staff ? (u.statut || (soldOut ? 'Loué' : 'Disponible')) : (soldOut ? 'Loué' : (u.statut === 'En construction' ? 'En construction' : 'Disponible'));2212    const prix = (!soldOut && u.prix != null && u.prix !== '') ? Number(u.prix).toLocaleString('fr-CA') + ' $' : '';2213    const tip = [staff ? (u.numero || '') : '', u.type || l.grandeur || '', prix, tipStatut].filter(Boolean).join(' · ');2214    const locked = staff ? false : soldOut;2215    return `<span class="d-grain${locked ? ' d-unit-locked' : ''}" data-uidx="${i}" aria-label="${esc(tip)}" style="background:${bg};border-color:${bd}"></span>`;2216  };2217  const tourFloorsHtml = () => floors.map(f => {2218    byFloor[f].sort((a,b) => String(a.u.numero||'').localeCompare(String(b.u.numero||''),'fr',{numeric:true}));2219    return `<div class="d-floor-tour"><div class="d-flbl-tour">${esc(f)}</div><div class="d-grains">${byFloor[f].map(grainFn).join('')}</div></div>`;2220  }).join('');2221  const floorsHtml = isTour ? tourFloorsHtml() : floors.map(f => {2222    // Ordre des tuiles : ordre manuel « ord » (glisser-déposer admin) sinon par numéro.2223    const useOrd = byFloor[f].some(c => c.u && c.u.ord != null);2224    byFloor[f].sort((a,b) => useOrd2225      ? ((a.u.ord!=null?a.u.ord:1e9) - (b.u.ord!=null?b.u.ord:1e9))2226      : String(a.u.numero||'').localeCompare(String(b.u.numero||''),'fr',{numeric:true}));2227    // Regroupe par position (Avant/Arrière…) si au moins une unité de l'étage en a une.2228    const groups = []; const gi = {};2229    byFloor[f].forEach(cell => { const p = (cell.u.position || ''); if (!(p in gi)){ gi[p] = groups.length; groups.push({ pos: p, cells: [] }); } groups[gi[p]].cells.push(cell); });2230    groups.sort((a,b) => posRank(a.pos) - posRank(b.pos));   // positions de gauche à droite2231    const anyPos = groups.some(g => g.pos);2232    const inner = anyPos2233      ? groups.map(g => `<div class="d-posgroup">${g.pos ? `<div class="d-poslbl">${esc(g.pos)}</div>` : ''}<div class="d-postiles">${g.cells.map(tileFn).join('')}</div></div>`).join('')2234      : byFloor[f].map(tileFn).join('');2235    return `<div class="d-bld-floor"><div class="d-floor-lbl">${esc(f)}</div><div class="d-units compact${anyPos?' d-hasgroups':''}">${inner}</div></div>`;2236  }).join('');2237  const bigBtn = units.length > 8 ? `<button class="d-bld-expand" id="d-bld-expand">🔍 Voir le plan en grand</button>` : '';2238  // Bascule entre immeubles liés (même valeur groupe_immeuble) — boutons au-dessus du plan2239  let switchHtml = '';2240  const grp = String(l.groupe_immeuble || '').trim();2241  if (grp){2242    const curPk = l._virtual ? l._parentPk : l.pk;2243    // On regroupe par immeuble PARENT (une entrée par immeuble), qu'il soit éclaté (cartes2244    // virtuelles) ou affiché en plan unique. Les cartes virtuelles héritent de groupe_immeuble.2245    const seenPk = new Set(); const uniq = [];2246    (allListings || []).forEach(x => {2247      if (String(x.groupe_immeuble || '').trim() !== grp) return;2248      const pk = x._virtual ? x._parentPk : x.pk;2249      if (pk == null || seenPk.has(pk)) return;2250      const hasUnits = (Array.isArray(x.units) && x.units.length) || (Array.isArray(x._bldUnits) && x._bldUnits.length);2251      if (!hasUnits) return;2252      seenPk.add(pk); uniq.push({ pk, titre: x.titre, id_app: x.id_app, ID_app: x.ID_app, nom_immeuble: x.nom_immeuble });2253    });2254    uniq.sort((a,b) => String(groupImmeubleLabel(a)).localeCompare(String(groupImmeubleLabel(b)), 'fr', { numeric:true }));2255    if (uniq.length > 1){2256      switchHtml = `<div class="d-bld-switch"><span class="d-switch-seg">` + uniq.map(s => {2257        const cur = String(s.pk) === String(curPk);2258        return `<button type="button" class="d-switch-btn${cur?' cur':''}" data-switchpk="${esc(String(s.pk))}"${cur?' disabled':''} title="${esc(s.titre||'')}">🏢&nbsp;${esc(groupImmeubleLabel(s))}</button>`;2259      }).join('') + `</span></div>`;2260    }2261  }2262  const tourLegend = isTour ? (() => {2263    const priv = privListing(l);2264    const statuts = priv ? ['Disponible','Loué','En construction','Départ planifié','Réservé Gestion ISR','Réservé propriétaire']2265                         : ['Disponible','Loué'].concat(units.some(u => u && u.statut === 'En construction') ? ['En construction'] : []);2266    return `<div class="d-legend-tour">${statuts.map(s => { const [bg,bd] = unitColors(s, priv); return `<span class="d-leg"><span class="d-leg-dot" style="background:${bg};border-color:${bd}"></span>${esc(s)}</span>`; }).join('')}</div>`;2267  })() : '';2268  const tourSkin = (() => { const s = String(l.batiment_skin || 'A').toUpperCase(); return /^[A-E]$/.test(s) ? s : 'A'; })();2269  const tourW = (() => { let mx = 1; Object.values(byFloor).forEach(a => { if (a.length > mx) mx = a.length; }); mx = Math.min(mx, 15);2270    // largeur = libellé(64) + espaceur(64) + padding(16) + 2 gaps flex(16) + bordure(8) + marge(6) + fenêtres2271    return 174 + mx * 16 + Math.max(0, mx - 1) * 5; })();2272  const bldBlock = isTour2273    ? `<div class="d-bld d-bld-tour sk-${tourSkin}" style="width:${tourW}px;max-width:100%"><div class="d-tour-roof"></div><div class="d-tour-body">${floorsHtml}</div></div>`2274    : `<div class="d-bld">${floorsHtml}</div>`;2275  // Tour : met en avant les modèles de logements (Modèle A, B…) + boutons Extérieur / Commodités,2276  // tout en gardant le plan des logements comme élément principal.2277  let tourExtras = '';2278  if (isTour){2279    const mods = (Array.isArray(l.photo_variants) ? l.photo_variants : []).map((v, oi) => ({ v, oi })).filter(x => x.v && Array.isArray(x.v.photos) && x.v.photos.length);2280    const ext = (Array.isArray(l.photos_ext) ? l.photos_ext : []).filter(Boolean);2281    const comm = (Array.isArray(l.photos_communs) ? l.photos_communs : []).filter(Boolean);2282    let modsHtml = '';2283    if (mods.length){2284      modsHtml = `<div class="tbld-models"><div class="tbld-x-lbl">🏠 Exemples de logements <span>— cliquez pour voir les photos</span></div><div class="tbld-models-row">` +2285        mods.map(({ v, oi }, mi) => { const cover = (v.main && v.photos.includes(v.main)) ? v.main : v.photos[0]; const label = /mod[eè]le/i.test(v.label || '') ? v.label : ('Modèle ' + (v.label || String.fromCharCode(65 + mi))); const sub = [v.grandeur || '', v.photos.length + ' 📷'].filter(Boolean).join(' · ');2286          return `<button type="button" class="tbld-model" data-model="${oi}" style="background-image:url('${esc(simg(cover, 500, 62))}')"><span class="tbld-model-cap"><b>${esc(label)}</b><span>${esc(sub)}</span></span></button>`; }).join('') +2287        `</div></div>`;2288    }2289    const btns = [];2290    if (ext.length) btns.push(`<button type="button" class="tbld-x-btn" data-extra="ext">📸 Extérieur <span>${ext.length}</span></button>`);2291    if (comm.length) btns.push(`<button type="button" class="tbld-x-btn" data-extra="comm">🛋️ Commodités <span>${comm.length}</span></button>`);2292    tourExtras = modsHtml + (btns.length ? `<div class="tbld-x-row">${btns.join('')}</div>` : '');2293  }2294  box.innerHTML = switchHtml + `<div class="d-bld-meta">${units.length} unité${units.length>1?'s':''} · <b style="color:#E67700">${dispo} disponible${dispo>1?'s':''}</b> · ${units.length-dispo} loué${(units.length-dispo)>1?'s':''}</div>${tourExtras}${tourLegend}${bigBtn}${bldBlock}`;2295  // Ouvre le panneau d'information de l'unité (double-clic sur les cartes à modèles).2296  const openUnit = (i, u) => {2297    if (privListing(l)){ if (u) openUnitPanel(l, u); return; }   // staff/propriétaire : toujours (même loué)2298    if (u && u.statut === 'Loué') return;                          // public : détails cachés seulement pour un logement LOUÉ2299    openUnitPanel(l, u);2300  };2301  // Simple clic sur une carte à modèles : bascule vers le modèle de l'unité cliquée.2302  const switchModel = (i, u) => {2303    if (!virt || !u || (!privListing(l) && u.statut === 'Loué')) return false;2304    const sameModel = l._modelLabel ? (u.photoModel === l._modelLabel) : (i === l._unitIdx);2305    if (sameModel) return false;                                   // déjà le modèle affiché2306    const sib = allListings.find(x => x._virtual && x._parentPk === l._parentPk &&2307      ((u.photoModel && x._modelLabel) ? x._modelLabel === u.photoModel : x._unitIdx === i));2308    if (sib){ openDetail(allListings.indexOf(sib)); return true; }2309    return false;2310  };2311  const wireUnitClicks = root => root.querySelectorAll('.d-unit, .d-grain').forEach(el => {2312    const i = +el.getAttribute('data-uidx'), u = units[i];2313    el.addEventListener('click', () => {2314      hideGrainTip();2315      // Cartes à modèles : simple clic = bascule de modèle ; les détails s'ouvrent au double-clic.2316      // Cartes simples (sans modèles) : simple clic ouvre directement (comportement inchangé).2317      if (virt){ switchModel(i, u); return; }2318      openUnit(i, u);   // ouvre la fiche complète de l'appartement (panneau d'unité)2319    });2320    el.addEventListener('dblclick', () => openUnit(i, u));2321    // Survol d'une « fenêtre » de tour : infobulle prix / dispo / type / modèle.2322    if (u && el.classList.contains('d-grain')){2323      el.addEventListener('mouseenter', e => showGrainTip(e, l, u));2324      el.addEventListener('mousemove', moveGrainTip);2325      el.addEventListener('mouseleave', hideGrainTip);2326    }2327  });2328  wireUnitClicks(box);2329  // Tour : ouvre la galerie d'un modèle de logement, ou des photos extérieur / commodités.2330  box.querySelectorAll('[data-model]').forEach(b => b.addEventListener('click', () => {2331    const m = (l.photo_variants || [])[+b.getAttribute('data-model')];2332    if (m && Array.isArray(m.photos) && m.photos.length) openLightbox(m.photos.filter(Boolean), 0);2333  }));2334  box.querySelectorAll('[data-extra]').forEach(b => b.addEventListener('click', () => {2335    const arr = (b.getAttribute('data-extra') === 'ext' ? l.photos_ext : l.photos_communs) || [];2336    const clean = arr.filter(Boolean);2337    if (clean.length) openLightbox(clean, 0);2338  }));2339  box.querySelectorAll('[data-switchpk]').forEach(b => b.addEventListener('click', () => {2340    const pk = b.getAttribute('data-switchpk');2341    let idx = (allListings || []).findIndex(x => String(x.pk) === String(pk) && !x._virtual);2342    if (idx < 0) idx = (allListings || []).findIndex(x => String(x._parentPk) === String(pk));   // immeuble éclaté → 1re carte2343    if (idx >= 0) openDetail(idx);2344  }));2345  const eb = box.querySelector('#d-bld-expand'); if (eb) eb.addEventListener('click', () => openBuildingFull(l, floorsHtml, wireUnitClicks, isTour, tourSkin, tourW));2346  sec.style.display = '';2347}2348let _bldZoom = 1;2349function _bldEl(){ const b = document.getElementById('bld-full-body'); return b ? b.querySelector('.d-bld') : null; }2350function bldApplyZoom(){ const el = _bldEl(); if (el) el.style.zoom = _bldZoom; }2351function bldZoomBy(f){ _bldZoom = Math.min(1.6, Math.max(0.2, _bldZoom * f)); bldApplyZoom(); }2352window.bldZoomBy = bldZoomBy;2353function bldZoomFit(){2354  const body = document.getElementById('bld-full-body'), el = _bldEl();2355  if (!body || !el) return;2356  el.style.zoom = 1;2357  const cw = body.clientWidth - 28;               // largeur dispo (padding 14px x2)2358  const w = el.scrollWidth;2359  if (cw <= 0 || w <= 0) return;                   // pas encore mesurable (mobile) : le fallback réessaiera2360  _bldZoom = (w > cw) ? Math.max(0.2, cw / w) : 1;  // rentre à l'écran, sinon 100 %2361  bldApplyZoom();2362  body.scrollLeft = 0; body.scrollTop = 0;2363}2364window.bldZoomFit = bldZoomFit;2365function openBuildingFull(l, floorsHtml, wireUnitClicks, isTour, tourSkin, tourW){2366  const ov = document.getElementById('bld-full-overlay'), body = document.getElementById('bld-full-body');2367  if (!ov || !body) return;2368  body.innerHTML = isTour2369    ? `<div class="d-bld d-bld-tour sk-${tourSkin||'A'}" style="width:${tourW||520}px;max-width:100%;margin:0 auto"><div class="d-tour-roof"></div><div class="d-tour-body">${floorsHtml}</div></div>`2370    : `<div class="d-bld">${floorsHtml}</div>`;2371  wireUnitClicks(body);2372  _bldZoom = 1; bldApplyZoom();2373  ov.classList.add('open'); ovOpened();2374  // Ajuste une fois la mise en page faite. Sur mobile le double rAF peut mesurer trop tôt2375  // (scrollWidth pas prêt) → fallbacks temporisés qui garantissent l'auto-zoom.2376  requestAnimationFrame(() => { requestAnimationFrame(bldZoomFit); });2377  setTimeout(bldZoomFit, 120);2378  setTimeout(bldZoomFit, 350);2379}2380function closeBuildingFull(){ const ov = document.getElementById('bld-full-overlay'); if (ov && ov.classList.contains('open')){ ov.classList.remove('open'); ovClosedUI(); } }2381window.closeBuildingFull = closeBuildingFull;2382// Glisser pour déplacer le plan (souris) — le tactile défile déjà nativement2383(function(){2384  const body = document.getElementById('bld-full-body'); if (!body) return;2385  let down = false, sx = 0, sy = 0, sl = 0, st = 0;2386  body.addEventListener('mousedown', e => { if (e.target.closest('.d-unit, .d-grain')) return; down = true; sx = e.clientX; sy = e.clientY; sl = body.scrollLeft; st = body.scrollTop; body.classList.add('grabbing'); });2387  window.addEventListener('mousemove', e => { if (!down) return; body.scrollLeft = sl - (e.clientX - sx); body.scrollTop = st - (e.clientY - sy); });2388  window.addEventListener('mouseup', () => { down = false; body.classList.remove('grabbing'); });2389  // Clic sur un appartement en plein écran → ferme le plan plein écran pour que son panneau2390  // d'infos s'ouvre AU PREMIER PLAN (sinon il s'ouvrait derrière le plan plein écran).2391  body.addEventListener('click', e => { const t = e.target.closest('.d-unit, .d-grain'); if (t && !t.classList.contains('d-unit-locked')) closeBuildingFull(); });2392  // Molette + Ctrl (souris) et pincer (tactile) pour zoomer2393  body.addEventListener('wheel', e => { if (!e.ctrlKey) return; e.preventDefault(); bldZoomBy(e.deltaY < 0 ? 1.1 : 0.9); }, { passive:false });2394  let pinch = null;2395  const dist = t => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);2396  body.addEventListener('touchstart', e => { if (e.touches.length === 2){ pinch = { d: dist(e.touches), z: _bldZoom }; } }, { passive:true });2397  body.addEventListener('touchmove', e => {2398    if (pinch && e.touches.length === 2){2399      e.preventDefault();2400      const r = dist(e.touches) / pinch.d;2401      _bldZoom = Math.min(1.6, Math.max(0.2, pinch.z * r));2402      bldApplyZoom();2403    }2404  }, { passive:false });2405  body.addEventListener('touchend', e => { if (e.touches.length < 2) pinch = null; });2406})();2407// Libellés des notes internes par unité (vue agent). Mêmes clés que l'admin.2408const UNIT_INT_ORDER = ['locataire_nom','locataire_tel'];   // par logement : locataire en place (nom + téléphone)2409const UNIT_INT_LABELS = { adresse_exacte:'Adresse exacte', directive_acces:'Directive d\'accès', loc_boite_lettres:'Boîte aux lettres', loc_cabanon:'Cabanon', borne_electrique:'Borne électrique', locataire_nom:'Locataire — nom', locataire_tel:'Locataire — téléphone', locataire_courriel:'Locataire — courriel', cout_electricite:'Coûts électricité (an)', tolerance_animaux:'Tolérance animaux', flexibilite_prix:'Flexibilité du prix', promotion_possible:'Promotion possible', psl_accepte:'PSL accepté', complement_info:'Complément d\'info', commentaire_honnete:'Commentaire honnête', problematique_connue:'Problématique connue', selection_candidat:'Sélection candidat', notes:'Notes' };2410function renderUnitIntNotes(raw){2411  if (raw == null || raw === '') return '<div style="color:#999;font-size:13px">Aucune note interne pour cette unité.</div>';2412  if (typeof raw === 'string') return `<div class="up-note">${esc(raw).replace(/\n/g,'<br>')}</div>`;   // ancien format (chaîne)2413  const rows = UNIT_INT_ORDER.filter(k => raw[k] != null && String(raw[k]).trim())2414    .map(k => `<div class="up-introw"><span class="up-intk">${esc(UNIT_INT_LABELS[k]||k)}</span><span class="up-intv">${esc(String(raw[k])).replace(/\n/g,'<br>')}</span></div>`).join('');2415  return rows || '<div style="color:#999;font-size:13px">Aucune note interne pour cette unité.</div>';2416}2417window.renderUnitIntNotes = renderUnitIntNotes;   // utilisée aussi par le bloc module (infos internes)2418async function openUnitPanel(l, u){2419  if (!u) return;2420  const ov = document.getElementById('unit-overlay'), m = document.getElementById('unit-modal');2421  const loue = !isAvail(u);2422  const row = (lb, val) => (val != null && val !== '') ? `<div class="up-row"><span class="up-k">${esc(lb)}</span><span class="up-v">${esc(val)}</span></div>` : '';2423  const prix = (u.prix != null && u.prix !== '') ? Number(u.prix).toLocaleString('fr-CA') + ' $ /mois' : '—';2424  const isStaff = false;   // site public : aucun outil interne (statut/mémo) dans le panneau d'unité2425  const staff = privListing(l);   // numéro d'appartement visible au staff ET au propriétaire (ses immeubles)2426  const headTitle = staff ? (u.numero || 'Unité') : (u.type || l.grandeur || 'Logement');2427  const headSub = staff ? [u.type || '', u.etage || ''].filter(Boolean).join(' · ') : (u.etage || '');2428  // Photos propres à cette unité = le modèle de photo qui lui est lié2429  const model = (u.photoModel && Array.isArray(l.photo_variants)) ? l.photo_variants.find(v => v && v.label === u.photoModel) : null;2430  const uphotos = modelPhotos(model);   // photo principale (★) en tête2431  window.__unitPhotos = uphotos;2432  const photosHtml = uphotos.length2433    ? `<div class="up-photos">${uphotos.slice(0,8).map((p,pi)=>`<img src="${esc(simg(p,600,66))}" loading="lazy" alt="Photo ${pi+1}" onclick="openLightbox(window.__unitPhotos,${pi})">`).join('')}</div>`2434    : '';2435  m.innerHTML = `<button class="unit-close" onclick="closeUnitPanel()">&times;</button>2436    <div class="up-head"><div><div class="up-num">${esc(headTitle)}</div><div class="up-sub">${esc(headSub)}</div></div>2437    <span class="up-stat ${loue?'st-loue':'st-dispo'}">${loue?'Loué':'Disponible'}</span></div>2438    ${photosHtml}2439    <div class="up-price">${prix}</div>2440    ${isStaff ? `<div class="up-sec" style="color:#E67700">Statut — disponibilité (agent)</div>2441      <select id="up-statut-sel" style="width:100%;font-size:14px;padding:9px 10px;border:1px solid #cfd6de;border-radius:8px;background:#fff;font-weight:600">${U_STATUTS.map(s=>`<option ${s===(u.statut||'Disponible')?'selected':''}>${esc(s)}</option>`).join('')}</select>2442      <div id="up-statut-msg" style="font-size:12px;margin-top:5px;min-height:16px"></div>2443      <div id="up-memo-wrap" style="margin-top:8px;${isAvail(u)?'display:none':''}">2444        <div class="up-sec" style="color:#8a5a00">📝 Mémo (période de réservation)</div>2445        <textarea id="up-memo" rows="2" style="width:100%;font-size:13px;padding:8px 10px;border:1px solid #E7C9A0;border-radius:8px;background:#FFFBF3;box-sizing:border-box;resize:vertical;font-family:inherit;line-height:1.4" placeholder="ex : Réservé pour M. Tremblay jusqu'au 15 août · dépôt reçu · clés le 1er">${esc(u.memo||'')}</textarea>2446        <div id="up-memo-msg" style="font-size:12px;margin-top:4px;min-height:14px"></div>2447      </div>` : ''}2448    <div class="up-grid">${row('Chambres',u.chambres)}${row('Salles de bain',u.sdb)}${row('Stationnements',u.stationnement)}${row('Superficie',(u.superficie!=null&&u.superficie!=='')?u.superficie+' pi²':'')}</div>2449    ${u.caract?`<div class="up-sec">Description</div><div class="up-caract">${esc(u.caract)}</div>`:''}2450    <div id="up-internal" class="up-internal"></div>`;2451  ov.classList.add('open'); ovOpened();2452  { const sel = document.getElementById('up-statut-sel'); if (sel) sel.addEventListener('change', () => agentSetUnitStatut(l, u, sel.value, sel)); }2453  { const mt = document.getElementById('up-memo'); if (mt) mt.addEventListener('change', () => agentSetUnitMemo(l, u, mt.value)); }2454  // Site public : aucune note interne affichée (gérées dans l'espace agent / propriétaire).2455  { const wrap = document.getElementById('up-internal'); if (wrap) wrap.innerHTML = ''; }2456}2457// Agent : mémo interne d'une unité (période de réservation) — via RPC sécurisée.2458async function agentSetUnitMemo(l, u, memo){2459  const parentPk = l._virtual ? l._parentPk : l.pk;2460  const msg = document.getElementById('up-memo-msg');2461  if (!parentPk){ if (msg){ msg.textContent = 'Impossible (fiche introuvable).'; msg.style.color = '#b91c1c'; } return; }2462  if (msg){ msg.textContent = 'Enregistrement…'; msg.style.color = '#888'; }2463  const { error } = await _sb.rpc('agent_set_unit_memo', { p_pk: String(parentPk), p_unit_id: String(u.id || ''), p_memo: memo || '' });2464  if (error){ if (msg){ msg.textContent = 'Échec : ' + error.message + ' — 81-unite-memo.sql exécuté ?'; msg.style.color = '#b91c1c'; } return; }2465  u.memo = memo;2466  const units = (Array.isArray(l.units) && l.units.length) ? l.units : (Array.isArray(l._bldUnits) ? l._bldUnits : []);2467  const t = units.find(x => String(x.id) === String(u.id)); if (t) t.memo = memo;2468  if (msg){ msg.textContent = '✅ Mémo enregistré'; msg.style.color = '#E67700'; }2469}2470// Agent : change le statut d'une unité (écrit les units en base + journalise l'historique)2471async function agentSetUnitStatut(l, u, newStatut, selEl){2472  const parentPk = l._virtual ? l._parentPk : l.pk;2473  const units = (Array.isArray(l.units) && l.units.length) ? l.units : (Array.isArray(l._bldUnits) ? l._bldUnits : []);2474  const target = units.find(x => String(x.id) === String(u.id));2475  const msg = document.getElementById('up-statut-msg');2476  if (!target || !parentPk){ if(msg){ msg.textContent = 'Impossible (fiche introuvable).'; msg.style.color='#b91c1c'; } return; }2477  const old = target.statut || 'Disponible';2478  if (old === newStatut) return;2479  if (selEl) selEl.disabled = true;2480  if (msg){ msg.textContent = 'Enregistrement…'; msg.style.color='#888'; }2481  target.statut = newStatut;   // optimiste (affichage local)2482  // Passe par une RPC sécurisée : le staff ne peut changer QUE le statut d'une unité (pas toute la fiche).2483  const { error } = await _sb.rpc('agent_set_unit_statut', { p_pk: String(parentPk), p_unit_id: String(u.id||''), p_statut: newStatut });2484  if (error){2485    target.statut = old; if (selEl){ selEl.value = old; selEl.disabled = false; }2486    if (msg){ msg.textContent = 'Échec : ' + error.message + ' — 41-historique-statuts.sql exécuté ?'; msg.style.color='#b91c1c'; }2487    return;2488  }2489  u.statut = newStatut;2490  // Journalisation (best-effort)2491  try {2492    const { data: { session } } = await _sb.auth.getSession();2493    const par = session && session.user ? session.user.email : '';2494    const label = [u.numero || '', u.type || '', u.etage || ''].filter(Boolean).join(' · ');2495    _sb.from('unit_status_history').insert({ listing_pk: parentPk, listing_titre: l.titre || '', unit_id: String(u.id || ''), unite_label: label, ancien_statut: old, nouveau_statut: newStatut, modifie_par: par, source: 'agent' }).then(()=>{}, ()=>{});2496  } catch(e){}2497  if (selEl) selEl.disabled = false;2498  if (msg){ msg.textContent = '✅ Statut mis à jour'; msg.style.color='#E67700'; }2499  const badge = document.querySelector('#unit-modal .up-stat');2500  if (badge){ const loue = !isAvail(u); badge.className = 'up-stat ' + (loue ? 'st-loue' : 'st-dispo'); badge.textContent = loue ? 'Loué' : 'Disponible'; }2501  { const mw = document.getElementById('up-memo-wrap'); if (mw) mw.style.display = isAvail(u) ? 'none' : ''; }2502  renderAgentBuilding(l);           // rafraîchit les tuiles du plan2503  if (typeof render === 'function') render();   // recalcule les compteurs de la grille2504}2505function closeUnitPanel(e){ if (e && e.target && e.target !== document.getElementById('unit-overlay')) return; document.getElementById('unit-overlay').classList.remove('open'); ovClosedUI(); }2506window.closeUnitPanel = closeUnitPanel;25072508// ---- Bouton « retour » du téléphone : ferme le modal ouvert (au lieu de quitter la page) ----2509const _OV_IDS = ['lightbox-overlay','unit-overlay','bld-full-overlay','visite-overlay','detail-overlay'];   // du plus « au-dessus » au plus « en dessous »2510let _ovPushed = 0, _ovIgnorePop = false;2511// Verrouille le défilement du fond dès qu'un overlay est ouvert → évite la 2e barre de défilement.2512function _syncBodyScroll(){ const anyOpen = _OV_IDS.some(x => { const el = document.getElementById(x); return el && el.classList.contains('open'); }); document.body.style.overflow = anyOpen ? 'hidden' : ''; }2513function ovOpened(url){ _ovPushed++; try { history.pushState({ ov: _ovPushed }, '', url || undefined); } catch(e){} _syncBodyScroll(); }2514function ovClosedUI(){ if (_ovPushed > 0){ _ovPushed--; _ovIgnorePop = true; try { history.back(); } catch(e){ _ovIgnorePop = false; } } _syncBodyScroll(); }2515window.addEventListener('popstate', () => {2516  if (_ovIgnorePop){ _ovIgnorePop = false; _syncBodyScroll(); return; }   // fermeture déclenchée par l'UI, déjà traitée2517  const id = _OV_IDS.find(x => { const el = document.getElementById(x); return el && el.classList.contains('open'); });2518  if (id){ _ovPushed = Math.max(0, _ovPushed - 1); document.getElementById(id).classList.remove('open'); }2519  _syncBodyScroll();2520});2521// Ferme sans bruit les overlays « enfants » d'une fiche (plan, unité, lightbox) restés ouverts.2522// Empêche une nouvelle fiche de s'ouvrir DERRIÈRE le plan plein écran (z-index plus élevé).2523function _closeChildOverlays(){2524  ['lightbox-overlay','unit-overlay','bld-full-overlay'].forEach(id => {2525    const el = document.getElementById(id);2526    if (el && el.classList.contains('open')){ el.classList.remove('open'); if (_ovPushed > 0) _ovPushed--; }2527  });2528}25292530// ---- Description agrégée : combine les caractéristiques des unités DISPONIBLES ----2531// Champs communs à toutes les dispos → affichés ; champs qui diffèrent → « plusieurs… ».2532// À mesure que des unités sont louées, elles sortent du calcul → la description se précise.2533const CARAC_SEL_DEF = { laveuse:['Non incluse','👕'], rangement:['Non inclus','🗄️'], clim:['Non incluse','🍃'], chauffage:['Non inclus','🔥'], eau:['Non incluse','💧'], meuble:['Non meublé','🛋️'], animaux:['Non acceptés','🐾'] };2534function genAggregateAnnonce(l){2535  const units = Array.isArray(l.units) ? l.units : [];2536  if (!units.length) return l.description || '';2537  // Unités OFFERTES (tout sauf « Loué ») : à mesure que des unités se louent, elles sortent2538  // du calcul → le texte se précise (prix, grandeurs, étages) selon les dispos restantes.2539  const U = offeredUnits(l);2540  const C = U.map(u => u.carac || {});2541  if (!C.some(c => c && Object.keys(c).length)) return l.description || '';2542  const num = x => { const v = parseFloat(x); return isNaN(v) ? null : v; };2543  const commonSel = k => { const vs = C.map(c => c[k] || ''); const f = vs[0]; return vs.every(v => v === f) ? f : null; };2544  const allBool = k => C.every(c => c[k]);2545  const lines = [];2546  const anyImmediate = C.some(c => c.dispoImmediate);2547  const dispos = [...new Set(C.map(c => c.dispo).filter(Boolean))];2548  const commonDispo = (!anyImmediate && dispos.length === 1) ? dispos[0] : null;2549  lines.push(anyImmediate ? 'Logement libre dès maintenant !' : (commonDispo ? `Logement libre dès ${commonDispo} !` : (U.length > 1 ? 'Plusieurs disponibilités !' : 'Logement libre dès maintenant !')));2550  const prices = U.map(u => num(u.prix)).filter(x => x != null);2551  if (prices.length){ const mn = Math.min(...prices), mx = Math.max(...prices); lines.push(mn === mx ? `Prix : ${mn} $` : `Prix : de ${mn} à ${mx} $`); }2552  lines.push('');2553  const rues = [...new Set(C.map(c => c.rue).filter(Boolean))];2554  if (rues.length === 1) lines.push(`📍 ${rues[0]} — ${anyImmediate ? 'Disponible dès maintenant' : (commonDispo ? `Disponible ${commonDispo}` : 'Disponible')}`);2555  const etages = [...new Set(U.map(u => u.etage).filter(Boolean))];2556  if (etages.length === 1) lines.push(`🏢 Situé au ${etages[0]}`);2557  else if (etages.length > 1) lines.push(`🏢 Plusieurs étages disponibles (${etages.join(', ')})`);2558  const types = [...new Set(U.map(u => u.type).filter(Boolean))];2559  if (types.length === 1) lines.push(`🏠 Type : ${types[0]}`);2560  else if (types.length > 1) lines.push(`🏠 Grandeurs disponibles : ${types.join(', ')}`);2561  const rng = (get, em, sing, plur) => { const vs = U.map(get).map(num).filter(x => x != null && x > 0); if (!vs.length) return; const mn = Math.min(...vs), mx = Math.max(...vs); lines.push(mn === mx ? `${em} ${mn} ${mn>1?plur:sing}` : `${em} ${mn} à ${mx} ${plur}`); };2562  rng(u => u.chambres, '🛏️', 'Chambre à coucher', 'Chambres à coucher');2563  rng(u => u.sdb, '🛁', 'Salle de bain', 'Salles de bain');2564  const sel = k => { const v = commonSel(k); return (v && v !== CARAC_SEL_DEF[k][0]) ? v : null; };2565  let v; v = sel('laveuse'); if (v) lines.push(`👕 ${v}`);2566  v = sel('rangement'); if (v) lines.push(`🗄️ ${v}`);2567  v = sel('clim'); if (v) lines.push(`🍃 ${v}`);2568  if (allBool('balcon')) lines.push('🔆 Espace balcon');2569  if (allBool('terrain')) lines.push('🌿 Accès au terrain');2570  v = sel('chauffage'); if (v) lines.push(`🔥 Chauffage ${v.toLowerCase()}`);2571  v = sel('eau'); if (v) lines.push('💧 Eau chaude incluse');2572  v = sel('meuble'); if (v) lines.push(`🛋️ ${v}`);2573  rng(u => u.stationnement, '🚗', 'Stationnement', 'Stationnements');2574  if (allBool('deneigement')) lines.push('❄️ Stationnement déneigé');2575  if (allBool('garage')) lines.push('🏠 Garage inclus');2576  if (allBool('plancher_chauffant')) lines.push('♨️ Plancher chauffant');2577  if (allBool('aspirateur_central')) lines.push('🧹 Aspirateur central');2578  if (allBool('ascenseur')) lines.push('🛗 Ascenseur');2579  if (allBool('electricite_inclus')) lines.push('💡 Électricité incluse');2580  if (allBool('internet_inclus')) lines.push('🌐 Internet inclus');2581  if (allBool('voisin_arriere')) lines.push('🌳 Sans voisin arrière');2582  if (allBool('non_fumeur')) lines.push('🚭 Logement non-fumeur');2583  const anim = commonSel('animaux'); if (anim === 'Non acceptés') lines.push('🚫 Animaux non acceptés'); else if (anim && anim !== CARAC_SEL_DEF.animaux[0]) lines.push(`🐾 ${anim}`);2584  if (allBool('credit')) lines.push('📋 Enquête de crédit et références demandées');2585  const kx = e => (e.emoji || '✨') + '|' + (e.texte || '').trim();2586  const sets = C.map(c => new Set((c.extras || []).filter(e => e && e.texte && e.texte.trim()).map(kx)));2587  if (sets.length){ const inter = [...(sets[0] || [])].filter(k => sets.every(s => s.has(k))); inter.forEach(k => { const p = k.split('|'); lines.push(`${p[0]} ${p.slice(1).join('|')}`); }); }2588  return lines.join('\n');2589}25902591// Identifiant anonyme de navigateur (persisté) → permet de compter des VISITEURS UNIQUES2592// sans aucune donnée personnelle.2593function visitorId(){2594  let v = ''; try { v = localStorage.getItem('visitorId') || ''; } catch(_){}2595  if (!v){ v = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : (Date.now() + '-' + Math.random().toString(36).slice(2)); try { localStorage.setItem('visitorId', v); } catch(_){} }2596  return v;2597}2598// Enregistre un événement de fiche (statistiques admin) : kind = 'view' (ouverture),2599// 'phone' (clic Appeler) ou 'contact' (clic Courriel). CHAQUE clic est compté ; seul un2600// garde-fou anti-double-déclenchement (4 s) évite qu'un même clic soit enregistré 2 fois.2601function trackEvent(l, kind){2602  try {2603    if (!l) return;2604    kind = kind || 'view';2605    const pk = String(l._virtual ? (l._parentPk != null ? l._parentPk : '') : (l.pk != null ? l.pk : ''));2606    const idapp = String(l._baseIdApp || l.id_app || l.ID_app || '');2607    if (!pk && !idapp) return;2608    const key = kind + ':' + (pk || idapp) + ':' + (l._virtual ? (l._modelLabel || l.grandeur || '') : ''), now = Date.now();2609    const windowMs = 4 * 1000;   // anti-double-clic seulement2610    let seen = {}; try { seen = JSON.parse(localStorage.getItem('viewsSeen') || '{}'); } catch(_){}2611    if (seen[key] && (now - seen[key]) < windowMs) return;2612    seen[key] = now; try { localStorage.setItem('viewsSeen', JSON.stringify(seen)); } catch(_){}2613    fetch(`${SUPABASE_URL}/rest/v1/listing_views`, {2614      method: 'POST',2615      headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json', Prefer: 'return=minimal' },2616      body: JSON.stringify({ listing_pk: pk || null, id_app: idapp || null, variant: (l._virtual ? (l._modelLabel || l.grandeur || null) : null), kind, visitor: visitorId() })2617    }).catch(() => {});2618  } catch(_){}2619}2620async function openDetail(idx) {2621  const _l0 = allListings[idx]; if (!_l0) return;2622  // Agent/staff : on n'ouvre pas le modal public — on l'envoie gérer la fiche dans « Annonces ».2623  if (window.__isStaff || document.body.classList.contains('is-staff')){2624    const ref = (_l0._baseIdApp || _l0.id_app || _l0.ID_app || '').toString().trim();2625    location.href = '/compte?agtab=apparts' + (ref ? '&agfiche=' + encodeURIComponent(ref) : '');2626    return;2627  }2628  _closeChildOverlays();   // ardoise propre : jamais une fiche derrière un plan resté ouvert2629  currentListing = allListings[idx];2630  currentPhotoIdx = 0;2631  const l = currentListing;2632  trackEvent(l, 'view');2633  // Clics « Appeler » / « Courriel » sur la fiche = intérêt fort (onclick assigné → pas d'empilement).2634  { const bc = document.getElementById('btn-call'); if (bc) bc.onclick = () => trackEvent(l, 'phone');2635    const be = document.getElementById('btn-email'); if (be) be.onclick = () => trackEvent(l, 'contact'); }2636  const photos = await getFolderPhotos(l);2637  const loyer = parseFloat(l.loyer)||0;2638  const etageNorm = normalizeEtage(getEtage(l));2639  const g = document.getElementById('detail-gallery');2640  // Photos « meublé virtuellement » : emoji ✨ sur la galerie + bouton, si présentes.2641  const staged = Array.isArray(l.staged_photos) ? l.staged_photos.filter(Boolean) : [];2642  window.__stagedPhotos = staged;2643  const sparkHtml = staged.length ? `<button class="staged-spark" onclick="event.stopPropagation();openLightbox(window.__stagedPhotos,0)" title="✨ Voir l'appartement en aménagement virtuel">✨</button>` : '';2644  if (photos.length) {2645    window._currentPhotos = photos;2646    let imgs = photos.map((p,i)=>`<img src="${esc(simg(p,1280,70))}" ${i===0?'':'loading="lazy"'} class="${i===0?'active':''}" alt="Photo ${i+1}" onclick="openLightbox(window._currentPhotos,${i})">`).join('');2647    let dots = photos.length>1?`<div class="gallery-nav">${photos.map((_,i)=>`<button class="gallery-dot ${i===0?'active':''}" onclick="goPhoto(${i})"></button>`).join('')}</div>`:'';2648    let arrows = photos.length>1?`<button class="gallery-prev" onclick="prevPhoto()">‹</button><button class="gallery-next" onclick="nextPhoto()">›</button>`:'';2649    let count = photos.length>1?`<div class="gallery-count">1 / ${photos.length}</div>`:'';2650    g.innerHTML = `<button class="detail-close" onclick="closeDetail()">&times;</button>${sparkHtml}${imgs}${dots}${arrows}${count}`;2651  } else {2652    g.innerHTML = `<button class="detail-close" onclick="closeDetail()">&times;</button>${sparkHtml}<div class="gallery-placeholder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" style="width:56px;height:56px;color:#ccc"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"/><path d="M9 21V12h6v9"/></svg></div>`;2653  }2654  document.getElementById('d-ville').textContent = '📍 '+(l.ville||'');2655  document.getElementById('d-title').textContent = l.titre||'';2656  { const fb = document.getElementById('d-fav'); if (fb){ const fid = l.ID_app||l.id_app||''; fb.setAttribute('data-app', fid); fb.classList.toggle('active', !!(window.__favs && window.__favs.has(fid))); } }2657  document.getElementById('d-price').innerHTML = (hasVariants(l)?'<span class="price-from">dès </span>':'')+minLoyer(l).toLocaleString('fr-CA')+' $<span> / mois</span>';2658  document.getElementById('d-badges').innerHTML = `<span class="detail-badge db-size">${esc(grandeurDisplay(l))}</span>${hasVariants(l)?`<span class="detail-badge db-etage">${l.variants.length} modèle${l.variants.length>1?'s':''}${totalUnits(l)?` · ${totalUnits(l)} unité${totalUnits(l)>1?'s':''}`:''}</span>`:(etageNorm?`<span class="detail-badge db-etage">${etageLabel(etageNorm)}</span>`:'')}${(!hasVariants(l) && !(Array.isArray(l.units)&&l.units.length)) ? `${l.chambres?`<span class="detail-badge db-size">🛏️ ${esc(l.chambres)} ch.</span>`:''}${l.sdb?`<span class="detail-badge db-size">🛁 ${esc(l.sdb)} s.d.b.</span>`:''}${l.stationnement?`<span class="detail-badge db-size">🚗 ${esc(l.stationnement)} stat.</span>`:''}` : ''}<span class="detail-badge db-dispo">${esc(dispoDisplay(l))}</span>${l.animaux&&!/aucun/i.test(l.animaux)?`<span class="detail-badge db-animal">${esc(l.animaux)}</span>`:''}${(l.ID_app && !l.created_by_owner)?`<span class="detail-badge db-ref">Réf. ${esc(l.ID_app)}</span>`:''}`;2659  // Bouton « Meublé virtuellement » — visible seulement si des photos meublées existent.2660  const stagedBtn = document.getElementById('d-staged-btn');2661  if (stagedBtn){2662    if (staged.length){ stagedBtn.style.display = ''; stagedBtn.onclick = () => openLightbox(window.__stagedPhotos, 0); }2663    else { stagedBtn.style.display = 'none'; stagedBtn.onclick = null; }2664  }2665  renderDetailVariants(l);2666  renderAgentBuilding(l);2667  renderDetailPhotoModels(l);2668  // Description = texte écrit (proprio/admin) + résumé auto des logements, sans doublon.2669  // genAggregateAnnonce renvoie le résumé pour un immeuble avec caractéristiques,2670  // sinon il renvoie l.description (aucune donnée perdue).2671  (function(){2672    // Texte « Caractéristique » (écrit + résumé auto, sans doublon) — l'affichage PAR DÉFAUT.2673    const typed = (l.description || '').trim();2674    const gen = (genAggregateAnnonce(l) || '').trim();2675    let caracText;2676    if (typed && gen && gen !== typed) {2677      const normLine = s => s.replace(/^[^0-9A-Za-zÀ-ÿ]+/, '').replace(/\s+/g, ' ').trim().toLowerCase();2678      const typedSet = new Set(typed.split('\n').map(normLine).filter(Boolean));2679      const genLines = gen.split('\n').map(normLine).filter(Boolean);2680      const overlap = genLines.length ? genLines.filter(x => typedSet.has(x)).length / genLines.length : 0;2681      caracText = overlap >= 0.6 ? gen : (typed + '\n\n' + gen);2682    } else caracText = typed || gen || '';2683    const iaText = String(l.description_ia || '').trim();2684    const descEl = document.getElementById('d-desc');2685    const tabsEl = document.getElementById('d-desc-tabs');2686    descEl.textContent = caracText;   // ← Caractéristique visible par défaut, toujours2687    if (tabsEl){2688      if (iaText){2689        // Un texte « Description » (IA) existe → on propose au visiteur de basculer.2690        tabsEl.style.display = 'flex';2691        tabsEl.innerHTML = '<button type="button" class="ddt-btn ddt-on" data-dtab="carac">🏷️ Caractéristique</button><button type="button" class="ddt-btn" data-dtab="desc">📝 Description</button>';2692        const setTab = t => { descEl.textContent = (t === 'desc') ? iaText : caracText; tabsEl.querySelectorAll('.ddt-btn').forEach(b => b.classList.toggle('ddt-on', b.getAttribute('data-dtab') === t)); };2693        tabsEl.querySelectorAll('.ddt-btn').forEach(b => b.addEventListener('click', () => setTab(b.getAttribute('data-dtab'))));2694      } else { tabsEl.style.display = 'none'; tabsEl.innerHTML = ''; }2695    }2696  })();26972698  renderSimilar(l);   // « Logements similaires » au bas de la fiche26992700  // Section infos internes — visible seulement pour le staff connecté (admins + agents, données protégées par RLS)2701  window.__currentPk = l.pk;2702  const iw = document.getElementById('d-internal-wrap');2703  if (iw){2704    if (privListing(l)){2705      iw.style.display = 'block';2706      const dint = document.getElementById('d-internal'); dint.innerHTML = '';2707      const parentPk = l._virtual ? l._parentPk : l.pk;2708      // Plus de bouton : les infos internes s'affichent directement pour le staff.2709      if (l._virtual && l._unitId){2710        // Carte d'appartement : infos 🏢 immeuble + 🔒 notes de CETTE unité2711        window.showApartmentInternal && window.showApartmentInternal(parentPk, l._unitId, l.pk);2712      } else {2713        // Immeuble (plan) → seulement le bloc 🏢 (les infos par logement sont par unité).2714        // Fiche appartement unique → toutes les infos internes de la fiche.2715        const isBuilding = l.fiche_type === 'immeuble' || (Array.isArray(l.units) && l.units.length > 0);2716        if (window.showInternalInfo) window.showInternalInfo(parentPk, { buildingOnly: isBuilding });2717      }2718    } else iw.style.display = 'none';2719  }27202721  // Contact : fiche PROPRIÉTAIRE → ses coordonnées + pas de formulaire de location.2722  //           fiche Gestion ISR   → coordonnées Gestion ISR + formulaire de location.2723  const btnCall = document.getElementById('btn-call'), btnEmail = document.getElementById('btn-email'), btnVis = document.getElementById('btn-visite-full');2724  // Mode lien propriétaire (?p=) : ses coordonnées (profil) sur TOUTES les fiches,2725  // y compris celles de Gestion ISR. Un contact propre à la fiche a priorité.2726  const oc = window.__ownerContact;2727  const ph = ((l.contact_phone||'').trim()) || (oc ? (oc.contact_phone||'').trim() : '');2728  const em = ((l.contact_email||'').trim()) || (oc ? (oc.contact_email||'').trim() : '');2729  // Une fiche confiée à Gestion ISR (pool ISR) est traitée comme une annonce ISR (contact ISR), pas en mode propriétaire.2730  const ownerMode = !!(oc || (l.created_by_owner && !l.__isr));2731  if (ownerMode){2732    if (ph){ btnCall.href = 'tel:'+ph.replace(/[^\d+]/g,''); btnCall.style.display=''; } else { btnCall.style.display='none'; }2733    if (em){ btnEmail.href = 'mailto:'+em; btnEmail.style.display=''; } else { btnEmail.style.display='none'; }2734    // Sur un lien propriétaire (?p=), on GARDE le formulaire de demande : il est rattaché à SON CRM2735    // (owner_code). Hors lien propriétaire, pas de formulaire (simple contact).2736    if (window.__ownerCode){ btnVis.style.display=''; btnVis.onclick = ()=>openVisite(l); }2737    else { btnVis.style.display = 'none'; }2738  } else {2739    btnCall.href = 'tel:+18194721127'; btnCall.style.display='';2740    btnEmail.href = 'mailto:info@gestion-isr.com'; btnEmail.style.display='';2741    btnVis.style.display = ''; btnVis.onclick = ()=>openVisite(l);2742  }2743  // Coordonnées du propriétaire affichées EN BAS de la fiche (nom + tél + courriel).2744  { const nm = ((l.contact_name||'').trim()) || (oc ? (oc.contact_name||'').trim() : '');2745    const box = document.getElementById('d-owner-contact');2746    if (box){2747      if (ownerMode && (nm||ph||em)){2748        box.innerHTML = '<hr class="detail-divider"><div class="detail-section"><h3 style="margin:0 0 6px">📞 Pour joindre le propriétaire</h3>'2749          + (nm?`<div style="font-weight:600">${esc(nm)}</div>`:'')2750          + (ph?`<div style="margin-top:3px">☎ <a href="tel:${ph.replace(/[^\d+]/g,'')}" style="color:inherit">${esc(ph)}</a></div>`:'')2751          + (em?`<div style="margin-top:3px">✉ <a href="mailto:${esc(em)}" style="color:inherit">${esc(em)}</a></div>`:'')2752          + '</div>';2753        box.style.display='';2754      } else { box.style.display='none'; box.innerHTML=''; }2755    }2756  }2757  // Mini-carte dans le modal2758  const detailMapEl = document.getElementById('detail-map');2759  detailMapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:13px">Chargement de la carte…</div>';2760  if (window._detailMap) {2761    window._detailMap.remove();2762    window._detailMap = null;2763  }2764  const adresse = l.adresse_geo || l.id || '';2765  if (adresse) {2766    setTimeout(async () => {2767      const gpsMatch = adresse.match(/^\s*(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*$/);2768      let coords;2769      if (gpsMatch) {2770        coords = {lat: parseFloat(gpsMatch[1]), lng: parseFloat(gpsMatch[2])};2771      } else {2772        coords = await geocode(adresse);2773      }2774      if (coords && typeof L !== 'undefined') {2775        window._detailMap = L.map('detail-map', {zoomControl:true, scrollWheelZoom:false}).setView([coords.lat, coords.lng], 15);2776        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {2777          attribution: '© OpenStreetMap',2778          maxZoom: 192779        }).addTo(window._detailMap);2780        L.marker([coords.lat, coords.lng]).addTo(window._detailMap);2781      } else {2782        detailMapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:13px">Localisation non disponible</div>';2783      }2784    }, 300);2785  } else {2786    detailMapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:13px">Adresse non spécifiée</div>';2787  }2788  // Sujet pré-rempli — seulement pour le contact Gestion ISR (ne pas écraser le courriel du propriétaire)2789  if (!ownerMode){2790    document.getElementById('btn-email').href = 'mailto:info@gestion-isr.com?subject=' + encodeURIComponent('Demande d\'information — ' + (l.titre||''));2791  } else if (em){2792    document.getElementById('btn-email').href = 'mailto:' + em + '?subject=' + encodeURIComponent('Demande d\'information — ' + (l.titre||''));2793  }2794  document.getElementById('detail-overlay').classList.add('open'); ovOpened(l.ID_app ? '#' + l.ID_app : null);2795  if (window.logListingView) window.logListingView(l.ID_app || l.id_app || '', l.titre || '');2796}27972798let lightboxPhotos = [], lightboxIdx = 0;27992800// ---- Modèles de photos (variantes) — vue publique ----2801// Photos d'un modèle ordonnées avec la photo principale (★) en tête.2802function modelPhotos(v){ const ph = (v && Array.isArray(v.photos)) ? v.photos.filter(Boolean) : []; return (v && v.main && ph.includes(v.main)) ? [v.main, ...ph.filter(p => p !== v.main)] : ph.slice(); }2803function pvClean(l){ return offeredModels(l).filter(v => v && Array.isArray(v.photos) && v.photos.length).map(v => ({ ...v, photos: modelPhotos(v) })); }2804// Précharge la 1re photo de chaque modèle (taille visionneuse) → passer d'un modèle2805// à l'autre devient instantané, plus de délai au clic.2806const _pvPreloaded = new Set();2807function _preloadModelCovers(pv){2808  (pv || []).forEach(v => {2809    if (!v || !v.photos || !v.photos.length) return;2810    const url = simg(v.photos[0], 1600, 78);2811    if (_pvPreloaded.has(url)) return;2812    _pvPreloaded.add(url);2813    const im = new Image(); im.src = url;2814  });2815}2816// Ouvre la visionneuse avec des boutons pour passer d'un modèle à l'autre2817function openModelsLightbox(models, mi){2818  const pv = (models || []).filter(v => v && Array.isArray(v.photos) && v.photos.length);2819  if (!pv.length) return;2820  mi = Math.max(0, Math.min(mi || 0, pv.length - 1));2821  window.__lbModels = pv;2822  const bar = document.getElementById('lightbox-models');2823  if (bar){2824    bar.innerHTML = pv.map((v, i) => `<button class="lb-mbtn${i===mi?' active':''}" onclick="event.stopPropagation();switchLbModel(${i})">${esc(v.label || ('Modèle ' + String.fromCharCode(65 + i)))}</button>`).join('');2825    bar.classList.toggle('show', pv.length > 1);   // barre visible dès qu'il y a plus d'un modèle2826  }2827  openLightbox(pv[mi].photos, 0, true);2828  _preloadModelCovers(pv);   // les autres modèles seront prêts instantanément2829}2830window.switchLbModel = (i) => {2831  const pv = window.__lbModels || []; if (!pv[i]) return;2832  const bar = document.getElementById('lightbox-models');2833  if (bar) bar.querySelectorAll('.lb-mbtn').forEach((b, bi) => b.classList.toggle('active', bi === i));2834  lightboxPhotos = pv[i].photos; lightboxIdx = 0;2835  document.getElementById('lightbox-img').src = simg(lightboxPhotos[0], 1600, 78);2836  document.getElementById('lb-prev').style.display = lightboxPhotos.length > 1 ? 'flex' : 'none';2837  document.getElementById('lb-next').style.display = lightboxPhotos.length > 1 ? 'flex' : 'none';2838  document.getElementById('lightbox-count').textContent = lightboxPhotos.length > 1 ? ('1 / ' + lightboxPhotos.length) : '';2839  _lbPreload();2840};2841window.openVariantPhotos = (idx) => { const pv = pvClean(allListings[idx]); if (pv.length) openModelsLightbox(pv, 0); };2842window.openModelPhotos = (i) => { const pv = window.__detailPv || []; if (pv.length) openModelsLightbox(pv, i); };2843function renderDetailPhotoModels(l){2844  const sec = document.getElementById('d-photo-models-sec'), box = document.getElementById('d-photo-models');2845  if (!sec || !box) return;2846  const head = sec.querySelector('.d-models-head');2847  // Fiche d'un appartement (virtuelle) : la navigation se fait via le plan (renderAgentBuilding).2848  if (l._virtual){ sec.style.display = 'none'; return; }2849  if (head) head.innerHTML = `📷 Modèles d'appartement <span>— cliquez pour voir les photos de chaque modèle 👇</span>`;2850  const pv = pvClean(l); window.__detailPv = pv;2851  if (!pv.length){ sec.style.display = 'none'; return; }2852  box.innerHTML = pv.map((v, i) => `<button class="pv-model-btn" onclick="openModelPhotos(${i})">Modèle ${esc(v.label || String.fromCharCode(65 + i))} <span>${v.photos.length} photo${v.photos.length>1?'s':''}</span></button>`).join('');2853  sec.style.display = '';2854  _preloadModelCovers(pv);   // fiche ouverte : les modèles s'afficheront sans délai au clic2855}2856function _lbPreload(){2857  if (!lightboxPhotos || lightboxPhotos.length < 2) return;2858  [1, -1].forEach(d => { const j = (lightboxIdx + d + lightboxPhotos.length) % lightboxPhotos.length; const im = new Image(); im.src = simg(lightboxPhotos[j], 1600, 78); });2859}2860// Ouvre les photos « meublé virtuellement » d'un logement (depuis une carte de la liste).2861window.openStaged = function(idx){2862  const l = allListings[idx]; if (!l) return;2863  const s = Array.isArray(l.staged_photos) ? l.staged_photos.filter(Boolean) : [];2864  if (s.length){ window.__stagedPhotos = s; openLightbox(s, 0); }2865};2866function openLightbox(photos, idx, keepModels) {2867  if (!keepModels){ const bar = document.getElementById('lightbox-models'); if (bar){ bar.classList.remove('show'); bar.innerHTML = ''; } window.__lbModels = null; }2868  lightboxPhotos = photos;2869  lightboxIdx = idx;2870  document.getElementById('lightbox-img').src = simg(photos[idx], 1600, 78);2871  document.getElementById('lb-prev').style.display = photos.length > 1 ? 'flex' : 'none';2872  document.getElementById('lb-next').style.display = photos.length > 1 ? 'flex' : 'none';2873  document.getElementById('lightbox-count').textContent = photos.length > 1 ? (idx+1)+' / '+photos.length : '';2874  document.getElementById('lightbox-overlay').classList.add('open'); ovOpened();2875  _lbPreload();2876}28772878function closeLightbox() {2879  document.getElementById('lightbox-overlay').classList.remove('open');2880  ovClosedUI();2881}28822883function closeLightboxOutside(e) {2884  if (e.target === document.getElementById('lightbox-overlay')) closeLightbox();2885}28862887function lightboxNav(dir) {2888  lightboxIdx = (lightboxIdx + dir + lightboxPhotos.length) % lightboxPhotos.length;2889  document.getElementById('lightbox-img').src = simg(lightboxPhotos[lightboxIdx], 1600, 78);2890  document.getElementById('lightbox-count').textContent = (lightboxIdx+1)+' / '+lightboxPhotos.length;2891  _lbPreload();2892}28932894// Navigation clavier2895document.addEventListener('keydown', e => {2896  const lb = document.getElementById('lightbox-overlay');2897  if (lb.classList.contains('open')) {2898    if (e.key === 'ArrowRight') lightboxNav(1);2899    if (e.key === 'ArrowLeft') lightboxNav(-1);2900    if (e.key === 'Escape') closeLightbox();2901  }2902});29032904async function goPhoto(i){2905  const photos = await getFolderPhotos(currentListing);2906  currentPhotoIdx=i;2907  document.querySelectorAll('#detail-gallery img').forEach((img,idx)=>img.classList.toggle('active',idx===i));2908  document.querySelectorAll('.gallery-dot').forEach((d,idx)=>d.classList.toggle('active',idx===i));2909  const cnt=document.querySelector('.gallery-count');2910  if(cnt) cnt.textContent=(i+1)+' / '+photos.length;2911}2912async function nextPhoto(){const p=await getFolderPhotos(currentListing);goPhoto((currentPhotoIdx+1)%p.length);}2913async function prevPhoto(){const p=await getFolderPhotos(currentListing);goPhoto((currentPhotoIdx-1+p.length)%p.length);}2914// Swipe tactile (mobile) pour changer de photo — galerie du détail et lightbox plein écran.2915(function wirePhotoSwipe(){2916  function addSwipe(el, onLeft, onRight){2917    if (!el) return; let x0 = null, y0 = null, moved = false;2918    el.addEventListener('touchstart', e => { const t = e.touches[0]; x0 = t.clientX; y0 = t.clientY; moved = false; }, { passive:true });2919    el.addEventListener('touchmove', () => { moved = true; }, { passive:true });2920    el.addEventListener('touchend', e => {2921      if (x0 == null) return; const t = e.changedTouches[0]; const dx = t.clientX - x0, dy = t.clientY - y0;2922      if (moved && Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy) * 1.3){ (dx < 0 ? onLeft : onRight)(); }2923      x0 = null;2924    }, { passive:true });2925  }2926  addSwipe(document.getElementById('detail-gallery'), () => { try { nextPhoto(); } catch(e){} }, () => { try { prevPhoto(); } catch(e){} });2927  addSwipe(document.getElementById('lightbox-overlay'), () => { try { lightboxNav(1); } catch(e){} }, () => { try { lightboxNav(-1); } catch(e){} });2928})();2929// ---- Logements similaires ----2930function similarListings(l, max){2931  max = max || 4;2932  const cur = l._virtual ? l._parentPk : l.pk;2933  const types = offeredTypes(l), price = minLoyer(l);2934  const seen = new Set([cur == null ? 'self' : String(cur)]);2935  const scored = [];2936  for (const x of (allListings || [])){2937    if (x === l) continue;2938    const xpk = x._virtual ? x._parentPk : x.pk;2939    const key = xpk == null ? ('x' + scored.length) : String(xpk);2940    if (seen.has(key)) continue;2941    if (!listingHasAvailability(x)) continue;2942    let score = 0;2943    if (x.ville && l.ville && x.ville === l.ville) score += 3;2944    const xt = offeredTypes(x);2945    if (types.length && xt.some(t => types.includes(t))) score += 3;2946    const xp = minLoyer(x);2947    if (price != null && xp != null){ const d = Math.abs(xp - price) / price; if (d <= 0.15) score += 2; else if (d <= 0.35) score += 1; }2948    if (score <= 0) continue;2949    seen.add(key); scored.push({ x, score, xp });2950  }2951  scored.sort((a, b) => b.score - a.score || ((a.xp == null ? 1e9 : a.xp) - (b.xp == null ? 1e9 : b.xp)));2952  return scored.slice(0, max).map(s => s.x);2953}2954function renderSimilar(l){2955  const sec = document.getElementById('d-similar-sec'), box = document.getElementById('d-similar');2956  if (!sec || !box) return;2957  const items = similarListings(l, 4);2958  if (!items.length){ sec.style.display = 'none'; box.innerHTML = ''; return; }2959  sec.style.display = '';2960  const photoOf = x => { let p = x.main_photo || (Array.isArray(x.photos) && x.photos.length ? x.photos[0] : ''); if (!p && Array.isArray(x.photo_variants)){ const v = x.photo_variants.find(v => v && Array.isArray(v.photos) && v.photos.length); if (v) p = (v.main && v.photos.includes(v.main)) ? v.main : v.photos[0]; } return p; };2961  box.innerHTML = items.map(x => {2962    const idx = allListings.indexOf(x), pu = photoOf(x), bg = pu ? `style="background-image:url('${esc(simg(pu, 320, 55))}')"` : '';2963    const g = (grandeurDisplay(x) || x.grandeur || '').trim(), price = minLoyer(x), titre = (x.titre || '').trim();2964    const showG = g && !titre.toLowerCase().startsWith(g.toLowerCase());2965    const et = (x.etiquette || '').trim();2966    return `<div class="sim-card" data-sidx="${idx}"><div class="sim-photo" ${bg}>${et && et.toLowerCase() !== 'loué' ? `<span class="sim-badge">${esc(et)}</span>` : ''}${pu ? '' : '🏠'}</div><div class="sim-b"><div class="sim-t">${esc((showG ? g + ' · ' : '') + titre)}</div><div class="sim-v">${esc(x.ville || '')}</div><div class="sim-p">${(hasVariants(x) || hasUnits(x)) ? 'dès ' : ''}${price != null ? price.toLocaleString('fr-CA') : '—'} $</div></div></div>`;2967  }).join('');2968  box.querySelectorAll('[data-sidx]').forEach(c => c.addEventListener('click', () => { const i = +c.getAttribute('data-sidx'); if (i >= 0) openDetail(i); }));2969}29702971// ---- Partage / Publication ----2972function shareNote(m){ const n = document.getElementById('share-note'); if (n) n.textContent = m || ''; }2973async function shareCopy(s, msg){ try { await navigator.clipboard.writeText(s); shareNote(msg); } catch(_){ shareNote('Copie impossible — copiez le texte manuellement.'); } }2974// Lien de partage = la fiche sur le site public normal (hash), JAMAIS la page SEO /logement/2975// (qui empile les photos). On encode accents ET parenthèses → lien 100% ASCII pour SMS/courriel ;2976// la page publique décode le hash et ouvre la fiche. Ex : https://location.gestion-isr.com/#az101-App72977function ficheShareUrl(l){2978  const ref = (l.id_app || l.ID_app || l._baseIdApp || '').toString().trim();2979  const safe = encodeURIComponent(ref).replace(/[()'!*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());2980  // Conserve le contexte de la page (vitrine proprio ?p= ou agent ?a=) pour que la fiche s'ouvre bien.2981  let qs = '';2982  try { const sp = new URLSearchParams(location.search); const p = sp.get('p'), a = sp.get('a');2983    if (p) qs = '?p=' + encodeURIComponent(p); else if (a) qs = '?a=' + encodeURIComponent(a); } catch(_){}2984  return location.origin + '/' + qs + '#' + safe;2985}2986function partagerFiche(){ shareListing(currentListing); }2987function closeShare(){ document.getElementById('share-overlay').classList.remove('open'); }2988async function shareListing(l){2989  if (!l) return;2990  const url = ficheShareUrl(l);2991  const title = (l.titre || 'Logement à louer — Gestion ISR').toString();2992  const txt = (l.description_ia || '').trim() || (genAggregateAnnonce(l) || '').trim() || (l.description || '').trim();2993  // Partage natif (feuille de partage du système) : l'expérience attendue sur mobile2994  // et navigateurs récents — l'utilisateur choisit l'app (Messages, Facebook, courriel…).2995  if (navigator.share){2996    try { await navigator.share({ title, text: txt || title, url }); return; }2997    catch(e){ if (e && e.name === 'AbortError') return; }   // annulé par l'utilisateur2998  }2999  openShareSheet(url, title, txt);   // repli maison (desktop sans partage natif)3000}3001function openShareSheet(url, title, txt){3002  const caption = (txt ? txt + '\n\n' : '') + url;3003  const acts = [3004    { ic: '🔗', t: 'Copier le lien', fn: () => shareCopy(url, 'Lien copié ✓') },3005    { ic: '📋', t: 'Copier le texte', fn: () => shareCopy(caption, 'Texte + lien copiés ✓') },3006    { ic: '📘', t: 'Facebook', fn: () => { window.open('https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url), '_blank', 'noopener'); shareNote('Fenêtre Facebook ouverte.'); } },3007    { ic: '💬', t: 'WhatsApp', fn: () => { window.open('https://wa.me/?text=' + encodeURIComponent(caption), '_blank', 'noopener'); } },3008    { ic: '✉️', t: 'Courriel', fn: () => { window.location.href = 'mailto:?subject=' + encodeURIComponent(title) + '&body=' + encodeURIComponent(caption); } },3009    { ic: '📷', t: 'Instagram', fn: () => shareCopy(caption, 'Légende copiée — créez votre publication Instagram et collez.') }3010  ];3011  const box = document.getElementById('share-actions');3012  box.innerHTML = acts.map((a, i) => `<button class="share-btn" data-sa="${i}"><span class="ic">${a.ic}</span>${a.t}</button>`).join('');3013  box.querySelectorAll('[data-sa]').forEach(b => b.onclick = () => acts[+b.getAttribute('data-sa')].fn());3014  shareNote('Choisis un réseau, ou copie le lien / le texte à coller où tu veux.');3015  document.getElementById('share-overlay').classList.add('open');3016}30173018function closeDetail(){3019  _closeChildOverlays();   // ferme aussi le plan/unité restés ouverts par-dessus la fiche3020  document.getElementById('detail-overlay').classList.remove('open');3021  ovClosedUI();3022}3023function closeDetailOutside(e){if(e.target===document.getElementById('detail-overlay'))closeDetail();}30243025// ---- Outil de partage (agent) : mêmes filtres que la vue admin « Sélections » ----3026let agentSelChecked = new Set();3027function agentSelCriteria(){3028  const chips = id => [...document.querySelectorAll('#'+id+' input:checked')].map(i=>i.value);3029  const b = (document.getElementById('asel-budget')||{}).value;3030  return { types: chips('asel-type'), budget: (b==null||b==='')?null:Number(b), etages: chips('asel-etage'), pets: chips('asel-animaux') };3031}3032// Filtre « dur » identique à l'admin (selPass) : un logement est exclu si un critère coché n'est pas rempli.3033function agentSelPass(l, c){3034  if (c.types.length){ const gs = offeredTypes(l); if(!gs.some(g=>c.types.includes(g))) return false; }3035  if (c.budget!=null && !isNaN(c.budget)){ const m=minLoyer(l); if(m==null || m>c.budget) return false; }3036  if (c.etages.length){ const es=listingEtagesNorm(l); if(!es.some(e=>c.etages.includes(e))) return false; }3037  if (c.pets.length){ const a=(l.animaux||'').toLowerCase(); if(a.includes('aucun')) return false; if(!c.pets.some(p=>petAllowed(a,p))) return false; }3038  return true;3039}3040function agentSelVisible(){3041  const c = agentSelCriteria(); const seen=new Set();3042  const q = ((document.getElementById('asel-search')||{}).value||'').toLowerCase().trim();3043  return (allListings||[]).filter(l=>{3044    const idapp=String(l.id_app||l.ID_app||''); if(!idapp||seen.has(idapp)) return false; seen.add(idapp);3045    if((l.etiquette||'').toLowerCase()==='loué') return false;   // partageable = disponible3046    if(!agentSelPass(l, c)) return false;3047    if(q){ const hay=((l.titre||'')+' '+(l.ville||'')+' '+grandeurDisplay(l)+' '+idapp).toLowerCase(); if(!hay.includes(q)) return false; }3048    return true;3049  });3050}3051let _aselSearchT=null;3052window.agentSelSearchInput = function(){ if(_aselSearchT) clearTimeout(_aselSearchT); _aselSearchT=setTimeout(()=>{ _aselSearchT=null; agentSelRender(); }, 180); };3053window.openAgentSel = function(){ const ov=document.getElementById('agent-sel-overlay'); if(!ov)return; ov.style.display='flex'; document.body.style.overflow='hidden'; agentSelRender(); };3054window.closeAgentSel = function(){ const ov=document.getElementById('agent-sel-overlay'); if(!ov)return; ov.style.display='none'; if(typeof _syncBodyScroll==='function') _syncBodyScroll(); else document.body.style.overflow=''; };3055window.agentSelRender = function(){3056  const list=document.getElementById('agent-sel-list'); if(!list)return;3057  const vis = agentSelVisible();3058  const cnt=document.getElementById('agent-sel-count');3059  if(cnt) cnt.innerHTML=`${vis.length} logement${vis.length>1?'s':''} affiché${vis.length>1?'s':''} · <b style="color:#E67700">${agentSelChecked.size} sélectionné${agentSelChecked.size!==1?'s':''}</b> — coche pour partager`;3060  list.innerHTML = vis.length ? vis.map(l=>{3061    const idapp=String(l.id_app||l.ID_app||''); const on=agentSelChecked.has(idapp); const mn=minLoyer(l);3062    const photo = l.main_photo || (Array.isArray(l.photos)&&l.photos.length?l.photos[0]:'');3063    const thumb = photo ? `<img class="selthumb" loading="lazy" decoding="async" src="${esc(photo)}" alt="">` : '<div class="selthumb"></div>';3064    const meta=[grandeurDisplay(l), l.ville, (mn?((hasVariants(l)||hasUnits(l))?'dès ':'')+Number(mn).toLocaleString('fr-CA')+' $':'')].filter(Boolean).join(' · ');3065    return `<label class="sel-row ${on?'':'off'}"><input type="checkbox" data-agentsel="${esc(idapp)}" ${on?'checked':''} onchange="window.agentSelToggle&&window.agentSelToggle(this)">${thumb}<div class="selinfo"><b>${esc(l.titre||idapp)}</b><div class="m">${esc(meta)}</div></div></label>`;3066  }).join('') : '<div style="color:#888;text-align:center;padding:2rem">Aucun logement ne correspond aux filtres.</div>';3067  agentSelUpdateShare();3068};3069window.agentSelToggle = function(cb){3070  const id=cb.getAttribute('data-agentsel');3071  if(cb.checked) agentSelChecked.add(id); else agentSelChecked.delete(id);3072  const row=cb.closest('.sel-row'); if(row) row.classList.toggle('off', !cb.checked);3073  const cnt=document.getElementById('agent-sel-count'); if(cnt) cnt.innerHTML=cnt.innerHTML.replace(/<b[^>]*>[\s\S]*?<\/b>/,`<b style="color:#E67700">${agentSelChecked.size} sélectionné${agentSelChecked.size!==1?'s':''}</b>`);3074  agentSelUpdateShare();3075};3076window.agentSelAll = function(){ agentSelVisible().forEach(l=>{ const id=String(l.id_app||l.ID_app||''); if(id) agentSelChecked.add(id); }); agentSelRender(); };3077window.agentSelNone = function(){ agentSelChecked.clear(); agentSelRender(); };3078window.agentSelResetFilters = function(){ ['asel-type','asel-etage','asel-animaux'].forEach(id=>{ const el=document.getElementById(id); if(el) el.querySelectorAll('input:checked').forEach(i=>i.checked=false); }); const b=document.getElementById('asel-budget'); if(b) b.value=''; const s=document.getElementById('asel-search'); if(s) s.value=''; agentSelRender(); };3079window.agentSelUpdateShare = function(){3080  const box=document.getElementById('agent-sel-share'); if(!box)return;3081  const ids=[...agentSelChecked];3082  if(!ids.length){ box.innerHTML='<div style="color:#888;font-size:13px">Coche un ou plusieurs appartements pour générer un lien personnalisé.</div>'; return; }3083  const link=location.origin+'/?apps='+ids.map(encodeURIComponent).join(',');3084  box.innerHTML=`<div class="sel-linkbox"><label style="font-size:12px;font-weight:700;color:#E67700;display:block;margin-bottom:6px">🔗 Lien personnalisé — ${ids.length} logement${ids.length>1?'s':''}</label>3085    <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">3086      <input class="lnk" id="agent-sel-link" readonly value="${esc(link)}" style="flex:1;min-width:220px">3087      <button class="sel-btn primary" onclick="window.agentSelCopy&&window.agentSelCopy(this)">Copier</button>3088      <button class="sel-btn" onclick="var i=document.getElementById('agent-sel-link');if(i)window.open(i.value,'_blank')">Aperçu</button>3089    </div>3090    <div style="font-size:11.5px;color:#888;margin-top:6px">Envoie ce lien à un candidat : il n'y verra que ces logements.</div></div>`;3091};3092window.agentSelCopy = function(btn){3093  const inp=document.getElementById('agent-sel-link'); if(!inp)return; inp.select();3094  const fb=()=>{ if(btn){ const t=btn.textContent; btn.textContent='Copié ✓'; setTimeout(()=>{ btn.textContent=t; },1500); } };3095  if(navigator.clipboard){ navigator.clipboard.writeText(inp.value).then(fb).catch(()=>{ try{document.execCommand('copy');}catch(e){} fb(); }); }3096  else { try{document.execCommand('copy');}catch(e){} fb(); }3097};30983099let visiteListing = null;3100const VF_MOIS = ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'];3101function vfFillDates(){3102  const mo = document.getElementById('vf-date-mois'); const an = document.getElementById('vf-date-annee');3103  if (mo && mo.options.length <= 1) mo.insertAdjacentHTML('beforeend', VF_MOIS.map(m => `<option>${m}</option>`).join(''));3104  if (an && an.options.length <= 1){ const y = new Date().getFullYear(); let o = ''; for (let i = 0; i <= 5; i++) o += `<option>${y + i}</option>`; an.insertAdjacentHTML('beforeend', o); }3105}3106// ===== Assistant « Formulaire de location » (modal fiche) =====3107const VW_TYPES=['2½','3½','4½','5½','6½','Maison','Indifférent'];3108const VW_ANIMAUX=['Aucun','Chat','Chien'];3109const VW_ENQUETE=['Oui','Non','Incertain'];3110const VW_CRITERES=[['🔆','Balcon'],['🌿','Accès au terrain'],['❄️','Stationnement déneigé'],['🅿️','Stationnement'],['🏠','Garage inclus'],['🍃','Climatisation / thermopompe'],['🔥','Chauffage inclus'],['💧','Eau chaude incluse'],['👕','Laveuse / sécheuse'],['🛋️','Meublé'],['💡','Électricité incluse'],['🌐','Internet inclus'],['🧹','Aspirateur central'],['♨️','Plancher chauffant'],['🛗','Ascenseur'],['🌳','Sans voisin arrière'],['🚭','Non-fumeur'],['🗄️','Rangement / locker']];3111const vwSel={types:new Set(),animaux:'',enquete:'',criteres:new Set()};3112let vwStep=1;3113function vwChip(host,items,multi,key,exclusiveFirst){3114  const el=document.getElementById(host); if(!el) return;3115  el.innerHTML=items.map(v=>`<button type="button" class="vw-chip" data-v="${v.replace(/"/g,'&quot;')}">${v}</button>`).join('');3116  el.querySelectorAll('.vw-chip').forEach(b=>b.addEventListener('click',()=>{3117    const v=b.getAttribute('data-v');3118    if(multi){ const set=vwSel[key];3119      if(exclusiveFirst && v===items[0]){ set.clear(); set.add(v); }3120      else { set.delete(items[0]); if(set.has(v)) set.delete(v); else set.add(v); }3121      el.querySelectorAll('.vw-chip').forEach(x=>x.classList.toggle('on',set.has(x.getAttribute('data-v'))));3122    } else { vwSel[key]=v; el.querySelectorAll('.vw-chip').forEach(x=>x.classList.toggle('on',x.getAttribute('data-v')===v)); }3123  }));3124}3125function vwRenderChoices(){3126  vwChip('vf-types',VW_TYPES,true,'types',true);3127  vwChip('vf-animaux',VW_ANIMAUX,false,'animaux');3128  vwChip('vf-enquete',VW_ENQUETE,false,'enquete');3129  const cr=document.getElementById('vf-criteres');3130  if(cr){ cr.innerHTML=VW_CRITERES.map(([em,lbl])=>`<div class="vw-ci" data-v="${lbl.replace(/"/g,'&quot;')}"><span class="ck">✓</span><span class="cem">${em}</span><span>${lbl}</span></div>`).join('');3131    cr.querySelectorAll('.vw-ci').forEach(b=>b.addEventListener('click',()=>{ const v=b.getAttribute('data-v'); if(vwSel.criteres.has(v))vwSel.criteres.delete(v); else vwSel.criteres.add(v); b.classList.toggle('on',vwSel.criteres.has(v)); })); }3132}3133function vwShowStep(n){3134  vwStep=n;3135  document.querySelectorAll('#visite-form section[data-vstep]').forEach(s=>s.style.display=(+s.getAttribute('data-vstep')===n)?'':'none');3136  document.querySelectorAll('#vw-steps .vw-st').forEach(s=>{ const k=+s.getAttribute('data-st'); s.classList.toggle('active',k===n); s.classList.toggle('done',k<n); });3137  document.getElementById('vf-prev').style.display=n>1?'':'none';3138  document.getElementById('vf-next').style.display=n<3?'':'none';3139  document.getElementById('vf-submit').style.display=n===3?'':'none';3140  const m=document.getElementById('visite-msg'); if(m){ m.textContent=''; m.className='vf-msg'; }3141  const body=document.querySelector('.vw-body'); if(body) body.scrollTop=0;3142}3143function vwErr(t){ const m=document.getElementById('visite-msg'); if(m){ m.textContent=t; m.className='vf-msg err'; } }3144function vwValidate(n){3145  const g=id=>(document.getElementById(id).value||'').trim();3146  if(n===1){3147    if(!g('vf-nom')) return 'Entrez votre nom complet.';3148    const moyen=document.getElementById('vf-moyen').value;3149    if(!moyen) return 'Choisissez votre méthode de communication préférée.';3150    if((moyen==='Téléphone'||moyen==='Les deux') && !g('vf-tel')) return 'Entrez votre numéro de téléphone.';3151    if((moyen==='Courriel'||moyen==='Les deux') && !g('vf-courriel')) return 'Entrez votre adresse courriel.';3152    if((moyen==='Courriel'||moyen==='Les deux') && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(g('vf-courriel'))) return 'Adresse courriel invalide.';3153    return null;3154  }3155  if(n===2){3156    if(!vwSel.types.size) return 'Choisissez au moins un type d\'appartement.';3157    if(!g('vf-date-mois')||!g('vf-date-annee')) return 'Précisez la date d\'emménagement souhaitée.';3158    if(!vwSel.animaux) return 'Indiquez si vous avez des animaux.';3159    if(!vwSel.enquete) return 'Indiquez si vous croyez passer une enquête de crédit.';3160    return null;3161  }3162  return null;3163}3164function vwReset(){3165  vwSel.types.clear(); vwSel.criteres.clear(); vwSel.animaux=''; vwSel.enquete='';3166  const mo=document.getElementById('vf-moyen'); if(mo) mo.value='';3167  const wt=document.getElementById('vf-wrap-tel'); if(wt) wt.style.display='none';3168  const wc=document.getElementById('vf-wrap-courriel'); if(wc) wc.style.display='none';3169  vwRenderChoices(); vwShowStep(1);3170}3171{ const mo=document.getElementById('vf-moyen'); if(mo) mo.addEventListener('change',()=>{ const v=mo.value;3172    document.getElementById('vf-wrap-tel').style.display=(v==='Téléphone'||v==='Les deux')?'':'none';3173    document.getElementById('vf-wrap-courriel').style.display=(v==='Courriel'||v==='Les deux')?'':'none'; }); }3174{ const b=document.getElementById('vf-next'); if(b) b.addEventListener('click',()=>{ const e=vwValidate(vwStep); if(e) return vwErr(e); vwShowStep(vwStep+1); }); }3175{ const b=document.getElementById('vf-prev'); if(b) b.addEventListener('click',()=>vwShowStep(vwStep-1)); }3176function openVisite(l){3177  visiteListing = (l && typeof l === 'object') ? l : null;3178  const titre = visiteListing ? (visiteListing.titre || '') : (typeof l === 'string' ? l : '');3179  document.getElementById('modal-apt-name').textContent = titre ? ('Logement : ' + titre) : '';3180  const f = document.getElementById('visite-form'); if (f){ f.reset(); f.style.display = ''; }3181  vfFillDates();3182  vwReset();   // assistant : réinitialise les étapes, les puces et revient à l'étape 13183  const sc = document.getElementById('visite-success'); if (sc) sc.style.display = 'none';3184  const sb = document.getElementById('vf-submit'); if (sb){ sb.disabled = false; sb.textContent = 'Envoyer ma demande'; }3185  const m = document.getElementById('visite-msg'); if (m){ m.textContent = ''; m.className = 'vf-msg'; }3186  document.getElementById('visite-overlay').classList.add('open'); ovOpened();3187}3188function openVisiteFromCard(titre){ openVisite(titre); }3189function openVisiteFromCardIdx(idx){ openVisite(allListings[idx] || ''); }3190document.getElementById('visite-form').addEventListener('submit', async e => {3191  e.preventDefault();3192  for (const n of [1,2]){ const er = vwValidate(n); if (er){ vwShowStep(n); return vwErr(er); } }3193  const g = id => (document.getElementById(id).value || '').trim();3194  const btn = document.getElementById('vf-submit'), msg = document.getElementById('visite-msg');3195  btn.disabled = true; btn.textContent = 'Envoi…';3196  const moyen = document.getElementById('vf-moyen').value;3197  const lead = {3198    nom: g('vf-nom'),3199    courriel: (moyen==='Courriel'||moyen==='Les deux') ? (g('vf-courriel')||null) : null,3200    telephone: (moyen==='Téléphone'||moyen==='Les deux') ? (g('vf-tel')||null) : null,3201    moyen_communication: moyen || null,3202    logement: visiteListing ? (visiteListing.titre || null) : null,3203    logement_ref: visiteListing ? (visiteListing.ID_app || visiteListing.id_app || null) : null,3204    date_emmenagement: [g('vf-date-mois'), g('vf-date-annee')].filter(Boolean).join(' ') || null,3205    type_appartement: [...vwSel.types].join(', ') || null,3206    budget: g('vf-budget') ? parseInt(g('vf-budget')) : null,3207    animaux: vwSel.animaux || null,3208    enquete_credit: vwSel.enquete || null,3209    criteres: [...vwSel.criteres].join(', ') || null,3210    message: g('vf-message') || null3211  };3212  // Lien propriétaire (?p=) : rattache la demande au CRM du propriétaire plutôt qu'à l'admin.3213  if (window.__ownerCode) lead.owner_code = window.__ownerCode;3214  try {3215    let { error } = await _sb.from('leads').insert(lead);3216    if (error && /column|PGRST204|schema cache|criteres/i.test(error.message || '')){3217      // Repli si la colonne criteres n'existe pas encore (migration 143) → dans le message.3218      const merged = [lead.criteres ? ('Critères souhaités : ' + lead.criteres) : '', lead.message].filter(Boolean).join('\n\n') || null;3219      const { criteres: _c, ...rest } = lead;3220      ({ error } = await _sb.from('leads').insert({ ...rest, message: merged }));3221    }3222    if (error) throw error;3223    document.getElementById('visite-form').style.display = 'none';3224    document.getElementById('visite-success').style.display = 'block';3225  } catch(err){3226    msg.className = 'vf-msg err'; msg.textContent = 'Erreur : ' + (err.message || 'envoi impossible');3227    try { msg.scrollIntoView({ block:'center', behavior:'smooth' }); } catch(_){}3228    btn.disabled = false; btn.textContent = 'Envoyer ma demande';3229  }3230});3231function closeVisite(){document.getElementById('visite-overlay').classList.remove('open');ovClosedUI();}3232function closeVisiteOutside(e){if(e.target===document.getElementById('visite-overlay'))closeVisite();}32333234loadListings().then(() => {3235  let hash = window.location.hash.replace('#', '');3236  // Décodage : un id_app éclaté contient « ½ » → dans l'URL il devient %C2%BD.3237  // Sans décoder, la comparaison échouerait et la fiche ne s'ouvrirait pas.3238  try { hash = decodeURIComponent(hash); } catch(e) {}3239  if (hash) {3240    const h = normIdApp(hash);3241    const base = h.split('-')[0];3242    let idx = allListings.findIndex(l => normIdApp(l.ID_app || l.id_app) === h);3243    // Repli IMMEUBLE : un lien au niveau immeuble (#az106) doit ouvrir la 1re carte de cet3244    // immeuble, car les immeubles sont éclatés en une carte par modèle (#az106-150, #az106-152-B…).3245    if (idx < 0) idx = allListings.findIndex(l => normIdApp(String(l.ID_app || l.id_app || '').split('-')[0]) === h);3246    // Repli base : un lien éclaté dont la carte exacte n'existe plus (#az106-App104) → l'immeuble.3247    if (idx < 0) idx = allListings.findIndex(l => normIdApp(String(l.ID_app || l.id_app || '').split('-')[0]) === base);3248    if (idx !== -1) openDetail(idx);3249  }3250});32513252// ===== CARTE =====3253let map = null, markers = [], geocodeCache = {};3254let mapVisible = false;32553256// ===== Carte : caractéristiques, rayon et panneau de résultats =====3257let mapExtras = new Set();3258let radiusCenter = null, radiusKm = 20, radiusCircle = null, radiusPicking = false, radiusMarker = null;32593260function mapExtrasMatch(l){ for (const k of mapExtras){ const def = EXTRA_CRIT.find(d => d.key === k); if (def && !listingHasExtra(l, def)) return false; } return true; }3261function haversineKm(a, b){ const R=6371, toR=x=>x*Math.PI/180; const dLat=toR(b.lat-a.lat), dLng=toR(b.lng-a.lng); const s=Math.sin(dLat/2)**2 + Math.cos(toR(a.lat))*Math.cos(toR(b.lat))*Math.sin(dLng/2)**2; return 2*R*Math.asin(Math.sqrt(s)); }32623263(function initExtras(){3264  const box = document.getElementById('ms-extras'); if (!box || typeof EXTRA_CRIT === 'undefined') return;3265  box.innerHTML = EXTRA_CRIT.map(d => `<label><input type="checkbox" value="${d.key}">${esc(d.label)}</label>`).join('');3266  box.addEventListener('change', e => {3267    const cb = e.target; if (!cb || cb.type !== 'checkbox') return;3268    if (cb.checked) mapExtras.add(cb.value); else mapExtras.delete(cb.value);3269    if (mapVisible && map) updateMapMarkers();3270  });3271})();32723273function drawRadiusCircle(){3274  if (!map) return;3275  if (radiusCircle){ try { map.removeLayer(radiusCircle); } catch(e){} radiusCircle = null; }3276  if (radiusMarker){ try { map.removeLayer(radiusMarker); } catch(e){} radiusMarker = null; }3277  if (radiusCenter){3278    radiusCircle = L.circle([radiusCenter.lat, radiusCenter.lng], { radius: radiusKm*1000, color:'#16A34A', weight:2, fillColor:'#16A34A', fillOpacity:0.08 }).addTo(map);3279    radiusMarker = L.marker([radiusCenter.lat, radiusCenter.lng], { icon: L.divIcon({ className:'', html:'<div style="background:#16A34A;color:#fff;border-radius:50%;width:26px;height:26px;display:flex;align-items:center;justify-content:center;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.3)">📍</div>', iconSize:[26,26], iconAnchor:[13,13] }) }).addTo(map);3280  }3281}32823283function renderMapResults(){3284  const list = document.getElementById('mr-list'), head = document.getElementById('mr-head');3285  if (!list || !map) return;3286  window._mapFeaturesOpen = false;   // on affiche la liste → les caractéristiques ne sont plus à l'écran3287  const items = Array.isArray(window._mapItems) ? window._mapItems : [];3288  const b = map.getBounds();3289  const vis = items.filter(it => b.contains([it.coords.lat, it.coords.lng]));3290  const fab = document.getElementById('fab-results'); if (fab) fab.textContent = '📋 ' + vis.length + ' logement' + (vis.length>1?'s':'');3291  if (head){ head.style.display = ''; head.textContent = vis.length ? (vis.length + ' logement' + (vis.length>1?'s':'') + ' dans la zone') : 'Aucun logement visible'; }3292  if (!vis.length){ list.innerHTML = '<div class="mr-empty">Aucun logement dans cette zone.<br>Déplace ou dézoome la carte.</div>'; return; }3293  const photoOf = (l) => { let p = l.main_photo || (Array.isArray(l.photos)&&l.photos.length?l.photos[0]:''); if(!p && Array.isArray(l.photo_variants)){ const pv=l.photo_variants.find(v=>v&&Array.isArray(v.photos)&&v.photos.length); if(pv) p=(pv.main&&pv.photos.includes(pv.main))?pv.main:pv.photos[0]; } return p; };3294  list.innerHTML = vis.map((it, i) => {3295    const l = it.l; const pu = photoOf(l);3296    const img = pu ? `<img loading="lazy" src="${esc(simg(pu,200,60))}" alt="">` : '<div class="mr-noimg">🏠</div>';3297    const price = (minLoyer(l)!=null) ? ((hasVariants(l)||hasUnits(l)?'dès ':'')+minLoyer(l).toLocaleString('fr-CA')+' $/mois') : 'Prix sur demande';3298    return `<div class="mr-card" data-mr="${i}">${img}<div class="mr-b"><div class="mr-t">${esc(l.titre||'')}</div><div class="mr-sub">📍 ${esc(l.ville||'')}${l.grandeur?' · '+esc(l.grandeur):''}</div><div class="mr-price">${esc(price)}</div></div></div>`;3299  }).join('');3300  list.querySelectorAll('[data-mr]').forEach(el => el.addEventListener('click', () => {3301    const it = vis[+el.getAttribute('data-mr')]; if (!it) return;3302    try { map.panTo([it.coords.lat, it.coords.lng]); } catch(e){}3303    // Affiche les caractéristiques du logement dans ce même panneau.3304    showListingFeatures(allListings.indexOf(it.l));3305  }));3306}33073308// Affiche les caractéristiques d'un logement dans le panneau de droite (bouton 📋 de la bulle).3309window.showListingFeatures = function(idx){3310  const l = allListings[idx]; if (!l) return;3311  const list = document.getElementById('mr-list'), head = document.getElementById('mr-head');3312  if (!list) return;3313  window._mapFeaturesOpen = true;    // empêche moveend/zoomend (ex. panTo) de réécrire la liste3314  document.body.classList.add('ms-results-open');   // mobile : ouvre la feuille de résultats3315  try { map && map.closePopup(); } catch(e){}3316  if (head) head.style.display = 'none';   // le contenu a son propre titre + bouton retour3317  const photoOf = (x) => { let p = x.main_photo || (Array.isArray(x.photos)&&x.photos.length?x.photos[0]:''); if(!p && Array.isArray(x.photo_variants)){ const pv=x.photo_variants.find(v=>v&&Array.isArray(v.photos)&&v.photos.length); if(pv) p=(pv.main&&pv.photos.includes(pv.main))?pv.main:pv.photos[0]; } return p; };3318  const pu = photoOf(l);3319  const price = (minLoyer(l)!=null) ? ((hasVariants(l)||hasUnits(l)?'dès ':'')+minLoyer(l).toLocaleString('fr-CA')+' $/mois') : 'Prix sur demande';3320  const u = (Array.isArray(l.units) ? l.units.filter(x => isAvail(x)) : [])[0] || l.carac || {};3321  const g = v => (v != null && v !== '') ? v : null;3322  const rows = [];3323  const add = (ic,k,v) => { if (v) rows.push(`<div style="display:flex;gap:8px;padding:7px 0;border-bottom:1px solid #f0f0ee;font-size:13px"><span style="flex:0 0 108px;color:#888">${ic} ${k}</span><span style="font-weight:600;color:#333">${esc(String(v))}</span></div>`); };3324  add('🏠','Type', grandeurDisplay(l) || l.grandeur);3325  add('💰','Loyer', price);3326  add('📅','Disponibilité', dispoDisplay(l) || l.date_dispo_affichage);3327  add('🛏️','Chambres', g(l.chambres) || g(u.chambres));3328  add('🚿','Salles de bain', g(l.sdb) || g(u.sdb));3329  add('🏢','Étage', g(l.etage) || g(u.etage));3330  add('🚗','Stationnement', g(l.stationnement) || g(u.stationnement));3331  const sup = g(l.superficie) || g(u.superficie); add('📐','Superficie', sup ? (sup + ' pi²') : null);3332  add('🐾','Animaux', g(l.animaux));3333  const feats = (typeof EXTRA_CRIT !== 'undefined') ? EXTRA_CRIT.filter(d => { try { return listingHasExtra(l, d); } catch(e){ return false; } }) : [];3334  const featHtml = feats.length3335    ? `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">${feats.map(d => `<span style="background:#E6F4EA;color:#166534;font-size:12px;font-weight:600;border-radius:20px;padding:3px 10px">✓ ${esc(d.label)}</span>`).join('')}</div>`3336    : '<div style="color:#999;font-size:12.5px;margin-top:6px">Aucune caractéristique particulière indiquée.</div>';3337  const desc = l.description ? `<div style="font-size:12.5px;color:#555;line-height:1.5;margin-top:10px;white-space:pre-wrap">${esc(String(l.description).slice(0,400))}${String(l.description).length>400?'…':''}</div>` : '';3338  list.innerHTML = `3339    <button id="mr-back" class="mr-back-btn">← Retour à la liste</button>3340    <div style="background:#fff;border:1px solid #e8e8e5;border-radius:12px;overflow:hidden">3341      ${pu ? `<img src="${esc(simg(pu,360,62))}" style="width:100%;height:150px;object-fit:cover;display:block" alt="">` : ''}3342      <div style="padding:12px 14px">3343        <div style="font-size:15px;font-weight:800;color:#222;line-height:1.25">${esc(l.titre||'')}</div>3344        <div style="font-size:12.5px;color:#777;margin:3px 0 8px">📍 ${esc(l.ville||'')}</div>3345        ${rows.join('')}3346        <div style="font-size:11.5px;font-weight:800;color:#7a4a00;text-transform:uppercase;letter-spacing:.04em;margin:13px 0 2px">Caractéristiques</div>3347        ${featHtml}${desc}3348        <button class="popup-btn" style="margin-top:12px;width:100%" onclick="document.getElementById('map-section').style.display='none';document.getElementById('map-toggle-btn').classList.remove('active');document.body.classList.remove('map-open');mapVisible=false;openDetail(${idx})">Voir la fiche complète</button>3349      </div>3350    </div>`;3351  const back = document.getElementById('mr-back'); if (back) back.addEventListener('click', renderMapResults);3352  // Repart du haut du panneau (chaque zone a son propre défilement).3353  const mr = document.getElementById('map-results'); if (mr) mr.scrollTop = 0;3354};33553356(function initRadius(){3357  const btn = document.getElementById('ms-radius-btn'), km = document.getElementById('ms-radius-km'), hint = document.getElementById('ms-radius-hint');3358  if (!btn) return;3359  btn.addEventListener('click', () => {3360    if (radiusCenter){ radiusCenter = null; radiusPicking = false; btn.textContent = '📍 Placer'; btn.classList.remove('active','pick'); if (hint) hint.textContent = 'Clique « Placer » puis un point sur la carte.'; if (mapVisible && map) updateMapMarkers(); return; }3361    radiusPicking = !radiusPicking;3362    btn.classList.toggle('pick', radiusPicking);3363    btn.textContent = radiusPicking ? '🖱 Clique la carte…' : '📍 Placer';3364    if (hint) hint.textContent = radiusPicking ? 'Clique maintenant un point sur la carte.' : 'Clique « Placer » puis un point sur la carte.';3365  });3366  if (km) km.addEventListener('change', () => { radiusKm = parseInt(km.value,10)||20; if (radiusCenter && mapVisible && map) updateMapMarkers(); });3367})();33683369// Plein écran (API Fullscreen) sur la section carte — surtout utile sur mobile.3370function toggleMapFullscreen(){3371  const el = document.getElementById('map-section'); if (!el) return;3372  const d = document;3373  const isFs = d.fullscreenElement || d.webkitFullscreenElement;3374  try {3375    if (!isFs){ const req = el.requestFullscreen || el.webkitRequestFullscreen; if (req) req.call(el); }3376    else { const ex = d.exitFullscreen || d.webkitExitFullscreen; if (ex) ex.call(d); }3377  } catch(e){}3378  setTimeout(() => { try { if (map) { map.invalidateSize(); renderMapResults(); } } catch(e){} }, 300);3379}3380window.toggleMapFullscreen = toggleMapFullscreen;3381document.addEventListener('fullscreenchange', () => { setTimeout(() => { try { if (map){ map.invalidateSize(); } } catch(e){} }, 150); });33823383(function resetExtrasRadius(){3384  const rb = document.getElementById('ms-reset'); if (!rb) return;3385  rb.addEventListener('click', () => {3386    mapExtras.clear(); document.querySelectorAll('#ms-extras input').forEach(c => c.checked = false);3387    radiusCenter = null; radiusPicking = false;3388    const b = document.getElementById('ms-radius-btn'); if (b){ b.textContent = '📍 Placer'; b.classList.remove('active','pick'); }3389    const h = document.getElementById('ms-radius-hint'); if (h) h.textContent = 'Clique « Placer » puis un point sur la carte.';3390  });3391})();33923393function wireMapEvents(){3394  if (!map || map.__wired) return; map.__wired = true;3395  map.on('moveend zoomend', () => { if (!window._mapFeaturesOpen) renderMapResults(); });3396  map.on('click', (e) => {3397    if (!radiusPicking) return;3398    radiusCenter = { lat: e.latlng.lat, lng: e.latlng.lng };3399    radiusKm = parseInt((document.getElementById('ms-radius-km')||{}).value,10) || 20;3400    radiusPicking = false;3401    const btn = document.getElementById('ms-radius-btn'), hint = document.getElementById('ms-radius-hint');3402    if (btn){ btn.classList.remove('pick'); btn.classList.add('active'); btn.textContent = '✕ Retirer le point'; }3403    if (hint) hint.textContent = 'Rayon actif. Clique « Retirer » pour l\'enlever.';3404    updateMapMarkers();3405  });3406}34073408// (Carte mobile : auto-réduction au scroll retirée — elle changeait les limites de la carte3409//  donc les résultats, et perturbait le défilement. Mise en page fixe + stable désormais.)34103411function toggleMap() {3412  mapVisible = !mapVisible;3413  const section = document.getElementById('map-section');3414  const btn = document.getElementById('map-toggle-btn');3415  section.classList.toggle('open', mapVisible);3416  btn.classList.toggle('active', mapVisible);3417  document.body.classList.toggle('map-open', mapVisible);   // masque le contenu du bas3418  if (!mapVisible){ document.body.classList.remove('ms-filters-open','ms-results-open'); section.classList.remove('map-collapsed'); try { if (document.fullscreenElement) document.exitFullscreen(); } catch(e){} }3419  btn.innerHTML = mapVisible3420    ? `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg> Masquer la carte`3421    : `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg> Voir la carte`;3422  if (mapVisible) {3423    syncMapSide();3424    if (!map) initMap();3425    else { updateMapMarkers(); setTimeout(() => { try { map.invalidateSize(); } catch(e){} }, 60); }3426    section.scrollIntoView({ behavior: 'smooth', block: 'start' });3427  }3428}3429// Panneau de filtres de la carte → pilote les mêmes filtres que la liste (applyFilters).3430function syncMapSide(){3431  const $g = id => document.getElementById(id);3432  const mv = $g('ms-ville'), fv = $g('f-ville');3433  if (mv && fv && mv.options.length <= 1) mv.innerHTML = '<option value="">Toutes</option>' + [...fv.options].filter(o => o.value).map(o => `<option>${o.value.replace(/"/g,'')}</option>`).join('');3434  const pair = [['ms-search','f-search'],['ms-ville','f-ville'],['ms-type','f-taille'],['ms-etage','f-etage'],['ms-animaux','f-animaux'],['ms-pmin','f-pmin'],['ms-pmax','f-pmax']];3435  pair.forEach(([a,b]) => { const x = $g(a), y = $g(b); if (x && y) x.value = y.value; });3436}3437function mapSideApply(){3438  const $g = id => document.getElementById(id);3439  [['f-search','ms-search'],['f-ville','ms-ville'],['f-taille','ms-type'],['f-etage','ms-etage'],['f-animaux','ms-animaux'],['f-pmin','ms-pmin'],['f-pmax','ms-pmax']].forEach(([dst,src]) => { const y = $g(dst), x = $g(src); if (y && x) y.value = x.value; });3440  applyFilters();3441}3442(function wireMapSide(){3443  const $g = id => document.getElementById(id);3444  ['ms-ville','ms-type','ms-etage','ms-animaux'].forEach(id => { const el = $g(id); if (el) el.addEventListener('change', mapSideApply); });3445  ['ms-search','ms-pmin','ms-pmax'].forEach(id => { const el = $g(id); if (el) el.addEventListener('input', mapSideApply); });3446  const rb = $g('ms-reset'); if (rb) rb.addEventListener('click', () => { ['ms-search','ms-ville','ms-type','ms-etage','ms-animaux','ms-pmin','ms-pmax'].forEach(id => { const el = $g(id); if (el) el.value = ''; }); if (window.resetFilters) resetFilters(); });3447})();34483449function initMap() {3450  if (typeof L === 'undefined') {3451    console.error('Leaflet non chargé');3452    return;3453  }3454  // Zoom molette : on DÉSACTIVE celui de Leaflet (certains pilotes de souris envoient un3455  // « delta » énorme → la carte sautait plusieurs niveaux d'un coup) et on le remplace par3456  // un zoom à PAS FIXE (±0,5 niveau par cran, peu importe l'ampleur du delta) + throttle.3457  map = L.map('map', {3458    scrollWheelZoom: false,3459    zoomSnap: 0.25,3460    zoomDelta: 0.53461  }).setView([45.8797, -72.4939], 11);3462  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {3463    attribution: '© OpenStreetMap contributors',3464    maxZoom: 193465  }).addTo(map);3466  (function customWheelZoom(){3467    // Zoom molette ROBUSTE : beaucoup de souris envoient une RAFALE d'événements par cran.3468    // On ne zoome donc qu'UNE fois par geste (1 niveau), centré sur le milieu de la carte,3469    // et on ne se ré-arme que lorsque la molette s'est ARRÊTÉE (130 ms sans événement).3470    // Impossible de sauter plusieurs niveaux, quelle que soit l'ampleur du delta.3471    let armed = true, idleTimer = null;3472    map.getContainer().addEventListener('wheel', (e) => {3473      e.preventDefault();3474      if (armed) {3475        armed = false;3476        const dir = e.deltaY < 0 ? 1 : -1;   // molette vers le haut = zoom avant3477        const target = Math.max(map.getMinZoom(), Math.min(map.getMaxZoom(), Math.round(map.getZoom()) + dir));3478        map.setZoom(target, { animate: false });3479      }3480      clearTimeout(idleTimer);3481      idleTimer = setTimeout(() => { armed = true; }, 130);3482    }, { passive: false });3483  })();3484  wireMapEvents();3485  updateMapMarkers();34863487  // Légende3488  const legend = L.control({position: 'bottomright'});3489  legend.onAdd = function() {3490    const div = L.DomUtil.create('div');3491    div.style.cssText = 'background:#fff;padding:10px 12px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.15);font-family:system-ui,sans-serif;font-size:12px;line-height:1.8';3492    div.innerHTML = `3493      <div style="font-weight:600;margin-bottom:4px;color:#444">Grandeur</div>3494      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#3B82F6;margin-right:6px"></span>3½</div>3495      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#FB8C00;margin-right:6px"></span>4½</div>3496      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#F97316;margin-right:6px"></span>5½</div>3497      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#8B5CF6;margin-right:6px"></span>6½</div>3498      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#EF4444;margin-right:6px"></span>Maison</div>3499    `;3500    return div;3501  };3502  legend.addTo(map);3503}35043505async function geocode(address) {3506  if (geocodeCache[address]) return geocodeCache[address];3507  try {3508    const q = encodeURIComponent(address + ', Québec, Canada');3509    const url = `https://nominatim.openstreetmap.org/search?format=json&q=${q}&limit=1`;3510    const resp = await fetch(url, {headers:{'Accept-Language':'fr'}});3511    const data = await resp.json();3512    if (data.length > 0) {3513      const coords = {lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon)};3514      geocodeCache[address] = coords;3515      return coords;3516    }3517  } catch(e) {}3518  return null;3519}35203521async function updateMapMarkers() {3522  if (!map) return;3523  markers.forEach(m => map.removeLayer(m));3524  markers = [];35253526  const grandeurColors = {3527    '3½': '#3B82F6',3528    '4½': '#FB8C00',3529    '5½': '#F97316',3530    '6½': '#8B5CF6',3531    'Maison': '#EF4444'3532  };35333534  const photoOf = (l) => {3535    let p = l.main_photo || (Array.isArray(l.photos) && l.photos.length ? l.photos[0] : '');3536    if (!p && Array.isArray(l.photo_variants)) {3537      const pv = l.photo_variants.find(v => v && Array.isArray(v.photos) && v.photos.length);3538      if (pv) p = (pv.main && pv.photos.includes(pv.main)) ? pv.main : pv.photos[0];3539    }3540    return p;3541  };3542  const priceTxt = (l) => { const v = minLoyer(l); return v != null ? v.toLocaleString('fr-CA') + ' $' : 'Prix sur demande'; };3543  const openFicheJs = (l) => `try{if(document.fullscreenElement)document.exitFullscreen()}catch(e){};document.body.classList.remove('map-open','ms-results-open','ms-filters-open');document.getElementById('map-section').classList.remove('open');document.getElementById('map-section').style.display='none';document.getElementById('map-toggle-btn').classList.remove('active');mapVisible=false;openDetail(${allListings.indexOf(l)})`;35443545  // Résoudre les coordonnées de chaque logement filtré (un seul géocodage par adresse)…3546  const resolved = await Promise.all(filtered.map(async (l) => {3547    if (!listingHasAvailability(l)) return null;   // logement entièrement loué → pas de marqueur3548    if (mapExtras.size && !mapExtrasMatch(l)) return null;   // filtre caractéristiques3549    const address = l.adresse_geo || l.id || '';3550    if (!address) return null;3551    const gpsMatch = address.match(/^\s*(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*$/);3552    const coords = gpsMatch ? { lat: parseFloat(gpsMatch[1]), lng: parseFloat(gpsMatch[2]) } : await geocode(address);3553    if (!coords) return null;3554    if (radiusCenter && haversineKm(radiusCenter, coords) > radiusKm) return null;   // filtre rayon3555    return { l, coords };3556  }));35573558  // …puis REGROUPER ceux qui partagent exactement la même position (plusieurs modèles d'un3559  // même immeuble). Sans ça, les marqueurs s'empilent et un seul est cliquable.3560  const groups = new Map();3561  resolved.forEach(r => {3562    if (!r) return;3563    const key = r.coords.lat.toFixed(5) + ',' + r.coords.lng.toFixed(5);3564    if (!groups.has(key)) groups.set(key, { coords: r.coords, items: [] });3565    groups.get(key).items.push(r.l);3566  });35673568  window._mapItems = [];   // {l, coords, marker} pour le panneau de résultats3569  groups.forEach(g => {3570    const items = g.items;3571    if (items.length === 1) {3572      const l = items[0];3573      const types = offeredTypes(l);3574      const label = types.length ? (types.length === 1 ? types[0] : (types[0] + '…')) : (l.grandeur || '?');3575      const color = grandeurColors[types[0] || l.grandeur] || '#FB8C00';3576      const pu = photoOf(l);3577      const photoImg = pu ? `<img class="popup-photo" src="${esc(simg(pu, 320, 55))}" alt="">` : '';3578      const markerIcon = L.divIcon({3579        className: '',3580        html: `<div style="background:${color};color:#fff;border-radius:50%;width:34px;height:34px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3)">${esc(label)}</div>`,3581        iconSize: [34, 34], iconAnchor: [17, 17], popupAnchor: [0, -18]3582      });3583      const marker = L.marker([g.coords.lat, g.coords.lng], { icon: markerIcon }).bindPopup(`3584        ${photoImg}3585        <div class="popup-body">3586          <div class="popup-title">${esc(l.titre)}</div>3587          <div class="popup-dispo" style="color:#555">${esc(grandeurDisplay(l))}</div>3588          <div class="popup-price">${hasVariants(l)||hasUnits(l)?'dès ':''}${priceTxt(l)} / mois</div>3589          <div class="popup-dispo">${esc(dispoDisplay(l))}</div>3590          <div style="display:flex;gap:6px;margin-top:8px">3591            <button class="popup-btn" style="flex:1 1 auto;width:auto;margin:0" onclick="${openFicheJs(l)}">Voir la fiche</button>3592            <button class="popup-btn" style="flex:0 0 auto;width:auto;margin:0;background:#0E6E82;padding:6px 12px" title="Voir les caractéristiques" onclick="showListingFeatures(${allListings.indexOf(l)})">📋 Détails</button>3593          </div>3594        </div>3595      `);3596      marker.addTo(map); markers.push(marker);3597      window._mapItems.push({ l, coords: g.coords, marker });3598    } else {3599      // Plusieurs logements à la même adresse → un marqueur « immeuble » listant les modèles.3600      const sorted = items.slice().sort((a, b) => (minLoyer(a) != null ? minLoyer(a) : 1e9) - (minLoyer(b) != null ? minLoyer(b) : 1e9));3601      const base = (sorted[0].titre || '').split(' — ')[0] || sorted[0].ville || 'Logements';3602      const pu = photoOf(sorted.find(x => photoOf(x)) || sorted[0]);3603      const photoImg = pu ? `<img class="popup-photo" src="${esc(simg(pu, 320, 55))}" alt="">` : '';3604      const rows = sorted.map(l => {3605        const t = offeredTypes(l)[0] || l.grandeur || '?';3606        const bg = grandeurColors[t] || '#FB8C00';3607        return `<div class="popup-row"><button class="pr-main" onclick="${openFicheJs(l)}"><span class="pr-badge" style="background:${bg}">${esc(t)}</span><span class="pr-txt"><b>dès ${priceTxt(l)}</b><span class="pr-sub">${esc(dispoDisplay(l))}</span></span></button><button class="pr-feat" title="Voir les caractéristiques" onclick="showListingFeatures(${allListings.indexOf(l)})">📋</button></div>`;3608      }).join('');3609      const markerIcon = L.divIcon({3610        className: '',3611        html: `<div style="background:#FB8C00;color:#fff;border-radius:50%;width:38px;height:38px;display:flex;flex-direction:column;align-items:center;justify-content:center;line-height:1;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3)"><span style="font-size:13px;font-weight:800">${items.length}</span><span style="font-size:7px;font-weight:600;letter-spacing:.02em;opacity:.92">LOGTS</span></div>`,3612        iconSize: [38, 38], iconAnchor: [19, 19], popupAnchor: [0, -20]3613      });3614      const marker = L.marker([g.coords.lat, g.coords.lng], { icon: markerIcon }).bindPopup(`3615        ${photoImg}3616        <div class="popup-body">3617          <div class="popup-title">${esc(base)}</div>3618          <div class="popup-dispo" style="color:#555;margin-bottom:7px">${items.length} logements disponibles</div>3619          <div class="popup-list">${rows}</div>3620        </div>3621      `, { maxHeight: 340 });3622      marker.addTo(map); markers.push(marker);3623      items.forEach(l => window._mapItems.push({ l, coords: g.coords, marker }));3624    }3625  });36263627  drawRadiusCircle();3628  // Ajuster la vue : sur le rayon s'il est défini, sinon sur l'ensemble des marqueurs.3629  try { map.invalidateSize(); } catch(e) {}3630  if (radiusCenter && radiusCircle) {3631    try { map.fitBounds(radiusCircle.getBounds().pad(0.08), { maxZoom: 15 }); } catch(e) {}3632  } else if (markers.length > 0) {3633    // Au tout premier affichage, le conteneur venait d'être rendu visible : Leaflet avait3634    // mémorisé une taille de 0, et fitBounds retombait alors sur le zoom 0 (monde entier).3635    const group = L.featureGroup(markers);3636    map.fitBounds(group.getBounds().pad(0.1), { maxZoom: 16 });3637  }3638  renderMapResults();3639}36403641// Mettre à jour la carte quand les filtres changent3642const origRenderTags = renderTags;3643renderTags = function() {3644  origRenderTags();3645  if (mapVisible && map) updateMapMarkers();3646};3647</script>36483649<!-- Journalisation des vues d'appartement (visiteur connecté) -->3650<script type="module">3651import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm';3652const _sb = createClient('https://jfgkriojazaohgjezijj.supabase.co', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpmZ2tyaW9qYXphb2hnamV6aWpqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODEzNDE5NjQsImV4cCI6MjA5NjkxNzk2NH0.VVLfhvpkQ9WgJOhCYW3y6kRZIJbAQXTCANv6mXce1Yw');3653// Exposé en global : le 1er <script> (classique) référence `_sb` (formulaire de location,3654// note interne par unité). Sans ça, `_sb` reste confiné au module → « _sb is not defined ».3655window._sb = _sb;3656let _lastView = null;3657window.logListingView = async (id_app, titre) => {3658  try {3659    if (id_app && id_app === _lastView) return;3660    _lastView = id_app;3661    const { data: { session } } = await _sb.auth.getSession();3662    if (!session) return;3663    await _sb.from('events').insert({ user_id: session.user.id, type: 'listing_view', detail: { id_app: id_app || null, titre: titre || null } });3664  } catch(e) {}3665};36663667// ---- Favoris ----3668window.__favs = new Set();3669async function _loadFavs(){3670  try {3671    const { data: { session } } = await _sb.auth.getSession();3672    if (!session) return;3673    const { data } = await _sb.from('favorites').select('id_app').eq('user_id', session.user.id);3674    window.__favs = new Set((data || []).map(f => f.id_app));3675    if (window.refreshHearts) window.refreshHearts();3676  } catch(e) {}3677}3678window.toggleFavorite = async (id_app, btn) => {3679  if (!id_app) return;3680  const { data: { session } } = await _sb.auth.getSession();3681  if (!session){ if (confirm('Connecte-toi pour enregistrer tes favoris. Aller à la page profil ?')) location.href = '/compte.html'; return; }3682  try {3683    if (window.__favs.has(id_app)){3684      await _sb.from('favorites').delete().eq('user_id', session.user.id).eq('id_app', id_app);3685      window.__favs.delete(id_app);3686    } else {3687      await _sb.from('favorites').insert({ user_id: session.user.id, id_app });3688      window.__favs.add(id_app);3689    }3690    if (btn) btn.classList.toggle('active', window.__favs.has(id_app));3691    if (window.refreshHearts) window.refreshHearts();   // synchronise cartes + fiche ouverte3692  } catch(e) {}3693};3694_loadFavs();36953696// ---- Infos internes (admins + agents de location — RLS protège les données) ----3697window.__isStaff = false;3698(async () => {3699  try {3700    const { data: { session } } = await _sb.auth.getSession();3701    if (!session){ document.body.classList.remove('is-staff'); try { localStorage.removeItem('gisr_staff'); } catch(e){} return; }3702    const { data } = await _sb.rpc('is_staff');3703    window.__isStaff = data === true;3704    try { if (window.__isStaff) localStorage.setItem('gisr_staff', '1'); else localStorage.removeItem('gisr_staff'); } catch(e){}3705    document.body.classList.toggle('is-staff', window.__isStaff);3706    // Agent/staff : la barre agent + l'outil « Créer une sélection » restent ; AUCUNE info interne3707    // n'est révélée sur le site public (le badge « infos internes visibles » est supprimé).3708    if (window.__isStaff){ const ast = document.getElementById('agent-share-top'); if (ast) ast.style.display = 'inline-flex'; if (typeof renderAgentNewRail === 'function') renderAgentNewRail(); }3709  } catch(e) {}3710})();3711const INT_VIEW = [3712  ['🏢 Immeuble', [['date_construction','Date de construction'],['directive_acces_immeuble',"Directive d'accès à l'immeuble"],['qualite_locataires',"Qualité des locataires dans l'immeuble"],['notes','Notes internes générales']]],3713  ['🏠 Logement — Accès & emplacements', [['adresse_exacte','Adresse exacte'],['directive_acces',"Directive d'accès"],['loc_boite_lettres','Boîte aux lettres'],['loc_cabanon','Cabanon'],['borne_electrique','Borne électrique — conditions']]],3714  ['Locataire en place', [['locataire_nom','Nom'],['locataire_tel','Téléphone'],['locataire_courriel','Courriel']]],3715  ['Coûts & négociation', [['cout_electricite',"Coûts d'électricité (annuel, estimé)"],['tolerance_animaux','Tolérance animaux (réelle)'],['flexibilite_prix','Flexibilité du prix'],['promotion_possible','Promotion possible'],['psl_accepte','PSL accepté']]],3716  ['Appréciation & sélection', [['complement_info',"Complément d'information"],['commentaire_honnete','Commentaire honnête'],['problematique_connue','Problématique connue'],['selection_candidat','Sélection candidat']]]3717];3718const _intE2 = s => String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');3719const _intLbl = t => `<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:#999;margin-bottom:2px">${t}</div>`;3720const _intSec = t => `<div style="font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;color:#8a5a00;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #f0d98b">${t}</div>`;3721const _intBox = inner => `<div style="background:#fffdf3;border:1px solid #f0d98b;border-radius:12px;padding:14px;font-size:14px;line-height:1.5;color:#333">${inner}</div>`;3722// Rend les groupes de champs internes (INT_VIEW) présents dans la ligne listings_internal.3723function _intGroupsHtml(d, groups){3724  let html = '', any = false;3725  (groups || []).forEach(([title, fields]) => {3726    const rows = fields.filter(([k]) => d[k]).map(([k, label]) => `<div style="margin-bottom:10px">${_intLbl(_intE2(label))}${_intE2(d[k])}</div>`).join('');3727    if (rows){ any = true; html += _intSec(_intE2(title)) + rows; }3728  });3729  return { html, any };3730}3731// Immeuble (plan) ou fiche appartement unique. buildingOnly=true → seulement le bloc 🏢 Immeuble.3732window.showInternalInfo = async (pk, opts) => {3733  opts = opts || {};3734  const box = document.getElementById('d-internal'); if (!box) return;3735  const btn = document.getElementById('d-internal-btn'); if (btn) btn.style.display = 'none';3736  box.innerHTML = '<div style="color:#888;font-size:13px">Chargement…</div>';3737  try {3738    const data = await fetchInternal(pk);   // staff = table complète ; propriétaire = RPC curatif3739    const d = data || {};3740    // Immeuble : on affiche TOUS les champs internes de niveau immeuble (coûts, négociation,3741    // appréciation, notes…). Les champs propres à un logement (adresse, locataire) restent vides3742    // ici — ils s'affichent en cliquant un appartement du plan — donc ne s'affichent pas.3743    const r = _intGroupsHtml(d, INT_VIEW);3744    let html = r.html, any = r.any;3745    if (!opts.buildingOnly){3746      const vn = d.variant_notes || {};3747      const vnRows = Object.keys(vn).filter(k => vn[k]).map(k => { const p = k.split('||'); return `<div style="margin-bottom:8px">${_intLbl((p[0]?('Variante '+_intE2(p[0])):'') + (p[1]?' · '+_intE2(p[1]):''))}${_intE2(vn[k])}</div>`; }).join('');3748      if (vnRows){ any = true; html += _intSec('Notes par étage') + vnRows; }3749    }3750    box.innerHTML = any ? _intBox(html) : _intBox(`<div style="color:#999;font-size:13px">${opts.buildingOnly ? 'Aucune info interne au niveau immeuble. Les infos par logement : clique un appartement dans le plan.' : 'Aucune information interne saisie.'}</div>`);3751  } catch(err) { box.innerHTML = '<div style="color:#b91c1c;font-size:13px">Impossible de charger les infos internes.</div>'; }3752};3753// Carte d'un appartement (virtuelle) : infos 🏢 IMMEUBLE + 🔒 notes de CETTE unité.3754window.showApartmentInternal = async (pk, unitId, viewPk) => {3755  const box = document.getElementById('d-internal'); if (!box) return;3756  box.innerHTML = '<div style="color:#888;font-size:13px">Chargement…</div>';3757  try {3758    const data = await fetchInternal(pk);3759    if (!document.getElementById('detail-overlay').classList.contains('open') || (viewPk != null && window.__currentPk !== viewPk)) return;3760    const d = data || {};3761    const bldg = _intGroupsHtml(d, INT_VIEW);   // tous les champs internes de niveau immeuble3762    const note = d.unit_notes && d.unit_notes[unitId];3763    const unitHtml = window.renderUnitIntNotes ? window.renderUnitIntNotes(note) : '';3764    box.innerHTML = (bldg.any ? _intBox(bldg.html) : '') + `<div class="up-sec" style="color:#8a5a00">🔒 Notes internes (cet appartement)</div>${unitHtml}`;3765  } catch(e){ box.innerHTML = ''; }3766};37673768// ============ Widget de clavardage (visiteur <-> équipe) ============3769(function(){3770  const $c = id => document.getElementById(id);3771  const bubble = $c('chatw-bubble'), panel = $c('chatw-panel'), body = $c('chatw-body');3772  if (!bubble) return;3773  // Lien de partage propriétaire (?p=) : pas de clavardage Gestion ISR (le visiteur contacte le propriétaire)3774  if (new URLSearchParams(location.search).get('p')){ const w = $c('chatw'); if (w) w.style.display = 'none'; return; }3775  let cwOpen = false, cwUid = null, cwSub = null, cwUnread = 0, cwLastTs = null, cwPollTimer = null, cwNote = '';3776  const cwShown = new Set();3777  const cwEsc = s => String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');3778  const cwFmt = t => new Date(t).toLocaleString('fr-CA',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});3779  function cwBadge(n){ cwUnread = n; const b = $c('chatw-badge'); if (!b) return; if (n > 0){ b.textContent = n; b.style.display = 'flex'; } else b.style.display = 'none'; }3780  bubble.addEventListener('click', () => cwOpen ? cwClose() : cwOpenPanel());3781  $c('chatw-close').addEventListener('click', cwClose);3782  // Bulle d'accroche : apparaît une fois par visite après un court délai, invite à écrire.3783  const teaser = $c('chatw-teaser');3784  function cwHideTeaser(){ if (teaser) teaser.style.display = 'none'; }3785  function cwDismissTeaser(){ cwHideTeaser(); try { sessionStorage.setItem('chatw-teaser-seen', '1'); } catch(_){} }3786  if (teaser){3787    let shown = false; try { shown = sessionStorage.getItem('chatw-teaser-seen') === '1'; } catch(_){}3788    if (!shown){3789      setTimeout(() => { if (!cwOpen){ teaser.style.display = 'block'; try { sessionStorage.setItem('chatw-teaser-seen', '1'); } catch(_){} setTimeout(() => { if (teaser.style.display === 'block') teaser.style.display = 'none'; }, 12000); } }, 4500);3790    }3791    teaser.addEventListener('click', () => { cwHideTeaser(); if (!cwOpen) cwOpenPanel(); });3792    const tx = $c('chatw-teaser-x'); if (tx) tx.addEventListener('click', e => { e.stopPropagation(); cwDismissTeaser(); });3793    teaser.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); cwHideTeaser(); if (!cwOpen) cwOpenPanel(); } });3794  }3795  function cwOpenPanel(){ cwOpen = true; cwHideTeaser(); panel.style.display = 'flex'; cwRoute(); }3796  function cwClose(){ cwOpen = false; panel.style.display = 'none'; }3797  async function cwRoute(){ const { data:{ session } } = await _sb.auth.getSession(); if (session){ cwUid = session.user.id; cwShowChat(); } else cwShowStart(); }3798  function cwShowStart(){3799    body.innerHTML = `<div class="chatw-pad"><p class="chatw-intro">Bonjour 👋 Laisse ton prénom et ton courriel pour discuter avec notre équipe. On t'enverra aussi un lien pour activer ton compte (mot de passe + profil).</p>3800      <div class="chatw-field"><label>Prénom</label><input id="cw-prenom" autocomplete="given-name"></div>3801      <div class="chatw-field"><label>Courriel</label><input id="cw-courriel" type="email" autocomplete="email"></div>3802      <div class="chatw-err" id="cw-err"></div>3803      <button class="chatw-btn primary" id="cw-start">Accéder au chat</button></div>`;3804    $c('cw-start').onclick = cwStart;3805  }3806  function cwErr(m){ const e = $c('cw-err'); if (e){ e.textContent = m; e.style.display = 'block'; } }3807  async function cwStart(){3808    const prenom = ($c('cw-prenom').value||'').trim(), courriel = ($c('cw-courriel').value||'').trim();3809    if (!prenom || !courriel){ cwErr("Merci d'indiquer ton prénom et ton courriel."); return; }3810    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(courriel)){ cwErr('Adresse courriel invalide.'); return; }3811    $c('cw-start').disabled = true;3812    const { data, error } = await _sb.auth.signInAnonymously();3813    if (error){ $c('cw-start').disabled = false; cwErr('Erreur : ' + error.message); return; }3814    cwUid = data.user.id;3815    await _sb.from('profiles').upsert({ user_id: cwUid, nom: prenom, courriel, source: 'chat' });3816    try {3817      const { error: ue } = await _sb.auth.updateUser({ email: courriel, data: { full_name: prenom } }, { emailRedirectTo: location.origin + '/compte.html?activation=1' });3818      cwNote = ue ? '' : "📧 Un courriel d'activation t'a été envoyé — clique le lien pour choisir ton mot de passe et finaliser ton compte.";3819    } catch(_){ cwNote = ''; }3820    cwShowChat();3821  }3822  async function cwShowChat(){3823    try { const { data:{ session } } = await _sb.auth.getSession(); if (session && _sb.realtime && _sb.realtime.setAuth) await _sb.realtime.setAuth(session.access_token); } catch(_){}3824    body.innerHTML = `<div id="chatw-msgs"></div><form id="chatw-form"><input id="chatw-input" placeholder="Écris ton message…" autocomplete="off"><button type="submit" aria-label="Envoyer">➤</button></form>`;3825    cwShown.clear(); cwLastTs = null;3826    const { data } = await _sb.from('messages').select('*').eq('user_id', cwUid).order('created_at');3827    const box = $c('chatw-msgs');3828    if (!data || !data.length){ box.innerHTML = '<div id="cw-empty" style="color:#999;font-size:13px;text-align:center;margin:auto">Écris-nous, on te répond rapidement 👇</div>'; }3829    else data.forEach(cwAppend);3830    if (cwNote){ const bx = $c('chatw-msgs'); if (bx) bx.insertAdjacentHTML('afterbegin', '<div style="background:#FFF7EF;border:1px solid #F3E0CC;color:#8a5a2b;font-size:12.5px;border-radius:10px;padding:8px 10px;margin-bottom:6px">' + cwNote + '</div>'); }3831    cwScroll();3832    _sb.rpc('mark_my_messages_read');3833    cwBadge(0); cwEnsurePoll();3834    $c('chatw-form').addEventListener('submit', cwSend);3835    if (!cwSub){3836      cwSub = _sb.channel('cw-' + cwUid).on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: 'user_id=eq.' + cwUid }, p => {3837        if (p.new.sender === 'admin'){3838          if (cwOpen && $c('chatw-msgs')){ cwAppend(p.new); cwScroll(); _sb.rpc('mark_my_messages_read'); }3839          else cwBadge(cwUnread + 1);3840        }3841      }).subscribe();3842    }3843  }3844  function cwAppend(m){ const box = $c('chatw-msgs'); if (!box) return; if (cwShown.has(m.id)) return; cwShown.add(m.id); if (!cwLastTs || m.created_at > cwLastTs) cwLastTs = m.created_at; const e = $c('cw-empty'); if (e) e.remove(); const d = document.createElement('div'); d.className = 'cw-msg ' + (m.sender === 'visitor' ? 'visitor' : 'admin'); d.innerHTML = cwEsc(m.body) + `<span class="cw-tm">${cwFmt(m.created_at)}</span>`; box.appendChild(d); }3845  function cwEnsurePoll(){ if (!cwPollTimer) cwPollTimer = setInterval(cwPoll, 4000); }3846  async function cwPoll(){3847    if (!cwUid) return;3848    if (!cwOpen || !$c('chatw-msgs')){3849      const { count } = await _sb.from('messages').select('id', { count: 'exact', head: true }).eq('user_id', cwUid).eq('sender', 'admin').eq('read', false);3850      cwBadge(count || 0); return;3851    }3852    let q = _sb.from('messages').select('*').eq('user_id', cwUid).order('created_at');3853    if (cwLastTs) q = q.gt('created_at', cwLastTs);3854    const { data } = await q;3855    if (data && data.length){ data.forEach(cwAppend); cwScroll(); _sb.rpc('mark_my_messages_read'); }3856  }3857  function cwScroll(){ const box = $c('chatw-msgs'); if (box) box.scrollTop = box.scrollHeight; }3858  async function cwSend(e){ e.preventDefault(); const inp = $c('chatw-input'); const b = (inp.value||'').trim(); if (!b || !cwUid) return; inp.value = ''; const { data, error } = await _sb.from('messages').insert({ user_id: cwUid, sender: 'visitor', body: b }).select().single(); if (!error && data){ cwAppend(data); cwScroll(); } }3859  (async () => { const { data:{ session } } = await _sb.auth.getSession(); if (session){ cwUid = session.user.id; cwEnsurePoll(); const { count } = await _sb.from('messages').select('id', { count: 'exact', head: true }).eq('user_id', cwUid).eq('sender', 'admin').eq('read', false); if (count) cwBadge(count); } })();3860})();3861</script>38623863<!-- Widget de clavardage (Gestion ISR) -->3864<style>3865/* Barres d'onglets mobile (candidat #mnav + agent #agnav) — sur la vue publique.3866   Affichées en portrait (≤640px) ET sur téléphone en paysage (hauteur réduite) → la barre3867   reste disponible quand on tourne le téléphone. */3868#mnav,#agnav{display:none}3869@media(max-width:640px), (orientation:landscape) and (max-height:500px){3870  #mnav,#agnav{position:fixed;left:0;right:0;bottom:0;z-index:60;align-items:stretch;gap:0;margin:0;padding:6px 2px calc(6px + env(safe-area-inset-bottom,0px));background:linear-gradient(180deg,#13233f,#0a1526);border-top:1px solid rgba(255,255,255,.08);justify-content:space-around;box-shadow:0 -4px 16px rgba(0,0,0,.25)}3871  #mnav{display:flex}                  /* barre candidat par défaut */3872  body.is-staff #mnav{display:none}    /* staff connecté : on cache la barre candidat… */3873  body.is-staff #agnav{display:flex}   /* …et on montre la barre agent */3874  #mnav button,#agnav button{flex:1 1 0;display:flex;flex-direction:column;align-items:center;gap:3px;background:none;border:none;color:#9fb0c6;font-size:8.5px;font-weight:700;white-space:nowrap;letter-spacing:-.01em;padding:5px 1px;cursor:pointer;position:relative}3875  #mnav button .ic,#agnav button .ic{width:22px;height:22px;display:flex;align-items:center;justify-content:center}3876  #mnav button .ic svg,#agnav button .ic svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}3877  #mnav button.active,#agnav button.active{color:#5b9bff}3878  #mnav button:active,#agnav button:active{opacity:.6}3879  #mnav .mlock{display:none;position:absolute;top:1px;right:16%;font-size:9px;line-height:1}3880  #mnav.locked button[data-lock]{color:#6b7a90;opacity:.6;pointer-events:none;cursor:default}3881  #mnav.locked button[data-lock] .mlock{display:block}3882  #chatw{display:none !important}   /* chat accessible via l'onglet Chat de la barre */3883  .hide-mobile{display:none !important}   /* Alertes nouveautés · Mon profil · Nous joindre (doublons de la barre) */3884  #staff-badge{display:none !important}   /* le badge agent masquait la barre d'onglets du bas */3885  body{padding-bottom:62px}  /* évite que la barre masque le bas de page */3886}3887#chatw{position:fixed;right:18px;bottom:18px;z-index:3000;font-family:system-ui,-apple-system,sans-serif}3888#chatw-bubble{width:60px;height:60px;border-radius:50%;border:2px solid #fff;background:linear-gradient(135deg,#FFB04D,#FB8C00);color:#fff;cursor:pointer;box-shadow:0 6px 20px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center;position:relative;transition:transform .15s,filter .15s}3889#chatw-bubble:hover{filter:brightness(1.05);transform:scale(1.05)}3890#chatw-online{position:absolute;right:2px;bottom:2px;width:15px;height:15px;border-radius:50%;background:#22c55e;border:2.5px solid #fff}3891#chatw-badge{position:absolute;top:-2px;right:-2px;min-width:20px;height:20px;background:#EF4444;color:#fff;border-radius:10px;font-size:12px;font-weight:700;display:none;align-items:center;justify-content:center;padding:0 5px;border:2px solid #fff}3892#chatw-teaser{position:absolute;right:4px;bottom:74px;width:236px;background:#fff;border:1px solid #eee;border-radius:16px;border-bottom-right-radius:5px;box-shadow:0 10px 30px rgba(0,0,0,.16);padding:13px 30px 13px 15px;font-size:13.5px;line-height:1.45;color:#333;cursor:pointer;display:none;transform-origin:bottom right;animation:cwTeaserIn .35s cubic-bezier(.18,.9,.32,1.2)}3893#chatw-teaser:hover{border-color:#FB8C00}3894#chatw-teaser strong{display:block;font-weight:700;color:#1f1f1f;margin-bottom:2px}3895#chatw-teaser-x{position:absolute;top:6px;right:6px;width:22px;height:22px;border:none;background:#f2f2f0;color:#888;border-radius:50%;font-size:12px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}3896#chatw-teaser-x:hover{background:#e6e5e2;color:#444}3897@keyframes cwTeaserIn{from{opacity:0;transform:translateY(8px) scale(.92)}to{opacity:1;transform:none}}3898#chatw-panel{position:absolute;right:0;bottom:70px;width:340px;max-width:calc(100vw - 32px);height:480px;max-height:calc(100vh - 120px);background:#fff;border:1px solid #e8e8e5;border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,.22);display:flex;flex-direction:column;overflow:hidden}3899.chatw-head{background:linear-gradient(135deg,#00B2FF,#006AFF);color:#fff;padding:14px 16px;display:flex;justify-content:space-between;align-items:center}3900.chatw-head b{font-size:15px}3901.chatw-sub{font-size:12px;opacity:.85}3902.chatw-head button{background:rgba(255,255,255,.2);border:none;color:#fff;width:28px;height:28px;border-radius:8px;cursor:pointer;font-size:15px}3903#chatw-body{flex:1;display:flex;flex-direction:column;min-height:0}3904.chatw-pad{padding:18px 16px;overflow-y:auto}3905.chatw-intro{font-size:14px;color:#555;line-height:1.5;margin:0 0 16px}3906.chatw-btn{display:block;width:100%;padding:12px;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;border:none;margin-bottom:10px}3907.chatw-btn.primary{background:#0084FF;color:#fff}3908.chatw-btn.primary:hover{background:#006AFF}3909.chatw-btn.ghost{background:#fff;border:1px solid #ddd;color:#444}3910.chatw-btn.ghost:hover{border-color:#0084FF;color:#006AFF}3911.chatw-field{margin-bottom:10px}3912.chatw-field label{display:block;font-size:12px;font-weight:600;color:#555;margin-bottom:4px}3913.chatw-field input{width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid #d8d8d4;border-radius:9px;font-size:14px;outline:none}3914.chatw-field input:focus{border-color:#0084FF}3915.chatw-back{background:none;border:none;color:#888;font-size:13px;cursor:pointer;padding:0;margin-bottom:12px}3916.chatw-err{font-size:13px;color:#B91C1C;margin:4px 0 8px;display:none}3917#chatw-msgs{flex:1;overflow-y:auto;padding:14px;background:#faf9f7;display:flex;flex-direction:column;gap:8px}3918.cw-msg{max-width:80%;padding:9px 12px;border-radius:14px;font-size:14px;line-height:1.4;white-space:pre-wrap;word-break:break-word}3919.cw-msg.visitor{align-self:flex-end;background:#0084FF;color:#fff;border-bottom-right-radius:4px}3920.cw-msg.admin{align-self:flex-start;background:#fff;border:1px solid #e8e8e5;color:#1a1a1a;border-bottom-left-radius:4px}3921.cw-tm{display:block;font-size:10px;opacity:.7;margin-top:2px}3922#chatw-form{display:flex;gap:8px;padding:10px;border-top:1px solid #f0f0ee;background:#fff}3923#chatw-form input{flex:1;padding:10px 12px;border:1px solid #ddd;border-radius:9px;font-size:14px;outline:none}3924#chatw-form button{background:#0084FF;color:#fff;border:none;border-radius:9px;padding:0 16px;font-weight:700;cursor:pointer;font-size:16px}3925@media(max-width:480px){#chatw-panel{width:calc(100vw - 24px)}}3926</style>3927<div id="chatw">3928  <div id="chatw-teaser" role="button" tabindex="0" aria-label="Ouvrir le clavardage"><button id="chatw-teaser-x" aria-label="Fermer">✕</button><strong>Bonjour 👋</strong>Une question sur un logement ? On vous répond&nbsp;!</div>3929  <button id="chatw-bubble" title="Clavardage" aria-label="Ouvrir le clavardage">3930    <svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 12a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/><path d="M5 20c0-3.3 3.1-5.5 7-5.5s7 2.2 7 5.5"/></svg>3931    <span id="chatw-badge"></span><span id="chatw-online" aria-hidden="true"></span>3932  </button>3933  <div id="chatw-panel" style="display:none">3934    <div class="chatw-head"><div><b>Clavardage</b><div class="chatw-sub">Gestion ISR · réponse rapide</div></div><button id="chatw-close" aria-label="Fermer">✕</button></div>3935    <div id="chatw-body"></div>3936  </div>3937</div>3938<!-- ===================== Popup « Visite libre » (journée portes ouvertes) ===================== -->3939<style>3940  .oh-overlay{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:16px;opacity:0;visibility:hidden;pointer-events:none;transition:opacity .3s ease,visibility .3s ease}3941  .oh-overlay.oh-open{opacity:1;visibility:visible;pointer-events:auto}3942  .oh-backdrop{position:absolute;inset:0;background:rgba(26,26,26,.7);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);cursor:pointer}3943  .oh-card{position:relative;width:100%;max-width:430px;background:#FDFCFA;border-radius:24px;overflow:hidden;box-shadow:0 12px 40px -14px rgba(45,45,45,.4);transform:translateY(26px) scale(.96);opacity:0;transition:transform .35s ease,opacity .35s ease}3944  .oh-overlay.oh-open .oh-card{transform:none;opacity:1}3945  .oh-close{position:absolute;right:12px;top:12px;z-index:2;width:36px;height:36px;border:none;border-radius:50%;background:rgba(255,255,255,.92);color:#3f3f3f;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.12);transition:background .15s,color .15s}3946  .oh-close:hover{background:#fff;color:#e08600}3947  .oh-media{position:relative}3948  .oh-media img{display:block;width:100%;height:232px;object-fit:cover;object-position:center 65%}3949  .oh-badge{position:absolute;left:16px;top:16px;background:#FF9900;color:#1f1f1f;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;padding:5px 12px;border-radius:999px;box-shadow:0 8px 30px -6px rgba(255,153,0,.45)}3950  .oh-body{padding:24px}3951  .oh-body h2{margin:0;font-size:23px;font-weight:800;color:#1f1f1f;letter-spacing:-.01em;line-height:1.15}3952  .oh-list{list-style:none;margin:16px 0 0;padding:0;display:flex;flex-direction:column;gap:10px;font-size:14px;color:#3f3f3f}3953  .oh-list li{display:flex;align-items:flex-start;gap:12px}3954  .oh-list svg{margin-top:2px;width:20px;height:20px;flex:0 0 auto;color:#e08600}3955  .oh-list strong{color:#1f1f1f}3956  .oh-actions{display:flex;gap:12px;margin-top:22px;flex-wrap:wrap}3957  .oh-cta{flex:1;min-width:150px;display:inline-flex;align-items:center;justify-content:center;background:#FF9900;color:#1f1f1f;text-decoration:none;font-weight:700;font-size:14px;padding:12px 22px;border-radius:999px;box-shadow:0 8px 30px -6px rgba(255,153,0,.45);transition:transform .15s,background .15s,color .15s}3958  .oh-cta:hover{transform:scale(1.03);background:#e08600;color:#fff}3959  .oh-later{background:none;border:none;color:#3f3f3f;font-weight:600;font-size:14px;padding:12px 18px;cursor:pointer;border-radius:999px;transition:color .15s}3960  .oh-later:hover{color:#FF9900}3961  @media(max-width:430px){.oh-media img{height:190px}.oh-body{padding:20px}.oh-body h2{font-size:20px}}3962</style>3963<div id="oh-overlay" class="oh-overlay" role="presentation">3964  <div class="oh-backdrop" data-oh-close></div>3965  <div class="oh-card" role="dialog" aria-modal="true" aria-labelledby="oh-title">3966    <button class="oh-close" data-oh-close aria-label="Fermer"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18" stroke-linecap="round"/></svg></button>3967    <div class="oh-media" id="oh-media">3968      <img id="oh-img" alt="Photo de l'appartement">3969      <span class="oh-badge">Visite libre</span>3970    </div>3971    <div class="oh-body">3972      <h2 id="oh-title">Venez visiter l'appartement&nbsp;!</h2>3973      <ul class="oh-list">3974        <li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M8 3v3M16 3v3M4 9h16M5 5h14a1 1 0 011 1v13a1 1 0 01-1 1H5a1 1 0 01-1-1V6a1 1 0 011-1z" stroke-linecap="round" stroke-linejoin="round"/></svg><span id="oh-when"></span></li>3975        <li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 21s-7-6.2-7-11a7 7 0 1114 0c0 4.8-7 11-7 11zM12 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z" stroke-linecap="round" stroke-linejoin="round"/></svg><span id="oh-where"></span></li>3976      </ul>3977      <div class="oh-actions">3978        <a class="oh-cta" id="oh-cta" href="#">Voir le logement</a>3979        <button class="oh-later" data-oh-close type="button">Plus tard</button>3980      </div>3981    </div>3982  </div>3983</div>3984<script>3985(function(){3986  // ⚙️  À MODIFIER pour chaque visite libre (mets enabled:false pour désactiver le popup) :3987  var OPEN_HOUSE = {3988    enabled: true,3989    id: '2026-10-03-grands-ducs',                 // change l'id pour un NOUVEL événement (réaffiche le popup)3990    title: "Venez visiter l'appartement !",3991    date: 'Samedi 3 octobre',3992    time: '9 h 00 à 12 h 00',3993    address: '1969-1971, rue des Grands-Ducs',3994    city: 'Drummondville',3995    image: '/visite-libre.webp',                  // photo dans le dépôt (racine) ; ou une URL https://… ; ou '' pour aucune3996    url: '/?fiche=az231',                          // ouvre la fiche (réf. az…) sur le site de location3997    endsAt: '2026-10-03T12:00:00-04:00',           // le popup ne s'affiche plus après cette date/heure (Québec)3998    repeatHours: 12                                 // ré-affichage : au plus une fois par X heures et par navigateur3999  };4000  var ov = document.getElementById('oh-overlay'); if (!ov) return;4001  var key = 'openhouse-last-' + OPEN_HOUSE.id;      // horodatage du dernier affichage (localStorage)4002  var freqMs = (OPEN_HOUSE.repeatHours || 12) * 3600 * 1000;4003  function esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }4004  function lastSeen(){ try { return parseInt(localStorage.getItem(key)||'0', 10) || 0; } catch(e){ return 0; } }4005  function markSeen(){ try { localStorage.setItem(key, String(Date.now())); } catch(e){} }4006  if (!OPEN_HOUSE.enabled || (Date.now() - lastSeen()) < freqMs || Date.now() > new Date(OPEN_HOUSE.endsAt).getTime()){ ov.parentNode && ov.parentNode.removeChild(ov); return; }4007  // Remplissage depuis la config4008  document.getElementById('oh-title').textContent = OPEN_HOUSE.title;4009  document.getElementById('oh-when').innerHTML = '<strong>'+esc(OPEN_HOUSE.date)+'</strong>, de '+esc(OPEN_HOUSE.time);4010  document.getElementById('oh-where').textContent = OPEN_HOUSE.address + (OPEN_HOUSE.city ? (', '+OPEN_HOUSE.city) : '');4011  var cta = document.getElementById('oh-cta'); cta.setAttribute('href', OPEN_HOUSE.url || '#');4012  var media = document.getElementById('oh-media'), img = document.getElementById('oh-img');4013  if (OPEN_HOUSE.image){ img.onerror = function(){ media.style.display = 'none'; }; img.src = OPEN_HOUSE.image; }4014  else { media.style.display = 'none'; }4015  function close(){ ov.classList.remove('oh-open'); markSeen(); document.removeEventListener('keydown', onKey); setTimeout(function(){ ov.parentNode && ov.parentNode.removeChild(ov); }, 320); }4016  function onKey(e){ if (e.key === 'Escape') close(); }4017  Array.prototype.forEach.call(ov.querySelectorAll('[data-oh-close]'), function(el){ el.addEventListener('click', close); });4018  cta.addEventListener('click', markSeen);   // marque « vu » ; la navigation suit le href4019  document.addEventListener('keydown', onKey);4020  // Affiché : on horodate tout de suite → pas de réapparition avant `repeatHours`.4021  setTimeout(function(){ ov.classList.add('oh-open'); markSeen(); }, 1200);4022})();4023</script>4024<!-- Barre d'onglets mobile : amène le visiteur vers l'espace candidat (inscription si non connecté) -->4025<nav id="mnav" class="locked" aria-label="Navigation">4026  <button class="active" onclick="window.scrollTo({top:0,behavior:'smooth'})"><span class="ic"><svg viewBox="0 0 24 24"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h5v-6h4v6h5V10"/></svg></span>Logements</button>4027  <button onclick="location.href='/compte?tab=profil'"><span class="ic"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 4-6 8-6s8 2 8 6"/></svg></span>Profil</button>4028  <button data-lock onclick="location.href='/compte?tab=association'"><span class="ic"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1"/></svg></span>Association<span class="mlock">🔒</span></button>4029  <button data-lock onclick="location.href='/compte?tab=favoris'"><span class="ic"><svg viewBox="0 0 24 24"><path d="M12 21s-7-4.6-9.3-9A5 5 0 0 1 12 6a5 5 0 0 1 9.3 3c0 3.4-9.3 12-9.3 12z"/></svg></span>Favoris<span class="mlock">🔒</span></button>4030  <button data-lock onclick="location.href='/compte?tab=alertes'"><span class="ic"><svg viewBox="0 0 24 24"><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></span>Alertes<span class="mlock">🔒</span></button>4031  <button data-lock onclick="location.href='/compte?tab=chat'"><span class="ic"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></span>Chat<span class="mlock">🔒</span></button>4032</nav>4033<!-- Barre d'onglets mobile — agent de location (remplace la barre candidat quand staff connecté) -->4034<nav id="agnav" aria-label="Navigation agent">4035  <button onclick="location.href='/compte?agtab=dashboard'"><span class="ic"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg></span>Tableau</button>4036  <button class="active" onclick="window.scrollTo({top:0,behavior:'smooth'})"><span class="ic"><svg viewBox="0 0 24 24"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h5v-6h4v6h5V10"/></svg></span>Logements</button>4037  <button onclick="location.href='/compte?agtab=crm'"><span class="ic"><svg viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><path d="M3 20c0-3 3-5 6-5s6 2 6 5"/><path d="M16 7a3 3 0 0 1 0 6M21 20c0-2.2-1.6-4-4-4.5"/></svg></span>CRM</button>4038  <button onclick="location.href='/compte?agtab=calendrier'"><span class="ic"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M3 9h18M8 2v4M16 2v4"/></svg></span>Calendrier</button>4039  <button onclick="location.href='/compte?agtab=chat'"><span class="ic"><svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></span>Chat</button>4040</nav>4041<script>4042  // Déverrouille les onglets de la barre mobile si le visiteur est déjà connecté.4043  (function(){4044    function unlock(){ try { const n = document.getElementById('mnav'); if (n) n.classList.remove('locked'); } catch(_){} }4045    function check(){ const sb = window._sb; if (sb && sb.auth && sb.auth.getSession){ sb.auth.getSession().then(({ data }) => { if (data && data.session) unlock(); }).catch(function(){}); return true; } return false; }4046    document.addEventListener('DOMContentLoaded', function(){4047      if (check()) return;4048      var t = 0, iv = setInterval(function(){ if (check() || ++t > 20) clearInterval(iv); }, 150);4049    });4050  })();4051</script>4052<!-- Chat visiteur → propriétaire (uniquement sur un lien propriétaire ?p=) -->4053<style>4054  #oc-fab{position:fixed;right:18px;bottom:18px;z-index:3100;width:60px;height:60px;border-radius:50%;border:2px solid #fff;background:linear-gradient(135deg,#FFB04D,#FB8C00);color:#fff;cursor:pointer;box-shadow:0 6px 20px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center;transition:transform .15s,filter .15s}4055  #oc-fab:hover{filter:brightness(1.05);transform:scale(1.05)}4056  #oc-fab .dot{position:absolute;right:2px;bottom:2px;width:15px;height:15px;border-radius:50%;background:#22c55e;border:2.5px solid #fff}4057  #oc-fab .oc-nb{position:absolute;top:-2px;right:-2px;min-width:20px;height:20px;background:#EF4444;color:#fff;border-radius:10px;font-size:12px;font-weight:700;display:none;align-items:center;justify-content:center;padding:0 5px;border:2px solid #fff}4058  #oc-panel{position:fixed;right:18px;bottom:88px;z-index:3100;width:340px;max-width:calc(100vw - 32px);height:480px;max-height:calc(100vh - 120px);background:#fff;border:1px solid #e8e8e5;border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,.22);display:none;flex-direction:column;overflow:hidden;font-family:system-ui,-apple-system,sans-serif}4059  #oc-panel.open{display:flex}4060  .oc-head{background:linear-gradient(135deg,#FFB04D,#FB8C00);color:#fff;padding:14px 16px;display:flex;justify-content:space-between;align-items:center;gap:8px}4061  .oc-head b{font-size:15px;display:block}4062  .oc-head .sub{font-size:12px;opacity:.9}4063  .oc-head button{background:rgba(255,255,255,.22);border:none;color:#fff;width:28px;height:28px;border-radius:8px;cursor:pointer;font-size:15px;flex:none}4064  #oc-body{flex:1;display:flex;flex-direction:column;min-height:0}4065  .oc-pad{padding:18px 16px;overflow-y:auto}4066  .oc-intro{font-size:14px;color:#555;line-height:1.5;margin:0 0 14px}4067  .oc-field{margin-bottom:10px}4068  .oc-field label{display:block;font-size:12px;font-weight:600;color:#555;margin-bottom:4px}4069  .oc-field input{width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid #d8d8d4;border-radius:9px;font-size:14px;outline:none;font-family:inherit}4070  .oc-field input:focus{border-color:#FB8C00}4071  .oc-err{font-size:13px;color:#B91C1C;margin:2px 0 8px;display:none}4072  .oc-start-btn{display:block;width:100%;padding:12px;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;border:none;background:#FB8C00;color:#fff}4073  #oc-msgs{flex:1;overflow-y:auto;padding:14px;background:#faf9f7;display:flex;flex-direction:column;gap:8px}4074  .oc-msg{max-width:80%;padding:9px 12px;border-radius:14px;font-size:14px;line-height:1.4;white-space:pre-wrap;word-break:break-word}4075  .oc-msg.v{align-self:flex-end;background:#FB8C00;color:#fff;border-bottom-right-radius:4px}4076  .oc-msg.o{align-self:flex-start;background:#fff;border:1px solid #e8e8e5;color:#1a1a1a;border-bottom-left-radius:4px}4077  #oc-form{display:flex;gap:8px;padding:10px;border-top:1px solid #f0f0ee;background:#fff}4078  #oc-form input{flex:1;padding:10px 12px;border:1px solid #ddd;border-radius:9px;font-size:14px;outline:none;font-family:inherit}4079  #oc-form button{background:#FB8C00;color:#fff;border:none;border-radius:9px;padding:0 15px;font-weight:700;cursor:pointer;font-size:15px}4080  @media(max-width:480px){#oc-panel{width:calc(100vw - 24px);right:12px}}4081</style>4082<script type="module">4083import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm';4084const _ocsb = createClient('https://jfgkriojazaohgjezijj.supabase.co','eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpmZ2tyaW9qYXphb2hnamV6aWpqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODEzNDE5NjQsImV4cCI6MjA5NjkxNzk2NH0.VVLfhvpkQ9WgJOhCYW3y6kRZIJbAQXTCANv6mXce1Yw');4085const $oc=id=>document.getElementById(id);4086const ocEsc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');4087const _pTok=(new URLSearchParams(location.search).get('p')||'').trim();4088let ocOwnerUid=null, ocOwnerName='', ocVisitorUid=null, ocVisitorName='', ocVisitorEmail='', ocVisitorPhone='', ocSub=null, ocOpen=false, ocUnread=0, ocPoll=null, ocLastTs=null; const ocSeen=new Set();4089function ocPersonSvg(){ return '<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/><path d="M5 20c0-3.3 3.1-5.5 7-5.5s7 2.2 7 5.5"/></svg>'; }4090if(_pTok){4091  (async()=>{4092    try{ const r=await _ocsb.rpc('owner_uid_from_token',{p_token:_pTok}); if(!r.error && r.data) ocOwnerUid=r.data; }catch(_){}4093    if(!ocOwnerUid) return;                         // propriétaire non résolu → pas de bulle4094    const oc=(window.__ownerContact)||{}; ocOwnerName=((oc.contact_name)||'').trim();4095    try { const { data:{ session } } = await _ocsb.auth.getSession(); if(session){ ocVisitorUid=session.user.id; if(_ocsb.realtime&&_ocsb.realtime.setAuth) await _ocsb.realtime.setAuth(session.access_token); } } catch(_){}4096    mountOcBubble();4097    if(ocVisitorUid) ocSubscribe();4098  })();4099}4100function mountOcBubble(){4101  if($oc('oc-fab')) return;4102  const fab=document.createElement('button'); fab.id='oc-fab'; fab.type='button'; fab.setAttribute('aria-label','Discuter avec le propriétaire');4103  fab.innerHTML=ocPersonSvg()+'<span class="dot"></span><span class="oc-nb" id="oc-nb">0</span>';4104  const panel=document.createElement('div'); panel.id='oc-panel';4105  const title = ocOwnerName ? ocEsc(ocOwnerName) : 'Le propriétaire';4106  panel.innerHTML=`<div class="oc-head"><div><b>${title}</b><div class="sub">Réponse directe du propriétaire</div></div><button id="oc-close" type="button" aria-label="Fermer">✕</button></div><div id="oc-body"></div>`;4107  document.body.appendChild(fab); document.body.appendChild(panel);4108  fab.addEventListener('click', ()=>ocSetOpen(!ocOpen));4109  $oc('oc-close').addEventListener('click', ()=>ocSetOpen(false));4110  ocRenderBody();4111}4112function ocSetOpen(v){ ocOpen=v; const p=$oc('oc-panel'); if(p) p.classList.toggle('open',v); if(v){ ocUnread=0; ocBadge(); const mb=$oc('oc-msgs'); if(mb) mb.scrollTop=mb.scrollHeight; if(ocVisitorUid) ocMarkRead(); } }4113function ocBadge(){ const n=$oc('oc-nb'); if(n){ n.textContent=ocUnread; n.style.display=ocUnread>0?'flex':'none'; } }4114function ocBubbleHtml(m){ return `<div class="oc-msg ${m.sender==='owner'?'o':'v'}">${ocEsc(m.body)}</div>`; }4115function ocAppend(m){ const b=$oc('oc-msgs'); if(!b) return; b.insertAdjacentHTML('beforeend', ocBubbleHtml(m)); b.scrollTop=b.scrollHeight; }4116function ocRenderBody(){4117  const b=$oc('oc-body'); if(!b) return;4118  if(!ocVisitorUid){4119    b.innerHTML=`<div class="oc-pad"><p class="oc-intro">Une question sur un logement ? Laissez vos coordonnées, ${ocOwnerName?ocEsc(ocOwnerName):'le propriétaire'} vous répondra directement.</p>4120      <div class="oc-field"><label>Votre nom *</label><input id="oc-name" maxlength="80" autocomplete="name"></div>4121      <div class="oc-field"><label>Courriel</label><input id="oc-email" type="email" autocomplete="email" placeholder="vous@exemple.com"></div>4122      <div class="oc-field"><label>Téléphone</label><input id="oc-phone" type="tel" autocomplete="tel" placeholder="819 555-1234"></div>4123      <div class="oc-err" id="oc-err">Indiquez votre nom et au moins un moyen de vous joindre (courriel ou téléphone).</div>4124      <button class="oc-start-btn" id="oc-startbtn" type="button">Commencer la discussion</button></div>`;4125    $oc('oc-startbtn').addEventListener('click', ocStart);4126  } else {4127    b.innerHTML=`<div id="oc-msgs"></div><form id="oc-form"><input id="oc-input" placeholder="Votre message…" autocomplete="off"><button type="submit" aria-label="Envoyer">➤</button></form>`;4128    $oc('oc-form').addEventListener('submit', e=>{ e.preventDefault(); const v=$oc('oc-input').value; $oc('oc-input').value=''; ocSend(v); });4129    ocLoadThread();4130  }4131}4132async function ocStart(){4133  const name=($oc('oc-name').value||'').trim(), email=($oc('oc-email').value||'').trim(), phone=($oc('oc-phone').value||'').trim();4134  const err=$oc('oc-err');4135  if(!name || (!email && !phone)){ if(err) err.style.display='block'; return; }4136  if(err) err.style.display='none';4137  const btn=$oc('oc-startbtn'); if(btn){ btn.disabled=true; btn.textContent='…'; }4138  try {4139    let { data:{ session } } = await _ocsb.auth.getSession();4140    if(!session){ const r=await _ocsb.auth.signInAnonymously(); if(r.error) throw r.error; session=r.data.session; }4141    ocVisitorUid=session.user.id; ocVisitorName=name; ocVisitorEmail=email; ocVisitorPhone=phone;4142    if(_ocsb.realtime&&_ocsb.realtime.setAuth) await _ocsb.realtime.setAuth(session.access_token);4143    try{ await _ocsb.from('profiles').upsert({ user_id: ocVisitorUid, nom: name, courriel: email||null, source:'vitrine-chat' }); }catch(_){}4144    ocRenderBody(); ocSubscribe();4145  } catch(e){ console.error(e); if(btn){ btn.disabled=false; btn.textContent='Commencer la discussion'; } alert('Impossible pour le moment. Réessayez.'); }4146}4147async function ocSend(body){4148  body=(body||'').trim(); if(!body || !ocVisitorUid || !ocOwnerUid) return;4149  ocAppend({ sender:'visitor', body });4150  const { error }=await _ocsb.from('owner_messages').insert({ owner_user_id:ocOwnerUid, visitor_user_id:ocVisitorUid, visitor_name:ocVisitorName||'Visiteur', visitor_email:ocVisitorEmail||null, visitor_phone:ocVisitorPhone||null, sender:'visitor', body });4151  if(error) console.error('oc send', error);4152}4153async function ocLoadThread(){4154  if(!ocVisitorUid || !ocOwnerUid) return;4155  const { data }=await _ocsb.from('owner_messages').select('*').eq('owner_user_id',ocOwnerUid).eq('visitor_user_id',ocVisitorUid).order('created_at');4156  (data||[]).forEach(m=>{ if(m.id) ocSeen.add(m.id); if(m.visitor_name&&!ocVisitorName) ocVisitorName=m.visitor_name; if(m.visitor_email&&!ocVisitorEmail) ocVisitorEmail=m.visitor_email; if(m.visitor_phone&&!ocVisitorPhone) ocVisitorPhone=m.visitor_phone; });4157  const b=$oc('oc-msgs'); if(b){ b.innerHTML=(data||[]).map(ocBubbleHtml).join(''); b.scrollTop=b.scrollHeight; }4158  if(data&&data.length) ocLastTs=data[data.length-1].created_at;4159  ocMarkRead();4160}4161async function ocMarkRead(){ try{ await _ocsb.from('owner_messages').update({read:true}).eq('owner_user_id',ocOwnerUid).eq('visitor_user_id',ocVisitorUid).eq('sender','owner').eq('read',false); }catch(_){} }4162function ocSubscribe(){4163  if(ocSub || !ocVisitorUid) return;4164  ocSub=_ocsb.channel('oc-'+ocVisitorUid).on('postgres_changes',{ event:'INSERT', schema:'public', table:'owner_messages', filter:'visitor_user_id=eq.'+ocVisitorUid }, p=>{4165    const m=p.new; if(!m || m.sender!=='owner') return;4166    if(m.id && ocSeen.has(m.id)) return; if(m.id) ocSeen.add(m.id);4167    if(m.created_at) ocLastTs=m.created_at;4168    ocAppend(m);4169    if(ocOpen){ _ocsb.from('owner_messages').update({read:true}).eq('id',m.id); } else { ocUnread++; ocBadge(); }4170  }).subscribe();4171  if(!ocPoll) ocPoll=setInterval(ocPollFn, 5000);4172}4173async function ocPollFn(){4174  if(!ocVisitorUid || !ocOwnerUid) return;4175  let q=_ocsb.from('owner_messages').select('*').eq('owner_user_id',ocOwnerUid).eq('visitor_user_id',ocVisitorUid).eq('sender','owner').order('created_at');4176  if(ocLastTs) q=q.gt('created_at',ocLastTs);4177  const { data }=await q;4178  (data||[]).forEach(m=>{ if(m.id && ocSeen.has(m.id)) return; if(m.id) ocSeen.add(m.id); if(m.created_at) ocLastTs=m.created_at; ocAppend(m); if(!ocOpen){ ocUnread++; ocBadge(); } });4179}4180</script>4181</body>4182</html>4183
Files main
Menu

Theme