/* =========================================================
   HOUSEATOR — Public homepage logic
   (Listings grid, mortgage calculator, modal, theme,
    hero search, favorites). Auth/dashboard live elsewhere.
   ========================================================= */

HOUSEATOR.setupTheme();
HOUSEATOR.setupModal();

/* Back-to-top floating button — appears after the user scrolls past ~300px,
   smooth-scrolls to the top on click. */
(function setupBackToTop() {
  const btn = document.getElementById("backToTop");
  if (!btn) return;
  const onScroll = () => {
    btn.classList.toggle("visible", window.scrollY > 300);
  };
  window.addEventListener("scroll", onScroll, { passive: true });
  btn.addEventListener("click", () => {
    window.scrollTo({ top: 0, behavior: "smooth" });
  });
  onScroll();
})();

/* Mobile hamburger — toggles .mobile-open on the header which expands the
   .nav-links slide-down panel. Closes on link tap, outside click, and ESC. */
(function setupMobileMenu() {
  const header   = document.querySelector(".site-header");
  const burger   = document.getElementById("navHamburger");
  const navLinks = document.getElementById("primaryNav");
  const overlay  = document.getElementById("mobileMenu");
  if (!header || !burger || !navLinks) return;
  const setOpen = (open) => {
    header.classList.toggle("mobile-open", open);
    if (overlay) {
      overlay.classList.toggle("is-open", open);
      overlay.setAttribute("aria-hidden", open ? "false" : "true");
    }
    document.body.classList.toggle("mobile-menu-open", open);
    burger.setAttribute("aria-expanded", open ? "true" : "false");
    burger.setAttribute("aria-label", open ? "Close menu" : "Open menu");
  };
  burger.addEventListener("click", (e) => {
    e.stopPropagation();
    setOpen(!header.classList.contains("mobile-open"));
  });
  navLinks.addEventListener("click", (e) => {
    if (e.target.closest("a")) setOpen(false);
  });
  if (overlay) {
    /* Pick a random reveal image per page load. Local route-67.jpg is
       already optimized (~185KB); the Unsplash URLs are CDN-optimized
       via auto=format (serves WebP/AVIF), w=1280 (caps resolution), and
       q=70 (compression) so each weighs in well under 200KB. */
    const REVEAL_IMAGES = [
      "images/route-67.jpg",
      "https://images.unsplash.com/photo-1568605114967-8130f3a36994?auto=format&fit=crop&w=1280&q=70",
      "https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1280&q=70",
      "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?auto=format&fit=crop&w=1280&q=70",
      "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1280&q=70",
      "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1280&q=70"
    ];
    const pick = REVEAL_IMAGES[Math.floor(Math.random() * REVEAL_IMAGES.length)];
    /* Preload so the photo is decoded before the drag-reveal exposes it. */
    const pre = new Image();
    pre.src = pick;
    overlay.style.setProperty("--reveal-img", `url("${pick}")`);

    /* Take over link navigation so the menu can animate out cleanly before
       the anchor jump or page change. Without this the browser scrolls or
       navigates while the overlay is still on screen, which makes the tap
       feel inert. Hash links smooth-scroll to the section after a short
       delay; page links (login.html, etc.) navigate after the same delay. */
    overlay.addEventListener("click", (e) => {
      const link = e.target.closest("a");
      if (!link) return;
      const href = link.getAttribute("href");
      if (!href) return;
      e.preventDefault();
      setOpen(false);
      setTimeout(() => {
        if (href.startsWith("#")) {
          const target = document.querySelector(href);
          if (target) target.scrollIntoView({ behavior: "smooth", block: "start" });
        } else {
          window.location.href = href;
        }
      }, 280);
    });
    /* Drive the soft-hole + frosted-disc effect ported from active-c2.
       Updates --mx/--my CSS vars on cursor move (desktop preview) and on
       touch start/move (the primary mobile interaction).
       Additionally, when the pointer is near the center of the menu, grow
       --reveal-r / --reveal-feather so the gradient mask opens to full
       screen and the background photograph is revealed. The frosted disc
       (--hole-r / --hole-feather) stays small so the image isn't blurred. */
    const updatePos = (x, y) => {
      overlay.style.setProperty("--mx", x + "px");
      overlay.style.setProperty("--my", y + "px");
      const rect = overlay.getBoundingClientRect();
      const cx = rect.width  / 2;
      const cy = rect.height / 2;
      const dist = Math.hypot(x - cx, y - cy);
      const diag = Math.hypot(cx, cy);
      /* proximity = 1 at exact center, ramps to 0 outside ~40% of the
         distance to the corner. Squared for a snappier, more deliberate
         reveal — you have to actually aim for the middle. */
      const t = Math.max(0, 1 - dist / (diag * 0.4));
      const eased = t * t;
      const baseR       = 110;
      const baseFeather = 240;
      const fullR       = Math.max(rect.width, rect.height) * 1.4;
      const fullFeather = Math.max(rect.width, rect.height) * 1.8;
      overlay.style.setProperty("--reveal-r",       (baseR       + (fullR       - baseR)       * eased) + "px");
      overlay.style.setProperty("--reveal-feather", (baseFeather + (fullFeather - baseFeather) * eased) + "px");
    };
    overlay.addEventListener("mousemove", (e) => updatePos(e.clientX, e.clientY));
    overlay.addEventListener("touchstart", (e) => {
      const t = e.touches[0]; if (t) updatePos(t.clientX, t.clientY);
    }, { passive: true });
    overlay.addEventListener("touchmove", (e) => {
      const t = e.touches[0]; if (t) updatePos(t.clientX, t.clientY);
    }, { passive: true });
  }
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && header.classList.contains("mobile-open")) setOpen(false);
  });
})();

const PROPERTIES = HOUSEATOR.PROPERTIES;
const fmtPrice   = HOUSEATOR.fmtPrice;
const fmtUSD     = HOUSEATOR.fmtUSD;
const fmtUSDInt  = HOUSEATOR.fmtUSDInt;

/* =========================================================
   LISTINGS GRID
   ========================================================= */
const propertyGrid = document.getElementById("propertyGrid");
const sortSelect = document.getElementById("sortSelect");
const categoryChips = document.getElementById("categoryChips");
let activeFilter = "all";
let activeSort = "featured";

function matchesCategory(p, filter) {
  if (filter === "all") return true;
  if (filter === "house") return p.type === "House";
  if (filter === "condo") return p.type === "Condo";
  if (filter === "townhouse") return p.type === "Townhouse";
  if (filter === "luxury") return p.type === "Luxury";
  if (filter === "new") return p.daysOnMarket <= 10;
  return true;
}

/* Map-section filter scores. Each property's score is derived from its
   region — close enough for a demo, and avoids hard-coding scores per
   listing. Higher = better, except commute (lower minutes = better). */
const NEIGHBORHOOD_SCORES = {
  walkability: { Urban: 95, Coastal: 78, Suburban: 58, Mountain: 32, Rural: 22 },
  schools:     { Suburban: 95, Urban: 86, Coastal: 88, Mountain: 64, Rural: 58 },
  commute:     { Urban: 12, Coastal: 22, Suburban: 28, Mountain: 55, Rural: 45 },
  crime:       { Suburban: 92, Mountain: 90, Rural: 88, Coastal: 78, Urban: 64 },
  parks:       { Mountain: 96, Rural: 90, Suburban: 80, Coastal: 74, Urban: 58 }
};

/* Distance + time metrics per region — Urban is dense, Mountain is remote.
   Each listing gets a small deterministic jitter (based on its id) so the
   numbers feel listing-specific rather than identical within a region. */
const REGION_METRICS = {
  Coastal:  { schoolKm: 1.2, amenityKm: 0.8, highwayKm: 3.5, safety: 78, parkKm: 1.0 },
  Urban:    { schoolKm: 0.4, amenityKm: 0.2, highwayKm: 1.5, safety: 64, parkKm: 0.6 },
  Suburban: { schoolKm: 0.5, amenityKm: 1.4, highwayKm: 2.2, safety: 92, parkKm: 0.7 },
  Mountain: { schoolKm: 4.2, amenityKm: 5.5, highwayKm: 8.0, safety: 90, parkKm: 0.4 },
  Rural:    { schoolKm: 2.8, amenityKm: 3.2, highwayKm: 5.0, safety: 88, parkKm: 1.5 }
};

/* Deterministic per-listing jitter so two Urban listings show slightly
   different distances. Stays small (±~0.3) so it doesn't change rankings. */
function listingJitter(id, salt) {
  const x = Math.sin(id * 91 + (salt || 0)) * 10000;
  return (x - Math.floor(x)) - 0.5; /* -0.5 .. +0.5 */
}

function sortByMapFilter(list, filter) {
  const scores = NEIGHBORHOOD_SCORES[filter];
  if (!scores) return list;
  const arr = [...list];
  if (filter === "commute") {
    /* Lower minutes = better commute → ascending */
    return arr.sort((a, b) => (scores[a.region] ?? 99) - (scores[b.region] ?? 99));
  }
  return arr.sort((a, b) => (scores[b.region] ?? 0) - (scores[a.region] ?? 0));
}

function sortProperties(list, mode) {
  const arr = [...list];
  switch (mode) {
    case "priceAsc":  return arr.sort((a, b) => a.price - b.price);
    case "priceDesc": return arr.sort((a, b) => b.price - a.price);
    case "newest":    return arr.sort((a, b) => a.daysOnMarket - b.daysOnMarket);
    case "bedsDesc":  return arr.sort((a, b) => b.beds - a.beds);
    case "sqftDesc":  return arr.sort((a, b) => b.sqft - a.sqft);
    default:
      if (mode && mode.startsWith("map-")) return sortByMapFilter(list, mode.slice(4));
      return arr.sort((a, b) => b.lead - a.lead);
  }
}

function renderListings() {
  const list = sortProperties(
    PROPERTIES.filter(p => matchesCategory(p, activeFilter)),
    activeSort
  );

  propertyGrid.innerHTML = list.map(p => `
    <article class="property-card" data-id="${p.id}">
      <div class="media-block">
        <div class="image-carousel" data-carousel>
          <div class="carousel-track">
            ${p.images.map(src => `<img src="${src}" alt="${p.title}" loading="lazy" />`).join("")}
          </div>
          <button class="carousel-btn prev" aria-label="Previous image">‹</button>
          <button class="carousel-btn next" aria-label="Next image">›</button>
          <div class="carousel-dots"></div>
        </div>
        <span class="status-badge ${p.status.toLowerCase()}">${p.status}</span>
        <div class="price-tag">${fmtPrice(p.price)}</div>
      </div>
      <div class="property-content">
        <h3>${p.title}</h3>
        <p class="addr">${p.address}</p>
        <div class="property-features">
          ${p.tags.slice(0, 3).map(t => `<span class="tag">${t}</span>`).join("")}
        </div>
        <div class="specs">
          <span><b>${p.beds}</b> bd</span>
          <span><b>${p.baths}</b> ba</span>
          <span><b>${p.sqft.toLocaleString()}</b> ft²</span>
          <span><b>${p.year}</b></span>
        </div>
        <div class="property-stats" data-stats></div>
      </div>
      ${iconRailHtml(new Set())}
    </article>
  `).join("");
  /* Mobile per-card stat rail starts empty — each card seeds its (empty)
     stats stack so the box collapses until the user lights an icon. */
  propertyGrid.querySelectorAll(".property-card").forEach(renderCardStats);

  propertyGrid.querySelectorAll("[data-carousel]").forEach(HOUSEATOR.initCarousel);
  propertyGrid.querySelectorAll(".property-card").forEach(card => {
    card.addEventListener("click", (e) => {
      /* Mobile per-card stat rail: tapping an icon toggles that stat for
         this listing and must NOT open the property modal. */
      const ico = e.target.closest(".mr-ico");
      if (ico) {
        e.stopPropagation();
        e.preventDefault();
        applyRailToggle(ico, card);
        return;
      }
      HOUSEATOR.openPropertyModal(parseInt(card.dataset.id, 10));
    });
  });
}

