16
Stunning Modern Ocean View 3 Bedroom Home on Calle Perezoso in Ojochal Costa Rica
  • Property Type
  • Home
  • 3
  • Bedrooms
  • 3
  • Bathrooms
  • 520 m²
  • House Size
  • 27000 m²
  • Land Size
Stunning Modern Ocean View 3 Bedroom Home on Calle Perezoso in Ojochal Costa Rica
  • $899,000

Description

Absolutely stunning modern home with 3 bedrooms and 3 bathrooms, nestled on a sprawling 2.7-hectare (6.7-acre) property offering breathtaking ocean views. Situated on the esteemed Calle Perezoso in Ojochal, this residence is just minutes away from pristine beaches and a plethora of fine dining establishments and recreational activities. The potential to subdivide this property into four additional lots of 5000 square meters each presents a remarkable opportunity.

The estimated market value for each of these new lots, boasting mountain views, is approximately US$120,000. Additionally, the fourth lot, featuring an enchanting ocean view, is priced at US$150,000. In total, the combined market value for all four lots reaches an impressive US$510,000.

Considering the current list price of US$995,000, this home, along with its expansive 7000 square meter plot, effectively translates to a mere US$485,000—an extraordinary value. The substantial plot easily accommodates the potential construction of two additional two-bedroom residences.

Furthermore, there’s ample room to expand the main house, allowing for the incorporation of two or three extra bedrooms along with an indoor garage. This residence spans two floors, featuring 3 bedrooms with 3 full bathrooms, complemented by an outdoor shower.

The master suite occupies the entire second floor, boasting a generously-sized wrap-around terrace that offers awe-inspiring vistas of the sea and mountains. For added convenience, the property includes a storage room housing all technical equipment, a carport (for one car), as well as a total of 5 parking spaces. Not to mention, a grand 60-foot by 8-foot swimming pool graces the grounds. One of the standout features of this modern abode is its self-sustaining solar system, which translates to remarkably low electrical bills.

This property offers a rare opportunity to own a contemporary masterpiece in an idyllic setting, combining luxurious living with eco-conscious design. Don’t miss your chance to call this extraordinary residence home

Details

  • Property ID 13848
  • Price $899,000
  • House Size 520 m²
  • Land Size 27000 m²
  • Bedrooms 3
  • Bathrooms 3
  • Garage 1
  • Year Built 2016
  • Property Type Home
  • Property Status Sold
  • Pool Yes - private
  • View Ocean - Full
  • Interior Features Tile Floors
  • Building Type Concrete Block with Stucco
  • Outdoor Space Features Balcony, Patio/Terraza
  • Lot/Garden/Land Features Landscaped
  • Location Features 2-Wheel Drive Access
  • Drinking Water AyA or ASADA - Legal Source
  • Septic/Sewer Septic System
  • Internet Fiber/Cable, Cellular/Mobile
  • Roof Metal
  • Air Conditioning Yes - Bedrooms Only
  • Electrical Service Utility Grid Connected- ICE, Solar Installed
  • Appliances Refrigerator, Ceiling Fans
  • Floors 2-Story
  • Agent MLS # c21-mvw-25935
  • Financing Available No - No Financing Available
  • City/Town: Ojochal
  • Province: Puntarenas
  • Area: Calle Perezoso

Contact Information

Inquire About This Property

Compare listings

