{"slug":"public-map","headLinks":["https://unpkg.com/leaflet@1.9.4/dist/leaflet.css","https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Inter:wght@400;500;600;700&display=swap"],"styles":"\n  :root{\n    --ink:#1b2430; --ink-soft:#4a5568; --paper:#f6f4ef; --panel:#ffffff;\n    --gold:#b8925a; --gold-deep:#8f6f42; --line:#e4ddd0;\n    --available:#3f8f6b; --sold:#b5484c; --construction:#c98a2c; --upcoming:#3b6ea5;\n    --shadow:0 10px 30px rgba(27,36,48,0.14);\n  }\n  *{box-sizing:border-box;}\n  body,html{margin:0;padding:0;height:100%;width:100%;font-family:'Inter',sans-serif;background:var(--paper);color:var(--ink);}\n  /* Blocks casual text-selection/copying of page content. Inputs stay\n     selectable so visitors can still type and edit form fields normally. */\n  body{ -webkit-user-select:none; user-select:none; }\n  input, textarea{ -webkit-user-select:text; user-select:text; }\n  img{ -webkit-user-drag:none; user-drag:none; pointer-events:auto; }\n  #map{position:absolute;inset:0;z-index:0;}\n  #map.night-mode .leaflet-tile-pane{\n    filter: invert(100%) hue-rotate(180deg) brightness(96%) contrast(88%);\n  }\n\n  #topbar{\n    position:fixed;top:0;left:0;right:0;z-index:1200;\n    display:flex;align-items:center;justify-content:space-between;\n    padding:12px 26px;\n    background:transparent;\n  }\n  #topbar .brand{\n    display:flex;flex-direction:column;align-items:center;gap:4px;color:var(--ink);\n    background:rgba(255,255,255,0.8);backdrop-filter:blur(6px);\n    padding:10px 18px;border-radius:12px;\n    box-shadow:0 2px 10px rgba(27,36,48,0.12);\n    transition:padding .25s ease, width .25s ease, height .25s ease, border-radius .25s ease;\n    cursor:pointer;\n  }\n  /* Auto-collapses to just the icon after 30s (see collapseBrand() in the\n     main script) — shrinks to a small round badge instead of the full\n     logo+title panel. Hovering (desktop) or tapping (touch) it brings the\n     full panel back. */\n  #topbar .brand.collapsed{\n    width:44px;height:44px;padding:8px;border-radius:50%;\n    justify-content:center;\n  }\n  #topbar .brand.collapsed .brand-text{display:none;}\n  #topbar .brand.collapsed .brand-logos{gap:0;}\n  #topbar .brand.collapsed .brand-by,\n  #topbar .brand.collapsed .logo-secondary{display:none;}\n  #topbar .brand.collapsed img.logo{height:26px;max-width:26px;}\n  .brand-logos{display:flex;align-items:center;gap:8px;}\n  /* Brand stack: logo panel on top, intro video panel directly below it\n     (not beside it) — video panel hidden until the \"Intro video\" script\n     shows it. */\n  #brand-stack{display:flex;flex-direction:column;align-items:center;gap:8px;}\n  #intro-video-panel{\n    display:none;\n    background:rgba(255,255,255,0.8);backdrop-filter:blur(6px);\n    border-radius:12px;box-shadow:0 2px 10px rgba(27,36,48,0.12);\n    padding:6px;\n  }\n  #intro-video-panel.show{display:block;}\n  #brand-video-wrap{\n    width:220px;height:124px;border-radius:8px;overflow:hidden;\n    background:#000;flex:none;position:relative;\n  }\n  #brand-video-frame{position:absolute;inset:0;width:100%;height:100%;}\n  #brand-video-frame iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;}\n  @media (max-width:640px){\n    #brand-video-wrap{width:150px;height:84px;}\n  }\n  .brand-by{font-size:11px;font-style:italic;color:var(--ink-soft);flex:none;}\n  #topbar .brand img.logo{height:38px;width:auto;max-width:130px;object-fit:contain;display:block;transition:height .25s ease,max-width .25s ease;}\n  #topbar .brand img.logo-secondary{height:28px;max-width:90px;}\n  #topbar .brand .brand-text{display:flex;flex-direction:column;align-items:center;text-align:center;}\n  #topbar .brand h1{font-family:'Playfair Display',serif;font-size:19px;margin:0;letter-spacing:.3px;color:var(--ink);}\n  #topbar .brand span{font-size:10.5px;color:var(--ink-soft);letter-spacing:1px;text-transform:uppercase;margin-top:1px;}\n  #toolbar{display:flex;gap:8px;align-items:center;position:relative;}\n  #search-input{\n    width:220px;padding:10px 16px;border-radius:999px;\n    border:1px solid #e6dfd0;background:#ffffff;\n    font-size:13px;color:var(--ink);font-family:'Inter',sans-serif;\n    box-shadow:0 1px 4px rgba(27,36,48,0.1);\n    text-overflow:ellipsis;\n  }\n  #search-input::placeholder{color:#8a8577;}\n  #search-input:focus{outline:2px solid var(--gold);}\n  .btn{\n    border:1px solid #e6dfd0;background:#ffffff;color:#1b2430;\n    padding:10px 16px;border-radius:999px;font-size:13px;font-weight:600;cursor:pointer;\n    display:flex;align-items:center;gap:6px;transition:.2s ease;white-space:nowrap;\n    text-decoration:none;\n    box-shadow:0 1px 4px rgba(27,36,48,0.1);\n  }\n  .btn:hover{background:var(--gold);color:#241a0d;border-color:var(--gold);transform:translateY(-1px);box-shadow:0 5px 12px rgba(184,146,90,0.4);}\n  .btn.on{background:var(--ink);color:#fff;border-color:var(--ink);}\n  .btn.on:hover{background:#000;color:#fff;border-color:#000;}\n  .btn:focus-visible{outline:3px solid var(--gold);outline-offset:2px;}\n\n  #area-toast{\n    position:fixed;top:86px;left:50%;transform:translateX(-50%) translateY(-8px);\n    z-index:1300;background:var(--ink);color:#fff;padding:9px 20px;border-radius:999px;\n    font-size:13px;font-weight:600;letter-spacing:.2px;box-shadow:0 8px 24px rgba(27,36,48,0.3);\n    opacity:0;pointer-events:none;transition:opacity .25s ease, transform .25s ease;\n    white-space:nowrap;\n  }\n  #area-toast.show{opacity:1;transform:translateX(-50%) translateY(0);}\n\n  /* TV / kiosk mode: append ?tv=1 to the URL. Makes the recenter button\n     large and impossible to miss on a big screen with a remote/D-pad. */\n  body.tv-mode #btn-near-me{\n    padding:16px 26px;font-size:16px;border-width:2px;border-color:var(--gold);\n    box-shadow:0 6px 20px rgba(184,146,90,0.45);\n  }\n  body.tv-mode #topbar{padding:20px 30px;}\n  body.tv-mode #search-input{font-size:15px;padding:12px 18px;}\n\n  #legend{\n    position:absolute;bottom:20px;left:20px;z-index:1200;background:var(--panel);\n    border-radius:10px;box-shadow:var(--shadow);padding:12px 16px;font-size:12px;max-width:210px;\n  }\n  #legend h4{margin:0 0 8px;font-family:'Playfair Display',serif;font-size:13px;color:var(--ink);display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;}\n  #legend.collapsed h4{margin-bottom:0;}\n  #legend.collapsed #legend-content{display:none;}\n  #legend-arrow{font-size:10px;color:var(--ink-soft);transition:transform .2s ease;}\n  #legend.collapsed #legend-arrow{transform:rotate(-90deg);}\n  #legend .row{display:flex;align-items:center;gap:8px;margin-bottom:5px;color:var(--ink-soft);}\n  #legend .swatch{width:11px;height:11px;border-radius:50%;flex:none;}\n\n  /* Sticky WhatsApp / Call buttons — fixed to the viewport, always\n     reachable while browsing the map. Sits above Leaflet's bottom-right\n     zoom control so the two never overlap. */\n  #sticky-contact{\n    position:fixed;right:20px;bottom:104px;z-index:1250;\n    display:flex;flex-direction:column;gap:12px;\n  }\n  #sticky-contact a{\n    width:52px;height:52px;border-radius:50%;flex:none;\n    display:flex;align-items:center;justify-content:center;\n    box-shadow:0 6px 18px rgba(27,36,48,0.3);color:#fff;\n    transition:transform .2s ease, box-shadow .2s ease;\n  }\n  #sticky-contact a:hover{transform:translateY(-2px) scale(1.06);box-shadow:0 10px 24px rgba(27,36,48,0.38);}\n  #sticky-contact a:focus-visible{outline:3px solid var(--gold);outline-offset:3px;}\n  #sticky-whatsapp{background:#25D366;}\n  #sticky-call{background:var(--ink);}\n  #sticky-contact svg{width:26px;height:26px;}\n  @media (max-width:640px){\n    #sticky-contact{right:14px;bottom:88px;gap:10px;}\n    #sticky-contact a{width:46px;height:46px;}\n    #sticky-contact svg{width:22px;height:22px;}\n  }\n  @media (max-width:380px){\n    #sticky-contact{right:10px;bottom:80px;gap:8px;}\n    #sticky-contact a{width:42px;height:42px;}\n    #sticky-contact svg{width:20px;height:20px;}\n  }\n  body.tv-mode #sticky-contact{display:none;} /* remote/D-pad has no use for a tap-to-call button */\n\n  .pin-wrap{width:32px;height:44px;position:relative;transform-origin:50% 100%;}\n  .pin-circle{\n    position:absolute;top:0;left:2px;width:28px;height:28px;border-radius:50%;\n    box-shadow:0 3px 7px rgba(0,0,0,0.35);\n    display:flex;align-items:center;justify-content:center;overflow:hidden;\n    border:2px solid rgba(255,255,255,0.9);\n  }\n  .pin-circle img{width:100%;height:100%;object-fit:cover;}\n  .pin-circle .glyph{color:#fff;font-size:13px;}\n  .pin-tail{\n    position:absolute;top:22px;left:50%;transform:translateX(-50%);\n    width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;\n    border-top:15px solid currentColor;filter:drop-shadow(0 2px 1px rgba(0,0,0,0.25));\n  }\n  .pin-ground{\n    position:absolute;bottom:1px;left:50%;transform:translateX(-50%);\n    width:10px;height:4px;border-radius:50%;background:rgba(0,0,0,0.28);filter:blur(1px);\n  }\n  .pin-radar{\n    position:absolute;top:0;left:2px;width:28px;height:28px;border-radius:50%;\n    border:3px solid currentColor;animation:radar-pulse 1.6s ease-out infinite;pointer-events:none;\n  }\n  @keyframes radar-pulse{\n    0%{transform:scale(0.6);opacity:0.9;}\n    100%{transform:scale(2.6);opacity:0;}\n  }\n  .infra-pin{\n    border-radius:10px;border:2.5px solid rgba(255,255,255,0.95);\n    display:flex;align-items:center;justify-content:center;\n    box-shadow:0 6px 14px rgba(0,0,0,0.45), inset 0 2px 3px rgba(255,255,255,0.5), inset 0 -3px 5px rgba(0,0,0,0.25);\n    background-image:linear-gradient(145deg, rgba(255,255,255,0.35), rgba(0,0,0,0.15));\n    background-blend-mode:overlay;\n    transition:transform .15s ease, box-shadow .15s ease;\n    cursor:pointer;\n  }\n  .infra-pin:hover{\n    transform:scale(1.15) translateY(-2px);\n    box-shadow:0 10px 20px rgba(0,0,0,0.5), inset 0 2px 3px rgba(255,255,255,0.6), inset 0 -3px 5px rgba(0,0,0,0.3);\n  }\n  .infra-pin img{width:70%;height:70%;object-fit:contain;filter:drop-shadow(0 1px 1px rgba(0,0,0,0.4));}\n\n  .teaser-popup .leaflet-popup-content-wrapper,\n  .infra-hover-popup .leaflet-popup-content-wrapper,\n  .road-hover-popup .leaflet-popup-content-wrapper {\n    border-radius:12px;padding:0;box-shadow:0 12px 30px rgba(27,36,48,0.3);border:none;\n  }\n  .teaser-popup .leaflet-popup-content,\n  .infra-hover-popup .leaflet-popup-content,\n  .road-hover-popup .leaflet-popup-content {\n    margin:0;\n    /* Mobile/tablet: fluid width tied to the viewport. */\n    width:clamp(300px, 88vw, 360px) !important;\n    max-width:calc(100vw - 16px) !important;\n  }\n  /* Desktop: was pinned to a single fixed 360px regardless of window size —\n     now it actually scales with the browser window instead of staying the\n     same size on a small laptop and a huge monitor. */\n  @media (min-width:641px){\n    .teaser-popup .leaflet-popup-content,\n    .infra-hover-popup .leaflet-popup-content,\n    .road-hover-popup .leaflet-popup-content {\n      width:clamp(320px, 24vw, 400px) !important;\n    }\n  }\n  /* Tall cards (logo + video + every field) get their own scroll instead\n     of overflowing past the bottom of a short mobile screen. */\n  .teaser-popup .leaflet-popup-content{\n    max-height:calc(100vh - 110px);\n    overflow-y:auto;\n    -webkit-overflow-scrolling:touch;\n  }\n  @media (max-width:640px){\n    .teaser-popup .leaflet-popup-content{ max-height:calc(100dvh - 100px); }\n  }\n  .teaser-popup .leaflet-popup-tip,\n  .infra-hover-popup .leaflet-popup-tip,\n  .road-hover-popup .leaflet-popup-tip {display:none;}\n  \n  .teaser-card{font-family:'Inter',sans-serif;border-radius:12px;overflow:hidden;background:#fff;}\n  .teaser-card img{width:100%;height:120px;object-fit:cover;display:block;}\n  /* Logo banner: centred logo in its own framed box at the top */\n  .tc-logo-wrap{\n    display:flex;align-items:center;justify-content:center;\n    background:#fff;padding:16px;border-bottom:1px solid var(--line);\n    min-height:80px;\n  }\n  .tc-logo-wrap img{\n    width:auto;height:auto;max-height:72px;max-width:80%;\n    object-fit:contain;display:block;\n  }\n  .teaser-card .tc-body{padding:14px 16px 14px;}\n  /* Header row: logo left, name + key details right — 50/50 split */\n  .tc-top{\n    display:grid;grid-template-columns:1fr 1fr;align-items:stretch;\n    border-bottom:1px solid var(--line);\n  }\n  .tc-top.no-logo{grid-template-columns:1fr;}\n  .tc-top .tc-logo-wrap{\n    border-bottom:none;border-right:1px solid var(--line);\n    padding:12px;min-height:0;\n  }\n  .tc-top .tc-logo-wrap img{max-width:100%;max-height:96px;height:auto;}\n  .tc-head{padding:14px 14px 12px;min-width:0;}\n  .tc-head .tc-title{margin-bottom:6px;overflow-wrap:anywhere;}\n  .tc-head .tc-field{margin-bottom:5px;}\n  .tc-head .tc-meta{font-size:12.5px;overflow-wrap:anywhere;}\n  .tc-head .tc-status,.tc-head .tc-type{font-size:10.5px;padding:3px 9px;}\n  .tc-body > .tc-brochure-label:first-child{margin-top:-14px;border-top:none;}\n  .teaser-card .tc-title{\n    font-family:'Playfair Display',serif;font-size:15px;font-weight:700;\n    color:var(--ink);margin:0 0 8px;line-height:1.35;\n  }\n  .teaser-card .tc-details{margin-bottom:4px;}\n  .teaser-card .tc-field{margin-bottom:6px;line-height:1.4;}\n  .teaser-card .tc-field:last-child{margin-bottom:0;}\n  .teaser-card .tc-meta{font-size:13px;color:var(--ink-soft);line-height:1.4;}\n  .teaser-card .tc-status{\n    display:inline-block;padding:3px 10px;border-radius:20px;\n    font-size:11px;font-weight:700;color:#fff;margin-right:5px;\n  }\n  .teaser-card .tc-type{\n    display:inline-block;padding:3px 10px;border-radius:20px;\n    font-size:11px;font-weight:600;color:var(--ink-soft);background:#f0ebe0;\n  }\n  /* PROJECT VIDEO section — full-bleed out of body padding */\n  .tc-brochure-label{\n    background:var(--paper);color:var(--ink-soft);font-size:10px;font-weight:700;\n    text-transform:uppercase;letter-spacing:.8px;\n    padding:8px 16px;margin:12px -16px 0;\n    border-top:1px solid var(--line);border-bottom:1px solid var(--line);\n  }\n  .tc-brochure-media{\n    display:block;\n    width:calc(100% + 32px);\n    margin:0 -16px;\n    /* 16:9 of the card body width (card - 32px padding) */\n    aspect-ratio:16/9;\n    height:auto;\n    min-height:160px;\n    background:#000;\n    position:relative;\n    overflow:hidden;\n    flex-shrink:0;\n  }\n  .tc-brochure-media img.tc-brochure-image{\n    width:100%;height:100%;object-fit:cover;display:block;\n    position:absolute;inset:0;\n  }\n  .tc-video-wrap{\n    display:none;\n    position:absolute;inset:0;\n    width:100%;height:100%;\n  }\n  .tc-video-wrap.playing{display:block;}\n  .tc-video-wrap iframe{\n    display:block;\n    position:absolute;inset:0;\n    width:100%;height:100%;\n    border:0;\n  }\n  .tc-video-fallback{\n    position:absolute;inset:0;display:flex;align-items:center;justify-content:center;\n    color:#fff;background:rgba(0,0,0,0.35);font-family:'Inter',sans-serif;\n    font-size:14px;font-weight:700;text-decoration:none;letter-spacing:.3px;\n    transition:background .15s ease;\n  }\n  .tc-video-fallback:hover{background:rgba(0,0,0,0.5);}\n  /* Action buttons */\n  .teaser-card .tc-actions{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;}\n  .teaser-card .tc-whatsapp{\n    display:flex;align-items:center;justify-content:center;gap:5px;flex:1;\n    background:#25D366;color:#fff;border:none;border-radius:8px;\n    padding:10px 0;font-size:13px;font-weight:700;text-decoration:none;\n    font-family:'Inter',sans-serif;transition:.15s ease;\n  }\n  .teaser-card .tc-whatsapp:hover{background:#1ebe57;}\n  .teaser-card .tc-whatsapp svg{width:16px;height:16px;flex:none;display:block;}\n  @media (max-width:640px){ .teaser-card .tc-whatsapp svg{width:19px;height:19px;} }\n  .teaser-card button{\n    flex:1;background:var(--ink);color:#fff;border:none;border-radius:8px;\n    padding:10px 0;font-size:13px;font-weight:700;cursor:pointer;\n    font-family:'Inter',sans-serif;transition:.15s ease;\n  }\n  .teaser-card button:hover{background:#000;}\n  /* Compact Enquire button (desktop + mobile) */\n  .teaser-card .tc-actions button[data-pin]{\n    flex:0 0 auto;width:auto;padding:0 16px;height:34px;min-height:34px;\n    font-size:12.5px;border-radius:999px;\n  }\n  .teaser-card button.tc-share{\n    flex:1 1 100%;justify-content:center;margin-top:2px;\n    background:#fff;color:var(--ink);border:1px solid #e6dfd0;padding:9px 12px;\n    display:flex;align-items:center;gap:5px;border-radius:8px;\n  }\n  .teaser-card button.tc-share:hover{background:var(--gold);color:#241a0d;border-color:var(--gold);}\n  @media (max-width:480px){\n    .tc-brochure-media{min-height:140px;}\n  }\n  /* Mobile: the action buttons stay pinned to the bottom of the card while\n     it scrolls, and Share sits on the same row as a compact icon button —\n     before, it was on its own row below the fold and never seen. */\n  @media (max-width:640px){\n    .teaser-card{overflow:clip;}\n    .teaser-card .tc-actions{\n      position:sticky;bottom:0;z-index:2;flex-wrap:nowrap;\n      background:#fff;margin:12px -16px -14px;padding:10px 16px 12px;\n      border-top:1px solid var(--line);\n      box-shadow:0 -6px 14px rgba(27,36,48,0.06);\n    }\n    .teaser-card button.tc-share{\n      flex:0 0 46px;width:46px;margin-top:0;padding:0;font-size:17px;\n    }\n    .teaser-card .tc-share-label{display:none;}\n  }\n\n  .infra-hover-card, .road-hover-card { font-family: 'Inter', sans-serif; overflow: hidden; }\n  .infra-hover-card .ihc-header, .road-hover-card .rhc-header { display: flex; align-items: center; gap: 10px; padding: 12px 14px; color: #fff; }\n  .infra-hover-card .ihc-emoji, .road-hover-card .rhc-emoji { font-size: 24px; line-height: 1; }\n  .infra-hover-card .ihc-emoji img { width: 28px; height: 28px; object-fit: contain; }\n  .infra-hover-card .ihc-title-wrap, .road-hover-card .rhc-title-wrap { flex: 1; min-width: 0; }\n  .infra-hover-card .ihc-title, .road-hover-card .rhc-title { font-family: 'Playfair Display', serif; font-size: 14px; font-weight: 600; color: #fff; margin: 0; line-height: 1.3; word-wrap: break-word; }\n  .infra-hover-card .ihc-type, .road-hover-card .rhc-type { font-size: 10px; font-weight: 600; color: rgba(255,255,255,0.85); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }\n  .infra-hover-card .ihc-body, .road-hover-card .rhc-body { padding: 12px 14px; background: var(--panel); }\n  .infra-hover-card .ihc-notes, .road-hover-card .rhc-notes { font-size: 12px; color: var(--ink-soft); line-height: 1.4; margin: 0; word-wrap: break-word; }\n  .infra-hover-card .ihc-empty, .road-hover-card .rhc-empty { font-size: 11px; color: var(--ink-soft); font-style: italic; margin: 0; }\n  .road-hover-card .rhc-footer { padding: 8px 14px; background: var(--paper); border-top: 1px solid var(--line); font-size: 10px; color: var(--ink-soft); display: flex; align-items: center; gap: 4px; }\n\n  .leaflet-popup-content{font-family:'Inter',sans-serif;font-size:12.5px;}\n  .leaflet-control-attribution{ max-width:58vw; font-size:10px; line-height:1.5; }\n  .leaflet-popup-content b{font-family:'Playfair Display',serif;}\n\n  /* Overlay shared by lead modal + detail panel */\n  #overlay-bg{\n    position:absolute;inset:0;z-index:1700;background:rgba(27,36,48,0.5);\n    display:none;align-items:center;justify-content:center;\n  }\n  #overlay-bg.open{display:flex;}\n\n  #lead-box{\n    width:360px;max-width:92vw;background:var(--panel);border-radius:14px;box-shadow:var(--shadow);\n    padding:24px 24px 20px;\n  }\n  #lead-box h3{margin:0 0 4px;font-family:'Playfair Display',serif;font-size:clamp(17px,4.5vw,19px);color:var(--ink);}\n  #lead-box p{margin:0 0 16px;font-size:12.5px;color:var(--ink-soft);}\n  .field{margin-bottom:13px;}\n  .field label{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--ink-soft);margin-bottom:5px;}\n  .field input, .field select{\n    width:100%;padding:10px 11px;border:1px solid var(--line);border-radius:7px;font-size:13.5px;\n    font-family:'Inter',sans-serif;color:var(--ink);background:#fdfcfa;\n  }\n  .field input:focus, .field select:focus{outline:2px solid var(--gold);border-color:var(--gold);}\n  #lead-error{font-size:11.5px;color:var(--sold);min-height:14px;margin-bottom:6px;}\n  #lead-consent-row{display:flex;align-items:flex-start;gap:9px;margin-bottom:14px;}\n  #lead-consent-row input[type=checkbox]{width:17px;height:17px;flex:none;margin-top:2px;cursor:pointer;}\n  #lead-consent-row label{font-size:10.5px;line-height:1.5;color:var(--ink-soft);font-weight:400;text-transform:none;letter-spacing:0;cursor:pointer;}\n  .lead-actions{display:flex;gap:10px;margin-top:6px;}\n  .lead-actions button{flex:1;padding:11px 0;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;border:1px solid var(--line);}\n  #lead-submit{background:var(--gold);color:#241a0d;border-color:var(--gold);touch-action:manipulation;}\n  #lead-submit:hover{background:var(--gold-deep);color:#fff;}\n  #lead-cancel{background:#fff;color:var(--ink-soft);}\n\n  #detail-panel{\n    position:absolute;top:0;right:0;height:100%;width:400px;max-width:92vw;\n    background:var(--panel);box-shadow:-8px 0 30px rgba(27,36,48,0.2);\n    z-index:1500;transform:translateX(100%);transition:transform .28s ease;\n    display:flex;flex-direction:column;\n  }\n  #detail-panel.open{transform:translateX(0);}\n  #detail-head{padding:0;position:relative;}\n  #detail-head img{width:100%;height:200px;object-fit:cover;display:block;}\n  #detail-close{\n    position:absolute;top:14px;right:14px;background:rgba(27,36,48,0.6);color:#fff;border:none;\n    width:32px;height:32px;border-radius:50%;font-size:15px;cursor:pointer;\n    display:flex;align-items:center;justify-content:center;padding:0;line-height:1;\n  }\n  #detail-body{padding:22px 24px;overflow-y:auto;flex:1;}\n  #detail-body h2{font-family:'Playfair Display',serif;font-size:clamp(18px,5vw,22px);margin:0 0 6px;}\n  #detail-body .status-badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:700;color:#fff;margin-bottom:14px;}\n  .detail-row{margin-bottom:16px;}\n  .detail-row label{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--ink-soft);margin-bottom:4px;}\n  .detail-row .value{font-size:14.5px;color:var(--ink);line-height:1.5;}\n  .detail-row .value.price{font-family:'Playfair Display',serif;font-size:19px;color:var(--gold-deep);}\n  .detail-share{\n    width:100%;margin-top:6px;background:#fff;color:var(--ink);border:1px solid #e6dfd0;\n    border-radius:8px;padding:11px 0;font-size:13px;font-weight:700;cursor:pointer;\n    font-family:'Inter',sans-serif;display:flex;align-items:center;justify-content:center;gap:7px;\n  }\n  .detail-share:hover{background:var(--gold);color:#241a0d;border-color:var(--gold);}\n\n  /* Legal (Privacy / Disclaimer) popup */\n  #legal-overlay{\n    position:fixed;inset:0;z-index:1900;background:rgba(27,36,48,0.55);\n    display:none;align-items:center;justify-content:center;padding:20px;\n  }\n  #legal-overlay.open{display:flex;}\n  #legal-box{\n    width:640px;max-width:100%;max-height:85vh;background:var(--panel);\n    border-radius:14px;box-shadow:var(--shadow);display:flex;flex-direction:column;overflow:hidden;\n  }\n  #legal-box-head{\n    display:flex;align-items:center;justify-content:space-between;\n    padding:20px 24px;border-bottom:1px solid var(--line);flex:none;\n  }\n  #legal-box-head h2{font-family:'Playfair Display',serif;font-size:clamp(16px,4.5vw,20px);margin:0;color:var(--ink);line-height:1.3;}\n  #legal-close{\n    background:var(--paper);border:1px solid var(--line);color:var(--ink-soft);\n    width:32px;height:32px;border-radius:50%;font-size:14px;cursor:pointer;flex:none;\n    display:flex;align-items:center;justify-content:center;padding:0;line-height:1;\n    transition:.2s ease;\n  }\n  #legal-close:hover{background:var(--gold);color:#241a0d;border-color:var(--gold);}\n  #legal-body{\n    padding:26px 28px;overflow-y:auto;line-height:1.75;font-size:14.5px;color:var(--ink-soft);\n  }\n  #legal-body h3{font-family:'Playfair Display',serif;font-size:17px;color:var(--ink);margin:24px 0 10px;line-height:1.3;}\n  #legal-body h3:first-child{margin-top:0;}\n  #legal-body p{margin:0 0 14px;}\n  #legal-body ul{margin:0 0 14px;padding-left:22px;}\n  #legal-body li{margin-bottom:6px;}\n  #legal-body .contact-block{\n    background:var(--paper);border:1px solid var(--line);border-radius:10px;\n    padding:16px 18px;margin:8px 0 16px;font-size:13.5px;color:var(--ink);line-height:1.6;\n  }\n  #legal-body .contact-block strong{display:block;font-family:'Playfair Display',serif;font-size:15px;margin-bottom:6px;}\n  #legal-body .legal-updated{font-size:11.5px;color:var(--ink-soft);text-transform:uppercase;letter-spacing:.5px;margin:0 0 18px;}\n  #legal-body a{color:var(--gold-deep);}\n\n  #loading{\n    position:absolute;inset:0;z-index:2000;background:var(--paper);\n    display:flex;align-items:center;justify-content:center;flex-direction:column;gap:10px;\n    font-family:'Playfair Display',serif;color:var(--ink-soft);\n  }\n  #loading .spin{width:26px;height:26px;border:3px solid var(--line);border-top-color:var(--gold);border-radius:50%;animation:spin 0.9s linear infinite;}\n  @keyframes spin{to{transform:rotate(360deg);}}\n\n  #search-suggestions{\n    display:none;position:absolute;top:calc(100% + 6px);left:0;right:0;\n    background:var(--panel);border-radius:12px;box-shadow:var(--shadow);\n    padding:6px;max-height:320px;overflow-y:auto;z-index:1300;\n  }\n  #search-suggestions.open{display:block;}\n  .suggestion-item{\n    padding:9px 12px;border-radius:8px;font-size:13px;color:var(--ink);\n    cursor:pointer;font-family:'Inter',sans-serif;transition:.15s ease;\n    display:flex;align-items:center;justify-content:space-between;gap:10px;\n  }\n  .suggestion-item:hover, .suggestion-item.active{background:var(--paper);color:var(--gold-deep);}\n  .suggestion-item .sg-type{font-size:10px;color:var(--ink-soft);text-transform:uppercase;letter-spacing:.4px;flex:none;}\n\n  #btn-near-me.locating{opacity:.7;pointer-events:none;}\n  .me-pulse{\n    width:16px;height:16px;border-radius:50%;background:#2c78e0;\n    border:3px solid #fff;box-shadow:0 0 0 rgba(44,120,224,0.5);\n    animation:me-ping 1.8s infinite;\n  }\n  @keyframes me-ping{\n    0%{box-shadow:0 0 0 0 rgba(44,120,224,0.5);}\n    70%{box-shadow:0 0 0 14px rgba(44,120,224,0);}\n    100%{box-shadow:0 0 0 0 rgba(44,120,224,0);}\n  }\n\n  @media (max-width:640px){\n    #topbar{ flex-wrap:wrap; gap:8px; padding:10px 12px; }\n    #topbar .brand .brand-text{ display:none; }\n    #topbar .brand{ padding:6px 10px; }\n    #topbar .brand-logos{ gap:6px; }\n    #topbar .brand img.logo-main{ height:32px; }\n    #topbar .brand img.logo-secondary{ height:24px; max-width:70px; }\n    #topbar .brand-by{ font-size:10px; }\n    #toolbar{ width:100%; flex-wrap:nowrap; }\n    #search-input{ flex:1 1 auto; width:auto; min-width:0; min-height:38px; padding:8px 12px; font-size:12px; order:1; }\n    .btn{\n      padding:0; width:38px; height:38px; min-height:38px; flex:0 0 38px;\n      font-size:15px; justify-content:center; white-space:nowrap; order:2;\n      border-radius:50%;\n    }\n    /* Text label hidden on the compact mobile row — icon only, with the\n       full label still read by screen readers via aria-label. */\n    .btn .btn-label{ display:none; }\n    #legend{\n      left:12px; right:12px; bottom:12px; max-width:none;\n      max-height:45vh; overflow-y:auto; font-size:12px; padding:12px 14px;\n    }\n    #legend .row{ margin-bottom:6px; min-height:28px; }\n    #legend-toggle{ padding:6px 2px; min-height:36px; display:flex; align-items:center; justify-content:space-between; }\n    #lead-box{ padding:18px 18px 16px; }\n    .lead-actions button{ min-height:46px; }\n    #detail-panel{width:100%;}\n    #detail-body{ padding:18px 16px; }\n    #detail-close{ width:40px; height:40px; }\n    #legal-box{ max-height:90vh; }\n    #legal-box-head{ padding:16px 18px; }\n    #legal-body{ padding:18px 18px; font-size:13.5px; }\n    .teaser-card .tc-whatsapp, .teaser-card button{ min-height:42px; font-size:13.5px; }\n    .leaflet-popup-close-button{ width:34px !important; height:34px !important; font-size:22px !important; }\n    .leaflet-control-attribution{ max-width:85vw; font-size:9px; line-height:1.4; }\n    /* Removes the ~300ms tap delay and stray double-tap zoom on buttons/cards\n       so taps register immediately, the way a native app feels. */\n    .btn, .teaser-card button, .teaser-card .tc-whatsapp, #legend-toggle,\n    #detail-close, .lead-actions button, .leaflet-marker-icon{\n      touch-action:manipulation; -webkit-tap-highlight-color:transparent;\n    }\n  }\n  @media (max-width:380px){\n    #topbar .brand span{ display:none; }\n    #topbar .brand-by, #topbar .brand img.logo-secondary{ display:none; }\n    #search-input{ font-size:12px; }\n    .leaflet-control-attribution{ max-width:92vw; font-size:8.5px; }\n  }\n  @media (hover:none) and (pointer:coarse){\n    /* Touch devices can't hover, so give buttons a visible pressed state\n       instead — otherwise taps feel unresponsive with no feedback at all. */\n    .btn:active, .teaser-card button:active, .teaser-card .tc-whatsapp:active{\n      transform:scale(0.96); opacity:.9;\n    }\n  }\n\n  /* ---------- Mobile: single-line header + compact legend ---------- */\n  @media (max-width:640px){\n    /* Logo badge, search and satellite all sit on one row */\n    #topbar{ flex-wrap:nowrap; align-items:center; gap:8px; padding:10px 10px; }\n    #brand-stack{ position:relative; flex:none; }\n    #topbar .brand,\n    #topbar .brand.collapsed{\n      width:44px; height:44px; padding:6px; border-radius:50%;\n      justify-content:center; flex:none;\n      background:#fff; box-shadow:0 2px 10px rgba(27,36,48,0.14);\n    }\n    #topbar .brand .brand-text,\n    #topbar .brand .brand-by,\n    #topbar .brand .logo-secondary{ display:none; }\n    #topbar .brand .brand-logos{ gap:0; }\n    #topbar .brand img.logo,\n    #topbar .brand img.logo-main{ width:32px; height:32px; max-width:32px; object-fit:contain; }\n    /* Intro video floats under the badge instead of widening the row */\n    #intro-video-panel{ position:absolute; top:calc(100% + 8px); left:0; }\n    #toolbar{ flex:1 1 auto; width:auto; min-width:0; gap:6px; }\n    #search-input{ height:44px; min-height:44px; font-size:14px; padding:8px 16px; }\n    .btn{ width:44px; height:44px; min-height:44px; flex:0 0 44px; }\n\n    /* Near Me removed on mobile (location is still detected on page load) */\n    #btn-near-me{ display:none !important; }\n\n    /* Legend: small pill at bottom-left, opens upward when tapped */\n    #legend{\n      left:10px; right:auto; bottom:14px;\n      max-width:min(240px, calc(100vw - 90px));\n      padding:6px 12px; border-radius:999px; font-size:11.5px;\n    }\n    #legend:not(.collapsed){ border-radius:12px; padding:10px 12px; max-height:45vh; }\n    #legend-toggle{ min-height:26px; padding:0; font-size:12px; gap:8px; }\n    #legend .row{ min-height:22px; margin-bottom:4px; }\n  }\n\n  /* ---------- Project card: close button + mobile polish ---------- */\n  .teaser-popup .tc-close{\n    position:absolute;top:8px;right:8px;z-index:20;\n    width:34px;height:34px;border-radius:50%;border:none;padding:0;\n    display:flex;align-items:center;justify-content:center;\n    background:rgba(255,255,255,0.95);color:var(--ink);\n    font-size:22px;line-height:1;cursor:pointer;\n    box-shadow:0 2px 8px rgba(27,36,48,0.18);\n    touch-action:manipulation;-webkit-tap-highlight-color:transparent;\n  }\n  .teaser-popup .tc-close:hover{background:var(--gold);color:#241a0d;}\n  .tc-head{padding-right:44px;}\n  @media (max-width:640px){\n    /* Card fits between the search bar and the legend, never under them */\n    .teaser-popup .leaflet-popup-content{\n      width:min(88vw, 350px) !important;\n      max-height:calc(100dvh - 190px);\n    }\n    .teaser-popup .leaflet-popup-content-wrapper{border-radius:14px;}\n    .tc-top .tc-logo-wrap{padding:6px;}\n    .tc-top .tc-logo-wrap img{width:100%;max-width:100%;max-height:120px;}\n    .tc-head{padding:12px 44px 10px 12px;}\n    .teaser-card .tc-title{font-size:14.5px;margin-bottom:4px;}\n    .tc-head .tc-meta{font-size:12px;}\n    .tc-head .tc-field{margin-bottom:4px;}\n    .teaser-card .tc-body{padding:12px 14px;}\n    .teaser-card .tc-meta{font-size:12.5px;}\n    .teaser-card .tc-field{margin-bottom:4px;}\n    .tc-brochure-label{padding:6px 14px;margin:10px -14px 0;}\n    .tc-brochure-media{width:calc(100% + 28px);margin:0 -14px;min-height:0;}\n    /* Smaller action buttons */\n    .teaser-card .tc-actions{\n      gap:6px;margin:10px -14px -12px;padding:8px 14px 10px;\n    }\n    .teaser-card .tc-actions button,\n    .teaser-card .tc-actions .tc-whatsapp{\n      min-height:36px !important;height:36px;padding:0 10px;\n      font-size:12.5px !important;border-radius:8px;\n    }\n    .teaser-card button.tc-share{flex:0 0 36px;width:36px;font-size:15px !important;padding:0;}\n    /* WhatsApp: small round icon button on mobile */\n    .teaser-card .tc-actions .tc-whatsapp{\n      flex:0 0 36px;width:36px;padding:0 !important;border-radius:50% !important;gap:0;\n    }\n    .teaser-card .tc-whatsapp .tc-wa-label{display:none;}\n    .teaser-card .tc-actions button[data-pin]{\n      flex:0 0 auto !important;width:auto;padding:0 14px !important;\n      height:32px;min-height:32px !important;font-size:12px !important;border-radius:999px !important;\n    }\n    /* Floating WhatsApp / Call buttons step aside while a card is open */\n    body.card-open #sticky-contact{opacity:0;pointer-events:none;transform:translateX(12px);}\n    #sticky-contact{transition:opacity .2s ease, transform .2s ease;}\n  }\n\n  /* ===== Compact project card (all screen sizes) ===== */\n  /* Smaller card */\n  .teaser-popup .leaflet-popup-content{\n    width:min(290px, calc(100vw - 32px)) !important;\n    max-height:min(480px, calc(100vh - 110px));\n  }\n  @media (max-width:640px){\n    .teaser-popup .leaflet-popup-content{\n      width:min(80vw, 280px) !important;\n      max-height:min(440px, calc(100dvh - 190px));\n    }\n  }\n  .tc-brochure-media{min-height:0;}\n\n  /* Header: logo fills its whole box (cover), text column slightly wider */\n  .tc-top{grid-template-columns:44% 56%;}\n  .tc-top.no-logo{grid-template-columns:1fr;}\n  .tc-top .tc-logo-wrap{\n    position:relative;padding:0 !important;min-height:120px;overflow:hidden;\n    background:#fff;\n  }\n  /* Logo fills the whole box but is never cropped — any logo shape\n     (wide, tall, square) scales to fit neatly. */\n  .tc-top .tc-logo-wrap img{\n    position:absolute;inset:8px;width:calc(100% - 16px) !important;height:calc(100% - 16px) !important;\n    max-width:none !important;max-height:none !important;object-fit:contain;object-position:center;\n  }\n  @media (max-width:640px){ .tc-top .tc-logo-wrap{min-height:110px;} }\n  .tc-head{padding:10px 34px 10px 10px !important;}\n  .teaser-card .tc-title{font-size:14px;margin-bottom:4px;}\n  .tc-head .tc-meta{font-size:11.5px;}\n  .teaser-card .tc-body{padding:10px 12px;}\n  .teaser-card .tc-meta{font-size:12px;}\n  .tc-brochure-label{padding:6px 12px;margin:10px -12px 0;}\n  .tc-brochure-media{width:calc(100% + 24px);margin:0 -12px;}\n  .teaser-popup .tc-close{width:28px;height:28px;font-size:18px;top:6px;right:6px;}\n\n  /* Action row: small Enquire pill + round WhatsApp + round Share */\n  .teaser-card .tc-actions{\n    flex-wrap:nowrap;align-items:center;gap:8px;margin-top:10px;\n  }\n  .teaser-card .tc-actions .tc-whatsapp,\n  .teaser-card .tc-actions button.tc-share{\n    flex:0 0 34px !important;width:34px !important;height:34px !important;\n    min-height:34px !important;padding:0 !important;margin:0;gap:0;\n    border-radius:50% !important;display:flex;align-items:center;justify-content:center;\n    font-size:15px !important;\n  }\n  .teaser-card .tc-whatsapp .tc-wa-label,\n  .teaser-card .tc-share-label{display:none !important;}\n  .teaser-card .tc-whatsapp svg{width:18px !important;height:18px !important;}\n  @media (max-width:640px){\n    .teaser-card .tc-actions{margin:10px -12px -10px;padding:8px 12px 10px;}\n  }\n\n  /* ===== Card: hidden scrollbar + floating action buttons ===== */\n  /* Card still scrolls, but the scrollbar is hidden */\n  .teaser-popup .leaflet-popup-content{scrollbar-width:none;-ms-overflow-style:none;}\n  .teaser-popup .leaflet-popup-content::-webkit-scrollbar{display:none;width:0;height:0;}\n\n  /* Enquire / WhatsApp / Share: small round buttons, stacked and fixed to\n     the middle of the card's right edge — they stay put while it scrolls */\n  .teaser-popup .leaflet-popup-content-wrapper{position:relative;}\n  .teaser-popup .tc-actions.tc-actions-float{\n    position:absolute !important;right:8px;top:50%;transform:translateY(-50%);\n    display:flex;flex-direction:column;flex-wrap:nowrap;align-items:center;gap:8px;\n    margin:0 !important;padding:0 !important;background:none !important;\n    border:none !important;box-shadow:none !important;z-index:15;\n  }\n  .teaser-popup .tc-actions-float .tc-whatsapp,\n  .teaser-popup .tc-actions-float button{\n    flex:0 0 36px !important;width:36px !important;height:36px !important;\n    min-height:36px !important;padding:0 !important;margin:0 !important;gap:0 !important;\n    border-radius:50% !important;display:flex !important;align-items:center;justify-content:center;\n    font-size:15px !important;box-shadow:0 3px 10px rgba(27,36,48,0.25);\n    border:2px solid #fff !important;\n  }\n  .teaser-popup .tc-actions-float button[data-pin]{background:var(--ink);color:#fff;}\n  .teaser-popup .tc-actions-float button.tc-share{background:#fff;color:var(--ink);}\n  .teaser-popup .tc-actions-float svg{width:18px !important;height:18px !important;display:block;}\n  .teaser-popup .tc-enq-label,\n  .teaser-popup .tc-wa-label,\n  .teaser-popup .tc-share-label{display:none !important;}\n  /* Keep text clear of the floating buttons */\n  .teaser-card .tc-details{padding-right:40px;}\n\n  /* ===== Shorter video + tap-friendly pins + responsive polish ===== */\n  /* Video box a little shorter (2:1 instead of 16:9) */\n  .tc-brochure-media{aspect-ratio:2/1 !important;min-height:0 !important;}\n  .tc-video-wrap iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;}\n  .tc-video-fallback{color:#fff !important;text-shadow:0 1px 4px rgba(0,0,0,0.6);}\n\n  /* Pins: bigger invisible tap area so every pin is easy to hit on phones */\n  .leaflet-marker-icon{cursor:pointer;}\n  .pin-wrap::before{\n    content:'';position:absolute;left:-8px;right:-8px;top:-8px;bottom:-4px;\n    border-radius:50%;\n  }\n  @media (hover:none), (max-width:640px){\n    .pin-wrap::before{left:-12px;right:-12px;top:-12px;bottom:-6px;}\n    .infra-pin{position:relative;}\n    .infra-pin::before{content:'';position:absolute;inset:-10px;}\n  }\n  /* The empty space in the top bar no longer blocks taps on pins under it */\n  #topbar{pointer-events:none;}\n  #topbar > *, #topbar #toolbar > *, #search-suggestions{pointer-events:auto;}\n  #toolbar{pointer-events:none;}\n  #brand-stack{pointer-events:none;}\n  #brand-stack > *{pointer-events:auto;}\n  #intro-video-panel:not(.show){pointer-events:none;}\n\n  /* Very small phones */\n  @media (max-width:360px){\n    .teaser-popup .leaflet-popup-content{width:calc(100vw - 28px) !important;}\n    .tc-top .tc-logo-wrap{min-height:96px;}\n    .teaser-card .tc-title{font-size:13.5px;}\n  }\n  /* Landscape phones: card fits the short screen */\n  @media (max-height:500px) and (orientation:landscape){\n    .teaser-popup .leaflet-popup-content{max-height:calc(100dvh - 90px) !important;}\n    .tc-brochure-media{aspect-ratio:21/9 !important;}\n  }\n\n  /* ===== Mobile: floating WhatsApp / Call buttons no longer overlap the\n     zoom buttons or the map credits ===== */\n  @media (max-width:640px){\n    /* Zoom +/- stays; WhatsApp / Call sit just above it (exact spot is\n       measured by placeStickyContact() in the script). */\n    #sticky-contact{\n      right:12px !important;\n      bottom:calc(125px + env(safe-area-inset-bottom, 0px)) !important;\n      gap:10px !important;\n    }\n    #sticky-contact a{width:46px !important;height:46px !important;}\n    #sticky-contact svg{width:22px !important;height:22px !important;}\n    /* Map credits stay small and readable under the buttons */\n    .leaflet-bottom.leaflet-right .leaflet-control-attribution{\n      margin:0 !important;padding:2px 6px !important;border-radius:6px 0 0 0;\n      background:rgba(255,255,255,0.85) !important;\n    }\n  }\n  @media (max-width:380px){\n    #sticky-contact{right:10px !important;gap:8px !important;}\n    #sticky-contact a{width:42px !important;height:42px !important;}\n    #sticky-contact svg{width:20px !important;height:20px !important;}\n  }\n  /* Phones turned sideways: buttons sit side by side to save height */\n  @media (max-height:500px) and (orientation:landscape){\n    #sticky-contact{flex-direction:row !important;}\n  }\n\n  /* ===== One set of action buttons only + never hidden behind × ===== */\n  .teaser-popup .leaflet-popup-content .tc-actions{display:none !important;}\n  .teaser-popup .leaflet-popup-content-wrapper{min-height:176px;}\n  .teaser-popup .tc-actions.tc-actions-float{\n    top:max(44px, calc(50% - 62px)) !important;\n    transform:none !important;\n    display:flex !important;\n  }\n\n  /* ===== Smaller status badge + \"Enquire Now\" badge button ===== */\n  .teaser-card .tc-status,\n  .teaser-card .tc-head .tc-status{\n    font-size:9.5px !important;padding:2px 8px !important;line-height:1.45;\n    white-space:nowrap;letter-spacing:.1px;\n  }\n  .teaser-card .tc-type,\n  .teaser-card .tc-head .tc-type{\n    font-size:9.5px !important;padding:2px 8px !important;line-height:1.45;white-space:nowrap;\n  }\n  .teaser-popup .tc-actions-float button[data-pin]{\n    flex:0 0 52px !important;width:52px !important;height:52px !important;min-height:52px !important;\n    background:transparent !important;border:none !important;overflow:visible;\n    box-shadow:0 4px 12px rgba(47,59,147,0.35);\n    transition:transform .2s ease;\n  }\n  .teaser-popup .tc-actions-float button[data-pin]:hover{transform:scale(1.07);}\n  .teaser-popup .tc-actions-float button[data-pin] svg.tc-enq-art{\n    width:52px !important;height:52px !important;overflow:visible;\n  }\n  .teaser-popup .tc-actions.tc-actions-float{top:max(44px, calc(50% - 70px)) !important;}\n  .teaser-popup .leaflet-popup-content-wrapper{min-height:192px;}\n  .teaser-card .tc-details{padding-right:52px;}\n\n  /* Floating buttons live outside .teaser-card, so give WhatsApp its green here */\n  .teaser-popup .tc-actions-float a.tc-whatsapp{background:#25D366 !important;color:#fff !important;text-decoration:none;}\n  .teaser-popup .tc-actions-float a.tc-whatsapp:hover{background:#1ebe57 !important;}\n\n  .teaser-card .tc-rera{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.3px;overflow-wrap:anywhere;}\n\n  /* ===== Compare cart (buyer accounts) ===== */\n  .teaser-popup .tc-actions-float button.tc-compare{background:#fff;color:var(--gold-deep);}\n  .teaser-popup .tc-actions-float button.tc-compare.on{background:var(--gold);color:#fff;}\n  .teaser-popup .tc-compare-label{display:none !important;}\n  .teaser-card button.tc-compare svg{width:18px;height:18px;}\n\n  #compare-bar{\n    position:fixed;left:50%;transform:translateX(-50%);\n    bottom:calc(24px + env(safe-area-inset-bottom, 0px));z-index:1200;\n    display:flex;align-items:center;gap:8px;padding:10px 14px 10px 16px;\n    border:none;border-radius:999px;background:var(--ink);color:#fff;\n    font:600 14px 'Inter',sans-serif;box-shadow:var(--shadow);cursor:pointer;\n  }\n  #compare-bar[hidden]{display:none;}\n  #compare-bar svg{width:18px;height:18px;}\n  #compare-bar .cb-count{background:var(--gold);border-radius:999px;padding:2px 9px;font-size:12px;}\n  #compare-bar:focus-visible{outline:3px solid var(--gold);outline-offset:2px;}\n\n  #compare-modal{\n    position:fixed;inset:0;z-index:9000;background:rgba(27,36,48,0.55);\n    display:flex;align-items:center;justify-content:center;padding:16px;\n  }\n  #compare-modal[hidden]{display:none;}\n  body.compare-open{overflow:hidden;}\n  /* The Install App pill (a fixed React button) would sit on top of the table. */\n  body.compare-open button[aria-label=\"Install Mappingg app\"]{display:none !important;}\n  .cmp-panel{\n    background:var(--panel);color:var(--ink);border-radius:16px;box-shadow:var(--shadow);\n    width:min(1100px, 100%);max-height:calc(100dvh - 32px);\n    display:flex;flex-direction:column;overflow:hidden;font-family:'Inter',sans-serif;\n  }\n  .cmp-head{display:flex;align-items:center;justify-content:space-between;padding:12px 12px 12px 18px;border-bottom:1px solid var(--line);}\n  .cmp-head h2{margin:0;font:700 20px 'Playfair Display',serif;}\n  .cmp-close{border:none;background:none;font-size:28px;line-height:1;width:44px;height:44px;cursor:pointer;color:var(--ink-soft);}\n  .cmp-close:focus{outline:none;}\n  .cmp-close:focus-visible{outline:2px solid var(--gold);outline-offset:-2px;border-radius:10px;}\n  .cmp-note{padding:10px 18px;background:#fbf4e8;color:var(--gold-deep);font-size:13px;border-bottom:1px solid var(--line);}\n  .cmp-scroll{overflow:auto;-webkit-overflow-scrolling:touch;flex:1 1 auto;}\n  .cmp-table{border-collapse:separate;border-spacing:0;width:100%;min-width:calc(140px + var(--cols) * 200px);font-size:13.5px;}\n  .cmp-table th,.cmp-table td{padding:10px 12px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top;}\n  .cmp-table td{overflow-wrap:anywhere;}\n  .cmp-table thead th{position:sticky;top:0;background:var(--panel);z-index:2;}\n  .cmp-table tbody th{\n    position:sticky;left:0;z-index:1;width:140px;min-width:140px;background:var(--paper);\n    color:var(--ink-soft);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;\n  }\n  .cmp-table thead th:first-child{left:0;z-index:3;background:var(--paper);}\n  .cmp-count{font-size:11px;font-weight:700;color:var(--ink-soft);text-transform:uppercase;letter-spacing:.6px;}\n  .cmp-proj{position:relative;display:flex;flex-direction:column;gap:6px;padding-right:30px;}\n  .cmp-proj img{height:48px;max-width:130px;object-fit:contain;object-position:left;}\n  .cmp-title{font-weight:700;font-size:14px;line-height:1.3;}\n  .cmp-remove{\n    position:absolute;top:-2px;right:-2px;width:32px;height:32px;border-radius:50%;\n    border:1px solid var(--line);background:#fff;color:var(--ink-soft);font-size:18px;line-height:1;cursor:pointer;\n  }\n  .cmp-remove:hover{color:var(--sold);border-color:var(--sold);}\n  .cmp-empty-val{color:#9aa1aa;}\n  .cmp-status{display:inline-block;color:#fff;border-radius:999px;padding:2px 10px;font-size:12px;font-weight:600;}\n  .cmp-video{color:var(--upcoming);font-weight:600;text-decoration:none;}\n  .cmp-actions-row td,.cmp-actions-row th{border-bottom:none;}\n  .cmp-actions{display:flex;flex-direction:column;gap:6px;}\n  .cmp-actions button{\n    min-height:38px;border-radius:8px;padding:8px 10px;font:600 13px 'Inter',sans-serif;cursor:pointer;\n    border:1px solid var(--ink);background:#fff;color:var(--ink);\n  }\n  .cmp-actions button.primary{background:var(--ink);color:#fff;}\n  .cmp-foot{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 18px;border-top:1px solid var(--line);font-size:13px;color:var(--ink-soft);}\n  .cmp-foot button{border:none;background:none;color:var(--sold);font:600 13px 'Inter',sans-serif;cursor:pointer;padding:8px 0;}\n  .cmp-emptystate{padding:48px 20px;text-align:center;color:var(--ink-soft);line-height:1.6;}\n  @media (max-width:640px){\n    #compare-bar{bottom:calc(88px + env(safe-area-inset-bottom, 0px));}\n    #compare-modal{padding:0;align-items:stretch;}\n    .cmp-panel{border-radius:0;max-height:none;height:100dvh;width:100%;}\n    .cmp-table{min-width:calc(100px + var(--cols) * 165px);font-size:13px;}\n    .cmp-table tbody th{width:100px;min-width:100px;}\n    .cmp-foot span{display:none;}\n  }\n\n  /* \"Click on the pin\" hover hint (desktop) */\n  .leaflet-tooltip.pin-hint{\n    background:var(--ink);color:#fff;border:none;border-radius:999px;\n    padding:6px 12px;font:600 12.5px 'Inter',sans-serif;white-space:nowrap;\n    box-shadow:0 6px 18px rgba(27,36,48,0.28);\n  }\n  .leaflet-tooltip.pin-hint::before{border-top-color:var(--ink);}\n  .leaflet-tooltip.pin-hint .pin-hint-ic{margin-right:3px;}\n","scripts":[{"type":"inline","code":"\n(function(){\n  var SUPABASE_URL = '';\n  var SUPABASE_ANON_KEY = 'local';\n  window.__mapSettingsReady = fetch(SUPABASE_URL + '/rest/v1/map_settings?id=eq.1&select=gtm_container_id,search_console_verification,youtube_video_url', {\n    headers: { 'apikey': SUPABASE_ANON_KEY, 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY }\n  }).then(function(r){ return r.json(); }).then(function(rows){\n    var row = rows && rows[0];\n    if(!row) return {};\n    if(row.search_console_verification){\n      var meta = document.createElement('meta');\n      meta.name = 'google-site-verification';\n      meta.content = row.search_console_verification;\n      document.head.appendChild(meta);\n    }\n    if(row.gtm_container_id){\n      window.dataLayer = window.dataLayer || [];\n      window.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });\n      var j = document.createElement('script');\n      j.async = true;\n      j.src = 'https://www.googletagmanager.com/gtm.js?id=' + row.gtm_container_id;\n      document.head.appendChild(j);\n      document.addEventListener('DOMContentLoaded', function(){\n        var ifr = document.createElement('iframe');\n        ifr.src = 'https://www.googletagmanager.com/ns.html?id=' + row.gtm_container_id;\n        ifr.height = 0; ifr.width = 0; ifr.style.display = 'none'; ifr.style.visibility = 'hidden';\n        var ns = document.createElement('noscript');\n        ns.appendChild(ifr);\n        document.body.insertBefore(ns, document.body.firstChild);\n      });\n    }\n    return row;\n  }).catch(function(){ /* fails silently — tracking is never critical to the page working */ return {}; });\n})();\n"},{"type":"inline","code":"\n(function(){\n  var ALLOWED_HOSTS = []; // e.g. ['associatte.com','www.associatte.com']\n  if(ALLOWED_HOSTS.length && ALLOWED_HOSTS.indexOf(location.hostname) === -1){\n    document.write('<div style=\"display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;text-align:center;padding:20px;color:#4a5568;\">This map is licensed for use on its original website only.</div>');\n    window.stop ? window.stop() : (document.execCommand && document.execCommand('Stop'));\n  }\n})();\n"},{"type":"ext","src":"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js","async":false},{"type":"ext","src":"/db-shim.js","async":false},{"type":"inline","code":"\n(async function(){\n\n  const SUPABASE_URL = '';\n  const SUPABASE_ANON_KEY = 'local';\n  const sb = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);\n\n  // ---------- Tag Manager tracking helper ----------\n  // Pushes to the dataLayer directly (GTM's native format) instead of\n  // calling gtag() — GTM's own snippet doesn't define a gtag() function,\n  // only dataLayer.push(). Create a GA4 tag inside GTM triggered on a\n  // custom event named \"site_action\" to pick these up — this is how\n  // lead_submitted, whatsapp_click, and enquire_click reach your analytics.\n  function trackEvent(category, action, label){\n    if(window.dataLayer){\n      window.dataLayer.push({ event: 'site_action', event_category: category, event_action: action, event_label: label });\n    }\n  }\n\n  const statusColor = {available:'#3f8f6b', sold:'#b5484c', construction:'#c98a2c', upcoming:'#3b6ea5'};\n  const statusLabel = {available:'Available', sold:'Sold', construction:'Under Construction', upcoming:'Upcoming'};\n\n  const infraTypes = {\n    bridge:          {emoji:'🌉', label:'Bridge',              color:'#3b6ea5'},\n    highway_bridge:  {emoji:'🛣️', label:'Highway Bridge',       color:'#8f6f42'},\n    flyover:         {emoji:'🎢', label:'Flyover',              color:'#7a5c9e'},\n    bullet_train:    {emoji:'🚄', label:'Bullet Train Station', color:'#b5484c'},\n    metro:           {emoji:'🚇', label:'Metro Station',        color:'#3f8f6b'},\n    metro_line:      {emoji:'🚇', label:'Metro Line',           color:'#3f8f6b'},\n    railway_station: {emoji:'🚉', label:'Railway Station',      color:'#5b7c99'},\n    hotel:           {emoji:'🏨', label:'Hotel',                color:'#a0527a'},\n    hospital:        {emoji:'🏥', label:'Hospital',             color:'#c0392b'},\n    underpass:       {emoji:'🔽', label:'Underpass',            color:'#5c6670'},\n    proposed_road:   {emoji:'🚧', label:'Proposed Road',        color:'#c98a2c'},\n    school:          {emoji:'🏫', label:'School',                color:'#5c8a3f'},\n    garden:          {emoji:'🌳', label:'Garden',                color:'#22c55e'}\n  };\n  const infraSizes = {small:{px:26, font:13}, medium:{px:36, font:18}, large:{px:48, font:24}};\n\n  // Custom infra types created in the admin editor (e.g. \"Helipad\") get merged in here\n  // once fetched below, so they show with their real name instead of falling back to\n  // \"Bridge\". This never touches the built-in list above.\n  function registerCustomInfraType(ct){\n    try{\n      infraTypes[ct.key] = { emoji: ct.emoji || '📍', label: ct.label, color: ct.color || '#3b6ea5' };\n    }catch(err){\n      console.error('registerCustomInfraType failed (non-fatal):', err);\n    }\n  }\n\n  function escapeHtml(str){\n    if(!str) return '';\n    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;').replace(/'/g,'&#39;');\n  }\n\n\n  function pinShareUrl(pin){\n    const url = new URL(window.location.href);\n    url.search = '';\n    url.searchParams.set('pin', pin.id);\n    return url.toString();\n  }\n\n  async function sharePin(pin){\n    const url = pinShareUrl(pin);\n    const shareData = {\n      title: pin.title || 'Land parcel — Associatte',\n      text: 'Check out this Project on the Associatte Map: ' + (pin.title || ''),\n      url\n    };\n    if(navigator.share){\n      try{ await navigator.share(shareData); return; }\n      catch(_e){ /* user cancelled or share failed — fall through to copy */ }\n    }\n    try{\n      await navigator.clipboard.writeText(url);\n      alert('Link copied — you can now paste and share it.');\n    }catch(_e){\n      window.prompt('Copy this link to share:', url);\n    }\n  }\n  window.sharePin = sharePin;\n\n  function teaserLine(p){\n    if(p.status === 'construction'){\n      const bits = [p.configuration, p.sqft].filter(Boolean).join(' · ');\n      const poss = p.possession_timeline ? ('Possession: ' + p.possession_timeline) : '';\n      return [bits, poss].filter(Boolean).join('<br>') || 'Details on request';\n    }\n    if(p.status === 'upcoming'){\n      const bits = [p.configuration, p.sqft].filter(Boolean).join(' · ');\n      const launch = p.launch_date ? ('Launching: ' + p.launch_date) : '';\n      return [bits, launch].filter(Boolean).join('<br>') || 'Launching soon';\n    }\n    if(p.status === 'available'){\n      return p.whats_available || (p.price || 'Enquire for availability');\n    }\n    return 'This project has been sold out.';\n  }\n\n  function infraHoverContent(b){\n    const meta = infraTypes[b.type] || infraTypes.bridge;\n    const color = b.color || meta.color;\n    const title = (b.title && b.title.trim()) ? b.title.trim() : meta.label;\n    const emojiInner = b.icon_image ? `<img src=\"${escapeHtml(b.icon_image)}\" alt=\"\">` : meta.emoji;\n    const notesHtml = (b.notes && b.notes.trim()) ? `<p class=\"ihc-notes\">${escapeHtml(b.notes.trim())}</p>` : `<p class=\"ihc-empty\">No notes added yet</p>`;\n    return `<div class=\"infra-hover-card\">\n      <div class=\"ihc-header\" style=\"background:${color};\">\n        <div class=\"ihc-emoji\">${emojiInner}</div>\n        <div class=\"ihc-title-wrap\">\n          <p class=\"ihc-title\">${escapeHtml(title)}</p>\n          <div class=\"ihc-type\">${escapeHtml(meta.label)}</div>\n        </div>\n      </div>\n      <div class=\"ihc-body\">${notesHtml}</div>\n    </div>`;\n  }\n\n  function roadHoverContent(r){\n    const meta = infraTypes[r.type] || infraTypes.proposed_road;\n    const color = r.color || meta.color;\n    const title = (r.title && r.title.trim()) ? r.title.trim() : meta.label;\n    const notesHtml = (r.notes && r.notes.trim()) ? `<p class=\"rhc-notes\">${escapeHtml(r.notes.trim())}</p>` : `<p class=\"rhc-empty\">No notes added yet</p>`;\n    const pointCount = r.points ? r.points.length : 0;\n    return `<div class=\"road-hover-card\">\n      <div class=\"rhc-header\" style=\"background:${color};\">\n        <div class=\"rhc-emoji\">${meta.emoji}</div>\n        <div class=\"rhc-title-wrap\">\n          <p class=\"rhc-title\">${escapeHtml(title)}</p>\n          <div class=\"rhc-type\">${escapeHtml(meta.label)}</div>\n        </div>\n      </div>\n      <div class=\"rhc-body\">${notesHtml}</div>\n      <div class=\"rhc-footer\">📍 ${pointCount} point${pointCount === 1 ? '' : 's'}</div>\n    </div>`;\n  }\n\n  // zoomSnap/zoomDelta of 0.25 splits each zoom level into 4 smaller steps —\n  // so +/- clicks, scroll notches, and pinch gestures move the map gradually\n  // instead of jumping a whole level at once.\n  const map = L.map('map', {zoomControl:false, minZoom:4, maxZoom:20, zoomSnap:0.25, zoomDelta:0.25}).setView([18.5215, 73.9140], 14.5);\n  map.attributionControl.setPrefix(\n    '<a href=\"https://associatte.com\" target=\"_blank\" rel=\"noopener\">Associatte.com</a> · ' +\n    '<a href=\"javascript:void(0)\" onclick=\"openLegalModal(\\'privacy\\')\">Privacy Policy</a> · <a href=\"javascript:void(0)\" onclick=\"openLegalModal(\\'disclaimer\\')\">Disclaimer</a> · <a href=\"/map-data\" target=\"_blank\" rel=\"noopener\">Map data</a>'\n  );\n  L.control.zoom({position:'bottomright'}).addTo(map);\n  // Ask for the visitor's location the moment the map opens — in parallel\n  // with loading the pins — so the permission prompt shows up immediately\n  // instead of after every pin has loaded. focusOnLoad() further below\n  // uses the result once the pins are on the map.\n  const earlyLocation = new Promise(resolve=>{\n    if(!navigator.geolocation) return resolve(null);\n    navigator.geolocation.getCurrentPosition(\n      pos=> resolve({ lat:pos.coords.latitude, lng:pos.coords.longitude }),\n      ()=> resolve(null),\n      { enableHighAccuracy:true, timeout:10000, maximumAge:120000 }\n    );\n  });\n  // Keeps the floating WhatsApp / Call buttons stacked just above the\n  // zoom +/- buttons on every screen size, so they never overlap.\n  function placeStickyContact(){\n    const sc = document.getElementById('sticky-contact');\n    const zc = document.querySelector('.leaflet-control-zoom');\n    if(!sc || !zc) return;\n    const r = zc.getBoundingClientRect();\n    if(!r.height) return;\n    const bottom = Math.round(window.innerHeight - r.top + 12);\n    sc.style.setProperty('bottom', bottom + 'px', 'important');\n    sc.style.setProperty('right', Math.round(window.innerWidth - r.right) + 'px', 'important');\n  }\n  [0, 300, 1000].forEach(d=> setTimeout(placeStickyContact, d));\n  window.addEventListener('load', placeStickyContact);\n  window.addEventListener('resize', placeStickyContact);\n  window.addEventListener('orientationchange', ()=> setTimeout(placeStickyContact, 300));\n  // Only one card (project popup, infra popup, or road popup) open at a\n  // time — opening a new one always closes whichever was open before it,\n  // no matter which marker/layer it came from. This is enforced centrally\n  // here (map-level 'popupopen') rather than per-marker, so it works even\n  // though individual popups use autoClose:false/closeOnClick:false (that\n  // setting only stops Leaflet's own default auto-close; it's needed so a\n  // popup doesn't vanish mid-hover or mid-tap while someone is interacting\n  // with the Enquire/WhatsApp/Share buttons inside it).\n  let activePopup = null;\n  map.on('popupopen', (e)=>{\n    // Hard guarantee: close every other open card/popup on the map.\n    map.eachLayer(l=>{\n      if(l.getPopup && l.isPopupOpen && l.isPopupOpen() && l.getPopup() !== e.popup) l.closePopup();\n    });\n    if(activePopup && activePopup !== e.popup){\n      const prevSource = activePopup._source;\n      if(prevSource && typeof prevSource.closePopup === 'function') prevSource.closePopup();\n      else map.closePopup(activePopup);\n    }\n    activePopup = e.popup;\n    document.body.classList.toggle('card-open', !!(e.popup.options.className || '').includes('teaser-popup'));\n  });\n  // Clicking/tapping anywhere outside the card closes it; on touch devices\n  // dragging the map does too.\n  function closeActiveCard(){\n    if(!activePopup) return;\n    const src = activePopup._source;\n    if(src && typeof src.closePopup === 'function') src.closePopup();\n    else map.closePopup(activePopup);\n  }\n  // Cards open on click, so clicking the empty map closes them on every device.\n  map.on('click', closeActiveCard);\n  if(window.matchMedia('(hover: none)').matches){\n    map.on('dragstart', closeActiveCard);\n  }\n  map.on('popupclose', (e)=>{\n    if(activePopup === e.popup){ activePopup = null; document.body.classList.remove('card-open'); }\n  });\n  // Loads the official YouTube IFrame Player API once (needed for reliable\n  // onError detection — see the popupopen handler in renderPin below).\n  // Anything that needs the API calls whenYouTubeApiReady(fn); fn runs\n  // immediately if the API is already loaded, or queues until it is.\n  let ytApiReady = false;\n  let ytApiReadyQueue = [];\n  function whenYouTubeApiReady(cb){\n    if(ytApiReady && window.YT && window.YT.Player) cb();\n    else ytApiReadyQueue.push(cb);\n  }\n  window.onYouTubeIframeAPIReady = function(){\n    ytApiReady = true;\n    ytApiReadyQueue.forEach(cb=> cb());\n    ytApiReadyQueue = [];\n  };\n  (function loadYouTubeApi(){\n    if(window.YT && window.YT.Player){ ytApiReady = true; return; }\n    const tag = document.createElement('script');\n    tag.src = 'https://www.youtube.com/iframe_api';\n    document.head.appendChild(tag);\n  })();\n  const baseTiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n    attribution: '&copy; <a href=\"https://www.openstreetmap.org/copyright\" target=\"_blank\" rel=\"noopener\">OpenStreetMap</a> contributors',\n    subdomains: 'abc', maxZoom: 20, maxNativeZoom: 19\n  }).addTo(map);\n  const satTiles = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {\n    attribution: 'Imagery &copy; Esri, Maxar, Earthstar Geographics',\n    maxZoom: 20\n  });\n  let basemapMode = 'map';\n  const basemapBtn = document.getElementById('btn-basemap');\n  const mapContainer = document.getElementById('map');\n  \n  function setBasemapMode(mode){\n    if(map.hasLayer(baseTiles)) map.removeLayer(baseTiles);\n    if(map.hasLayer(satTiles)) map.removeLayer(satTiles);\n    mapContainer.classList.remove('night-mode');\n    basemapBtn.classList.remove('on');\n    if(mode === 'map'){\n      baseTiles.addTo(map);\n      basemapBtn.innerHTML = '<span class=\"btn-icon\">🛰️</span><span class=\"btn-label\"> Satellite</span>';\n      basemapBtn.setAttribute('aria-label', 'Toggle satellite view');\n    } else if(mode === 'satellite'){\n      satTiles.addTo(map);\n      basemapBtn.innerHTML = '<span class=\"btn-icon\">🌙</span><span class=\"btn-label\"> Night</span>'; basemapBtn.classList.add('on');\n      basemapBtn.setAttribute('aria-label', 'Toggle night mode');\n    } else {\n      baseTiles.addTo(map);\n      mapContainer.classList.add('night-mode');\n      basemapBtn.innerHTML = '<span class=\"btn-icon\">🗺️</span><span class=\"btn-label\"> Map</span>'; basemapBtn.classList.add('on');\n      basemapBtn.setAttribute('aria-label', 'Switch to map view');\n    }\n    basemapMode = mode;\n  }\n  basemapBtn.addEventListener('click', ()=>{\n    setBasemapMode(basemapMode === 'map' ? 'satellite' : basemapMode === 'satellite' ? 'night' : 'map');\n  });\n\n  document.getElementById('loading').style.display = 'none';\n\n  // ---------- Collapsed logo/title panel ----------\n  // Starts as just the icon and expands only while hovered (desktop) or\n  // briefly after a tap (touch). To use a dedicated\n  // small icon instead of the full logo when collapsed, set\n  // COLLAPSED_LOGO_SRC below to that image's path — defaults to the same\n  // logo file if left as null.\n  const COLLAPSED_LOGO_SRC = '/img/mappingg-icon-mark.png';\n  const brandEl = document.querySelector('#topbar .brand');\n  const brandLogoImg = brandEl.querySelector('img.logo');\n  const brandFullLogoSrc = brandLogoImg.getAttribute('src');\n  let brandHasAutoHidden = false;\n  let brandTapRevealTimer = null;\n  let brandAutoCollapseTimer = null;\n\n  function collapseBrand(){\n    brandEl.classList.add('collapsed');\n    brandHasAutoHidden = true;\n    if(COLLAPSED_LOGO_SRC){ brandLogoImg.style.display = ''; brandLogoImg.src = COLLAPSED_LOGO_SRC; }\n  }\n  function expandBrand(){\n    if(window.matchMedia('(max-width:640px)').matches) return; // mobile stays a round badge\n    brandEl.classList.remove('collapsed');\n    if(COLLAPSED_LOGO_SRC) brandLogoImg.src = brandFullLogoSrc;\n  }\n  // Mobile: the badge is always the round icon mark from the start.\n  const mobileBrandMq = window.matchMedia('(max-width:640px)');\n  function isMobileBrand(){ return mobileBrandMq.matches; }\n  if(isMobileBrand()) collapseBrand();\n  mobileBrandMq.addEventListener('change', (ev)=>{\n    if(ev.matches) collapseBrand();\n    else if(!brandHasAutoHidden || !brandEl.matches(':hover')) expandBrand();\n  });\n  collapseBrand();\n\n  brandEl.addEventListener('mouseenter', ()=>{\n    clearTimeout(brandTapRevealTimer);\n    expandBrand();\n  });\n  brandEl.addEventListener('mouseleave', ()=>{\n    if(brandHasAutoHidden) collapseBrand();\n  });\n  brandEl.addEventListener('click', ()=>{\n    if(!brandEl.classList.contains('collapsed')) return;\n    expandBrand();\n    clearTimeout(brandTapRevealTimer);\n    brandTapRevealTimer = setTimeout(collapseBrand, 4000);\n  });\n\n  // ---------- Intro video (its own panel, separate from the logo) ----------\n  // Reads the YouTube link set in the admin panel's \"Tracking & SEO\" panel\n  // (youtube_video_url on map_settings). 3 seconds after the page loads,\n  // a separate panel appears next to the logo and autoplays it muted\n  // (browsers block unmuted autoplay). The panel hides again once the\n  // video ends. The logo itself is never touched. If no link is set,\n  // nothing appears.\n  function extractYouTubeId(url){\n    if(!url) return null;\n    try{\n      const u = new URL(url.trim());\n      if(u.hostname.replace('www.','') === 'youtu.be') return u.pathname.slice(1).split('/')[0] || null;\n      const v = u.searchParams.get('v');\n      if(v) return v;\n      const embedMatch = u.pathname.match(/\\/embed\\/([^/?]+)/);\n      if(embedMatch) return embedMatch[1];\n      const shortsMatch = u.pathname.match(/\\/shorts\\/([^/?]+)/);\n      if(shortsMatch) return shortsMatch[1];\n    }catch(e){ /* not a valid URL */ }\n    return null;\n  }\n\n  function setupIntroVideo(videoId){\n    if(!videoId) return;\n    const introPanel = document.getElementById('intro-video-panel');\n    const videoFrame = document.getElementById('brand-video-frame');\n    let reverted = false;\n\n    function hidePanel(){\n      if(reverted) return;\n      reverted = true;\n      introPanel.classList.remove('show');\n      videoFrame.innerHTML = '';\n    }\n\n    function playIntroVideo(){\n      introPanel.classList.add('show');\n      // Plain iframe — no YT API, guaranteed fills the container\n      videoFrame.innerHTML = `<iframe\n        src=\"https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=1&controls=1&rel=0&playsinline=1&modestbranding=1\"\n        allow=\"autoplay; encrypted-media; picture-in-picture\"\n        allowfullscreen\n        referrerpolicy=\"strict-origin-when-cross-origin\"\n        title=\"Intro video\"\n        style=\"position:absolute;inset:0;width:100%;height:100%;border:0;display:block;\"></iframe>`;\n    }\n\n    setTimeout(playIntroVideo, 3000);\n  }\n\n  if(window.__mapSettingsReady){\n    window.__mapSettingsReady.then(function(settings){\n      setupIntroVideo(extractYouTubeId(settings && settings.youtube_video_url));\n    });\n  }\n\n  // ---------- Collapsed legend panel ----------\n  // Mirrors the brand/logo panel above: shows just the \"Legend\" title and\n  // opens only while hovered (desktop). On touch, a tap opens it and it\n  // re-collapses a few seconds later (or when tapped shut again).\n  const legendEl = document.getElementById('legend');\n  let legendHasAutoHidden = false;\n  let legendAutoTimer = null;\n  let legendTapCollapseTimer = null;\n\n  function collapseLegend(){\n    legendEl.classList.add('collapsed');\n    legendHasAutoHidden = true;\n  }\n  function expandLegend(){\n    legendEl.classList.remove('collapsed');\n  }\n\n  collapseLegend();\n  legendEl.addEventListener('mouseenter', ()=>{\n    if(window.matchMedia('(hover: none)').matches) return;\n    clearTimeout(legendTapCollapseTimer);\n    expandLegend();\n  });\n  legendEl.addEventListener('mouseleave', ()=>{\n    if(window.matchMedia('(hover: none)').matches) return;\n    collapseLegend();\n  });\n\n  document.getElementById('legend-toggle').addEventListener('click', ()=>{\n    if(!window.matchMedia('(hover: none)').matches) return;\n    clearTimeout(legendAutoTimer);\n    clearTimeout(legendTapCollapseTimer);\n    if(legendEl.classList.contains('collapsed')){\n      expandLegend();\n      // Was auto-hidden before (or is the mobile default) — re-collapse\n      // shortly after being opened again, same as the logo panel.\n      if(legendHasAutoHidden) legendTapCollapseTimer = setTimeout(collapseLegend, 4000);\n    } else {\n      collapseLegend();\n    }\n  });\n\n  const searchInputEl = document.getElementById('search-input');\n  if(window.innerWidth <= 480){\n    searchInputEl.placeholder = '🔍 Search projects';\n  }\n\n  // ---------- Fast, fail-safe pin loading ----------\n  // Every project logo is stored inside the pins table as a large image,\n  // so \"select *\" pulls ~5 MB in one query. Supabase cancels public\n  // queries after 3 seconds, so on a busy moment (e.g. right after an\n  // admin edit) that single query failed and the map showed NO pins.\n  // Now: all pin details load first in one small, fast query (pins appear\n  // at once), then logos/brochures load in small batches and fill in.\n  // A failed load never wipes the pins already on the map — it retries.\n  const PIN_LIGHT_COLS = 'id,number,lat,lng,title,type,status,price,description,updated_at,configuration,sqft,possession_timeline,launch_date,whats_available,highlighted,size,developer,location,key_usp,field_visibility,custom_fields,youtube_video_url,brochure_width,brochure_height,rera_number';\n  const pinImageCache = new Map();   // id -> { updated_at, image, brochure_image }\n  const wait = (ms)=> new Promise(r=> setTimeout(r, ms));\n  async function fetchPinsLight(){\n    for(let attempt = 0; attempt < 4; attempt++){\n      const { data, error } = await sb.from('pins').select(PIN_LIGHT_COLS);\n      if(!error && Array.isArray(data)) return data;\n      await wait(800 * (attempt + 1));\n    }\n    return null;\n  }\n  function applyCachedImages(list){\n    list.forEach(p=>{\n      const c = pinImageCache.get(p.id);\n      if(c && c.updated_at === p.updated_at){ p.image = c.image; p.brochure_image = c.brochure_image; }\n    });\n  }\n  async function fetchImageBatch(ids){\n    for(let attempt = 0; attempt < 4; attempt++){\n      const { data, error } = await sb.from('pins').select('id,updated_at,image,brochure_image').in('id', ids);\n      if(!error && Array.isArray(data)) return data;\n      await wait(800 * (attempt + 1));\n    }\n    // Batch still failing: fetch those logos one by one so none are skipped.\n    const rows = [];\n    for(const id of ids){\n      for(let attempt = 0; attempt < 3; attempt++){\n        const { data, error } = await sb.from('pins').select('id,updated_at,image,brochure_image').eq('id', id);\n        if(!error && Array.isArray(data)){ rows.push(...data); break; }\n        await wait(1000 * (attempt + 1));\n      }\n    }\n    return rows;\n  }\n  // Retry wrapper for the small tables (infra, roads, settings, areas).\n  async function withRetry(makeQuery, tries){\n    let res;\n    for(let attempt = 0; attempt < (tries || 4); attempt++){\n      res = await makeQuery();\n      if(!res.error) return res;\n      await wait(800 * (attempt + 1));\n    }\n    return res;\n  }\n  function refreshPinMarker(pin){\n    const m = pinMarkerMap.get(pin.id);\n    if(!m){ return; }\n    const redo = ()=>{\n      const cur = pins.find(p => p.id === pin.id);\n      if(!cur) return;\n      const old = pinMarkerMap.get(cur.id);\n      const wasShown = old ? map.hasLayer(old) : true;\n      renderPin(cur);\n      if(!wasShown){ const nm = pinMarkerMap.get(cur.id); if(nm) map.removeLayer(nm); }\n    };\n    if(m.isPopupOpen && m.isPopupOpen()) m.once('popupclose', ()=> setTimeout(redo, 50));\n    else redo();\n  }\n  async function loadPinImages(list){\n    const need = list.filter(p=>{ const c = pinImageCache.get(p.id); return !(c && c.updated_at === p.updated_at); });\n    const chunks = [];\n    for(let i = 0; i < need.length; i += 8) chunks.push(need.slice(i, i + 8).map(p => p.id));\n    let next = 0;\n    async function worker(){\n      while(next < chunks.length){\n        const rows = await fetchImageBatch(chunks[next++]);\n        rows.forEach(r=>{\n          pinImageCache.set(r.id, { updated_at:r.updated_at, image:r.image, brochure_image:r.brochure_image });\n          const pin = pins.find(p => p.id === r.id);\n          if(!pin || (!r.image && !r.brochure_image)) return;\n          pin.image = r.image; pin.brochure_image = r.brochure_image;\n          refreshPinMarker(pin);\n        });\n      }\n    }\n    await Promise.all([worker(), worker(), worker()]);\n    // Final check: any pin still without its logo gets another try shortly.\n    const missing = pins.filter(p => !p.image && !pinImageCache.has(p.id));\n    if(missing.length) setTimeout(()=> loadPinImages(missing), 4000);\n  }\n\n  const [pinsRes, infraRes, roadsRes, settingsRes, customTypesRes, areaBoundariesRes] = await Promise.all([\n    fetchPinsLight().then(data => ({ data })),\n    withRetry(()=> sb.from('infra_markers').select('*')),\n    withRetry(()=> sb.from('roads').select('*')),\n    withRetry(()=> sb.from('map_settings').select('*').eq('id', 1).single()),\n    withRetry(()=> sb.from('infra_types').select('*')),\n    withRetry(()=> sb.from('area_boundaries').select('*'), 2)\n  ]);\n  let pins = pinsRes.data || [];\n  const bridges = infraRes.data || [];\n  const roads = roadsRes.data || [];\n  const boundary = (settingsRes.data && settingsRes.data.boundary) || [];\n  // Locality boundaries (Kharadi, Viman Nagar, NIBM/Lullanagar, Solapur\n  // Highway...) drawn in the admin tool. Falls back to an empty list if the\n  // table doesn't exist yet, so search still works via the hardcoded\n  // fallback centres further below.\n  const areaBoundaries = (!areaBoundariesRes.error && areaBoundariesRes.data) ? areaBoundariesRes.data : [];\n\n  // Merge in any custom infra types (e.g. \"Helipad\") before anything renders, so\n  // markers and roads pick up the right label/emoji/colour on the very first paint.\n  if(!customTypesRes.error && customTypesRes.data){\n    customTypesRes.data.forEach(ct => registerCustomInfraType(ct));\n  }\n\n  if(boundary.length > 1){\n    L.polygon(boundary, {color:'#e74c3c', weight:2, dashArray:'5,5', fillColor:'#e74c3c', fillOpacity:0.03, interactive:false}).addTo(map);\n  }\n\n  // ---------- TV / big-screen recentring ----------\n  // Root cause of the \"scattered on load, fixes itself after +/-\" issue:\n  // Leaflet measures its container size the instant it initialises. On some\n  // TV/smart-display browsers the on-screen container finishes resizing a\n  // beat *after* that, so tiles get positioned against a stale size until\n  // something (like clicking zoom) forces Leaflet to remeasure. We fix that\n  // two ways below: automatically (no input needed) and via a manual button.\n\n  // Shows the whole radius map — boundary if we have one, else every pin —\n  // centred and fully framed. Kept as the fallback for kiosk/TV screens and\n  // for anyone who declines location access on \"Near Me\".\n  function fitFullMap(){\n    map.invalidateSize(true);\n    let bounds = null;\n    if(boundary.length > 1){\n      bounds = L.latLngBounds(boundary);\n    } else if(pins.length){\n      bounds = L.latLngBounds(pins.map(p => [p.lat, p.lng]));\n    }\n    if(bounds && bounds.isValid()){\n      map.fitBounds(bounds, {padding:[60,60]});\n    } else {\n      map.setView([18.5215, 73.9140], 14.5);\n    }\n  }\n\n  // Just remeasures the container and redraws tiles in place, without\n  // moving/zooming the view the visitor already has. Safe to call silently\n  // and repeatedly.\n  function silentResync(){ map.invalidateSize(false); }\n\n  // ---------- \"Near Me\" (the headline feature) ----------\n  // Uses the visitor's real GPS location to surface whichever of our pins\n  // are actually close to them — a far stronger hook than a generic\n  // \"browse listings\" button. Falls back to the old full-radius recenter\n  // (fitFullMap) if location is denied/unavailable, so TV/kiosk screens\n  // (which won't have GPS) keep working exactly as before.\n  const nearMeBtn = document.getElementById('btn-near-me');\n  let meMarker = null;\n  const NEAR_ME_RADIUS_KM = 6;\n\n  function haversineKm(lat1, lng1, lat2, lng2){\n    const R = 6371, toRad = d => d * Math.PI / 180;\n    const dLat = toRad(lat2 - lat1), dLng = toRad(lng2 - lng1);\n    const a = Math.sin(dLat/2)**2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng/2)**2;\n    return R * 2 * Math.asin(Math.sqrt(a));\n  }\n\n  function placeMeMarker(lat, lng){\n    if(meMarker) map.removeLayer(meMarker);\n    meMarker = L.marker([lat, lng], {\n      icon: L.divIcon({ className:'', html:'<div class=\"me-pulse\"></div>', iconSize:[16,16], iconAnchor:[8,8] }),\n      zIndexOffset: 1000\n    }).addTo(map);\n  }\n\n  function showNearMe(lat, lng){\n    placeMeMarker(lat, lng);\n\n    pins.forEach(pin=>{\n      const marker = pinMarkerMap.get(pin.id);\n      if(marker && !map.hasLayer(marker)) marker.addTo(map);\n    });\n\n    const nearby = pins\n      .map(p => ({ pin:p, dist: haversineKm(lat, lng, p.lat, p.lng) }))\n      .filter(x => x.dist <= NEAR_ME_RADIUS_KM)\n      .sort((a,b) => a.dist - b.dist);\n\n    if(nearby.length){\n      const bounds = L.latLngBounds([[lat,lng], ...nearby.map(x => [x.pin.lat, x.pin.lng])]);\n      map.flyToBounds(bounds, { padding:[90,90], maxZoom:16, animate:true, duration:1.1 });\n      showAreaToast(`${nearby.length} project${nearby.length > 1 ? 's' : ''} near you`);\n      const closest = pinMarkerMap.get(nearby[0].pin.id);\n      // Just highlight the closest pin — its card opens only when clicked.\n      if(closest) setTimeout(()=> pulseMarker(closest), 900);\n    } else {\n      map.flyTo([lat, lng], 14, { animate:true, duration:1.1 });\n      showAreaToast('No projects near you yet — showing your location');\n    }\n  }\n\n  // Single shared entry point for \"find the visitor's location and show\n  // their nearby projects\" — used both by the Near Me button (with visible\n  // Locating… UI) and automatically on page load (silently, so a first-time\n  // visitor doesn't have to find/tap the button themselves).\n  function locateAndShowNearby(opts){\n    opts = opts || {};\n    if(!navigator.geolocation){\n      if(opts.onSettled) opts.onSettled();\n      fitFullMap();\n      return;\n    }\n    if(opts.onStart) opts.onStart();\n    navigator.geolocation.getCurrentPosition(\n      (pos)=>{\n        if(opts.onSettled) opts.onSettled();\n        showNearMe(pos.coords.latitude, pos.coords.longitude);\n      },\n      ()=>{\n        // Denied, unavailable, or timed out — fall back to the full-radius\n        // view. Kept quiet on the automatic page-load attempt (a toast\n        // about denied location before the visitor has done anything\n        // feels intrusive); the manual button click still explains itself.\n        if(opts.onSettled) opts.onSettled();\n        if(!opts.silent) showAreaToast('Location unavailable — showing full map');\n        fitFullMap();\n      },\n      { enableHighAccuracy:true, timeout:8000, maximumAge:120000 }\n    );\n  }\n\n  if(nearMeBtn){\n    nearMeBtn.addEventListener('click', ()=>{\n      nearMeBtn.classList.add('locating');\n      const labelEl = nearMeBtn.querySelector('.btn-label');\n      const origLabel = labelEl ? labelEl.textContent : nearMeBtn.textContent;\n      if(labelEl) labelEl.textContent = ' Locating…'; else nearMeBtn.textContent = '📍 Locating…';\n      locateAndShowNearby({\n        onSettled: ()=>{\n          nearMeBtn.classList.remove('locating');\n          if(labelEl) labelEl.textContent = origLabel; else nearMeBtn.textContent = origLabel;\n        }\n      });\n    });\n  }\n\n  // Locality dropdown is populated further below, once knownAreas (from\n  // Supabase) is available — see \"Locality dropdown\" near the area-search code.\n\n  // ?tv=1 in the URL = kiosk/TV mode: bigger, more remote-friendly controls.\n  // (The full-radius-map default view below now applies to every visitor,\n  // not just TV — ?tv=1 just adjusts sizing for a big screen.)\n  const isTvMode = new URLSearchParams(window.location.search).get('tv') === '1';\n  if(isTvMode){\n    document.body.classList.add('tv-mode');\n  }\n\n  // Automatic fix: remeasure a few times as the TV's browser finishes\n  // laying out the page and loading fonts/images. This runs regardless of\n  // TV mode so it also helps anyone on an unusual screen/resize situation.\n  [300, 900, 1800, 3200].forEach(delay => setTimeout(silentResync, delay));\n  window.addEventListener('load', silentResync);\n  window.addEventListener('resize', silentResync);\n  window.addEventListener('orientationchange', silentResync);\n  if(document.fonts && document.fonts.ready){\n    document.fonts.ready.then(silentResync);\n  }\n\n  const pinScale = {small:0.78, medium:1, large:1.35};\n  // If a pin/infra image URL is broken or missing on the server, this swaps\n  // it out cleanly instead of leaving the browser's ugly broken-image icon\n  // showing on the map.\n  window.__pinImgFallback = function(imgEl){ imgEl.style.display = 'none'; };\n  window.__infraImgFallback = function(imgEl, emoji){ imgEl.outerHTML = emoji; };\n  // Markers are ~32px, so they load a small cached thumbnail of the pin's\n  // image (served by /api/pins/[id]/image) instead of the full-size file.\n  const pinThumb = (src)=> (typeof src === 'string' && src.startsWith('/api/media/')) ? src + '&w=96' : src;\n  function pinIcon(pin){\n    const color = statusColor[pin.status] || statusColor.available;\n    const head = pin.image ? `<img src=\"${pinThumb(pin.image)}\" alt=\"\" onerror=\"window.__pinImgFallback(this)\">` : `<span class=\"glyph\">●</span>`;\n    const radar = pin.highlighted ? '<div class=\"pin-radar\"></div>' : '';\n    const scale = pinScale[pin.size] || 1;\n    return L.divIcon({\n      className:'',\n      html:`<div class=\"pin-wrap\" style=\"color:${color};transform:scale(${scale});\">\n              ${radar}\n              <div class=\"pin-circle\" style=\"background:${color};\">${head}</div>\n              <div class=\"pin-tail\"></div>\n              <div class=\"pin-ground\"></div>\n            </div>`,\n      iconSize:[32,44], iconAnchor:[16,44], popupAnchor:[0,-42]\n    });\n  }\n  function infraIcon(b){\n    const meta = infraTypes[b.type] || infraTypes.bridge;\n    const sz = infraSizes[b.size] || infraSizes.medium;\n    const color = b.color || meta.color;\n    const inner = b.icon_image ? `<img src=\"${pinThumb(b.icon_image)}\" alt=\"\" onerror=\"window.__infraImgFallback(this, '${meta.emoji}')\">` : meta.emoji;\n    return L.divIcon({\n      className:'',\n      html:`<div class=\"infra-pin\" style=\"width:${sz.px}px;height:${sz.px}px;background:${color};font-size:${sz.font}px;\">${inner}</div>`,\n      iconSize:[sz.px, sz.px], iconAnchor:[sz.px/2, sz.px], popupAnchor:[0, -sz.px+2]\n    });\n  }\n\n  const pinMarkerMap = new Map();\n  // On touch devices there's no real \"hover\" — a tap can synthesize\n  // mouseout/mouseleave as the finger lifts, which was closing the popup\n  // out from under anyone trying to tap Enquire/WhatsApp/Share inside it.\n  // On these devices the popup only closes via its own × button or by\n  // tapping elsewhere on the map — never on a hover-out timer.\n  const isTouchDevice = window.matchMedia('(hover: none)').matches;\n  // Business WhatsApp number for the card's WhatsApp button — country code +\n  // number, digits only, no + or spaces (e.g. '919876543210'). Replace this\n  // before going live; until then the button links to a placeholder number.\n  // Desktop only: shifts an open card so it stays fully visible (clear of\n  // the top bar and screen edges) WITHOUT moving the map.\n  function fitCardInView(popup){\n    const el = popup && popup._container;\n    if(!el) return;\n    // _updatePosition only moves the card — it doesn't re-render content,\n    // so buttons, listeners and the video player stay intact.\n    popup.options.offset = [0, -2];\n    popup._updatePosition();\n    const mapRect = map.getContainer().getBoundingClientRect();\n    const topbar = document.getElementById('topbar');\n    const topLimit = Math.max(mapRect.top + 10, topbar ? topbar.getBoundingClientRect().bottom + 10 : 0);\n    const bottomLimit = mapRect.bottom - 10, leftLimit = mapRect.left + 10, rightLimit = mapRect.right - 10;\n    const r = el.getBoundingClientRect();\n    let dx = 0, dy = 0;\n    if(r.right > rightLimit) dx = rightLimit - r.right;\n    if(r.left + dx < leftLimit) dx = leftLimit - r.left;\n    if(r.bottom > bottomLimit) dy = bottomLimit - r.bottom;\n    if(r.top + dy < topLimit) dy = topLimit - r.top;\n    if(dx || dy){\n      popup.options.offset = [dx, -2 + dy];\n      popup._updatePosition();\n    }\n  }\n  const WHATSAPP_NUMBER = '918228828200';\n  // Call number for the sticky Call button — same format as above, no + or\n  // spaces. Defaults to the WhatsApp number; change independently if your\n  // sales line is different.\n  const CALL_NUMBER = '918228828200';\n  document.getElementById('sticky-whatsapp').href =\n    `https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(\"Hi, I'm interested in a project on Mappingg.com. Could you share more details?\")}`;\n  document.getElementById('sticky-call').href = `tel:+${CALL_NUMBER}`;\n  document.getElementById('sticky-whatsapp').addEventListener('click', ()=> trackEvent('lead', 'sticky_whatsapp_click', 'sticky-button'));\n  document.getElementById('sticky-call').addEventListener('click', ()=> trackEvent('lead', 'sticky_call_click', 'sticky-button'));\n  function whatsappUrl(pin){\n    const msg = encodeURIComponent(`Hi, I'm interested in ${pin.title || 'this project'} on the Mappingg.com. Could you share more details?`);\n    return `https://wa.me/${WHATSAPP_NUMBER}?text=${msg}`;\n  }\n  // Display-only field visibility — purely what the admin set for this\n  // project in the editor's \"Public Card Visibility\" panel. No toggle\n  // controls appear here; visitors just see the card as configured.\n  // ---------- Compare cart (buyer accounts) ----------\n  // Signed-in buyers can add up to COMPARE_MAX projects from their cards and\n  // compare them side by side. The cart is saved on their account\n  // (/api/my/compare) so it follows them to other devices. Only the fields\n  // the public card already shows are compared (and only where the admin\n  // left them visible) — description etc. stay behind the enquiry form.\n  // Other roles never see any of this.\n  var compareEnabled = false;\n  var compareIds = [];\n  var COMPARE_MAX = 4;\n  const COMPARE_ICON = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 3v18M7 21h10M4 7h16M4 7l-3 7a3 3 0 0 0 6 0zM20 7l-3 7a3 3 0 0 0 6 0z\"/></svg>';\n\n  const compareBar = document.createElement('button');\n  compareBar.type = 'button';\n  compareBar.id = 'compare-bar';\n  compareBar.hidden = true;\n  document.body.appendChild(compareBar);\n  compareBar.addEventListener('click', ()=> openCompare());\n\n  const compareModal = document.createElement('div');\n  compareModal.id = 'compare-modal';\n  compareModal.setAttribute('role', 'dialog');\n  compareModal.setAttribute('aria-modal', 'true');\n  compareModal.setAttribute('aria-label', 'Compare projects');\n  compareModal.hidden = true;\n  document.body.appendChild(compareModal);\n\n  let compareToastTimer = null;\n  function compareToast(text){\n    const el = document.getElementById('area-toast');\n    if(!el) return;\n    el.textContent = text;\n    el.classList.add('show');\n    clearTimeout(compareToastTimer);\n    compareToastTimer = setTimeout(()=> el.classList.remove('show'), 2600);\n  }\n\n  function comparePins(){ return compareIds.map(id => pins.find(p => p.id === id)).filter(Boolean); }\n\n  function compareBtnHtml(id){\n    const on = compareIds.includes(id);\n    const label = on ? 'Remove from compare' : 'Add to compare';\n    return `<button class=\"tc-compare${on ? ' on' : ''}\" data-compare-pin=\"${id}\" type=\"button\" aria-pressed=\"${on}\" aria-label=\"${label}\" title=\"${label}\">${COMPARE_ICON}<span class=\"tc-compare-label\">${on ? 'Added' : 'Compare'}</span></button>`;\n  }\n\n  function refreshCompareUI(){\n    document.querySelectorAll('button[data-compare-pin]').forEach(btn=>{\n      const on = compareIds.includes(btn.dataset.comparePin);\n      const label = on ? 'Remove from compare' : 'Add to compare';\n      btn.classList.toggle('on', on);\n      btn.setAttribute('aria-pressed', String(on));\n      btn.setAttribute('aria-label', label);\n      btn.title = label;\n      const l = btn.querySelector('.tc-compare-label');\n      if(l) l.textContent = on ? 'Added' : 'Compare';\n    });\n    const n = comparePins().length;\n    compareBar.hidden = !compareEnabled || n === 0;\n    compareBar.innerHTML = `${COMPARE_ICON}<span>Compare</span><span class=\"cb-count\">${n}/${COMPARE_MAX}</span>`;\n    compareBar.setAttribute('aria-label', `Compare ${n} selected project${n === 1 ? '' : 's'}`);\n    if(!compareModal.hidden) renderCompare();\n  }\n\n  // One save in flight at a time: quick add/remove taps would otherwise race,\n  // and an older reply (or an older write on the server) could undo a newer\n  // change. Changes made mid-save are sent as soon as it finishes.\n  let compareSaving = null;\n  let compareDirty = false;\n  function saveCompare(){\n    if(compareSaving){ compareDirty = true; return compareSaving; }\n    compareSaving = fetch('/api/my/compare', {\n      method:'PUT', credentials:'same-origin',\n      headers:{ 'Content-Type':'application/json' },\n      body: JSON.stringify({ pins: compareIds })\n    })\n      .then(r => r.ok ? r.json() : Promise.reject(r))\n      .then(d => { if(!compareDirty && d && Array.isArray(d.pins)){ compareIds = d.pins; refreshCompareUI(); } })\n      .catch(()=> compareToast('Could not save your compare list — please try again'))\n      .finally(()=>{\n        compareSaving = null;\n        if(compareDirty){ compareDirty = false; saveCompare(); }\n      });\n    return compareSaving;\n  }\n\n  function toggleCompare(id){\n    if(compareIds.includes(id)){\n      compareIds = compareIds.filter(x => x !== id);\n      compareToast('Removed from compare');\n    } else {\n      if(compareIds.length >= COMPARE_MAX){\n        compareToast(`You can compare up to ${COMPARE_MAX} projects — remove one first`);\n        openCompare();\n        return;\n      }\n      compareIds = compareIds.concat(id);\n      const p = pins.find(x => x.id === id);\n      trackEvent('compare', 'add', p ? (p.title || ('#' + p.number)) : id);\n      const n = compareIds.length;\n      compareToast(n >= 2 ? `Added — ${n} projects ready to compare` : 'Added to compare — add another project to compare');\n    }\n    refreshCompareUI();\n    saveCompare();\n  }\n\n  // One row per compared attribute. A value only shows when the project has it\n  // and the admin hasn't hidden that field on its public card.\n  const COMPARE_ROWS = [\n    { label:'Developer', field:'developer', val: p => p.developer },\n    { label:'Location', field:'location', val: p => p.location },\n    { label:'Status', field:'status', html: p => `<span class=\"cmp-status\" style=\"background:${statusColor[p.status] || statusColor.available};\">${statusLabel[p.status] || 'Available'}</span>` },\n    { label:'Property Type', field:'type', val: p => p.type },\n    { label:'MahaRERA No.', field:'rera', val: p => p.rera_number },\n    { label:'Configuration', field:'configuration', val: p => p.configuration },\n    { label:'Carpet Area', field:'configuration', val: p => p.sqft },\n    { label:'Starting Price', field:'price', val: p => p.price },\n    { label:'Possession / Launch', field:'possession', val: p => p.status === 'upcoming' ? (p.launch_date ? 'Launching: ' + p.launch_date : '') : p.possession_timeline },\n    { label:'Key USP', field:'key_usp', val: p => p.key_usp },\n    { label:'Project Video', html: p => { const v = extractYouTubeId(p.youtube_video_url); return v ? `<a class=\"cmp-video\" href=\"https://www.youtube.com/watch?v=${v}\" target=\"_blank\" rel=\"noopener\">▶ Watch video</a>` : ''; } },\n  ];\n\n  function renderCompare(){\n    const list = comparePins();\n    const head = `<div class=\"cmp-head\"><h2>Compare projects</h2><button type=\"button\" class=\"cmp-close\" data-cmp-close aria-label=\"Close\">&times;</button></div>`;\n    if(!list.length){\n      compareModal.innerHTML = `<div class=\"cmp-panel\">${head}<div class=\"cmp-emptystate\">Your compare list is empty.<br>Open a project card and tap <strong>Compare</strong> to add it.</div></div>`;\n      return;\n    }\n    const dash = '<span class=\"cmp-empty-val\">—</span>';\n    const cell = (p, row)=>{\n      if(row.field && !isFieldOn(p, row.field)) return dash;\n      if(row.html){ const h = row.html(p); return h || dash; }\n      const v = row.val(p);\n      return v ? escapeHtml(String(v)) : dash;\n    };\n    let rows = COMPARE_ROWS\n      .filter(row => list.some(p => cell(p, row) !== dash))\n      .map(row => `<tr><th scope=\"row\">${row.label}</th>${list.map(p => `<td>${cell(p, row)}</td>`).join('')}</tr>`)\n      .join('');\n    // Admin-added custom fields, matched across projects by their label.\n    const customLabels = [];\n    list.forEach(p => (Array.isArray(p.custom_fields) ? p.custom_fields : []).forEach(cf=>{\n      if(cf && cf.visible !== false && cf.label && cf.value && !customLabels.includes(cf.label)) customLabels.push(cf.label);\n    }));\n    customLabels.forEach(label=>{\n      rows += `<tr><th scope=\"row\">${escapeHtml(label)}</th>${list.map(p=>{\n        const cf = (Array.isArray(p.custom_fields) ? p.custom_fields : []).find(x => x && x.visible !== false && x.label === label && x.value);\n        return `<td>${cf ? escapeHtml(String(cf.value)) : dash}</td>`;\n      }).join('')}</tr>`;\n    });\n    rows += `<tr class=\"cmp-actions-row\"><th scope=\"row\"></th>${list.map(p => `<td><div class=\"cmp-actions\">\n        <button type=\"button\" class=\"primary\" data-cmp-view=\"${p.id}\">View on map</button>\n        ${isFieldOn(p, 'enquire') ? `<button type=\"button\" data-cmp-enquire=\"${p.id}\">Enquire</button>` : ''}\n      </div></td>`).join('')}</tr>`;\n\n    const projHead = list.map(p => `<th scope=\"col\"><div class=\"cmp-proj\">\n        ${p.image ? `<img src=\"${p.image}\" alt=\"\" onerror=\"this.style.display='none'\">` : ''}\n        <span class=\"cmp-title\">${escapeHtml(p.title || 'Untitled project')}</span>\n        <button type=\"button\" class=\"cmp-remove\" data-cmp-remove=\"${p.id}\" aria-label=\"Remove ${escapeHtml(p.title || 'project')} from compare\" title=\"Remove\">&times;</button>\n      </div></th>`).join('');\n    const note = list.length < 2 ? '<div class=\"cmp-note\">Add at least one more project to compare side by side.</div>' : '';\n    compareModal.innerHTML = `<div class=\"cmp-panel\">${head}${note}\n      <div class=\"cmp-scroll\"><table class=\"cmp-table\" style=\"--cols:${list.length}\">\n        <thead><tr><th scope=\"col\"><span class=\"cmp-count\">${list.length} of ${COMPARE_MAX}</span></th>${projHead}</tr></thead>\n        <tbody>${rows}</tbody>\n      </table></div>\n      <div class=\"cmp-foot\"><span>Tap <strong>Compare</strong> on any project card to add it.</span><button type=\"button\" data-cmp-clear>Clear all</button></div>\n    </div>`;\n  }\n\n  function openCompare(){\n    if(!compareEnabled) return;\n    renderCompare();\n    compareModal.hidden = false;\n    document.body.classList.add('compare-open');\n    const closeBtn = compareModal.querySelector('[data-cmp-close]');\n    if(closeBtn) closeBtn.focus();\n  }\n  function closeCompare(){\n    compareModal.hidden = true;\n    document.body.classList.remove('compare-open');\n  }\n\n  compareModal.addEventListener('click', (ev)=>{\n    const t = ev.target;\n    if(t === compareModal || t.closest('[data-cmp-close]')){ closeCompare(); return; }\n    const rm = t.closest('[data-cmp-remove]');\n    if(rm){ toggleCompare(rm.dataset.cmpRemove); return; }\n    if(t.closest('[data-cmp-clear]')){ compareIds = []; refreshCompareUI(); saveCompare(); return; }\n    const view = t.closest('[data-cmp-view]');\n    if(view){\n      const p = pins.find(x => x.id === view.dataset.cmpView);\n      closeCompare();\n      if(!p) return;\n      map.flyTo([p.lat, p.lng], 16, { duration:1 });\n      setTimeout(()=>{ const m = pinMarkerMap.get(p.id); if(m){ m.openPopup(); pulseMarker(m); } }, 1100);\n      return;\n    }\n    const enq = t.closest('[data-cmp-enquire]');\n    if(enq){\n      const p = pins.find(x => x.id === enq.dataset.cmpEnquire);\n      closeCompare();\n      if(p){ trackEvent('lead', 'enquire_click', p.title || ('#' + p.number)); requestDetails(p); }\n    }\n  });\n  document.addEventListener('keydown', (ev)=>{ if(ev.key === 'Escape' && !compareModal.hidden) closeCompare(); });\n\n  // Turn the cart on only for signed-in buyers, then load their saved list.\n  fetch('/api/auth/session', { credentials:'same-origin' })\n    .then(r => r.json())\n    .then(body=>{\n      const u = body && body.session && body.session.user;\n      if(!u || u.role !== 'buyer') return;\n      compareEnabled = true;\n      return fetch('/api/my/compare', { credentials:'same-origin' })\n        .then(r => r.ok ? r.json() : null)\n        .then(d=>{\n          if(d && Array.isArray(d.pins)) compareIds = d.pins;\n          if(d && d.max) COMPARE_MAX = d.max;\n          refreshCompareUI();\n        });\n    })\n    .catch(()=>{ /* no cart if the session can't be read */ });\n\n  function isFieldOn(pin, fieldId){\n    const v = pin.field_visibility ? pin.field_visibility[fieldId] : undefined;\n    return v !== false; // visible unless the admin explicitly turned it off\n  }\n\n  function renderPin(pin){\n    if(pinMarkerMap.has(pin.id)) map.removeLayer(pinMarkerMap.get(pin.id));\n    const marker = L.marker([pin.lat, pin.lng], {icon: pinIcon(pin)}).addTo(map);\n    const color = statusColor[pin.status] || statusColor.available;\n    // Card layout: the project's logo/marker image sits in its own banner\n    // at the top of the popup. Further down, a \"Project Brochure\" section\n    // shows the uploaded brochure image — swapping to the project's\n    // YouTube video (muted autoplay) a few seconds after the popup opens,\n    // if one is set. See the popupopen / popupclose handlers below.\n    let pinVideoRevert = null;\n    const buildCardHtml = () => {\n    const logoHtml = pin.image ? `<div class=\"tc-logo-wrap\"><img src=\"${pin.image}\" alt=\"\"></div>` : '';\n    const videoId = extractYouTubeId(pin.youtube_video_url);\n    // Project Brochure section: if a video is set, it plays immediately\n    // (muted autoplay) when the popup opens — no image, no delay. The\n    // brochure image only shows when there's no video for this project.\n    // The box's shape defaults to widescreen (16:9) but the admin can set\n    // their own width/height (a ratio or exact pixels) per project.\n    const hasBrochure = !!(pin.brochure_image || videoId);\n    const brochureImgHtml = (!videoId && pin.brochure_image) ? `<img class=\"tc-brochure-image\" src=\"${pin.brochure_image}\" alt=\"\">` : '';\n    const brochureVideoHtml = videoId ? `<div class=\"tc-video-wrap\" data-video-id=\"${videoId}\"></div>` : '';\n    const brochureRatioStyle = (pin.brochure_width && pin.brochure_height)\n      ? ` style=\"height:${Math.round((pin.brochure_height / pin.brochure_width) * 100)}px;\"`\n      : '';\n    const brochureHtml = hasBrochure\n      ? `<div class=\"tc-brochure-label\">PROJECT VIDEO</div>\n         <div class=\"tc-brochure-media\"${brochureRatioStyle}>${brochureImgHtml}${brochureVideoHtml}</div>`\n      : '';\n    const compareBtn = compareEnabled ? compareBtnHtml(pin.id) : '';\n    const shareBtn = `<button class=\"tc-share\" data-share-pin=\"${pin.id}\" type=\"button\" aria-label=\"Share this project\" title=\"Share this project\">🔗<span class=\"tc-share-label\"> Share</span></button>`;\n\n    // The full \"Project Card\" field set. Project Name is the card header and\n    // always shows; everything else only appears if the project has data\n    // for it AND the admin hasn't hidden it in the editor.\n    const configValue = [pin.configuration, pin.sqft].filter(Boolean).join(' · ');\n    const possessionValue = pin.status === 'upcoming'\n      ? (pin.launch_date ? 'Launching: ' + pin.launch_date : '')\n      : (pin.possession_timeline ? 'Possession: ' + pin.possession_timeline : '');\n    const fields = [\n      pin.developer ? { id:'developer', label:'Developer', html:`<div class=\"tc-meta\">${escapeHtml(pin.developer)}</div>` } : null,\n      pin.location ? { id:'location', label:'Location', html:`<div class=\"tc-meta\">${escapeHtml(pin.location)}</div>` } : null,\n      { id:'status', label:'Status', html:`<span class=\"tc-status\" style=\"background:${color};\">${statusLabel[pin.status] || 'Available'}</span>` },\n      pin.type ? { id:'type', label:'Property Type', html:`<span class=\"tc-type\">${escapeHtml(pin.type)}</span>` } : null,\n      pin.rera_number ? { id:'rera', label:'MahaRERA No.', html:`<div class=\"tc-meta tc-rera\">${escapeHtml(pin.rera_number)}</div>` } : null,\n      configValue ? { id:'configuration', label:'Configuration', html:`<div class=\"tc-meta\">${escapeHtml(configValue)}</div>` } : null,\n      pin.price ? { id:'price', label:'Starting Price', html:`<div class=\"tc-meta\">${escapeHtml(pin.price)}</div>` } : null,\n      possessionValue ? { id:'possession', label: pin.status === 'upcoming' ? 'Launch' : 'Possession', html:`<div class=\"tc-meta\">${escapeHtml(possessionValue)}</div>` } : null,\n      pin.key_usp ? { id:'key_usp', label:'Key USP', html:`<div class=\"tc-meta\">${escapeHtml(pin.key_usp)}</div>` } : null\n    ].filter(f => f && isFieldOn(pin, f.id));\n\n    // Admin-added custom fields — each carries its own visible flag directly\n    // (set per-field in the editor), so they don't go through isFieldOn.\n    const customFields = Array.isArray(pin.custom_fields)\n      ? pin.custom_fields.filter(cf => cf && cf.visible !== false && (cf.label || cf.value))\n      : [];\n    customFields.forEach(cf=>{\n      fields.push({ id:'custom-' + cf.id, label: cf.label, html:`<div class=\"tc-meta\">${cf.label ? `<strong>${escapeHtml(cf.label)}:</strong> ` : ''}${escapeHtml(cf.value || '')}</div>` });\n    });\n\n    // Card header is split 50/50: logo on the left, project name +\n    // developer, location, status and type on the right. Everything else\n    // (configuration, price, possession, USP, custom fields) runs full\n    // width underneath.\n    const headIds = ['developer','location','status','type'];\n    const headFieldsHtml = fields.filter(f=> headIds.includes(f.id)).map(f=> `<div class=\"tc-field\">${f.html}</div>`).join('');\n    const fieldsHtml = fields.filter(f=> !headIds.includes(f.id)).map(f=> `<div class=\"tc-field\">${f.html}</div>`).join('');\n\n    // Enquire and WhatsApp are actions, not data — shown purely based on the\n    // admin's Public Card Visibility setting for this project.\n    const enquireBtn = isFieldOn(pin, 'enquire') ? `<button data-pin=\"${pin.id}\" type=\"button\" aria-label=\"Enquire now\" title=\"Enquire now\"><svg class=\"tc-enq-art\" viewBox=\"0 0 60 60\" aria-hidden=\"true\"><circle cx=\"30\" cy=\"30\" r=\"29\" fill=\"#2f3b93\"/><text x=\"30\" y=\"29\" text-anchor=\"middle\" font-family=\"Arial Narrow,Arial,Helvetica,sans-serif\" font-weight=\"800\" font-size=\"11\" fill=\"#fff\" textLength=\"44\" lengthAdjust=\"spacingAndGlyphs\">ENQUIRE</text><text x=\"30\" y=\"42\" text-anchor=\"middle\" font-family=\"Arial Narrow,Arial,Helvetica,sans-serif\" font-weight=\"800\" font-size=\"12.5\" fill=\"#fff\" textLength=\"26\" lengthAdjust=\"spacingAndGlyphs\">NOW</text><g transform=\"translate(39 3) rotate(12)\"><rect width=\"17\" height=\"12\" rx=\"1.5\" fill=\"#e9b85c\" stroke=\"#fff\" stroke-width=\"1.2\"/><path d=\"M1 1.2 L8.5 7 L16 1.2\" fill=\"none\" stroke=\"#8a5a14\" stroke-width=\"1.1\" stroke-linejoin=\"round\"/></g></svg><span class=\"tc-enq-label\">Enquire</span></button>` : '';\n    const whatsappBtn = isFieldOn(pin, 'whatsapp') ? `<a class=\"tc-whatsapp\" href=\"${whatsappUrl(pin)}\" target=\"_blank\" rel=\"noopener\" aria-label=\"WhatsApp\" title=\"WhatsApp\"><svg viewBox=\"0 0 32 32\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M16.02 3C9.4 3 4 8.38 4 15c0 2.29.64 4.44 1.75 6.28L4 29l7.94-1.7A11.94 11.94 0 0 0 16.02 27C22.65 27 28 21.63 28 15S22.65 3 16.02 3zm0 21.7c-1.95 0-3.77-.55-5.32-1.5l-.38-.23-4.71 1.01 1-4.6-.25-.4A9.63 9.63 0 0 1 6.3 15c0-5.36 4.36-9.7 9.72-9.7 5.36 0 9.72 4.34 9.72 9.7 0 5.36-4.36 9.7-9.72 9.7zm5.34-7.27c-.29-.15-1.73-.86-2-.95-.27-.1-.46-.15-.66.15-.2.29-.76.95-.93 1.15-.17.19-.34.22-.63.07-.29-.15-1.2-.44-2.29-1.41-.85-.75-1.42-1.68-1.59-1.97-.17-.29-.02-.45.13-.6.13-.13.29-.34.44-.51.15-.17.19-.29.29-.49.1-.19.05-.36-.02-.51-.07-.15-.66-1.6-.9-2.19-.24-.57-.48-.5-.66-.5-.17-.01-.36-.01-.56-.01-.19 0-.51.07-.78.36-.27.29-1.02 1-1.02 2.43 0 1.43 1.05 2.82 1.19 3.01.15.19 2.06 3.15 5 4.42.7.3 1.24.48 1.67.62.7.22 1.34.19 1.84.12.56-.08 1.73-.71 1.98-1.39.24-.68.24-1.27.17-1.39-.07-.12-.26-.19-.55-.34z\"/></svg><span class=\"tc-wa-label\">WhatsApp</span></a>` : '';\n\n    const titleHtml = isFieldOn(pin, 'name') ? `<p class=\"tc-title\">${escapeHtml(pin.title || 'Untitled project')}</p>` : '';\n    return `<div class=\"teaser-card\">\n      <div class=\"tc-top${logoHtml ? '' : ' no-logo'}\">\n        ${logoHtml}\n        <div class=\"tc-head\">${titleHtml}${headFieldsHtml}</div>\n      </div>\n      <div class=\"tc-body\">\n        ${fieldsHtml ? `<div class=\"tc-details\">${fieldsHtml}</div>` : ''}\n        ${brochureHtml}\n        <div class=\"tc-actions\">\n          ${enquireBtn}\n          ${whatsappBtn}\n          ${shareBtn}\n          ${compareBtn}\n        </div>\n      </div></div>`;\n    };\n    marker.bindPopup(buildCardHtml, {\n      closeButton:false, className:'teaser-popup', offset:[0,-2], autoClose:false, closeOnClick:false,\n      // Keeps the card clear of the fixed search bar at the top of the\n      // screen and off the very edges on mobile, instead of opening flush\n      // against it (which is what made it look like one long merged card).\n      // Desktop: the map never pans when a card opens — the card is nudged\n      // to stay on screen instead (see fitCardInView). Touch keeps autoPan.\n      autoPan: isTouchDevice,\n      autoPanPaddingTopLeft:[12, 90], autoPanPaddingBottomRight:[12, 20]\n    });\n    // Cards open on click/tap and stay until × or a map click. Hovering never opens a card.\n    // Leaflet's built-in click toggle is switched off: it would close an open\n    // card and re-render it, which wipes out the video player.\n    marker.off('click', marker._openPopup, marker);\n    const opensOnHover = false; // project cards open on click only, never on hover\n    // Desktop hint: hovering a project pin shows how to open its card. Hidden\n    // while the card is open, and never shown on touch screens (no hover there).\n    if(!isTouchDevice){\n      const hintScale = pinScale[pin.size] || 1;\n      marker.bindTooltip('<span class=\"pin-hint-ic\">👆</span> Click on the pin', {\n        direction:'top', offset:[0, -Math.round(44 * hintScale) - 4], className:'pin-hint', opacity:1,\n      });\n      marker.on('popupopen', ()=>{ marker.closeTooltip(); const t = marker.getTooltip(); if(t) t.setOpacity(0); });\n      marker.on('popupclose', ()=>{ const t = marker.getTooltip(); if(t) t.setOpacity(1); });\n    }\n    let openedByHover = false;\n    let closeTimer = null;\n    function cancelClose(){ if(closeTimer){ clearTimeout(closeTimer); closeTimer = null; } }\n    function scheduleClose(){ if(!openedByHover) return; cancelClose(); closeTimer = setTimeout(()=> marker.closePopup(), 400); }\n    marker.on('mouseover', ()=>{\n      if(!opensOnHover) return;\n      cancelClose();\n      if(!marker.isPopupOpen()){ openedByHover = true; marker.openPopup(); }\n    });\n    marker.on('mouseout', scheduleClose);\n    marker.on('click', (ev)=>{\n      L.DomEvent.stopPropagation(ev);\n      cancelClose();\n      openedByHover = false;\n      if(!marker.isPopupOpen()) marker.openPopup();\n    });\n    marker.on('popupclose', ()=>{ openedByHover = false; cancelClose(); });\n    marker.on('popupopen', (e)=>{\n      if(!isTouchDevice){\n        fitCardInView(e.popup);\n        const logoImg = e.popup._contentNode.querySelector('.tc-logo-wrap img');\n        if(logoImg && !logoImg.complete) logoImg.addEventListener('load', ()=> fitCardInView(e.popup), {once:true});\n      }\n      // Our own close (×) button, fixed to the popup frame so it never\n      // scrolls away with a long card, and large enough to tap on mobile.\n      // (Leaflet's built-in one was unreliable to tap on phones.)\n      const frame = e.popup._container;\n      if(frame && !frame.querySelector('.tc-close')){\n        const closeBtn = L.DomUtil.create('button', 'tc-close', frame);\n        closeBtn.type = 'button';\n        closeBtn.setAttribute('aria-label', 'Close');\n        closeBtn.innerHTML = '&times;';\n        L.DomEvent.disableClickPropagation(closeBtn);\n        L.DomEvent.on(closeBtn, 'click', (ev)=>{ L.DomEvent.stop(ev); marker.closePopup(); });\n      }\n      const btn = e.popup._contentNode.querySelector('button[data-pin]');\n      if(btn) btn.addEventListener('click', ()=>{ trackEvent('lead', 'enquire_click', pin.title || ('#' + pin.number)); requestDetails(pin); });\n      const waBtnEl = e.popup._contentNode.querySelector('a.tc-whatsapp');\n      if(waBtnEl) waBtnEl.addEventListener('click', ()=> trackEvent('lead', 'whatsapp_click', pin.title || ('#' + pin.number)));\n      const shareBtnEl = e.popup._contentNode.querySelector('button[data-share-pin]');\n      if(shareBtnEl) shareBtnEl.addEventListener('click', (evt)=>{ evt.stopPropagation(); sharePin(pin); });\n      const compareBtnEl = e.popup._contentNode.querySelector('button[data-compare-pin]');\n      if(compareBtnEl) compareBtnEl.addEventListener('click', (evt)=>{ evt.stopPropagation(); toggleCompare(pin.id); });\n      const popupEl = e.popup._container;\n      if(popupEl){\n        // Hover-opened card: moving onto it keeps it open, clicking in it pins it.\n        popupEl.addEventListener('mouseenter', cancelClose);\n        popupEl.addEventListener('mouseleave', scheduleClose);\n        popupEl.addEventListener('click', ()=>{ openedByHover = false; cancelClose(); });\n        // Clicks inside the YouTube player never reach this page; the window\n        // losing focus to that iframe is the only sign of them.\n        if(openedByHover){\n          const pinOnVideoClick = ()=> setTimeout(()=>{\n            const a = document.activeElement;\n            if(a && a.tagName === 'IFRAME' && popupEl.contains(a)){ openedByHover = false; cancelClose(); }\n          }, 0);\n          window.addEventListener('blur', pinOnVideoClick);\n          marker.once('popupclose', ()=> window.removeEventListener('blur', pinOnVideoClick));\n        }\n        // Stop map clicks inside the popup from closing it or firing other pins\n        L.DomEvent.on(popupEl, 'click', L.DomEvent.stopPropagation);\n      }\n      // If a video is set, play it immediately using the official YouTube\n      // IFrame Player API (not a plain iframe) — this is what reliably\n      // fires onError when a video owner has disabled embedding, which a\n      // raw <iframe src=\"...embed/...\"> does NOT do on its own (that's\n      // why the previous approach still showed YouTube's own broken\n      // \"Error 153\" screen instead of our clean fallback). Two safety\n      // nets so something always actually displays: (1) the video's own\n      // YouTube thumbnail sits behind the player as a poster, visible the\n      // instant the card opens; (2) onError swaps this area to a \"Watch\n      // Video\" button instead of YouTube's own error screen.\n      const videoWrap = e.popup._contentNode.querySelector('.tc-video-wrap');\n      if(videoWrap){\n        const vid = videoWrap.dataset.videoId;\n        videoWrap.classList.add('playing');\n        videoWrap.style.background = `#000 url('https://img.youtube.com/vi/${vid}/hqdefault.jpg') center/cover no-repeat`;\n\n        const playerDivId = 'yt-player-' + pin.id + '-' + Date.now();\n        // Build the iframe ourselves so it always sends a referrer and the\n        // autoplay permission — this is what lets it start playing\n        // straight away (muted) on mobile and desktop.\n        const playerDiv = document.createElement('iframe');\n        playerDiv.id = playerDivId;\n        playerDiv.src = 'https://www.youtube.com/embed/' + vid +\n          '?autoplay=1&mute=1&playsinline=1&controls=1&rel=0&modestbranding=1&loop=1&playlist=' + vid +\n          '&enablejsapi=1&origin=' + encodeURIComponent(location.origin);\n        playerDiv.setAttribute('allow', 'autoplay; encrypted-media; picture-in-picture');\n        playerDiv.setAttribute('allowfullscreen', '');\n        playerDiv.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');\n        playerDiv.setAttribute('title', 'Project video');\n        videoWrap.appendChild(playerDiv);\n\n        let ytPlayer = null;\n        let destroyed = false;\n        const showFallback = ()=>{\n          if(destroyed) return;\n          videoWrap.innerHTML = `<a href=\"https://www.youtube.com/watch?v=${vid}\" target=\"_blank\" rel=\"noopener\" class=\"tc-video-fallback\">▶ Watch Video</a>`;\n        };\n        whenYouTubeApiReady(()=>{\n          if(destroyed) return;\n          try{\n            ytPlayer = new YT.Player(playerDivId, {\n              events:{\n                onReady:(ev)=>{ try{ ev.target.mute(); ev.target.playVideo(); }catch(err){} },\n                // 101/150 = embedding disabled by the video owner; 100 = removed\n                // or private; 2 = invalid id; 5 = HTML5 player error.\n                onError: showFallback\n              }\n            });\n          }catch(err){ showFallback(); }\n        });\n\n        pinVideoRevert = function(){\n          destroyed = true;\n          try{ if(ytPlayer && ytPlayer.destroy) ytPlayer.destroy(); }catch(err){}\n          videoWrap.classList.remove('playing');\n          videoWrap.innerHTML = '';\n          videoWrap.style.background = '';\n          videoWrap.style.cursor = '';\n        };\n      }\n    });\n    marker.on('popupopen', (e)=>{\n      const wrap = e.popup._wrapper;\n      const acts = e.popup._contentNode.querySelector('.tc-actions');\n      if(!wrap || !acts) return;\n      wrap.querySelectorAll(':scope > .tc-actions-float').forEach(n=> n.remove());\n      // The original button row stays hidden inside the card (so it can\n      // never show up as a second set of buttons). The floating column is\n      // a copy whose taps are passed on to the originals.\n      const float = acts.cloneNode(true);\n      float.classList.add('tc-actions-float');\n      wrap.appendChild(float);\n      L.DomEvent.disableClickPropagation(float);\n      const forward = (sel)=>{\n        const b = float.querySelector(sel);\n        if(!b) return;\n        b.addEventListener('click', (ev)=>{\n          ev.preventDefault(); ev.stopPropagation();\n          const orig = e.popup._contentNode.querySelector(sel);\n          if(orig) orig.click();\n        });\n      };\n      forward('button[data-pin]');\n      forward('button[data-share-pin]');\n      forward('button[data-compare-pin]');\n      const waFloat = float.querySelector('a.tc-whatsapp');\n      if(waFloat) waFloat.addEventListener('click', ()=> trackEvent('lead', 'whatsapp_click', pin.title || ('#' + pin.number)));\n    });\n    marker.on('popupclose', ()=>{\n      // Stop playback when the popup closes so it starts fresh next time.\n      if(pinVideoRevert){ pinVideoRevert(); pinVideoRevert = null; }\n    });\n    pinMarkerMap.set(pin.id, marker);\n  }\n\n  pins.forEach(renderPin);\n\n  // ---------- Live sync from the editor ----------\n  // Any change an admin makes in the editor — including flipping a Public\n  // Card Visibility switch — updates this row in Supabase, and this\n  // subscription pushes that change to every visitor's map instantly,\n  // with no page refresh needed.\n  let reloadTimer = null;\n  function scheduleReload(delay){\n    clearTimeout(reloadTimer);\n    reloadTimer = setTimeout(reloadPins, delay == null ? 800 : delay);\n  }\n  async function reloadPins(){\n    const data = await fetchPinsLight();\n    // Load failed: keep every pin already on the map and try again soon.\n    if(!data){ scheduleReload(5000); return; }\n    applyCachedImages(data);\n    pins = data;\n    pinMarkerMap.forEach(m=> map.removeLayer(m));\n    pinMarkerMap.clear();\n    pins.forEach(renderPin);\n    loadPinImages(pins);\n    refreshCompareUI();\n  }\n  // Several edits in a row (the editor saves field by field) now trigger\n  // one refresh instead of a burst of heavy reloads.\n  sb.channel('public-pins-changes')\n    .on('postgres_changes', { event: '*', schema: 'public', table: 'pins' }, ()=> scheduleReload())\n    .subscribe();\n  // If the very first load failed, keep retrying until the pins appear.\n  if(!pins.length) scheduleReload(3000);\n  loadPinImages(pins);\n\n  // Default view on load: every visitor's location is fetched automatically\n  // (requested as soon as the map opens — see earlyLocation) and the map\n  // centres on them and whichever of our projects are actually near\n  // them — no button tap needed. Falls back to the full-radius view if\n  // location is denied/unavailable, so TV/kiosk screens and anyone who says\n  // no to the permission prompt still land on a normal, usable map. A\n  // shared pin link (?pin=<id>) is the one exception — that still zooms\n  // straight to the specific project being shared, skipping geolocation.\n  (function focusOnLoad(){\n    const urlPinId = new URLSearchParams(window.location.search).get('pin');\n    const sharedPin = urlPinId ? pins.find(p => p.id === urlPinId) : null;\n    if(sharedPin){\n      const marker = pinMarkerMap.get(sharedPin.id);\n      if(marker){\n        map.setView([sharedPin.lat, sharedPin.lng], 17, { animate:true });\n        // Look the marker up again when opening: once the pin's logo loads,\n        // refreshPinMarker() swaps in a new marker and this one leaves the map.\n        setTimeout(()=>{ const m = pinMarkerMap.get(sharedPin.id); if(m) m.openPopup(); }, 650);\n      }\n      // Still mark where the visitor is, without moving away from the shared pin.\n      earlyLocation.then(loc=>{ if(loc) placeMeMarker(loc.lat, loc.lng); });\n      return;\n    }\n    earlyLocation.then(loc=>{\n      if(loc) showNearMe(loc.lat, loc.lng);\n      else fitFullMap();\n    });\n  })();\n\n  bridges.forEach(b=>{\n    const marker = L.marker([b.lat, b.lng], {icon: infraIcon(b)}).addTo(map);\n    marker.bindPopup(infraHoverContent(b), {closeButton:false, className:'infra-hover-popup', offset:[0,-4]});\n    marker.on('mouseover', ()=> marker.openPopup());\n    marker.on('mouseout', ()=> marker.closePopup());\n  });\n\n  const roadWeights = {small:3, medium:5, large:8};\n  function isBridgeLike(type){ return type === 'bridge' || type === 'highway_bridge' || type === 'flyover'; }\n  roads.forEach(r=>{\n    if(!r.points || r.points.length < 2) return;\n    const meta = infraTypes[r.type] || infraTypes.proposed_road;\n    const color = r.color || meta.color;\n    const weight = r.weight || 5;\n    const isMetro = r.type === 'metro_line';\n    if(isBridgeLike(r.type)){\n      L.polyline(r.points, {\n        color:'#ffffff', weight: weight + 6, opacity:0.9, lineJoin:'round', lineCap:'round', interactive:false\n      }).addTo(map);\n    }\n    const line = L.polyline(r.points, {\n      color, weight: isMetro ? Math.min(weight, 3) : weight, opacity:0.95,\n      dashArray: (r.type === 'proposed_road') ? '10,8' : (isMetro ? '7,5' : null),\n      lineJoin:'round', lineCap:'round'\n    }).addTo(map);\n    if(r.title || r.notes) line.bindPopup(roadHoverContent(r), {closeButton:false, className:'road-hover-popup'});\n  });\n\n  const overlayBg = document.getElementById('overlay-bg');\n  const leadBox = document.getElementById('lead-box');\n  const detailPanel = document.getElementById('detail-panel');\n  const leadRole = document.getElementById('lead-role');\n  const leadName = document.getElementById('lead-name');\n  const leadWhatsapp = document.getElementById('lead-whatsapp');\n  const leadWhatsappCode = document.getElementById('lead-whatsapp-code');\n  leadWhatsapp.addEventListener('input', ()=>{\n    leadWhatsapp.value = leadWhatsapp.value.replace(/\\D/g, '').slice(0, 10);\n  });\n  const leadEmail = document.getElementById('lead-email');\n  const leadConsent = document.getElementById('lead-consent');\n  const leadError = document.getElementById('lead-error');\n  const leadSubmit = document.getElementById('lead-submit');\n  const leadCancel = document.getElementById('lead-cancel');\n  const leadTitle = document.getElementById('lead-title');\n\n  let pendingPin = null;\n\n  function unlockKey(pinId){ return 'lead_unlocked_' + pinId; }\n\n  function requestDetails(pin){\n    map.closePopup();\n    if(localStorage.getItem(unlockKey(pin.id))){\n      showDetails(pin);\n      return;\n    }\n    pendingPin = pin;\n    leadTitle.textContent = 'See full details — ' + (pin.title || 'this project');\n    leadError.textContent = '';\n    leadName.value = ''; leadWhatsapp.value = ''; leadEmail.value = '';\n    leadWhatsappCode.value = '+91';\n    leadRole.value = 'buyer';\n    leadConsent.checked = true;\n    leadBox.style.display = 'block';\n    detailPanel.classList.remove('open');\n    overlayBg.classList.add('open');\n  }\n\n  leadCancel.addEventListener('click', ()=>{\n    overlayBg.classList.remove('open');\n    leadBox.style.display = 'none';\n    pendingPin = null;\n  });\n  overlayBg.addEventListener('click', (e)=>{ if(e.target === overlayBg){ overlayBg.classList.remove('open'); leadBox.style.display='none'; pendingPin=null; } });\n\n  let leadSubmitInProgress = false;\n  leadSubmit.addEventListener('click', async ()=>{\n    // Guards against a submission firing twice from a single tap — some\n    // touchscreens/browsers can generate two click events for one physical\n    // tap faster than the disabled-attribute check below can catch it.\n    if(leadSubmitInProgress) return;\n    leadSubmitInProgress = true;\n    try{\n    const name = leadName.value.trim();\n    const whatsappDigits = leadWhatsapp.value.trim();\n    const email = leadEmail.value.trim();\n    const role = leadRole.value;\n    if(!name || !whatsappDigits || !email){\n      leadError.textContent = 'Please fill in all fields.';\n      return;\n    }\n    if(!/^\\d{10}$/.test(whatsappDigits)){\n      leadError.textContent = 'WhatsApp number must be exactly 10 digits.';\n      return;\n    }\n    const whatsapp = leadWhatsappCode.value + ' ' + whatsappDigits;\n    if(!/^\\S+@\\S+\\.\\S+$/.test(email)){\n      leadError.textContent = 'Please enter a valid email.';\n      return;\n    }\n    if(!leadConsent.checked){\n      leadError.textContent = 'Please provide consent to continue.';\n      return;\n    }\n    leadSubmit.disabled = true;\n    leadSubmit.textContent = 'Submitting…';\n    const { error } = await sb.from('leads').insert({\n      pin_id: pendingPin.id, role, name, whatsapp, email, consent: leadConsent.checked\n    });\n    leadSubmit.disabled = false;\n    leadSubmit.textContent = 'View Details';\n    if(error){\n      leadError.textContent = 'Something went wrong — please try again.';\n      return;\n    }\n    localStorage.setItem(unlockKey(pendingPin.id), '1');\n    trackEvent('lead', 'lead_submitted', pendingPin.title || ('#' + pendingPin.number));\n\n    // Also sends this lead to your Google Sheet, via the Apps Script Web\n    // App you deploy separately (see google-sheets-lead-webhook.gs). This\n    // never blocks or fails the actual submission above — if the Sheet is\n    // slow, offline, or not set up yet, the lead is still saved to Supabase\n    // and the visitor sees no difference either way.\n    const SHEETS_WEBHOOK_URL = 'https://script.google.com/macros/s/AKfycbwrD-sBwB_3AiX9X-aLUR-3BPNdsVohTC0ZbHp_nNUsV4HrTLZ9hmBemH9sKs_sFZDg/exec';\n    if(SHEETS_WEBHOOK_URL){\n      fetch(SHEETS_WEBHOOK_URL, {\n        method: 'POST',\n        mode: 'no-cors',\n        headers: { 'Content-Type': 'text/plain' },\n        body: JSON.stringify({\n          project: pendingPin.title || ('#' + pendingPin.number),\n          role, name, whatsapp, email, consent: leadConsent.checked\n        })\n      }).catch(()=>{ /* fails silently — Supabase above is the source of truth */ });\n    }\n\n    leadBox.style.display = 'none';\n    showDetails(pendingPin);\n    } finally {\n      leadSubmitInProgress = false;\n    }\n  });\n\n  function showDetails(pin){\n    const color = statusColor[pin.status] || statusColor.available;\n    const img = document.getElementById('detail-img');\n    if(pin.image){ img.src = pin.image; img.style.display = 'block'; }\n    else { img.style.display = 'none'; }\n\n    let rows = '';\n    if(pin.status === 'sold'){\n      rows = `<div class=\"detail-row\"><label>Status</label><div class=\"value\">This project has been sold out.</div></div>`;\n    } else {\n      if(pin.price) rows += `<div class=\"detail-row\"><label>Price</label><div class=\"value price\">${escapeHtml(pin.price)}</div></div>`;\n      if(pin.status === 'construction' || pin.status === 'upcoming'){\n        if(pin.configuration) rows += `<div class=\"detail-row\"><label>Configuration</label><div class=\"value\">${escapeHtml(pin.configuration)}</div></div>`;\n        if(pin.sqft) rows += `<div class=\"detail-row\"><label>Sq Ft</label><div class=\"value\">${escapeHtml(pin.sqft)}</div></div>`;\n      }\n      if(pin.status === 'construction' && pin.possession_timeline)\n        rows += `<div class=\"detail-row\"><label>Possession Timeline</label><div class=\"value\">${escapeHtml(pin.possession_timeline)}</div></div>`;\n      if(pin.status === 'upcoming' && pin.launch_date)\n        rows += `<div class=\"detail-row\"><label>Approximate Launch Date</label><div class=\"value\">${escapeHtml(pin.launch_date)}</div></div>`;\n      if(pin.status === 'available' && pin.whats_available)\n        rows += `<div class=\"detail-row\"><label>What's Available</label><div class=\"value\">${escapeHtml(pin.whats_available)}</div></div>`;\n    }\n    if(pin.description) rows += `<div class=\"detail-row\"><label>Description</label><div class=\"value\">${escapeHtml(pin.description)}</div></div>`;\n\n    const detailShareBtn = `<button class=\"detail-share\" type=\"button\" id=\"detail-share-btn\">🔗 Share this project</button>`;\n    document.getElementById('detail-body').innerHTML = `\n      <h2>${escapeHtml(pin.title || 'Untitled project')}</h2>\n      <span class=\"status-badge\" style=\"background:${color};\">${statusLabel[pin.status] || 'Available'}</span>\n      ${pin.developer ? `<div class=\"detail-row\"><label>Developer</label><div class=\"value\">${escapeHtml(pin.developer)}</div></div>` : ''}\n      ${pin.location ? `<div class=\"detail-row\"><label>Location</label><div class=\"value\">${escapeHtml(pin.location)}</div></div>` : ''}\n      ${pin.type ? `<div class=\"detail-row\"><label>Type</label><div class=\"value\">${escapeHtml(pin.type)}</div></div>` : ''}\n      ${pin.rera_number && isFieldOn(pin, 'rera') ? `<div class=\"detail-row\"><label>MahaRERA No.</label><div class=\"value\">${escapeHtml(pin.rera_number)}</div></div>` : ''}\n      ${rows}\n      ${pin.key_usp ? `<div class=\"detail-row\"><label>Key USP</label><div class=\"value\">${escapeHtml(pin.key_usp)}</div></div>` : ''}\n      ${detailShareBtn}\n    `;\n    const shareEl = document.getElementById('detail-share-btn');\n    if(shareEl) shareEl.addEventListener('click', ()=> sharePin(pin));\n    overlayBg.classList.add('open');\n    detailPanel.classList.add('open');\n  }\n\n  document.getElementById('detail-close').addEventListener('click', ()=>{\n    detailPanel.classList.remove('open');\n    overlayBg.classList.remove('open');\n  });\n\n  // Privacy Policy / Disclaimer — shown as an in-page popup instead of navigating away.\n  const legalContent = {\n    privacy: {\n      title: 'Privacy Policy',\n      html: `\n        <p class=\"legal-updated\">Last Updated: 30 September 2026</p>\n        <p>This privacy policy (\"Policy\") describes how we collect, protect and use the personally identifiable information (\"Personal Information\") you (\"User\", \"you\" or \"your\") provide on the <a href=\"https://associatte.com\" target=\"_blank\" rel=\"noopener\">https://associatte.com</a> website and any of its products or services (collectively, \"Website\" or \"Services\"). It also describes the choices available to you regarding our use of your personal information and how you can access and update this information. This Policy does not apply to the practices of companies that we do not own or control, or to individuals that we do not employ or manage.</p>\n\n        <h3>Collection of Personal Information</h3>\n        <p>We receive and store any information you knowingly provide to us when you publish content, fill any online forms on the Website. You can choose not to provide us with certain information, but then you may not be able to take advantage of some of the Website's features.</p>\n\n        <h3>Collection of Non-Personal Information</h3>\n        <p>When you visit the Website our servers automatically record information that your browser sends. This data may include information such as your computer's IP address, browser type and version, operating system type and version, language preferences or the webpage you were visiting before you came to our Website, pages of our Website that you visit, the time spent on those pages, information you search for on our Website, access times and dates, and other statistics.</p>\n\n        <h3>Use of Collected Information</h3>\n        <p>Any of the information we collect from you may be used to personalize your experience; improve our website; improve customer service and respond to queries and emails of our customers; send notification emails such as password reminders, updates, etc; run and operate our Website and Services. Non-personal information collected is used only to identify potential cases of abuse and establish statistical information regarding Website traffic and usage. This statistical information is not otherwise aggregated in such a way that would identify any particular user of the system.</p>\n\n        <h3>Children</h3>\n        <p>We do not knowingly collect any personal information from children under the age of 13. If you are under the age of 13, please do not submit any personal information through our Website or Service. We encourage parents and legal guardians to monitor their children's Internet usage and to help enforce this Policy by instructing their children never to provide personal information through our Website or Service without their permission. If you have reason to believe that a child under the age of 13 has provided personal information to us through our Website or Service, please contact us.</p>\n\n        <h3>Newsletters</h3>\n        <p>We offer electronic newsletters to which you may voluntarily subscribe at any time. You may choose to stop receiving our newsletter or marketing emails by following the unsubscribe instructions included in these emails or by contacting us.</p>\n\n        <h3>Cookies</h3>\n        <p>The Website uses \"cookies\" to help personalize your online experience. A cookie is a text file that is placed on your hard disk by a web page server. Cookies cannot be used to run programs or deliver viruses to your computer. Cookies are uniquely assigned to you, and can only be read by a web server in the domain that issued the cookie to you. We may use cookies to collect, store, and track information for statistical purposes to operate our Website and Services. You have the ability to accept or decline cookies. Most web browsers automatically accept cookies, but you can usually modify your browser setting to decline cookies if you prefer.</p>\n\n        <h3>Maps and OpenStreetMap</h3>\n        <p>The interactive maps on this Website are built with map data from <a href=\"https://www.openstreetmap.org/copyright\" target=\"_blank\" rel=\"noopener\">OpenStreetMap</a>, &copy; OpenStreetMap contributors, made available under the Open Database License (ODbL). To display the map, your browser loads map images (\"tiles\") directly from third-party servers rather than from us:</p>\n        <ul>\n          <li><strong>Standard map view</strong> &ndash; tiles are served by the OpenStreetMap Foundation (tile.openstreetmap.org).</li>\n          <li><strong>Satellite view</strong> &ndash; imagery is served by Esri (server.arcgisonline.com).</li>\n          <li><strong>3D map view</strong> &ndash; OpenStreetMap-based tiles are served by an independent open-source map tile service.</li>\n        </ul>\n        <p>When your browser requests these tiles, the provider automatically receives technical information such as your IP address, browser type and version, the address of the page you are viewing (referrer), and which map area and zoom level you are looking at. This is standard for any website that displays an online map. These providers process that information under their own privacy policies, which we do not control, including the <a href=\"https://osmfoundation.org/wiki/Privacy_Policy\" target=\"_blank\" rel=\"noopener\">OpenStreetMap Foundation Privacy Policy</a> and its <a href=\"https://operations.osmfoundation.org/policies/tiles/\" target=\"_blank\" rel=\"noopener\">Tile Usage Policy</a>, and the <a href=\"https://www.esri.com/en-us/privacy/overview\" target=\"_blank\" rel=\"noopener\">Esri Privacy Statement</a>. Their servers may be located outside India.</p>\n        <p>We do not send your name, phone number, email address or any enquiry details to OpenStreetMap, Esri or any other map provider. The map does not read your device's GPS or location, and the locality search box is matched within the page itself &ndash; your search terms are not sent to OpenStreetMap or to any geocoding service.</p>\n\n        <h3>Information Security</h3>\n        <p>We secure information you provide on computer servers in a controlled, secure environment, protected from unauthorized access, use, or disclosure. We maintain reasonable administrative, technical, and physical safeguards in an effort to protect against unauthorized access, use, modification, and disclosure of personal information in its control and custody. However, no data transmission over the Internet or wireless network can be guaranteed. Therefore, while we strive to protect your personal information, you acknowledge that:</p>\n        <ul>\n          <li>There are security and privacy limitations of the Internet which are beyond our control;</li>\n          <li>The security, integrity, and privacy of any and all information and data exchanged between you and our Website cannot be guaranteed;</li>\n          <li>Any such information and data may be viewed or tampered with in transit by a third party, despite best efforts.</li>\n        </ul>\n\n        <h3>Data Breach</h3>\n        <p>In the event we become aware that the security of the Website has been compromised or users Personal Information has been disclosed to unrelated third parties as a result of external activity, including, but not limited to, security attacks or fraud, we reserve the right to take reasonably appropriate measures, including, but not limited to, investigation and reporting, as well as notification to and cooperation with law enforcement authorities. In the event of a data breach, we will make reasonable efforts to notify affected individuals if we believe that there is a reasonable risk of harm to the user as a result of the breach or if notice is otherwise required by law. When we do we will send you an email.</p>\n\n        <h3>Changes and Amendments</h3>\n        <p>We reserve the right to modify this privacy policy relating to the Website or Services at any time, effective upon posting of an updated version of this privacy policy on the Website. When we do we will send you an email to notify you. Continued use of the Website after any such changes shall constitute your consent to such changes.</p>\n\n        <h3>Acceptance of This Policy</h3>\n        <p>You acknowledge that you have read this Policy and agree to all its terms and conditions. By using the Website or its Services you agree to be bound by this Policy. If you do not agree to abide by the terms of this Policy, you are not authorized to use or access the Website and its Services.</p>\n\n        <h3>Contacting Us</h3>\n        <p>If you have any questions about this Policy, please contact us:</p>\n        <div class=\"contact-block\">\n          <strong>Associatte PropTech Pvt Ltd</strong>\n          302 and 303, Naren Pearl, 3rd Floor<br>\n          Magarpatta Road, Above Axis and IndusInd Bank<br>\n          Hadapsar, Pune - 411028<br>\n          Email: <a href=\"mailto:mappingg.associatte@gmail.com\">mappingg.associatte@gmail.com</a>\n        </div>\n        <p>You consent to receive communications from us by way of SMS/email/phone calls and RCS with respect to your transactions on our Website. Users will be required to register their valid phone numbers and e-mail addresses to facilitate such communication. We may also use your e-mail address to send you updates, newsletters, changes to features of the Service, and the like to provide You better Services.</p>\n        <p style=\"text-align:center;margin-top:18px;\">© 2026 Associatte PropTech Pvt Ltd. All rights reserved.</p>\n      `\n    },\n    disclaimer: {\n      title: 'Disclaimer',\n      html: `\n        <p class=\"legal-updated\">Last Updated: 30 August 2026</p>\n        <p>This is not the official website of developer &amp; property, it belongs to authorised channel partner for information purpose only. All rights for logo &amp; images are reserved to developer. Thank you for visiting our website.</p>\n        <p>This disclaimer (\"Disclaimer\") is applicable to this website and all microsites and websites owned by us. By using or accessing this website you agree with the Disclaimer without any qualification or limitation.</p>\n        <p>This website is in the process of being updated. By accessing this website, the viewer confirms that the information including brochures and marketing collaterals on this website are solely for informational purposes only and the viewer has not relied on this information for making any booking/purchase in any project of the company.</p>\n        <p>Nothing on this website, constitutes advertising, marketing, booking, selling or an offer for sale, or invitation to purchase a unit in any project by the company. The company is not liable for any consequence of any action taken by the viewer relying on such material/ information on this website.</p>\n        <p style=\"text-align:center;margin-top:18px;\">© 2026 Associatte PropTech Pvt Ltd. All rights reserved.</p>\n      `\n    }\n  };\n\n  const legalOverlay = document.getElementById('legal-overlay');\n  const legalTitle = document.getElementById('legal-title');\n  const legalBody = document.getElementById('legal-body');\n\n  function openLegalModal(kind){\n    const entry = legalContent[kind];\n    if(!entry) return;\n    legalTitle.textContent = entry.title;\n    legalBody.innerHTML = entry.html;\n    legalBody.scrollTop = 0;\n    legalOverlay.classList.add('open');\n  }\n  function closeLegalModal(){ legalOverlay.classList.remove('open'); }\n  // Exposed globally so the attribution control's inline onclick (built as a plain\n  // HTML string outside this closure) can call it.\n  window.openLegalModal = openLegalModal;\n\n  document.getElementById('legal-close').addEventListener('click', closeLegalModal);\n  legalOverlay.addEventListener('click', (e)=>{ if(e.target === legalOverlay) closeLegalModal(); });\n  document.addEventListener('keydown', (e)=>{ if(e.key === 'Escape') closeLegalModal(); });\n\n  // ---------- Area / locality search ----------\n  // Typing a known Pune-area name focuses the map on that locality. Mundhwa\n  // is handled as a fixed, single reference point (same centre every time)\n  // with the Associatte pin auto-opened and highlighted — since that's your\n  // home project. Every other locality (Kharadi, Solapur Highway,\n  // NIBM/Lullanagar, Viman Nagar, and anything else added later in the admin\n  // tool) is data-driven from the area_boundaries table: once you draw that\n  // locality's boundary there, search zooms to the exact drawn shape (and to\n  // your actual project pins inside it, once you have some); until then it\n  // falls back to a wider reference view around that locality's centre.\n  const MUNDHWA_CENTER = { lat: 18.5215, lng: 73.9140, zoom: 15.5 };\n  const MUNDHWA_MATCH = ['mundhwa'];\n\n  const knownAreas = areaBoundaries.map(a => ({\n    id: a.id,\n    label: a.label,\n    match: a.match || [],\n    drawnBoundary: (a.boundary && a.boundary.length >= 3) ? a.boundary : null,\n    fallbackLat: a.fallback_lat,\n    fallbackLng: a.fallback_lng,\n    fallbackZoom: a.fallback_zoom || 14\n  }));\n\n  const areaToastEl = document.getElementById('area-toast');\n  let areaToastTimer = null;\n  function showAreaToast(text){\n    areaToastEl.textContent = '📍 ' + text;\n    areaToastEl.classList.add('show');\n    clearTimeout(areaToastTimer);\n    areaToastTimer = setTimeout(()=> areaToastEl.classList.remove('show'), 2600);\n  }\n\n  function textMatches(q, aliasList){\n    return aliasList.some(alias => alias.includes(q) || q.includes(alias));\n  }\n\n  // Generic point-in-polygon (ray-casting) test — used both for your main\n  // Mundhwa boundary and for any locality boundary drawn in the admin tool.\n  function isInsidePolygon(lat, lng, polygon){\n    if(!polygon || polygon.length < 3) return false;\n    const x = lat, y = lng;\n    let inside = false;\n    for(let i = 0, j = polygon.length - 1; i < polygon.length; j = i++){\n      const xi = polygon[i][0], yi = polygon[i][1];\n      const xj = polygon[j][0], yj = polygon[j][1];\n      const intersects = ((yi > y) !== (yj > y)) &&\n        (x < (xj - xi) * (y - yi) / (yj - yi) + xi);\n      if(intersects) inside = !inside;\n    }\n    return inside;\n  }\n  function isInsideMundhwaBoundary(lat, lng){ return isInsidePolygon(lat, lng, boundary); }\n\n  // Pins that actually belong to a given (non-Mundhwa) area: inside its drawn\n  // boundary if one exists, otherwise within a rough box of its fallback\n  // centre — but never anything that's really inside the main Mundhwa\n  // boundary, so that cluster can't leak into another locality's results.\n  const FALLBACK_RADIUS = 0.03;\n  function pinsInArea(area){\n    if(area.drawnBoundary){\n      return pins.filter(p => isInsidePolygon(p.lat, p.lng, area.drawnBoundary));\n    }\n    if(area.fallbackLat == null || area.fallbackLng == null) return [];\n    return pins.filter(p =>\n      Math.abs(p.lat - area.fallbackLat) <= FALLBACK_RADIUS &&\n      Math.abs(p.lng - area.fallbackLng) <= FALLBACK_RADIUS &&\n      !isInsideMundhwaBoundary(p.lat, p.lng)\n    );\n  }\n\n  // Briefly pulses a marker (like the \"highlighted\" glow) without touching\n  // its stored data — used when search brings a specific project into focus.\n  function pulseMarker(marker){\n    const el = marker.getElement && marker.getElement();\n    const wrap = el && el.querySelector('.pin-wrap');\n    if(!wrap || wrap.querySelector('.pin-radar')) return;\n    const radar = document.createElement('div');\n    radar.className = 'pin-radar';\n    wrap.insertBefore(radar, wrap.firstChild);\n    setTimeout(()=> radar.remove(), 4200);\n  }\n\n  function focusMundhwa(){\n    map.flyTo([MUNDHWA_CENTER.lat, MUNDHWA_CENTER.lng], MUNDHWA_CENTER.zoom, { animate:true, duration:1.1 });\n    pins.forEach(pin=>{\n      const marker = pinMarkerMap.get(pin.id);\n      if(marker && !map.hasLayer(marker)) marker.addTo(map);\n    });\n    const companyPin = pins.find(p => p.number === 1) ||\n      pins.find(p => (p.title || '').toLowerCase().includes('associatte'));\n    if(companyPin){\n      const marker = pinMarkerMap.get(companyPin.id);\n      if(marker){\n        setTimeout(()=> pulseMarker(marker), 900);\n      }\n    }\n    showAreaToast('Mundhwa, Pune');\n  }\n\n  function focusArea(area){\n    pins.forEach(pin=>{\n      const marker = pinMarkerMap.get(pin.id);\n      if(marker && !map.hasLayer(marker)) marker.addTo(map);\n    });\n    const nearbyPins = pinsInArea(area);\n    if(nearbyPins.length){\n      // Projects exist here — fit tightly around them with a bit of extra\n      // zoom so they're clearly the focus, not lost in a wide shot.\n      const bounds = L.latLngBounds(nearbyPins.map(p => [p.lat, p.lng]));\n      map.flyToBounds(bounds, { padding:[90,90], maxZoom:17, animate:true, duration:1.1 });\n      showAreaToast(`${area.label} — ${nearbyPins.length} project${nearbyPins.length > 1 ? 's' : ''}`);\n    } else if(area.drawnBoundary){\n      // No projects yet, but the locality's boundary has been drawn —\n      // zoom straight to that exact shape.\n      map.flyToBounds(L.latLngBounds(area.drawnBoundary), { padding:[50,50], animate:true, duration:1.1 });\n      showAreaToast(`${area.label} — no projects added yet`);\n    } else if(area.fallbackLat != null && area.fallbackLng != null){\n      // No boundary drawn yet either — fall back to a general reference view.\n      map.flyTo([area.fallbackLat, area.fallbackLng], area.fallbackZoom, { animate:true, duration:1.1 });\n      showAreaToast(`${area.label} — no projects added yet`);\n    }\n  }\n\n  // ---------- Search suggestions, built from your own listing data ----------\n  // Project names, locations, and configuration types (2 BHK, 3 BHK, etc.)\n  // pulled straight from your pins — never a random real-world place. This\n  // is what should power public search; the Google location search is for\n  // the admin editor only, where navigating anywhere makes sense.\n  function buildSearchSuggestions(){\n    const seen = new Map(); // label (lowercase) -> {label, type, pin?, area?}\n    pins.forEach(pin=>{\n      if(pin.title){\n        seen.set(pin.title.toLowerCase(), { label: pin.title, type: 'Project', pin });\n      }\n      if(pin.location){\n        const key = pin.location.toLowerCase();\n        if(!seen.has(key)) seen.set(key, { label: pin.location, type: 'Location' });\n      }\n      if(pin.configuration){\n        pin.configuration.split(/[,/]/).map(s=>s.trim()).filter(Boolean).forEach(cfg=>{\n          const key = cfg.toLowerCase();\n          if(!seen.has(key)) seen.set(key, { label: cfg, type: 'Configuration' });\n        });\n      }\n    });\n    knownAreas.forEach(area=>{\n      const key = area.label.toLowerCase();\n      if(!seen.has(key)) seen.set(key, { label: area.label, type: 'Area', area });\n    });\n    if(!seen.has('mundhwa')) seen.set('mundhwa', { label: 'Mundhwa', type: 'Area', isMundhwa: true });\n    return Array.from(seen.values());\n  }\n\n  const searchSuggestionsEl = document.getElementById('search-suggestions');\n  let searchSuggestionsData = [];\n  let activeSuggestionIndex = -1;\n\n  function renderSuggestions(list){\n    activeSuggestionIndex = -1;\n    if(!list.length){ searchSuggestionsEl.classList.remove('open'); searchSuggestionsEl.innerHTML=''; return; }\n    searchSuggestionsEl.innerHTML = list.map((s,i)=>\n      `<div class=\"suggestion-item\" data-i=\"${i}\"><span>${s.label}</span><span class=\"sg-type\">${s.type}</span></div>`\n    ).join('');\n    searchSuggestionsEl.classList.add('open');\n    searchSuggestionsEl.querySelectorAll('.suggestion-item').forEach(el=>{\n      el.addEventListener('click', ()=> selectSuggestion(list[Number(el.dataset.i)]));\n    });\n  }\n\n  function selectSuggestion(s){\n    const input = document.getElementById('search-input');\n    input.value = s.label;\n    searchSuggestionsEl.classList.remove('open');\n    if(s.isMundhwa){ focusMundhwa(); return; }\n    if(s.type === 'Area' && s.area){ focusArea(s.area); return; }\n    applySearchFilter(s.label);\n    if(s.type === 'Project' && s.pin){\n      const marker = pinMarkerMap.get(s.pin.id);\n      if(marker){\n        map.flyTo([s.pin.lat, s.pin.lng], 16, { duration: 1 });\n        setTimeout(()=>{ marker.openPopup(); pulseMarker(marker); }, 700);\n      }\n    }\n  }\n\n  function applySearchFilter(rawQ){\n    const q = rawQ.trim().toLowerCase();\n\n    if(q.length >= 3){\n      if(textMatches(q, MUNDHWA_MATCH)){ focusMundhwa(); return; }\n      const areaMatch = knownAreas.find(area => textMatches(q, area.match));\n      if(areaMatch){ focusArea(areaMatch); return; }\n    }\n\n    const matches = [];\n    pins.forEach(pin=>{\n      const marker = pinMarkerMap.get(pin.id);\n      const haystack = [pin.title, pin.location, pin.configuration, pin.sqft, pin.whats_available]\n        .filter(Boolean).join(' ').toLowerCase();\n      const isMatch = !q || haystack.includes(q);\n      if(isMatch){\n        if(!map.hasLayer(marker)) marker.addTo(map);\n        matches.push(pin);\n      } else {\n        if(map.hasLayer(marker)) map.removeLayer(marker);\n      }\n    });\n    if(q && matches.length){\n      const bounds = L.latLngBounds(matches.map(p=>[p.lat, p.lng]));\n      map.fitBounds(bounds, {padding:[80,80], maxZoom:16});\n    }\n  }\n\n  searchSuggestionsData = buildSearchSuggestions();\n\n  document.getElementById('search-input').addEventListener('input', (e)=>{\n    const q = e.target.value.trim().toLowerCase();\n    applySearchFilter(e.target.value);\n    if(!q){ renderSuggestions([]); return; }\n    const matches = searchSuggestionsData\n      .filter(s => s.label.toLowerCase().includes(q))\n      .slice(0, 8);\n    renderSuggestions(matches);\n  });\n\n  document.getElementById('search-input').addEventListener('keydown', (e)=>{\n    const items = searchSuggestionsEl.querySelectorAll('.suggestion-item');\n    if(!items.length) return;\n    if(e.key === 'ArrowDown'){\n      e.preventDefault();\n      activeSuggestionIndex = Math.min(activeSuggestionIndex + 1, items.length - 1);\n    } else if(e.key === 'ArrowUp'){\n      e.preventDefault();\n      activeSuggestionIndex = Math.max(activeSuggestionIndex - 1, 0);\n    } else if(e.key === 'Enter' && activeSuggestionIndex >= 0){\n      e.preventDefault();\n      items[activeSuggestionIndex].click();\n      return;\n    } else if(e.key === 'Escape'){\n      renderSuggestions([]);\n      return;\n    } else {\n      return;\n    }\n    items.forEach((el,i)=> el.classList.toggle('active', i === activeSuggestionIndex));\n  });\n\n  document.addEventListener('click', (e)=>{\n    if(!e.target.closest('#toolbar')) renderSuggestions([]);\n  });\n\n  // ---------- View-source deterrent: TEMPORARILY DISABLED ----------\n  // Right-click / DevTools blocking is off while debugging the search\n  // feature. Say the word and I'll switch it back on.\n\n})();\n"}],"bodyHtml":"\n\n\n\n\n\n<div id=\"loading\"><div class=\"spin\"></div><div>Loading the map…</div></div>\n<div id=\"map\"></div>\n<div id=\"area-toast\"></div>\n\n<div id=\"sticky-contact\">\n  <a id=\"sticky-whatsapp\" href=\"#\" target=\"_blank\" rel=\"noopener\" aria-label=\"Chat on WhatsApp\" title=\"Chat on WhatsApp\">\n    <svg viewBox=\"0 0 32 32\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M16.02 3C9.4 3 4 8.38 4 15c0 2.29.64 4.44 1.75 6.28L4 29l7.94-1.7A11.94 11.94 0 0 0 16.02 27C22.65 27 28 21.63 28 15S22.65 3 16.02 3zm0 21.7c-1.95 0-3.77-.55-5.32-1.5l-.38-.23-4.71 1.01 1-4.6-.25-.4A9.63 9.63 0 0 1 6.3 15c0-5.36 4.36-9.7 9.72-9.7 5.36 0 9.72 4.34 9.72 9.7 0 5.36-4.36 9.7-9.72 9.7zm5.34-7.27c-.29-.15-1.73-.86-2-.95-.27-.1-.46-.15-.66.15-.2.29-.76.95-.93 1.15-.17.19-.34.22-.63.07-.29-.15-1.2-.44-2.29-1.41-.85-.75-1.42-1.68-1.59-1.97-.17-.29-.02-.45.13-.6.13-.13.29-.34.44-.51.15-.17.19-.29.29-.49.1-.19.05-.36-.02-.51-.07-.15-.66-1.6-.9-2.19-.24-.57-.48-.5-.66-.5-.17-.01-.36-.01-.56-.01-.19 0-.51.07-.78.36-.27.29-1.02 1-1.02 2.43 0 1.43 1.05 2.82 1.19 3.01.15.19 2.06 3.15 5 4.42.7.3 1.24.48 1.67.62.7.22 1.34.19 1.84.12.56-.08 1.73-.71 1.98-1.39.24-.68.24-1.27.17-1.39-.07-.12-.26-.19-.55-.34z\"></path></svg>\n  </a>\n  <a id=\"sticky-call\" href=\"#\" aria-label=\"Call us\" title=\"Call us\">\n    <svg viewBox=\"0 0 32 32\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M23.9 19.6l-4.1-1.4a1.6 1.6 0 0 0-1.6.4l-1.8 1.8a15.6 15.6 0 0 1-6.8-6.8l1.8-1.8c.4-.4.6-1 .4-1.6l-1.4-4.1A1.6 1.6 0 0 0 9 5H6.2A1.6 1.6 0 0 0 4.6 6.8C5.2 16.7 13.3 24.8 23.2 25.4a1.6 1.6 0 0 0 1.8-1.6V21a1.6 1.6 0 0 0-1.1-1.4z\"></path></svg>\n  </a>\n</div>\n\n\n<div id=\"topbar\">\n  <div id=\"brand-stack\">\n    <div class=\"brand collapsed\">\n      <div class=\"brand-logos\">\n        <img src=\"/img/Mappingg.com PNG logo(1).png\" alt=\"Mappingg.com\" class=\"logo logo-main\" onerror=\"this.style.display='none'\">\n        <span class=\"brand-by\">by</span>\n        <img src=\"/img/Associattelogo.png\" alt=\"Associatte\" class=\"logo logo-secondary\" onerror=\"this.style.display='none'\">\n      </div>\n      <div class=\"brand-text\">\n        <h1>Associatte Interactive Map</h1>\n        <span>Live projects &amp; infrastructure</span>\n      </div>\n    </div>\n    <div id=\"intro-video-panel\"><div id=\"brand-video-wrap\"><div id=\"brand-video-frame\"></div></div></div>\n  </div>\n  <div id=\"toolbar\">\n    <input type=\"text\" id=\"search-input\" autocomplete=\"off\" placeholder=\"📍 Where do you want to buy? Try Mundhwa, Kharadi…\">\n    <div id=\"search-suggestions\"></div>\n    <button class=\"btn\" id=\"btn-basemap\" aria-label=\"Toggle satellite view\"><span class=\"btn-icon\">🛰️</span><span class=\"btn-label\"> Satellite</span></button>\n    <button class=\"btn\" id=\"btn-near-me\" title=\"See projects near your current location\" tabindex=\"0\" aria-label=\"Show Near Me\"><span class=\"btn-icon\">📍</span><span class=\"btn-label\"> Near Me</span></button>\n  </div>\n</div>\n\n\n<div id=\"legend\" class=\"collapsed\">\n  <h4 id=\"legend-toggle\">Legend <span id=\"legend-arrow\">▾</span></h4>\n  <div id=\"legend-content\">\n    <div class=\"row\"><span class=\"swatch\" style=\"background:var(--available)\"></span> Available</div>\n    <div class=\"row\"><span class=\"swatch\" style=\"background:var(--sold)\"></span> Sold</div>\n    <div class=\"row\"><span class=\"swatch\" style=\"background:var(--construction)\"></span> Under Construction</div>\n    <div class=\"row\"><span class=\"swatch\" style=\"background:var(--upcoming)\"></span> Upcoming</div>\n    <div class=\"row\" style=\"margin-top:6px;color:#9a8f7a;\">Tap a pin, then \"View Full Details\" to see pricing, configuration &amp; more.</div>\n  </div>\n</div>\n\n\n<div id=\"overlay-bg\">\n  <div id=\"lead-box\" style=\"display:none;\">\n    <h3 id=\"lead-title\">See full project details</h3>\n    <p>Quick details so we can follow up on this project for you.</p>\n    <div class=\"field\">\n      <label>I'm a</label>\n      <select id=\"lead-role\">\n        <option value=\"developer\">Developer / Builder</option>\n        <option value=\"agent\">Sales Agent / Broker / Channel Partner</option>\n        <option value=\"buyer\" selected>Buyer / Investor</option>\n      </select>\n    </div>\n    <div class=\"field\">\n      <label>Name</label>\n      <input type=\"text\" id=\"lead-name\" placeholder=\"Your full name\">\n    </div>\n    <div class=\"field\">\n      <label>WhatsApp Number</label>\n      <div style=\"display:flex;gap:8px;\">\n        <select id=\"lead-whatsapp-code\" style=\"flex:0 0 90px;\">\n        <option value=\"+93\">🇦🇫 +93</option><option value=\"+355\">🇦🇱 +355</option><option value=\"+213\">🇩🇿 +213</option><option value=\"+1684\">🇦🇸 +1684</option><option value=\"+376\">🇦🇩 +376</option><option value=\"+244\">🇦🇴 +244</option><option value=\"+1264\">🇦🇮 +1264</option><option value=\"+1268\">🇦🇬 +1268</option><option value=\"+54\">🇦🇷 +54</option><option value=\"+374\">🇦🇲 +374</option><option value=\"+297\">🇦🇼 +297</option><option value=\"+61\">🇦🇺 +61</option><option value=\"+43\">🇦🇹 +43</option><option value=\"+994\">🇦🇿 +994</option><option value=\"+1242\">🇧🇸 +1242</option><option value=\"+973\">🇧🇭 +973</option><option value=\"+880\">🇧🇩 +880</option><option value=\"+1246\">🇧🇧 +1246</option><option value=\"+375\">🇧🇾 +375</option><option value=\"+32\">🇧🇪 +32</option><option value=\"+501\">🇧🇿 +501</option><option value=\"+229\">🇧🇯 +229</option><option value=\"+1441\">🇧🇲 +1441</option><option value=\"+975\">🇧🇹 +975</option><option value=\"+591\">🇧🇴 +591</option><option value=\"+387\">🇧🇦 +387</option><option value=\"+267\">🇧🇼 +267</option><option value=\"+55\">🇧🇷 +55</option><option value=\"+673\">🇧🇳 +673</option><option value=\"+359\">🇧🇬 +359</option><option value=\"+226\">🇧🇫 +226</option><option value=\"+257\">🇧🇮 +257</option><option value=\"+855\">🇰🇭 +855</option><option value=\"+237\">🇨🇲 +237</option><option value=\"+1\">🇨🇦 +1</option><option value=\"+238\">🇨🇻 +238</option><option value=\"+1345\">🇰🇾 +1345</option><option value=\"+236\">🇨🇫 +236</option><option value=\"+235\">🇹🇩 +235</option><option value=\"+56\">🇨🇱 +56</option><option value=\"+86\">🇨🇳 +86</option><option value=\"+57\">🇨🇴 +57</option><option value=\"+269\">🇰🇲 +269</option><option value=\"+242\">🇨🇬 +242</option><option value=\"+243\">🇨🇩 +243</option><option value=\"+682\">🇨🇰 +682</option><option value=\"+506\">🇨🇷 +506</option><option value=\"+385\">🇭🇷 +385</option><option value=\"+53\">🇨🇺 +53</option><option value=\"+357\">🇨🇾 +357</option><option value=\"+420\">🇨🇿 +420</option><option value=\"+45\">🇩🇰 +45</option><option value=\"+253\">🇩🇯 +253</option><option value=\"+1767\">🇩🇲 +1767</option><option value=\"+1809\">🇩🇴 +1809</option><option value=\"+593\">🇪🇨 +593</option><option value=\"+20\">🇪🇬 +20</option><option value=\"+503\">🇸🇻 +503</option><option value=\"+240\">🇬🇶 +240</option><option value=\"+291\">🇪🇷 +291</option><option value=\"+372\">🇪🇪 +372</option><option value=\"+268\">🇸🇿 +268</option><option value=\"+251\">🇪🇹 +251</option><option value=\"+679\">🇫🇯 +679</option><option value=\"+358\">🇫🇮 +358</option><option value=\"+33\">🇫🇷 +33</option><option value=\"+594\">🇬🇫 +594</option><option value=\"+689\">🇵🇫 +689</option><option value=\"+241\">🇬🇦 +241</option><option value=\"+220\">🇬🇲 +220</option><option value=\"+995\">🇬🇪 +995</option><option value=\"+49\">🇩🇪 +49</option><option value=\"+233\">🇬🇭 +233</option><option value=\"+350\">🇬🇮 +350</option><option value=\"+30\">🇬🇷 +30</option><option value=\"+299\">🇬🇱 +299</option><option value=\"+1473\">🇬🇩 +1473</option><option value=\"+590\">🇬🇵 +590</option><option value=\"+1671\">🇬🇺 +1671</option><option value=\"+502\">🇬🇹 +502</option><option value=\"+224\">🇬🇳 +224</option><option value=\"+245\">🇬🇼 +245</option><option value=\"+592\">🇬🇾 +592</option><option value=\"+509\">🇭🇹 +509</option><option value=\"+504\">🇭🇳 +504</option><option value=\"+852\">🇭🇰 +852</option><option value=\"+36\">🇭🇺 +36</option><option value=\"+354\">🇮🇸 +354</option><option value=\"+91\" selected>🇮🇳 +91</option><option value=\"+62\">🇮🇩 +62</option><option value=\"+98\">🇮🇷 +98</option><option value=\"+964\">🇮🇶 +964</option><option value=\"+353\">🇮🇪 +353</option><option value=\"+972\">🇮🇱 +972</option><option value=\"+39\">🇮🇹 +39</option><option value=\"+1876\">🇯🇲 +1876</option><option value=\"+81\">🇯🇵 +81</option><option value=\"+962\">🇯🇴 +962</option><option value=\"+7\">🇰🇿 +7</option><option value=\"+254\">🇰🇪 +254</option><option value=\"+686\">🇰🇮 +686</option><option value=\"+965\">🇰🇼 +965</option><option value=\"+996\">🇰🇬 +996</option><option value=\"+856\">🇱🇦 +856</option><option value=\"+371\">🇱🇻 +371</option><option value=\"+961\">🇱🇧 +961</option><option value=\"+266\">🇱🇸 +266</option><option value=\"+231\">🇱🇷 +231</option><option value=\"+218\">🇱🇾 +218</option><option value=\"+423\">🇱🇮 +423</option><option value=\"+370\">🇱🇹 +370</option><option value=\"+352\">🇱🇺 +352</option><option value=\"+853\">🇲🇴 +853</option><option value=\"+261\">🇲🇬 +261</option><option value=\"+265\">🇲🇼 +265</option><option value=\"+60\">🇲🇾 +60</option><option value=\"+960\">🇲🇻 +960</option><option value=\"+223\">🇲🇱 +223</option><option value=\"+356\">🇲🇹 +356</option><option value=\"+692\">🇲🇭 +692</option><option value=\"+596\">🇲🇶 +596</option><option value=\"+222\">🇲🇷 +222</option><option value=\"+230\">🇲🇺 +230</option><option value=\"+52\">🇲🇽 +52</option><option value=\"+691\">🇫🇲 +691</option><option value=\"+373\">🇲🇩 +373</option><option value=\"+377\">🇲🇨 +377</option><option value=\"+976\">🇲🇳 +976</option><option value=\"+382\">🇲🇪 +382</option><option value=\"+1664\">🇲🇸 +1664</option><option value=\"+212\">🇲🇦 +212</option><option value=\"+258\">🇲🇿 +258</option><option value=\"+95\">🇲🇲 +95</option><option value=\"+264\">🇳🇦 +264</option><option value=\"+674\">🇳🇷 +674</option><option value=\"+977\">🇳🇵 +977</option><option value=\"+31\">🇳🇱 +31</option><option value=\"+687\">🇳🇨 +687</option><option value=\"+64\">🇳🇿 +64</option><option value=\"+505\">🇳🇮 +505</option><option value=\"+227\">🇳🇪 +227</option><option value=\"+234\">🇳🇬 +234</option><option value=\"+683\">🇳🇺 +683</option><option value=\"+850\">🇰🇵 +850</option><option value=\"+389\">🇲🇰 +389</option><option value=\"+47\">🇳🇴 +47</option><option value=\"+968\">🇴🇲 +968</option><option value=\"+92\">🇵🇰 +92</option><option value=\"+680\">🇵🇼 +680</option><option value=\"+970\">🇵🇸 +970</option><option value=\"+507\">🇵🇦 +507</option><option value=\"+675\">🇵🇬 +675</option><option value=\"+595\">🇵🇾 +595</option><option value=\"+51\">🇵🇪 +51</option><option value=\"+63\">🇵🇭 +63</option><option value=\"+48\">🇵🇱 +48</option><option value=\"+351\">🇵🇹 +351</option><option value=\"+1787\">🇵🇷 +1787</option><option value=\"+974\">🇶🇦 +974</option><option value=\"+262\">🇷🇪 +262</option><option value=\"+40\">🇷🇴 +40</option><option value=\"+7\">🇷🇺 +7</option><option value=\"+250\">🇷🇼 +250</option><option value=\"+685\">🇼🇸 +685</option><option value=\"+378\">🇸🇲 +378</option><option value=\"+239\">🇸🇹 +239</option><option value=\"+966\">🇸🇦 +966</option><option value=\"+221\">🇸🇳 +221</option><option value=\"+381\">🇷🇸 +381</option><option value=\"+248\">🇸🇨 +248</option><option value=\"+232\">🇸🇱 +232</option><option value=\"+65\">🇸🇬 +65</option><option value=\"+421\">🇸🇰 +421</option><option value=\"+386\">🇸🇮 +386</option><option value=\"+677\">🇸🇧 +677</option><option value=\"+252\">🇸🇴 +252</option><option value=\"+27\">🇿🇦 +27</option><option value=\"+82\">🇰🇷 +82</option><option value=\"+211\">🇸🇸 +211</option><option value=\"+34\">🇪🇸 +34</option><option value=\"+94\">🇱🇰 +94</option><option value=\"+1869\">🇰🇳 +1869</option><option value=\"+1758\">🇱🇨 +1758</option><option value=\"+1784\">🇻🇨 +1784</option><option value=\"+249\">🇸🇩 +249</option><option value=\"+597\">🇸🇷 +597</option><option value=\"+46\">🇸🇪 +46</option><option value=\"+41\">🇨🇭 +41</option><option value=\"+963\">🇸🇾 +963</option><option value=\"+886\">🇹🇼 +886</option><option value=\"+992\">🇹🇯 +992</option><option value=\"+255\">🇹🇿 +255</option><option value=\"+66\">🇹🇭 +66</option><option value=\"+670\">🇹🇱 +670</option><option value=\"+228\">🇹🇬 +228</option><option value=\"+676\">🇹🇴 +676</option><option value=\"+1868\">🇹🇹 +1868</option><option value=\"+216\">🇹🇳 +216</option><option value=\"+90\">🇹🇷 +90</option><option value=\"+993\">🇹🇲 +993</option><option value=\"+1649\">🇹🇨 +1649</option><option value=\"+688\">🇹🇻 +688</option><option value=\"+256\">🇺🇬 +256</option><option value=\"+380\">🇺🇦 +380</option><option value=\"+971\">🇦🇪 +971</option><option value=\"+44\">🇬🇧 +44</option><option value=\"+1\">🇺🇸 +1</option><option value=\"+598\">🇺🇾 +598</option><option value=\"+998\">🇺🇿 +998</option><option value=\"+678\">🇻🇺 +678</option><option value=\"+379\">🇻🇦 +379</option><option value=\"+58\">🇻🇪 +58</option><option value=\"+84\">🇻🇳 +84</option><option value=\"+1284\">🇻🇬 +1284</option><option value=\"+1340\">🇻🇮 +1340</option><option value=\"+967\">🇾🇪 +967</option><option value=\"+260\">🇿🇲 +260</option><option value=\"+263\">🇿🇼 +263</option>\n        </select>\n        <input type=\"tel\" id=\"lead-whatsapp\" placeholder=\"10-digit number\" maxlength=\"10\" inputmode=\"numeric\" style=\"flex:1;\">\n      </div>\n    </div>\n    <div class=\"field\">\n      <label>Email</label>\n      <input type=\"email\" id=\"lead-email\" placeholder=\"you@example.com\">\n    </div>\n    <div id=\"lead-consent-row\">\n      <input type=\"checkbox\" id=\"lead-consent\" checked>\n      <label for=\"lead-consent\">I Consent to The Processing of Provided Data According To Privacy Policy | Terms &amp; Conditions, I Authorize Associatte and its representatives to Call, SMS, Email or WhatsApp Me About Its Products and Offers. This Consent Overrides Any Registration For DNC/NDNC.</label>\n    </div>\n    <div id=\"lead-error\"></div>\n    <div class=\"lead-actions\">\n      <button id=\"lead-cancel\">Cancel</button>\n      <button id=\"lead-submit\">View Details</button>\n    </div>\n  </div>\n\n  \n  <div id=\"detail-panel\">\n    <div id=\"detail-head\">\n      <button id=\"detail-close\">✕</button>\n      <img id=\"detail-img\" style=\"display:none;\">\n    </div>\n    <div id=\"detail-body\"></div>\n  </div>\n</div>\n\n\n<div id=\"legal-overlay\">\n  <div id=\"legal-box\">\n    <div id=\"legal-box-head\">\n      <h2 id=\"legal-title\">Privacy Policy</h2>\n      <button id=\"legal-close\">✕</button>\n    </div>\n    <div id=\"legal-body\"></div>\n  </div>\n</div>\n\n\n\n\n\n"}