/* Two chip groups now exist: the original under the listings header AND a
   duplicate above the map. Treat them as one — clicking in either group
   updates `activeFilter`, re-marks BOTH groups' active state, and re-renders
   the listings grid + map pins so the views stay in sync. */
const mapCategoryChips = document.getElementById("mapCategoryChips");
function syncCategoryChipsActive(filter) {
  [categoryChips, mapCategoryChips].forEach(group => {
    if (!group) return;
    group.querySelectorAll(".chip").forEach(c => {
      c.classList.toggle("active", c.dataset.filter === filter);
    });
  });
}
function handleCategoryChipClick(e) {
  const btn = e.target.closest(".chip");
  if (!btn) return;
  activeFilter = btn.dataset.filter;
  syncCategoryChipsActive(activeFilter);
  renderListings();
  /* Map markers also respect the filter so the visualisation matches */
  if (typeof renderDefaultPins === "function" && leafletMap) {
    renderDefaultPins();
  }
}
if (categoryChips)    categoryChips.addEventListener("click", handleCategoryChipClick);
if (mapCategoryChips) mapCategoryChips.addEventListener("click", handleCategoryChipClick);
if (sortSelect) {
  sortSelect.addEventListener("change", () => {
    activeSort = sortSelect.value;
    renderListings();
  });
}

/* =========================================================
   MORTGAGE CALCULATOR
   ========================================================= */
const homePrice       = document.getElementById("homePrice");
const downPayment     = document.getElementById("downPayment");
const downPercent     = document.getElementById("downPercent");
const downPercentLabel= document.getElementById("downPercentLabel");
const interestRate    = document.getElementById("interestRate");
const loanTerm        = document.getElementById("loanTerm");
const propertyTax     = document.getElementById("propertyTax");
const homeInsurance   = document.getElementById("homeInsurance");
const calcBtn         = document.getElementById("calculateButton");

function calculatePayment() {
  const price  = Math.max(0, +homePrice.value || 0);
  const down   = Math.min(price, Math.max(0, +downPayment.value || 0));
  const rate   = Math.max(0, +interestRate.value || 0) / 100 / 12;
  const term   = Math.max(1, +loanTerm.value || 30) * 12;
  const tax    = Math.max(0, +propertyTax.value || 0);
  const ins    = Math.max(0, +homeInsurance.value || 0);

  const loan = price - down;
  let pi;
  if (rate === 0) pi = loan / term;
  else pi = loan * (rate * Math.pow(1 + rate, term)) / (Math.pow(1 + rate, term) - 1);

  const taxMonthly = tax / 12;
  const insMonthly = ins / 12;
  const total = pi + taxMonthly + insMonthly;

  document.getElementById("monthlyPayment").textContent = fmtUSD(total);
  document.getElementById("piAmount").textContent       = fmtUSD(pi);
  document.getElementById("taxAmount").textContent      = fmtUSD(taxMonthly);
  document.getElementById("insAmount").textContent      = fmtUSD(insMonthly);
  document.getElementById("loanAmt").textContent        = fmtUSDInt(loan);
}

function syncDownFromPercent() {
  const pct = +downPercent.value;
  downPercentLabel.textContent = pct;
  downPayment.value = Math.round((+homePrice.value || 0) * pct / 100);
  calculatePayment();
}
function syncPercentFromDown() {
  const price = +homePrice.value || 1;
  const pct = Math.min(50, Math.max(0, Math.round(((+downPayment.value || 0) / price) * 100)));
  downPercent.value = pct;
  downPercentLabel.textContent = pct;
  calculatePayment();
}

if (calcBtn) {
  [homePrice, interestRate, loanTerm, propertyTax, homeInsurance].forEach(el =>
    el.addEventListener("input", calculatePayment)
  );
  homePrice.addEventListener("input", syncDownFromPercent);
  downPercent.addEventListener("input", syncDownFromPercent);
  downPayment.addEventListener("input", syncPercentFromDown);
  calcBtn.addEventListener("click", (e) => { e.preventDefault(); calculatePayment(); });
  calculatePayment();
}

/* =========================================================
   FAVORITES + HERO SEARCH + HERO CAROUSEL
   ========================================================= */
/* Hero "Featured" panel — render dynamically from the property the seller
   picked in the dashboard (HOUSEATOR.getFeaturedProperty). Falls back to
   the first non-sold listing when no explicit feature has been chosen, or
   when the chosen one is now sold. Runs BEFORE the .fav-btn / carousel
   global queries below so the dynamic markup gets wired up like the rest. */
(function renderHeroFeature() {
  const card = document.getElementById("heroFeature");
  if (!card) return;
  let feature = HOUSEATOR.getFeaturedProperty && HOUSEATOR.getFeaturedProperty();
  if (!feature || feature.status === "Sold") {
    feature = PROPERTIES.find(p => p.status !== "Sold") || PROPERTIES[0];
  }
  if (!feature) return;
  card.dataset.id = feature.id;
  card.innerHTML = `
    <div class="image-carousel" data-carousel>
      <div class="carousel-track">
        ${feature.images.map(src => `<img src="${src}" alt="${feature.title}" />`).join("")}
      </div>
      <button class="carousel-btn prev" aria-label="Previous image">‹</button>
      <button class="carousel-btn next" aria-label="Next image">›</button>
      <div class="carousel-dots"></div>
      <span class="badge featured-badge"><span class="feat-ghost" aria-hidden="true">Featured</span><span class="feat-main">Featured</span></span>
      <button class="fav-btn" aria-label="Save to favorites" title="Save">♡</button>
    </div>
    <div class="listing-body">
      <div class="listing-heading">
        <div>
          <h3>${feature.title}</h3>
          <p class="muted">${feature.address}</p>
        </div>
        <strong class="price">${fmtPrice(feature.price)}</strong>
      </div>
      <div class="listing-meta">
        <span><b>${feature.beds}</b> beds</span>
        <span><b>${feature.baths}</b> baths</span>
        <span><b>${feature.sqft.toLocaleString()}</b> ft²</span>
        <span><b>${feature.year}</b> built</span>
      </div>
      <div class="listing-tags">
        ${feature.tags.slice(0, 3).map(t => `<span class="tag">${t}</span>`).join("")}
      </div>
    </div>
  `;
  /* Body click (not on the carousel arrows or fav-btn) opens the modal */
  card.addEventListener("click", (e) => {
    if (e.target.closest(".carousel-btn") || e.target.closest(".fav-btn")) return;
    HOUSEATOR.openPropertyModal(feature.id);
  });
})();

document.querySelectorAll(".fav-btn").forEach(btn => {
  btn.addEventListener("click", (e) => {
    e.stopPropagation();
    btn.classList.toggle("active");
    btn.textContent = btn.classList.contains("active") ? "♥" : "♡";
  });
});

const heroSearch = document.getElementById("heroSearch");
if (heroSearch) {
  heroSearch.addEventListener("submit", (e) => {
    e.preventDefault();
    document.getElementById("listings").scrollIntoView({ behavior: "smooth", block: "start" });
  });
}

document.querySelectorAll(".hero-listing-card [data-carousel]").forEach(HOUSEATOR.initCarousel);

/* =========================================================
   FEATURED LISTING TITLE — "handwritten" reveal
   The cursive title's letters fade/ink in one after another, left to
   right, when the featured card scrolls into view — so the script reads
   as if it's being written. Non-space characters are wrapped in spans
   with a staggered animation-delay; spaces stay as text so the title can
   still wrap naturally on mobile. Replays every time it re-enters view.
   ========================================================= */
(function setupTitleWrite() {
  const h3 = document.querySelector(".hero-listing-card .listing-heading h3");
  if (!h3 || h3.dataset.twWired) return;
  h3.dataset.twWired = "1";

  const text = h3.textContent;
  const STEP = 0.055;                       /* seconds between letters */
  let idx = 0;
  h3.textContent = "";
  for (const ch of text) {
    if (ch === " ") {
      h3.appendChild(document.createTextNode(" "));
      continue;
    }
    const span = document.createElement("span");
    span.className = "tw-char";
    span.textContent = ch;
    span.style.animationDelay = (idx * STEP).toFixed(3) + "s";
    h3.appendChild(span);
    idx++;
  }

  const play = () => {
    h3.classList.remove("tw-go");
    void h3.offsetWidth;                     /* restart the CSS animation */
    h3.classList.add("tw-go");
  };

  if (!("IntersectionObserver" in window)) { play(); return; }
  const io = new IntersectionObserver((entries) => {
    entries.forEach(e => { if (e.isIntersecting) play(); });
  }, { threshold: 0.5 });
  io.observe(h3);
})();

/* =========================================================
   MAP LEGEND — click a filter to populate the in-map results panel.
   Sort + find both happen inside the Map section; we don't scroll
   the user to the listings grid below.
   ========================================================= */
const FILTER_LABELS = {
  walkability: "Walkability",
  schools:     "Schools",
  commute:     "Commute",
  crime:       "Low crime",
  parks:       "Parks"
};

const mapResults     = document.getElementById("mapResults");
const mapResultsList = document.getElementById("mapResultsList");
const mapResultsLabel = document.getElementById("mapResultsLabel");
const mapResultsCount = document.getElementById("mapResultsCount");
const mapResultsClose = document.getElementById("mapResultsClose");

function scoreFor(p, filter) {
  return (NEIGHBORHOOD_SCORES[filter] || {})[p.region] ?? 0;
}

/* Distance/time/safety metric for a listing under the active filter.
   Walkability + Schools render as kms, Commute as minutes, Crime as a
   100-point safety score. Each value gets per-listing jitter so cards
   in the same region don't look identical. */
function formatMetric(p, filter) {
  const m = REGION_METRICS[p.region];
  if (!m) return "—";
  const j = listingJitter(p.id, filter.length);
  switch (filter) {
    case "walkability": {
      const km = Math.max(0.1, m.amenityKm + j * 0.6);
      return `📍 ${km.toFixed(1)} km to amenities`;
    }
    case "schools": {
      const km = Math.max(0.1, m.schoolKm + j * 0.5);
      return `📍 ${km.toFixed(1)} km to school`;
    }
    case "commute": {
      const mins = Math.max(2, Math.round((NEIGHBORHOOD_SCORES.commute[p.region] || 30) + j * 6));
      return `⏱ ${mins} min to downtown`;
    }
    case "crime": {
      const score = Math.max(40, Math.min(99, Math.round(m.safety + j * 5)));
      return `🛡 Safety ${score}/100 <button type="button" class="metric-info" aria-label="What does this score mean?" title="What does this score mean?">i</button>`;
    }
    case "parks": {
      const km = Math.max(0.1, m.parkKm + j * 0.4);
      return `🌳 ${km.toFixed(1)} km to nearest park`;
    }
    default: return "";
  }
}

/* =========================================================
   INTERACTIVE MAP — Leaflet with OSM tiles. Markers are fully
   self-contained inline SVG so they cannot fail to render.
   ========================================================= */
/* Fallback center if fitBounds can't find any listings — roughly
   between San Diego coastal area and Julian to the east. */
const MAP_DEFAULT_CENTER = [32.93, -116.95];
const MAP_DEFAULT_ZOOM   = 10;

