Tropical Paradise 3-Bedroom Home Oasis In Ojochal Costa Rica
  • $499,000

Description

Welcome to your dream tropical retreat on the stunning Costa Rican coast! This beautifully renovated three bedroom, two bathroom home is situated on the corner of Calle Perezoso and Avenida Principal in the charming town of Ojochal. With its value-added upgrades and prime location, this home offers an incredible opportunity for those seeking a slice of paradise.

ach of the three bedrooms is thoughtfully designed, providing a cozy and comfortable space to unwind after a day of exploring all that Costa Rica has to offer. The three bathrooms boast luxurious finishes and spa-like features, providing a touch of indulgence and relaxation.

One of the highlights of this home is its value-added upgrades. From upgraded flooring and fixtures to energy-efficient appliances and solar panels, no detail has been overlooked in making this home both stylish and sustainable. The property also features a secure gated entrance and ample parking space, providing peace of mind and convenience.

While this home doesn’t have an ocean view, its prime location in Ojochal offers easy access to the area’s pristine beaches, lush rainforests, and abundant wildlife. Whether it’s exploring the nearby Marino Ballena National Park, indulging in world-class dining at local restaurants, or simply relaxing in your own private oasis, this home offers a lifestyle that is truly unparalleled.

With its perfect blend of modern comforts, thoughtful upgrades, and unbeatable location, this newly-renovated home on the corner of Calle Perezoso and Avenida Principal is a rare gem in the heart of Ojochal. Don’t miss out on the opportunity to make this your own tropical haven. Schedule a showing today and experience the pura vida lifestyle that Costa Rica has to offer!

The Community

If you’re looking for that great place to live, this is it! Calle Perezoso (“Lazy Street”) long has been known as the premier street in Ojochal and this property does not disappoint.

Two entry points onto this street (“Calle”) allow for a great circular drive. It is a very sought-after location only minutes from the main highway and many amazing restaurants, shops, and pharmacies in the village of Ojochal.

The presence of abundant wildlife will delight your senses as you watch scarlet macaws fly overhead. During the green season, multiple waterfalls can be seen in the mountains above the town. You are approximately 3.65 kilometers from the beach at Playa Ventanas and only 1 kilometer from Reserva Playa Tortuga (Costa Rican Sea Turtle Reserve).

The area is populated by families of all ages from those with young children to retirees. There are two bilingual elementary schools just a minute’s walk from the doorstep and the public school is a 10-minute walk down the town road.

Key value-adds of this property:

The current owners are begrudgingly selling it after completing extensive renovations and upgrades that have genuinely completed this fabulous property. The home was originally built in 2019 but it wasn’t lived in until last year, which is when the upgrade work began. The extensive work done recently includes professional landscaping, retrofitting the home with US-standard utility access, and other valuable additions (see below for a full list of key details about this home).

  • Total professional landscaping with ornamentals and fruit trees (coconuts, plantains, citrus, pineapples, and more)
  • Groundwater management across the entire property
  • Surge protection throughout the whole house
  • Electric-powered steel security gate
  • An oversized single-car carport
  • Maximized storage potential
  • Solar air conditioning and solar pool pump
  • Reserve water system
  • International-standard septic system
  • Highly energy-efficient tankless water heater
  • Brand new appliances with a fully stocked kitchen
  • Imported stainless-steel fixtures (USA)
  • High-efficiency DC-motor electromagnetic fans with LED lighting
  • Under-cabinet LED lighting
  • Zebra blinds in the bedrooms
  • Brand new granite countertops and backsplash
  • A large “Krauss” single-basin stainless-steel sink in the kitchen
  • New furnishings include several custom-made pieces (media center, dining bench, small 4-person dining table, large armoires, and additional smaller pieces)
  • Bathroom mirror cabinets finished with an easy-to-clean wood “topper” that matches the cabinet finishes
  • Adjustable opening window panes above the entrance door to increase cross-draft air-flow
  • Newly painted interior and exterior

Property Documents

Plano

Details

  • Property ID 15489
  • Price $499,000
  • House Size 222 m²
  • Land Size 900 m²
  • Bedrooms 3
  • Bathrooms 2
  • Year Built 2019
  • Property Type Home
  • Property Status Sold
  • Pool Yes - private
  • View Jungle/Forest
  • Interior Features Tile Floors
  • Building Type Concrete Block with Stucco
  • Outdoor Space Features Patio/Terraza
  • Lot/Garden/Land Features Landscaped, Fruit Trees
  • Location Features 2-Wheel Drive Access
  • Drinking Water AyA or ASADA - Legal Source
  • Septic/Sewer Septic System
  • Internet Fiber/Cable, Cellular/Mobile
  • Roof Tile
  • Air Conditioning Yes - Whole House
  • Electrical Service Utility Grid Connected- ICE
  • Appliances Refrigerator, Ceiling Fans
  • Elevation (approx) feet 149
  • Floors 1-Story
  • Agent MLS # otp-ro-5854
  • 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); })();