/* =========================================================
   HOUSEATOR — Shared data, helpers, and UI primitives
   Loaded by every page (index, login, dashboard).
   ========================================================= */

window.HOUSEATOR = (function () {
  const AUTH_KEY  = "houseator_auth_v1";
  const USER_KEY  = "houseator_user";
  const STORE_KEY = "houseator_props_v1";
  const TODO_KEY  = "houseator_todos_v1";
  const LEADS_KEY = "houseator_leads_v1";
  const CONTACTS_KEY = "houseator_contacts_v1";

  /* ---------- Property dataset ---------- */
  const PROPERTIES = [
    { id: 1, title: "Riverfront Luxury Villa", address: "1240 Marina Way, La Jolla, CA",
      region: "Coastal", type: "Luxury", beds: 4, baths: 3, sqft: 3100, year: 2021,
      lat: 32.860, lng: -117.270,
      price: 975000, status: "Available", lead: 93, daysOnMarket: 9,
      tags: ["Drone view", "3D Tour", "Open house"],
      images: [
        "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Sun-drenched riverfront villa with chef's kitchen, private dock, and panoramic water views from the primary suite." },
    { id: 2, title: "Downtown Skyline Condo", address: "501 Market St #1804, San Diego, CA",
      region: "Urban", type: "Condo", beds: 2, baths: 2, sqft: 1350, year: 2019,
      lat: 32.711, lng: -117.160,
      price: 625000, status: "Available", lead: 78, daysOnMarket: 14,
      tags: ["Walk score 98", "Smart home", "Near transit"],
      images: [
        "https://images.unsplash.com/photo-1572120360610-d971b9d7767c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1493809842364-78817add7ffb?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Sleek 18th-floor condo with floor-to-ceiling windows, integrated smart-home automation, and rooftop pool access." },
    { id: 3, title: "Suburban Family Home", address: "812 Maple Grove Ln, Carlsbad, CA",
      region: "Suburban", type: "House", beds: 3, baths: 2, sqft: 2050, year: 2014,
      lat: 33.165, lng: -117.345,
      price: 795000, status: "Available", lead: 84, daysOnMarket: 22,
      tags: ["Mortgage-ready", "School district A+", "Backyard"],
      images: [
        "https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1568605114967-8130f3a36994?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Light-filled family home with open kitchen, flex office space, and a fully landscaped backyard with built-in grill." },
    { id: 4, title: "Oak Valley Estate", address: "9 Oak Valley Rd, Rancho Santa Fe, CA",
      region: "Suburban", type: "Luxury", beds: 5, baths: 4, sqft: 4200, year: 2020,
      lat: 33.020, lng: -117.205,
      price: 1420000, status: "Available", lead: 96, daysOnMarket: 6,
      tags: ["Pool", "Wine cellar", "Smart irrigation"],
      images: [
        "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600585154526-990dced4db0d?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1613977257363-707ba9348227?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Estate-grade home with resort pool, 4-car garage, gourmet kitchen, and private guest casita." },
    { id: 5, title: "Bayfront Condo", address: "76 Harborlight Dr #5C, San Diego, CA",
      region: "Coastal", type: "Condo", beds: 3, baths: 2, sqft: 1620, year: 2018,
      lat: 32.717, lng: -117.180,
      price: 820000, status: "Pending", lead: 79, daysOnMarket: 11,
      tags: ["Bay views", "Concierge", "Gym"],
      images: [
        "https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1554995207-c18c203602cb?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600210491892-03d54c0aaf87?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Front-row bay views, full concierge service, dedicated parking, and access to a private marina slip." },
    { id: 6, title: "Maple Green Home", address: "318 Maple Green Way, Mission Hills, CA",
      region: "Urban", type: "Townhouse", beds: 4, baths: 3, sqft: 2200, year: 2016,
      lat: 32.748, lng: -117.180,
      price: 705000, status: "Sold", lead: 88, daysOnMarket: 18,
      tags: ["Sold over asking", "Garage", "EV charging"],
      images: [
        "https://images.unsplash.com/photo-1502673530728-f79b4cab31b1?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1574739782594-db4ead022697?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1564540583246-934409427776?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Modern townhouse with rooftop deck, EV-ready garage, and energy-efficient appliances throughout." },
    { id: 7, title: "Hillside Retreat", address: "55 Summit Ridge, Julian, CA",
      region: "Mountain", type: "House", beds: 6, baths: 4, sqft: 4800, year: 2022,
      lat: 33.080, lng: -116.600,
      price: 1250000, status: "Available", lead: 96, daysOnMarket: 4,
      tags: ["Mountain views", "Hot tub", "Acreage"],
      images: [
        "https://images.unsplash.com/photo-1613490493576-7fde63acd811?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600585152220-90363fe7e115?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Mountain retreat on 2 acres with panoramic ridge views, vaulted ceilings, and a chef-grade outdoor kitchen." },
    { id: 8, title: "Pine Ridge Cabin", address: "12 Pine Ridge Loop, Big Bear, CA",
      region: "Mountain", type: "House", beds: 3, baths: 2, sqft: 1750, year: 2010,
      lat: 34.240, lng: -116.910,
      price: 489000, status: "Available", lead: 71, daysOnMarket: 31,
      tags: ["Vacation rental potential", "Fireplace"],
      images: [
        "https://images.unsplash.com/photo-1542718610-a1d656d1884c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1518173946687-a4c8892bbd9f?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1518780664697-55e3ad937233?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Cozy A-frame cabin with stone fireplace, hot tub deck, and proximity to ski resort and lake." },
    { id: 9, title: "Modern Townhouse", address: "2841 Coastline Ave, San Diego, CA",
      region: "Coastal", type: "Townhouse", beds: 4, baths: 3, sqft: 2800, year: 2024,
      lat: 32.780, lng: -117.255,
      price: 1280000, status: "Available", lead: 91, daysOnMarket: 7,
      tags: ["3D Tour", "Smart Home", "New build"],
      images: [
        "https://images.unsplash.com/photo-1568605114967-8130f3a36994?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Brand-new townhouse with EV-ready 2-car garage, rooftop deck, and full smart-home automation." },
    { id: 10, title: "Cypress Court", address: "44 Cypress Ct, Encinitas, CA",
      region: "Suburban", type: "House", beds: 4, baths: 3, sqft: 2480, year: 2008,
      lat: 33.040, lng: -117.293,
      price: 920000, status: "Sold", lead: 82, daysOnMarket: 25,
      tags: ["Pool", "Solar"],
      images: [
        "https://images.unsplash.com/photo-1600585154363-67eb9e2e2099?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1574362848149-11496d93a7c7?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Solar-powered family home with saltwater pool, mature landscaping, and home theater." },
    { id: 11, title: "Ocean Bluff Modern", address: "300 Ocean Bluff Dr, Solana Beach, CA",
      region: "Coastal", type: "Luxury", beds: 5, baths: 5, sqft: 5400, year: 2023,
      lat: 32.990, lng: -117.270,
      price: 3280000, status: "Available", lead: 99, daysOnMarket: 12,
      tags: ["Ocean views", "Infinity pool", "Architect designed"],
      images: [
        "https://images.unsplash.com/photo-1613553497126-a44624272024?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Award-winning architecture with infinity pool, glass-walled great room, and direct beach access." },
    { id: 12, title: "Garden District Loft", address: "210 Magnolia St #4A, San Diego, CA",
      region: "Urban", type: "Condo", beds: 1, baths: 1, sqft: 880, year: 2017,
      lat: 32.745, lng: -117.165,
      price: 415000, status: "Pending", lead: 65, daysOnMarket: 9,
      tags: ["Walkable", "Pet-friendly"],
      images: [
        "https://images.unsplash.com/photo-1600585154084-4e5fe7c39198?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Charming garden-district loft with exposed brick, soaring ceilings, and shared community courtyard." },
    { id: 13, title: "Rio Marina Ranch", address: "16240 Rio Marina Rd at Hwy 67, Lakeside, CA",
      region: "Rural", type: "Luxury", beds: 5, baths: 4, sqft: 3650, year: 2021,
      lat: 32.970, lng: -116.940,
      price: 1485000, status: "Available", lead: 91, daysOnMarket: 3,
      tags: ["Spanish architecture", "1.2 acres", "Vaulted ceilings", "Modern luxury", "Easy Hwy 67 access"],
      images: [
        "images/route-67.jpg",
        "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1554995207-c18c203602cb?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Stunning 5-bedroom Spanish-revival estate on 1.2 acres at the crossroads of Rio Marina Road and Hwy 67. Twin balconies overlooking a private pergola courtyard, arched entryway with custom iron doors, vaulted ceilings throughout, chef's kitchen with marble island, two-car garage, and mature olive and palm landscaping. Only a 25-minute drive to downtown San Diego via I-8." },

    /* ---- v5 expansion: ten new available listings spread across regions
       and types so each filter has more to show. ---- */
    { id: 14, title: "Sunset Beach Cottage", address: "412 Sandpiper Way, Pacific Beach, CA",
      region: "Coastal", type: "House", beds: 3, baths: 2, sqft: 1650, year: 2008,
      lat: 32.802, lng: -117.252,
      price: 1150000, status: "Available", lead: 78, daysOnMarket: 12,
      tags: ["Walk to beach", "Open kitchen", "Ocean breezes"],
      images: [
        "https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Sun-drenched single-story cottage three blocks from the boardwalk. Updated kitchen, hardwood floors, fenced backyard with mature lemon tree, and detached studio garage." },

    { id: 15, title: "Pacific Highlands Condo", address: "1212 Crestline Dr #4B, Del Mar, CA",
      region: "Coastal", type: "Condo", beds: 2, baths: 2, sqft: 1250, year: 2019,
      lat: 32.959, lng: -117.265,
      price: 925000, status: "Available", lead: 71, daysOnMarket: 8,
      tags: ["Rooftop pool", "Concierge", "Sunset views"],
      images: [
        "https://images.unsplash.com/photo-1493809842364-78817add7ffb?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1502005229762-cf1b2da7c5d6?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600573472556-e636c2acda88?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Bright 2-bed condo with floor-to-ceiling glass, west-facing balcony for sunsets, and shared rooftop pool + lounge. Building includes 24-hour concierge and reserved EV parking." },

    { id: 16, title: "Marina Vista Townhouse", address: "8 Anchor Bay Ln, Solana Beach, CA",
      region: "Coastal", type: "Townhouse", beds: 3, baths: 3, sqft: 1950, year: 2017,
      lat: 32.991, lng: -117.275,
      price: 1395000, status: "Available", lead: 82, daysOnMarket: 14,
      tags: ["Private patio", "Marina views", "Smart thermostat"],
      images: [
        "https://images.unsplash.com/photo-1572120360610-d971b9d7767c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Three-level townhouse with marina views from the top floor primary suite. Open-concept main floor, two-car attached garage, and walkable to Cedros Design District." },

    { id: 17, title: "Arts District Loft", address: "78 Gaslamp Ave #1502, San Diego, CA",
      region: "Urban", type: "Condo", beds: 1, baths: 1, sqft: 980, year: 2021,
      lat: 32.713, lng: -117.158,
      price: 680000, status: "Available", lead: 65, daysOnMarket: 9,
      tags: ["Exposed brick", "City skyline", "Gallery walk"],
      images: [
        "https://images.unsplash.com/photo-1502672023488-70e25813eb80?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Industrial-modern loft in a converted warehouse — exposed brick, 14ft ceilings, and skyline views from a 15th-floor perch in the Arts District." },

    { id: 18, title: "Mission Plaza Townhouse", address: "204 Mission Plaza Pl, San Diego, CA",
      region: "Urban", type: "Townhouse", beds: 3, baths: 3, sqft: 1850, year: 2018,
      lat: 32.762, lng: -117.152,
      price: 895000, status: "Available", lead: 70, daysOnMarket: 18,
      tags: ["Rooftop deck", "EV charger", "Walk to trolley"],
      images: [
        "https://images.unsplash.com/photo-1583608205776-bfd35f0d9f83?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1554995207-c18c203602cb?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1613977257363-707ba9348227?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Three-bedroom townhouse with private rooftop deck, ground-floor flex room for office or guest, and EV-ready 2-car garage. Trolley stop two blocks away." },

    { id: 19, title: "Maplewood Crossing", address: "1147 Maplewood Cir, Carlsbad, CA",
      region: "Suburban", type: "House", beds: 4, baths: 3, sqft: 2650, year: 2015,
      lat: 33.121, lng: -117.262,
      price: 1250000, status: "Available", lead: 80, daysOnMarket: 11,
      tags: ["Cul-de-sac", "Solar panels", "Top schools"],
      images: [
        "https://images.unsplash.com/photo-1568605114967-8130f3a36994?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600585154526-990dced4db0d?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Four-bedroom Carlsbad home on a quiet cul-de-sac, owned solar panels with net-zero in summer, large backyard with covered patio, and award-winning school district." },

    { id: 20, title: "Hidden Glen Estate", address: "8 Hidden Glen Rd, Rancho Bernardo, CA",
      region: "Suburban", type: "Luxury", beds: 5, baths: 5, sqft: 5400, year: 2019,
      lat: 33.020, lng: -117.075,
      price: 2450000, status: "Available", lead: 88, daysOnMarket: 7,
      tags: ["Resort pool", "Home theater", "Wine cellar"],
      images: [
        "https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1576941089067-2de3c901e126?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1493809842364-78817add7ffb?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Five-bedroom luxury estate behind a private gate. Resort-style pool with spa, dedicated home theater, climate-controlled wine cellar, and 4-car garage." },

    { id: 21, title: "Cedar Ridge Cabin", address: "44 Cedar Ridge Rd, Julian, CA",
      region: "Mountain", type: "House", beds: 3, baths: 2, sqft: 1820, year: 2011,
      lat: 33.082, lng: -116.605,
      price: 725000, status: "Available", lead: 58, daysOnMarket: 22,
      tags: ["Stone fireplace", "Wraparound deck", "Pine grove"],
      images: [
        "https://images.unsplash.com/photo-1572120360610-d971b9d7767c?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600585154084-4e5fe7c39198?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Cedar-shingled mountain cabin tucked into a pine grove. Floor-to-ceiling stone fireplace, wraparound deck, and a detached art studio. Walk to downtown Julian." },

    { id: 22, title: "Alpine Vista Lodge", address: "9 Alpine Vista Ct, Big Bear Lake, CA",
      region: "Mountain", type: "Luxury", beds: 4, baths: 4, sqft: 3400, year: 2020,
      lat: 34.246, lng: -116.911,
      price: 1580000, status: "Available", lead: 75, daysOnMarket: 19,
      tags: ["Lake view", "Hot tub", "Game room"],
      images: [
        "https://images.unsplash.com/photo-1502005229762-cf1b2da7c5d6?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600585154084-4e5fe7c39198?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Modern mountain lodge with sweeping lake views, vaulted timber ceilings, full game room with pool table, outdoor hot tub on the upper deck, and a ski-tuning mudroom." },

    { id: 23, title: "Meadow Creek Farm", address: "1900 Meadow Creek Way, Ramona, CA",
      region: "Rural", type: "House", beds: 4, baths: 3, sqft: 2800, year: 2013,
      lat: 33.038, lng: -116.864,
      price: 885000, status: "Available", lead: 67, daysOnMarket: 31,
      tags: ["3 acres", "Barn", "Fenced pasture"],
      images: [
        "https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1613977257363-707ba9348227?auto=format&fit=crop&w=1200&q=80",
        "https://images.unsplash.com/photo-1600585154526-990dced4db0d?auto=format&fit=crop&w=1200&q=80"
      ],
      desc: "Four-bedroom farmhouse on three flat, irrigated acres. Detached barn with two stalls and tack room, fenced pasture, fruit trees, and a screened porch facing sunset." }
  ];

  /* ---------- Formatting ---------- */
  function fmtPrice(n) {
    if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 2)}M`;
    return `$${n.toLocaleString()}`;
  }
  function fmtUSD(n) {
    return `$${n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
  }
  function fmtUSDInt(n) { return `$${Math.round(n).toLocaleString()}`; }

  /* ---------- Carousel (used by hero, listings, modal) ---------- */
  function initCarousel(root) {
    const track = root.querySelector(".carousel-track");
    const slides = track.children;
    const total = slides.length;
    const prev = root.querySelector(".carousel-btn.prev");
    const next = root.querySelector(".carousel-btn.next");
    const dotsBox = root.querySelector(".carousel-dots");
    let index = 0;
    let timer;

    if (dotsBox) {
      dotsBox.innerHTML = "";
      for (let i = 0; i < total; i++) {
        const b = document.createElement("button");
        b.type = "button";
        b.setAttribute("aria-label", `Go to image ${i + 1}`);
        if (i === 0) b.classList.add("active");
        b.addEventListener("click", (e) => { e.stopPropagation(); go(i); });
        dotsBox.appendChild(b);
      }
    }
    function go(i) {
      index = (i + total) % total;
      track.style.transform = `translateX(-${index * 100}%)`;
      if (dotsBox) [...dotsBox.children].forEach((d, k) => d.classList.toggle("active", k === index));
    }
    function step(d) { go(index + d); restart(); }
    function restart() { clearInterval(timer); timer = setInterval(() => go(index + 1), 6000); }

    if (prev) prev.addEventListener("click", (e) => { e.stopPropagation(); step(-1); });
    if (next) next.addEventListener("click", (e) => { e.stopPropagation(); step(1); });
    restart();
    root.addEventListener("mouseenter", () => clearInterval(timer));
    root.addEventListener("mouseleave", restart);
  }

  /* ---------- Property modal ---------- */
  function setupModal() {
    const modal = document.getElementById("propertyModal");
    if (!modal) return;
    modal.querySelectorAll("[data-close]").forEach(el => el.addEventListener("click", closeModal));
    document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !modal.hidden) closeModal(); });
  }
  function openPropertyModal(id) {
    const modal = document.getElementById("propertyModal");
    const body  = document.getElementById("modalBody");
    const p = PROPERTIES.find(x => x.id === id);
    if (!modal || !body || !p) return;
    /* The per-listing colored stat selectors live in script.js (index
       page only). Guarded so the modal still works on login/dashboard
       where script.js isn't loaded. */
    const hasRail = (typeof iconRailHtml === "function");
    const railHtml = hasRail ? iconRailHtml(new Set()) : "";
    /* The modal is shared with the public site. Inside the Seller Console
       (body.dash-body) the buyer-facing CTAs make no sense, so swap in the
       owner actions: Edit, Sold, Remove. They emit a "houseator:property-action"
       event that dashboard.js handles with its existing editor / CRUD logic. */
    const isConsole = document.body.classList.contains("dash-body");
    const ctaRowHtml = isConsole
      ? `<div class="cta-row console-cta-row">
            <button class="button button-secondary" data-console-act="edit">Edit</button>
            <button class="button button-primary" data-console-act="sold"${p.status === "Sold" ? " disabled" : ""}>Sold</button>
            <button class="button button-danger" data-console-act="remove">Remove</button>
          </div>`
      : `<div class="cta-row">
            <button class="button button-primary">Schedule Tour</button>
            <button class="button button-secondary">Request Info</button>
          </div>`;
    body.innerHTML = `
      <div class="modal-detail-grid">
        <div class="image-carousel" data-carousel>
          <div class="carousel-track">
            ${p.images.map(src => `<img src="${src}" alt="${p.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>
        </div>
        <div class="modal-info" data-id="${p.id}">
          <span class="tag tag-tour">${p.status}</span>
          <h3>${p.title}</h3>
          <p class="muted">${p.address}</p>
          <strong class="price">${fmtPrice(p.price)}</strong>
          <div class="specs-grid">
            <div><b>${p.beds}</b> Bedrooms</div>
            <div><b>${p.baths}</b> Bathrooms</div>
            <div><b>${p.sqft.toLocaleString()}</b> ft²</div>
            <div>Built <b>${p.year}</b></div>
            <div>Region: <b>${p.region}</b></div>
            <div>Type: <b>${p.type}</b></div>
          </div>
          ${hasRail ? `<div class="modal-stat-selectors">
            <span class="modal-stat-label">Tap to compare neighborhood stats</span>
            ${railHtml}
            <div class="property-stats" data-stats></div>
          </div>` : ""}
          <p class="desc">${p.desc}</p>
          <div class="property-features">
            ${p.tags.map(t => `<span class="tag">${t}</span>`).join("")}
          </div>
          ${ctaRowHtml}
        </div>
      </div>
    `;
    modal.hidden = false;
    document.body.style.overflow = "hidden";
    body.querySelectorAll("[data-carousel]").forEach(initCarousel);

    /* Console owner actions — delegate to dashboard.js via a custom event so
       the editor / mark-sold / remove flows reuse the existing handlers. */
    if (isConsole) {
      body.querySelectorAll("[data-console-act]").forEach(btn => {
        btn.addEventListener("click", (e) => {
          e.stopPropagation();
          document.dispatchEvent(new CustomEvent("houseator:property-action", {
            detail: { id: p.id, action: btn.dataset.consoleAct }
          }));
        });
      });
    }

    /* Seed + wire the stat selectors. The .modal-info element is the
       "card" renderCardStats expects (carries data-id, holds the rail
       and the [data-stats] box). One delegated listener, attached once. */
    if (hasRail && typeof renderCardStats === "function") {
      const info = body.querySelector(".modal-info");
      renderCardStats(info);
      if (!body.dataset.statRailWired) {
        body.dataset.statRailWired = "1";
        body.addEventListener("click", (e) => {
          const ico = e.target.closest(".mr-ico");
          if (!ico) return;
          e.stopPropagation();
          e.preventDefault();
          const info = ico.closest(".modal-info");
          /* Reuse script.js's shared rail logic (handles All/Reset);
             fall back to a plain toggle if it isn't loaded. */
          if (typeof applyRailToggle === "function") {
            applyRailToggle(ico, info);
          } else {
            const on = ico.classList.toggle("active");
            ico.setAttribute("aria-pressed", on ? "true" : "false");
            renderCardStats(info);
          }
        });
      }
    }
  }
  function closeModal() {
    const modal = document.getElementById("propertyModal");
    if (!modal) return;
    modal.hidden = true;
    document.body.style.overflow = "";
  }

  /* ---------- Auth helpers (sessionStorage only — cleared on tab close) ---------- */
  function isAuthed()  { try { return !!sessionStorage.getItem(AUTH_KEY); } catch (_) { return false; } }
  function setAuthed() { try { sessionStorage.setItem(AUTH_KEY, "1"); } catch (_) {} }
  function clearAuth() { try { sessionStorage.removeItem(AUTH_KEY); sessionStorage.removeItem(USER_KEY); } catch (_) {} }
  function setUser(name) { try { sessionStorage.setItem(USER_KEY, name || "Agent"); } catch (_) {} }
  function getUser() { try { return sessionStorage.getItem(USER_KEY) || "Agent"; } catch (_) { return "Agent"; } }

  /* ---------- Persistence + CRUD on listings ---------- */
  function persist() {
    try { localStorage.setItem(STORE_KEY, JSON.stringify(PROPERTIES)); } catch (_) {}
  }
  function loadStored() {
    try {
      const raw = localStorage.getItem(STORE_KEY);
      if (!raw) return null;
      const parsed = JSON.parse(raw);
      return Array.isArray(parsed) ? parsed : null;
    } catch (_) { return null; }
  }
  /* Snapshot the pristine seed before stored data replaces it.
     We need this for: (a) backfilling missing lat/lng on older saves,
     and (b) one-time migrations that add new seed listings. */
  const SEED_SNAPSHOT = JSON.parse(JSON.stringify(PROPERTIES));
  const SEED_COORDS = SEED_SNAPSHOT.reduce((acc, p) => {
    if (p.lat != null && p.lng != null) acc[p.id] = { lat: p.lat, lng: p.lng };
    return acc;
  }, {});

  /* Replace seed PROPERTIES with stored copy (in-place so existing references stay valid). */
  const stored = loadStored();
  if (stored && stored.length) {
    PROPERTIES.length = 0;
    stored.forEach(p => PROPERTIES.push(p));
  }

  let migratedAny = false;

  /* One-time migrations — versioned so they run exactly once per browser
     and don't fight user deletions on subsequent loads. */
  try {
    /* v3: add any seed property whose id isn't already in stored
       (introduced Rio Marina Ranch + future additions). */
    if (!localStorage.getItem("houseator_migration_v3")) {
      SEED_SNAPSHOT.forEach(seed => {
        if (!PROPERTIES.find(p => p.id === seed.id)) {
          PROPERTIES.push(JSON.parse(JSON.stringify(seed)));
          migratedAny = true;
        }
      });
      localStorage.setItem("houseator_migration_v3", "1");
    }
    /* v4: refresh Rio Marina Ranch (id 13) with the local hero image
       and updated details. Preserves user comments. */
    if (!localStorage.getItem("houseator_migration_v4")) {
      const seedRio = SEED_SNAPSHOT.find(s => s.id === 13);
      const idx = PROPERTIES.findIndex(p => p.id === 13);
      if (seedRio && idx >= 0) {
        const preservedComments = Array.isArray(PROPERTIES[idx].comments)
          ? PROPERTIES[idx].comments
          : [];
        PROPERTIES[idx] = JSON.parse(JSON.stringify(seedRio));
        PROPERTIES[idx].comments = preservedComments;
        migratedAny = true;
      }
      localStorage.setItem("houseator_migration_v4", "1");
    }
    /* v5: ten new available listings (ids 14–23) spread across regions
       and types. Same pattern as v3 — add any seed property whose id
       isn't already in the stored copy, so we don't fight user deletions. */
    if (!localStorage.getItem("houseator_migration_v5")) {
      SEED_SNAPSHOT.forEach(seed => {
        if (seed.id >= 14 && !PROPERTIES.find(p => p.id === seed.id)) {
          PROPERTIES.push(JSON.parse(JSON.stringify(seed)));
          migratedAny = true;
        }
      });
      localStorage.setItem("houseator_migration_v5", "1");
    }
    /* v6: refresh Meadow Creek Farm (id 23) — the first image URL in v5
       was unreliable. Replace the images array with the corrected seed
       version. Preserve user comments. */
    if (!localStorage.getItem("houseator_migration_v6")) {
      const seedMcf = SEED_SNAPSHOT.find(s => s.id === 23);
      const idx = PROPERTIES.findIndex(p => p.id === 23);
      if (seedMcf && idx >= 0) {
        PROPERTIES[idx].images = JSON.parse(JSON.stringify(seedMcf.images));
        migratedAny = true;
      }
      localStorage.setItem("houseator_migration_v6", "1");
    }
    /* v7: re-seed the images array on Rio Marina Ranch (13), Cedar Ridge
       Cabin (21), and Meadow Creek Farm (23) from the current seed.
       Stored copies in localStorage had broken/dark Unsplash URLs that
       earlier migrations couldn't reach (v4/v6 already ran for some
       browsers, and Cedar Ridge had no refresh migration at all).
       Preserves all other user data on these listings. */
    if (!localStorage.getItem("houseator_migration_v7")) {
      [13, 21, 23].forEach(id => {
        const seed = SEED_SNAPSHOT.find(s => s.id === id);
        const idx  = PROPERTIES.findIndex(p => p.id === id);
        if (seed && idx >= 0) {
          PROPERTIES[idx].images = JSON.parse(JSON.stringify(seed.images));
          migratedAny = true;
        }
      });
      localStorage.setItem("houseator_migration_v7", "1");
    }
  } catch (_) {}

  /* Always backfill missing fields (lat/lng/comments) for older saves. */
  PROPERTIES.forEach(p => {
    if (!Array.isArray(p.comments)) p.comments = [];
    if ((p.lat == null || p.lng == null) && SEED_COORDS[p.id]) {
      p.lat = SEED_COORDS[p.id].lat;
      p.lng = SEED_COORDS[p.id].lng;
      migratedAny = true;
    }
  });

  if (migratedAny) {
    try { localStorage.setItem(STORE_KEY, JSON.stringify(PROPERTIES)); } catch (_) {}
  }

  function nextId() {
    return PROPERTIES.reduce((m, p) => Math.max(m, p.id), 0) + 1;
  }
  function updateProperty(id, patch) {
    const i = PROPERTIES.findIndex(p => p.id === id);
    if (i < 0) return null;
    PROPERTIES[i] = { ...PROPERTIES[i], ...patch };
    persist();
    return PROPERTIES[i];
  }
  function addProperty(props) {
    const p = { id: nextId(), comments: [], daysOnMarket: 0, tags: [], images: [], ...props };
    PROPERTIES.push(p);
    persist();
    return p;
  }
  function deleteProperty(id) {
    const i = PROPERTIES.findIndex(p => p.id === id);
    if (i < 0) return false;
    PROPERTIES.splice(i, 1);
    persist();
    return true;
  }
  function resetProperties() {
    try { localStorage.removeItem(STORE_KEY); } catch (_) {}
  }

  /* ---------- To-Do tasks (separate store) ---------- */
  let TODOS = (function loadTodos() {
    try {
      const raw = localStorage.getItem(TODO_KEY);
      if (!raw) return seedTodos();
      const parsed = JSON.parse(raw);
      return Array.isArray(parsed) ? parsed : seedTodos();
    } catch (_) { return seedTodos(); }
  })();
  function persistTodos() {
    try { localStorage.setItem(TODO_KEY, JSON.stringify(TODOS)); } catch (_) {}
  }
  function seedTodos() {
    const today = new Date();
    const iso = (d) => d.toISOString().slice(0, 10);
    const plus = (n) => { const d = new Date(today); d.setDate(d.getDate() + n); return iso(d); };
    return [
      { id: 1, title: "Follow up with Jordan R. on Bayfront offer", description: "Buyer is comparing two listings — check by 3pm.", status: "todo", greyed: false, date: iso(today), assignee: "House1234", attachments: [], createdAt: Date.now() },
      { id: 2, title: "Confirm 3pm showing at Oak Valley Estate", description: "", status: "doing", greyed: false, date: iso(today), assignee: "House1234", attachments: [], createdAt: Date.now() },
      { id: 3, title: "Upload disclosure docs for Maple Green", description: "Title report + HOA bylaws.", status: "todo", greyed: false, date: plus(1), assignee: "House1234", attachments: [], createdAt: Date.now() },
      { id: 4, title: "Send weekly market report to seller list", description: "", status: "todo", greyed: false, date: plus(2), assignee: "House1234", attachments: [], createdAt: Date.now() },
      { id: 5, title: "Review AI pricing suggestion for new listing", description: "", status: "doing", greyed: false, date: plus(3), assignee: "House1234", attachments: [], createdAt: Date.now() },
      { id: 6, title: "Close on Cypress Court", description: "Wire confirmation received.", status: "done", greyed: false, date: plus(-2), assignee: "House1234", attachments: [], createdAt: Date.now() }
    ];
  }
  function listTodos() { return TODOS; }
  function nextTodoId() { return TODOS.reduce((m, t) => Math.max(m, t.id), 0) + 1; }
  function addTodo(props = {}) {
    const t = {
      id: nextTodoId(),
      title: "", description: "",
      status: "todo", greyed: false,
      date: null, assignee: getUser(),
      attachments: [], createdAt: Date.now(),
      ...props
    };
    TODOS.push(t); persistTodos();
    return t;
  }
  function updateTodo(id, patch) {
    const i = TODOS.findIndex(t => t.id === id);
    if (i < 0) return null;
    TODOS[i] = { ...TODOS[i], ...patch };
    persistTodos();
    return TODOS[i];
  }
  function deleteTodo(id) {
    const i = TODOS.findIndex(t => t.id === id);
    if (i < 0) return false;
    TODOS.splice(i, 1); persistTodos();
    return true;
  }
  function resetTodos() {
    try { localStorage.removeItem(TODO_KEY); } catch (_) {}
  }

  /* ---------- Property comment helpers (with stable IDs + grey-out flag) ---------- */
  function ensurePropertyCommentIds(p) {
    p.comments = p.comments || [];
    let max = p.comments.reduce((m, c) => Math.max(m, c.id || 0), 0);
    p.comments.forEach(c => {
      if (!c.id) c.id = ++max;
      if (typeof c.greyed !== "boolean") c.greyed = false;
    });
  }
  function addPropertyComment(propId, body, author) {
    const p = PROPERTIES.find(x => x.id === propId);
    if (!p) return null;
    ensurePropertyCommentIds(p);
    const nextId = p.comments.reduce((m, c) => Math.max(m, c.id), 0) + 1;
    const c = { id: nextId, author: author || getUser(), body: String(body), at: Date.now(), greyed: false };
    p.comments.push(c);
    persist();
    return c;
  }
  function updatePropertyComment(propId, commentId, patch) {
    const p = PROPERTIES.find(x => x.id === propId);
    if (!p) return null;
    ensurePropertyCommentIds(p);
    const c = p.comments.find(c => c.id === commentId);
    if (!c) return null;
    Object.assign(c, patch);
    persist();
    return c;
  }
  function deletePropertyComment(propId, commentId) {
    const p = PROPERTIES.find(x => x.id === propId);
    if (!p) return false;
    ensurePropertyCommentIds(p);
    p.comments = p.comments.filter(c => c.id !== commentId);
    persist();
    return true;
  }

  /* ---------- Leads (separate store, with comments) ---------- */
  function seedLeads() {
    const now = Date.now();
    const m = (n) => now - n * 60 * 1000;
    return [
      { id: 1, name: "Jordan R.", email: "jordan.r@example.com", phone: "+1 555 0142",
        interestedIn: "Bayfront Condo", status: "hot", source: "Website form", createdAt: now,
        comments: [
          { id: 1, author: "House1234", body: "Comparing Bayfront with Maple Green. Wants tour by Friday.", at: m(180), greyed: false },
          { id: 2, author: "House1234", body: "Pre-approved up to $900k.", at: m(60), greyed: false }
        ] },
      { id: 2, name: "Maya K.", email: "maya.k@example.com", phone: "+1 555 0188",
        interestedIn: "Oak Valley Estate", status: "warm", source: "Open house", createdAt: now,
        comments: [
          { id: 1, author: "House1234", body: "Tour scheduled for 3pm. Bring inspection checklist.", at: m(22), greyed: false }
        ] },
      { id: 3, name: "Devon T.", email: "devon.t@example.com", phone: "+1 555 0211",
        interestedIn: "Hillside Retreat", status: "warm", source: "Zillow inquiry", createdAt: now,
        comments: [] },
      { id: 4, name: "Sara P.", email: "sara.p@example.com", phone: "+1 555 0309",
        interestedIn: "Saved 6 properties", status: "cold", source: "Newsletter", createdAt: now,
        comments: [] }
    ];
  }
  let LEADS = (function loadLeads() {
    try {
      const raw = localStorage.getItem(LEADS_KEY);
      if (!raw) return seedLeads();
      const parsed = JSON.parse(raw);
      return Array.isArray(parsed) ? parsed : seedLeads();
    } catch (_) { return seedLeads(); }
  })();
  function persistLeads() {
    try { localStorage.setItem(LEADS_KEY, JSON.stringify(LEADS)); } catch (_) {}
  }
  function listLeads() { return LEADS; }
  function nextLeadId() { return LEADS.reduce((m, l) => Math.max(m, l.id), 0) + 1; }
  function ensureLeadCommentIds(lead) {
    lead.comments = lead.comments || [];
    let max = lead.comments.reduce((m, c) => Math.max(m, c.id || 0), 0);
    lead.comments.forEach(c => {
      if (!c.id) c.id = ++max;
      if (typeof c.greyed !== "boolean") c.greyed = false;
    });
  }
  function addLead(props = {}) {
    const lead = {
      id: nextLeadId(),
      name: "", email: "", phone: "", interestedIn: "",
      status: "warm", source: "", createdAt: Date.now(),
      comments: [],
      ...props
    };
    LEADS.push(lead);
    persistLeads();
    return lead;
  }
  function updateLead(id, patch) {
    const i = LEADS.findIndex(l => l.id === id);
    if (i < 0) return null;
    LEADS[i] = { ...LEADS[i], ...patch };
    persistLeads();
    return LEADS[i];
  }
  function deleteLead(id) {
    const i = LEADS.findIndex(l => l.id === id);
    if (i < 0) return false;
    LEADS.splice(i, 1);
    persistLeads();
    return true;
  }
  function addLeadComment(leadId, body, author) {
    const lead = LEADS.find(l => l.id === leadId);
    if (!lead) return null;
    ensureLeadCommentIds(lead);
    const nextId = lead.comments.reduce((m, c) => Math.max(m, c.id), 0) + 1;
    const c = { id: nextId, author: author || getUser(), body: String(body), at: Date.now(), greyed: false };
    lead.comments.push(c);
    persistLeads();
    return c;
  }
  function updateLeadComment(leadId, commentId, patch) {
    const lead = LEADS.find(l => l.id === leadId);
    if (!lead) return null;
    ensureLeadCommentIds(lead);
    const c = lead.comments.find(c => c.id === commentId);
    if (!c) return null;
    Object.assign(c, patch);
    persistLeads();
    return c;
  }
  function deleteLeadComment(leadId, commentId) {
    const lead = LEADS.find(l => l.id === leadId);
    if (!lead) return false;
    ensureLeadCommentIds(lead);
    lead.comments = lead.comments.filter(c => c.id !== commentId);
    persistLeads();
    return true;
  }
  function resetLeads() {
    try { localStorage.removeItem(LEADS_KEY); } catch (_) {}
  }

  /* ---------- Contacts (people in your network: clients + vendors) ---------- */
  function seedContacts() {
    const now = Date.now();
    const d = (n) => now - n * 86400000;
    return [
      { id: 1, name: "Maria Sanchez", phone: "+1 619 555 0142", email: "maria.sanchez@pacifictitle.com",
        role: "Title Officer", company: "Pacific Title Co.", greyed: false, createdAt: d(40),
        comments: [
          { id: 1, author: "House1234", body: "Handles every coastal closing. Quick turnaround on payoffs.", at: d(12), greyed: false }
        ] },
      { id: 2, name: "David Chen", phone: "+1 619 555 0287", email: "david@bluepeakinspect.com",
        role: "Inspector", company: "BluePeak Home Inspections", greyed: false, createdAt: d(55),
        comments: [
          { id: 1, author: "House1234", body: "Catches structural issues — recommend for older builds.", at: d(7), greyed: false }
        ] },
      { id: 3, name: "Rachel Park", phone: "+1 619 555 0319", email: "rachel.park@coastallending.com",
        role: "Mortgage Lender", company: "Coastal Lending", greyed: false, createdAt: d(28),
        comments: [] },
      { id: 4, name: "Carlos Mendoza", phone: "+1 760 555 0411", email: "carlos@mendozapest.com",
        role: "Pest Control", company: "Mendoza Pest Solutions", greyed: true, createdAt: d(120),
        comments: [
          { id: 1, author: "House1234", body: "Slow to schedule — keeping in network as backup.", at: d(60), greyed: false }
        ] },
      { id: 5, name: "Linda Wagner", phone: "+1 619 555 0524", email: "linda@wagnervisuals.com",
        role: "Photographer", company: "Wagner Visuals", greyed: false, createdAt: d(18),
        comments: [] },
      { id: 6, name: "James Whitfield", phone: "+1 619 555 0633", email: "j.whitfield@example.com",
        role: "Buyer", company: "", greyed: false, createdAt: d(4),
        comments: [
          { id: 1, author: "House1234", body: "Looking in $700k–$1M range, coastal preferred.", at: d(2), greyed: false }
        ] },
      { id: 7, name: "Sara Kim", phone: "+1 858 555 0745", email: "sara.kim@example.com",
        role: "Seller", company: "", greyed: false, createdAt: d(10),
        comments: [] },
      { id: 8, name: "Tom Brennan", phone: "+1 619 555 0852", email: "tom@brennan-construction.com",
        role: "Contractor", company: "Brennan Construction", greyed: false, createdAt: d(90),
        comments: [] }
    ];
  }
  let CONTACTS = (function loadContacts() {
    try {
      const raw = localStorage.getItem(CONTACTS_KEY);
      if (!raw) return seedContacts();
      const parsed = JSON.parse(raw);
      return Array.isArray(parsed) ? parsed : seedContacts();
    } catch (_) { return seedContacts(); }
  })();
  function persistContacts() {
    try { localStorage.setItem(CONTACTS_KEY, JSON.stringify(CONTACTS)); } catch (_) {}
  }
  function listContacts() { return CONTACTS; }
  function nextContactId() { return CONTACTS.reduce((m, c) => Math.max(m, c.id), 0) + 1; }
  function ensureContactCommentIds(contact) {
    contact.comments = contact.comments || [];
    let max = contact.comments.reduce((m, c) => Math.max(m, c.id || 0), 0);
    contact.comments.forEach(c => {
      if (!c.id) c.id = ++max;
      if (typeof c.greyed !== "boolean") c.greyed = false;
    });
  }
  function addContact(props = {}) {
    const c = {
      id: nextContactId(),
      name: "", phone: "", email: "",
      role: "Other", company: "",
      greyed: false, createdAt: Date.now(),
      comments: [],
      ...props
    };
    CONTACTS.push(c);
    persistContacts();
    return c;
  }
  function updateContact(id, patch) {
    const i = CONTACTS.findIndex(c => c.id === id);
    if (i < 0) return null;
    CONTACTS[i] = { ...CONTACTS[i], ...patch };
    persistContacts();
    return CONTACTS[i];
  }
  function deleteContact(id) {
    const i = CONTACTS.findIndex(c => c.id === id);
    if (i < 0) return false;
    CONTACTS.splice(i, 1);
    persistContacts();
    return true;
  }
  function addContactComment(contactId, body, author) {
    const contact = CONTACTS.find(c => c.id === contactId);
    if (!contact) return null;
    ensureContactCommentIds(contact);
    const nextId = contact.comments.reduce((m, c) => Math.max(m, c.id), 0) + 1;
    const c = { id: nextId, author: author || getUser(), body: String(body), at: Date.now(), greyed: false };
    contact.comments.push(c);
    persistContacts();
    return c;
  }
  function updateContactComment(contactId, commentId, patch) {
    const contact = CONTACTS.find(c => c.id === contactId);
    if (!contact) return null;
    ensureContactCommentIds(contact);
    const c = contact.comments.find(c => c.id === commentId);
    if (!c) return null;
    Object.assign(c, patch);
    persistContacts();
    return c;
  }
  function deleteContactComment(contactId, commentId) {
    const contact = CONTACTS.find(c => c.id === contactId);
    if (!contact) return false;
    ensureContactCommentIds(contact);
    contact.comments = contact.comments.filter(c => c.id !== commentId);
    persistContacts();
    return true;
  }
  function resetContacts() {
    try { localStorage.removeItem(CONTACTS_KEY); } catch (_) {}
  }

  /* ---------- Theme toggle (shared) ---------- */
  function setupTheme() {
    const btn = document.getElementById("themeToggle");
    let saved = null;
    try { saved = localStorage.getItem("ho-theme"); } catch (_) {}
    if (saved === "dark") document.documentElement.setAttribute("data-theme", "dark");
    if (btn) {
      btn.addEventListener("click", () => {
        const isDark = document.documentElement.getAttribute("data-theme") === "dark";
        document.documentElement.setAttribute("data-theme", isDark ? "light" : "dark");
        try { localStorage.setItem("ho-theme", isDark ? "light" : "dark"); } catch (_) {}
      });
    }
  }

  /* ---------- Featured property (picked by the seller, shown on the
     public homepage hero). Persisted as a single id in localStorage. ---------- */
  const FEATURE_KEY = "houseator_feature_id_v1";
  function getFeatureId() {
    try {
      const raw = localStorage.getItem(FEATURE_KEY);
      if (!raw) return null;
      const n = parseInt(raw, 10);
      return Number.isFinite(n) ? n : null;
    } catch (_) { return null; }
  }
  function setFeatureId(id) {
    try {
      if (id == null) localStorage.removeItem(FEATURE_KEY);
      else localStorage.setItem(FEATURE_KEY, String(id));
    } catch (_) {}
  }
  function getFeaturedProperty() {
    const id = getFeatureId();
    if (id == null) return null;
    return PROPERTIES.find(p => p.id === id) || null;
  }

  return {
    PROPERTIES, fmtPrice, fmtUSD, fmtUSDInt,
    initCarousel, openPropertyModal, closeModal, setupModal,
    isAuthed, setAuthed, clearAuth, setUser, getUser, setupTheme,
    updateProperty, addProperty, deleteProperty, resetProperties,
    addPropertyComment, updatePropertyComment, deletePropertyComment,
    listTodos, addTodo, updateTodo, deleteTodo, resetTodos,
    listLeads, addLead, updateLead, deleteLead,
    addLeadComment, updateLeadComment, deleteLeadComment, resetLeads,
    listContacts, addContact, updateContact, deleteContact,
    addContactComment, updateContactComment, deleteContactComment, resetContacts,
    getFeatureId, setFeatureId, getFeaturedProperty
  };
})();