let leafletMap = null;
let markersById = {};

const CATEGORY_COLOR = {
  walkability: "#28a745",
  schools:     "#1e4fb7",
  commute:     "#f59e0b",
  crime:       "#5a2e91",
  parks:       "#8b5a2b"
};

/* Just the inner path data for each category icon (24x24 viewBox). */
const ICON_PATH = {
  walkability: '<circle cx="13" cy="4" r="2"/><path d="M9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6l1.8-.7"/>',
  schools:     '<path d="M5 13.18v4L12 21l7-3.82v-4L12 17zM12 3 1 9l11 6 9-4.91V17h2V9z"/>',
  commute:     '<path d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16m11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5M5 11l1.5-4.5h11L19 11z"/>'
  ,crime:      '<path d="M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5z"/>'
  ,parks:      '<path d="M17 12h2L12 2 5.05 12H7l-3.9 6h6.92v3h3.96v-3H21z"/>'
};

/* Inline SVG HTML for divIcon — renders directly in the DOM
   (no data-URL decoding step that can delay first paint). */
function defaultPinHtml(category) {
  const color = CATEGORY_COLOR[category] || "#1e4fb7";
  const path  = ICON_PATH[category] || "";
  return `<svg xmlns="http://www.w3.org/2000/svg" width="46" height="46" viewBox="0 0 46 46" style="display:block;">
    <circle cx="23" cy="23" r="20" fill="${color}" stroke="#ffffff" stroke-width="3"></circle>
    <g transform="translate(12 12) scale(0.917)" fill="#ffffff">${path}</g>
  </svg>`;
}

function rankPinHtml(filter, rank) {
  const color = CATEGORY_COLOR[filter] || "#1e4fb7";
  return `<svg xmlns="http://www.w3.org/2000/svg" width="46" height="46" viewBox="0 0 46 46" style="display:block;">
    <circle cx="23" cy="23" r="20" fill="${color}" stroke="#ffffff" stroke-width="3"></circle>
    <text x="23" y="29" text-anchor="middle" fill="#ffffff" font-family="Arial, sans-serif" font-size="17" font-weight="700">${rank}</text>
  </svg>`;
}

/* Multi-filter rank pin — outer ring is split into N equal segments, one per
   active filter, each in that filter's color. Inner disk is white so the
   rank number reads cleanly regardless of how many segments. */
function multiRankPinHtml(filters, rank) {
  const r = 18;
  const C = 2 * Math.PI * r;
  const segLen = C / filters.length;
  const segs = filters.map((f, i) => {
    const color = CATEGORY_COLOR[f] || "#1e4fb7";
    return `<circle cx="23" cy="23" r="${r}" fill="none" stroke="${color}" stroke-width="7"
              stroke-dasharray="${segLen} ${C - segLen}"
              stroke-dashoffset="${-i * segLen}"
              transform="rotate(-90 23 23)"/>`;
  }).join("");
  return `<svg xmlns="http://www.w3.org/2000/svg" width="46" height="46" viewBox="0 0 46 46" style="display:block;">
    <circle cx="23" cy="23" r="20" fill="#ffffff" stroke="#ffffff" stroke-width="3"/>
    ${segs}
    <circle cx="23" cy="23" r="13" fill="#ffffff"/>
    <text x="23" y="28" text-anchor="middle" fill="#0f172a" font-family="Arial, sans-serif" font-size="14" font-weight="700">${rank}</text>
  </svg>`;
}

function initLeafletMap() {
  if (typeof L === "undefined") {
    console.warn("[Houseator] Leaflet failed to load");
    return;
  }
  const host = document.getElementById("leafletMap");
  if (!host || leafletMap) return;
  try {
    leafletMap = L.map(host, {
      center: MAP_DEFAULT_CENTER,
      zoom: MAP_DEFAULT_ZOOM,
      minZoom: 4,
      maxZoom: 18,
      zoomControl: true,
      scrollWheelZoom: false   /* enabled on click — see hint logic below */
    });
    L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
      maxZoom: 19,
      attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(leafletMap);

    /* Click-to-engage scroll zoom (keeps page scroll natural) */
    const hint = document.createElement("div");
    hint.className = "map-scroll-hint";
    hint.textContent = "Click map to enable scroll-zoom";
    host.appendChild(hint);
    host.addEventListener("click", () => {
      leafletMap.scrollWheelZoom.enable();
      host.classList.add("scroll-zoom-active");
    });
    host.addEventListener("mouseleave", () => {
      leafletMap.scrollWheelZoom.disable();
      host.classList.remove("scroll-zoom-active");
    });

    /* Force Leaflet to recompute its container size in case the map div
       was sized between L.map() construction and now. */
    setTimeout(() => leafletMap && leafletMap.invalidateSize(), 0);

    /* Wait until the map's initial view has settled before adding markers.
       Adding too early can cause divIcons not to paint until the user
       interacts with the map. */
    leafletMap.whenReady(() => {
      renderDefaultPins();
    });

    /* Safety net — re-render after a short delay if no markers landed. */
    setTimeout(() => {
      if (Object.keys(markersById).length === 0) {
        console.log("[Houseator] Safety re-render of default pins");
        renderDefaultPins();
      }
    }, 600);

    console.log("[Houseator] Leaflet map initialised");
  } catch (e) {
    console.error("[Houseator] Leaflet init failed:", e);
  }
}
/* Run init now (script is at end of body so DOM is parsed) and on window load
   as a safety net if Leaflet was still parsing. */
initLeafletMap();
window.addEventListener("load", initLeafletMap);

function clearMapPins() {
  if (!leafletMap) return;
  Object.values(markersById).forEach(m => { try { m.remove(); } catch (_) {} });
  markersById = {};
}

/* The four neighborhood-insight glyphs, identical to the legend icons. */
const PIN_ICON_SVG = {
  walkability: `<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><circle cx="13" cy="4" r="2"/><path d="M9.8 8.9 7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6l1.8-.7"/></svg>`,
  schools:     `<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M5 13.18v4L12 21l7-3.82v-4L12 17zM12 3 1 9l11 6 9-4.91V17h2V9z"/></svg>`,
  commute:     `<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16m11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5M5 11l1.5-4.5h11L19 11z"/></svg>`,
  crime:       `<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5z"/></svg>`,
  parks:       `<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M17 12h2L12 2 5.05 12H7l-3.9 6h6.92v3h3.96v-3H21z"/></svg>`
};

const PIN_CAT_LABEL = {
  walkability: "Walkability",
  schools:     "Schools",
  commute:     "Commute",
  crime:       "Low crime",
  parks:       "Parks"
};

/* Each region has a "signature" lifestyle attribute, assigning every
   property one of the four legend categories so all four pin colours
   (green / blue / yellow / purple) appear on the map. */
const REGION_SIGNATURE = {
  Coastal:  "walkability",  /* beach boardwalks, walkable shoreline → GREEN */
  Urban:    "commute",      /* short transit-rich commute → YELLOW */
  Suburban: "schools",      /* family-friendly suburbs → BLUE */
  Mountain: "parks",        /* mountain trails + open space → BROWN */
  Rural:    "parks"         /* natural open land → BROWN */
};
function bestCategoryForProperty(p) {
  return REGION_SIGNATURE[p.region] || "walkability";
}

/* Mobile-only stats panel below the map. Populated when a marker is
   hovered (or tapped — Leaflet normalizes touch into mouseover). CSS
   keeps it display:none on desktop, so the event wiring is a no-op
   there. Re-uses NEIGHBORHOOD_SCORES (region-level data) so the panel
   shows the same numbers the legend chips drive. */
const mapHoverStats = document.getElementById("mapHoverStats");
let hoverStatsHideTimer = null;
function showMapHoverStats(p) {
  if (!mapHoverStats || !p) return;
  clearTimeout(hoverStatsHideTimer);
  const region = p.region;
  const walk    = (NEIGHBORHOOD_SCORES.walkability[region]) ?? "—";
  const school  = (NEIGHBORHOOD_SCORES.schools[region])     ?? "—";
  const commute = (NEIGHBORHOOD_SCORES.commute[region])     ?? "—";
  const crime   = (NEIGHBORHOOD_SCORES.crime[region])       ?? "—";
  const parks   = (NEIGHBORHOOD_SCORES.parks[region])       ?? "—";
  const sqftStr = (p.sqft || 0).toLocaleString();
  mapHoverStats.innerHTML = `
    <p class="stats-title">${p.title}</p>
    <p class="stats-address">${p.address}</p>
    <div class="stats-specs">
      <span class="stats-price">${fmtPrice(p.price)}</span>
      <span>${p.beds} bd · ${p.baths} ba · ${sqftStr} ft²</span>
      <span class="stats-region">${region} · ${p.type}</span>
    </div>
    <div class="stats-scores">
      <div class="sc-walk"><b>${walk}</b><small>Walk</small></div>
      <div class="sc-school"><b>${school}</b><small>School</small></div>
      <div class="sc-commute"><b>${commute}<span class="sc-unit">min</span></b><small>Commute</small></div>
      <div class="sc-crime"><b>${crime}</b><small>Crime</small></div>
      <div class="sc-parks"><b>${parks}</b><small>Parks</small></div>
    </div>
  `;
  mapHoverStats.classList.add("is-visible");
}
function hideMapHoverStats() {
  if (!mapHoverStats) return;
  /* Brief delay before clearing so quickly moving between two adjacent
     markers doesn't flash empty. The next showMapHoverStats cancels the
     timer. */
  hoverStatsHideTimer = setTimeout(() => {
    mapHoverStats.classList.remove("is-visible");
    /* Wait for the fade-out, then empty it so :empty { display:none }
       collapses the box. */
    setTimeout(() => {
      if (!mapHoverStats.classList.contains("is-visible")) {
        mapHoverStats.innerHTML = "";
      }
    }, 200);
  }, 120);
}

/* Default pins — L.divIcon with inline SVG. Markers render synchronously
   on first paint, no data-URL decoding step. */
function renderDefaultPins() {
  console.log("[Houseator] renderDefaultPins called");
  if (!leafletMap || typeof L === "undefined") {
    console.warn("[Houseator] renderDefaultPins: leaflet missing");
    return;
  }
  clearMapPins();
  let added = 0;
  /* Honor the active category filter from the listings/map chip groups
     so the map markers match what's visible in the listings grid. */
  const filtered = PROPERTIES.filter(p => matchesCategory(p, activeFilter));
  filtered.forEach(p => {
    if (p.lat == null || p.lng == null) return;
    try {
      const category = bestCategoryForProperty(p);
      const tooltipLabel = `${p.title} — best for ${PIN_CAT_LABEL[category]}`;
      const icon = L.divIcon({
        className: "cm-pin-leaflet",
        html: defaultPinHtml(category),
        iconSize: [46, 46],
        iconAnchor: [23, 23],
        tooltipAnchor: [0, -22]
      });
      const marker = L.marker([p.lat, p.lng], { icon, title: tooltipLabel, riseOnHover: true });
      marker.bindTooltip(tooltipLabel, { direction: "top" });
      marker.on("click", () => HOUSEATOR.openPropertyModal(p.id));
      marker.on("mouseover", () => showMapHoverStats(p));
      marker.on("mouseout",  () => hideMapHoverStats());
      marker.addTo(leafletMap);
      markersById[p.id] = marker;
      added++;
    } catch (e) {
      console.error("[Houseator] pin error for", p.title, e);
    }
  });
  console.log(`[Houseator] Rendered ${added} default pins`);

  /* Auto-fit the map so every pin in the SD region is visible. */
  fitMapToSdProperties();
}

