← back to Malden House

rental.js

801 lines

// rental.js — reads a saved rental from localStorage and patches rental.html
(function () {
  // Schema check — wipe stale v1/v2 records on first load
  try {
    const HTG_SCHEMA = 3;
    const v = Number(localStorage.getItem("htg.schema") || 1);
    if (v !== HTG_SCHEMA) {
      localStorage.removeItem("htg.rentals");
      localStorage.removeItem("htg.active");
      localStorage.removeItem("victorystays.rental");
      localStorage.setItem("htg.schema", HTG_SCHEMA);
    }
  } catch (e) {}

  function load() {
    const params = new URLSearchParams(location.search);
    const id = params.get("id") || localStorage.getItem("htg.active");
    const all = JSON.parse(localStorage.getItem("htg.rentals") || "{}");
    if (id && all[id]) return all[id];
    // Back-compat with old single-record key
    const legacy = localStorage.getItem("victorystays.rental");
    if (legacy) return JSON.parse(legacy);
    return null;
  }

  function splitStreet(street) {
    // "471 S Peck Dr" -> { number: "471", rest: "S PECK DR" }
    const m = String(street || "").match(/^\s*(\d+[\w-]*)\s+(.+)$/);
    if (!m) return { number: "—", rest: (street || "").toUpperCase() };
    return { number: m[1], rest: m[2].toUpperCase() };
  }

  function text(sel, val) { const el = document.querySelector(sel); if (el) el.textContent = val; }
  function html(sel, val) { const el = document.querySelector(sel); if (el) el.innerHTML = val; }
  function escapeHtml(s) {
    return String(s ?? "")
      .replace(/&/g, "&")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }
  const escapeAttr = escapeHtml;
  function formatMoney(v) {
    return v === null || v === undefined || v === "" ? "—" : escapeHtml(v.toLocaleString());
  }

  // Expand a date-range string like "Jul 15 – Jul 23, 2028" (or single "Jul 14, 2028")
  // into an array of { mm, dd, year, dow } entries.
  function expandDates(str) {
    const monthMap = { Jan:0, Feb:1, Mar:2, Apr:3, May:4, Jun:5, Jul:6, Aug:7, Sep:8, Oct:9, Nov:10, Dec:11 };
    const mNames = Object.keys(monthMap);
    const clean = String(str || "").replace(/[–—]/g, "-").trim();
    // Range with same or different months: "Jul 15 - Jul 23, 2028"
    let m = clean.match(/^(\w{3})\s+(\d{1,2})\s*-\s*(\w{3})\s+(\d{1,2}),\s*(\d{4})$/);
    if (m) {
      const [, m1, d1, m2, d2, y] = m;
      const start = new Date(+y, monthMap[m1], +d1);
      const end   = new Date(+y, monthMap[m2], +d2);
      const out = [];
      for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
        out.push({ mm: mNames[d.getMonth()], dd: d.getDate(), year: d.getFullYear(), dow: d.getDay() });
      }
      return out;
    }
    // Single-day: "Jul 14, 2028"
    m = clean.match(/^(\w{3})\s+(\d{1,2}),\s*(\d{4})$/);
    if (m) {
      const d = new Date(+m[3], monthMap[m[1]], +m[2]);
      return [{ mm: m[1], dd: +m[2], year: +m[3], dow: d.getDay() }];
    }
    return [];
  }

  // -----------------------------------------------------------------
  //  SCRIPTED TRAILER — commercial-style 30-second rental promo
  // -----------------------------------------------------------------
  function buildTrailer(ctx) {
    const { address, host, nearest, price, fullAddr } = ctx;
    const player = document.querySelector(".player");
    if (!player) return;
    const svQ  = encodeURIComponent(fullAddr);
    const sat  = `https://maps.google.com/maps?q=${svQ}&t=k&z=19&output=embed`;
    const sv   = `https://maps.google.com/maps?q=${svQ}&layer=c&output=svembed`;
    const c0   = nearest[0] || {};
    const days = expandDates(c0.dates || "Jul 14 – Jul 30, 2028");
    const dow  = ["SUN","MON","TUE","WED","THU","FRI","SAT"];
    const daysHTML = days.slice(0, 12).map(d => `
      <span class="tr-day"><em>${dow[d.dow]}</em><b>${d.dd}</b><i>${d.mm.toUpperCase()}</i></span>
    `).join("");

    const scenes = [
      { dur: 4000, id: "intro", html: `
          <div class="tr-intro">
            <div class="tr-kicker">✦ A PRIVATE HOME FOR THE GAMES</div>
            <div class="tr-title-big">${escapeHtml(address.street || "Your House")}</div>
            <div class="tr-title-sub">${escapeHtml((address.city || "Los Angeles").toUpperCase())} · CA</div>
            <div class="tr-intro-tag">HOSTED BY ${escapeHtml((host || "").toUpperCase())}</div>
          </div>` },
      { dur: 5000, id: "satellite", html: `
        <iframe class="tr-frame" src="${sat}" loading="lazy" title="Aerial"></iframe>
        <div class="tr-vignette"></div>
          <div class="tr-lower-third">
          <div class="tr-eyebrow">AERIAL · ${escapeHtml(address.zip || "")}</div>
          <div class="tr-headline">${escapeHtml(address.street || "")}</div>
          <div class="tr-sub">${escapeHtml(address.city || "")}, ${escapeHtml(address.state || "CA")}</div>
        </div>` },
      { dur: 5000, id: "streetview", html: `
        <iframe class="tr-frame" src="${sv}" loading="lazy" title="Street view"></iframe>
        <div class="tr-vignette"></div>
        <div class="tr-lower-third">
          <div class="tr-eyebrow">THE FRONT · 360°</div>
          <div class="tr-headline">Walk up to the door.</div>
          <div class="tr-sub">Live from Google Street View</div>
        </div>` },
      { dur: 4000, id: "photo1", html: `
        <img class="tr-photo" src="https://picsum.photos/seed/${encodeURIComponent((address.street||'living')+'-living')}/1600/900" alt="Living">
        <div class="tr-vignette"></div>
        <div class="tr-lower-third">
          <div class="tr-eyebrow">THE LIVING ROOM</div>
          <div class="tr-headline">Room for six.</div>
          <div class="tr-sub">Three bedrooms · two baths</div>
        </div>` },
      { dur: 4000, id: "photo2", html: `
        <img class="tr-photo" src="https://picsum.photos/seed/${encodeURIComponent((address.street||'kitchen')+'-kitchen')}/1600/900" alt="Kitchen">
        <div class="tr-vignette"></div>
        <div class="tr-lower-third">
          <div class="tr-eyebrow">THE KITCHEN</div>
          <div class="tr-headline">Dinner at home.</div>
          <div class="tr-sub">Cook in, or Postmates a medal-winner meal</div>
        </div>` },
      { dur: 5000, id: "olympic", html: `
          <div class="tr-olympic">
            <div class="tr-kicker">✦ CLOSEST GAMES EVENT</div>
          <div class="tr-title-big">${escapeHtml(c0.headline || c0.sports || "LA 2028")}</div>
          <div class="tr-olympic-venue">${escapeHtml(c0.name || "")} · ${(c0.mi || 0).toFixed(1)} mi from your door</div>
          <div class="tr-daybar">${daysHTML}</div>
        </div>` },
      { dur: 4000, id: "outro", html: `
          <div class="tr-outro">
            <div class="tr-kicker">✦ RESERVE</div>
            <div class="tr-title-big">$${formatMoney(price && price.mid)}<span class="tr-unit">/ night</span></div>
          <div class="tr-sub">${escapeHtml(c0.dates || "Jul 14 – Jul 30, 2028")}</div>
          <div class="tr-cta-row">
            <span class="tr-pill">victorystays.com/${(address.street || "").toLowerCase().replace(/[^a-z0-9]+/g, "-")}</span>
          </div>
          <div class="tr-outro-brand">VICTORY STAYS · LA 2028</div>
        </div>` },
    ];
    const total = scenes.reduce((a, s) => a + s.dur, 0);

    player.innerHTML = `
      <div class="tr-stage" id="tr-stage"></div>
      <button class="tr-playbtn" id="tr-playbtn" aria-label="Play">
        <span class="tr-playicon">▶</span>
        <span class="tr-playlabel">PLAY 30-SEC TRAILER</span>
      </button>
      <div class="tr-cover" id="tr-cover">
        <img src="${escapeAttr(sat.replace('&output=embed',''))}" alt="">
        <div class="tr-cover-overlay"></div>
        <div class="tr-cover-text">
          <div class="tr-kicker">✦ PROPERTY TRAILER</div>
          <div class="tr-title-big">${escapeHtml(address.street || "Your House")}</div>
          <div class="tr-sub">${escapeHtml((address.city || "Los Angeles").toUpperCase())} · 30 SECONDS</div>
        </div>
      </div>
      <div class="tr-hud"><span class="tr-live"><span class="dot"></span>LIVE TRAILER</span><span id="tr-music-toggle" class="tr-music">♪ MUSIC: ON</span></div>
      <div class="tr-controls" id="tr-controls" hidden>
        <div class="tr-scrub"><div class="tr-prog" id="tr-prog"></div></div>
        <div class="tr-bottom">
          <div class="tr-time"><button id="tr-pause">⏸</button><span id="tr-elapsed">0:00</span><span class="tr-dim"> / 0:${(total/1000).toFixed(0).padStart(2,"0")}</span></div>
          <button class="tr-book" onclick="document.getElementById('contact').scrollIntoView({behavior:'smooth'})">BOOK THIS HOUSE</button>
        </div>
      </div>
    `;

    const stage = document.getElementById("tr-stage");
    const cover = document.getElementById("tr-cover");
    const playBtn = document.getElementById("tr-playbtn");
    const controls = document.getElementById("tr-controls");
    const progEl = document.getElementById("tr-prog");
    const timeEl = document.getElementById("tr-elapsed");
    const pauseBtn = document.getElementById("tr-pause");
    const musicToggle = document.getElementById("tr-music-toggle");

    let running = false, paused = false, audioCtl = null, rafId = null, startTs = 0, accumMs = 0, musicOn = true;

    function startAudio() {
      try {
        const AC = window.AudioContext || window.webkitAudioContext;
        const ctx = new AC();
        const master = ctx.createGain(); master.gain.value = 0; master.connect(ctx.destination);
        master.gain.linearRampToValueAtTime(0.12, ctx.currentTime + 0.8);

        // Cinematic ambient pad — two slow detuned sines + filtered noise "air"
        function pad(freq, detune, gainVal) {
          const o = ctx.createOscillator(); o.type = "sine"; o.frequency.value = freq; o.detune.value = detune;
          const g = ctx.createGain(); g.gain.value = gainVal;
          o.connect(g); g.connect(master); o.start();
          return o;
        }
        const chord = [
          pad(130.81, 0, 0.22),   // C3
          pad(196.00, -4, 0.18),  // G3 slightly flat
          pad(261.63, 3, 0.14),   // C4
          pad(329.63, -6, 0.10),  // E4
        ];
        // Slow LFO on filter for "breathing"
        const filt = ctx.createBiquadFilter(); filt.type = "lowpass"; filt.frequency.value = 800;
        // Noise for air
        const bufSize = 2 * ctx.sampleRate;
        const noise = ctx.createBufferSource();
        const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
        const d = buf.getChannelData(0);
        for (let i = 0; i < bufSize; i++) d[i] = (Math.random() * 2 - 1) * 0.04;
        noise.buffer = buf; noise.loop = true;
        const ng = ctx.createGain(); ng.gain.value = 0.06;
        noise.connect(filt); filt.connect(ng); ng.connect(master);
        noise.start();

        // Gentle LFO breathing on master gain
        const lfo = ctx.createOscillator(); lfo.frequency.value = 0.15;
        const lfoGain = ctx.createGain(); lfoGain.gain.value = 0.03;
        lfo.connect(lfoGain); lfoGain.connect(master.gain);
        lfo.start();

        audioCtl = { ctx, master, chord, noise, lfo };
      } catch (e) { console.warn("audio:", e); }
    }
    function stopAudio() {
      if (!audioCtl) return;
      try {
        const { ctx, master } = audioCtl;
        master.gain.cancelScheduledValues(ctx.currentTime);
        master.gain.linearRampToValueAtTime(0.0001, ctx.currentTime + 0.6);
        setTimeout(() => { try { ctx.close(); } catch (e) {} }, 700);
      } catch (e) {}
      audioCtl = null;
    }

    function renderScene(i) {
      stage.innerHTML = `<div class="tr-scene active" data-id="${scenes[i].id}">${scenes[i].html}</div>`;
    }

    function tick() {
      if (!running || paused) return;
      const elapsed = accumMs + (performance.now() - startTs);
      if (elapsed >= total) { finish(); return; }
      // Pick scene
      let t = 0, idx = 0;
      for (let i = 0; i < scenes.length; i++) {
        if (elapsed < t + scenes[i].dur) { idx = i; break; }
        t += scenes[i].dur;
      }
      if (stage.firstChild && stage.firstChild.getAttribute("data-id") !== scenes[idx].id) renderScene(idx);
      progEl.style.width = (elapsed / total * 100) + "%";
      const secs = Math.floor(elapsed / 1000);
      timeEl.textContent = `0:${String(secs).padStart(2,"0")}`;
      rafId = requestAnimationFrame(tick);
    }

    function play() {
      if (running) return;
      running = true; paused = false; accumMs = 0; startTs = performance.now();
      cover.classList.add("hidden");
      playBtn.classList.add("hidden");
      controls.hidden = false;
      renderScene(0);
      if (musicOn) startAudio();
      rafId = requestAnimationFrame(tick);
    }
    function pause() {
      if (!running || paused) { // resume if paused
        if (paused) { paused = false; startTs = performance.now(); pauseBtn.textContent = "⏸"; if (musicOn && !audioCtl) startAudio(); rafId = requestAnimationFrame(tick); }
        return;
      }
      paused = true; accumMs += performance.now() - startTs; pauseBtn.textContent = "▶"; stopAudio();
    }
    function finish() {
      running = false; stopAudio();
      renderScene(scenes.length - 1);
      progEl.style.width = "100%";
      timeEl.textContent = `0:${(total/1000).toFixed(0).padStart(2,"0")}`;
      pauseBtn.textContent = "↺";
      pauseBtn.onclick = () => { pauseBtn.onclick = null; play(); pauseBtn.onclick = () => pause(); pauseBtn.textContent = "⏸"; };
    }

    playBtn.addEventListener("click", play);
    pauseBtn.addEventListener("click", pause);
    musicToggle.addEventListener("click", () => {
      musicOn = !musicOn;
      musicToggle.textContent = musicOn ? "♪ MUSIC: ON" : "♪ MUSIC: OFF";
      if (!musicOn) stopAudio(); else if (running && !paused) startAudio();
    });
  }

  // Fetch the Wikipedia page summary thumbnail for a given page title. Returns null on miss.
  async function wikiThumb(title) {
    if (!title) return null;
    try {
      const r = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`, { headers: { "Accept": "application/json" } });
      if (!r.ok) return null;
      const j = await r.json();
      return (j && j.thumbnail && j.thumbnail.source) || (j && j.originalimage && j.originalimage.source) || null;
    } catch (e) { return null; }
  }

  function patch(r) {
    const { address, host, email, phone, nearest, price, baseline } = r;
    const split = splitStreet(address.street || "");

    // Title & nav brand
    document.title = `${address.street || "Your home"} — LA 2028 Rental`;
    text(".brand-mark", (host.split(" ")[0] || "HOST").toUpperCase() + " HOUSE");
    const sub = document.querySelector(".brand-sub");
    if (sub) sub.textContent = `${address.city || "Los Angeles"} · LA 2028`;

    // Hero lede — swap Northridge-specific line for the real nearest venue
    const lede = document.querySelector(".hero-copy .lede");
    if (lede) {
      const n0 = nearest[0];
      lede.textContent =
        `A private home in ${address.city || "Los Angeles"} — ${n0.mi.toFixed(1)} miles from ${n0.name}, ` +
        `where ${n0.sports} land July 14–30, 2028.`;
    }

    // Property card
    const pcAddrB = document.querySelector(".pc-address b");
    const pcAddrS = document.querySelector(".pc-address span");
    if (pcAddrB) pcAddrB.textContent = split.number;
    if (pcAddrS) pcAddrS.textContent = split.rest;
    html(".pc-sub",
      `${escapeHtml(address.city || "")}${address.state ? ", " + escapeHtml(address.state) : ""} ${escapeHtml(address.zip || "")}<br>${escapeHtml(address.county || "Los Angeles County")}`
    );

    // Photo plate — swap placeholder img for real Google satellite view of the house
    const plateImg = document.querySelector(".plate-img img");
    if (plateImg) {
      const sat = document.createElement("iframe");
      sat.src = `https://maps.google.com/maps?q=${address.lat},${address.lng}&t=k&z=19&output=embed`;
      sat.loading = "lazy";
      sat.style.cssText = "width:100%;height:600px;border:0;display:block;";
      sat.title = `Satellite view — ${address.street || "home"}`;
      plateImg.replaceWith(sat);
    }
    const capI = document.querySelector(".plate-img figcaption i");
    if (capI) capI.textContent = `${address.street || "The house"} — satellite, live from Google Maps.`;
    const fig = document.querySelector(".plate-img .fig");
    if (fig) fig.textContent = "LIVE · GOOGLE SAT";
    text(".plate-meta span:first-child", `Satellite · ${address.street || ""}`.trim());
    text(".plate-meta span:last-child",
      `${address.city || ""} · ${address.state || ""} ${address.zip || ""}`.toUpperCase().trim());

    // Spec rows — update GAMES ACCESS + HOSTS + LOT/year (keep known-unknowns honest)
    const rows = document.querySelectorAll(".spec dl > div");
    if (rows.length >= 7) {
      // Year built — unknown for most; keep placeholder but mark as "built year on request"
      rows[0].querySelector("dd").textContent = "Single-family home — year on request";
      rows[0].querySelector("span").textContent = "PRIVATE";
      // Floor area — unknown
      rows[1].querySelector("dd").textContent = "Interior square footage on request";
      rows[1].querySelector("span").textContent = "INQUIRE";
      // Lot
      rows[2].querySelector("dd").textContent = `${address.city || "Los Angeles"} lot — size on request`;
      rows[2].querySelector("span").textContent = "PRIVATE";
      // Sleeps — keep
      // Parking — keep
      // Games access — dynamic
      const n0 = nearest[0];
      rows[5].querySelector("dt").textContent = "NEAREST GAMES VENUE";
      rows[5].querySelector("dd").textContent =
        `${n0.name} — ${n0.mi.toFixed(1)} mi · ~${n0.min} min drive`;
      rows[5].querySelector("span").textContent = `${n0.mi.toFixed(1)} MI`;
      // Hosts
      rows[6].querySelector("dd").textContent = `${host} · ${address.city || "Los Angeles"}`;
      rows[6].querySelector("span").textContent = "LOCAL HOST";
    }

    // Pick neighborhood library entry (BH / Northridge / SM / default LA)
    const NB = (window.HTG_NEIGHBORHOODS && window.HTG_NEIGHBORHOODS.find(address)) || null;

    // Replace Games event grid with address-specific nearest venues (top 4)
    const eg = document.querySelector(".event-grid");
    if (eg && nearest.length) {
      const classes = ["headline", "inverted", "ghost", "ghost"];
      eg.innerHTML = nearest.slice(0, 4).map((v, i) => {
        const headline = (v.headline || v.sports.split("·")[0].trim()).toUpperCase();
        const name = v.name.replace(/·.*$/, "").trim();
        const sports = v.sports.charAt(0).toUpperCase() + v.sports.slice(1);
        return `
        <article class="event ${classes[i] || 'ghost'}">
          <div class="row"><span>${(i + 1).toString().padStart(2, "0")} · ${escapeHtml(headline)}</span><span>${v.mi.toFixed(1)} MI</span></div>
          <h3>${escapeHtml(name)}</h3>
          <p>${escapeHtml(sports)} · ~${escapeHtml(v.min)} min from your door.</p>
          <div class="chips">
            <span class="pill ${i === 1 ? 'filled dark' : 'filled'}">${escapeHtml(v.dates || 'JUL 14–30')}</span>
            <span class="pill ${i === 1 ? 'outline dark' : (i === 0 ? 'outline' : 'outline light')}">${v.mi < 5 ? 'QUICK DRIVE' : v.mi < 15 ? 'SHORT DRIVE' : 'DAY TRIP'}</span>
          </div>
        </article>
      `; }).join("");
    }

    // Games section — swap h2/eyebrow/lede using the matched neighborhood
    const olympH2 = document.querySelector(".olympics header h2");
    const olympEyebrow = document.querySelector(".olympics header .eyebrow");
    const olympLede = document.querySelector(".olympics header .lede");
    if (NB) {
      if (olympH2 && NB.olympicsH2) olympH2.innerHTML = NB.olympicsH2;
      if (olympEyebrow && NB.olympicsEyebrow) olympEyebrow.textContent = NB.olympicsEyebrow;
      if (olympLede) {
        olympLede.textContent = (typeof NB.olympicsLede === "function")
          ? NB.olympicsLede(address, nearest)
          : (NB.olympicsLede || olympLede.textContent);
      }
    }

    // Guide — REWRITE things to do, h2, eyebrow, lede from neighborhood
    const guideH2 = document.querySelector(".guide h2");
    const guideEyebrow = document.querySelector(".guide .eyebrow");
    const guideLede = document.querySelector(".guide .lede");
    const guideList = document.querySelector(".guide-list");
    if (NB) {
      if (guideH2) {
        const h2 = (typeof NB.guideH2 === "function") ? NB.guideH2(escapeHtml(address.city || "")) : NB.guideH2;
        if (h2) guideH2.innerHTML = h2;
      }
      if (guideEyebrow && NB.guideEyebrow) guideEyebrow.textContent = NB.guideEyebrow;
      if (guideLede && NB.guideLede) guideLede.textContent = NB.guideLede;
      if (guideList && Array.isArray(NB.thingsToDo)) {
        guideList.innerHTML = NB.thingsToDo.map((it, i) => {
          const seed = (it.t || "").toLowerCase().replace(/[^a-z0-9]+/g, "-");
          const fallback = `https://picsum.photos/seed/${encodeURIComponent(seed)}/280/200`;
          return `
          <li data-wiki="${it.wiki || ''}" data-idx="${i}">
            <span class="n">${it.n}</span>
            <img class="thumb" src="${fallback}" alt="${it.t}" loading="lazy">
            <span class="t">${it.t}</span>
            <span class="d">${it.d}</span>
            <span class="k">${it.k}</span>
          </li>`;
        }).join("");
        // Async: replace thumbnails with Wikipedia page images where available
        guideList.querySelectorAll("li[data-wiki]").forEach(async (li) => {
          const title = li.getAttribute("data-wiki");
          if (!title) return;
          const src = await wikiThumb(title);
          if (src) {
            const img = li.querySelector(".thumb");
            if (img) img.src = src;
          }
        });
      }
    }

    // History — rewrite paragraphs + timeline from neighborhood
    const historyCopy = document.querySelector(".history-copy");
    const timelineEl = document.querySelector(".timeline");
    if (NB && historyCopy) {
      const hH2 = (typeof NB.historyH2 === "function") ? NB.historyH2(escapeHtml(address.city || "")) : NB.historyH2;
      historyCopy.innerHTML = `
        <div class="eyebrow accent">${NB.historyEyebrow || "§ 07 · THE NEIGHBORHOOD"}</div>
        <h2 class="display sm">${hH2 || "Your neighborhood<br>has stories."}</h2>
        ${(NB.historyParagraphs || []).map(p => `<p>${p}</p>`).join("")}
      `;
    }
    if (NB && timelineEl && Array.isArray(NB.timeline)) {
      timelineEl.style.display = "";
      timelineEl.innerHTML = NB.timeline.map(t => `<li><span class="y">${t.y}</span><span>${t.t}</span></li>`).join("");
    }

    // Map section — swap the three mockup images for real Google Maps iframes
    // Use address string (not lat/lng) so Google snaps to the street-frontage pano,
    // not the closest pano which can face the alley.
    const fullAddr = [address.street, address.city, address.state, address.zip].filter(Boolean).join(", ");
    const mapMainImg = document.querySelector(".map-main img");
    if (mapMainImg) {
      const sv = document.createElement("iframe");
      sv.src = `https://maps.google.com/maps?q=${encodeURIComponent(fullAddr)}&layer=c&output=svembed`;
      sv.loading = "lazy";
      sv.style.cssText = "width:100%;height:100%;border:0;display:block;";
      sv.title = "Street view";
      mapMainImg.replaceWith(sv);
    }
    const sideTiles = document.querySelectorAll(".map-side .map-tile img");
    if (sideTiles.length >= 1) {
      const sat = document.createElement("iframe");
      sat.src = `https://maps.google.com/maps?q=${address.lat},${address.lng}&t=k&z=18&output=embed`;
      sat.loading = "lazy";
      sat.style.cssText = "width:100%;height:100%;border:0;display:block;";
      sat.title = "Satellite";
      sideTiles[0].replaceWith(sat);
    }
    if (sideTiles.length >= 2 && nearest[0]) {
      const dir = document.createElement("iframe");
      const v = nearest[0];
      // Google Maps directions-style embed without API key
      dir.src = `https://maps.google.com/maps?saddr=${address.lat},${address.lng}&daddr=${v.lat},${v.lng}&output=embed`;
      dir.loading = "lazy";
      dir.style.cssText = "width:100%;height:100%;border:0;display:block;";
      dir.title = "Drive to " + v.name;
      sideTiles[1].replaceWith(dir);
      // Update the drive chip with the real nearest venue
      const driveChip = document.querySelector(".map-side .map-tile:nth-child(2) .chip-abs");
      if (driveChip) driveChip.textContent = `DRIVE TO ${v.name.toUpperCase()}`;
      const driveMeta = document.querySelector(".map-side .map-tile:nth-child(2) .drive-meta");
      if (driveMeta) driveMeta.innerHTML = `<b>${escapeHtml(v.min)} min</b><span>${v.mi.toFixed(1)} MILES</span>`;
    }
    // Rewrite map chips for clarity
    const liveChip = document.querySelector(".map-main .chip-abs.tl");
    if (liveChip) liveChip.textContent = `LIVE STREET VIEW · ${address.street || ""}`.trim();
    const headChip = document.querySelector(".map-main .chip-abs.tl2");
    if (headChip) headChip.textContent = `LAT ${address.lat.toFixed(4)} · LNG ${address.lng.toFixed(4)} · GOOGLE`;

    // Relabel the interior contact sheet as reference imagery
    const sheetHead = document.querySelector(".plate ~ .olympics");
    const cs = document.querySelectorAll("#host ~ .olympics"); // no-op guard
    // Gentle relabel on the "Contact Sheet" caption if present
    const csEyebrow = document.querySelectorAll(".eyebrow");
    csEyebrow.forEach(e => {
      if (/§ 05 · CONTACT SHEET/i.test(e.textContent)) e.textContent = "§ 05 · REFERENCE IMAGERY · SWAP WITH YOUR PHOTOS";
    });

    // Contact section
    text(".meta li:nth-child(1) b", email);
    text(".meta li:nth-child(2) b", phone);
    text(".meta li:nth-child(3) b", "8am–9pm Pacific · seven days");
    const contactH = document.querySelector(".contact h2");
    if (contactH) contactH.innerHTML = `Talk to<br>${escapeHtml(host.split(" ")[0])}.`;
    const contactCopy = document.querySelector(".contact-copy p");
    if (contactCopy) {
      contactCopy.textContent = `Write me directly — I read every inquiry. Tell me which LA 2028 events you're attending and how long you'd like to stay, and I'll reply within a day.`;
    }

    // Footer
    text(".footer .brand", "");
    const fBrand = document.querySelector(".footer .brand");
    if (fBrand) fBrand.innerHTML = `<span class="brand-dot"></span>${escapeHtml((host.split(" ")[0] || "").toUpperCase())} HOUSE · LA 2028 RENTAL`;
    text(".footer .addr", `${address.street || ""} · ${address.city || ""} · ${address.county || "Los Angeles County"} · California · ${address.zip || ""}`);

    // Scripted 30-second trailer — intro, house, photos, closest Games event, outro
    buildTrailer({ address, host, nearest, price, fullAddr });

    // Countdown / today-at-Games banner — date-aware (opens / live / wrapped)
    (function countdownBanner() {
      const OPEN = new Date(2028, 6, 14);
      const CLOSE = new Date(2028, 6, 30);
      const today = new Date(); today.setHours(0,0,0,0);
      const MS_DAY = 86400000;
      const daysTo = Math.round((OPEN - today) / MS_DAY);
      const daysSince = Math.round((today - CLOSE) / MS_DAY);
      const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
      let kicker, title, meta;
      if (today < OPEN) {
        kicker = "⏳ COUNTDOWN TO THE GAMES";
        title = `${daysTo.toLocaleString()} days until opening`;
        meta = `Opening ceremony · Friday, July 14, 2028 · SoFi Stadium`;
      } else if (today > CLOSE) {
        kicker = "THE GAMES HAVE WRAPPED";
        title = `${daysSince} days since closing`;
        meta = `See you at the 2030 winter edition.`;
      } else {
        kicker = "✦ TODAY AT THE GAMES";
        const tk = today.toISOString().slice(0,10);
        const live = (nearest || []).filter(v =>
          expandDates(v.dates || "").some(d =>
            new Date(d.year, MONTHS.indexOf(d.mm), d.dd).toISOString().slice(0,10) === tk
          )
        );
        title = live.length ? live.slice(0,3).map(v => v.headline || v.sports).join(" · ") : "A quiet day between heats";
        meta = live.length ? live.slice(0,3).map(v => `${v.name.replace(/·.*$/, "").trim()} (${v.mi.toFixed(1)} mi)`).join(" · ") : "Rest day — back tomorrow";
      }
      const banner = document.createElement("section");
      banner.className = "countdown-banner";
      banner.innerHTML = `<div class="cb-inner"><div class="cb-kicker">${escapeHtml(kicker)}</div><div class="cb-title">${escapeHtml(title)}</div><div class="cb-meta">${escapeHtml(meta)}</div></div>`;
      const hero = document.querySelector(".hero");
      if (hero) hero.parentNode.insertBefore(banner, hero.nextSibling);
    })();

    // Closest-event banner — prominent card with headline sport + exact day chips
    const c0 = nearest[0];
    if (c0) {
      const days = expandDates(c0.dates || "Jul 14 – Jul 30, 2028");
      const weekdayShort = ["SUN","MON","TUE","WED","THU","FRI","SAT"];
      const dayChipsHtml = days.map(d => `
        <span class="ce-day">
          <em>${weekdayShort[d.dow]}</em>
          <b>${d.dd}</b>
          <i>${d.mm.toUpperCase()}</i>
        </span>
      `).join("");
      const closestBanner = document.createElement("section");
      closestBanner.className = "closest-event";
      closestBanner.innerHTML = `
        <div class="ce-inner">
          <div class="ce-top">
            <div class="ce-left">
              <div class="ce-eyebrow">✦ CLOSEST GAMES EVENT TO YOUR DOOR</div>
              <div class="ce-title">${escapeHtml(c0.headline || c0.sports)}</div>
              <div class="ce-meta">${escapeHtml(c0.name)} · ${c0.mi.toFixed(1)} mi / ~${escapeHtml(c0.min)} min drive</div>
            </div>
            <div class="ce-right">
              <div class="ce-dates-label">WINDOW · ${days.length} DAY${days.length > 1 ? 'S' : ''}</div>
              <div class="ce-dates">${escapeHtml(c0.dates || "Jul 14 – Jul 30, 2028")}</div>
            </div>
          </div>
          ${days.length ? `<div class="ce-days" aria-label="Exact event days">${dayChipsHtml}</div>` : ""}
        </div>
      `;
      const hero = document.querySelector(".hero");
      if (hero) hero.parentNode.insertBefore(closestBanner, hero.nextSibling);
    }

    // Inject a Suggested-nightly card into the hero below the property card
    const heroBody = document.querySelector(".hero-body");
    const existing = document.querySelector(".price-card");
    if (heroBody && !existing && price) {
      const card = document.createElement("aside");
      card.className = "property-card price-card";
      card.style.marginLeft = "16px";
      card.innerHTML = `
        <div class="pc-label">SUGGESTED NIGHTLY</div>
        <div class="pc-address"><b>$${formatMoney(price.mid)}</b><span>/ NIGHT</span></div>
        <div class="pc-sub">Range $${formatMoney(price.low)} – $${formatMoney(price.high)}<br>Jul 14 – Jul 30, 2028</div>
        <hr>
        <div class="pc-stats">
          <div><b>$${formatMoney(baseline && baseline.base)}</b><span>ZIP BASE</span></div>
          <div><b>×${price.proximity.toFixed(2)}</b><span>PROXIMITY</span></div>
          <div><b>×${price.eventPremium.toFixed(1)}</b><span>LA 2028</span></div>
        </div>
      `;
      heroBody.appendChild(card);
    }

    // Budget calculator — slider for nights × price + event-ticket estimate
    (function budgetCalculator() {
      if (!price) return;
      const heroEl = document.querySelector(".hero");
      if (!heroEl) return;
      const panel = document.createElement("section");
      panel.className = "budget-calc";
      panel.innerHTML = `
        <div class="bc-inner">
          <div class="bc-left">
            <div class="bc-eyebrow">✦ TRIP BUDGET — YOUR ALL-IN COST</div>
            <h3>Plan it before you book it.</h3>
            <p>Adjust nights and event tickets below. The nightly rate is your suggested rate; ticket averages come from public secondary-market data.</p>
          </div>
          <div class="bc-right">
            <label>
              <span>NIGHTS <b id="bc-nv">7</b></span>
              <input type="range" id="bc-nights" min="1" max="17" value="7">
            </label>
            <label>
              <span>EVENT TICKETS (per person) <b id="bc-tv">2</b></span>
              <input type="range" id="bc-tix" min="0" max="10" value="2">
            </label>
            <label>
              <span>GUESTS <b id="bc-gv">4</b></span>
              <input type="range" id="bc-guests" min="1" max="6" value="4">
            </label>
            <div class="bc-breakdown">
              <div><span>Lodging</span><b id="bc-lodge">—</b></div>
              <div><span>Tickets</span><b id="bc-tickets">—</b></div>
              <div class="bc-total"><span>ALL-IN</span><b id="bc-total">—</b></div>
            </div>
          </div>
        </div>`;
      // Place after the closest-event banner / before spec
      const after = document.querySelector(".closest-event") || heroEl;
      after.parentNode.insertBefore(panel, after.nextSibling);
      const $ = s => panel.querySelector(s);
      const AVG_TICKET = 185; // rough secondary-market avg across events
      function recalc() {
        const nights  = +$("#bc-nights").value;
        const tix     = +$("#bc-tix").value;
        const guests  = +$("#bc-guests").value;
        const lodging = nights * price.mid;
        const tickets = tix * guests * AVG_TICKET;
        const total   = lodging + tickets;
        $("#bc-nv").textContent = nights;
        $("#bc-tv").textContent = tix;
        $("#bc-gv").textContent = guests;
        $("#bc-lodge").textContent = "$" + lodging.toLocaleString();
        $("#bc-tickets").textContent = "$" + tickets.toLocaleString();
        $("#bc-total").textContent = "$" + total.toLocaleString();
      }
      panel.querySelectorAll("input[type=range]").forEach(r => r.addEventListener("input", recalc));
      recalc();
    })();

    // Persist guest inquiries submitted through the contact form
    wireContactForm(r);

    // Cascade uploaded photos into the hero plate + video trailer
    applyUploadedPhotos(r);
  }

  // If the host has uploaded photos, use them as the hero plate cover image
  // and as the two "interior" photo scenes in the video trailer.
  function applyUploadedPhotos(rental) {
    const photos = rental.photos || [];
    if (!photos.length) return;
    // Hero plate — swap the satellite iframe for the first uploaded photo
    const plateSec = document.querySelector(".plate-img");
    if (plateSec) {
      const existing = plateSec.querySelector("iframe, img");
      const img = document.createElement("img");
      img.src = photos[0];
      img.alt = "Cover photo";
      img.style.cssText = "width:100%;height:600px;object-fit:cover;display:block;";
      if (existing) existing.replaceWith(img);
      const fig = plateSec.querySelector(".fig");
      if (fig) fig.textContent = "COVER · YOUR PHOTO";
      const capI = plateSec.querySelector("figcaption i");
      if (capI) capI.textContent = "Uploaded by the host.";
    }
    // Trailer scenes — replace photo1 / photo2 picsum srcs with host photos once playback hits them
    // Easiest: override the scene HTML by intercepting in buildTrailer. Alternative: monkey-patch
    // after the player renders. We'll wait a tick and replace picsum imgs inside the stage.
    setTimeout(() => {
      document.querySelectorAll(".player .tr-photo").forEach((img, i) => {
        const src = photos[i + 1] || photos[i];
        if (src) img.src = src;
      });
    }, 100);
    // Contact-sheet style: if more than 2 photos, fill the existing plate area with a grid
    // (already have plate + trailer — leave rest for v2)
  }

  // Wire the contact form on rental.html to save inquiries to the rental record
  function wireContactForm(rental) {
    const form = document.getElementById("contact-form");
    if (!form) return;
    form.addEventListener("submit", e => {
      e.preventDefault();
      const f = e.currentTarget;
      const events = Array.from(f.querySelectorAll(".events input[type=checkbox]"))
        .filter(c => c.checked).map(c => c.parentElement.textContent.trim());
      const inquiry = {
        id: "inq-" + Math.random().toString(36).slice(2, 10),
        firstName: f.first ? f.first.value : "",
        lastName:  f.last  ? f.last.value  : "",
        email:     f.email ? f.email.value : "",
        checkIn:   f.in    ? f.in.value    : "",
        checkOut:  f.out   ? f.out.value   : "",
        guests:    f.guests? f.guests.value: "",
        events,
        message:   f.msg   ? f.msg.value   : "",
        createdAt: new Date().toISOString(),
      };
      try {
        const all = JSON.parse(localStorage.getItem("htg.rentals") || "{}");
        if (all[rental.slug]) {
          all[rental.slug].inquiries = all[rental.slug].inquiries || [];
          all[rental.slug].inquiries.push(inquiry);
          localStorage.setItem("htg.rentals", JSON.stringify(all));
        }
      } catch (err) {}
      // UX: replace the form with a thank-you state
      form.innerHTML = `
        <div style="padding: 24px; border: 1px solid rgba(255,255,255,.25);">
          <div style="font-family: var(--font-body); font-weight: 800; font-size: 11px; letter-spacing: 0.24em; color: var(--accent); margin-bottom: 8px;">✓ INQUIRY SAVED</div>
          <div style="font-family: var(--font-serif); font-size: 19px; line-height: 1.5;">Thanks, ${escapeHtml(inquiry.firstName || "friend")} — your message is in the host's inbox.</div>
          <div style="font-family: var(--font-serif); font-style: italic; font-size: 15px; opacity: .7; margin-top: 8px;">They'll reply to ${escapeHtml(inquiry.email || "you")} within 24 hours.</div>
        </div>`;
    });
  }

  function render() {
    const r = load();
    if (!r) {
      document.body.innerHTML = `
        <div style="font-family: Inter, sans-serif; max-width: 520px; margin: 12vh auto; padding: 32px; text-align: center;">
          <h1 style="font-family: Archivo, sans-serif; font-weight: 900; font-size: 40px; margin: 0 0 12px;">No rental found.</h1>
          <p style="color: #555; font-size: 17px; line-height: 1.5;">Go to the signup page, enter your address, and we'll render a personalized rental microsite.</p>
          <p style="margin-top: 24px;"><a href="index.html" style="color: #E2302A; font-weight: 700; letter-spacing: 0.1em; font-size: 13px;">→ CLAIM YOUR ADDRESS</a></p>
        </div>
      `;
      return;
    }
    patch(r);
  }

  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", render);
  else render();
})();

// Live rail — clock ticker
(function railClock() {
  const el = document.querySelector("[data-clock] b");
  if (!el) return;
  function tick() {
    const d = new Date();
    // Show local time formatted as HH : MM : SS (Pacific is user's local assumption)
    const pad = n => String(n).padStart(2, "0");
    el.textContent = `${pad(d.getHours())} : ${pad(d.getMinutes())} : ${pad(d.getSeconds())}`;
  }
  tick();
  setInterval(tick, 1000);
})();