Compare
- by the time it is submitted there is no data-hz-id // ancestor left to walk to, the same disconnect the WhatsApp fix above // works around. The one moment the card is still reachable is the click // that OPENED the modal, so that is captured here and handed to // captureLead() by the modal's own id when it submits. var pendingModalCtx = {}; document.addEventListener("click", function(e){ var t = e.target && e.target.closest ? e.target.closest("[data-model-id]") : null; if (!t) return; var modalId = t.dataset.modelId; if (!modalId) return; var lid = "", el = t, depth = 0, card = null; while (el && depth < 20) { if (el.dataset && el.dataset.hzId) { card = el; lid = el.dataset.hzId; break; } el = el.parentElement; depth++; } var url = cbpCardUrl(card); if (lid || url) pendingModalCtx[modalId] = { lid: lid, url: url }; }, true); var sentEmails = {}; function fieldVal(form, sel){ try { var el = form.querySelector(sel); return el && el.value ? String(el.value).trim() : ""; } catch (e) { return ""; } } function looksEmail(s){ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s); } function captureLead(form){ if (!form || typeof form.querySelectorAll !== "function") return; var inputs = form.querySelectorAll("input, textarea"), i, el, val; // Email: prefer a real email field; fall back to scanning values (Green // Popups/lepopup uses opaque id-based field names, so type/name won't match). var email = fieldVal(form, 'input[type=email]') || fieldVal(form, 'input[name*="email" i]'); if (!looksEmail(email)) { email = ""; for (i = 0; i < inputs.length; i++) { val = (inputs[i].value || "").trim(); if (looksEmail(val)) { email = val; break; } } } if (!email) return; // no email → not a lead form if (sentEmails[email]) return; // dedupe per page life sentEmails[email] = 1; // Name: prefer a name-ish field; else first short text value that isn't the // email, a phone, or a long message. var name = fieldVal(form, 'input[name*="name" i]:not([name*="user" i])'); if (!name) { for (i = 0; i < inputs.length; i++) { el = inputs[i]; val = (el.value || "").trim(); var t = (el.type || "").toLowerCase(); if (!val || looksEmail(val)) continue; if (t === "email" || t === "tel" || t === "number" || t === "hidden" || t === "checkbox" || t === "radio" || t === "submit" || t === "button") continue; var digits = val.replace(/\D/g, ""); if (digits.length >= 7 && digits.length >= val.length - 4) continue; // phone-like if (val.length > 60 || !/[A-Za-z]/.test(val)) continue; // message / non-name name = val; break; } } // v3.54.0: L (CBP_VE_LISTING) is only ever set on the listing's OWN page. // A card's Email button submitted from a search-results list has no L to // fall back on - use the context stashed when that card's button was // clicked (pendingModalCtx, above) instead of leaving this lead // unattributed. var modalEl = form.closest ? form.closest(".modal") : null; var modalCtx = modalEl && modalEl.id ? pendingModalCtx[modalEl.id] : null; var leadExtra = { event_type: "form_submit", email: email, name: name, listing_id: (modalCtx && modalCtx.lid) || (L ? L.listing_id : "") }; if (modalCtx && modalCtx.url) leadExtra.page_url = modalCtx.url; var data = body(leadExtra); try { if (navigator.sendBeacon) { navigator.sendBeacon(ENDPOINT, new Blob([data], { type: "application/x-www-form-urlencoded" })); } else { fetch(ENDPOINT, { method: "POST", credentials: "include", keepalive: true, headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: data }).catch(function(){}); } } catch (e) {} } document.addEventListener("submit", function(e){ captureLead(e.target); }, true); // Green Popups (lepopup) forms have NO
tag — the submit control is an // , so no submit event fires. Capture on button // click instead: find the popup container (nearest ancestor holding a // lepopup field) and scan it. captureLead is container-agnostic. function lepopupRoot(el){ var n = el; while (n && n !== document.body) { if (n.querySelector && n.querySelector('input[name^="lepopup-"]')) return n; n = n.parentElement; } return null; } document.addEventListener("click", function(e){ var t = e.target; var btn = t && t.closest ? t.closest(".lepopup-button") : null; if (!btn) return; var root = lepopupRoot(btn); if (root) captureLead(root); }, true); // ── Favourites (v2.44.0). Read from the live custom.min.js (houzez.Favorites), // 2026-09-06: the heart is . // A member's saved heart is re-classed to .remove_fav; a guest's (cookie // houzez_favorite_listings) keeps .add-favorite-js and gains .remove-favorite. // Both Houzez handlers are delegated on document in the BUBBLE phase, so // this capture-phase observer sees every click first and must not stop it: // Houzez still saves / un-saves / shows its login modal exactly as before. // The click itself is the signal -- even when Houzez saves nothing for a // guest, the person just told us which listing they want. document.addEventListener("click", function(e){ var btn = e.target && e.target.closest ? e.target.closest(".add-favorite-js, .remove_fav") : null; if (!btn) return; var id = parseInt(btn.getAttribute("data-listid") || "", 10); if (!id) return; var removing = btn.classList.contains("remove_fav") || btn.classList.contains("remove-favorite"); var onPage = L && String(L.listing_id) === String(id); var data = body({ event_type: removing ? "favorite_remove" : "favorite_add", listing_id: id, listing_price: onPage ? L.price : "", listing_type: onPage ? L.primary_type : "" }); // The answer to this one beacon is read, unlike every other: the portal // counts the saves and says when to offer to keep them (v2.98.0). The // rule lives there, not here, so it cannot drift from the rest of the // site. keepalive is dropped because we are waiting for the body. try { fetch(ENDPOINT, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: data }) .then(function (r) { return r.json(); }) .then(function (j) { if (!j || !j.success || !j.data || !j.data.save_ask) return; if (window.CBPVE && window.CBPVE.saveCard) window.CBPVE.saveCard(j.data.save_ask, j.data.saves); }) .catch(function(){}); } catch (e2) {} }, true); // WhatsApp click (v3.53.0). Every "message the agent" button on the site — // Houzez's own per-card and per-listing contact icons, plus our co-broke // banner's Ask/Share-location buttons — is a plain or // , target=_blank, so the tab this // script runs in never navigates away: no sendBeacon needed, a normal // keepalive fetch has time to land. Chris, 2026-09-22: wants every one of // these logged so a listing+time-range search (or a site-wide search) can // find who reached for WhatsApp. listing_id comes from the nearest // Houzez v9 card wrapper (data-hz-id, grid/related cards) or, failing // that, the current single-listing page's own id — never both guessed at // once, so a related-listing card is never mis-attributed to the page // it happens to sit on. document.addEventListener("click", function(e){ var a = e.target && e.target.closest ? e.target.closest('a[href*="wa.me"],a[href*="api.whatsapp.com"]') : null; if (!a) return; var lid = "", el = a, depth = 0, card = null; while (el && depth < 20) { if (el.dataset && el.dataset.hzId) { card = el; lid = el.dataset.hzId; break; } el = el.parentElement; depth++; } if (!lid && L) lid = L.listing_id; // v3.54.0: on a search-results list the click's own page (location.href, // the "common" default) is the LIST, not the card's listing - read the // card's real URL instead so the VE timeline links to the property that // was actually asked about. var linkUrl = cbpWaUrl(a) || cbpCardUrl(card); var whatsappExtra = { event_type: "whatsapp_click", listing_id: lid }; if (linkUrl) whatsappExtra.page_url = linkUrl; var data = body(whatsappExtra); try { fetch(ENDPOINT, { method: "POST", credentials: "include", keepalive: true, headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: data }).catch(function(){}); } catch (e3) {} // Ad-platform conversion feed (v3.53.1). Chris, 2026-09-22: wants this // counted as a GA4/Google Ads/Bing conversion too. Checked in GTM // (container GTM-KDT3465R) with Tag Assistant: the site's existing // "WhatsApp - Link Click" / "WhatsApp 2 - Link Click" triggers (Just // Links, matching this exact URL) have three tags built and ready // (BingAds/GA4/GAds Conv. - WhatsApp - Click) but NONE of them have // fired on a real click — confirmed live, zero dataLayer activity. // Pushing our own named event here is a second, independent path into // the same three tags once GTM is repointed at a Custom Event trigger // for it, one that does not depend on GTM's own link-click listener // ever starting to catch this click. try { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: "whatsapp_click", whatsapp_listing_id: lid || undefined }); } catch (e4) {} }, true); // Guest Save-Search (v2.40.0) — see the PHP comment above where // $guest_save_endpoint is set. Empty for a logged-in visitor: the whole // block below is inert (the early-return on the capture listener never // finds a target worth intercepting, since GSS_ENDPOINT is falsy) and // Houzez's own button keeps working exactly as it always has. var GSS_ENDPOINT = "https://portal.costaballenaproperty.com/wp-admin/admin-ajax.php?action=cbp_ve_guest_save_search"; if (GSS_ENDPOINT) { var gssStyleTag = document.createElement("style"); gssStyleTag.textContent = "#cbpss-ov{position:fixed;inset:0;background:rgba(20,20,30,.55);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif}" + "#cbpss-ov .cbpss-card{background:#fff;border-radius:14px;max-width:380px;width:100%;padding:22px 22px 18px;box-shadow:0 20px 50px rgba(0,0,0,.25);box-sizing:border-box}" + "#cbpss-ov .cbpss-h{font-size:18px;font-weight:700;color:#16213d;margin-bottom:6px}" + "#cbpss-ov .cbpss-sum{font-size:13px;color:#5a6478;line-height:1.5;margin-bottom:14px}" + "#cbpss-ov .cbpss-name,#cbpss-ov .cbpss-email{width:100%;box-sizing:border-box;border:1px solid #cfd6e3;border-radius:10px;padding:12px 14px;font-size:16px;margin-bottom:6px}" + "#cbpss-ov .cbpss-name:focus,#cbpss-ov .cbpss-email:focus{outline:none;border-color:#0b5ed7;box-shadow:0 0 0 3px rgba(11,94,215,.15)}" + "#cbpss-ov .cbpss-err{color:#c0392b;font-size:13px;margin:4px 0}" + "#cbpss-ov .cbpss-row{display:flex;gap:10px;margin-top:14px}" + "#cbpss-ov button{border-radius:10px;padding:12px 16px;font-size:15px;font-weight:600;cursor:pointer;min-height:44px;border:1px solid #cfd6e3;background:#fff;color:#3f4a66;flex:0 0 auto}" + "#cbpss-ov .cbpss-save{background:#0b5ed7;border-color:#0b5ed7;color:#fff;flex:1 1 auto}" + "#cbpss-ov .cbpss-save:disabled{opacity:.6;cursor:default}"; document.head.appendChild(gssStyleTag); function gssOpen() { if (document.getElementById("cbpss-ov")) return; var S = window.CBP_VE_SEARCH; var summary = (S && S.summary) || "your current search"; var ov = document.createElement("div"); ov.id = "cbpss-ov"; var esc = function (s) { return String(s).replace(/&/g, "&").replace(/Save this search' + '
' + esc(summary) + '
' + '' + '' + '' + '
' + '' + '' + '
' + ''; document.body.appendChild(ov); var nameEl = ov.querySelector(".cbpss-name"); var emailEl = ov.querySelector(".cbpss-email"); var errEl = ov.querySelector(".cbpss-err"); var saveBtn = ov.querySelector(".cbpss-save"); setTimeout(function () { nameEl.focus(); }, 0); ov.addEventListener("click", function (e) { if (e.target === ov) ov.remove(); }); ov.querySelector(".cbpss-cancel").onclick = function () { ov.remove(); }; nameEl.addEventListener("keydown", function (e) { if (e.key === "Enter") emailEl.focus(); }); emailEl.addEventListener("keydown", function (e) { if (e.key === "Enter") saveBtn.click(); }); saveBtn.onclick = function () { var n = nameEl.value.trim(); var v = emailEl.value.trim(); if (!n) { errEl.textContent = "Enter your name."; errEl.style.display = "block"; return; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) { errEl.textContent = "Enter a valid email address."; errEl.style.display = "block"; return; } errEl.style.display = "none"; saveBtn.textContent = "Saving…"; saveBtn.disabled = true; // The QUERY STRING, exactly as Houzez's own save-search sends it (its // hidden field is search_URI). This is the search; search_params is a // parsed rendering of it kept for display and editing. Storing only // the parsed form is what made the portal rebuild a URL on every send, // and the rebuilt one did not return the same listings. var qs = ""; try { qs = (window.location.search || "").replace(/^\?/, ""); } catch (e) { qs = ""; } var data = "name=" + encodeURIComponent(n) + "&email=" + encodeURIComponent(v) + "&search_uri=" + encodeURIComponent(qs) + "&search_params=" + encodeURIComponent(JSON.stringify((S && S.params) || [])) + "&summary=" + encodeURIComponent(summary); fetch(GSS_ENDPOINT, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: data }).then(function (r) { return r.json(); }).then(function (j) { if (j && j.success) { ov.querySelector(".cbpss-card").innerHTML = '
✓ Saved
' + '
We\'ll email you when something new matches.
' + '
'; ov.querySelector(".cbpss-cancel").onclick = function () { ov.remove(); }; } else { errEl.textContent = (j && j.data && j.data.message) || "Something went wrong — try again."; errEl.style.display = "block"; saveBtn.textContent = "Save & get email alerts"; saveBtn.disabled = false; } }).catch(function () { errEl.textContent = "Network error — try again."; errEl.style.display = "block"; saveBtn.textContent = "Save & get email alerts"; saveBtn.disabled = false; }); }; } // Capture phase on document, attached as early as possible: this runs // BEFORE Houzez's own bubble-phase jQuery .on('click', ...) handler can // fire, regardless of attachment order. stopPropagation() here keeps the // event from ever reaching that handler (or the login modal it would // open) at all -- this is the only reason the fix works without editing // custom.js, which a Houzez update would overwrite wholesale anyway. document.addEventListener("click", function (e) { var btn = e.target && e.target.closest ? e.target.closest(".save_search_click") : null; if (!btn) return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); gssOpen(); }, true); } // "Email me the flyer PDF" (v3.50.3) — see the PHP comment above where // $flyer_req_endpoint is set. Houzez's print button is
  • , d-none d-md-block (desktop only) in the live theme — // verified by reading the actual DOM, same rule as never editing custom.js. // The d-none/d-md-class removal (mobile-friendly, Chris's requirement) only // ever runs once FLYER_REQ_ENABLED is confirmed true (the fetch().then() // above) — Chris: unchecked, "use the default Houzez version," meaning // untouched, not just non-functional. Houzez's own button is left exactly // alone, visually and behaviourally, until the setting says otherwise. var PDFREQ_ENDPOINT = "https://portal.costaballenaproperty.com/wp-admin/admin-ajax.php?action=cbp_ve_flyer_request"; function cbpprUpgradeButtons() { if (!FLYER_REQ_ENABLED) return; var btns = document.querySelectorAll(".houzez-print"); for (var i = 0; i < btns.length; i++) { var li = btns[i]; if (li.getAttribute("data-cbppr")) continue; li.setAttribute("data-cbppr", "1"); li.classList.remove("d-none", "d-md-block"); var span = li.querySelector("[role=button]") || li; span.setAttribute("title", "Email me this listing's PDF"); } } window.cbpprUpgradeButtons = cbpprUpgradeButtons; document.addEventListener("DOMContentLoaded", cbpprUpgradeButtons); var cbpprStyleTag = document.createElement("style"); cbpprStyleTag.textContent = "#cbppr-ov{position:fixed;inset:0;background:rgba(20,20,30,.55);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif}" + "#cbppr-ov .cbppr-card{background:#fff;border-radius:14px;max-width:380px;width:100%;padding:22px 22px 18px;box-shadow:0 20px 50px rgba(0,0,0,.25);box-sizing:border-box}" + "#cbppr-ov .cbppr-h{font-size:18px;font-weight:700;color:#16213d;margin-bottom:6px}" + "#cbppr-ov .cbppr-sum{font-size:13px;color:#5a6478;line-height:1.5;margin-bottom:14px}" + "#cbppr-ov .cbppr-name,#cbppr-ov .cbppr-email,#cbppr-ov .cbppr-fname,#cbppr-ov .cbppr-femail{width:100%;box-sizing:border-box;border:1px solid #cfd6e3;border-radius:10px;padding:12px 14px;font-size:16px;margin-bottom:6px}" + "#cbppr-ov .cbppr-name:focus,#cbppr-ov .cbppr-email:focus,#cbppr-ov .cbppr-fname:focus,#cbppr-ov .cbppr-femail:focus{outline:none;border-color:#0b5ed7;box-shadow:0 0 0 3px rgba(11,94,215,.15)}" + // Language — a small flip switch, not a form control the same size as // name/email. Chris, 2026-09-17: "much smaller... not such a big deal." // Unchecked = English (the default), checked = Spanish. "#cbppr-ov .cbppr-langrow{display:flex;align-items:center;gap:6px;font-size:12px;color:#8a93a6;margin:0 0 10px}" + "#cbppr-ov .cbppr-switch{position:relative;display:inline-block;width:32px;height:18px;flex:0 0 auto}" + "#cbppr-ov .cbppr-switch input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer;z-index:1}" + "#cbppr-ov .cbppr-switch-track{position:absolute;inset:0;background:#cfd6e3;border-radius:999px;transition:background .15s}" + "#cbppr-ov .cbppr-switch-thumb{position:absolute;top:2px;left:2px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .15s;box-shadow:0 1px 2px rgba(0,0,0,.3)}" + "#cbppr-ov .cbppr-switch input:checked ~ .cbppr-switch-track{background:#0b5ed7}" + "#cbppr-ov .cbppr-switch input:checked ~ .cbppr-switch-thumb{transform:translateX(14px)}" + "#cbppr-ov .cbppr-friend-toggle{display:flex;align-items:center;gap:8px;font-size:14px;color:#3f4a66;margin:4px 0 4px;cursor:pointer;min-height:44px}" + "#cbppr-ov .cbppr-friend-toggle input{width:18px;height:18px}" + "#cbppr-ov .cbppr-friend-block{border-top:1px solid #eef1f8;margin-top:4px;padding-top:10px}" + "#cbppr-ov .cbppr-err{color:#c0392b;font-size:13px;margin:4px 0}" + "#cbppr-ov .cbppr-row{display:flex;gap:10px;margin-top:14px}" + "#cbppr-ov button{border-radius:10px;padding:12px 16px;font-size:15px;font-weight:600;cursor:pointer;min-height:44px;border:1px solid #cfd6e3;background:#fff;color:#3f4a66;flex:0 0 auto}" + "#cbppr-ov .cbppr-save{background:#0b5ed7;border-color:#0b5ed7;color:#fff;flex:1 1 auto}" + "#cbppr-ov .cbppr-save:disabled{opacity:.6;cursor:default}"; document.head.appendChild(cbpprStyleTag); // A little flip switch, not a dropdown — unchecked (default) = English, // checked = Spanish. $LABEL is what the switch turns ON, since English is // already the assumed default and doesn't need its own labelled state. function cbpprLangSwitch(cls, label) { return ''; } function cbpprOpen(pid) { if (document.getElementById("cbppr-ov")) return; var ov = document.createElement("div"); ov.id = "cbppr-ov"; ov.innerHTML = '
    ' + '
    Get the PDF
    ' + '
    Tell us where to send it and we\'ll email you the listing PDF right away.
    ' + '' + '' + cbpprLangSwitch("cbppr-lang", "Send it in Spanish") + '' + '' + '' + '
    ' + '' + '' + '
    ' + '
    '; document.body.appendChild(ov); var nameEl = ov.querySelector(".cbppr-name"); var emailEl = ov.querySelector(".cbppr-email"); var langEl = ov.querySelector(".cbppr-lang"); var friendCheck = ov.querySelector(".cbppr-friend-check"); var friendBlock = ov.querySelector(".cbppr-friend-block"); var fnameEl = ov.querySelector(".cbppr-fname"); var femailEl = ov.querySelector(".cbppr-femail"); var flangEl = ov.querySelector(".cbppr-flang"); var errEl = ov.querySelector(".cbppr-err"); var saveBtn = ov.querySelector(".cbppr-save"); setTimeout(function () { nameEl.focus(); }, 0); ov.addEventListener("click", function (e) { if (e.target === ov) ov.remove(); }); ov.querySelector(".cbppr-cancel").onclick = function () { ov.remove(); }; nameEl.addEventListener("keydown", function (e) { if (e.key === "Enter") emailEl.focus(); }); emailEl.addEventListener("keydown", function (e) { if (e.key === "Enter") saveBtn.click(); }); friendCheck.addEventListener("change", function () { friendBlock.style.display = friendCheck.checked ? "block" : "none"; if (friendCheck.checked) fnameEl.focus(); }); var emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; saveBtn.onclick = function () { var n = nameEl.value.trim(); var v = emailEl.value.trim(); if (!n) { errEl.textContent = "Enter your name."; errEl.style.display = "block"; return; } if (!emailRe.test(v)) { errEl.textContent = "Enter a valid email address."; errEl.style.display = "block"; return; } var sendFriend = friendCheck.checked; var fn = sendFriend ? fnameEl.value.trim() : ""; var fv = sendFriend ? femailEl.value.trim() : ""; if (sendFriend && (!fn || !emailRe.test(fv))) { errEl.textContent = "Enter your friend's name and a valid email address, or uncheck \"send to a friend\"."; errEl.style.display = "block"; return; } errEl.style.display = "none"; saveBtn.textContent = "Sending…"; saveBtn.disabled = true; var data = "pid=" + encodeURIComponent(pid) + "&name=" + encodeURIComponent(n) + "&email=" + encodeURIComponent(v) + "&lang=" + ( langEl.checked ? "es" : "en" ) + (sendFriend ? "&friend_name=" + encodeURIComponent(fn) + "&friend_email=" + encodeURIComponent(fv) + "&friend_lang=" + ( flangEl.checked ? "es" : "en" ) : ""); fetch(PDFREQ_ENDPOINT, { method: "POST", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: data }).then(function (r) { return r.json(); }).then(function (j) { if (j && j.success) { // Same beacon as every other signal (favourites, saved searches): // logged here, not inside the mail-sending endpoint, which only // owns identity + delivery, never the audit trail (cbp_events). fetch(ENDPOINT, { method: "POST", credentials: "include", keepalive: true, headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: body({ event_type: "pdf_request", listing_id: pid }) }).catch(function(){}); var sumMsg = (j.data && j.data.friend_sent) ? "Check your email in a minute or two — and your friend's." : "Check your email in a minute or two."; ov.querySelector(".cbppr-card").innerHTML = '
    ✓ On its way
    ' + '
    ' + sumMsg + '
    ' + '
    '; ov.querySelector(".cbppr-cancel").onclick = function () { ov.remove(); }; } else { errEl.textContent = (j && j.data && j.data.message) || "Something went wrong — try again."; errEl.style.display = "block"; saveBtn.textContent = "Email me the PDF"; saveBtn.disabled = false; } }).catch(function () { errEl.textContent = "Network error — try again."; errEl.style.display = "block"; saveBtn.textContent = "Email me the PDF"; saveBtn.disabled = false; }); }; } // Capture phase, same race-winning technique as guest-save-search above — // Houzez's own bubble-phase handler (window.print()) must never fire. // Gated on FLYER_REQ_ENABLED: unchecked in Settings, this listener still // exists but does nothing — the click falls through untouched and Houzez's // own print handler runs exactly as if this feature were never installed. document.addEventListener("click", function (e) { if (!FLYER_REQ_ENABLED) return; var btn = e.target && e.target.closest ? e.target.closest(".houzez-print") : null; if (!btn) return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); var pid = parseInt(btn.getAttribute("data-propid") || (L ? L.listing_id : 0), 10); if (!pid) return; cbpprOpen(pid); }, true); })();