function fitMapToSdProperties() {
  if (!leafletMap || typeof L === "undefined") return;
  const sdPoints = PROPERTIES
    .filter(p => p.lat != null && p.lng != null && p.lat < 33.5)
    .map(p => [p.lat, p.lng]);
  if (sdPoints.length < 1) return;
  try {
    leafletMap.fitBounds(sdPoints, { padding: [40, 40], maxZoom: 11, animate: false });
  } catch (_) {}
}

function renderRankPins(filters, sortedListings) {
  if (!leafletMap || typeof L === "undefined") return;
  const arr = Array.isArray(filters) ? filters : [filters];
  clearMapPins();
  const validListings = sortedListings.filter(p => p.lat != null && p.lng != null);
  const markersForBounds = [];
  const isMulti = arr.length > 1;
  validListings.forEach((p, idx) => {
    const html = isMulti ? multiRankPinHtml(arr, idx + 1) : rankPinHtml(arr[0], idx + 1);
    const icon = L.divIcon({
      className: "cm-pin-leaflet",
      html,
      iconSize: [46, 46],
      iconAnchor: [23, 23],
      tooltipAnchor: [0, -22]
    });
    const marker = L.marker([p.lat, p.lng], { icon, title: `${p.title} — #${idx + 1}`, riseOnHover: true });
    marker.bindTooltip(`#${idx + 1} ${p.title}`, { direction: "top" });
    marker.on("mouseover", () => showMapHoverStats(p));
    marker.on("mouseout",  () => hideMapHoverStats());
    marker.on("click", () => {
      const card = mapResultsList.querySelector(`.map-result[data-id="${p.id}"]`);
      if (card) {
        card.scrollIntoView({ behavior: "smooth", block: "nearest" });
        card.classList.add("flash");
        setTimeout(() => card.classList.remove("flash"), 800);
      }
      HOUSEATOR.openPropertyModal(p.id);
    });
    marker.addTo(leafletMap);
    markersById[p.id] = marker;
    markersForBounds.push(marker);
  });
  if (markersForBounds.length) {
    try {
      const group = L.featureGroup(markersForBounds);
      leafletMap.fitBounds(group.getBounds().pad(0.2), { animate: true, maxZoom: 13 });
    } catch (_) {}
  }
}

function highlightMarkerForProperty(propertyId) {
  const marker = markersById[propertyId];
  if (!marker || !marker.getElement) return;
  const el = marker.getElement();
  if (el) el.classList.add("marker-highlighted");
}
function unhighlightMarkerForProperty(propertyId) {
  const marker = markersById[propertyId];
  if (!marker || !marker.getElement) return;
  const el = marker.getElement();
  if (el) el.classList.remove("marker-highlighted");
}

function renderMapPins(filters, sortedListings) {
  renderRankPins(filters, sortedListings);
}
function hideMapResultsResetView() {
  if (leafletMap) leafletMap.setView(MAP_DEFAULT_CENTER, MAP_DEFAULT_ZOOM, { animate: true });
}

/* Multi-select state — Set of active filter keys. Empty = no overlay. */
const activeFilters = new Set();

/* Per-filter 0..100 score where higher = better fit for that property
   (commute is "lower minutes = better", so we invert). Used as the basic
   building block for combined ranking when multiple filters are active. */
function normalizedScoreFor(p, filter) {
  const raw = (NEIGHBORHOOD_SCORES[filter] || {})[p.region];
  if (raw == null) return 0;
  if (filter === "commute") {
    /* commute values are minutes (12..55-ish). Invert to 0..100 fit. */
    return Math.max(0, Math.min(100, 100 - raw));
  }
  return raw;
}
function combinedScore(p, filters) {
  if (!filters.length) return 0;
  let sum = 0;
  filters.forEach(f => { sum += normalizedScoreFor(p, f); });
  return sum / filters.length;
}

/* Build the stacked metric rows shown on a result card, one row per
   currently-active filter. Each row carries its own color tint so the
   reader can match the pin's segment colors back to a line item. */
function metricRowsHtml(p, filters) {
  return filters.map(f => {
    const color = CATEGORY_COLOR[f] || "#1e4fb7";
    const ico = PIN_ICON_SVG[f] || "";
    return `<div class="metric-row" data-filter="${f}" style="--c:${color}">
      <span class="metric-row-ico">${ico}</span>
      <span class="metric-row-val">${formatMetric(p, f)}</span>
    </div>`;
  }).join("");
}

/* Mobile-only per-card icon rail: five small colored category buttons
   down the side of each result card. Tapping one toggles that single
   stat for that one property (independent of the global legend filter).
   activeSet seeds which icons start lit — we default to the globally
   active filters so the card still reflects the legend selection, but
   each card can then be tuned individually. */
/* 2×2 grid glyph for the All / Reset master toggle. */
const ALL_ICON_SVG = '<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="7.5" height="7.5" rx="1.6"/><rect x="13.5" y="3" width="7.5" height="7.5" rx="1.6"/><rect x="3" y="13.5" width="7.5" height="7.5" rx="1.6"/><rect x="13.5" y="13.5" width="7.5" height="7.5" rx="1.6"/></svg>';

function iconRailHtml(activeSet) {
  const cats = Object.keys(CATEGORY_COLOR);
  const allOn = cats.length > 0 && cats.every(f => activeSet.has(f));
  /* Leading master control: flips every category on, or — when they're
     all already on — clears them (reset). Title reflects the next action. */
  const allBtn =
    `<button type="button" class="mr-ico mr-ico-all${allOn ? " active" : ""}" ` +
    `data-stat="all" style="--c:#64748b" aria-pressed="${allOn}" ` +
    `title="${allOn ? "Reset — clear all stats" : "Show all stats"}" ` +
    `aria-label="${allOn ? "Reset all stats" : "Show all stats"}">${ALL_ICON_SVG}</button>`;
  return `<div class="map-result-iconrail" role="group" aria-label="Toggle stats for this listing">` +
    allBtn +
    cats.map(f => {
      const on = activeSet.has(f);
      return `<button type="button" class="mr-ico mr-ico-${f}${on ? " active" : ""}" ` +
        `data-stat="${f}" style="--c:${CATEGORY_COLOR[f]}" ` +
        `aria-pressed="${on}" title="${FILTER_LABELS[f]}" aria-label="${FILTER_LABELS[f]}">` +
        `${PIN_ICON_SVG[f] || ""}</button>`;
    }).join("") +
  `</div>`;
}

/* Keep the All/Reset button's lit state + label in sync with whether
   every category icon in this card's rail is active. */
function syncRailAllButton(card) {
  const rail = card && card.querySelector(".map-result-iconrail");
  if (!rail) return;
  const allBtn = rail.querySelector('.mr-ico[data-stat="all"]');
  if (!allBtn) return;
  const cats = [...rail.querySelectorAll('.mr-ico[data-stat]:not([data-stat="all"])')];
  const allOn = cats.length > 0 && cats.every(b => b.classList.contains("active"));
  allBtn.classList.toggle("active", allOn);
  allBtn.setAttribute("aria-pressed", allOn ? "true" : "false");
  allBtn.title = allOn ? "Reset — clear all stats" : "Show all stats";
  allBtn.setAttribute("aria-label", allOn ? "Reset all stats" : "Show all stats");
}

/* Shared rail click logic for every variant (listings grid, map-results
   panel, property modal). Handles the All/Reset master button and the
   individual category toggles, then re-renders that card's stats. */
function applyRailToggle(ico, card) {
  if (!ico || !card) return;
  if (ico.dataset.stat === "all") {
    const rail = card.querySelector(".map-result-iconrail");
    const cats = rail ? [...rail.querySelectorAll('.mr-ico[data-stat]:not([data-stat="all"])')] : [];
    const allOn = cats.length > 0 && cats.every(b => b.classList.contains("active"));
    const next = !allOn;          /* all on → clear; otherwise → show all */
    cats.forEach(b => {
      b.classList.toggle("active", next);
      b.setAttribute("aria-pressed", next ? "true" : "false");
    });
  } else {
    const on = ico.classList.toggle("active");
    ico.setAttribute("aria-pressed", on ? "true" : "false");
  }
  syncRailAllButton(card);
  renderCardStats(card);
}

/* Rebuild a card's mobile stats stack from whichever rail icons are lit.
   DOM-driven so the rail's .active classes are the single source of
   truth — no parallel state to keep in sync. Works for any card that
   has a [data-stats] container + .mr-ico rail (map-result panel cards
   AND the main listings grid). */
function renderCardStats(card) {
  if (!card) return;
  const statsBox = card.querySelector("[data-stats]");
  if (!statsBox) return;
  const id = parseInt(card.dataset.id, 10);
  const p = PROPERTIES.find(x => x.id === id);
  if (!p) { statsBox.innerHTML = ""; return; }
  const active = [...card.querySelectorAll(".mr-ico.active")]
    .map(b => b.dataset.stat)
    .filter(s => CATEGORY_COLOR[s]);   /* drop the "all" master button */
  statsBox.innerHTML = active.map(f => {
    const color = CATEGORY_COLOR[f] || "#1e4fb7";
    return `<div class="metric-row" data-filter="${f}" style="--c:${color}">
      <span class="metric-row-ico">${PIN_ICON_SVG[f] || ""}</span>
      <span class="metric-row-val">${formatMetric(p, f)}</span>
    </div>`;
  }).join("");
}

/* Label text for the panel header — "Walkability + Parks" or
   "Walkability, Schools, +2 more" once the list grows past three. */
function combinedLabel(filters) {
  const names = filters.map(f => FILTER_LABELS[f] || f);
  if (names.length <= 2) return names.join(" + ");
  if (names.length === 3) return names.join(", ");
  return `${names.slice(0, 2).join(", ")} + ${names.length - 2} more`;
}

function refreshMapResults() {
  const filters = [...activeFilters];
  if (!filters.length) { hideMapResults(); return; }

  /* Sort by combined normalized score across all selected filters. We
     show every listing so users see the full ranking, not just top 6. */
  const sorted = [...PROPERTIES].sort((a, b) =>
    combinedScore(b, filters) - combinedScore(a, filters)
  );

  mapResultsLabel.textContent = filters.length === 1
    ? `Top by ${FILTER_LABELS[filters[0]] || filters[0]}`
    : `Ranked by ${combinedLabel(filters)}`;
  mapResultsCount.textContent = `${sorted.length} of ${PROPERTIES.length} listings · ${filters.length} overlay${filters.length > 1 ? "s" : ""}`;

  const isMulti = filters.length > 1;
  const primaryFilter = filters[0];
  /* The rank chip uses the single-filter color when only one is active,
     and a multi-stripe conic gradient when multiple are active. */
  const rankChipStyle = isMulti
    ? `style="background: conic-gradient(${filters.map((f, i) => {
        const start = Math.round((i / filters.length) * 100);
        const end   = Math.round(((i + 1) / filters.length) * 100);
        return `${CATEGORY_COLOR[f]} ${start}% ${end}%`;
      }).join(", ")});"`
    : "";

  const activeSet = new Set(filters);
  mapResultsList.innerHTML = sorted.map((p, i) => `
    <article class="map-result${isMulti ? " is-multi" : ""}" data-id="${p.id}">
      <span class="map-result-rank ${isMulti ? "multi" : `filter-${primaryFilter}`}" ${rankChipStyle}>
        <span class="map-result-rank-num">${i + 1}</span>
      </span>
      <img src="${p.images[0]}" alt="" loading="lazy" />
      <div class="map-result-body">
        <strong>${p.title}</strong>
        <button type="button" class="map-result-address" data-zoom-id="${p.id}" title="Zoom the map to this address">
          <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>
          ${p.address}
        </button>
        <div class="map-result-metrics">${metricRowsHtml(p, filters)}</div>
        <div class="map-result-stats" data-stats></div>
        <div class="map-result-foot">
          <span class="map-result-price">${fmtPrice(p.price)}</span>
          <span class="map-result-region">${p.region}</span>
        </div>
      </div>
      ${iconRailHtml(activeSet)}
    </article>
  `).join("");
  /* Hover handlers attach per-card (mouseenter/leave don't bubble);
     also seed each card's mobile stats stack from its lit rail icons. */
  mapResultsList.querySelectorAll(".map-result").forEach(card => {
    const id = parseInt(card.dataset.id, 10);
    card.addEventListener("mouseenter", () => highlightMarkerForProperty(id));
    card.addEventListener("mouseleave", () => unhighlightMarkerForProperty(id));
    renderCardStats(card);
  });
  mapResults.hidden = false;
  renderMapPins(filters, sorted);
}

