SPB Git

spb/lou-ka Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

HTML 99.7%
203.3 KB · 2,778 lines html
Raw Blame History
1<!DOCTYPE html>2<html lang="fr">3<head>4<meta charset="UTF-8">5<meta http-equiv="Content-Security-Policy" content="6  default-src 'self';7  script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;8  style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;9  img-src 'self' data: blob: https:;10  font-src 'self' data:;11  connect-src 'self' https://jfgkriojazaohgjezijj.supabase.co wss://jfgkriojazaohgjezijj.supabase.co https://cdn.jsdelivr.net https://nominatim.openstreetmap.org https://*.tile.openstreetmap.org;12  frame-src 'self';13  worker-src 'self' blob:;14  object-src 'none';15  base-uri 'self';16">17<meta name="viewport" content="width=device-width, initial-scale=1.0">18<title>Logements disponibles — Gestion ISR</title>19<meta name="description" content="Découvrez nos logements disponibles dans le Centre-du-Québec avec Gestion ISR.">20<meta property="og:type" content="website">21<meta property="og:title" content="Logements disponibles — Gestion ISR">22<meta property="og:description" content="Découvrez nos logements disponibles dans le Centre-du-Québec avec Gestion ISR.">23<meta property="og:url" content="https://gestion-isr-logements.vercel.app/">24<meta property="og:image" content="https://gestion-isr-logements.vercel.app/og-image.png?v=20">25<meta property="og:image:secure_url" content="https://gestion-isr-logements.vercel.app/og-image.png?v=20">26<meta property="og:image:type" content="image/png">27<meta property="og:image:width" content="1200">28<meta property="og:image:height" content="630">29<meta name="twitter:card" content="summary_large_image">30<meta name="twitter:image" content="https://gestion-isr-logements.vercel.app/og-image.png?v=20">31<meta name="theme-color" content="#FB8C00">32<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">33<style>34*{box-sizing:border-box;margin:0;padding:0}35html,body{overflow-x:hidden;max-width:100%}36body{font-family:system-ui,sans-serif;background:#f5f5f3;color:#1a1a1a;min-height:100vh;width:100%}37.header{background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}38.logo-row{display:flex;align-items:center;gap:10px}39.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}40.brand{font-size:18px;font-weight:600}41.hero{background:#fff;border-bottom:1px solid #e8e8e5;padding:1.5rem 2rem}42.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}43.hero h1{font-size:24px;font-weight:600;margin-bottom:4px}44.hero p{font-size:14px;color:#666}45.filters-wrap{background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem;display:flex;gap:1rem;flex-wrap:wrap;align-items:flex-end}46.filter-group{display:flex;flex-direction:column;gap:4px;min-width:130px}47.filter-group label{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.04em}48.filter-group select{font-size:13px;padding:7px 10px;border:1px solid #ddd;border-radius:8px;background:#fff;color:#1a1a1a;cursor:pointer}49#browse-controls .filters-wrap,#browse-controls .tags-row{display:none!important}50.price-range{display:flex;flex-direction:column;gap:4px}51.price-range label{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.04em}52.price-inputs{display:flex;align-items:center;gap:6px}53.price-inputs input{width:80px;font-size:13px;padding:7px 8px;border:1px solid #ddd;border-radius:8px;color:#1a1a1a}54.price-inputs span{font-size:13px;color:#888}55.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}56.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}57.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}58.tag button{background:none;border:none;color:#888;cursor:pointer;font-size:14px;line-height:1;padding:0}59.tags-empty{font-size:13px;color:#aaa}60.quick-filters{display:flex;gap:8px;flex-wrap:wrap;align-items:center}61.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}62.quick-filter-btn:hover{border-color:#FB8C00;color:#FB8C00}63.quick-filter-btn.active{background:#FB8C00;color:#fff;border-color:#FB8C00}64.main{padding:1.5rem 2rem 3rem}65.results-line{font-size:13px;color:#888;margin-bottom:1rem}66.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}67.loading{grid-column:1/-1;text-align:center;padding:4rem;color:#888}68.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}69@keyframes spin{to{transform:rotate(360deg)}}70.card{background:#fff;border:1px solid #e8e8e5;border-radius:14px;overflow:hidden;transition:border-color 0.15s,transform 0.1s;cursor:pointer}71.card:hover{border-color:#FB8C00;transform:translateY(-2px)}72.card-promotion{border:3px solid #F97316 !important;box-shadow:0 0 0 5px rgba(249,115,22,0.25)}73.card-psl{border:3px solid #8B5CF6 !important;box-shadow:0 0 0 5px rgba(139,92,246,0.25)}74.card-derniere{border:3px solid #EF4444 !important;box-shadow:0 0 0 5px rgba(239,68,68,0.25)}75.card-coupdecoeur{border:3px solid #EF4444 !important;box-shadow:0 0 0 5px rgba(239,68,68,0.25)}76.card-img{height:180px;background:#f0f0ee;position:relative;overflow:hidden}77.card-img img{width:100%;height:100%;object-fit:cover}78.card-img-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#ccc}79.badge-row{position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:flex-end;align-items:flex-start;gap:6px}80.badge{font-size:11px;font-weight:600;padding:4px 9px;border-radius:12px;white-space:nowrap}81.badge-dispo{background:#FB8C00;color:#fff}82.badge-animal{background:#fff;color:#333;border:1px solid #ddd}83.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)}84.card-heart.active{color:#EF4444}85.card-heart:hover{background:#fff;color:#EF4444}86.badge-etiquette{background:#F59E0B;color:#fff}87.card-body{padding:1rem 1.1rem 1.1rem}88.card-ville{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:4px}89.card-title{font-size:15px;font-weight:600;margin-bottom:10px;line-height:1.3}90.card-tags{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:12px}91.card-tag{font-size:11px;padding:3px 8px;border-radius:6px;font-weight:500}92.ct-size{background:#E6F1FB;color:#0C447C}93.ct-date{background:#FAEEDA;color:#633806}94.ct-etage{background:#EAF3DE;color:#27500A}95.ct-variantes{background:#EEEDFE;color:#3C3489;font-weight:600}96.ct-incl{background:#F1EFE8;color:#444441}97.price-from{font-size:.62em;font-weight:600;color:#777;vertical-align:middle}98/* Modèles de photos (variantes) */99.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}100.card-models-btn:hover{background:#EEEDFE;border-color:#3C3489}101#d-photo-models{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px}102.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)}103.d-models-head{font-size:14px;font-weight:800;color:#b45309;margin-bottom:9px}104.d-models-head span{font-size:12.5px;font-weight:500;color:#a9743a}105.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}106.pv-model-btn:hover{background:#e5850f;color:#fff;border-color:#e5850f;transform:translateY(-1px)}107.pv-model-btn:hover span{color:#ffe8cf}108.pv-model-btn span{font-size:12px;font-weight:500;color:#b1936a}109.pv-model-btn.pv-model-cur{background:#e5850f;color:#fff;border-color:#e5850f;cursor:default;box-shadow:none}110.pv-model-btn.pv-model-cur span{color:#ffe8cf}111.pv-model-btn:disabled{opacity:1}112.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}113.lb-models.show{display:flex}114.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}115.lb-mbtn:hover{background:rgba(0,0,0,.7)}116.lb-mbtn.active{background:#fff;color:#111;border-color:#fff}117/* PHASE 2 — Immeuble & unités (vue agent) */118.d-bld-meta{font-size:12.5px;color:#555;margin:0 0 10px}119.d-bld{border:2px solid #cfd6de;border-radius:12px;overflow:hidden;background:#fbfcfd;width:fit-content;max-width:100%;margin:0 auto}120.d-bld-floor{display:flex;align-items:stretch;border-bottom:1px solid #e7ebef}121.d-bld-floor:last-child{border-bottom:none}122.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}123.d-units{flex:1;display:flex;flex-wrap:wrap;gap:7px;padding:8px;justify-content:center}124.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}125.d-unit:hover{transform:translateY(-2px);box-shadow:0 5px 13px rgba(0,0,0,.14)}126.d-unit-bar{display:flex;justify-content:space-between;align-items:center;gap:4px;padding:2px 7px;font-size:9px;font-weight:700}127.d-unit-body{padding:5px 7px 7px}128.d-unit-type{font-size:13px;font-weight:700;color:#1a1a1a}129.d-unit-etage{font-size:10px;color:#7a828b;margin-top:1px}130.d-unit-price{font-size:12.5px;font-weight:800;color:#E67700;margin-top:3px}131/* vue compacte : petites tuiles numérotées (comme un plan) */132.d-units.compact{gap:3px;padding:5px;align-items:stretch}133.d-units.compact.d-hasgroups{gap:8px;align-items:flex-start}134.d-posgroup{display:flex;flex-direction:column;gap:2px;border:1px dashed #cbd3da;border-radius:6px;padding:3px}135.d-poslbl{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.02em;color:#7a828b;text-align:center}136.d-postiles{display:flex;flex-wrap:wrap;gap:3px}137.d-unit.mini{width:50px;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}138.d-unit.mini .mn{font-size:8.5px;font-weight:800;line-height:1.05}139.d-unit.mini .mm{font-size:6.5px;font-weight:600;color:#46525f;line-height:1.15;margin-top:1px;white-space:nowrap}140.d-unit.mini:hover{transform:translateY(-1px);box-shadow:0 2px 6px rgba(0,0,0,.16)}141.d-unit.d-unit-locked{cursor:default;opacity:.72}142.d-unit.d-unit-locked:hover{transform:none;box-shadow:none}143.d-unit.mini.d-unit-cur{outline:2px solid #E67700;outline-offset:1px;box-shadow:0 0 0 3px rgba(230,119,0,.18)}144/* Bouton + plan plein écran (grands immeubles) */145.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}146.d-bld-expand:hover{background:#e9eef3;border-color:#9fb0c0}147@media (min-width:769px){ .d-bld-expand{display:none !important} }  /* plan plein écran : utile seulement sur mobile */148.bld-full-overlay{display:none;position:fixed;inset:0;z-index:180;background:#fff;flex-direction:column}149.bld-full-overlay.open{display:flex}150.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}151.bld-full-head button{background:none;border:none;font-size:28px;line-height:1;cursor:pointer;color:#888}152.bld-zoom-ctl{display:flex;align-items:center;gap:4px;flex:0 0 auto}153.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}154.bld-zoom-ctl button:active{background:#eef2f4}155.bld-zoom-ctl .bld-zoom-close{border:none;font-size:28px;color:#888;width:36px}156.bld-full-body{flex:1;overflow:auto;padding:14px;-webkit-overflow-scrolling:touch;cursor:grab;background:#f5f7f9;overscroll-behavior:contain}157.bld-full-body.grabbing{cursor:grabbing}158.bld-full-body .d-bld{width:max-content;min-width:100%;margin:0}159.bld-full-body .d-units.compact{gap:8px;padding:10px;justify-content:flex-start}160.bld-full-body .d-floor-lbl{flex:0 0 92px;font-size:11px}161.bld-full-body .d-unit.mini{width:130px;padding:9px 8px 10px}162.bld-full-body .d-unit.mini .mn{font-size:15px}163.bld-full-body .d-unit.mini .mm{font-size:12.5px}164/* barre d'outils du visuel */165.d-bld-toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 10px}166.d-seg{display:inline-flex;border:1px solid #d5dbe2;border-radius:8px;overflow:hidden}167.d-seg button{border:none;background:#fff;color:#556;font-size:12px;font-weight:600;padding:6px 13px;cursor:pointer}168.d-seg button.active{background:#E67700;color:#fff}169/* Panneau détail d'une unité */170.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}171.unit-overlay.open{display:flex}172.unit-modal{background:#fff;border-radius:16px;width:100%;max-width:460px;margin:auto;padding:20px;position:relative}173.unit-close{position:absolute;top:12px;right:14px;background:none;border:none;font-size:26px;line-height:1;color:#999;cursor:pointer}174.up-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding-right:24px}175.up-num{font-size:20px;font-weight:800;color:#1a1a1a}176.up-sub{font-size:13px;color:#7a828b;margin-top:2px}177.up-stat{font-size:11px;font-weight:700;padding:3px 10px;border-radius:12px;white-space:nowrap}178.up-stat.st-dispo{background:#FFECCF;color:#E67700}.up-stat.st-loue{background:#FCE4E4;color:#B42318}179.up-price{font-size:18px;font-weight:800;color:#E67700;margin:12px 0 4px}180.up-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;margin-top:10px}181.up-row{display:flex;justify-content:space-between;gap:10px;font-size:14px;border-bottom:1px solid #f2f4f6;padding-bottom:5px}182.up-k{color:#7a828b}.up-v{font-weight:600;color:#1a1a1a}183.up-sec{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;color:#8a8f96;margin:16px 0 6px}184.up-caract{font-size:14px;color:#333;line-height:1.5;white-space:pre-line}185.up-note{font-size:14px;color:#333;line-height:1.5;background:#fffdf3;border:1px solid #f0d98b;border-radius:10px;padding:10px 12px}186.up-photos{display:flex;gap:6px;overflow-x:auto;padding:2px 0 8px;-webkit-overflow-scrolling:touch}187.up-photos img{height:76px;width:104px;object-fit:cover;border-radius:8px;flex:0 0 auto;cursor:pointer;background:#eee}188.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}189.up-intk{font-size:11px;font-weight:700;color:#8a5a00;text-transform:uppercase;letter-spacing:.02em}190.up-intv{color:#333;line-height:1.45}191/* Matcher (association front, sans compte) */192/* Test photo en fond (repli vert d'origine si l'image manque) */193.matcher{background-color:#E67700;background-image:url('hero-matcher.jpg');background-size:cover;background-position:center 27%;background-repeat:no-repeat;padding:52px 2rem 30px}194.matcher-inner{width:fit-content;max-width:100%;margin:0 auto;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.5)}195.matcher-inner h2{margin:0 0 14px;font-size:24px;font-weight:800;text-align:center}196.matcher-inner>.matcher-inner,.matcher-inner p{margin:0 0 16px;font-size:14px;opacity:.94;text-align:center}197.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)}198.mx-field{flex:0 0 auto}199.mx-field:has(#mx-secteur){width:186px}200.mx-field:has(#mx-type){width:168px}201.mx-field:has(#mx-budget){width:206px}202.mx-field:has(#mx-etage){width:176px}203.mx-field:has(#mx-animaux){width:104px}204.mx-actions-inline{align-self:center;margin-left:-4px}205.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}206.mx-btn-go:hover{filter:brightness(1.06);transform:translateY(-1px)}207.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}208.mx-btn-more:hover{background:#FFECCF;border-color:#FB8C00}209.mx-more{display:none;flex:1 0 100%;border-top:1px dashed #e2e2df;margin-top:4px;padding-top:14px}210.mx-more.show{display:block}211.mx-more>label{display:block;font-size:12px;font-weight:700;color:#555;margin-bottom:8px;text-align:center}212.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}213.mx-field>input,.mx-field>select{width:100%;padding:8px 10px;border:1px solid #ddd;border-radius:8px;font-size:14px;color:#1a1a1a}214.mx-chips{display:flex;flex-wrap:wrap;gap:5px;justify-content:center}215.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}216.mx-chips label:has(input:checked){background:#FFECCF;border-color:#FB8C00;color:#E67700;font-weight:600}217.mx-chips input{position:absolute;opacity:0;width:0;height:0}218.matcher-actions{display:flex;align-items:center;gap:16px;margin-top:16px;flex-wrap:wrap}219.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}220.mx-btn-primary:hover{background:#FFF7EF}221.mx-btn-ghost{background:transparent;color:#fff;border:none;font-size:13px;font-weight:600;cursor:pointer;opacity:.92;text-decoration:underline}222.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}223.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}224.mx-sum-btn:hover{background:#FFF7EF}225.mx-legend{display:flex;gap:14px;margin:10px 2rem 0;font-size:11.5px;color:#777;font-weight:600}226.mx-legend .lg{display:inline-flex;align-items:center;gap:5px}227.mx-legend .lg::before{content:'';width:13px;height:13px;border-radius:3px;display:inline-block}228.mx-legend .lg-p::before{background:#0B7A5E;box-shadow:0 0 0 1px #FB8C00}229.mx-legend .lg-e::before{background:#3FBF97}230.mx-legend .lg-g::before{background:#F59E0B}231.card.tier-perfect{outline:3px solid #0B7A5E;outline-offset:-3px;box-shadow:0 6px 20px rgba(11,122,94,.28)}232.card.tier-excellent{outline:2px solid #3FBF97;outline-offset:-2px}233.card.tier-good{outline:2px solid #F59E0B;outline-offset:-2px}234.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}235.mx-tier-badge.tier-perfect{background:#0B7A5E;color:#fff}236.mx-tier-badge.tier-excellent{background:#CBEEDF;color:#0B6E52}237.mx-tier-badge.tier-good{background:#F3B44A;color:#5F3B00}238.mx-chips-res{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}239.mx-chip{font-size:11px;padding:2px 8px;border-radius:20px;background:#FFECCF;color:#E67700;font-weight:600}240.mx-chip.soft{background:#FAEEDA;color:#633806}241.mx-chip.miss{background:#FCE4E4;color:#B42318}242.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)}243.match-group-header:first-child{margin-top:0}244.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}245.vtable{width:100%;border-collapse:collapse;font-size:13px}246.vtable th,.vtable td{padding:9px 10px;text-align:left;border-bottom:1px solid #f0f0ee}247.vtable th{font-size:11px;text-transform:uppercase;letter-spacing:.03em;color:#888;font-weight:600;white-space:nowrap}248.vtable tbody tr:hover{background:#faf9f6}249.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}250.vtable .v-price{font-weight:700;color:#E67700;white-space:nowrap}251.vtable .v-note{color:#888;font-size:12px}252.vtable-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}253.card-footer{display:flex;align-items:center;justify-content:space-between;padding-top:10px;border-top:1px solid #f0f0ee}254.price{font-size:17px;font-weight:600}255.price span{font-size:12px;font-weight:400;color:#888}256.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}257.btn-visite-card:hover{background:#FB8C00;color:#fff}258/* MODAL DÉTAIL */259.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}260.detail-overlay.open{display:flex}261.detail-modal{background:#fff;border-radius:18px;width:100%;max-width:680px;overflow:hidden;margin:auto}262.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}263.gallery{position:relative;height:280px;background:#f0f0ee;overflow:hidden}264.gallery img{width:100%;height:100%;object-fit:cover;display:none}265.gallery img.active{display:block}266.gallery-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#ccc}267.gallery-nav{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:6px}268.gallery-dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,0.5);cursor:pointer;border:none;transition:background 0.15s}269.gallery-dot.active{background:#fff}270.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}271.gallery-prev{left:12px}272.gallery-next{right:12px}273.gallery-count{position:absolute;top:12px;right:12px;background:rgba(0,0,0,0.4);color:#fff;font-size:12px;padding:3px 8px;border-radius:12px}274.detail-body{padding:1.5rem}275.detail-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:1rem;gap:1rem}276.detail-ville{font-size:12px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:4px}277.detail-title{font-size:20px;font-weight:600;line-height:1.3}278.detail-price{font-size:22px;font-weight:700;color:#FB8C00;white-space:nowrap}279.detail-price span{font-size:13px;font-weight:400;color:#888}280.detail-badges{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1.25rem}281.detail-badge{font-size:12px;font-weight:600;padding:5px 12px;border-radius:20px}282.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}283.btn-staged:hover{background:linear-gradient(135deg,#F5871F,#EA6D0B);color:#fff;border-color:#F5871F}284.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}285.staged-spark:hover{filter:brightness(1.08);transform:scale(1.08)}286.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}287.card-staged:hover{filter:brightness(1.08);transform:scale(1.08)}288@keyframes stagedShine{0%{transform:translateX(-160%) rotate(20deg)}55%,100%{transform:translateX(220%) rotate(20deg)}}289.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}290@media (prefers-reduced-motion:reduce){.staged-spark::after,.card-staged::after{animation:none}}291.db-dispo{background:#FFECCF;color:#085041}292.db-animal{background:#F1EFE8;color:#444}293.db-size{background:#E6F1FB;color:#0C447C}294.db-etage{background:#EAF3DE;color:#27500A}295.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}296.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}297.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}298.detail-section{margin-bottom:1.25rem}299.detail-section h3{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#888;margin-bottom:8px}300.detail-desc{font-size:14px;color:#444;line-height:1.7;white-space:pre-line}301.inclusions-grid{display:flex;flex-wrap:wrap;gap:6px}302.incl-tag{font-size:12px;padding:4px 10px;background:#f5f5f3;border:1px solid #e8e8e5;border-radius:20px;color:#555}303.detail-divider{border:none;border-top:1px solid #f0f0ee;margin:1.25rem 0}304305/* BOUTONS CONTACT */306.contact-btns{display:flex;gap:10px;margin-bottom:0.75rem}307.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}308.btn-call:hover{background:#EA6C0A}309.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}310.btn-email:hover{background:#2563EB}311.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}312.btn-visite-full:hover{background:#E67700}313.btn-retour{width:100%;padding:10px;background:transparent;color:#666;border:1px solid #ddd;border-radius:10px;font-size:14px;cursor:pointer}314.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}315.btn-share:hover{background:#FFECCF}316/* NOUS JOINDRE */317#contact-section{background:#FFF7EF;color:#1a1a1a;padding:2.5rem 2rem;margin-top:1.5rem}318.contact-inner{max-width:820px;margin:0 auto;text-align:center}319.contact-inner h2{font-size:24px;font-weight:800;margin:0 0 6px}320.contact-lead{font-size:15px;color:#8a7f72;margin:0 0 1.75rem}321.contact-cards{display:flex;gap:16px;justify-content:center;flex-wrap:wrap}322.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}323.contact-card:hover{box-shadow:0 8px 22px rgba(0,0,0,.07);transform:translateY(-2px);border-color:#FB8C00}324.cc-icon{display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:50%;background:#FFECCF;color:#E67700;flex-shrink:0}325.cc-text{display:flex;flex-direction:column;text-align:left}326.cc-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;color:#a89a8a;font-weight:600}327.cc-value{font-size:16px;font-weight:700;color:#C25A0C}328/* MODAL VISITE */329.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}330.visite-overlay.open{display:flex}331.vf-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px 14px;padding:1.25rem;max-height:62vh;overflow-y:auto}332.vf-f{display:flex;flex-direction:column;gap:4px;font-size:12px;font-weight:600;color:#555}333.vf-f.vf-full{grid-column:1/-1}334.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}335.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}336.vf-submit:hover{background:#E67700}.vf-submit:disabled{opacity:.6;cursor:default}337.vf-msg{margin:1rem 1.25rem 0;font-size:13px;font-weight:600}338.vf-msg:empty{display:none;margin:0}339.vf-msg.ok{color:#E67700;background:#FFECCF;padding:10px 12px;border-radius:8px}340.vf-msg.err{color:#B42318;background:#FCE4E4;padding:10px 12px;border-radius:8px}341.visite-modal{background:#fff;border-radius:16px;width:100%;max-width:480px;max-height:90vh;overflow-y:auto;padding:1.5rem}342.modal-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:1.25rem}343.modal-title{font-size:17px;font-weight:600}344.modal-sub{font-size:13px;color:#888;margin-top:2px}345.btn-close{background:none;border:none;font-size:22px;color:#888;cursor:pointer}346.form-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}347.form-field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}348.form-field label{font-size:12px;color:#888;font-weight:500}349.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%}350.form-field textarea{resize:vertical;min-height:72px}351.err{font-size:11px;color:#A32D2D;display:none;margin-top:2px}352.divider{border:none;border-top:1px solid #f0f0ee;margin:1rem 0}353.btn-submit{width:100%;padding:10px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer}354.btn-submit:hover{background:#E67700}355.success-view{display:none;text-align:center;padding:1.5rem 0}356.success-icon{font-size:52px;margin-bottom:1rem}357.success-title{font-size:18px;font-weight:600;margin-bottom:6px}358.success-msg{font-size:14px;color:#666;line-height:1.6}359/* MINI CARTE MODAL */360.detail-map{height:220px;width:100%;border-radius:10px;overflow:hidden;margin-bottom:1.25rem}361/* LIGHTBOX */362.lightbox-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.95);z-index:300;align-items:center;justify-content:center}363.lightbox-overlay.open{display:flex}364.lightbox-img{max-width:95vw;max-height:92vh;object-fit:contain;border-radius:4px}365.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}366.lightbox-close:hover{background:rgba(255,255,255,0.3)}367.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}368.lightbox-prev:hover,.lightbox-next:hover{background:rgba(255,255,255,0.3)}369.lightbox-prev{left:1rem}370.lightbox-next{right:1rem}371.lightbox-count{position:absolute;bottom:1rem;left:50%;transform:translateX(-50%);color:rgba(255,255,255,0.7);font-size:13px}372.gallery img{cursor:zoom-in}373/* RUBAN LOUÉ */374.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)}375.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}376/* RUBAN LOUÉ */377.card-img{overflow:hidden;position:relative}378379/* ===================== MOBILE ===================== */380@media (max-width:768px){381  /* Marges horizontales resserrées (2rem -> 1rem) */382  .header{padding:0.7rem 1rem;flex-wrap:wrap;gap:8px}383  .brand{font-size:16px}384  .hero{padding:1.1rem 1rem}385  .hero h1{font-size:20px}386  .hero p{font-size:13px}387  .filters-wrap{padding:0.8rem 1rem;gap:0.6rem}388  .tags-row{padding:0.55rem 1rem}389  .main{padding:1.1rem 1rem 2.5rem}390  /* Grille en 1 colonne */391  .grid{grid-template-columns:1fr;gap:14px}392  /* Filtres : 2 par rangée, prix et reset pleine largeur */393  .filter-group{min-width:0;flex:1 1 44%}394  .price-range{flex:1 1 100%;min-width:0}395  .price-inputs{min-width:0}396  .price-inputs input{flex:1 1 0;min-width:0;width:auto}397  .btn-reset{align-self:stretch;flex:1 1 100%;padding:9px 14px}398  /* Matcher : pleine largeur, bouton sous les filtres */399  .matcher{padding:26px 1rem 22px}400  .matcher-inner{width:100%}401  .matcher-inner h2{font-size:20px}402  .matcher-grid{padding:14px;gap:12px}403  .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%}404  .mx-actions-inline{width:100%;margin-left:0;align-self:stretch}405  .mx-btn-go{width:100%}406  /* Modal détail : plein écran */407  .detail-overlay{padding:0}408  .detail-modal{border-radius:0;max-width:none;min-height:100vh}409  .detail-body{padding:1.1rem}410  .gallery{height:230px}411  .detail-header{flex-direction:column;gap:.3rem}412  .detail-title{font-size:18px}413  .detail-price{font-size:20px}414  /* Modèles de photos : 1 par rangée */415  .d-models-box{padding:12px}416  #d-photo-models{grid-template-columns:1fr}417  /* Panneau de correspondances : boutons empilés pleine largeur */418  .match-bar{margin:14px 1rem 0;padding:12px 14px}419  .mx-sum-actions{width:100%;flex-direction:column}420  .mx-sum-btn{width:100%}421  .mx-legend{margin:10px 1rem 0}422  /* Visuel immeuble : pleine largeur, tuiles plus grandes et lisibles */423  .d-bld{width:100%;max-width:none}424  .d-floor-lbl{flex:0 0 70px;font-size:9px;padding:6px 8px}425  .d-units.compact{gap:6px;padding:8px}426  .d-unit.mini{width:auto;flex:1 1 100px;min-width:88px;max-width:200px;padding:7px 6px 8px;border-radius:8px;border-width:2px}427  .d-unit.mini .mn{font-size:13px}428  .d-unit.mini .mm{font-size:10.5px}429  /* Panneau d'unité : quasi plein écran */430  .unit-overlay{padding:1rem}431  .unit-modal{max-width:none}432  /* Visite / lightbox */433  .vf-grid{grid-template-columns:1fr}434  /* Barre de recherche rue/ville : mise en évidence sur mobile */435  .search-wrap{padding:0.85rem 1rem;background:#FFF7EF;border-bottom:1px solid #FAD9B0}436  .search-wrap>div{max-width:none}437  .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)}438  .search-wrap svg{left:14px!important;color:#FB8C00!important}439  .lb-models{top:8px;gap:6px}440  .lb-mbtn{padding:6px 11px;font-size:12px}441}442@media (max-width:440px){443  .filter-group{flex:1 1 100%}444  .matcher-inner h2{font-size:18px}445  .gallery{height:200px}446}447/* Outil de partage agent — même sélecteur que la vue admin « Sélections » */448.sel-crit{background:#fff;border:1px solid #e8e8e5;border-radius:12px;padding:14px 16px}449.sel-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;align-items:start}450.sel-field>label{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.02em;color:#777;margin-bottom:6px}451.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}452.sel-chips{display:flex;flex-wrap:wrap;gap:5px}453.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}454.sel-chips label:has(input:checked){background:#FFECCF;border-color:#FB8C00;color:#E67700;font-weight:600}455.sel-chips input{margin:0}456.sel-count{font-size:13px;color:#555;margin:2px 0 10px;font-weight:600}457.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}458.sel-row.off{opacity:.5;background:#fafaf8}459.sel-row input[type=checkbox]{width:18px;height:18px;flex:0 0 auto;accent-color:#FB8C00;cursor:pointer}460.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}461.sel-row .selinfo{flex:1;min-width:0}462.sel-row .selinfo b{font-size:13.5px}463.sel-row .selinfo .m{font-size:12px;color:#888;margin-top:1px}464.sel-linkbox{background:#fff;border:1px solid #FB8C00;border-radius:12px;padding:14px 16px}465.sel-linkbox input.lnk{padding:8px 10px;border:1px solid #ddd;border-radius:7px;font-size:12.5px;background:#f7f7f5;color:#333}466.sel-btn{border:1px solid #d5d5d0;background:#fff;color:#333;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer}467.sel-btn.primary{background:#FB8C00;border-color:#FB8C00;color:#fff}468/* Cartes de rail (partagées avec la section « Nouveautés » vue agent) */469.fr-card{flex:0 0 208px;background:#fff;border:1px solid #e8e8e5;border-radius:12px;overflow:hidden;cursor:pointer;transition:border-color .15s}470.fr-card:hover{border-color:#FB8C00}471.fr-img{height:112px;width:100%;display:block;object-fit:cover;background:#f0f0ee}472.fr-body{padding:8px 10px 10px}473.fr-t{font-size:12.5px;font-weight:600;color:#1a1a1a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}474.fr-m{font-size:11.5px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}475.fr-p{font-size:13px;font-weight:700;color:#1a1a1a;margin-top:2px}476/* Section « Nouveautés » (vue agent) */477#agent-new-rail{margin:0 0 20px;background:#EEF6FF;border:1px solid #BFDBFE;border-radius:14px;padding:12px 14px}478#agent-new-rail .an-head{display:flex;align-items:baseline;gap:10px;margin:0 0 8px;flex-wrap:wrap}479#agent-new-rail .an-title{font-size:16px;font-weight:700;color:#1D4ED8}480#agent-new-rail .an-hint{font-size:11.5px;color:#64748b}481.an-scroll{display:flex;gap:12px;overflow-x:auto;padding-bottom:6px}482.an-scroll .fr-card{flex:0 0 208px}483.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)}484/* Bascule entre immeubles liés (groupe_immeuble) — au-dessus du plan des unités */485.d-bld-switch{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;margin:0 0 10px}486.d-switch-lbl{font-size:12px;color:#8a8f96}487.d-switch-seg{display:inline-flex;flex-wrap:wrap;border:1px solid #E67700;border-radius:9px;overflow:hidden}488.d-switch-btn{display:inline-flex;align-items:center;gap:4px;font-size:12.5px;font-weight:700;padding:6px 14px;color:#E67700;background:#fff;border:none;border-right:1px solid #bfe0d4;cursor:pointer;transition:background .12s}489.d-switch-btn:last-child{border-right:none}490.d-switch-btn:hover:not(.cur){background:#eef6f2}491.d-switch-btn.cur{background:#E67700;color:#fff;cursor:default}492.d-switch-btn[disabled]{opacity:1}493</style>494<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.css"495  integrity="sha384-b8ANgTJvdlAnWM5YGMpKn7Kodm+1k7NYNG9zdjTCcZcKatzYHwZ0RLdWarbJJVzU" crossorigin="anonymous"/>496<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js"497  integrity="sha384-u5N8qJeJOO2iqNjIKTdl6KeKsEikMAmCUBPc6sC6uGpgL34aPJ4VgNhuhumedpEk" crossorigin="anonymous"></script>498<style>499#map-section{display:none;background:#fff;border-bottom:1px solid #e8e8e5}500#map{height:480px;width:100%}501.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}502.map-toggle:hover{border-color:#FB8C00;color:#FB8C00}503.map-toggle.active{background:#FB8C00;color:#fff;border-color:#FB8C00}504.leaflet-popup-content{font-family:system-ui,sans-serif;min-width:180px}505.popup-title{font-size:13px;font-weight:600;margin-bottom:4px;line-height:1.3}506.popup-price{font-size:15px;font-weight:700;color:#FB8C00;margin-bottom:4px}507.popup-dispo{font-size:11px;color:#888;margin-bottom:8px}508.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}509.popup-btn:hover{background:#E67700}510</style>511</head>512<body>513<div class="header">514  <div class="logo-row">515    <img src="data:image/png;base64,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" alt="Gestion ISR" style="height:38px;width:auto">516  </div>517  <div style="display:flex;gap:8px;align-items:center">518    <a class="map-toggle" href="/compte.html" style="text-decoration:none">519      <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>520      Mon profil521    </a>522    <button class="map-toggle" id="map-toggle-btn" onclick="toggleMap()">523      <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>524      Voir la carte525    </button>526    <a class="map-toggle" href="/nous-joindre" style="text-decoration:none">527      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M4 4h16v12H5.2L4 17.2z"/></svg>528      Nous joindre529    </a>530    <button class="map-toggle" id="agent-share-top" onclick="window.openAgentSel&&window.openAgentSel()" style="display:none;background:#FB8C00;color:#fff;border-color:#FB8C00">531      🔗 Créer une sélection532    </button>533  </div>534</div>535<!-- CARTE -->536<div id="map-section">537  <div id="map"></div>538</div>539540<div class="hero">541  <div class="count-badge" id="count-badge">Chargement…</div>542  <h1>Trouvez votre prochain chez-vous</h1>543  <p>Parcourez nos logements disponibles et faites une demande de visite en quelques clics.</p>544</div>545546<!-- MATCHER : association d'appartements sans compte -->547<div id="matcher-panel" class="matcher">548  <div class="matcher-inner">549    <h2>🎯 Votre prochain logement se trouve ici !</h2>550    <div class="matcher-grid">551      <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>552      <div class="mx-field"><label>Type recherché</label><div class="mx-chips" id="mx-type">553        <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>554      </div></div>555      <div class="mx-field"><label>Budget maximal ($/mois)</label><input id="mx-budget" type="number" min="0" step="1" placeholder="ex : 1200"></div>556      <div class="mx-field"><label>Étage</label><div class="mx-chips" id="mx-etage">557        <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>558      </div></div>559      <div class="mx-field"><label>Animaux</label><div class="mx-chips" id="mx-animaux">560        <label><input type="checkbox" value="Chat">🐈 Chat</label><label><input type="checkbox" value="Chien">🐶 Chien</label>561      </div></div>562      <div class="mx-actions-inline">563        <button class="mx-btn-more" type="button" id="mx-more-btn" onclick="toggleMxMore()">➕ Plus de critères</button>564        <button class="mx-btn-go" onclick="runMatcher()">✨ Rechercher</button>565      </div>566      <div class="mx-more" id="mx-more">567        <label>Caractéristiques souhaitées</label>568        <div class="mx-chips" id="mx-extras">569          <label><input type="checkbox" value="meuble">🛋️ Meublé</label>570          <label><input type="checkbox" value="chauffage">🔥 Chauffage inclus</label>571          <label><input type="checkbox" value="terrain">🌿 Accès au terrain</label>572          <label><input type="checkbox" value="balcon">🔆 Balcon</label>573          <label><input type="checkbox" value="thermopompe">🍃 Thermopompe</label>574          <label><input type="checkbox" value="aspirateur">🧹 Aspirateur central</label>575        </div>576      </div>577    </div>578  </div>579</div>580<div id="match-summary" style="display:none"></div>581582<div id="browse-controls">583<div class="bed-wrap" style="background:#fff;border-bottom:1px solid #e8e8e5;padding:0.85rem 2rem;display:flex;align-items:center;gap:8px;flex-wrap:wrap">584  <span style="font-size:11px;color:#aaa;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-right:2px">Chambres&nbsp;:</span>585  <button class="quick-filter-btn bed-btn active" data-bed="" onclick="setBedrooms('')">Toutes</button>586  <button class="quick-filter-btn bed-btn" data-bed="1" onclick="setBedrooms('1')">🛏️ 1 chambre</button>587  <button class="quick-filter-btn bed-btn" data-bed="2" onclick="setBedrooms('2')">🛏️ 2 chambres</button>588  <button class="quick-filter-btn bed-btn" data-bed="3" onclick="setBedrooms('3')">🛏️ 3 chambres</button>589  <button class="quick-filter-btn bed-btn" data-bed="4" onclick="setBedrooms('4')">🛏️ 4+ chambres</button>590  <button class="quick-filter-btn bed-btn" data-bed="maison" onclick="setBedrooms('maison')">🏠 Maison</button>591</div>592<div class="search-wrap" style="background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem">593  <div style="position:relative;max-width:500px">594    <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>595    <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">596  </div>597</div>598<div class="filters-wrap">599  <div class="filter-group">600    <label>Ville</label>601    <select id="f-ville" onchange="applyFilters()"><option value="">Toutes les villes</option></select>602  </div>603  <div class="filter-group">604    <label>Grandeur</label>605    <select id="f-taille" onchange="applyFilters()"><option value="">Toutes</option><option>3½</option><option>4½</option><option>5½</option><option>6½</option><option>Maison</option></select>606  </div>607  <div class="filter-group">608    <label>Étage</label>609    <select id="f-etage" onchange="applyFilters()">610      <option value="">Tous</option>611      <option value="demi">Demi sous-sol</option>612      <option value="rdc">Rez-de-chaussée</option>613      <option value="1er">1er étage</option>614      <option value="2e">2e étage</option>615      <option value="2etages-ss">2 étages + SS</option>616      <option value="3e">3e étage</option>617    </select>618  </div>619  <div class="filter-group">620    <label>Disponibilité</label>621    <select id="f-dispo" onchange="applyFilters()">622      <option value="">Toutes</option>623      <option value="maintenant">Dès maintenant</option>624      <option value="aout">Août 2026</option>625    </select>626  </div>627  <div class="filter-group">628    <label>À la une</label>629    <select id="f-etiquette" onchange="applyFilters()">630      <option value="">Toutes</option>631      <option value="Promotion">Promotion</option>632      <option value="Nouveauté">Nouveauté</option>633      <option value="PSL Accepté">PSL Accepté</option>634      <option value="Dernière unité">Dernière unité</option>635      <option value="Internet inclus">Internet inclus</option>636      <option value="Coup de coeur">Coup de coeur</option>637      <option value="Garage">Garage</option>638      <option value="Logement meublé">Logement meublé</option>639    </select>640  </div>641  <div class="filter-group">642    <label>Animaux</label>643    <select id="f-animaux" onchange="applyFilters()">644      <option value="">Peu importe</option>645      <option value="Aucun">🚫 Aucun animal</option>646      <option value="Chat">🐈 Chat accepté</option>647      <option value="Petits">🐈🐶 Petits compagnons</option>648      <option value="Animaux">🐈🐶 Animaux acceptés</option>649    </select>650  </div>651  <div class="price-range">652    <label>Prix ($/mois)</label>653    <div class="price-inputs">654      <input type="number" id="f-pmin" placeholder="Min" step="50" oninput="applyFilters()">655      <span>—</span>656      <input type="number" id="f-pmax" placeholder="Max" step="50" oninput="applyFilters()">657    </div>658  </div>659  <div class="filter-group">660    <label>Trier par</label>661    <select id="f-tri" onchange="applyFilters()">662      <option value="">Par défaut</option>663      <option value="prix-asc">Prix croissant ↑</option>664      <option value="prix-desc">Prix décroissant ↓</option>665      <option value="dispo">Disponibilité</option>666    </select>667  </div>668  <button class="btn-reset" onclick="resetFilters()">Réinitialiser</button>669</div>670<div class="tags-row" id="tags-row">671  <div class="quick-filters" id="quick-filters">672    <span style="font-size:11px;color:#aaa;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-right:4px">À la une :</span>673    <button class="quick-filter-btn" onclick="setEtiquette('Promotion')">🏷️ Promotion</button>674    <button class="quick-filter-btn" onclick="setEtiquette('Nouveauté')">✨ Nouveauté</button>675    <button class="quick-filter-btn" onclick="setEtiquette('PSL Accepté')">🔥 PSL Accepté</button>676    <button class="quick-filter-btn" onclick="setEtiquette('Dernière unité')">⚡ Dernière unité</button>677    <button class="quick-filter-btn" onclick="setEtiquette('Internet inclus')">🛜 Internet inclus</button>678    <button class="quick-filter-btn" onclick="setEtiquette('Coup de coeur')">❤️ Coup de coeur</button>679    <button class="quick-filter-btn" onclick="setEtiquette('Garage')">🚗 Garage</button>680  </div>681</div>682</div><!-- /browse-controls -->683<div class="main">684  <div id="selection-banner" style="display:none"></div>685  <!-- Nouveautés (vue agent uniquement) : annonces des 10 derniers jours + compteur de jours -->686  <section id="agent-new-rail" style="display:none">687    <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>688    <div class="an-scroll" id="an-scroll"></div>689  </section>690  <div class="results-line" id="results-line"></div>691  <div class="grid" id="grid">692    <div class="loading"><div class="loading-spinner"></div>Chargement des logements…</div>693  </div>694</div>695696<!-- NOUS JOINDRE -->697<footer id="contact-section">698  <div class="contact-inner">699    <h2>Nous joindre</h2>700    <p class="contact-lead">Une question sur un logement ? Notre équipe est là pour vous répondre.</p>701    <div class="contact-cards">702      <a class="contact-card" href="mailto:info@gestion-isr.com">703        <span class="cc-icon" aria-hidden="true">704          <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>705        </span>706        <span class="cc-text"><span class="cc-label">Par courriel</span><span class="cc-value">info@gestion-isr.com</span></span>707      </a>708      <a class="contact-card" href="tel:+18194721127">709        <span class="cc-icon" aria-hidden="true">710          <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>711        </span>712        <span class="cc-text"><span class="cc-label">Par téléphone</span><span class="cc-value">819-472-1127</span></span>713      </a>714    </div>715  </div>716</footer>717718<!-- Indicateur « accès agent » (visible seulement pour le staff connecté) -->719<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>720<!-- Outil de partage (agent) : accessible via le bouton « Créer une sélection » de l'en-tête -->721722<div id="agent-sel-overlay" style="display:none;position:fixed;inset:0;z-index:1500;background:#f5f5f3;flex-direction:column">723  <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;background:#E67700;color:#fff;flex-shrink:0">724    <div style="font-size:16px;font-weight:800">🔗 Sélections partageables</div>725    <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>726  </div>727  <div style="flex-shrink:0;background:#fff;border-bottom:1px solid #e6e6e2;padding:12px 18px">728    <div style="position:relative;max-width:1040px;margin:0 auto">729      <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>730      <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">731    </div>732  </div>733  <div style="flex:1;overflow-y:auto;padding:16px 18px 32px;max-width:1040px;width:100%;margin:0 auto;box-sizing:border-box">734    <div class="sel-crit">735      <div class="sel-grid">736        <div class="sel-field"><label>Type</label><div class="sel-chips" id="asel-type">737          <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>738        </div></div>739        <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>740        <div class="sel-field"><label>Préférence d'étage</label><div class="sel-chips" id="asel-etage">741          <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>742        </div></div>743        <div class="sel-field"><label>Animaux</label><div class="sel-chips" id="asel-animaux">744          <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>745        </div></div>746      </div>747      <div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">748        <button class="sel-btn primary" onclick="window.agentSelAll&&window.agentSelAll()">Sélectionner tout</button>749        <button class="sel-btn" onclick="window.agentSelNone&&window.agentSelNone()">Enlever la sélection</button>750        <button class="sel-btn" onclick="window.agentSelResetFilters&&window.agentSelResetFilters()">Réinitialiser les filtres</button>751      </div>752    </div>753    <div id="agent-sel-count" class="sel-count" style="margin-top:16px"></div>754    <div id="agent-sel-list"></div>755    <div id="agent-sel-share" style="margin-top:12px"></div>756  </div>757</div>758759<!-- MODAL DÉTAIL -->760<div class="detail-overlay" id="detail-overlay" onclick="closeDetailOutside(event)">761  <div class="detail-modal" id="detail-modal">762    <div class="gallery" id="detail-gallery"></div>763    <div class="detail-body">764      <div class="detail-header">765        <div>766          <div class="detail-ville" id="d-ville"></div>767          <div class="detail-title" id="d-title"></div>768        </div>769        <div style="text-align:right">770          <div class="detail-price" id="d-price"></div>771        </div>772      </div>773      <div class="detail-badges" id="d-badges"></div>774      <button class="btn-staged" id="d-staged-btn" style="display:none">🛋️ Voir meublé virtuellement</button>775      <div id="d-photo-models-sec" class="d-models-box" style="display:none">776        <div class="d-models-head">📷 Modèles d'appartement <span>— cliquez pour voir les photos de chaque modèle 👇</span></div>777        <div id="d-photo-models"></div>778      </div>779      <div class="detail-section" id="d-variants-sec" style="display:none">780        <h3>Modèles disponibles</h3>781        <div id="d-variants"></div>782      </div>783      <div class="detail-section" id="d-building-sec" style="display:none">784        <h3>🏢 Immeuble — unités <span style="font-size:12px;font-weight:400;color:#8a5a00">(vue agent · clique une unité)</span></h3>785        <div id="d-building"></div>786      </div>787      <div class="detail-section">788        <h3>Description</h3>789        <div class="detail-desc" id="d-desc"></div>790      </div>791      <hr class="detail-divider">792      <div class="detail-section">793        <h3>Localisation</h3>794        <div class="detail-map" id="detail-map"></div>795      </div>796      <div id="d-internal-wrap" style="display:none">797        <hr class="detail-divider">798        <div class="detail-section">799          <h3>🔒 Infos internes</h3>800          <div id="d-internal"></div>801        </div>802      </div>803      <hr class="detail-divider">804      <div class="contact-btns">805        <a class="btn-call" id="btn-call" href="tel:+18194721127">806          <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>807          Appeler808        </a>809        <a class="btn-email" id="btn-email" href="mailto:info@gestion-isr.com">810          <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>811          Courriel812        </a>813      </div>814      <button class="btn-visite-full" id="btn-visite-full">Formulaire de location</button>815      <button class="btn-share" id="btn-share" onclick="partagerFiche()">816        <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>817        Partager cette fiche818      </button>819      <button class="btn-retour" onclick="closeDetail()">← Retour aux logements</button>820    </div>821  </div>822</div>823824<!-- DÉTAIL D'UNE UNITÉ (vue agent) -->825<div class="unit-overlay" id="unit-overlay" onclick="closeUnitPanel(event)">826  <div class="unit-modal" id="unit-modal"></div>827</div>828829<!-- PLAN DE L'IMMEUBLE EN PLEIN ÉCRAN (grands immeubles) -->830<div class="bld-full-overlay" id="bld-full-overlay">831  <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>832    <div class="bld-zoom-ctl">833      <button onclick="bldZoomBy(0.8)" title="Dézoomer" aria-label="Dézoomer">−</button>834      <button onclick="bldZoomFit()" title="Tout voir" aria-label="Tout voir">⊡</button>835      <button onclick="bldZoomBy(1.25)" title="Zoomer" aria-label="Zoomer">+</button>836      <button onclick="closeBuildingFull()" title="Fermer" class="bld-zoom-close">&times;</button>837    </div>838  </div>839  <div class="bld-full-body" id="bld-full-body"></div>840</div>841842<!-- LIGHTBOX -->843<div class="lightbox-overlay" id="lightbox-overlay" onclick="closeLightboxOutside(event)">844  <button class="lightbox-close" onclick="closeLightbox()">&times;</button>845  <div class="lb-models" id="lightbox-models" onclick="event.stopPropagation()"></div>846  <button class="lightbox-prev" id="lb-prev" onclick="lightboxNav(-1)">‹</button>847  <img class="lightbox-img" id="lightbox-img" src="" alt="">848  <button class="lightbox-next" id="lb-next" onclick="lightboxNav(1)">›</button>849  <div class="lightbox-count" id="lightbox-count"></div>850</div>851852<!-- MODAL VISITE -->853<div class="visite-overlay" id="visite-overlay" onclick="closeVisiteOutside(event)">854  <div class="visite-modal" style="max-width:560px;padding:0;overflow:hidden;border-radius:16px">855    <div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;background:#fff;border-bottom:1px solid #f0f0ee">856      <div>857        <div class="modal-title">Formulaire de location</div>858        <div class="modal-sub" id="modal-apt-name"></div>859      </div>860      <button class="btn-close" onclick="closeVisite()">&times;</button>861    </div>862    <form id="visite-form">863      <div class="vf-msg" id="visite-msg"></div>864      <div class="vf-grid">865        <label class="vf-f vf-full"><span>Nom complet *</span><input id="vf-nom" required></label>866        <label class="vf-f"><span>Courriel *</span><input id="vf-courriel" type="email" required></label>867        <label class="vf-f"><span>Téléphone *</span><input id="vf-tel" type="tel" required></label>868        <label class="vf-f"><span>Date d'emménagement souhaitée</span><input id="vf-date" placeholder="ex : 1 juillet 2026"></label>869        <label class="vf-f"><span>Type recherché</span><select id="vf-type"><option value="">—</option><option>3½</option><option>4½</option><option>5½</option><option>6½</option><option>Maison</option></select></label>870        <label class="vf-f"><span>Budget max ($/mois)</span><input id="vf-budget" type="number" min="0" step="1" placeholder="ex : 1300"></label>871        <label class="vf-f"><span>Animaux</span><select id="vf-animaux"><option value="">—</option><option>Aucun</option><option>Chat</option><option>Chien</option><option>Chat et chien</option></select></label>872        <label class="vf-f"><span>Enquête de crédit acceptée ?</span><select id="vf-credit"><option value="">—</option><option>Oui</option><option>Non</option><option>Incertain</option></select></label>873        <label class="vf-f vf-full"><span>Message / questions</span><textarea id="vf-message" rows="3" placeholder="Ta motivation, tes questions…"></textarea></label>874      </div>875      <button type="submit" class="vf-submit" id="vf-submit">Envoyer ma demande</button>876    </form>877    <div id="visite-success" style="display:none;padding:2.5rem 1.5rem;text-align:center">878      <div style="font-size:52px;line-height:1;margin-bottom:14px">✅</div>879      <div style="font-size:19px;font-weight:800;color:#E67700;margin-bottom:8px">Demande envoyée&nbsp;!</div>880      <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>881      <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>882    </div>883  </div>884</div>885886<script>887// Source de données : Supabase (remplace l'ancien Google Sheet via opensheet)888const SUPABASE_URL = 'https://jfgkriojazaohgjezijj.supabase.co';889const SUPABASE_ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpmZ2tyaW9qYXphb2hnamV6aWpqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODEzNDE5NjQsImV4cCI6MjA5NjkxNzk2NH0.VVLfhvpkQ9WgJOhCYW3y6kRZIJbAQXTCANv6mXce1Yw';890// On ne lit que les annonces actives (la sécurité côté base l'impose de toute façon)891const LISTINGS_URL = `${SUPABASE_URL}/rest/v1/listings?select=*&statut=eq.Actif`;892// Disponibilité d'une unité : seul « Disponible » est offert au public. Les autres893// statuts (Loué, Départ planifié, En construction, Réservé…) sont considérés non disponibles.894const NON_DISPO = ['Loué','Départ planifié','En construction','Réservé Gestion ISR','Réservé propriétaire'];895const U_STATUTS = ['Disponible', ...NON_DISPO];   // pour le sélecteur de statut (vue agent)896const isAvail = u => !NON_DISPO.includes(u && u.statut);897// Couleur de tuile par statut. Les rôles internes (staff) voient les couleurs distinctes ;898// le public voit « vert » pour les statuts réservés / départ planifié (non révélés).899const UNIT_COLORS_INT = {900  'Disponible':['#DCF5EB','#16B981'], 'Loué':['#FCE4E4','#E0736C'], 'En construction':['#DCE9FF','#3B82F6'],901  'Départ planifié':['#EEE1FB','#8B5CF6'], 'Réservé Gestion ISR':['#FFE6CC','#F5871F'], 'Réservé propriétaire':['#FEF9C3','#EAB308']902};903const UNIT_COLORS_PUB = {904  'Disponible':['#DCF5EB','#16B981'], 'Loué':['#FCE4E4','#E0736C'], 'En construction':['#DCE9FF','#3B82F6'],905  'Départ planifié':['#DCF5EB','#16B981'], 'Réservé Gestion ISR':['#DCF5EB','#16B981'], 'Réservé propriétaire':['#DCF5EB','#16B981']906};907function unitColors(statut){ const map = window.__isStaff ? UNIT_COLORS_INT : UNIT_COLORS_PUB; return map[statut] || map['Disponible']; }908909let allListings = [], filtered = [], currentListing = null, currentPhotoIdx = 0;910911// Échappement HTML — protège contre les injections (XSS) provenant des données du Sheet.912// À utiliser pour TOUTE donnée du Sheet insérée dans innerHTML ou un attribut HTML.913function esc(s) {914  return String(s == null ? '' : s)915    .replace(/&/g, '&amp;')916    .replace(/</g, '&lt;')917    .replace(/>/g, '&gt;')918    .replace(/"/g, '&quot;')919    .replace(/'/g, '&#39;');920}921922// Redimensionne les photos Supabase à la volée (render/image) — bien plus léger/rapide.923// Les URLs externes (data:) sont inchangées.924function simg(url, w, q){925  if (!url || typeof url !== 'string') return url || '';926  if (url.indexOf('/storage/v1/object/public/') !== -1)927    // resize=contain : indispensable, sinon Supabase écrase la largeur sans ajuster la hauteur (image déformée/zoomée)928    return url.replace('/storage/v1/object/public/', '/storage/v1/render/image/public/') + (url.indexOf('?') !== -1 ? '&' : '?') + 'width=' + w + '&resize=contain&quality=' + (q || 70);929  return url;930}931async function getFolderPhotos(l) {932  // Photos Supabase uniquement.933  if (Array.isArray(l.photos) && l.photos.length){934    // Photo principale (choisie via l'étoile dans l'admin) en tête935    if (l.main_photo && l.photos.includes(l.main_photo)) return [l.main_photo, ...l.photos.filter(p => p !== l.main_photo)];936    return l.photos.slice();937  }938  return [];939}940941function getEtage(l) {942  return l['Étage'] || l['etage'] || l['étage'] || l['ETAGE'] || l['Etage'] || '';943}944945function normalizeEtage(e) {946  if (!e) return '';947  const s = e.toLowerCase().trim()948    .replace(/é/g,'e').replace(/è/g,'e').replace(/ê/g,'e')949    .replace(/\s+/g,' ');950  if (951    s.includes('2 etages + ss') ||952    s.includes('2 etages ss') ||953    s.includes('2 etages sous-sol') ||954    s.includes('2 etages sous sol') ||955    s.includes('2 etages et sous-sol') ||956    s.includes('2 etages et sous sol') ||957    s.includes('deux etages sous-sol') ||958    s.includes('deux etages sous sol')959  ) return '2etages-ss';960  if (s.includes('demi')) return 'demi';961  if (s.includes('sous-sol') || s.includes('sous sol')) return 'sous-sol';962  if (s.includes('jardin')) return 'rdj';963  if (s.includes('rez') || s.includes('chaussee')) return 'rdc';964  if (s.includes('1er') || s.includes('premier')) return '1er';965  if (s.includes('2e') || s.includes('deuxieme')) return '2e';966  if (s.includes('3e') || s.includes('troisieme')) return '3e';967  return '';968}969970function etageLabel(e) {971  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'};972  return map[e] || e;973}974975// ---- Variantes (modèles regroupés dans une même fiche) ----976function hasVariants(l){ return Array.isArray(l.variants) && l.variants.length > 0; }977function 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]; }978function variantLoyers(l){ return (l.variants || []).flatMap(variantEffPrices); }979// Prix des unités : DISPONIBLES d'abord (repli sur toutes si tout est loué). Le prix public980// « dès… » d'un immeuble doit refléter les unités réellement disponibles, pas les louées.981function unitLoyers(l){982  if (!Array.isArray(l.units) || !l.units.length) return [];983  const avail = l.units.filter(u => isAvail(u));984  const src = avail.length ? avail : l.units;985  return src.map(u => parseFloat(u.prix)).filter(x => !isNaN(x) && x > 0);986}987function 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; }988function 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; }989function 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]; }990function 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 []; }991function 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; }992function variantEtages(v){ return [...new Set(variantFloors(v).filter(f => Number(f.qte) > 0).map(f => f.etage))]; }993// ---- Unités individuelles (modèle immeuble) ----994function hasUnits(l){ return Array.isArray(l.units) && l.units.length > 0; }995// Étages des unités DISPONIBLES (repli sur toutes si tout est loué). Source de vérité pour996// la recherche + le matcher d'un immeuble : le champ « Étage » du haut devient inutile ici.997function unitEtagesNorm(l){998  if (!hasUnits(l)) return [];999  const avail = l.units.filter(u => isAvail(u));1000  const src = avail.length ? avail : l.units;1001  return [...new Set(src.map(u => normalizeEtage(u.etage)).filter(Boolean))];1002}1003function totalUnits(l){ return (l.variants || []).reduce((s,v) => s + variantQte(v), 0); }1004function distinct(arr){ return [...new Set(arr.filter(x => x != null && x !== ''))]; }1005function grandeurDisplay(l){ if (hasVariants(l)){ const s = distinct(l.variants.map(v => v.grandeur || l.grandeur)); if (s.length) return s.join(' · '); } return l.grandeur || ''; }1006function dispoDisplay(l){ 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]; } return l.date_dispo_affichage || l.date_dispo || ''; }1007function variantMatchesGrandeur(l, taille){ return hasVariants(l) ? l.variants.some(v => (v.grandeur || l.grandeur) === taille) : (l.grandeur === taille); }1008function variantMatchesEtage(l, etage){1009  if (hasUnits(l)){ const es = unitEtagesNorm(l); if (es.length) return es.includes(etage); }1010  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); });1011  return normalizeEtage(getEtage(l)) === etage;1012}1013function variantMatchesDispo(l, dispo){ return hasVariants(l) ? l.variants.some(v => normalizeDispo(v.dispo || l.date_dispo_affichage) === dispo) : (normalizeDispo(l.date_dispo_affichage) === dispo); }1014function 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; }10151016// ===== MATCHER (association d'appartements, sans compte) =====1017const 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:''} };1018let matchCriteria = null, matchByApp = {};1019function listingEtagesNorm(l){1020  if (hasUnits(l)){ const e = unitEtagesNorm(l); if (e.length) return e; }1021  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))]; }1022  const n=normalizeEtage(getEtage(l)); return n?[n]:[];1023}1024function 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'); }1025// ---- Critères supplémentaires (caractéristiques) ----1026// `has` lit la donnée structurée d'une unité (unit.carac) ; `txt` est un repli sur le texte de1027// l'annonce, car la majorité des fiches n'ont pas encore de caractéristiques structurées.1028const EXTRA_CRIT = [1029  { key:'meuble',      label:'Meublé',             has:c => !!(c.meuble && c.meuble !== 'Non meublé'),       txt:t => /meubl/.test(t) },1030  { key:'chauffage',   label:'Chauffage inclus',   has:c => !!(c.chauffage && c.chauffage !== 'Non inclus'), txt:t => /chauffage/.test(t) },1031  { key:'terrain',     label:'Accès au terrain',   has:c => !!c.terrain,                                     txt:t => /acc[eè]s au terrain/.test(t) },1032  { key:'balcon',      label:'Balcon',             has:c => !!c.balcon,                                      txt:t => /balcon/.test(t) },1033  { key:'thermopompe', label:'Thermopompe',        has:c => /thermopompe/i.test(c.clim || ''),               txt:t => /thermopompe/.test(t) },1034  { key:'aspirateur',  label:'Aspirateur central', has:c => !!c.aspirateur_central,                          txt:t => /aspirateur central/.test(t) },1035];1036// Texte de l'annonce en minuscules, débarrassé des tournures négatives pour éviter les faux positifs1037// (« non meublé » ne doit pas matcher « meublé »).1038function _extraHay(l){1039  const units = Array.isArray(l.units) ? l.units : [];1040  const avail = units.filter(u => isAvail(u));1041  const src = avail.length ? avail : units;1042  return [l.description || '', ...src.map(u => u.caract || '')].join(' \n ').toLowerCase()1043    .replace(/non\s+meubl\w*/g, '')1044    .replace(/chauffage\s+non\s+inclus/g, '')1045    .replace(/(pas|aucun)\s+(de\s+)?balcon/g, '')1046    .replace(/(pas|aucun)\s+(d.)?acc[eè]s au terrain/g, '');1047}1048function listingHasExtra(l, def){1049  if (l.carac && def.has(l.carac)) return true;                   // fiche virtuelle (modèle) : carac directe1050  const units = Array.isArray(l.units) ? l.units : [];1051  const avail = units.filter(u => isAvail(u));1052  const src = avail.length ? avail : units;1053  if (src.some(u => u.carac && def.has(u.carac))) return true;   // donnée structurée = source de vérité1054  return def.txt(_extraHay(l));                                   // repli : texte de l'annonce1055}1056function matchListing(c, l){1057  const chips=[], blocks=[]; let score=0, specified=0, satisfied=0, soft=false;1058  if (c.types.length){ specified++; const gs = hasVariants(l) ? distinct(l.variants.map(v=>v.grandeur||l.grandeur)) : [l.grandeur]; const hit = gs.find(g=>c.types.includes(g)); if(hit){ chips.push({txt:'✓ '+hit}); score+=3; satisfied++; } else blocks.push('type'); }1059  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'); }1060  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 chips.push({txt:'✗ Étage',miss:true}); }1061  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; } }1062  // Caractéristiques : pastille ✗ plutôt qu'un blocage — les données sont encore partielles,1063  // on ne masque donc jamais une annonce, elle est seulement moins bien classée.1064  (c.extras || []).forEach(k => {1065    const def = EXTRA_CRIT.find(x => x.key === k); if (!def) return;1066    specified++;1067    if (listingHasExtra(l, def)){ chips.push({txt:'✓ '+def.label}); score+=1; satisfied++; }1068    else chips.push({txt:'✗ '+def.label, miss:true});1069  });1070  const compatible = blocks.length===0; let tier='ok';1071  if (compatible && specified>0){ if(satisfied===specified&&!soft) tier='perfect'; else if(satisfied===specified) tier='excellent'; else if(satisfied/specified>=0.6) tier='good'; }1072  return { compatible, score, chips, tier };1073}1074function readMatcherForm(){1075  const chips = sel => [...document.querySelectorAll(sel+' input:checked')].map(i=>i.value);1076  const b = document.getElementById('mx-budget').value.trim();1077  const sect = document.getElementById('mx-secteur');1078  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') };1079}1080function toggleMxMore(){1081  const p = document.getElementById('mx-more'), b = document.getElementById('mx-more-btn');1082  if (!p || !b) return;1083  const show = !p.classList.contains('show');1084  p.classList.toggle('show', show);1085  b.textContent = show ? '➖ Moins de critères' : '➕ Plus de critères';1086}1087window.toggleMxMore = toggleMxMore;1088function runMatcher(){1089  const c = readMatcherForm();1090  matchCriteria = c;1091  const avail = allListings.filter(l => (l.etiquette||'').toLowerCase() !== 'loué' && (!c.secteur || String(l.ville||'').trim() === c.secteur));1092  const scored = avail.map(l => ({ l, m: matchListing(c, l) })).filter(x => x.m.compatible)1093    .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)));1094  matchByApp = {}; scored.forEach(x => { matchByApp[x.l.ID_app || x.l.id_app] = x.m; });1095  filtered = scored.map(x => x.l);1096  document.getElementById('matcher-panel').style.display = 'none';1097  document.getElementById('browse-controls').style.display = 'none';1098  showMatchSummary(c, filtered);1099  render();1100  document.getElementById('match-summary').scrollIntoView({ behavior:'smooth', block:'start' });1101}1102function editMatcher(){1103  matchCriteria = null; matchByApp = {};1104  document.getElementById('matcher-panel').style.display = '';1105  document.getElementById('browse-controls').style.display = '';1106  document.getElementById('match-summary').style.display = 'none';1107  filtered = [...allListings]; render();1108  document.getElementById('matcher-panel').scrollIntoView({ behavior:'smooth', block:'start' });1109}1110function clearMatcher(){1111  matchCriteria = null; matchByApp = {};1112  document.getElementById('matcher-panel').style.display = '';1113  document.getElementById('browse-controls').style.display = '';1114  document.getElementById('match-summary').style.display = 'none';1115  filtered = [...allListings]; render(); renderTags();1116  window.scrollTo({ top:0, behavior:'smooth' });1117}1118function showMatchSummary(c, list){1119  const parts = [];1120  if (c.secteur) parts.push('📍 ' + c.secteur);1121  if (c.types.length) parts.push(c.types.join(', '));1122  if (c.budget) parts.push('≤ ' + c.budget + ' $');1123  if (c.etages.length) parts.push(c.etages.map(e => etageLabel(e)).join(', '));1124  if (c.pets.length) parts.push('🐾 ' + c.pets.join(', '));1125  if (c.extras && c.extras.length) parts.push(c.extras.map(k => (EXTRA_CRIT.find(x => x.key === k) || {}).label).filter(Boolean).join(', '));1126  const perfectN = list.filter(l => (matchByApp[l.ID_app||l.id_app]||{}).tier === 'perfect').length;1127  const el = document.getElementById('match-summary');1128  el.style.display = 'block';1129  el.innerHTML = `<div class="match-bar">1130    <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>1131    <div class="mx-sum-actions" style="display:flex;gap:8px">1132      <button class="mx-sum-btn" onclick="editMatcher()">✏️ Modifier mes critères</button>1133      <button class="mx-sum-btn" onclick="clearMatcher()">Voir tous les logements</button>1134    </div>1135  </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>' : ''}`;1136}11371138// Sélection partagée par l'admin : /?apps=az101,az102 → n'affiche que ces logements1139function applySharedSelection() {1140  const appsParam = new URLSearchParams(location.search).get('apps');1141  if (!appsParam) return;1142  const want = new Set(appsParam.split(',').map(s => normIdApp(s)).filter(Boolean));1143  if (!want.size) return;1144  allListings = allListings.filter(l => {1145    const base = normIdApp(l.id_app || l.ID_app);1146    // cartes d'immeuble éclatées : id_app suffixé « base-N » ou « base-<modèle> »1147    return want.has(base) || [...want].some(w => base.startsWith(w + '-'));1148  });1149  showSelectionBanner(allListings.length);1150}1151function showSelectionBanner(n, title) {1152  const el = document.getElementById('selection-banner');1153  if (!el) return;1154  el.style.display = 'block';1155  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>';1156  const lead = title || '📋 Sélection personnalisée';1157  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">1158    <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>`;1159}1160// Lien propriétaire : /?p=<token> → n'affiche que les logements ACTIFS de ce propriétaire1161async function applyOwnerSelection() {1162  const token = new URLSearchParams(location.search).get('p');1163  if (!token) return;1164  try {1165    const resp = await fetch(`${SUPABASE_URL}/rest/v1/rpc/owner_public_listings`, {1166      method: 'POST',1167      headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json' },1168      body: JSON.stringify({ p_token: token.trim() })1169    });1170    const rows = await resp.json();1171    if (!Array.isArray(rows)) return;1172    const want = new Set(rows.map(r => normIdApp(r.id_app)).filter(Boolean));1173    allListings = allListings.filter(l => {1174      const base = normIdApp(l.id_app || l.ID_app);1175      // cartes d'immeuble éclatées : id_app suffixé « base-N »1176      return want.has(base) || [...want].some(w => base.startsWith(w + '-'));1177    });1178    // Coordonnées du propriétaire (profil) → affichées sur TOUTES ses fiches via le lien.1179    try {1180      const cr = await fetch(`${SUPABASE_URL}/rest/v1/rpc/owner_public_contact`, {1181        method: 'POST',1182        headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json' },1183        body: JSON.stringify({ p_token: token.trim() })1184      });1185      const cd = await cr.json();1186      if (Array.isArray(cd) && cd[0]) window.__ownerContact = cd[0];1187    } catch (_) {}1188    showOwnerTopLink();   // pas de grosse bannière : juste un lien discret en haut1189  } catch (_) {}1190}1191// Lien discret « Voir tous les logements » en haut, quand on est sur un lien propriétaire.1192function showOwnerTopLink(){1193  const actions = document.getElementById('map-toggle-btn') && document.getElementById('map-toggle-btn').parentElement;1194  if (!actions || document.getElementById('owner-alllink')) return;1195  const a = document.createElement('a');1196  a.id = 'owner-alllink';1197  a.href = '/';1198  a.className = 'map-toggle';1199  a.title = 'Voir l’ensemble des logements du site';1200  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';1201  a.style.cssText = 'background:#FB8C00;color:#fff;border-color:#FB8C00;text-decoration:none;white-space:nowrap;order:-1';1202  actions.insertBefore(a, actions.firstChild);1203}1204// Un immeuble (fiche_type immeuble) est éclaté en UNE carte par unité DISPONIBLE : photos du1205// modèle lié (★ en tête, sinon album de l'immeuble), type/prix/étage/caractéristiques de l'unité.1206// Chaque carte est une annonce virtuelle (recherche + matcher). Les unités louées n'apparaissent pas.1207function cleanIdApp(s){ return String(s || '').replace(/\s+/g, '').replace(/½/g,'.5').replace(/¼/g,'.25').replace(/¾/g,'.75'); }1208// Normalise un id_app pour la COMPARAISON des liens partagés : minuscules, sans espaces,1209// fractions converties → un ancien lien « …-3½ » (%C2%BD) correspond au nouvel « …-3.5 ».1210function normIdApp(s){ return String(s || '').toLowerCase().replace(/\s+/g, '').replace(/½/g,'.5').replace(/¼/g,'.25').replace(/¾/g,'.75'); }1211function expandBuildingCards(list){1212  const out = [];1213  (list || []).forEach(l => {1214    const avail = Array.isArray(l.units) ? l.units.map((u, idx) => ({ u, idx })).filter(x => x.u && isAvail(x.u)) : [];1215    const models = Array.isArray(l.photo_variants) ? l.photo_variants : [];1216    // Éclatement en cartes UNIQUEMENT si au moins une unité dispo est liée à un modèle de photo.1217    // Sinon l'immeuble garde son plan (une carte + le visuel des unités par étage).1218    const anyModel = avail.some(x => x.u.photoModel && models.some(m => m && m.label === x.u.photoModel));1219    if (l.fiche_type === 'appartement' || !avail.length || !anyModel){ out.push(l); return; }1220    // Regroupe les unités disponibles PAR MODÈLE de photo : une seule carte par modèle1221    // (plusieurs unités identiques = un seul choix affiché). Les unités sans modèle1222    // gardent une carte chacune.1223    const groups = new Map();1224    avail.forEach(({ u, idx }) => {1225      const hasModel = u.photoModel && models.some(m => m && m.label === u.photoModel);1226      const key = hasModel ? ('m:' + u.photoModel) : ('u:' + idx);1227      if (!groups.has(key)) groups.set(key, { label: hasModel ? u.photoModel : '', items: [] });1228      groups.get(key).items.push({ u, idx });1229    });1230    groups.forEach(g => {1231      const items = g.items, first = items[0].u;1232      const model = g.label ? models.find(m => m && m.label === g.label) : null;1233      const mp = model ? modelPhotos(model) : [];1234      const photos = mp.length ? mp : (Array.isArray(l.photos) && l.photos.length ? l.photos : []);1235      const prices = items.map(x => parseFloat(x.u.prix)).filter(p => !isNaN(p) && p > 0);1236      const minPrix = prices.length ? Math.min(...prices) : (first.prix != null ? first.prix : l.loyer);1237      const types = [...new Set(items.map(x => x.u.type).filter(Boolean))];1238      const typeLabel = types.length === 1 ? types[0] : ((model && model.grandeur) || types.join(' / '));1239      const single = items.length === 1;1240      const vl = Object.assign({}, l);1241      vl._virtual = true; vl._parentPk = l.pk; vl._modelLabel = g.label;1242      vl._unitIdx = items[0].idx; vl._unitId = single ? (first.id || null) : null;1243      vl._groupUnitIds = items.map(x => x.u.id).filter(Boolean);1244      vl._unitCount = items.length;1245      vl._bldUnits = l.units;   // plan complet de l'immeuble sur la fiche1246      vl.pk = (l.pk != null ? l.pk : (l.id_app || 'x')) + ':' + (g.label ? ('m' + g.label) : ('u' + items[0].idx));1247      vl.id_app = cleanIdApp((l.id_app || l.ID_app || '') + '-' + (g.label || (items[0].idx + 1)));1248      vl.ID_app = vl.id_app;1249      vl.titre = (l.titre || '') + (typeLabel ? ' — ' + typeLabel : '');1250      vl.grandeur = typeLabel || l.grandeur || '';1251      if (minPrix != null) vl.loyer = minPrix;1252      vl.etage = single ? (first.etage || '') : '';1253      vl.units = null; vl.variants = null; vl.photo_variants = null;1254      vl.photos = photos.slice(); vl.main_photo = photos[0] || null;1255      // Meublé virtuellement : rendu propre au modèle si présent, sinon celui de la fiche (repli)1256      vl.staged_photos = (model && Array.isArray(model.staged) && model.staged.length) ? model.staged.slice() : (l.staged_photos || null);1257      vl.carac = single ? (first.carac || null) : null;1258      vl.description = single ? (first.caract || l.description || '') : (l.description || '');1259      vl.chambres = first.chambres; vl.sdb = first.sdb;1260      out.push(vl);1261    });1262  });1263  return out;1264}1265async function loadListings() {1266  try {1267    const resp = await fetch(LISTINGS_URL, {1268      headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON }1269    });1270    const json = await resp.json();1271    allListings = (Array.isArray(json) ? json : []).filter(l => l.pk || l.id);1272    // Compat : l'ancien code utilise l.ID_app ; en base la colonne s'appelle id_app1273    allListings.forEach(l => { if (l.ID_app === undefined) l.ID_app = l.id_app; });1274    allListings = expandBuildingCards(allListings);   // une carte par unité disponible (immeubles)1275    // Les fiches créées par des propriétaires ne s'affichent PAS sur le site Gestion ISR ;1276    // elles ne sont visibles que via le lien de partage du propriétaire (?p=<token>).1277    if (!new URLSearchParams(location.search).get('p')) {1278      allListings = allListings.filter(l => !l.created_by_owner);1279    }1280    applySharedSelection();1281    await applyOwnerSelection();1282    filtered = [...allListings];1283    buildVilleFilter();1284    renderAgentNewRail();1285    render();1286    // Lien profond : ?fiche=<réf> ouvre directement la fiche de l'annonce1287    { const fiche = new URLSearchParams(location.search).get('fiche');1288      if (fiche){ const f = String(fiche).toLowerCase();1289        let idx = allListings.findIndex(l => String(l.id_app || l.ID_app || '').toLowerCase() === f);1290        if (idx < 0) idx = allListings.findIndex(l => String(l.id_app || l.ID_app || '').toLowerCase().split('-')[0] === f);1291        if (idx >= 0) openDetail(idx);1292      }1293    }1294  } catch(e) {1295    document.getElementById('grid').innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:4rem;color:#888">Impossible de charger les logements.</div>';1296  }1297}12981299function buildVilleFilter() {1300  const villes = [...new Set(allListings.map(l => l.ville).filter(Boolean))].sort();1301  const sel = document.getElementById('f-ville');1302  if (sel) villes.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; sel.appendChild(o); });1303  // Dropdown « Secteur visé » du matcher (mêmes secteurs = villes)1304  const ms = document.getElementById('mx-secteur');1305  if (ms) villes.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; ms.appendChild(o); });1306}13071308function normalizeDispo(d) {1309  if (!d) return '';1310  const s = d.toLowerCase();1311  if (s.includes('maintenant') || s.includes('dès')) return 'maintenant';1312  if (s.includes('mars')) return 'mars';1313  if (s.includes('mai')) return 'mai';1314  if (s.includes('juin')) return 'juin';1315  if (s.includes('juillet')) return 'juillet';1316  if (s.includes('août') || s.includes('aout')) return 'aout';1317  return '';1318}13191320let bedFilter = '';1321function grandeurToBedrooms(g){1322  const s = String(g || '');1323  const m = s.match(/(\d+)\s*(?:½|1\/2|\.5)?/);1324  if (m){ const n = parseInt(m[1], 10); if (!isNaN(n) && n >= 3 && n <= 12) return n - 2; }1325  return null;1326}1327function bedroomsOf(l){1328  const nums = new Set();1329  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); } };1330  const addGrand = g => { const b = grandeurToBedrooms(g); if (b != null) nums.add(b); };1331  if (l.chambres != null && l.chambres !== '') addNum(l.chambres); else addGrand(l.grandeur);1332  if (Array.isArray(l.units)) l.units.forEach(u => { if (u && u.chambres != null && u.chambres !== '') addNum(u.chambres); else if (u) addGrand(u.type); });1333  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); });1334  return nums;1335}1336function isMaison(l){1337  const has = g => /maison/i.test(String(g || ''));1338  if (has(l.grandeur)) return true;1339  if (Array.isArray(l.units) && l.units.some(u => has(u && u.type))) return true;1340  if (Array.isArray(l.variants) && l.variants.some(v => has(v && (v.grandeur || v.type)))) return true;1341  return false;1342}1343function matchesBedrooms(l, sel){1344  if (!sel) return true;1345  if (sel === 'maison') return isMaison(l);1346  const nums = bedroomsOf(l);1347  if (!nums.size) return false;1348  if (sel === '4') return [...nums].some(n => n >= 4);1349  return nums.has(parseInt(sel, 10));1350}1351function setBedrooms(v){1352  bedFilter = v;1353  document.querySelectorAll('.bed-btn').forEach(b => b.classList.toggle('active', b.getAttribute('data-bed') === v));1354  applyFilters();1355}1356function etiquetteColor(e){1357  const s = String(e || '').toLowerCase();1358  if (s.includes('promo')) return '#F5871F';1359  if (s.includes('derni')) return '#DC2626';1360  if (s.includes('psl')) return '#7C3AED';1361  if (s.includes('internet')) return '#0EA5E9';1362  if (s.includes('coeur') || s.includes('cœur')) return '#DB2777';1363  if (s.includes('garage')) return '#64748B';1364  if (s.includes('meubl')) return '#0D9488';1365  return '#F59E0B';1366}1367function applyFilters() {1368  const ville = document.getElementById('f-ville').value;1369  const taille = document.getElementById('f-taille').value;1370  const etage = document.getElementById('f-etage').value;1371  const animaux = document.getElementById('f-animaux').value;1372  const dispo = document.getElementById('f-dispo').value;1373  const etiquette = document.getElementById('f-etiquette').value;1374  const search = document.getElementById('f-search').value.toLowerCase().trim();1375  const pmin = parseFloat(document.getElementById('f-pmin').value) || 0;1376  const pmax = parseFloat(document.getElementById('f-pmax').value) || Infinity;1377  filtered = allListings.filter(l => {1378    if (etiquette && (l.etiquette||'').toLowerCase() !== etiquette.toLowerCase()) return false;1379    if (search) {1380      const haystack = ((l.titre||'')+(l.ville||'')+(l.description||'')+(l.ID_app||'')).toLowerCase();1381      if (!haystack.includes(search)) return false;1382    }1383    if (ville && l.ville !== ville) return false;1384    if (bedFilter && !matchesBedrooms(l, bedFilter)) return false;1385    if (taille && !variantMatchesGrandeur(l, taille)) return false;1386    if (etage && !variantMatchesEtage(l, etage)) return false;1387    if (animaux) {1388      const a = (l.animaux || '').toLowerCase();1389      if (animaux === 'Aucun' && !a.includes('aucun')) return false;1390      if (animaux === 'Chat' && !a.includes('chat')) return false;1391      if (animaux === 'Petits' && !a.includes('petit')) return false;1392      if (animaux === 'Animaux' && (!a || a.includes('aucun'))) return false;1393    }1394    if (dispo && !variantMatchesDispo(l, dispo)) return false;1395    const loyers = loyerSet(l);1396    if (loyers.length && !loyers.some(x => x >= pmin && x <= pmax)) return false;1397    return true;1398  });1399  // Tri1400  const tri = document.getElementById('f-tri').value;1401  if (tri === 'prix-asc') {1402    filtered.sort((a,b) => minLoyer(a) - minLoyer(b));1403  } else if (tri === 'prix-desc') {1404    filtered.sort((a,b) => maxLoyer(b) - maxLoyer(a));1405  } else if (tri === 'dispo') {1406    filtered.sort((a,b) => dispoOrderIndex(a) - dispoOrderIndex(b));1407  }1408  render(); renderTags();1409}14101411function resetFilters() {1412  ['f-ville','f-taille','f-etage','f-animaux','f-dispo','f-etiquette','f-tri'].forEach(id => document.getElementById(id).value = '');1413  document.getElementById('f-pmin').value = '';1414  document.getElementById('f-pmax').value = '';1415  document.getElementById('f-search').value = '';1416  bedFilter = '';1417  document.querySelectorAll('.bed-btn').forEach(b => b.classList.toggle('active', b.getAttribute('data-bed') === ''));1418  filtered = [...allListings];1419  render(); renderTags();1420}14211422function renderTags() {1423  const tags = [];1424  const ville = document.getElementById('f-ville').value;1425  const taille = document.getElementById('f-taille').value;1426  const etage = document.getElementById('f-etage').value;1427  const animaux = document.getElementById('f-animaux').value;1428  const dispo = document.getElementById('f-dispo').value;1429  const etiquette = document.getElementById('f-etiquette').value;1430  const pmin = document.getElementById('f-pmin').value;1431  const pmax = document.getElementById('f-pmax').value;1432  const dispoLabels = {'maintenant':'Dès maintenant','mars':'Mars 2026','mai':'Mai 2026','juin':'Juin 2026','juillet':'Juillet 2026','aout':'Août 2026'};1433  if (ville) tags.push({label:'Ville : '+ville, clear:()=>{document.getElementById('f-ville').value='';applyFilters();}});1434  if (bedFilter) tags.push({label: bedFilter==='maison' ? 'Maison' : ((bedFilter==='4'?'4+':bedFilter)+' chambre'+(bedFilter==='1'?'':'s')), clear:()=>setBedrooms('')});1435  if (taille) tags.push({label:taille, clear:()=>{document.getElementById('f-taille').value='';applyFilters();}});1436  if (etage) tags.push({label:etageLabel(etage), clear:()=>{document.getElementById('f-etage').value='';applyFilters();}});1437  if (animaux) tags.push({label:animaux, clear:()=>{document.getElementById('f-animaux').value='';applyFilters();}});1438  if (etiquette) tags.push({label:'À la une : '+etiquette, clear:()=>{document.getElementById('f-etiquette').value='';applyFilters();}});1439  if (dispo) tags.push({label:'Dispo : '+(dispoLabels[dispo]||dispo), clear:()=>{document.getElementById('f-dispo').value='';applyFilters();}});1440  if (pmin||pmax) tags.push({label:'Prix : '+(pmin||'0')+' — '+(pmax||'∞')+' $', clear:()=>{document.getElementById('f-pmin').value='';document.getElementById('f-pmax').value='';applyFilters();}});1441  const row = document.getElementById('tags-row');1442  // Filtrer les tags pour exclure étiquette (géré par les chips)1443  const otherTags = tags.filter(t => !t.label.startsWith('À la une'));1444  1445  // Toujours afficher les chips1446  let html = `<div class="quick-filters" id="quick-filters">1447    <span style="font-size:11px;color:#aaa;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-right:4px">À la une :</span>1448    <button class="quick-filter-btn" onclick="setEtiquette('Promotion')">🏷️ Promotion</button>1449    <button class="quick-filter-btn" onclick="setEtiquette('Nouveauté')">✨ Nouveauté</button>1450    <button class="quick-filter-btn" onclick="setEtiquette('PSL Accepté')">🔥 PSL Accepté</button>1451    <button class="quick-filter-btn" onclick="setEtiquette('Dernière unité')">⚡ Dernière unité</button>1452    <button class="quick-filter-btn" onclick="setEtiquette('Internet inclus')">🛜 Internet inclus</button>1453    <button class="quick-filter-btn" onclick="setEtiquette('Coup de coeur')">❤️ Coup de coeur</button>1454    <button class="quick-filter-btn" onclick="setEtiquette('Garage')">🚗 Garage</button>1455  </div>`;1456  1457  if (otherTags.length) {1458    html += otherTags.map((t,i)=>`<span class="tag">${esc(t.label)}<button onclick="clearTag(${i})">&times;</button></span>`).join('');1459  }1460  1461  row.innerHTML = html;1462  window._tagClears = otherTags.map(t=>t.clear);1463  updateQuickFilters();1464}14651466function clearTag(i){window._tagClears[i]();}14671468function setEtiquette(val) {1469  const sel = document.getElementById('f-etiquette');1470  if (sel.value === val) {1471    sel.value = '';1472  } else {1473    sel.value = val;1474  }1475  applyFilters();1476}14771478function updateQuickFilters() {1479  const val = document.getElementById('f-etiquette').value;1480  document.querySelectorAll('.quick-filter-btn').forEach(btn => {1481    const btnVal = btn.getAttribute('onclick').match(/setEtiquette\('(.+?)'\)/);1482    if (btnVal) {1483      btn.classList.toggle('active', btnVal[1] === val);1484    }1485  });1486}14871488window.refreshHearts = function(){1489  document.querySelectorAll('.card-heart[data-app]').forEach(h => {1490    h.classList.toggle('active', !!(window.__favs && window.__favs.has(h.getAttribute('data-app'))));1491  });1492};14931494// Nouveautés (vue agent) : annonces créées depuis ≤ 10 jours, avec le nombre de jours affiché.1495function renderAgentNewRail(){1496  const sec = document.getElementById('agent-new-rail'), track = document.getElementById('an-scroll');1497  if (!sec || !track) return;1498  if (!window.__isStaff){ sec.style.display = 'none'; return; }   // agent uniquement1499  const sp = new URLSearchParams(location.search);1500  if (sp.get('apps') || sp.get('p')){ sec.style.display = 'none'; return; }1501  const now = Date.now(); const seenPk = new Set();1502  const items = (allListings || []).map(l => ({ l, days: l.created_at ? Math.floor((now - new Date(l.created_at).getTime()) / 86400000) : 999 }))1503    .filter(({ l, days }) => {1504      if (days < 0 || days > 10) return false;1505      if (String(l.etiquette || '').trim().toLowerCase() === 'loué') return false;1506      const pk = l._virtual ? l._parentPk : l.pk;1507      if (pk == null || seenPk.has(pk)) return false; seenPk.add(pk);1508      return true;1509    }).sort((a, b) => a.days - b.days).slice(0, 20);1510  if (!items.length){ sec.style.display = 'none'; return; }1511  const card = ({ l, days }) => {1512    const id = String(l.id_app || l.ID_app || '');1513    const photo = l.main_photo || (Array.isArray(l.photos) && l.photos.length ? l.photos[0] : '');1514    const mn = minLoyer(l);1515    const meta = [grandeurDisplay(l), l.ville].filter(Boolean).join(' · ');1516    const prix = mn ? ((hasVariants(l) || hasUnits(l)) ? 'dès ' : '') + Number(mn).toLocaleString('fr-CA') + ' $' : '';1517    const img = photo ? `<img class="fr-img" loading="lazy" decoding="async" src="${esc(simg(photo, 420, 62))}" alt="">` : '<div class="fr-img"></div>';1518    const badge = `<span class="fr-days">${days < 1 ? 'Nouveau' : days + 'j'}</span>`;1519    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>`;1520  };1521  track.innerHTML = items.map(card).join('');1522  sec.style.display = '';1523  track.querySelectorAll('[data-newapp]').forEach(el => el.addEventListener('click', () => {1524    const id = el.getAttribute('data-newapp');1525    const idx = (allListings || []).findIndex(x => String(x.id_app || x.ID_app || '') === id);1526    if (idx >= 0) openDetail(idx);1527  }));1528}1529async function render() {1530  const g = document.getElementById('grid');1531  const n = filtered.length;1532  // Total réel de logements = somme des unités (les variantes comptent pour leur nombre d'unités)1533  const units = filtered.reduce((s,l) => s + (hasVariants(l) ? Math.max(totalUnits(l), 1) : 1), 0);1534  document.getElementById('count-badge').textContent = units+' logement'+(units!==1?'s':'')+' disponible'+(units!==1?'s':'');1535  document.getElementById('results-line').textContent = units+' logement'+(units!==1?'s':'')+' trouvé'+(units!==1?'s':'') + (units!==n ? ` · ${n} annonce${n!==1?'s':''}` : '');1536  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;}1537  1538  // Tri par défaut prix croissant (sauf en mode correspondances, trié par pertinence)1539  if (!matchCriteria && !document.getElementById('f-tri').value) {1540    filtered.sort((a,b) => minLoyer(a) - minLoyer(b));1541  }15421543  const cards = filtered.map((l, idx) => {1544    // Vignette SYNCHRONE : photos Supabase déjà chargées → aucun appel réseau bloquant.1545    const thumb = (l.main_photo && Array.isArray(l.photos) && l.photos.includes(l.main_photo))1546      ? l.main_photo1547      : (Array.isArray(l.photos) && l.photos.length ? l.photos[0] : null);1548    const loyer = parseFloat(l.loyer)||0;1549    const etageNorm = normalizeEtage(getEtage(l));1550    const etageTag = etageNorm ? `<span class="card-tag ct-etage">${etageLabel(etageNorm)}</span>` : '';1551    // Immeuble à unités : badge « X dispo · Y unités » plutôt qu'un étage top-level trompeur.1552    const nU = hasUnits(l) ? l.units.length : 0;1553    const dispoU = hasUnits(l) ? l.units.filter(u => isAvail(u)).length : 0;1554    const etageCard = hasUnits(l)1555      ? `<span class="card-tag ct-variantes">${dispoU} dispo · ${nU} unité${nU>1?'s':''}</span>`1556      : (l._unitCount > 11557          ? `<span class="card-tag ct-variantes">${l._unitCount} logements disponibles</span>`1558          : (etageNorm ? `<span class="card-tag ct-etage">${etageLabel(etageNorm)}</span>` : ''));1559    const etiqLow = (l.etiquette || '').toLowerCase();1560    const cardClass = etiqLow === 'promotion' ? ' card-promotion'1561      : etiqLow === 'psl accepté' ? ' card-psl'1562      : etiqLow === 'dernière unité' ? ' card-derniere'1563      : etiqLow === 'coup de coeur' ? ' card-coupdecoeur'1564      : '';1565    const mm = matchCriteria ? (matchByApp[l.ID_app || l.id_app] || null) : null;1566    const mTier = (mm && TIER[mm.tier]) ? TIER[mm.tier] : null;1567    const tierCls = (mTier && mTier.cls) ? ' ' + mTier.cls : '';1568    const tierBadge = (mTier && mTier.label) ? `<div class="mx-tier-badge ${mTier.cls}">${mTier.label}</div>` : '';1569    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>` : '';1570    return `<div class="card${cardClass}${tierCls}" onclick="openDetail(${allListings.indexOf(l)})">1571      <div class="card-img">1572        ${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>`}1573        <div class="badge-row">1574          ${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>`:''}1575        </div>1576        ${l.etiquette&&l.etiquette.toLowerCase()==='loué'?`<div class="ribbon-loue">LOUÉ</div>`:''}1577        ${l.etiquette&&l.etiquette.toLowerCase()==='nouveauté'?`<div class="ribbon-nouveau">NOUVEAUTÉ</div>`:''}1578        ${(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>`:''}1579        <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>1580      </div>1581      <div class="card-body">1582        ${tierBadge}1583        <div class="card-ville">📍 ${esc(l.ville)}</div>1584        <div class="card-title">${esc(l.titre)}</div>1585        <div class="card-tags">1586          <span class="card-tag ct-size">${esc(grandeurDisplay(l))}</span>1587          <span class="card-tag ct-date">${esc(dispoDisplay(l))}</span>1588          ${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}1589        </div>1590        ${Array.isArray(l.photo_variants)&&l.photo_variants.length?`<button class="card-models-btn" onclick="event.stopPropagation();openVariantPhotos(${allListings.indexOf(l)})">📷 Voir les modèles (${l.photo_variants.length})</button>`:''}1591        ${matchChips}1592        <div class="card-footer">1593          <div class="price">${hasVariants(l)?`<span class="price-from">dès </span>`:''}${minLoyer(l).toLocaleString('fr-CA')} $<span> / mois</span></div>1594          <button class="btn-visite-card" onclick="event.stopPropagation();openVisiteFromCardIdx(${allListings.indexOf(l)})">Demander une visite</button>1595        </div>1596      </div>1597    </div>`;1598  });1599  if (matchCriteria){1600    const counts = {}; filtered.forEach(l => { const t = (matchByApp[l.ID_app||l.id_app]||{}).tier || 'ok'; counts[t] = (counts[t]||0) + 1; });1601    let out = '', last = null;1602    filtered.forEach((l, idx) => {1603      const t = (matchByApp[l.ID_app||l.id_app]||{}).tier || 'ok';1604      if (t !== last){ last = t; out += matchGroupHeader(t, counts[t]); }1605      out += cards[idx];1606    });1607    g.innerHTML = out;1608  } else {1609    g.innerHTML = cards.join('');1610  }1611}1612function matchGroupHeader(tier, n){1613  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' };1614  return `<div class="match-group-header" style="--gc:${info.color}"><span>${esc(info.label)}</span><span class="mgh-count">${n}</span></div>`;1615}16161617function renderDetailVariants(l){1618  const sec = document.getElementById('d-variants-sec');1619  const box = document.getElementById('d-variants');1620  if (!hasVariants(l)){ sec.style.display = 'none'; box.innerHTML = ''; return; }1621  const rows = l.variants.map((v, i) => {1622    const label = v.label || String.fromCharCode(65 + i);1623    const grandeur = v.grandeur || l.grandeur || '';1624    const sup = (v.superficie != null && v.superficie !== '') ? Number(v.superficie).toLocaleString('fr-CA') + ' pi²' : '—';1625    const floors = variantFloors(v).filter(f => Number(f.qte) > 0);1626    const et = floors.length1627      ? 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>')1628      : '—';1629    const tot = variantQte(v);1630    const dispo = v.dispo || '—';1631    const eff = variantEffPrices(v);1632    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') + ' $') : '—';1633    const st = v.statut === 'Loué' ? 'Loué' : 'Disponible';1634    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>`;1635    return `<tr>1636      <td><span class="v-label">${esc(label)}</span>${grandeur ? esc(grandeur) : ''}${stB}${v.note ? `<div class="v-note">${esc(v.note)}</div>` : ''}</td>1637      <td>${esc(sup)}</td>1638      <td>${et}</td>1639      <td>${esc(dispo)}</td>1640      <td>${tot ? esc(tot) + (tot > 1 ? ' unités' : ' unité') : '—'}</td>1641      <td class="v-price">${esc(prix)}</td>1642    </tr>`;1643  }).join('');1644  box.innerHTML = `<div class="vtable-scroll"><table class="vtable">1645    <thead><tr><th>Modèle</th><th>Superficie</th><th>Étages disponibles</th><th>Disponibilité</th><th>Total</th><th>Loyer</th></tr></thead>1646    <tbody>${rows}</tbody></table></div>`;1647  sec.style.display = '';1648}1649// ---- PHASE 2 — Immeuble & unités (vue agent) ----1650const FLOOR_ORDER = ['Penthouse','5e étage','4e étage','3e étage','2e étage','1er étage','Rez-de-chaussée','Demi sous-sol','Sous-sol'];1651function orderFloorsTop(names){ const idx = n => { const i = FLOOR_ORDER.indexOf(n); return i < 0 ? 50 : i; }; return [...names].sort((a,b) => idx(a) - idx(b)); }1652// Libellé court d'un immeuble pour les boutons de bascule : le texte entre parenthèses1653// du titre (« … (Immeuble 1635) » → « 1635 »), sinon la référence.1654function groupImmeubleLabel(s){1655  const t = String((s && s.titre) || '');1656  const m = t.match(/\(([^)]+)\)/);1657  if (m){ const inner = m[1].replace(/immeuble\s*/i, '').trim(); return inner || m[1].trim(); }1658  return (s && (s.id_app || s.ID_app)) || 'Immeuble';1659}1660function renderAgentBuilding(l){1661  const sec = document.getElementById('d-building-sec'), box = document.getElementById('d-building');1662  if (!sec || !box) return;1663  // Plan affiché pour une fiche IMMEUBLE ayant des unités — y compris sur une carte d'appartement1664  // (virtuelle), via les unités de l'immeuble parent (_bldUnits). Une fiche « appartement » jamais.1665  const units = (Array.isArray(l.units) && l.units.length) ? l.units : (Array.isArray(l._bldUnits) ? l._bldUnits : null);1666  if (l.fiche_type === 'appartement' || !units){ sec.style.display = 'none'; return; }1667  const virt = !!l._virtual;1668  const vs = document.getElementById('d-variants-sec'); if (vs) vs.style.display = 'none';   // le visuel remplace le tableau des modèles1669  const h3 = sec.querySelector('h3');1670  if (h3) h3.innerHTML = virt1671    ? '🏢 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>'1672    : (window.__isStaff1673      ? '🏢 Immeuble — unités <span style="font-size:12px;font-weight:400;color:#8a5a00">(vue agent · clique une unité)</span>'1674      : '🏢 Unités disponibles <span style="font-size:12px;font-weight:400;color:#8a8f96">(clique une unité pour les détails)</span>');1675  const byFloor = {}; units.forEach((u,i) => { const f = u.etage || 'Autre'; (byFloor[f] = byFloor[f] || []).push({ u, i }); });1676  const floors = orderFloorsTop(Object.keys(byFloor));1677  const dispo = units.filter(u => isAvail(u)).length;1678  const tileFn = ({ u, i }) => {1679    const loue = !isAvail(u);1680    const [bg, bd] = unitColors(u.statut);1681    // Le prix n'est jamais montré pour un logement non disponible.1682    const prix = (!loue && u.prix != null && u.prix !== '') ? Number(u.prix).toLocaleString('fr-CA') + ' $' : '';1683    const staff = !!window.__isStaff;1684    // Statut affiché en infobulle : le public ne voit pas « réservé » (montré « Disponible »).1685    const tipStatut = staff ? (u.statut || (loue ? 'Loué' : 'Disponible')) : (u.statut === 'Loué' ? 'Loué' : (u.statut === 'En construction' ? 'En construction' : 'Disponible'));1686    // Le numéro d'appartement n'est visible que pour l'agent ; le candidat voit type + prix.1687    const line1 = staff ? (u.numero || ('App ' + (i + 1))) : (u.type || 'Logement');1688    const line2 = staff ? [u.type || '', prix].filter(Boolean).join(' · ') : prix;1689    const tip = [staff ? (u.numero || '') : '', u.type || '', prix, tipStatut].filter(Boolean).join(' · ');1690    // Sur une carte virtuelle : seuls les appartements DISPONIBLES sont cliquables (ils ont une fiche).1691    // Le staff peut TOUJOURS cliquer (pour changer le statut, même un logement loué).1692    const locked = staff ? false : (virt ? loue : loue);1693    // Sur une carte virtuelle : toutes les unités du MODÈLE ouvert sont marquées « courant ».1694    const cur = virt && (l._modelLabel ? (u.photoModel === l._modelLabel) : (i === l._unitIdx));1695    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}">1696      <div class="mn">${esc(line1)}</div>1697      <div class="mm">${line2 ? esc(line2) : '&nbsp;'}</div>1698    </div>`;1699  };1700  const floorsHtml = floors.map(f => {1701    // Regroupe par position (Avant/Arrière…) si au moins une unité de l'étage en a une.1702    const groups = []; const gi = {};1703    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); });1704    const anyPos = groups.some(g => g.pos);1705    const inner = anyPos1706      ? 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('')1707      : byFloor[f].map(tileFn).join('');1708    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>`;1709  }).join('');1710  const bigBtn = units.length > 8 ? `<button class="d-bld-expand" id="d-bld-expand">🔍 Voir le plan en grand</button>` : '';1711  // Bascule entre immeubles liés (même valeur groupe_immeuble) — boutons au-dessus du plan1712  let switchHtml = '';1713  const grp = String(l.groupe_immeuble || '').trim();1714  if (grp){1715    const curPk = l._virtual ? l._parentPk : l.pk;1716    // On regroupe par immeuble PARENT (une entrée par immeuble), qu'il soit éclaté (cartes1717    // virtuelles) ou affiché en plan unique. Les cartes virtuelles héritent de groupe_immeuble.1718    const seenPk = new Set(); const uniq = [];1719    (allListings || []).forEach(x => {1720      if (String(x.groupe_immeuble || '').trim() !== grp) return;1721      const pk = x._virtual ? x._parentPk : x.pk;1722      if (pk == null || seenPk.has(pk)) return;1723      const hasUnits = (Array.isArray(x.units) && x.units.length) || (Array.isArray(x._bldUnits) && x._bldUnits.length);1724      if (!hasUnits) return;1725      seenPk.add(pk); uniq.push({ pk, titre: x.titre, id_app: x.id_app, ID_app: x.ID_app });1726    });1727    uniq.sort((a,b) => String(groupImmeubleLabel(a)).localeCompare(String(groupImmeubleLabel(b)), 'fr', { numeric:true }));1728    if (uniq.length > 1){1729      switchHtml = `<div class="d-bld-switch"><span class="d-switch-seg">` + uniq.map(s => {1730        const cur = String(s.pk) === String(curPk);1731        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>`;1732      }).join('') + `</span></div>`;1733    }1734  }1735  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>${bigBtn}<div class="d-bld">${floorsHtml}</div>`;1736  // Ouvre le panneau d'information de l'unité (double-clic sur les cartes à modèles).1737  const openUnit = (i, u) => {1738    if (window.__isStaff){ if (u) openUnitPanel(l, u); return; }   // staff : toujours (même loué)1739    if (u && !isAvail(u)) return;                                  // public : pas de détails si non dispo1740    openUnitPanel(l, u);1741  };1742  // Simple clic sur une carte à modèles : bascule vers le modèle de l'unité cliquée.1743  const switchModel = (i, u) => {1744    if (!virt || !u || !isAvail(u)) return false;1745    const sameModel = l._modelLabel ? (u.photoModel === l._modelLabel) : (i === l._unitIdx);1746    if (sameModel) return false;                                   // déjà le modèle affiché1747    const sib = allListings.find(x => x._virtual && x._parentPk === l._parentPk &&1748      ((u.photoModel && x._modelLabel) ? x._modelLabel === u.photoModel : x._unitIdx === i));1749    if (sib){ openDetail(allListings.indexOf(sib)); return true; }1750    return false;1751  };1752  const wireUnitClicks = root => root.querySelectorAll('.d-unit').forEach(el => {1753    const i = +el.getAttribute('data-uidx'), u = units[i];1754    el.addEventListener('click', () => {1755      // Cartes à modèles : simple clic = bascule de modèle ; les détails s'ouvrent au double-clic.1756      // Cartes simples (sans modèles) : simple clic ouvre directement (comportement inchangé).1757      if (virt){ switchModel(i, u); return; }1758      openUnit(i, u);1759    });1760    el.addEventListener('dblclick', () => openUnit(i, u));1761  });1762  wireUnitClicks(box);1763  box.querySelectorAll('[data-switchpk]').forEach(b => b.addEventListener('click', () => {1764    const pk = b.getAttribute('data-switchpk');1765    let idx = (allListings || []).findIndex(x => String(x.pk) === String(pk) && !x._virtual);1766    if (idx < 0) idx = (allListings || []).findIndex(x => String(x._parentPk) === String(pk));   // immeuble éclaté → 1re carte1767    if (idx >= 0) openDetail(idx);1768  }));1769  const eb = box.querySelector('#d-bld-expand'); if (eb) eb.addEventListener('click', () => openBuildingFull(l, floorsHtml, wireUnitClicks));1770  sec.style.display = '';1771}1772let _bldZoom = 1;1773function _bldEl(){ const b = document.getElementById('bld-full-body'); return b ? b.querySelector('.d-bld') : null; }1774function bldApplyZoom(){ const el = _bldEl(); if (el) el.style.zoom = _bldZoom; }1775function bldZoomBy(f){ _bldZoom = Math.min(1.6, Math.max(0.2, _bldZoom * f)); bldApplyZoom(); }1776window.bldZoomBy = bldZoomBy;1777function bldZoomFit(){1778  const body = document.getElementById('bld-full-body'), el = _bldEl();1779  if (!body || !el) return;1780  el.style.zoom = 1;1781  const cw = body.clientWidth - 28;               // largeur dispo (padding 14px x2)1782  const w = el.scrollWidth;1783  if (cw <= 0 || w <= 0) return;                   // pas encore mesurable (mobile) : le fallback réessaiera1784  _bldZoom = (w > cw) ? Math.max(0.2, cw / w) : 1;  // rentre à l'écran, sinon 100 %1785  bldApplyZoom();1786  body.scrollLeft = 0; body.scrollTop = 0;1787}1788window.bldZoomFit = bldZoomFit;1789function openBuildingFull(l, floorsHtml, wireUnitClicks){1790  const ov = document.getElementById('bld-full-overlay'), body = document.getElementById('bld-full-body');1791  if (!ov || !body) return;1792  body.innerHTML = `<div class="d-bld">${floorsHtml}</div>`;1793  wireUnitClicks(body);1794  _bldZoom = 1; bldApplyZoom();1795  ov.classList.add('open'); ovOpened();1796  // Ajuste une fois la mise en page faite. Sur mobile le double rAF peut mesurer trop tôt1797  // (scrollWidth pas prêt) → fallbacks temporisés qui garantissent l'auto-zoom.1798  requestAnimationFrame(() => { requestAnimationFrame(bldZoomFit); });1799  setTimeout(bldZoomFit, 120);1800  setTimeout(bldZoomFit, 350);1801}1802function closeBuildingFull(){ const ov = document.getElementById('bld-full-overlay'); if (ov && ov.classList.contains('open')){ ov.classList.remove('open'); ovClosedUI(); } }1803window.closeBuildingFull = closeBuildingFull;1804// Glisser pour déplacer le plan (souris) — le tactile défile déjà nativement1805(function(){1806  const body = document.getElementById('bld-full-body'); if (!body) return;1807  let down = false, sx = 0, sy = 0, sl = 0, st = 0;1808  body.addEventListener('mousedown', e => { if (e.target.closest('.d-unit')) return; down = true; sx = e.clientX; sy = e.clientY; sl = body.scrollLeft; st = body.scrollTop; body.classList.add('grabbing'); });1809  window.addEventListener('mousemove', e => { if (!down) return; body.scrollLeft = sl - (e.clientX - sx); body.scrollTop = st - (e.clientY - sy); });1810  window.addEventListener('mouseup', () => { down = false; body.classList.remove('grabbing'); });1811  // Molette + Ctrl (souris) et pincer (tactile) pour zoomer1812  body.addEventListener('wheel', e => { if (!e.ctrlKey) return; e.preventDefault(); bldZoomBy(e.deltaY < 0 ? 1.1 : 0.9); }, { passive:false });1813  let pinch = null;1814  const dist = t => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);1815  body.addEventListener('touchstart', e => { if (e.touches.length === 2){ pinch = { d: dist(e.touches), z: _bldZoom }; } }, { passive:true });1816  body.addEventListener('touchmove', e => {1817    if (pinch && e.touches.length === 2){1818      e.preventDefault();1819      const r = dist(e.touches) / pinch.d;1820      _bldZoom = Math.min(1.6, Math.max(0.2, pinch.z * r));1821      bldApplyZoom();1822    }1823  }, { passive:false });1824  body.addEventListener('touchend', e => { if (e.touches.length < 2) pinch = null; });1825})();1826// Libellés des notes internes par unité (vue agent). Mêmes clés que l'admin.1827const UNIT_INT_ORDER = ['adresse_exacte','directive_acces','loc_boite_lettres','loc_cabanon','borne_electrique','locataire_nom','locataire_tel','locataire_courriel','cout_electricite','tolerance_animaux','flexibilite_prix','promotion_possible','psl_accepte','complement_info','commentaire_honnete','problematique_connue','selection_candidat','notes'];1828const 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' };1829function renderUnitIntNotes(raw){1830  if (raw == null || raw === '') return '<div style="color:#999;font-size:13px">Aucune note interne pour cette unité.</div>';1831  if (typeof raw === 'string') return `<div class="up-note">${esc(raw).replace(/\n/g,'<br>')}</div>`;   // ancien format (chaîne)1832  const rows = UNIT_INT_ORDER.filter(k => raw[k] != null && String(raw[k]).trim())1833    .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('');1834  return rows || '<div style="color:#999;font-size:13px">Aucune note interne pour cette unité.</div>';1835}1836window.renderUnitIntNotes = renderUnitIntNotes;   // utilisée aussi par le bloc module (infos internes)1837async function openUnitPanel(l, u){1838  if (!u) return;1839  const ov = document.getElementById('unit-overlay'), m = document.getElementById('unit-modal');1840  const loue = !isAvail(u);1841  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>` : '';1842  const prix = (u.prix != null && u.prix !== '') ? Number(u.prix).toLocaleString('fr-CA') + ' $ /mois' : '—';1843  const staff = !!window.__isStaff;1844  const headTitle = staff ? (u.numero || 'Unité') : (u.type || 'Logement');1845  const headSub = staff ? [u.type || '', u.etage || ''].filter(Boolean).join(' · ') : (u.etage || '');1846  // Photos propres à cette unité = le modèle de photo qui lui est lié1847  const model = (u.photoModel && Array.isArray(l.photo_variants)) ? l.photo_variants.find(v => v && v.label === u.photoModel) : null;1848  const uphotos = modelPhotos(model);   // photo principale (★) en tête1849  window.__unitPhotos = uphotos;1850  const photosHtml = uphotos.length1851    ? `<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>`1852    : '';1853  m.innerHTML = `<button class="unit-close" onclick="closeUnitPanel()">&times;</button>1854    <div class="up-head"><div><div class="up-num">${esc(headTitle)}</div><div class="up-sub">${esc(headSub)}</div></div>1855    <span class="up-stat ${loue?'st-loue':'st-dispo'}">${loue?'Loué':'Disponible'}</span></div>1856    ${photosHtml}1857    <div class="up-price">${prix}</div>1858    ${staff ? `<div class="up-sec" style="color:#E67700">Statut — disponibilité (agent)</div>1859      <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>1860      <div id="up-statut-msg" style="font-size:12px;margin-top:5px;min-height:16px"></div>` : ''}1861    <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>1862    ${u.caract?`<div class="up-sec">Description</div><div class="up-caract">${esc(u.caract)}</div>`:''}1863    <div id="up-internal" class="up-internal"></div>`;1864  ov.classList.add('open'); ovOpened();1865  { const sel = document.getElementById('up-statut-sel'); if (sel) sel.addEventListener('change', () => agentSetUnitStatut(l, u, sel.value, sel)); }1866  // La VISIBILITÉ des infos internes est décidée par la RLS elle-même (source de vérité), pas1867  // seulement par le drapeau calculé au chargement : un candidat reçoit une ligne vide (aucun1868  // accès), un agent/admin reçoit la ligne. On affiche donc la note si la RLS a laissé lire1869  // la ligne (data non nul) OU si le drapeau staff est déjà connu.1870  try {1871    const { data } = await _sb.from('listings_internal').select('unit_notes').eq('listing_pk', l._virtual ? l._parentPk : l.pk).maybeSingle();1872    const wrap = document.getElementById('up-internal'); if (!wrap) return;1873    if (data || window.__isStaff){1874      const note = data && data.unit_notes && data.unit_notes[u.id];1875      wrap.innerHTML = `<div class="up-sec" style="color:#8a5a00">🔒 Notes internes (unité)</div>${renderUnitIntNotes(note)}`;1876    } else {1877      wrap.innerHTML = '';   // candidat : aucune info interne1878    }1879  } catch(e){ const wrap = document.getElementById('up-internal'); if (wrap) wrap.innerHTML = ''; }1880  // Diagnostic temporaire (visible si l'URL contient « diag ») — n'affiche que TES propres infos de session1881  try {1882    if (/diag/i.test(location.href)){1883      const { data: { session } } = await _sb.auth.getSession();1884      const { data: st, error: stErr } = await _sb.rpc('is_staff');1885      let lect = '?';1886      try { const { data: d2, error: e2 } = await _sb.from('listings_internal').select('unit_notes').eq('listing_pk', l.pk).maybeSingle();1887        lect = e2 ? ('ERREUR: '+e2.message) : (d2 ? ('ligne lue · note unité = ' + (d2.unit_notes && d2.unit_notes[u.id] ? '«'+(typeof d2.unit_notes[u.id]==='object'?JSON.stringify(d2.unit_notes[u.id]):d2.unit_notes[u.id])+'»' : 'vide')) : 'aucune ligne interne'); }1888      catch(ex){ lect = 'EXCEPTION: ' + ex.message; }1889      const wrap = document.getElementById('up-internal');1890      if (wrap) wrap.insertAdjacentHTML('beforeend', `<div style="margin-top:12px;padding:10px;border:1px dashed #b58900;border-radius:8px;font-size:12px;line-height:1.5;color:#444;background:#fffdf3">🩺 <b>DIAG</b><br>connecté&nbsp;: <b>${session ? 'OUI ('+esc(session.user.email)+')' : 'NON'}</b><br>is_staff (base)&nbsp;: <b>${st===true?'OUI':(st===false?'NON':'?')}</b>${stErr?(' — err: '+esc(stErr.message)):''}<br>window.__isStaff&nbsp;: <b>${String(window.__isStaff)}</b><br>listing_pk&nbsp;: ${esc(l.pk||'∅')}<br>unit_id&nbsp;: ${esc(u.id||'∅')}<br>lecture note&nbsp;: <b>${esc(lect)}</b></div>`);1891    }1892  } catch(e){}1893}1894// Agent : change le statut d'une unité (écrit les units en base + journalise l'historique)1895async function agentSetUnitStatut(l, u, newStatut, selEl){1896  const parentPk = l._virtual ? l._parentPk : l.pk;1897  const units = (Array.isArray(l.units) && l.units.length) ? l.units : (Array.isArray(l._bldUnits) ? l._bldUnits : []);1898  const target = units.find(x => String(x.id) === String(u.id));1899  const msg = document.getElementById('up-statut-msg');1900  if (!target || !parentPk){ if(msg){ msg.textContent = 'Impossible (fiche introuvable).'; msg.style.color='#b91c1c'; } return; }1901  const old = target.statut || 'Disponible';1902  if (old === newStatut) return;1903  if (selEl) selEl.disabled = true;1904  if (msg){ msg.textContent = 'Enregistrement…'; msg.style.color='#888'; }1905  target.statut = newStatut;   // optimiste (affichage local)1906  // Passe par une RPC sécurisée : le staff ne peut changer QUE le statut d'une unité (pas toute la fiche).1907  const { error } = await _sb.rpc('agent_set_unit_statut', { p_pk: String(parentPk), p_unit_id: String(u.id||''), p_statut: newStatut });1908  if (error){1909    target.statut = old; if (selEl){ selEl.value = old; selEl.disabled = false; }1910    if (msg){ msg.textContent = 'Échec : ' + error.message + ' — 41-historique-statuts.sql exécuté ?'; msg.style.color='#b91c1c'; }1911    return;1912  }1913  u.statut = newStatut;1914  // Journalisation (best-effort)1915  try {1916    const { data: { session } } = await _sb.auth.getSession();1917    const par = session && session.user ? session.user.email : '';1918    const label = [u.numero || '', u.type || '', u.etage || ''].filter(Boolean).join(' · ');1919    _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(()=>{}, ()=>{});1920  } catch(e){}1921  if (selEl) selEl.disabled = false;1922  if (msg){ msg.textContent = '✅ Statut mis à jour'; msg.style.color='#E67700'; }1923  const badge = document.querySelector('#unit-modal .up-stat');1924  if (badge){ const loue = !isAvail(u); badge.className = 'up-stat ' + (loue ? 'st-loue' : 'st-dispo'); badge.textContent = loue ? 'Loué' : 'Disponible'; }1925  renderAgentBuilding(l);           // rafraîchit les tuiles du plan1926  if (typeof render === 'function') render();   // recalcule les compteurs de la grille1927}1928function closeUnitPanel(e){ if (e && e.target && e.target !== document.getElementById('unit-overlay')) return; document.getElementById('unit-overlay').classList.remove('open'); ovClosedUI(); }1929window.closeUnitPanel = closeUnitPanel;19301931// ---- Bouton « retour » du téléphone : ferme le modal ouvert (au lieu de quitter la page) ----1932const _OV_IDS = ['lightbox-overlay','unit-overlay','bld-full-overlay','visite-overlay','detail-overlay'];   // du plus « au-dessus » au plus « en dessous »1933let _ovPushed = 0, _ovIgnorePop = false;1934// Verrouille le défilement du fond dès qu'un overlay est ouvert → évite la 2e barre de défilement.1935function _syncBodyScroll(){ const anyOpen = _OV_IDS.some(x => { const el = document.getElementById(x); return el && el.classList.contains('open'); }); document.body.style.overflow = anyOpen ? 'hidden' : ''; }1936function ovOpened(url){ _ovPushed++; try { history.pushState({ ov: _ovPushed }, '', url || undefined); } catch(e){} _syncBodyScroll(); }1937function ovClosedUI(){ if (_ovPushed > 0){ _ovPushed--; _ovIgnorePop = true; try { history.back(); } catch(e){ _ovIgnorePop = false; } } _syncBodyScroll(); }1938window.addEventListener('popstate', () => {1939  if (_ovIgnorePop){ _ovIgnorePop = false; _syncBodyScroll(); return; }   // fermeture déclenchée par l'UI, déjà traitée1940  const id = _OV_IDS.find(x => { const el = document.getElementById(x); return el && el.classList.contains('open'); });1941  if (id){ _ovPushed = Math.max(0, _ovPushed - 1); document.getElementById(id).classList.remove('open'); }1942  _syncBodyScroll();1943});1944// Ferme sans bruit les overlays « enfants » d'une fiche (plan, unité, lightbox) restés ouverts.1945// Empêche une nouvelle fiche de s'ouvrir DERRIÈRE le plan plein écran (z-index plus élevé).1946function _closeChildOverlays(){1947  ['lightbox-overlay','unit-overlay','bld-full-overlay'].forEach(id => {1948    const el = document.getElementById(id);1949    if (el && el.classList.contains('open')){ el.classList.remove('open'); if (_ovPushed > 0) _ovPushed--; }1950  });1951}19521953// ---- Description agrégée : combine les caractéristiques des unités DISPONIBLES ----1954// Champs communs à toutes les dispos → affichés ; champs qui diffèrent → « plusieurs… ».1955// À mesure que des unités sont louées, elles sortent du calcul → la description se précise.1956const 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','🐾'] };1957function genAggregateAnnonce(l){1958  const units = Array.isArray(l.units) ? l.units : [];1959  if (!units.length) return l.description || '';1960  const avail = units.filter(u => isAvail(u));1961  const U = avail.length ? avail : units;1962  const C = U.map(u => u.carac || {});1963  if (!C.some(c => c && Object.keys(c).length)) return l.description || '';1964  const num = x => { const v = parseFloat(x); return isNaN(v) ? null : v; };1965  const commonSel = k => { const vs = C.map(c => c[k] || ''); const f = vs[0]; return vs.every(v => v === f) ? f : null; };1966  const allBool = k => C.every(c => c[k]);1967  const lines = [];1968  const anyImmediate = C.some(c => c.dispoImmediate);1969  const dispos = [...new Set(C.map(c => c.dispo).filter(Boolean))];1970  const commonDispo = (!anyImmediate && dispos.length === 1) ? dispos[0] : null;1971  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 !')));1972  const prices = U.map(u => num(u.prix)).filter(x => x != null);1973  if (prices.length){ const mn = Math.min(...prices), mx = Math.max(...prices); lines.push(mn === mx ? `Prix : ${mn} $` : `Prix : de ${mn} à ${mx} $`); }1974  lines.push('');1975  const rues = [...new Set(C.map(c => c.rue).filter(Boolean))];1976  if (rues.length === 1) lines.push(`📍 ${rues[0]} — ${anyImmediate ? 'Disponible dès maintenant' : (commonDispo ? `Disponible ${commonDispo}` : 'Disponible')}`);1977  const etages = [...new Set(U.map(u => u.etage).filter(Boolean))];1978  if (etages.length === 1) lines.push(`🏢 Situé au ${etages[0]}`);1979  else if (etages.length > 1) lines.push(`🏢 Plusieurs étages disponibles (${etages.join(', ')})`);1980  const types = [...new Set(U.map(u => u.type).filter(Boolean))];1981  if (types.length === 1) lines.push(`🏠 Type : ${types[0]}`);1982  else if (types.length > 1) lines.push(`🏠 Grandeurs disponibles : ${types.join(', ')}`);1983  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}`); };1984  rng(u => u.chambres, '🛏️', 'Chambre à coucher', 'Chambres à coucher');1985  rng(u => u.sdb, '🛁', 'Salle de bain', 'Salles de bain');1986  const sel = k => { const v = commonSel(k); return (v && v !== CARAC_SEL_DEF[k][0]) ? v : null; };1987  let v; v = sel('laveuse'); if (v) lines.push(`👕 ${v}`);1988  v = sel('rangement'); if (v) lines.push(`🗄️ ${v}`);1989  v = sel('clim'); if (v) lines.push(`🍃 ${v}`);1990  if (allBool('balcon')) lines.push('🔆 Espace balcon');1991  if (allBool('terrain')) lines.push('🌿 Accès au terrain');1992  v = sel('chauffage'); if (v) lines.push(`🔥 Chauffage ${v.toLowerCase()}`);1993  v = sel('eau'); if (v) lines.push('💧 Eau chaude incluse');1994  v = sel('meuble'); if (v) lines.push(`🛋️ ${v}`);1995  rng(u => u.stationnement, '🚗', 'Stationnement', 'Stationnements');1996  if (allBool('deneigement')) lines.push('❄️ Stationnement déneigé');1997  if (allBool('garage')) lines.push('🏠 Garage inclus');1998  if (allBool('plancher_chauffant')) lines.push('♨️ Plancher chauffant');1999  if (allBool('aspirateur_central')) lines.push('🧹 Aspirateur central');2000  if (allBool('electricite_inclus')) lines.push('💡 Électricité incluse');2001  if (allBool('internet_inclus')) lines.push('🌐 Internet inclus');2002  if (allBool('voisin_arriere')) lines.push('🌳 Sans voisin arrière');2003  if (allBool('non_fumeur')) lines.push('🚭 Logement non-fumeur');2004  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}`);2005  if (allBool('credit')) lines.push('📋 Enquête de crédit et références demandées');2006  const kx = e => (e.emoji || '✨') + '|' + (e.texte || '').trim();2007  const sets = C.map(c => new Set((c.extras || []).filter(e => e && e.texte && e.texte.trim()).map(kx)));2008  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('|')}`); }); }2009  return lines.join('\n');2010}20112012async function openDetail(idx) {2013  _closeChildOverlays();   // ardoise propre : jamais une fiche derrière un plan resté ouvert2014  currentListing = allListings[idx];2015  currentPhotoIdx = 0;2016  const l = currentListing;2017  const photos = await getFolderPhotos(l);2018  const loyer = parseFloat(l.loyer)||0;2019  const etageNorm = normalizeEtage(getEtage(l));2020  const g = document.getElementById('detail-gallery');2021  // Photos « meublé virtuellement » : emoji ✨ sur la galerie + bouton, si présentes.2022  const staged = Array.isArray(l.staged_photos) ? l.staged_photos.filter(Boolean) : [];2023  window.__stagedPhotos = staged;2024  const sparkHtml = staged.length ? `<button class="staged-spark" onclick="event.stopPropagation();openLightbox(window.__stagedPhotos,0)" title="✨ Voir l'appartement en aménagement virtuel">✨</button>` : '';2025  if (photos.length) {2026    window._currentPhotos = photos;2027    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('');2028    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>`:'';2029    let arrows = photos.length>1?`<button class="gallery-prev" onclick="prevPhoto()">‹</button><button class="gallery-next" onclick="nextPhoto()">›</button>`:'';2030    let count = photos.length>1?`<div class="gallery-count">1 / ${photos.length}</div>`:'';2031    g.innerHTML = `<button class="detail-close" onclick="closeDetail()">&times;</button>${sparkHtml}${imgs}${dots}${arrows}${count}`;2032  } else {2033    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>`;2034  }2035  document.getElementById('d-ville').textContent = '📍 '+(l.ville||'');2036  document.getElementById('d-title').textContent = l.titre||'';2037  document.getElementById('d-price').innerHTML = (hasVariants(l)?'<span class="price-from">dès </span>':'')+minLoyer(l).toLocaleString('fr-CA')+' $<span> / mois</span>';2038  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>`:''}`;2039  // Bouton « Meublé virtuellement » — visible seulement si des photos meublées existent.2040  const stagedBtn = document.getElementById('d-staged-btn');2041  if (stagedBtn){2042    if (staged.length){ stagedBtn.style.display = ''; stagedBtn.onclick = () => openLightbox(window.__stagedPhotos, 0); }2043    else { stagedBtn.style.display = 'none'; stagedBtn.onclick = null; }2044  }2045  renderDetailVariants(l);2046  renderAgentBuilding(l);2047  renderDetailPhotoModels(l);2048  // Description = texte écrit (proprio/admin) + résumé auto des logements, sans doublon.2049  // genAggregateAnnonce renvoie le résumé pour un immeuble avec caractéristiques,2050  // sinon il renvoie l.description (aucune donnée perdue).2051  (function(){2052    const typed = (l.description || '').trim();2053    const gen = (genAggregateAnnonce(l) || '').trim();2054    let out;2055    if (typed && gen && gen !== typed) {2056      // Évite le doublon quand la description écrite est (à peu de choses près) le même2057      // résumé que celui généré — souvent une version PÉRIMÉE. On compare ligne par ligne2058      // (sans emoji/casse/espaces) ; si ça se recoupe largement, on ne garde que le résumé à jour.2059      const normLine = s => s.replace(/^[^0-9A-Za-zÀ-ÿ]+/, '').replace(/\s+/g, ' ').trim().toLowerCase();2060      const typedSet = new Set(typed.split('\n').map(normLine).filter(Boolean));2061      const genLines = gen.split('\n').map(normLine).filter(Boolean);2062      const overlap = genLines.length ? genLines.filter(x => typedSet.has(x)).length / genLines.length : 0;2063      out = overlap >= 0.6 ? gen : (typed + '\n\n' + gen);2064    }2065    else out = typed || gen || '';2066    document.getElementById('d-desc').textContent = out;2067  })();20682069  // Section infos internes — visible seulement pour le staff connecté (admins + agents, données protégées par RLS)2070  window.__currentPk = l.pk;2071  const iw = document.getElementById('d-internal-wrap');2072  if (iw){2073    if (window.__isStaff){2074      iw.style.display = 'block';2075      const dint = document.getElementById('d-internal'); dint.innerHTML = '';2076      const parentPk = l._virtual ? l._parentPk : l.pk;2077      // Plus de bouton : les infos internes s'affichent directement pour le staff.2078      if (l._virtual && l._unitId){2079        // Carte d'appartement : infos 🏢 immeuble + 🔒 notes de CETTE unité2080        window.showApartmentInternal && window.showApartmentInternal(parentPk, l._unitId, l.pk);2081      } else {2082        // Immeuble (plan) → seulement le bloc 🏢 (les infos par logement sont par unité).2083        // Fiche appartement unique → toutes les infos internes de la fiche.2084        const isBuilding = l.fiche_type === 'immeuble' || (Array.isArray(l.units) && l.units.length > 0);2085        if (window.showInternalInfo) window.showInternalInfo(parentPk, { buildingOnly: isBuilding });2086      }2087    } else iw.style.display = 'none';2088  }20892090  // Contact : fiche PROPRIÉTAIRE → ses coordonnées + pas de formulaire de location.2091  //           fiche Gestion ISR   → coordonnées Gestion ISR + formulaire de location.2092  const btnCall = document.getElementById('btn-call'), btnEmail = document.getElementById('btn-email'), btnVis = document.getElementById('btn-visite-full');2093  // Mode lien propriétaire (?p=) : ses coordonnées (profil) sur TOUTES les fiches,2094  // y compris celles de Gestion ISR. Un contact propre à la fiche a priorité.2095  const oc = window.__ownerContact;2096  const ph = ((l.contact_phone||'').trim()) || (oc ? (oc.contact_phone||'').trim() : '');2097  const em = ((l.contact_email||'').trim()) || (oc ? (oc.contact_email||'').trim() : '');2098  if (oc || l.created_by_owner){2099    if (ph){ btnCall.href = 'tel:'+ph.replace(/[^\d+]/g,''); btnCall.style.display=''; } else { btnCall.style.display='none'; }2100    if (em){ btnEmail.href = 'mailto:'+em; btnEmail.style.display=''; } else { btnEmail.style.display='none'; }2101    btnVis.style.display = 'none';   // pas de formulaire de location2102  } else {2103    btnCall.href = 'tel:+18194721127'; btnCall.style.display='';2104    btnEmail.href = 'mailto:info@gestion-isr.com'; btnEmail.style.display='';2105    btnVis.style.display = ''; btnVis.onclick = ()=>openVisite(l);2106  }2107  // Mini-carte dans le modal2108  const detailMapEl = document.getElementById('detail-map');2109  detailMapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:13px">Chargement de la carte…</div>';2110  if (window._detailMap) {2111    window._detailMap.remove();2112    window._detailMap = null;2113  }2114  const adresse = l.adresse_geo || l.id || '';2115  if (adresse) {2116    setTimeout(async () => {2117      const gpsMatch = adresse.match(/^\s*(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*$/);2118      let coords;2119      if (gpsMatch) {2120        coords = {lat: parseFloat(gpsMatch[1]), lng: parseFloat(gpsMatch[2])};2121      } else {2122        coords = await geocode(adresse);2123      }2124      if (coords && typeof L !== 'undefined') {2125        window._detailMap = L.map('detail-map', {zoomControl:true, scrollWheelZoom:false}).setView([coords.lat, coords.lng], 15);2126        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {2127          attribution: '© OpenStreetMap',2128          maxZoom: 192129        }).addTo(window._detailMap);2130        L.marker([coords.lat, coords.lng]).addTo(window._detailMap);2131      } else {2132        detailMapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:13px">Localisation non disponible</div>';2133      }2134    }, 300);2135  } else {2136    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>';2137  }2138  document.getElementById('btn-email').href = 'mailto:info@gestion-isr.com?subject=' + encodeURIComponent('Demande d\'information — ' + (l.titre||''));2139  document.getElementById('detail-overlay').classList.add('open'); ovOpened(l.ID_app ? '#' + l.ID_app : null);2140  if (window.logListingView) window.logListingView(l.ID_app || l.id_app || '', l.titre || '');2141}21422143let lightboxPhotos = [], lightboxIdx = 0;21442145// ---- Modèles de photos (variantes) — vue publique ----2146// Photos d'un modèle ordonnées avec la photo principale (★) en tête.2147function 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(); }2148function pvClean(l){ return Array.isArray(l && l.photo_variants) ? l.photo_variants.filter(v => v && Array.isArray(v.photos) && v.photos.length).map(v => ({ ...v, photos: modelPhotos(v) })) : []; }2149// Précharge la 1re photo de chaque modèle (taille visionneuse) → passer d'un modèle2150// à l'autre devient instantané, plus de délai au clic.2151const _pvPreloaded = new Set();2152function _preloadModelCovers(pv){2153  (pv || []).forEach(v => {2154    if (!v || !v.photos || !v.photos.length) return;2155    const url = simg(v.photos[0], 1600, 78);2156    if (_pvPreloaded.has(url)) return;2157    _pvPreloaded.add(url);2158    const im = new Image(); im.src = url;2159  });2160}2161// Ouvre la visionneuse avec des boutons pour passer d'un modèle à l'autre2162function openModelsLightbox(models, mi){2163  const pv = (models || []).filter(v => v && Array.isArray(v.photos) && v.photos.length);2164  if (!pv.length) return;2165  mi = Math.max(0, Math.min(mi || 0, pv.length - 1));2166  window.__lbModels = pv;2167  const bar = document.getElementById('lightbox-models');2168  if (bar){2169    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('');2170    bar.classList.toggle('show', pv.length > 1);   // barre visible dès qu'il y a plus d'un modèle2171  }2172  openLightbox(pv[mi].photos, 0, true);2173  _preloadModelCovers(pv);   // les autres modèles seront prêts instantanément2174}2175window.switchLbModel = (i) => {2176  const pv = window.__lbModels || []; if (!pv[i]) return;2177  const bar = document.getElementById('lightbox-models');2178  if (bar) bar.querySelectorAll('.lb-mbtn').forEach((b, bi) => b.classList.toggle('active', bi === i));2179  lightboxPhotos = pv[i].photos; lightboxIdx = 0;2180  document.getElementById('lightbox-img').src = simg(lightboxPhotos[0], 1600, 78);2181  document.getElementById('lb-prev').style.display = lightboxPhotos.length > 1 ? 'flex' : 'none';2182  document.getElementById('lb-next').style.display = lightboxPhotos.length > 1 ? 'flex' : 'none';2183  document.getElementById('lightbox-count').textContent = lightboxPhotos.length > 1 ? ('1 / ' + lightboxPhotos.length) : '';2184  _lbPreload();2185};2186window.openVariantPhotos = (idx) => { const pv = pvClean(allListings[idx]); if (pv.length) openModelsLightbox(pv, 0); };2187window.openModelPhotos = (i) => { const pv = window.__detailPv || []; if (pv.length) openModelsLightbox(pv, i); };2188function renderDetailPhotoModels(l){2189  const sec = document.getElementById('d-photo-models-sec'), box = document.getElementById('d-photo-models');2190  if (!sec || !box) return;2191  const head = sec.querySelector('.d-models-head');2192  // Fiche d'un appartement (virtuelle) : la navigation se fait via le plan (renderAgentBuilding).2193  if (l._virtual){ sec.style.display = 'none'; return; }2194  if (head) head.innerHTML = `📷 Modèles d'appartement <span>— cliquez pour voir les photos de chaque modèle 👇</span>`;2195  const pv = pvClean(l); window.__detailPv = pv;2196  if (!pv.length){ sec.style.display = 'none'; return; }2197  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('');2198  sec.style.display = '';2199  _preloadModelCovers(pv);   // fiche ouverte : les modèles s'afficheront sans délai au clic2200}2201function _lbPreload(){2202  if (!lightboxPhotos || lightboxPhotos.length < 2) return;2203  [1, -1].forEach(d => { const j = (lightboxIdx + d + lightboxPhotos.length) % lightboxPhotos.length; const im = new Image(); im.src = simg(lightboxPhotos[j], 1600, 78); });2204}2205// Ouvre les photos « meublé virtuellement » d'un logement (depuis une carte de la liste).2206window.openStaged = function(idx){2207  const l = allListings[idx]; if (!l) return;2208  const s = Array.isArray(l.staged_photos) ? l.staged_photos.filter(Boolean) : [];2209  if (s.length){ window.__stagedPhotos = s; openLightbox(s, 0); }2210};2211function openLightbox(photos, idx, keepModels) {2212  if (!keepModels){ const bar = document.getElementById('lightbox-models'); if (bar){ bar.classList.remove('show'); bar.innerHTML = ''; } window.__lbModels = null; }2213  lightboxPhotos = photos;2214  lightboxIdx = idx;2215  document.getElementById('lightbox-img').src = simg(photos[idx], 1600, 78);2216  document.getElementById('lb-prev').style.display = photos.length > 1 ? 'flex' : 'none';2217  document.getElementById('lb-next').style.display = photos.length > 1 ? 'flex' : 'none';2218  document.getElementById('lightbox-count').textContent = photos.length > 1 ? (idx+1)+' / '+photos.length : '';2219  document.getElementById('lightbox-overlay').classList.add('open'); ovOpened();2220  _lbPreload();2221}22222223function closeLightbox() {2224  document.getElementById('lightbox-overlay').classList.remove('open');2225  ovClosedUI();2226}22272228function closeLightboxOutside(e) {2229  if (e.target === document.getElementById('lightbox-overlay')) closeLightbox();2230}22312232function lightboxNav(dir) {2233  lightboxIdx = (lightboxIdx + dir + lightboxPhotos.length) % lightboxPhotos.length;2234  document.getElementById('lightbox-img').src = simg(lightboxPhotos[lightboxIdx], 1600, 78);2235  document.getElementById('lightbox-count').textContent = (lightboxIdx+1)+' / '+lightboxPhotos.length;2236  _lbPreload();2237}22382239// Navigation clavier2240document.addEventListener('keydown', e => {2241  const lb = document.getElementById('lightbox-overlay');2242  if (lb.classList.contains('open')) {2243    if (e.key === 'ArrowRight') lightboxNav(1);2244    if (e.key === 'ArrowLeft') lightboxNav(-1);2245    if (e.key === 'Escape') closeLightbox();2246  }2247});22482249async function goPhoto(i){2250  const photos = await getFolderPhotos(currentListing);2251  currentPhotoIdx=i;2252  document.querySelectorAll('#detail-gallery img').forEach((img,idx)=>img.classList.toggle('active',idx===i));2253  document.querySelectorAll('.gallery-dot').forEach((d,idx)=>d.classList.toggle('active',idx===i));2254  const cnt=document.querySelector('.gallery-count');2255  if(cnt) cnt.textContent=(i+1)+' / '+photos.length;2256}2257async function nextPhoto(){const p=await getFolderPhotos(currentListing);goPhoto((currentPhotoIdx+1)%p.length);}2258async function prevPhoto(){const p=await getFolderPhotos(currentListing);goPhoto((currentPhotoIdx-1+p.length)%p.length);}2259function partagerFiche() {2260  const url = window.location.href;2261  navigator.clipboard.writeText(url).then(() => {2262    const btn = document.getElementById('btn-share');2263    btn.innerHTML = '✅ Lien copié !';2264    btn.style.background = '#FFECCF';2265    setTimeout(() => {2266      btn.innerHTML = '<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> Partager cette fiche';2267      btn.style.background = 'transparent';2268    }, 2500);2269  });2270}22712272function closeDetail(){2273  _closeChildOverlays();   // ferme aussi le plan/unité restés ouverts par-dessus la fiche2274  document.getElementById('detail-overlay').classList.remove('open');2275  ovClosedUI();2276}2277function closeDetailOutside(e){if(e.target===document.getElementById('detail-overlay'))closeDetail();}22782279// ---- Outil de partage (agent) : mêmes filtres que la vue admin « Sélections » ----2280let agentSelChecked = new Set();2281function agentSelCriteria(){2282  const chips = id => [...document.querySelectorAll('#'+id+' input:checked')].map(i=>i.value);2283  const b = (document.getElementById('asel-budget')||{}).value;2284  return { types: chips('asel-type'), budget: (b==null||b==='')?null:Number(b), etages: chips('asel-etage'), pets: chips('asel-animaux') };2285}2286// Filtre « dur » identique à l'admin (selPass) : un logement est exclu si un critère coché n'est pas rempli.2287function agentSelPass(l, c){2288  if (c.types.length){ const gs = hasVariants(l) ? distinct(l.variants.map(v=>v.grandeur||l.grandeur)) : [l.grandeur]; if(!gs.some(g=>c.types.includes(g))) return false; }2289  if (c.budget!=null && !isNaN(c.budget)){ const m=minLoyer(l); if(m==null || m>c.budget) return false; }2290  if (c.etages.length){ const es=listingEtagesNorm(l); if(!es.some(e=>c.etages.includes(e))) return false; }2291  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; }2292  return true;2293}2294function agentSelVisible(){2295  const c = agentSelCriteria(); const seen=new Set();2296  const q = ((document.getElementById('asel-search')||{}).value||'').toLowerCase().trim();2297  return (allListings||[]).filter(l=>{2298    const idapp=String(l.id_app||l.ID_app||''); if(!idapp||seen.has(idapp)) return false; seen.add(idapp);2299    if((l.etiquette||'').toLowerCase()==='loué') return false;   // partageable = disponible2300    if(!agentSelPass(l, c)) return false;2301    if(q){ const hay=((l.titre||'')+' '+(l.ville||'')+' '+grandeurDisplay(l)+' '+idapp).toLowerCase(); if(!hay.includes(q)) return false; }2302    return true;2303  });2304}2305let _aselSearchT=null;2306window.agentSelSearchInput = function(){ if(_aselSearchT) clearTimeout(_aselSearchT); _aselSearchT=setTimeout(()=>{ _aselSearchT=null; agentSelRender(); }, 180); };2307window.openAgentSel = function(){ const ov=document.getElementById('agent-sel-overlay'); if(!ov)return; ov.style.display='flex'; document.body.style.overflow='hidden'; agentSelRender(); };2308window.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=''; };2309window.agentSelRender = function(){2310  const list=document.getElementById('agent-sel-list'); if(!list)return;2311  const vis = agentSelVisible();2312  const cnt=document.getElementById('agent-sel-count');2313  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`;2314  list.innerHTML = vis.length ? vis.map(l=>{2315    const idapp=String(l.id_app||l.ID_app||''); const on=agentSelChecked.has(idapp); const mn=minLoyer(l);2316    const photo = l.main_photo || (Array.isArray(l.photos)&&l.photos.length?l.photos[0]:'');2317    const thumb = photo ? `<img class="selthumb" loading="lazy" decoding="async" src="${esc(photo)}" alt="">` : '<div class="selthumb"></div>';2318    const meta=[grandeurDisplay(l), l.ville, (mn?((hasVariants(l)||hasUnits(l))?'dès ':'')+Number(mn).toLocaleString('fr-CA')+' $':'')].filter(Boolean).join(' · ');2319    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>`;2320  }).join('') : '<div style="color:#888;text-align:center;padding:2rem">Aucun logement ne correspond aux filtres.</div>';2321  agentSelUpdateShare();2322};2323window.agentSelToggle = function(cb){2324  const id=cb.getAttribute('data-agentsel');2325  if(cb.checked) agentSelChecked.add(id); else agentSelChecked.delete(id);2326  const row=cb.closest('.sel-row'); if(row) row.classList.toggle('off', !cb.checked);2327  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>`);2328  agentSelUpdateShare();2329};2330window.agentSelAll = function(){ agentSelVisible().forEach(l=>{ const id=String(l.id_app||l.ID_app||''); if(id) agentSelChecked.add(id); }); agentSelRender(); };2331window.agentSelNone = function(){ agentSelChecked.clear(); agentSelRender(); };2332window.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(); };2333window.agentSelUpdateShare = function(){2334  const box=document.getElementById('agent-sel-share'); if(!box)return;2335  const ids=[...agentSelChecked];2336  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; }2337  const link=location.origin+'/?apps='+ids.map(encodeURIComponent).join(',');2338  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>2339    <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">2340      <input class="lnk" id="agent-sel-link" readonly value="${esc(link)}" style="flex:1;min-width:220px">2341      <button class="sel-btn primary" onclick="window.agentSelCopy&&window.agentSelCopy(this)">Copier</button>2342      <button class="sel-btn" onclick="var i=document.getElementById('agent-sel-link');if(i)window.open(i.value,'_blank')">Aperçu</button>2343    </div>2344    <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>`;2345};2346window.agentSelCopy = function(btn){2347  const inp=document.getElementById('agent-sel-link'); if(!inp)return; inp.select();2348  const fb=()=>{ if(btn){ const t=btn.textContent; btn.textContent='Copié ✓'; setTimeout(()=>{ btn.textContent=t; },1500); } };2349  if(navigator.clipboard){ navigator.clipboard.writeText(inp.value).then(fb).catch(()=>{ try{document.execCommand('copy');}catch(e){} fb(); }); }2350  else { try{document.execCommand('copy');}catch(e){} fb(); }2351};23522353let visiteListing = null;2354function openVisite(l){2355  visiteListing = (l && typeof l === 'object') ? l : null;2356  const titre = visiteListing ? (visiteListing.titre || '') : (typeof l === 'string' ? l : '');2357  document.getElementById('modal-apt-name').textContent = titre ? ('Logement : ' + titre) : '';2358  const f = document.getElementById('visite-form'); if (f){ f.reset(); f.style.display = ''; }2359  const sc = document.getElementById('visite-success'); if (sc) sc.style.display = 'none';2360  const sb = document.getElementById('vf-submit'); if (sb){ sb.disabled = false; sb.textContent = 'Envoyer ma demande'; }2361  const m = document.getElementById('visite-msg'); if (m){ m.textContent = ''; m.className = 'vf-msg'; }2362  document.getElementById('visite-overlay').classList.add('open'); ovOpened();2363}2364function openVisiteFromCard(titre){ openVisite(titre); }2365function openVisiteFromCardIdx(idx){ openVisite(allListings[idx] || ''); }2366document.getElementById('visite-form').addEventListener('submit', async e => {2367  e.preventDefault();2368  const g = id => (document.getElementById(id).value || '').trim();2369  const btn = document.getElementById('vf-submit'), msg = document.getElementById('visite-msg');2370  btn.disabled = true; btn.textContent = 'Envoi…';2371  const lead = {2372    nom: g('vf-nom'), courriel: g('vf-courriel'), telephone: g('vf-tel'),2373    logement: visiteListing ? (visiteListing.titre || null) : null,2374    logement_ref: visiteListing ? (visiteListing.ID_app || visiteListing.id_app || null) : null,2375    date_emmenagement: g('vf-date') || null, type_appartement: g('vf-type') || null,2376    budget: g('vf-budget') ? parseInt(g('vf-budget')) : null,2377    animaux: g('vf-animaux') || null,2378    enquete_credit: g('vf-credit') || null, message: g('vf-message') || null2379  };2380  try {2381    const { error } = await _sb.from('leads').insert(lead);2382    if (error) throw error;2383    document.getElementById('visite-form').reset();2384    // Écran de succès plein : impossible à manquer, peu importe le défilement.2385    document.getElementById('visite-form').style.display = 'none';2386    document.getElementById('visite-success').style.display = 'block';2387  } catch(err){2388    msg.className = 'vf-msg err'; msg.textContent = 'Erreur : ' + (err.message || 'envoi impossible');2389    try { msg.scrollIntoView({ block:'center', behavior:'smooth' }); } catch(_){}2390    btn.disabled = false; btn.textContent = 'Envoyer ma demande';2391  }2392});2393function closeVisite(){document.getElementById('visite-overlay').classList.remove('open');ovClosedUI();}2394function closeVisiteOutside(e){if(e.target===document.getElementById('visite-overlay'))closeVisite();}23952396loadListings().then(() => {2397  let hash = window.location.hash.replace('#', '');2398  // Décodage : un id_app éclaté contient « ½ » → dans l'URL il devient %C2%BD.2399  // Sans décoder, la comparaison échouerait et la fiche ne s'ouvrirait pas.2400  try { hash = decodeURIComponent(hash); } catch(e) {}2401  if (hash) {2402    const idx = allListings.findIndex(l => normIdApp(l.ID_app || l.id_app) === normIdApp(hash));2403    if (idx !== -1) openDetail(idx);2404  }2405});24062407// ===== CARTE =====2408let map = null, markers = [], geocodeCache = {};2409let mapVisible = false;24102411function toggleMap() {2412  mapVisible = !mapVisible;2413  const section = document.getElementById('map-section');2414  const btn = document.getElementById('map-toggle-btn');2415  section.style.display = mapVisible ? 'block' : 'none';2416  btn.classList.toggle('active', mapVisible);2417  btn.innerHTML = mapVisible2418    ? `<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`2419    : `<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`;2420  if (mapVisible) {2421    if (!map) initMap();2422    else updateMapMarkers();2423  }2424}24252426function initMap() {2427  if (typeof L === 'undefined') {2428    console.error('Leaflet non chargé');2429    return;2430  }2431  map = L.map('map').setView([45.8797, -72.4939], 11);2432  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {2433    attribution: '© OpenStreetMap contributors',2434    maxZoom: 192435  }).addTo(map);2436  updateMapMarkers();24372438  // Légende2439  const legend = L.control({position: 'bottomright'});2440  legend.onAdd = function() {2441    const div = L.DomUtil.create('div');2442    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';2443    div.innerHTML = `2444      <div style="font-weight:600;margin-bottom:4px;color:#444">Grandeur</div>2445      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#3B82F6;margin-right:6px"></span>3½</div>2446      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#FB8C00;margin-right:6px"></span>4½</div>2447      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#F97316;margin-right:6px"></span>5½</div>2448      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#8B5CF6;margin-right:6px"></span>6½</div>2449      <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#EF4444;margin-right:6px"></span>Maison</div>2450    `;2451    return div;2452  };2453  legend.addTo(map);2454}24552456async function geocode(address) {2457  if (geocodeCache[address]) return geocodeCache[address];2458  try {2459    const q = encodeURIComponent(address + ', Québec, Canada');2460    const url = `https://nominatim.openstreetmap.org/search?format=json&q=${q}&limit=1`;2461    const resp = await fetch(url, {headers:{'Accept-Language':'fr'}});2462    const data = await resp.json();2463    if (data.length > 0) {2464      const coords = {lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon)};2465      geocodeCache[address] = coords;2466      return coords;2467    }2468  } catch(e) {}2469  return null;2470}24712472async function updateMapMarkers() {2473  if (!map) return;2474  markers.forEach(m => map.removeLayer(m));2475  markers = [];24762477  const grandeurColors = {2478    '3½': '#3B82F6',2479    '4½': '#FB8C00',2480    '5½': '#F97316',2481    '6½': '#8B5CF6',2482    'Maison': '#EF4444'2483  };24842485  // Géocoder uniquement les logements filtrés2486  const promises = filtered.map(async (l, idx) => {2487    const address = l.adresse_geo || l.id || '';2488    if (!address) return;2489    // Détecter si c'est des coordonnées GPS (ex: "45.8797, -72.4939")2490    const gpsMatch = address.match(/^\s*(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*$/);2491    let coords;2492    if (gpsMatch) {2493      coords = {lat: parseFloat(gpsMatch[1]), lng: parseFloat(gpsMatch[2])};2494    } else {2495      coords = await geocode(address);2496    }2497    if (!coords) return;2498    const loyer = parseFloat(l.loyer)||0;2499    const color = grandeurColors[l.grandeur] || '#FB8C00';2500    const markerIcon = L.divIcon({2501      className: '',2502      html: `<div style="background:${color};color:#fff;border-radius:50%;width:32px;height:32px;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(l.grandeur||'?')}</div>`,2503      iconSize: [32, 32],2504      iconAnchor: [16, 16],2505      popupAnchor: [0, -18]2506    });2507    const marker = L.marker([coords.lat, coords.lng], {icon: markerIcon})2508      .bindPopup(`2509        <div class="popup-title">${esc(l.titre)}</div>2510        <div class="popup-price">${hasVariants(l)?'dès ':''}${minLoyer(l).toLocaleString('fr-CA')} $ / mois</div>2511        <div class="popup-dispo">${esc(dispoDisplay(l))}</div>2512        <button class="popup-btn" onclick="document.getElementById('map-section').style.display='none';document.getElementById('map-toggle-btn').classList.remove('active');mapVisible=false;openDetail(${allListings.indexOf(l)})">Voir la fiche</button>2513      `);2514    marker.addTo(map);2515    markers.push(marker);2516  });25172518  await Promise.all(promises);25192520  // Ajuster la vue sur les marqueurs2521  if (markers.length > 0) {2522    const group = L.featureGroup(markers);2523    map.fitBounds(group.getBounds().pad(0.1));2524  }2525}25262527// Mettre à jour la carte quand les filtres changent2528const origRenderTags = renderTags;2529renderTags = function() {2530  origRenderTags();2531  if (mapVisible && map) updateMapMarkers();2532};2533</script>25342535<!-- Journalisation des vues d'appartement (visiteur connecté) -->2536<script type="module">2537import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm';2538const _sb = createClient('https://jfgkriojazaohgjezijj.supabase.co', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpmZ2tyaW9qYXphb2hnamV6aWpqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODEzNDE5NjQsImV4cCI6MjA5NjkxNzk2NH0.VVLfhvpkQ9WgJOhCYW3y6kRZIJbAQXTCANv6mXce1Yw');2539// Exposé en global : le 1er <script> (classique) référence `_sb` (formulaire de location,2540// note interne par unité). Sans ça, `_sb` reste confiné au module → « _sb is not defined ».2541window._sb = _sb;2542let _lastView = null;2543window.logListingView = async (id_app, titre) => {2544  try {2545    if (id_app && id_app === _lastView) return;2546    _lastView = id_app;2547    const { data: { session } } = await _sb.auth.getSession();2548    if (!session) return;2549    await _sb.from('events').insert({ user_id: session.user.id, type: 'listing_view', detail: { id_app: id_app || null, titre: titre || null } });2550  } catch(e) {}2551};25522553// ---- Favoris ----2554window.__favs = new Set();2555async function _loadFavs(){2556  try {2557    const { data: { session } } = await _sb.auth.getSession();2558    if (!session) return;2559    const { data } = await _sb.from('favorites').select('id_app').eq('user_id', session.user.id);2560    window.__favs = new Set((data || []).map(f => f.id_app));2561    if (window.refreshHearts) window.refreshHearts();2562  } catch(e) {}2563}2564window.toggleFavorite = async (id_app, btn) => {2565  if (!id_app) return;2566  const { data: { session } } = await _sb.auth.getSession();2567  if (!session){ if (confirm('Connecte-toi pour enregistrer tes favoris. Aller à la page profil ?')) location.href = '/compte.html'; return; }2568  try {2569    if (window.__favs.has(id_app)){2570      await _sb.from('favorites').delete().eq('user_id', session.user.id).eq('id_app', id_app);2571      window.__favs.delete(id_app);2572    } else {2573      await _sb.from('favorites').insert({ user_id: session.user.id, id_app });2574      window.__favs.add(id_app);2575    }2576    if (btn) btn.classList.toggle('active', window.__favs.has(id_app));2577  } catch(e) {}2578};2579_loadFavs();25802581// ---- Infos internes (admins + agents de location — RLS protège les données) ----2582window.__isStaff = false;2583(async () => {2584  try {2585    const { data: { session } } = await _sb.auth.getSession();2586    if (!session) return;2587    const { data } = await _sb.rpc('is_staff');2588    window.__isStaff = data === true;2589    if (window.__isStaff){ const sb = document.getElementById('staff-badge'); if (sb) sb.style.display = 'inline-flex'; const ast = document.getElementById('agent-share-top'); if (ast) ast.style.display = 'inline-flex'; if (typeof renderAgentNewRail === 'function') renderAgentNewRail(); }2590    // Si une fiche est déjà ouverte au moment où l'on apprend qu'on est staff, on révèle le bouton2591    const iw = document.getElementById('d-internal-wrap');2592    if (window.__isStaff && iw && document.getElementById('detail-overlay').classList.contains('open') && window.__currentPk){2593      iw.style.display = 'block';2594      const b = document.getElementById('d-internal-btn'); if (b){ b.style.display=''; b.onclick = () => window.showInternalInfo(window.__currentPk); }2595      if (typeof currentListing !== 'undefined' && currentListing) renderAgentBuilding(currentListing);   // révèle le visuel immeuble2596    }2597  } catch(e) {}2598})();2599const INT_VIEW = [2600  ['🏢 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']]],2601  ['🏠 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']]],2602  ['Locataire en place', [['locataire_nom','Nom'],['locataire_tel','Téléphone'],['locataire_courriel','Courriel']]],2603  ['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é']]],2604  ['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']]]2605];2606const _intE2 = s => String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');2607const _intLbl = t => `<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:#999;margin-bottom:2px">${t}</div>`;2608const _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>`;2609const _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>`;2610// Rend les groupes de champs internes (INT_VIEW) présents dans la ligne listings_internal.2611function _intGroupsHtml(d, groups){2612  let html = '', any = false;2613  (groups || []).forEach(([title, fields]) => {2614    const rows = fields.filter(([k]) => d[k]).map(([k, label]) => `<div style="margin-bottom:10px">${_intLbl(_intE2(label))}${_intE2(d[k])}</div>`).join('');2615    if (rows){ any = true; html += _intSec(_intE2(title)) + rows; }2616  });2617  return { html, any };2618}2619// Immeuble (plan) ou fiche appartement unique. buildingOnly=true → seulement le bloc 🏢 Immeuble.2620window.showInternalInfo = async (pk, opts) => {2621  opts = opts || {};2622  const box = document.getElementById('d-internal'); if (!box) return;2623  const btn = document.getElementById('d-internal-btn'); if (btn) btn.style.display = 'none';2624  box.innerHTML = '<div style="color:#888;font-size:13px">Chargement…</div>';2625  try {2626    const { data, error } = await _sb.from('listings_internal').select('*').eq('listing_pk', pk).maybeSingle();2627    if (error) throw error;2628    const d = data || {};2629    const r = _intGroupsHtml(d, opts.buildingOnly ? [INT_VIEW[0]] : INT_VIEW);2630    let html = r.html, any = r.any;2631    if (!opts.buildingOnly){2632      const vn = d.variant_notes || {};2633      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('');2634      if (vnRows){ any = true; html += _intSec('Notes par étage') + vnRows; }2635    }2636    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>`);2637  } catch(err) { box.innerHTML = '<div style="color:#b91c1c;font-size:13px">Impossible de charger les infos internes.</div>'; }2638};2639// Carte d'un appartement (virtuelle) : infos 🏢 IMMEUBLE + 🔒 notes de CETTE unité.2640window.showApartmentInternal = async (pk, unitId, viewPk) => {2641  const box = document.getElementById('d-internal'); if (!box) return;2642  box.innerHTML = '<div style="color:#888;font-size:13px">Chargement…</div>';2643  try {2644    const { data } = await _sb.from('listings_internal').select('*').eq('listing_pk', pk).maybeSingle();2645    if (!document.getElementById('detail-overlay').classList.contains('open') || (viewPk != null && window.__currentPk !== viewPk)) return;2646    const d = data || {};2647    const bldg = _intGroupsHtml(d, [INT_VIEW[0]]);2648    const note = d.unit_notes && d.unit_notes[unitId];2649    const unitHtml = window.renderUnitIntNotes ? window.renderUnitIntNotes(note) : '';2650    box.innerHTML = (bldg.any ? _intBox(bldg.html) : '') + `<div class="up-sec" style="color:#8a5a00">🔒 Notes internes (cet appartement)</div>${unitHtml}`;2651  } catch(e){ box.innerHTML = ''; }2652};26532654// ============ Widget de clavardage (visiteur <-> équipe) ============2655(function(){2656  const $c = id => document.getElementById(id);2657  const bubble = $c('chatw-bubble'), panel = $c('chatw-panel'), body = $c('chatw-body');2658  if (!bubble) return;2659  let cwOpen = false, cwUid = null, cwSub = null, cwUnread = 0, cwLastTs = null, cwPollTimer = null, cwNote = '';2660  const cwShown = new Set();2661  const cwEsc = s => String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');2662  const cwFmt = t => new Date(t).toLocaleString('fr-CA',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});2663  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'; }2664  bubble.addEventListener('click', () => cwOpen ? cwClose() : cwOpenPanel());2665  $c('chatw-close').addEventListener('click', cwClose);2666  function cwOpenPanel(){ cwOpen = true; panel.style.display = 'flex'; cwRoute(); }2667  function cwClose(){ cwOpen = false; panel.style.display = 'none'; }2668  async function cwRoute(){ const { data:{ session } } = await _sb.auth.getSession(); if (session){ cwUid = session.user.id; cwShowChat(); } else cwShowStart(); }2669  function cwShowStart(){2670    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>2671      <div class="chatw-field"><label>Prénom</label><input id="cw-prenom" autocomplete="given-name"></div>2672      <div class="chatw-field"><label>Courriel</label><input id="cw-courriel" type="email" autocomplete="email"></div>2673      <div class="chatw-err" id="cw-err"></div>2674      <button class="chatw-btn primary" id="cw-start">Accéder au chat</button></div>`;2675    $c('cw-start').onclick = cwStart;2676  }2677  function cwErr(m){ const e = $c('cw-err'); if (e){ e.textContent = m; e.style.display = 'block'; } }2678  async function cwStart(){2679    const prenom = ($c('cw-prenom').value||'').trim(), courriel = ($c('cw-courriel').value||'').trim();2680    if (!prenom || !courriel){ cwErr("Merci d'indiquer ton prénom et ton courriel."); return; }2681    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(courriel)){ cwErr('Adresse courriel invalide.'); return; }2682    $c('cw-start').disabled = true;2683    const { data, error } = await _sb.auth.signInAnonymously();2684    if (error){ $c('cw-start').disabled = false; cwErr('Erreur : ' + error.message); return; }2685    cwUid = data.user.id;2686    await _sb.from('profiles').upsert({ user_id: cwUid, nom: prenom, courriel, source: 'chat' });2687    try {2688      const { error: ue } = await _sb.auth.updateUser({ email: courriel, data: { full_name: prenom } }, { emailRedirectTo: location.origin + '/compte.html?activation=1' });2689      cwNote = ue ? '' : "📧 Un courriel d'activation t'a été envoyé — clique le lien pour choisir ton mot de passe et finaliser ton compte.";2690    } catch(_){ cwNote = ''; }2691    cwShowChat();2692  }2693  async function cwShowChat(){2694    try { const { data:{ session } } = await _sb.auth.getSession(); if (session && _sb.realtime && _sb.realtime.setAuth) await _sb.realtime.setAuth(session.access_token); } catch(_){}2695    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>`;2696    cwShown.clear(); cwLastTs = null;2697    const { data } = await _sb.from('messages').select('*').eq('user_id', cwUid).order('created_at');2698    const box = $c('chatw-msgs');2699    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>'; }2700    else data.forEach(cwAppend);2701    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>'); }2702    cwScroll();2703    _sb.rpc('mark_my_messages_read');2704    cwBadge(0); cwEnsurePoll();2705    $c('chatw-form').addEventListener('submit', cwSend);2706    if (!cwSub){2707      cwSub = _sb.channel('cw-' + cwUid).on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: 'user_id=eq.' + cwUid }, p => {2708        if (p.new.sender === 'admin'){2709          if (cwOpen && $c('chatw-msgs')){ cwAppend(p.new); cwScroll(); _sb.rpc('mark_my_messages_read'); }2710          else cwBadge(cwUnread + 1);2711        }2712      }).subscribe();2713    }2714  }2715  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); }2716  function cwEnsurePoll(){ if (!cwPollTimer) cwPollTimer = setInterval(cwPoll, 4000); }2717  async function cwPoll(){2718    if (!cwUid) return;2719    if (!cwOpen || !$c('chatw-msgs')){2720      const { count } = await _sb.from('messages').select('id', { count: 'exact', head: true }).eq('user_id', cwUid).eq('sender', 'admin').eq('read', false);2721      cwBadge(count || 0); return;2722    }2723    let q = _sb.from('messages').select('*').eq('user_id', cwUid).order('created_at');2724    if (cwLastTs) q = q.gt('created_at', cwLastTs);2725    const { data } = await q;2726    if (data && data.length){ data.forEach(cwAppend); cwScroll(); _sb.rpc('mark_my_messages_read'); }2727  }2728  function cwScroll(){ const box = $c('chatw-msgs'); if (box) box.scrollTop = box.scrollHeight; }2729  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(); } }2730  (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); } })();2731})();2732</script>27332734<!-- Widget de clavardage (Gestion ISR) -->2735<style>2736#chatw{position:fixed;right:18px;bottom:18px;z-index:3000;font-family:system-ui,-apple-system,sans-serif}2737#chatw-bubble{width:58px;height:58px;border-radius:50%;border:none;background:#FB8C00;color:#fff;font-size:24px;cursor:pointer;box-shadow:0 6px 20px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center;position:relative}2738#chatw-bubble:hover{background:#E67700}2739#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}2740#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}2741.chatw-head{background:#FB8C00;color:#fff;padding:14px 16px;display:flex;justify-content:space-between;align-items:center}2742.chatw-head b{font-size:15px}2743.chatw-sub{font-size:12px;opacity:.85}2744.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}2745#chatw-body{flex:1;display:flex;flex-direction:column;min-height:0}2746.chatw-pad{padding:18px 16px;overflow-y:auto}2747.chatw-intro{font-size:14px;color:#555;line-height:1.5;margin:0 0 16px}2748.chatw-btn{display:block;width:100%;padding:12px;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;border:none;margin-bottom:10px}2749.chatw-btn.primary{background:#FB8C00;color:#fff}2750.chatw-btn.primary:hover{background:#E67700}2751.chatw-btn.ghost{background:#fff;border:1px solid #ddd;color:#444}2752.chatw-btn.ghost:hover{border-color:#FB8C00;color:#E67700}2753.chatw-field{margin-bottom:10px}2754.chatw-field label{display:block;font-size:12px;font-weight:600;color:#555;margin-bottom:4px}2755.chatw-field input{width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid #d8d8d4;border-radius:9px;font-size:14px;outline:none}2756.chatw-field input:focus{border-color:#FB8C00}2757.chatw-back{background:none;border:none;color:#888;font-size:13px;cursor:pointer;padding:0;margin-bottom:12px}2758.chatw-err{font-size:13px;color:#B91C1C;margin:4px 0 8px;display:none}2759#chatw-msgs{flex:1;overflow-y:auto;padding:14px;background:#faf9f7;display:flex;flex-direction:column;gap:8px}2760.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}2761.cw-msg.visitor{align-self:flex-end;background:#FB8C00;color:#fff;border-bottom-right-radius:4px}2762.cw-msg.admin{align-self:flex-start;background:#fff;border:1px solid #e8e8e5;color:#1a1a1a;border-bottom-left-radius:4px}2763.cw-tm{display:block;font-size:10px;opacity:.7;margin-top:2px}2764#chatw-form{display:flex;gap:8px;padding:10px;border-top:1px solid #f0f0ee;background:#fff}2765#chatw-form input{flex:1;padding:10px 12px;border:1px solid #ddd;border-radius:9px;font-size:14px;outline:none}2766#chatw-form button{background:#FB8C00;color:#fff;border:none;border-radius:9px;padding:0 16px;font-weight:700;cursor:pointer;font-size:16px}2767@media(max-width:480px){#chatw-panel{width:calc(100vw - 24px)}}2768</style>2769<div id="chatw">2770  <button id="chatw-bubble" title="Clavardage" aria-label="Ouvrir le clavardage">💬<span id="chatw-badge"></span></button>2771  <div id="chatw-panel" style="display:none">2772    <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>2773    <div id="chatw-body"></div>2774  </div>2775</div>2776</body>2777</html>2778