/* Backwards-compatible wrapper kept for anything else that called the
   old single-filter API (hero quick-filter buttons, etc.). */
function showMapResults(filter) {
  activeFilters.clear();
  activeFilters.add(filter);
  syncLegendActiveClasses();
  refreshMapResults();
}

function syncLegendActiveClasses() {
  document.querySelectorAll(".legend-row").forEach(row => {
    row.classList.toggle("active", activeFilters.has(row.dataset.mapFilter));
  });
}

/* Delegated click handler — attached ONCE to the results list.
   Handles both address-click-to-zoom and card-click-to-open-modal. */
if (mapResultsList && !mapResultsList.dataset.clickWired) {
  mapResultsList.dataset.clickWired = "1";
  mapResultsList.addEventListener("click", (e) => {
    /* Per-card stat toggle (mobile icon rail). Checked first so a tap on
       an icon never bubbles up to open the property modal. */
    const ico = e.target.closest(".mr-ico");
    if (ico) {
      e.stopPropagation();
      e.preventDefault();
      applyRailToggle(ico, ico.closest(".map-result"));
      return;
    }
    /* The safety "i" button has its own dedicated listener that shows
       the rating popover — don't also open the property modal for it. */
    if (e.target.closest(".metric-info")) return;
    const addressBtn = e.target.closest(".map-result-address");
    if (addressBtn) {
      e.stopPropagation();
      e.preventDefault();
      const id = parseInt(addressBtn.dataset.zoomId, 10);
      console.log("[Houseator] Address clicked, zooming to property", id);
      zoomToProperty(id);
      return;
    }
    const card = e.target.closest(".map-result");
    if (card) {
      const id = parseInt(card.dataset.id, 10);
      HOUSEATOR.openPropertyModal(id);
    }
  });
}

/* Zoom the map to a property and pulse its marker.
   Uses setView (instant + animated) which is more reliable than flyTo. */
function zoomToProperty(id) {
  console.log("[Houseator] zoomToProperty called, id =", id);
  const p = PROPERTIES.find(x => x.id === id);
  if (!p) { console.warn("[Houseator] no property for id", id); return; }
  if (p.lat == null || p.lng == null) { console.warn("[Houseator] no coords for", p.title); return; }
  if (!leafletMap) { console.warn("[Houseator] no leafletMap"); return; }

  const targetZoom = 15;
  console.log("[Houseator] setView →", p.title, p.lat, p.lng);

  /* Simple, reliable map movement — setView with animation. */
  leafletMap.setView([p.lat, p.lng], targetZoom, {
    animate: true,
    duration: 0.9,
    easeLinearity: 0.25
  });

  /* Pulse the matching marker after the move completes */
  setTimeout(() => {
    const marker = markersById[id];
    if (!marker) { console.warn("[Houseator] no marker for id", id); return; }
    const el = marker.getElement && marker.getElement();
    if (!el) { console.warn("[Houseator] marker has no DOM element"); return; }
    el.classList.remove("zoom-pulse");
    void el.offsetWidth;
    el.classList.add("zoom-pulse");
    setTimeout(() => el.classList.remove("zoom-pulse"), 3000);
  }, 1000);
}
function hideMapResults() {
  mapResults.hidden = true;
  activeFilters.clear();
  syncLegendActiveClasses();
  /* renderDefaultPins() already auto-fits the map to all SD listings */
  renderDefaultPins();
}

/* Legend rows now TOGGLE — clicking one adds or removes that filter from
   the active set. The panel re-ranks live across all currently-selected
   categories so users can layer two, three, or all five overlays. */
document.querySelectorAll(".legend-row").forEach(btn => {
  btn.addEventListener("click", () => {
    const filter = btn.dataset.mapFilter;
    if (!filter) return;
    if (activeFilters.has(filter)) activeFilters.delete(filter);
    else activeFilters.add(filter);
    syncLegendActiveClasses();
    if (activeFilters.size === 0) hideMapResults();
    else refreshMapResults();
  });
});

/* All / None controls — quick way to overlay every category at once or
   reset back to the default pin view without unclicking each row. */
const legendSelectAll = document.getElementById("legendSelectAll");
const legendClearAll  = document.getElementById("legendClearAll");
if (legendSelectAll) {
  legendSelectAll.addEventListener("click", () => {
    document.querySelectorAll(".legend-row").forEach(r => {
      if (r.dataset.mapFilter) activeFilters.add(r.dataset.mapFilter);
    });
    syncLegendActiveClasses();
    refreshMapResults();
  });
}
if (legendClearAll) {
  legendClearAll.addEventListener("click", hideMapResults);
}

mapResultsClose.addEventListener("click", hideMapResults);

/* =========================================================
   FLOATING RESULTS PANEL — draggable header, persisted position
   ========================================================= */
(function makeMapResultsDraggable() {
  if (!mapResults) return;
  const handle = document.getElementById("mapResultsHandle");
  if (!handle) return;
  const PANEL_POS_KEY = "houseator_panel_pos_v1";

  /* Restore saved position on init */
  try {
    const saved = JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null");
    if (saved && typeof saved.left === "number" && typeof saved.top === "number") {
      const { left, top } = clampPosition(saved.left, saved.top);
      mapResults.style.left = `${left}px`;
      mapResults.style.top  = `${top}px`;
      mapResults.style.right = "auto";
    }
  } catch (_) {}

  function clampPosition(left, top) {
    const w = mapResults.offsetWidth  || 340;
    const h = mapResults.offsetHeight || 200;
    const vw = window.innerWidth;
    const vh = window.innerHeight;
    /* Keep at least 80px of the panel on-screen on each side */
    const minL = 80 - w;
    const maxL = vw - 80;
    const minT = 0;
    const maxT = vh - 60;
    return {
      left: Math.max(minL, Math.min(maxL, left)),
      top:  Math.max(minT, Math.min(maxT, top))
    };
  }

  let dragging = false;
  let startX = 0, startY = 0;
  let startLeft = 0, startTop = 0;

  function getPoint(e) {
    if (e.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
    return { x: e.clientX, y: e.clientY };
  }

  function onDragStart(e) {
    /* Don't start drag if clicking the close button */
    if (e.target.closest(".map-results-close")) return;
    const pt = getPoint(e);
    if (pt.x == null) return;
    e.preventDefault();
    dragging = true;
    startX = pt.x;
    startY = pt.y;
    const rect = mapResults.getBoundingClientRect();
    startLeft = rect.left;
    startTop  = rect.top;
    /* Switch from right-anchored to left/top so JS can move it */
    mapResults.style.left = `${startLeft}px`;
    mapResults.style.top  = `${startTop}px`;
    mapResults.style.right = "auto";
    mapResults.classList.add("is-dragging");
  }

  function onDragMove(e) {
    if (!dragging) return;
    const pt = getPoint(e);
    if (pt.x == null) return;
    e.preventDefault();
    const dx = pt.x - startX;
    const dy = pt.y - startY;
    const { left, top } = clampPosition(startLeft + dx, startTop + dy);
    mapResults.style.left = `${left}px`;
    mapResults.style.top  = `${top}px`;
  }

  function onDragEnd() {
    if (!dragging) return;
    dragging = false;
    mapResults.classList.remove("is-dragging");
    /* Persist the new position */
    try {
      const rect = mapResults.getBoundingClientRect();
      localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left: rect.left, top: rect.top }));
    } catch (_) {}
  }

  /* Mouse */
  handle.addEventListener("mousedown", onDragStart);
  document.addEventListener("mousemove", onDragMove);
  document.addEventListener("mouseup", onDragEnd);
  /* Touch */
  handle.addEventListener("touchstart", onDragStart, { passive: false });
  document.addEventListener("touchmove", onDragMove, { passive: false });
  document.addEventListener("touchend", onDragEnd);

  /* Re-clamp if viewport resizes so the panel can't end up off-screen */
  window.addEventListener("resize", () => {
    if (!mapResults.style.left) return;
    const cur = mapResults.getBoundingClientRect();
    const { left, top } = clampPosition(cur.left, cur.top);
    mapResults.style.left = `${left}px`;
    mapResults.style.top  = `${top}px`;
  });
})();

/* =========================================================
   FLOATING LEGEND PANEL — same drag mechanics as the results
   panel, separate persisted position so each remembers where
   the user parked it.
   ========================================================= */
(function makeMapOverlayDraggable() {
  const overlay = document.getElementById("mapOverlay");
  const handle  = document.getElementById("mapOverlayHandle");
  if (!overlay || !handle) return;
  const POS_KEY = "houseator_legend_pos_v1";

  function clampPosition(left, top) {
    const w = overlay.offsetWidth  || 240;
    const h = overlay.offsetHeight || 200;
    const vw = window.innerWidth;
    const vh = window.innerHeight;
    /* Keep at least 80px of the panel on-screen on each side */
    return {
      left: Math.max(80 - w, Math.min(vw - 80, left)),
      top:  Math.max(0,      Math.min(vh - 60, top))
    };
  }

  /* Restore saved position (fall back to default bottom-left CSS otherwise) */
  try {
    const saved = JSON.parse(localStorage.getItem(POS_KEY) || "null");
    if (saved && typeof saved.left === "number" && typeof saved.top === "number") {
      const { left, top } = clampPosition(saved.left, saved.top);
      overlay.style.left = `${left}px`;
      overlay.style.top  = `${top}px`;
      overlay.style.right = "auto";
      overlay.style.bottom = "auto";
    }
  } catch (_) {}

  let dragging = false;
  let startX = 0, startY = 0;
  let startLeft = 0, startTop = 0;

  function getPoint(e) {
    if (e.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
    return { x: e.clientX, y: e.clientY };
  }

  function onDragStart(e) {
    const pt = getPoint(e);
    if (pt.x == null) return;
    e.preventDefault();
    dragging = true;
    startX = pt.x;
    startY = pt.y;
    const rect = overlay.getBoundingClientRect();
    startLeft = rect.left;
    startTop  = rect.top;
    overlay.style.left = `${startLeft}px`;
    overlay.style.top  = `${startTop}px`;
    overlay.style.right = "auto";
    overlay.style.bottom = "auto";
    overlay.classList.add("is-dragging");
  }
  function onDragMove(e) {
    if (!dragging) return;
    const pt = getPoint(e);
    if (pt.x == null) return;
    e.preventDefault();
    const { left, top } = clampPosition(startLeft + (pt.x - startX), startTop + (pt.y - startY));
    overlay.style.left = `${left}px`;
    overlay.style.top  = `${top}px`;
  }
  function onDragEnd() {
    if (!dragging) return;
    dragging = false;
    overlay.classList.remove("is-dragging");
    try {
      const rect = overlay.getBoundingClientRect();
      localStorage.setItem(POS_KEY, JSON.stringify({ left: rect.left, top: rect.top }));
    } catch (_) {}
  }

  handle.addEventListener("mousedown", onDragStart);
  document.addEventListener("mousemove", onDragMove);
  document.addEventListener("mouseup", onDragEnd);
  handle.addEventListener("touchstart", onDragStart, { passive: false });
  document.addEventListener("touchmove", onDragMove, { passive: false });
  document.addEventListener("touchend", onDragEnd);

  window.addEventListener("resize", () => {
    if (!overlay.style.left) return;
    const cur = overlay.getBoundingClientRect();
    const { left, top } = clampPosition(cur.left, cur.top);
    overlay.style.left = `${left}px`;
    overlay.style.top  = `${top}px`;
  });
})();

/* =========================================================
   KEEP-IN-SECTION — both floating panels live inside the Map
   section. When the user scrolls past it, they wiggle and dart
   back toward the section; when it returns to view they bounce
   back in. Pure CSS animations driven by class-toggling here.
   ========================================================= */
(function makePanelsHomesick() {
  const mapSection = document.getElementById("map");
  const panels = [document.getElementById("mapOverlay"), document.getElementById("mapResults")].filter(Boolean);
  if (!mapSection || !panels.length) return;

  /* Track each panel's last "out direction" so we know which return
     animation to play — coming back from above vs below feels different. */
  const lastDir = new WeakMap();   /* panel → "above" | "below" | null */

  let firstRun = true;
  let ticking  = false;

  function applyState() {
    const r = mapSection.getBoundingClientRect();
    /* Early-trigger margin — start fleeing while a sliver of the section is
       still on-screen, so the user feels the panels react immediately to
       leaving the section rather than after a full scroll-past. */
    const EDGE = 120;
    const sectionAbove = r.bottom < EDGE;
    const sectionBelow = r.top    > window.innerHeight - EDGE;

    panels.forEach(panel => {
      /* If user explicitly closed the results panel, don't fight them */
      if (panel.id === "mapResults" && panel.hidden) {
        panel.classList.remove("panel-out-above", "panel-out-below",
                                "panel-returning-above", "panel-returning-below");
        panel.style.transform = "";
        panel.style.opacity = "";
        panel.style.pointerEvents = "";
        lastDir.set(panel, null);
        return;
      }

      const prevDir = lastDir.get(panel);

      if (sectionAbove) {
        if (prevDir === "above") return;
        panel.classList.remove("panel-returning-above", "panel-returning-below", "panel-out-below");
        if (firstRun) {
          /* First paint: snap into the hidden position with inline styles so
             the flee animation does NOT play on page load. */
          panel.style.transform = "translateY(-140vh)";
          panel.style.opacity = "0";
          panel.style.pointerEvents = "none";
        } else {
          /* Subsequent runs: let the keyframe flee animation play. */
          panel.style.transform = "";
          panel.style.opacity = "";
          panel.style.pointerEvents = "";
          panel.classList.add("panel-out-above");
        }
        lastDir.set(panel, "above");
      } else if (sectionBelow) {
        if (prevDir === "below") return;
        panel.classList.remove("panel-returning-above", "panel-returning-below", "panel-out-above");
        if (firstRun) {
          panel.style.transform = "translateY(140vh)";
          panel.style.opacity = "0";
          panel.style.pointerEvents = "none";
        } else {
          panel.style.transform = "";
          panel.style.opacity = "";
          panel.style.pointerEvents = "";
          panel.classList.add("panel-out-below");
        }
        lastDir.set(panel, "below");
      } else {
        /* Section is at least partially in view — bring panel home */
        if (prevDir == null) return;
        panel.classList.remove("panel-out-above", "panel-out-below");
        /* Clear any first-run inline styles so the return animation can drive transform */
        panel.style.transform = "";
        panel.style.opacity = "";
        panel.style.pointerEvents = "";
        const returnCls = prevDir === "above" ? "panel-returning-above" : "panel-returning-below";
        panel.classList.add(returnCls);
        lastDir.set(panel, null);
        /* Right panel has a 140ms delay before its return animation starts,
           so we wait long enough for that one to fully resolve before
           stripping the class. */
        setTimeout(() => panel.classList.remove(returnCls), 1300);
      }
    });

    firstRun = false;
  }

  function onScroll() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      applyState();
      ticking = false;
    });
  }
  window.addEventListener("scroll",  onScroll, { passive: true });
  window.addEventListener("resize",  onScroll);
  /* Run once on init so a deep-link / mid-page reload starts in the right state */
  applyState();
})();

/* Hero quick-filter buttons → smooth-scroll to map + activate the matching legend filter */
document.querySelectorAll("[data-jump-filter]").forEach(btn => {
  btn.addEventListener("click", () => {
    const filter = btn.dataset.jumpFilter;
    const mapSection = document.getElementById("map");
    if (mapSection) mapSection.scrollIntoView({ behavior: "smooth", block: "start" });
    /* Wait for the scroll to settle, then click the matching legend row. */
    setTimeout(() => {
      const legendBtn = document.querySelector(`.legend-row[data-map-filter="${filter}"]`);
      if (legendBtn && !legendBtn.classList.contains("active")) legendBtn.click();
    }, 650);
  });
});

/* =========================================================
   Safety-rating info popover (Low Crime filter only)
   ========================================================= */
let activeMetricPopover = null;

function buildSafetyPopover() {
  const el = document.createElement("div");
  el.className = "metric-popover";
  el.setAttribute("role", "dialog");
  el.setAttribute("aria-label", "Safety Rating explanation");
  el.innerHTML = `
    <h5>Safety Rating</h5>
    <p>A 1–100 composite of local crime statistics, emergency response times, and neighborhood infrastructure. Higher = safer.</p>
    <ul class="popover-scale">
      <li><b>90+</b><span>Very low crime</span></li>
      <li><b>75–89</b><span>Below average</span></li>
      <li><b>60–74</b><span>Average</span></li>
      <li><b>&lt; 60</b><span>Elevated</span></li>
    </ul>
    <p class="popover-source muted small">Sources: FBI NIBRS · municipal data. Refreshed quarterly.</p>
  `;
  return el;
}

function positionPopover(popover, anchorEl) {
  const rect = anchorEl.getBoundingClientRect();
  const popW = popover.offsetWidth || 260;
  const popH = popover.offsetHeight || 160;
  const margin = 10;
  const left = Math.max(8, Math.min(window.innerWidth - popW - 8, rect.left + rect.width / 2 - popW / 2));
  let top = rect.top - popH - margin;
  let placement = "above";
  if (top < 8) {
    top = rect.bottom + margin;
    placement = "below";
  }
  popover.style.left = `${left}px`;
  popover.style.top = `${top}px`;
  popover.dataset.placement = placement;
  /* Pin the arrow to the icon's centre even if popover is offset horizontally */
  const arrowX = rect.left + rect.width / 2 - left;
  popover.style.setProperty("--arrow-x", `${arrowX}px`);
}

function hideMetricPopover() {
  if (!activeMetricPopover) return;
  activeMetricPopover.classList.remove("open");
  const el = activeMetricPopover;
  activeMetricPopover = null;
  setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 220);
}

function showMetricPopover(anchorEl) {
  hideMetricPopover();
  const popover = buildSafetyPopover();
  document.body.appendChild(popover);
  /* Need offsetHeight measured AFTER mount but BEFORE animation */
  positionPopover(popover, anchorEl);
  activeMetricPopover = popover;
  /* Trigger fade/scale-in next frame */
  requestAnimationFrame(() => popover.classList.add("open"));
}

/* =========================================================
   Per-filter score popover — hovering or tapping a chip
   icon in the mobile map legend pops a small dialog listing
   each region's score for that category. Reuses the
   .metric-popover styling already used by the safety popover.
   ========================================================= */
/* Each filter maps to a REGION_METRICS key whose value is rendered as a
   distance alongside the score (km for walkability/schools/parks). Commute
   has no extra distance — the score itself is the minutes figure. Crime
   shows the score on its own. */
const FILTER_META = {
  walkability: { label: "Walkability", unit: "score", distKey: "amenityKm" },
  schools:     { label: "Schools",     unit: "score", distKey: "schoolKm"  },
  commute:     { label: "Commute",     unit: "min",   distKey: null        },
  crime:       { label: "Low crime",   unit: "score", distKey: null        },
  parks:       { label: "Parks",       unit: "score", distKey: "parkKm"    }
};

function buildScorePopover(filterKey) {
  const meta = FILTER_META[filterKey];
  const scores = NEIGHBORHOOD_SCORES[filterKey];
  if (!meta || !scores) return null;
  const el = document.createElement("div");
  el.className = "metric-popover metric-popover-compact";
  el.dataset.filter = filterKey;
  el.setAttribute("role", "dialog");
  el.setAttribute("aria-label", `${meta.label} scores by region`);
  /* Commute is "lower is better"; everything else is "higher is better".
     Sort so the most desirable region renders first. */
  const ascending = filterKey === "commute";
  const entries = Object.entries(scores).sort((a, b) => ascending ? a[1] - b[1] : b[1] - a[1]);
  const rows = entries.map(([region, val]) => {
    const valStr = meta.unit === "min" ? `${val} min` : val;
    const distKm = meta.distKey ? (REGION_METRICS[region] || {})[meta.distKey] : null;
    const distStr = distKm != null ? ` · ${distKm} km` : "";
    return `<li><span>${region}</span><b>${valStr}${distStr}</b></li>`;
  }).join("");
  el.innerHTML = `
    <h5>${meta.label}</h5>
    <ul class="popover-scale">${rows}</ul>
  `;
  return el;
}

function showScorePopover(anchorEl, filterKey) {
  hideMetricPopover();
  const popover = buildScorePopover(filterKey);
  if (!popover) return;
  document.body.appendChild(popover);
  positionPopover(popover, anchorEl);
  activeMetricPopover = popover;
  requestAnimationFrame(() => popover.classList.add("open"));
}

/* Wire each mobile chip's colored icon to the score popover. The chip's
   text still toggles the filter — stopPropagation() on the icon click
   keeps those two actions independent. Hover support comes for free for
   mouse users; the click handler covers touch users on mobile.
   iOS Safari fires synthetic mouseenter → click on tap, so a naive
   "click toggles" handler would hide the popover the same frame it was
   shown. The lastShownAt timestamp suppresses the close-on-tap-open
   case while still allowing tap-to-close after the popover is visible. */
document.querySelectorAll(".map-legend-chips .legend-row .legend-ico").forEach(icon => {
  const row = icon.closest(".legend-row");
  const filterKey = row && row.dataset.mapFilter;
  if (!filterKey) return;
  let leaveTimer = null;
  let lastShownAt = 0;
  const open = () => {
    clearTimeout(leaveTimer);
    showScorePopover(icon, filterKey);
    lastShownAt = Date.now();
  };
  icon.addEventListener("mouseenter", open);
  icon.addEventListener("mouseleave", () => {
    leaveTimer = setTimeout(() => hideMetricPopover(), 180);
  });
  icon.addEventListener("click", (e) => {
    e.stopPropagation();
    const sameFilter = activeMetricPopover && activeMetricPopover.dataset.filter === filterKey;
    if (sameFilter) {
      /* If we just opened it (touch tap fires synthetic mouseenter
         right before click), don't immediately close — that would
         flicker. Past the 350ms guard, the user is asking to close. */
      if (Date.now() - lastShownAt > 350) hideMetricPopover();
    } else {
      open();
    }
  });
});

/* Event delegation on the in-map results list */
mapResultsList.addEventListener("click", (e) => {
  const info = e.target.closest(".metric-info");
  if (!info) return;
  e.stopPropagation();   /* don't open the property modal too */
  if (activeMetricPopover) {
    hideMetricPopover();
  } else {
    showMetricPopover(info);
  }
});

/* Click anywhere else (or on the popover itself except close intent) → dismiss */
document.addEventListener("click", (e) => {
  if (!activeMetricPopover) return;
  if (e.target.closest(".metric-popover")) return; /* keep open while clicking inside */
  hideMetricPopover();
});
document.addEventListener("keydown", (e) => {
  if (e.key === "Escape" && activeMetricPopover) hideMetricPopover();
});
window.addEventListener("scroll", () => { if (activeMetricPopover) hideMetricPopover(); }, { passive: true });
window.addEventListener("resize", () => { if (activeMetricPopover) hideMetricPopover(); });

renderListings();

/* =========================================================
   COMPARE PROPERTIES — side-by-side up to 4 listings with
   per-card image cycler + Smart Match score that re-ranks
   when the user switches buyer profile.
   ========================================================= */
const COMPARE_KEY = "houseator_compare_v1";
const COMPARE_EMPH_KEY = "houseator_compare_emphasized_v1";
const COMPARE_ORDER_KEY = "houseator_compare_roworder_v1";
let compareIds = (() => {
  try {
    const saved = JSON.parse(sessionStorage.getItem(COMPARE_KEY) || "null");
    if (Array.isArray(saved)) return saved.slice(0, 4);
  } catch (_) {}
  return [];
})();
const compareImageIdx = {};
let compareProfile = "family";
const emphasizedRows = new Set((() => {
  try {
    const a = JSON.parse(sessionStorage.getItem(COMPARE_EMPH_KEY) || "[]");
    return Array.isArray(a) ? a : [];
  } catch (_) { return []; }
})());

/* Row order — user can drag-reorder the middle rows via the label column.
   "image" stays at top, "actions" stays at bottom (anchored). */
const DEFAULT_ROW_ORDER = [
  "image", "title", "price", "beds", "baths", "sqft", "ppsqft",
  "year", "region", "type", "status", "monthly", "score", "actions"
];
const FIXED_ROWS = new Set(["image", "actions"]);
let rowOrder = (() => {
  try {
    const saved = JSON.parse(sessionStorage.getItem(COMPARE_ORDER_KEY) || "null");
    if (Array.isArray(saved) &&
        saved.length === DEFAULT_ROW_ORDER.length &&
        DEFAULT_ROW_ORDER.every(r => saved.includes(r))) {
      return saved;
    }
  } catch (_) {}
  return [...DEFAULT_ROW_ORDER];
})();
function saveRowOrder() {
  try { sessionStorage.setItem(COMPARE_ORDER_KEY, JSON.stringify(rowOrder)); } catch (_) {}
}
function saveEmphasis() {
  try { sessionStorage.setItem(COMPARE_EMPH_KEY, JSON.stringify([...emphasizedRows])); } catch (_) {}
}
function applyEmphasis() {
  document.querySelectorAll(".compare-table [data-row]").forEach(cell => {
    cell.classList.toggle("row-emphasized", emphasizedRows.has(cell.dataset.row));
  });
}

const PROFILES = {
  family: {
    label: "Family",
    blurb: "More bedrooms, suburban-friendly regions, and great schools weighted highest.",
    weights: { price: -.35, beds: 1, baths: .6, sqft: .8, year: .3, region: { Suburban: .8, Coastal: .3 } }
  },
  "first-time": {
    label: "First-time",
    blurb: "Affordability does the heavy lifting; bonus for move-in-ready 'Available' status.",
    weights: { price: -1, beds: .3, baths: .3, sqft: .3, year: .2, status: { Available: .5 } }
  },
  investor: {
    label: "Investor",
    blurb: "Lower price-per-sqft, urban density, and short days-on-market score best.",
    weights: { price: -.4, sqft: .5, dom: -.6, year: .2, region: { Urban: .6, Coastal: .3 } }
  },
  luxury: {
    label: "Luxury",
    blurb: "Premium type bonus, newest builds, largest square-footage, ocean adjacency.",
    weights: { price: .6, sqft: 1, year: 1, type: { Luxury: 1.5 }, beds: .4, baths: .4, region: { Coastal: .5 } }
  }
};

function smartMatchScore(p, all, profileKey) {
  const profile = PROFILES[profileKey] || PROFILES.family;
  const w = profile.weights;
  const norm = (key) => {
    const vals = all.map(x => Number(x[key]) || 0);
    const lo = Math.min(...vals), hi = Math.max(...vals);
    if (hi === lo) return 0.5;
    return ((Number(p[key]) || 0) - lo) / (hi - lo);
  };
  let raw = 0;
  if (w.price) raw += w.price * norm("price");
  if (w.beds)  raw += w.beds  * norm("beds");
  if (w.baths) raw += w.baths * norm("baths");
  if (w.sqft)  raw += w.sqft  * norm("sqft");
  if (w.year)  raw += w.year  * norm("year");
  if (w.dom)   raw += w.dom   * norm("daysOnMarket");
  if (w.region && w.region[p.region]) raw += w.region[p.region];
  if (w.type   && w.type[p.type])     raw += w.type[p.type];
  if (w.status && w.status[p.status]) raw += w.status[p.status];
  return Math.max(0, Math.min(100, Math.round(50 + raw * 25)));
}

function estimatedMonthlyPayment(price) {
  const loan = price * 0.8;
  const r = 0.0675 / 12;
  const n = 30 * 12;
  return loan * (r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
}

function findCompareWinners(props) {
  if (props.length < 2) return {};
  const winners = {};
  ["price", "daysOnMarket"].forEach(k => {
    let bestId = null, bestVal = Infinity;
    props.forEach(p => { if ((p[k] ?? Infinity) < bestVal) { bestVal = p[k]; bestId = p.id; } });
    winners[k] = bestId;
  });
  ["beds", "baths", "sqft", "year"].forEach(k => {
    let bestId = null, bestVal = -Infinity;
    props.forEach(p => { if ((p[k] ?? -Infinity) > bestVal) { bestVal = p[k]; bestId = p.id; } });
    winners[k] = bestId;
  });
  return winners;
}

function compareList() {
  return compareIds.map(id => PROPERTIES.find(p => p.id === id)).filter(Boolean);
}
function saveCompare() {
  try { sessionStorage.setItem(COMPARE_KEY, JSON.stringify(compareIds)); } catch (_) {}
}
function escapeAttr(s) {
  return String(s ?? "").replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function renderCompare() {
  const table = document.getElementById("compareTable");
  if (!table) return;
  const props = compareList();
  const winners = findCompareWinners(props);
  const profile = PROFILES[compareProfile];
  const blurbEl = document.getElementById("compareProfileBlurb");
  if (blurbEl) blurbEl.textContent = profile.blurb;

  const scores = {};
  let bestMatchId = null, bestMatchScore = -1;
  props.forEach(p => {
    const s = smartMatchScore(p, props.length ? props : [p], compareProfile);
    scores[p.id] = s;
    if (s > bestMatchScore) { bestMatchScore = s; bestMatchId = p.id; }
  });

  const LABEL_FOR_ROW = {
    image:   `<div class="cc-cell cc-img-cell" data-row="image"><span class="cc-empty-label">Photo</span></div>`,
    title:   `<div class="cc-cell cc-title-cell cc-row-toggle cc-row-drag" data-row="title" title="Drag to reorder · Click to highlight">Property</div>`,
    price:   `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="price" title="Drag to reorder · Click to highlight">Price</div>`,
    beds:    `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="beds" title="Drag to reorder · Click to highlight">Beds</div>`,
    baths:   `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="baths" title="Drag to reorder · Click to highlight">Baths</div>`,
    sqft:    `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="sqft" title="Drag to reorder · Click to highlight">Sq Ft</div>`,
    ppsqft:  `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="ppsqft" title="Drag to reorder · Click to highlight">$ / Sq Ft</div>`,
    year:    `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="year" title="Drag to reorder · Click to highlight">Year</div>`,
    region:  `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="region" title="Drag to reorder · Click to highlight">Region</div>`,
    type:    `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="type" title="Drag to reorder · Click to highlight">Type</div>`,
    status:  `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="status" title="Drag to reorder · Click to highlight">Status</div>`,
    monthly: `<div class="cc-cell cc-row-toggle cc-row-drag" data-row="monthly" title="Drag to reorder · Click to highlight">Est. Monthly</div>`,
    score:   `<div class="cc-cell cc-score-cell cc-row-toggle cc-row-drag" data-row="score" title="Drag to reorder · Click to highlight">Smart Match</div>`,
    actions: `<div class="cc-cell cc-actions-cell" data-row="actions"></div>`
  };
  const labelCol = `<div class="compare-headcol">${rowOrder.map(r => LABEL_FOR_ROW[r] || "").join("")}</div>`;

  const propCols = props.map(p => {
    if (compareImageIdx[p.id] == null) compareImageIdx[p.id] = 0;
    const imgIdx = compareImageIdx[p.id] % p.images.length;
    const winnerCls = (k) => winners[k] === p.id && props.length > 1 ? " winner" : "";
    const score = scores[p.id];
    const isBest = bestMatchId === p.id && props.length > 1;
    const CELL_FOR_ROW = {
      image: `<div class="cc-cell cc-img-cell" data-row="image">
        ${isBest ? `<span class="best-match-ribbon">★ Best Match</span>` : ""}
        <button type="button" class="cc-remove-top" data-cc-remove aria-label="Remove from comparison" title="Remove from comparison">✕</button>
        <div class="cc-image">
          <img src="${escapeAttr(p.images[imgIdx])}" alt="${escapeAttr(p.title)}" loading="lazy" />
          <button type="button" class="cc-img-nav prev" data-cc-img="prev" aria-label="Previous image">‹</button>
          <button type="button" class="cc-img-nav next" data-cc-img="next" aria-label="Next image">›</button>
          <div class="cc-img-dots">
            ${p.images.map((_, i) => `<span class="cc-img-dot${i === imgIdx ? " active" : ""}"></span>`).join("")}
          </div>
        </div>
      </div>`,
      title: `<div class="cc-cell cc-title-cell" data-row="title">
        <strong>${escapeAttr(p.title)}</strong>
        <span class="muted small">${escapeAttr(p.address)}</span>
      </div>`,
      price:   `<div class="cc-cell cc-price${winnerCls("price")}" data-row="price">${fmtPrice(p.price)}</div>`,
      beds:    `<div class="cc-cell${winnerCls("beds")}" data-row="beds">${p.beds}</div>`,
      baths:   `<div class="cc-cell${winnerCls("baths")}" data-row="baths">${p.baths}</div>`,
      sqft:    `<div class="cc-cell${winnerCls("sqft")}" data-row="sqft">${p.sqft.toLocaleString()}</div>`,
      ppsqft:  `<div class="cc-cell" data-row="ppsqft">$${Math.round(p.price / (p.sqft || 1)).toLocaleString()}</div>`,
      year:    `<div class="cc-cell${winnerCls("year")}" data-row="year">${p.year}</div>`,
      region:  `<div class="cc-cell" data-row="region">${escapeAttr(p.region)}</div>`,
      type:    `<div class="cc-cell" data-row="type">${escapeAttr(p.type)}</div>`,
      status:  `<div class="cc-cell" data-row="status"><span class="status ${p.status.toLowerCase()}">${p.status}</span></div>`,
      monthly: `<div class="cc-cell cc-monthly" data-row="monthly">${fmtUSDInt(estimatedMonthlyPayment(p.price))}/mo</div>`,
      score:   `<div class="cc-cell cc-score-cell" data-row="score">
        <div class="cc-score">
          <div class="cc-score-bar" style="width:${score}%"></div>
          <span class="cc-score-num">${score}</span>
        </div>
      </div>`,
      actions: `<div class="cc-cell cc-actions-cell" data-row="actions">
        <button type="button" class="cc-remove" data-cc-remove>✕ Remove</button>
      </div>`
    };
    return `
      <div class="compare-col${isBest ? " is-best-match" : ""}" data-id="${p.id}">
        ${rowOrder.map(r => CELL_FOR_ROW[r] || "").join("")}
      </div>
    `;
  }).join("");

  let addCol = "";
  if (compareIds.length < 4) {
    addCol = `
      <div class="compare-add-col">
        <button type="button" class="compare-add-btn" id="compareAddBtn">
          <span class="compare-add-plus">+</span>
          <span>Add Property</span>
          <span class="muted small">${compareIds.length}/4 selected</span>
        </button>
      </div>
    `;
  }

  /* Empty state when nothing is added yet */
  if (props.length === 0) {
    table.innerHTML = `
      <div class="compare-empty">
        <p>No properties selected yet.</p>
        <button type="button" class="button button-primary" id="compareAddBtn">+ Add your first property</button>
      </div>
    `;
  } else {
    table.innerHTML = labelCol + propCols + addCol;
  }

  table.querySelectorAll(".compare-col").forEach(col => {
    const id = parseInt(col.dataset.id, 10);
    col.querySelectorAll("[data-cc-remove]").forEach(btn =>
      btn.addEventListener("click", (e) => {
        e.stopPropagation();
        removeFromCompare(id);
      })
    );
    col.querySelectorAll("[data-cc-img]").forEach(btn => {
      btn.addEventListener("click", () => {
        const dir = btn.dataset.ccImg === "next" ? 1 : -1;
        const p = PROPERTIES.find(x => x.id === id);
        if (!p) return;
        compareImageIdx[id] = (compareImageIdx[id] + dir + p.images.length) % p.images.length;
        renderCompare();
      });
    });
    const img = col.querySelector(".cc-image img");
    if (img) img.addEventListener("click", () => {
      const p = PROPERTIES.find(x => x.id === id);
      if (!p) return;
      compareImageIdx[id] = (compareImageIdx[id] + 1) % p.images.length;
      renderCompare();
    });
  });

  const addBtn = document.getElementById("compareAddBtn");
  if (addBtn) addBtn.addEventListener("click", openComparePicker);

  /* Restore any previously-emphasized rows */
  applyEmphasis();
}

/* One-time wire-up: label click toggles emphasis, label drag reorders rows.
   A movement threshold separates a click from a drag so both gestures work
   from the same handle without interfering. */
(function wireRowEmphasisAndDrag() {
  const table = document.getElementById("compareTable");
  if (!table || table.dataset.emphWired) return;
  table.dataset.emphWired = "1";

  let dragState = null;
  let suppressClickUntil = 0;
  const DRAG_THRESHOLD = 5;

  /* Emphasis (click) */
  table.addEventListener("click", (e) => {
    if (Date.now() < suppressClickUntil) return;
    const label = e.target.closest(".compare-headcol .cc-row-toggle");
    if (!label) return;
    const row = label.dataset.row;
    if (!row) return;
    const isAdding = !emphasizedRows.has(row);
    if (isAdding) emphasizedRows.add(row);
    else emphasizedRows.delete(row);
    saveEmphasis();
    applyEmphasis();
    if (isAdding) {
      table.querySelectorAll(`[data-row="${row}"]`).forEach(cell => {
        cell.classList.remove("just-emphasized");
        void cell.offsetWidth;
        cell.classList.add("just-emphasized");
        setTimeout(() => cell.classList.remove("just-emphasized"), 900);
      });
    }
  });

  /* Drag */
  table.addEventListener("pointerdown", (e) => {
    if (e.button !== 0) return;
    const handle = e.target.closest(".compare-headcol .cc-row-drag");
    if (!handle) return;
    const row = handle.dataset.row;
    if (!row || FIXED_ROWS.has(row)) return;

    /* Snapshot label-column positions to compute drop targets */
    const positions = [];
    rowOrder.forEach((r, idx) => {
      const el = table.querySelector(`.compare-headcol [data-row="${r}"]`);
      if (!el) return;
      const rect = el.getBoundingClientRect();
      positions.push({ row: r, idx, top: rect.top, height: rect.height, mid: rect.top + rect.height / 2 });
    });

    dragState = {
      handle,
      row,
      startY: e.clientY,
      positions,
      fromIdx: rowOrder.indexOf(row),
      targetIdx: rowOrder.indexOf(row),
      dragging: false,
      pointerId: e.pointerId
    };
    try { handle.setPointerCapture(e.pointerId); } catch (_) {}
  });

  table.addEventListener("pointermove", (e) => {
    if (!dragState) return;
    const dy = e.clientY - dragState.startY;

    /* Cross the threshold before treating as a drag */
    if (!dragState.dragging) {
      if (Math.abs(dy) < DRAG_THRESHOLD) return;
      dragState.dragging = true;
      table.querySelectorAll(`[data-row="${dragState.row}"]`).forEach(cell => {
        cell.classList.add("row-dragging");
      });
    }

    /* Translate every cell in the dragged row in lockstep */
    table.querySelectorAll(`[data-row="${dragState.row}"]`).forEach(cell => {
      cell.style.transform = `translateY(${dy}px)`;
    });

    /* Determine target slot from cursor vs label midpoints */
    const draggedMid = dragState.positions.find(rp => rp.row === dragState.row).mid + dy;
    let newIdx = dragState.fromIdx;
    for (const rp of dragState.positions) {
      if (rp.row === dragState.row || FIXED_ROWS.has(rp.row)) continue;
      if (rp.idx < dragState.fromIdx && draggedMid < rp.mid) {
        if (rp.idx < newIdx) newIdx = rp.idx;
      } else if (rp.idx > dragState.fromIdx && draggedMid > rp.mid) {
        if (rp.idx > newIdx) newIdx = rp.idx;
      }
    }
    if (newIdx !== dragState.targetIdx) {
      dragState.targetIdx = newIdx;
      table.querySelectorAll(".drop-target").forEach(el => el.classList.remove("drop-target"));
      const targetRow = rowOrder[newIdx];
      if (targetRow && targetRow !== dragState.row) {
        table.querySelectorAll(`[data-row="${targetRow}"]`).forEach(cell => {
          cell.classList.add("drop-target");
        });
      }
    }
  });

  function endDrag() {
    if (!dragState) return;
    const wasDragging = dragState.dragging;
    const droppedRow = dragState.row;
    const didMove = dragState.fromIdx !== dragState.targetIdx;

    /* Clear in-flight visuals */
    table.querySelectorAll(".row-dragging").forEach(el => {
      el.classList.remove("row-dragging");
      el.style.transform = "";
    });
    table.querySelectorAll(".drop-target").forEach(el => el.classList.remove("drop-target"));

    if (wasDragging) {
      suppressClickUntil = Date.now() + 250;
      if (didMove) {
        const newOrder = [...rowOrder];
        const [moved] = newOrder.splice(dragState.fromIdx, 1);
        newOrder.splice(dragState.targetIdx, 0, moved);
        rowOrder = newOrder;
        saveRowOrder();
        renderCompare();
        /* Lock-in animation on the dropped row */
        requestAnimationFrame(() => {
          table.querySelectorAll(`[data-row="${droppedRow}"]`).forEach(cell => {
            cell.classList.add("row-locked-in");
            setTimeout(() => cell.classList.remove("row-locked-in"), 650);
          });
        });
      }
    }
    dragState = null;
  }
  table.addEventListener("pointerup", endDrag);
  table.addEventListener("pointercancel", endDrag);
})();

function addToCompare(id) {
  if (compareIds.includes(id) || compareIds.length >= 4) return;
  /* Defensive: sold listings can't be compared, no matter the entry point */
  const p = PROPERTIES.find(x => x.id === id);
  if (!p || p.status === "Sold") return;
  compareIds.push(id);
  compareImageIdx[id] = 0;
  saveCompare();
  renderCompare();
}
function removeFromCompare(id) {
  compareIds = compareIds.filter(x => x !== id);
  delete compareImageIdx[id];
  saveCompare();
  renderCompare();
}
function clearCompare() {
  compareIds = [];
  Object.keys(compareImageIdx).forEach(k => delete compareImageIdx[k]);
  saveCompare();
  renderCompare();
}

const comparePickerModal = document.getElementById("comparePickerModal");
const comparePickerList  = document.getElementById("comparePickerList");

function openComparePicker() {
  if (!comparePickerModal) return;
  /* Sort: available + pending first, then sold (faded, disabled). Within
     each group, keep the original order. */
  const sortedProps = [
    ...PROPERTIES.filter(p => p.status !== "Sold"),
    ...PROPERTIES.filter(p => p.status === "Sold")
  ];
  comparePickerList.innerHTML = sortedProps.map(p => {
    const added  = compareIds.includes(p.id);
    const isSold = p.status === "Sold";
    /* Sold rows are disabled so users can only compare available stock.
       Added rows are disabled too (already in the comparison). */
    const disabled = isSold || added;
    const cls = [
      "compare-picker-row",
      added  ? "added"   : "",
      isSold ? "is-sold" : ""
    ].filter(Boolean).join(" ");
    const rightCell = isSold
      ? `<span class="compare-picker-sold">Sold</span>`
      : (added
          ? `<span class="compare-picker-added">✓ Added</span>`
          : `<span class="compare-picker-plus">+</span>`);
    return `
      <button type="button" class="${cls}" data-add-id="${p.id}" ${disabled ? "disabled" : ""}${isSold ? ` title="This listing is sold and can't be compared"` : ""}>
        <img src="${escapeAttr(p.images[0])}" alt="" loading="lazy" />
        <div class="compare-picker-info">
          <strong>${escapeAttr(p.title)}</strong>
          <span class="muted small">${escapeAttr(p.address)}</span>
          <span class="compare-picker-meta">${fmtPrice(p.price)} • ${p.beds} bd • ${p.baths} ba • ${p.sqft.toLocaleString()} ft²</span>
        </div>
        ${rightCell}
      </button>
    `;
  }).join("");
  comparePickerList.querySelectorAll("[data-add-id]:not([disabled])").forEach(btn => {
    btn.addEventListener("click", () => {
      addToCompare(parseInt(btn.dataset.addId, 10));
      closeComparePicker();
    });
  });
  comparePickerModal.hidden = false;
  document.body.style.overflow = "hidden";
}
function closeComparePicker() {
  if (!comparePickerModal) return;
  comparePickerModal.hidden = true;
  document.body.style.overflow = "";
}
if (comparePickerModal) {
  comparePickerModal.querySelectorAll("[data-cp-close]").forEach(el =>
    el.addEventListener("click", closeComparePicker)
  );
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && !comparePickerModal.hidden) closeComparePicker();
  });
}

document.querySelectorAll("#compareProfile [data-profile]").forEach(btn => {
  btn.addEventListener("click", () => {
    document.querySelectorAll("#compareProfile [data-profile]").forEach(b => b.classList.remove("active"));
    btn.classList.add("active");
    compareProfile = btn.dataset.profile;
    renderCompare();
  });
});

const compareClearBtn = document.getElementById("compareClearBtn");
if (compareClearBtn) compareClearBtn.addEventListener("click", clearCompare);

renderCompare();
