// ═══════════════════════════════════════════════════════════════════════ // GOOGLE PLACES AUTOCOMPLETE // ═══════════════════════════════════════════════════════════════════════ let autocomplete; let addressSelected = false; function initAutocomplete() { const addressInput = document.getElementById('propertyAddress'); // Initialize autocomplete with restrictions autocomplete = new google.maps.places.Autocomplete(addressInput, { componentRestrictions: { country: 'us' }, types: ['address'], fields: ['address_components', 'formatted_address', 'geometry'] }); // Bias results toward PA/DE area (Chester County center) const defaultBounds = new google.maps.LatLngBounds( new google.maps.LatLng(39.5, -76.0), // SW corner new google.maps.LatLng(40.3, -75.0) // NE corner ); autocomplete.setBounds(defaultBounds); // Handle place selection autocomplete.addListener('place_changed', handlePlaceSelect); // Track if user manually edits after selection addressInput.addEventListener('input', function() { if (addressSelected) { // User is editing - clear the auto-filled fields document.getElementById('propertyCity').value = ''; document.getElementById('propertyState').value = ''; document.getElementById('zipCode').value = ''; addressSelected = false; document.getElementById('addressHint').textContent = 'Select from dropdown to auto-fill city, state, ZIP'; document.getElementById('addressHint').style.color = ''; hideDistrictSelect(); } }); } function handlePlaceSelect() { const place = autocomplete.getPlace(); if (!place.address_components) { return; } // Extract address components let streetNumber = ''; let streetName = ''; let city = ''; let state = ''; let zip = ''; for (const component of place.address_components) { const type = component.types[0]; switch (type) { case 'street_number': streetNumber = component.long_name; break; case 'route': streetName = component.long_name; break; case 'locality': city = component.long_name; break; case 'administrative_area_level_1': state = component.short_name; break; case 'postal_code': zip = component.long_name; break; } } // If no locality, try sublocality or neighborhood if (!city) { for (const component of place.address_components) { if (component.types.includes('sublocality_level_1') || component.types.includes('neighborhood') || component.types.includes('administrative_area_level_3')) { city = component.long_name; break; } } } // Update form fields document.getElementById('propertyAddress').value = streetNumber + ' ' + streetName; document.getElementById('propertyCity').value = city; document.getElementById('propertyState').value = state; document.getElementById('zipCode').value = zip; addressSelected = true; // Check if ZIP is in coverage area if (zip && !ZIP_MAP[zip] && !MULTI_DISTRICT_ZIPS[zip]) { document.getElementById('addressHint').textContent = '⚠️ This ZIP may be outside our coverage area'; document.getElementById('addressHint').style.color = '#b45309'; hideDistrictSelect(); hideNeighborhoodSelect(); } else if (zip && MULTI_DISTRICT_ZIPS[zip]) { // Multi-district ZIP - show selection dropdown document.getElementById('addressHint').textContent = '✓ Address confirmed - please select your school district below'; document.getElementById('addressHint').style.color = '#2d6a4f'; showDistrictSelect(zip); hideNeighborhoodSelect(); // Will show after district selected } else if (zip && ZIP_MAP[zip]) { document.getElementById('addressHint').textContent = '✓ Address confirmed'; document.getElementById('addressHint').style.color = '#2d6a4f'; hideDistrictSelect(); // Show neighborhood for single-district ZIP const districtKey = ZIP_MAP[zip]; const district = DISTRICTS[districtKey]; if (district) { showNeighborhoodSelect(district.name); } } else { hideDistrictSelect(); hideNeighborhoodSelect(); } } function showDistrictSelect(zip) { const group = document.getElementById('districtSelectGroup'); const select = document.getElementById('districtSelect'); const districts = MULTI_DISTRICT_ZIPS[zip]; // Clear and populate options select.innerHTML = ''; districts.forEach(d => { const option = document.createElement('option'); option.value = d.key; option.textContent = d.label; select.appendChild(option); }); group.style.display = 'block'; select.required = true; // When district is selected, show neighborhoods select.onchange = function() { const selectedKey = this.value; if (selectedKey && DISTRICTS[selectedKey]) { showNeighborhoodSelect(DISTRICTS[selectedKey].name); } else { hideNeighborhoodSelect(); } }; } function hideDistrictSelect() { const group = document.getElementById('districtSelectGroup'); const select = document.getElementById('districtSelect'); group.style.display = 'none'; select.required = false; select.value = ''; } function showNeighborhoodSelect(districtName) { if (!NEIGHBORHOOD_DATA || !NEIGHBORHOOD_DATA[districtName]) { hideNeighborhoodSelect(); return; } const group = document.getElementById('neighborhoodSelectGroup'); const select = document.getElementById('neighborhoodSelect'); const notice = document.getElementById('neighborhoodNotice'); const neighborhoods = NEIGHBORHOOD_DATA[districtName]; // Clear and populate options select.innerHTML = ''; select.innerHTML += ''; select.innerHTML += ''; select.innerHTML += ''; neighborhoods.forEach(n => { const option = document.createElement('option'); option.value = n.id; option.textContent = `${n.name} (${n.priceTier})`; select.appendChild(option); }); group.style.display = 'block'; notice.style.display = 'none'; // Handle selection changes select.onchange = function() { if (this.value === 'not_listed' || this.value === 'dont_know') { notice.style.display = 'flex'; } else { notice.style.display = 'none'; } }; } function hideNeighborhoodSelect() { const group = document.getElementById('neighborhoodSelectGroup'); const select = document.getElementById('neighborhoodSelect'); group.style.display = 'none'; select.value = ''; } function getSelectedNeighborhood(districtName) { const select = document.getElementById('neighborhoodSelect'); const value = select.value; if (!value || value === 'not_listed' || value === 'dont_know' || !NEIGHBORHOOD_DATA || !NEIGHBORHOOD_DATA[districtName]) { return null; } return NEIGHBORHOOD_DATA[districtName].find(n => n.id === value) || null; } /** * OfferEdge District Parameters * Professional market estimates based on 20+ years combined experience * © 2026 Vincent Cyr Group LLC */ // Neighborhood-level market data (2025 sales) // Loaded from market-data-full.json - 2,318 neighborhoods across 25 districts // Neighborhood-level market data (2025 sales) - 2,318 neighborhoods const ZIP_MAP = { // Chester County "19301": "tredyffrin_easttown", "19310": "octorara", "19312": "tredyffrin_easttown", "19317": "unionville_chadds_ford", "19320": "coatesville", "19330": "octorara", "19333": "tredyffrin_easttown", "19335": "downingtown", "19341": "downingtown", "19343": "downingtown", "19344": "twin_valley", "19346": "avon_grove", "19347": "avon_grove", "19348": "kennett", "19351": "oxford", "19352": "avon_grove", "19353": "downingtown", "19357": "unionville_chadds_ford", "19358": "coatesville", "19360": "avon_grove", "19362": "octorara", "19363": "oxford", "19365": "coatesville", "19366": "unionville_chadds_ford", "19369": "coatesville", "19372": "coatesville", "19374": "kennett", "19375": "unionville_chadds_ford", "19376": "coatesville", "19380": "west_chester", "19382": "west_chester", "19383": "west_chester", "19390": "kennett", "19395": "west_chester", "19421": "phoenixville", "19425": "phoenixville", "19432": "great_valley", "19442": "phoenixville", "19457": "owen_j_roberts", "19460": "phoenixville", "19464": "owen_j_roberts", "19465": "owen_j_roberts", "19468": "phoenixville", "19470": "twin_valley", "19475": "spring_ford", "19480": "downingtown", "19520": "twin_valley", "19525": "owen_j_roberts", "19540": "twin_valley", "19567": "twin_valley", // Delaware County "19003": "haverford", "19008": "marple_newtown", "19010": "radnor", "19013": "chester_upland", "19014": "garnet_valley", "19015": "garnet_valley", "19017": "garnet_valley", "19018": "southeast_delco", "19022": "chichester", "19023": "southeast_delco", "19026": "upper_darby", "19029": "interboro", "19032": "ridley", "19033": "penn_delco", "19036": "penn_delco", "19037": "rose_tree_media", "19039": "west_chester", "19041": "haverford", "19050": "upper_darby", "19060": "garnet_valley", "19061": "garnet_valley", "19064": "springfield_delco", "19073": "marple_newtown", "19076": "interboro", "19078": "ridley", "19079": "ridley", "19081": "wallingford_swarthmore", "19082": "upper_darby", "19083": "upper_darby", "19085": "radnor", "19086": "wallingford_swarthmore", "19087": "radnor", "19094": "william_penn", "19342": "garnet_valley", // Montgomery County "19004": "lower_merion", "19066": "lower_merion", "19072": "lower_merion", "19096": "lower_merion", "19403": "methacton", "19426": "spring_ford", "19428": "perkiomen_valley", "19473": "spring_ford", "19492": "perkiomen_valley", // New Castle County, DE "19701": "appoquinimink", "19702": "christina", "19709": "appoquinimink", "19711": "christina", "19713": "christina", "19720": "christina", "19734": "appoquinimink", "19802": "brandywine_de", "19803": "red_clay", "19804": "red_clay", "19805": "red_clay", "19806": "red_clay", "19807": "red_clay", "19808": "red_clay", "19809": "brandywine_de", "19810": "red_clay", "19977": "smyrna" }; // ZIPs that span multiple school districts - user must select const MULTI_DISTRICT_ZIPS = { "19342": [ { key: "garnet_valley", label: "Garnet Valley School District" }, { key: "west_chester", label: "West Chester Area School District" } ], "19382": [ { key: "west_chester", label: "West Chester Area School District" }, { key: "unionville_chadds_ford", label: "Unionville-Chadds Ford School District" }, { key: "great_valley", label: "Great Valley School District" } ], "19348": [ { key: "unionville_chadds_ford", label: "Unionville-Chadds Ford School District" }, { key: "kennett", label: "Kennett Consolidated School District" } ], "19341": [ { key: "downingtown", label: "Downingtown Area School District" }, { key: "west_chester", label: "West Chester Area School District" } ], "19335": [ { key: "downingtown", label: "Downingtown Area School District" }, { key: "west_chester", label: "West Chester Area School District" }, { key: "coatesville", label: "Coatesville Area School District" } ], "19355": [ { key: "great_valley", label: "Great Valley School District" }, { key: "tredyffrin_easttown", label: "Tredyffrin-Easttown School District" } ], "19311": [ { key: "avon_grove", label: "Avon Grove School District" }, { key: "kennett", label: "Kennett Consolidated School District" } ], "19350": [ { key: "kennett", label: "Kennett Consolidated School District" }, { key: "avon_grove", label: "Avon Grove School District" } ], "19063": [ { key: "rose_tree_media", label: "Rose Tree Media School District" }, { key: "garnet_valley", label: "Garnet Valley School District" } ] // Add more multi-district ZIPs here as identified }; const SEASONAL_FACTORS = { high: { 0: 1.15, 1: 1.10, 2: 0.95, 3: 0.85, 4: 0.80, 5: 0.85, 6: 0.95, 7: 1.05, 8: 0.95, 9: 1.00, 10: 1.10, 11: 1.20 }, medium: { 0: 1.08, 1: 1.05, 2: 0.98, 3: 0.92, 4: 0.90, 5: 0.92, 6: 0.98, 7: 1.02, 8: 0.98, 9: 1.00, 10: 1.05, 11: 1.10 }, low: { 0: 1.03, 1: 1.02, 2: 0.99, 3: 0.97, 4: 0.96, 5: 0.97, 6: 0.99, 7: 1.01, 8: 0.99, 9: 1.00, 10: 1.02, 11: 1.04 } }; // ═══════════════════════════════════════════════════════════════════════ // SCORING ALGORITHM // ═══════════════════════════════════════════════════════════════════════ function calculateLikelihood(district, inputs) { // Start with district baseline let score = district.priceFlexibility; const factors = {}; // 1. Market Pressure Adjustment if (district.marketPressure === "tight") { score -= 8; factors.market = "Cyr Team posture: Seller-leaning"; } else if (district.marketPressure === "loose") { score += 10; factors.market = "Cyr Team posture: Buyer-leaning"; } else { factors.market = "Balanced"; } // 2. Days on Market Impact (scoring uses ratio, labels use absolute buckets) const domRatio = inputs.daysOnMarket / district.typicalDOM; if (domRatio > 1.5) { score += 15; } else if (domRatio > 1.0) { score += 8; } else if (domRatio > 0.5) { // neutral - no score change } else { score -= 5; } // DOM bucket labels (absolute days, not ratio) if (inputs.daysOnMarket <= 14) { factors.dom = "Fresh listing"; } else if (inputs.daysOnMarket <= 30) { factors.dom = "Active range"; } else if (inputs.daysOnMarket <= 60) { factors.dom = "Stuck zone"; } else { factors.dom = "Extended time on market"; } // 3. Price Position const [medLow, medHigh] = district.medianPriceRange; const price = inputs.listPrice; if (price > medHigh * 1.2) { score += 12; factors.price = "Above local norms"; } else if (price > medHigh) { score += 6; factors.price = "Above local norms"; } else if (price < medLow * 0.8) { score -= 8; factors.price = "Below local norms"; } else if (price < medLow) { score -= 4; factors.price = "Below local norms"; } else { factors.price = "Within typical range"; } // 4. Seasonal Adjustment (reduced weight for fresh listings) const month = new Date().getMonth(); const seasonalFactor = SEASONAL_FACTORS[district.seasonalSensitivity][month]; const isFreshListing = inputs.daysOnMarket <= 14; const seasonalWeight = isFreshListing ? 0.5 : 1.0; // 50% weight for fresh listings const seasonalAdjustment = (seasonalFactor - 1) * 20 * seasonalWeight; score += seasonalAdjustment; if (seasonalFactor > 1.05) { factors.season = isFreshListing ? "Slower season (reduced—new listing)" : "Slower season"; } else if (seasonalFactor < 0.95) { factors.season = isFreshListing ? "Peak season (reduced—new listing)" : "Peak season"; } else { factors.season = "Neutral timing"; } // 5. Property Condition if (inputs.condition === "needsWork") { score += 10; } else if (inputs.condition === "fair") { score += 5; } else if (inputs.condition === "excellent") { score -= 5; } // 6. Seller Motivation (if known) if (inputs.motivation === "veryMotivated") { score += 15; } else if (inputs.motivation === "motivated") { score += 8; } else if (inputs.motivation === "notMotivated") { score -= 10; } // 7. Prior Reductions if (inputs.priorReductions === "multiple") { score += 12; factors.priorReductions = "Multiple reductions"; } else if (inputs.priorReductions === "one") { score += 8; factors.priorReductions = "One prior reduction"; } else if (inputs.priorReductions === "none") { factors.priorReductions = "No reductions yet"; } else { factors.priorReductions = "Unknown"; } // Clamp to reasonable range score = Math.max(5, Math.min(85, Math.round(score))); // ═══════════════════════════════════════════════════════════════════ // POSTURE ALIGNMENT RULES (prevent contradictory outputs) // ═══════════════════════════════════════════════════════════════════ // Rule 1: If price is Below local norms → never Seller-leaning if (factors.price === "Below local norms" && factors.market === "Cyr Team posture: Seller-leaning") { factors.market = "Balanced"; factors.postureOverride = "adjusted-below-norms"; } // Rule 2: If market is Balanced district AND no strong competition → default Balanced posture // (marketPressure already handles this, but we track for transparency) return { score, factors, typicalAdjustment: district.typicalAdjustment }; } function getLikelihoodLevel(score) { if (score <= 20) return { level: "Low", class: "low" }; if (score <= 40) return { level: "Moderate", class: "moderate" }; if (score <= 60) return { level: "Elevated", class: "elevated" }; return { level: "High", class: "high" }; } // Helper: Clamp leverage/risk to valid values function clampLevel(level) { const valid = ['Low', 'Moderate', 'High']; if (valid.includes(level)) return level; if (level === 'High+' || level === 'Very High') return 'High'; return 'Moderate'; // fallback } // ═══════════════════════════════════════════════════════════════════════ // MARKET CONTEXT NOTE - Supporting information (not a leverage driver) // Data informs characterization, but no percentages surfaced (BrightMLS compliant) // ═══════════════════════════════════════════════════════════════════════ function generateMarketContext(districtName, propertySignals, userMode) { // propertySignals = { dom, domBand, hasReduction, pricePosition, leverage } // userMode = 'buyer', 'buyer_agreed', 'seller', 'agent_buyer', 'agent_seller' const stats = DISTRICT_STATS ? DISTRICT_STATS[districtName] : null; if (!stats) return null; const parts = []; // 1. SALE-TO-LIST CONTEXT (trailing 6 months) - characterization only const pctGE = stats.pctGE; const pctLESS = stats.pctLESS; if (pctGE && pctGE >= 65) { parts.push(`Homes in ${districtName} have generally been selling at or above asking price.`); } else if (pctGE && pctGE >= 58) { parts.push(`Most homes in ${districtName} have been selling near or above asking.`); } else if (pctLESS && pctLESS >= 55) { parts.push(`Most homes in ${districtName} have been selling below asking price.`); } // 2. YEAR-OVER-YEAR CONTEXT - characterization only, no point values const yoyChange = stats.yoyChange; if (yoyChange !== null && yoyChange !== undefined) { if (yoyChange >= 10) { parts.push(`The market has tightened significantly compared to this time last year.`); } else if (yoyChange >= 5) { parts.push(`The market is somewhat tighter than this time last year.`); } else if (yoyChange <= -10) { parts.push(`The market has softened notably compared to this time last year.`); } else if (yoyChange <= -5) { parts.push(`The market is somewhat softer than this time last year.`); } } // 3. MONTHS OF INVENTORY CONTEXT - general characterization only const moi = stats.monthsOfInventory; if (moi !== null && moi !== undefined) { if (moi < 2) { parts.push(`Current inventory is very tight—a strong seller's market.`); } else if (moi < 3) { parts.push(`Inventory is low, which tends to favor sellers.`); } else if (moi >= 5) { parts.push(`Inventory is elevated, giving buyers more options and negotiating room.`); } } // 4. CONFLICTING SIGNALS (property vs market) - if applicable const { domBand, hasReduction, leverage } = propertySignals || {}; if (pctGE && pctGE >= 58 && (domBand === 'stuck' || domBand === 'extended')) { parts.push(`In a market where most homes are moving, extended time on market often points to something property-specific—price positioning, condition, or showing access.`); } else if (pctGE && pctGE >= 58 && hasReduction) { parts.push(`The seller has already adjusted price despite favorable market conditions—this may indicate motivation or pricing that needed correction.`); } else if (pctLESS && pctLESS >= 55 && domBand === 'fresh') { parts.push(`Even in a softer market, fresh listings typically hold early leverage. The broader trends matter more as time passes.`); } // 5. AGENT CTA (mode-specific — not shown to agents) if (userMode === 'buyer') { parts.push(`Your agent can provide more specific market data for this property and neighborhood to help you craft your offer strategy.`); } else if (userMode === 'buyer_agreed') { parts.push(`Ask your agent for detailed market data on this neighborhood—it can sharpen your offer strategy.`); } else if (userMode === 'seller') { parts.push(`Your agent can walk you through specific market data for your neighborhood to help calibrate your pricing strategy.`); } // Agent modes ('agent_buyer', 'agent_seller') — no CTA needed if (parts.length === 0) { return null; } return { title: 'Market Context', note: parts.join(' ') }; } function generateNarrative(district, inputs, result, userType = 'Buyer', agentClientType = 'buyerClient') { const { score, typicalAdjustment } = result; const { level } = getLikelihoodLevel(score); const adjustmentFormatted = typicalAdjustment.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }); let dataAnalysis = ""; let cyrPerspective = ""; const isLuxury = inputs.listPrice >= 2000000; const isFreshListing = inputs.daysOnMarket <= 14; const isStale = inputs.daysOnMarket >= 90; const hasMultipleReductions = inputs.priorReductions === 'multiple'; const hasOneReduction = inputs.priorReductions === 'one'; const hasNoReductions = inputs.priorReductions === 'none'; const isTestingMarket = inputs.motivation === 'notMotivated'; const isVeryMotivated = inputs.motivation === 'veryMotivated'; const isTightMarket = district.marketPressure === 'tight'; const isLooseMarket = district.marketPressure === 'loose'; // Check price position const [medLow, medHigh] = district.medianPriceRange; const isOverpriced = inputs.listPrice > medHigh * 1.2; const isUnderpriced = inputs.listPrice < medLow * 0.9; // Check seasonality (Dec = 11, Jan = 0) const month = new Date().getMonth(); const isSlowSeason = month === 11 || month === 0 || month === 1; // Mode-specific metric labels const metricLabel = userType === 'Seller' ? 'buyer negotiation pressure' : userType === 'Agent' ? 'negotiation leverage' : 'likelihood of price flexibility'; // ═══════════════════════════════════════════════════════════════════ // PRIORITY 1: MULTIPLE OFFERS/DEADLINE - Competition is real // ═══════════════════════════════════════════════════════════════════ if (inputs.multipleOffers === 'yes') { dataAnalysis = `Multiple offers or a deadline has been indicated for this ${district.name} property. Our algorithm shows ${level.toLowerCase()} likelihood of price flexibility, but active competition changes the dynamics significantly.`; cyrPerspective = `The Cyr Team's perspective: When multiple offers are in play, negotiating leverage takes a back seat to positioning. This is about winning, not extracting concessions. Lead with your best terms—price, timeline, contingencies, earnest money. Consider what makes your offer stand out beyond just the number. A personal letter, flexible closing date, or cleaner terms can make the difference when dollars are close.`; } // ═══════════════════════════════════════════════════════════════════ // PRIORITY 2: BACK ON MARKET - Higher uncertainty // ═══════════════════════════════════════════════════════════════════ else if (inputs.backOnMarket === 'yes') { dataAnalysis = `This property came back on market after a prior contract fell through. At ${inputs.daysOnMarket} days on market in ${district.name}, our analysis indicates ${level.toLowerCase()} likelihood of price flexibility.`; cyrPerspective = `The Cyr Team's perspective: Back-on-market properties warrant investigation before strategy. Why did the prior deal fall through? Was it financing, inspection findings, appraisal, or cold feet? The answer shapes everything. If it was buyer-side issues, the seller may be motivated but cautious. If inspection revealed problems, you need to know what you're walking into. Ask the listing agent directly—most will tell you. Then decide if the opportunity is worth the uncertainty.`; } // ═══════════════════════════════════════════════════════════════════ // LUXURY PROPERTIES ($2M+) - Different rules entirely // ═══════════════════════════════════════════════════════════════════ else if (isLuxury) { dataAnalysis = `This property is in the luxury segment where standard market patterns don't apply. Days on market of ${inputs.daysOnMarket} should be evaluated differently—luxury properties routinely sit longer due to a much smaller buyer pool.`; cyrPerspective = `The Cyr Team's perspective: Luxury properties don't follow normal DOM or price adjustment patterns. If this is part of the seller's portfolio, they likely have no urgency—they're holding out for their number. You may eventually see reasonableness, but it takes time. The seller's priority is the strength of your finances and your ability to close. Know what you're willing to pay, hold to that number, and don't chase.`; } // ═══════════════════════════════════════════════════════════════════ // TESTING THE MARKET - Low priority scenario // ═══════════════════════════════════════════════════════════════════ else if (isTestingMarket) { dataAnalysis = `Our analysis suggests ${level.toLowerCase()} likelihood of price flexibility, but the seller motivation you've indicated changes the equation significantly.`; cyrPerspective = `The Cyr Team's perspective: A seller "testing the market" should not be at the top of your list. Expect unreasonable terms favoring them, and a tiresome, frustrating process. Unless you absolutely, positively must have this house, it's not worth the effort to pursue. That said, properties are almost always worth a look—you'll understand it better, and the seller knows there's potential interest if their expectations aren't met.`; } // ═══════════════════════════════════════════════════════════════════ // SIGNIFICANTLY OVERPRICED - Anticipate the reduction (before Price-Firm check) // ═══════════════════════════════════════════════════════════════════ else if (isOverpriced) { dataAnalysis = `This property is priced well above typical for ${district.name}. Our analysis indicates ${level.toLowerCase()} likelihood of price flexibility.`; cyrPerspective = `The Cyr Team's perspective: First question: Does this property actually offer more than typical homes in the area? If so, the seller may believe those features justify the premium—though appraisal may be a consideration at this level. Otherwise, they may be testing the market in a buyer-leaning moment. If you want value, consider engaging before the next price move—sellers are often most responsive in that window.`; } // ═══════════════════════════════════════════════════════════════════ // 90+ DOM WITH NO REDUCTIONS - Price-Firm Seller // ═══════════════════════════════════════════════════════════════════ else if (isStale && hasNoReductions) { dataAnalysis = `This property has been on market ${inputs.daysOnMarket} days without any price reductions—well above the typical ${district.typicalDOM} days for ${district.name}. Our algorithm shows ${level.toLowerCase()} likelihood of flexibility, but the seller's behavior suggests otherwise.`; cyrPerspective = `The Cyr Team's perspective: This is a difficult property to get an accepted offer on. The seller is ignoring market conditions—fixed on a certain price or terms and refusing to acknowledge that the market thinks differently. You can make an offer, but it's unlikely the seller will be cooperative. Over time, they may take it off the market or change agents (blaming them, not themselves). Worth a look, but set expectations low.`; } // ═══════════════════════════════════════════════════════════════════ // MULTIPLE REDUCTIONS - Adjustment Mode (seller adjusting to reality) // ═══════════════════════════════════════════════════════════════════ else if (hasMultipleReductions) { dataAnalysis = `Multiple price reductions signal a seller who is adjusting to market reality. Combined with ${inputs.daysOnMarket} days on market in ${district.name}, our analysis indicates ${level.toLowerCase()} likelihood of further flexibility.`; cyrPerspective = `The Cyr Team's perspective: This seller is showing some level of desperation—maybe not extreme, but they clearly believe price is the only factor in getting an offer. They may be ignoring condition issues; the price and condition aren't in alignment with the market. But at least they're acknowledging the price has been too high. Go in with an offer below ask AND include whatever contingencies you need. Even if they don't engage immediately, they may circle back to you later.`; } // ═══════════════════════════════════════════════════════════════════ // FRESH LISTING IN TIGHT MARKET - Competitive situation // ═══════════════════════════════════════════════════════════════════ else if (isFreshListing && isTightMarket) { dataAnalysis = `This is a fresh listing (${inputs.daysOnMarket} days) in ${district.name}, a competitive market. Our algorithm shows ${level.toLowerCase()} likelihood of flexibility, but timing matters here.`; cyrPerspective = `The Cyr Team's perspective: If this listing is priced correctly, presented well, and aligned with market expectations, there's going to be very little room to negotiate—if anything, you may have to come in at ask or better and possibly give up contingency protections. Ask the listing agent if there are competing offers. If this is "the one," bring your absolute best terms. However, if this property doesn't sell within 14 days in this market, something's off—and that's when opportunity opens up.`; } // ═══════════════════════════════════════════════════════════════════ // UNDERPRICED - Deal or red flag? // ═══════════════════════════════════════════════════════════════════ else if (isUnderpriced) { dataAnalysis = `This property is priced below typical for ${district.name}. Our analysis shows ${level.toLowerCase()} likelihood of further flexibility, though the pricing itself tells a story.`; cyrPerspective = `The Cyr Team's perspective: This often reflects a property needing substantial work—systems, cosmetic, or both. You'll often see "as-is" language in the notes (which is redundant—all properties sell as-is—but signals intent). The seller may want an auction-like environment, or they're just trying to move it quickly. But the lower price still may not reflect the true value given condition issues. Always worth a look to gauge condition yourself.`; } // ═══════════════════════════════════════════════════════════════════ // SLOW SEASON LISTING - Need-based timing // ═══════════════════════════════════════════════════════════════════ else if (isSlowSeason) { dataAnalysis = `Listed during the slower winter market, this ${district.name} property has been on market ${inputs.daysOnMarket} days. Our analysis indicates ${level.toLowerCase()} likelihood of price flexibility.`; cyrPerspective = `The Cyr Team's perspective: Winter listings can indicate either strategy or timing needs. At ${inputs.daysOnMarket} days on market, there may be room for a reasonable conversation—especially if the seller has already adjusted once. You're more likely to get traction with a fair offer and clear terms than an aggressive opener.`; } // ═══════════════════════════════════════════════════════════════════ // ONE REDUCTION - Some flexibility shown // ═══════════════════════════════════════════════════════════════════ else if (hasOneReduction) { dataAnalysis = `The seller has already reduced the price once, and at ${inputs.daysOnMarket} days on market in ${district.name}, our analysis indicates ${level.toLowerCase()} likelihood of additional flexibility.`; cyrPerspective = `The Cyr Team's perspective: One reduction shows the seller is at least acknowledging market feedback. They're not completely fixed on their original number. This is a property where a well-structured offer—even below the reduced price—can start a productive conversation. Include your contingencies; show you're a serious buyer with reasonable terms.`; } // ═══════════════════════════════════════════════════════════════════ // DEFAULT NARRATIVE - Standard analysis // ═══════════════════════════════════════════════════════════════════ else { if (level === "Low") { dataAnalysis = `Based on current conditions in ${district.name}, this property shows a low likelihood of seller price flexibility. At ${inputs.daysOnMarket} days on market, it's ${inputs.daysOnMarket < district.typicalDOM ? 'still relatively fresh' : 'been available a while'}, but market dynamics favor sellers here.`; cyrPerspective = `The Cyr Team's perspective: The market here is relatively competitive, and properties in this price range tend to hold firm. If you're interested, consider a strong offer close to asking price with favorable terms. That said, every property is worth a look—you'll understand it better, and your interest signals to the seller that there's demand.`; } else if (level === "Moderate") { dataAnalysis = `The ${district.name} market suggests moderate potential for negotiation on this listing. At ${inputs.daysOnMarket} days on market against a typical ${district.typicalDOM} days, there may be room for conversation.`; cyrPerspective = `The Cyr Team's perspective: Sellers here don't always reduce, but there's reasonable opportunity—particularly if your offer demonstrates serious intent. A well-structured offer that reflects market conditions could open dialogue. Include your terms and contingencies; let them see you're a real buyer, not a lowball opportunist.`; } else if (level === "Elevated") { dataAnalysis = `Current market dynamics in ${district.name} indicate an elevated likelihood that this seller may show flexibility. there may be room for negotiation.`; cyrPerspective = `The Cyr Team's perspective: The combination of market timing and property positioning suggests meaningful room for negotiation. This is the kind of situation where a strategic offer below asking makes sense. Go in with your terms, and don't be afraid to ask for what you need on inspections and contingencies.`; } else { dataAnalysis = `Market conditions in ${district.name} strongly favor buyers for this property. At ${inputs.daysOnMarket} days on market, our analysis shows high likelihood of seller flexibility.`; cyrPerspective = `The Cyr Team's perspective: Multiple factors are working in your favor here. The seller is likely feeling market pressure and may be ready to make a deal. Consider an offer that reflects fair market value—they may be ready to meet you there. Don't leave money on the table by offering too close to ask.`; } } // Build DOM band label for context line const domBandLabel = inputs.daysOnMarket <= 14 ? 'a fresh listing' : inputs.daysOnMarket <= 30 ? 'in the active range' : inputs.daysOnMarket <= 60 ? 'the "stuck zone"' : 'extended time on market'; // Build season label for context line (month already declared above) const seasonLabel = (month >= 11 || month <= 1) ? 'the slower winter season' : (month >= 2 && month <= 4) ? 'the active spring market' : (month >= 5 && month <= 7) ? 'summer' : 'the fall market'; // ═══════════════════════════════════════════════════════════════════ // MODE-SPECIFIC NARRATIVE GENERATION // ═══════════════════════════════════════════════════════════════════ let conclusionLine = ''; let contextLine = `It's listed in ${seasonLabel} and has been on market ${inputs.daysOnMarket} days (${domBandLabel}).`; let perspectiveLine = cyrPerspective; if (userType === 'Seller') { // SELLER MODE - always use seller-specific perspective conclusionLine = `Based on the details you entered and our local market experience, this property shows ${level.toLowerCase()} ${metricLabel}.`; // Generate seller-specific perspective (override any buyer language from priority scenarios) if (inputs.daysOnMarket <= 14) { perspectiveLine = `The Cyr Team's perspective: Early days—focus on maximizing exposure and gathering showing feedback. What are agents saying after showings? That feedback shapes your next move.`; } else if (inputs.daysOnMarket <= 30) { perspectiveLine = `The Cyr Team's perspective: You're in the critical evaluation window. If showings are strong but no offers, it may be terms or condition. If showings are light, it's likely price. This is decision time.`; } else if (inputs.daysOnMarket <= 60) { if (hasOneReduction) { perspectiveLine = `The Cyr Team's perspective: At ${inputs.daysOnMarket} days, buyers tend to feel they have room to ask—especially after a prior reduction. Sellers typically get more traction by making the home easy to say yes to: tight presentation and clear terms that reduce friction.`; } else { perspectiveLine = `The Cyr Team's perspective: At ${inputs.daysOnMarket} days without a contract, it's worth diagnosing what's holding it back—price, presentation, showing friction, or terms. Decide what you're willing to adjust first, and what you want to hold firm on.`; } } else { perspectiveLine = `The Cyr Team's perspective: Extended time on market requires honest evaluation. The market has been giving feedback—the question is whether to adjust meaningfully now or wait for conditions to shift. Incremental reductions rarely re-engage serious buyers at this stage.`; } } else if (userType === 'Agent') { // AGENT MODE - conditional for buyer/seller client // agentClientType is passed as parameter // Skip redundant "Based on..." line - level shown in metric header already // Use context line + perspective only // Agent-native perspective with context awareness const winterContext = isSlowSeason ? 'Winter listings can reflect strategy or timing needs. ' : ''; const domContext = inputs.daysOnMarket > 30 ? `At ${inputs.daysOnMarket} days on market${hasOneReduction ? '—especially after a prior reduction—' : ', '}` : ''; if (agentClientType === 'sellerClient') { // SELLER CLIENT perspective perspectiveLine = `The Cyr Team's perspective: ${winterContext}${domContext}buyers often test the edges (inspection, credits, timeline). Protect price by reducing friction: tight presentation, crisp concession boundaries, and clear terms. If the last 7–10 days show flat activity or consistent price-driven feedback, that's your signal to consider one targeted adjustment.`; } else { // BUYER CLIENT perspective perspectiveLine = `The Cyr Team's perspective: ${winterContext}${domContext}there may be room for a reasonable conversation. Lead fair on price, win with clean terms, and use listing-agent intel to decide how hard to press.`; } // Return Agent narrative (definition shown separately under metric label, no redundant conclusion) return `

${contextLine}

${perspectiveLine}

`; } else { // BUYER MODE (default) conclusionLine = `Based on the details you entered and our local market experience, this property shows ${level.toLowerCase()} ${metricLabel}.`; perspectiveLine = cyrPerspective; } return `

${conclusionLine}

${contextLine}

${perspectiveLine}

`; } // ═══════════════════════════════════════════════════════════════════════ // FORM HANDLING - TWO STEP FLOW // ═══════════════════════════════════════════════════════════════════════ const propertyForm = document.getElementById('propertyForm'); const contactForm = document.getElementById('contactForm'); const step1Section = document.getElementById('step1Section'); const step2Section = document.getElementById('step2Section'); const resultsSection = document.getElementById('resultsSection'); const zipNotFound = document.getElementById('zipNotFound'); const step1Error = document.getElementById('step1Error'); const step2Error = document.getElementById('step2Error'); // Store property data between steps let propertyData = {}; let analysisResult = {}; // Check for returning user and pre-fill contact form const savedUserContact = localStorage.getItem('oe_user_contact'); if (savedUserContact) { try { const userData = JSON.parse(savedUserContact); if (userData.firstName) document.getElementById('firstName').value = userData.firstName; if (userData.lastName) document.getElementById('lastName').value = userData.lastName; if (userData.email) document.getElementById('email').value = userData.email; if (userData.phone) document.getElementById('phone').value = userData.phone; if (userData.userType) { document.getElementById('userType').value = userData.userType; // If Agent was saved, show the client type selector if (userData.userType === 'Agent') { document.getElementById('agentClientGroup').style.display = 'block'; document.getElementById('agentClientType').required = true; } } if (userData.agencyStatus) { document.getElementById('agencyStatus').value = userData.agencyStatus; // Trigger agency warning if pre-filled status requires acknowledgment (Buyer/Seller only) // But DON'T carry over the acknowledgment - they must re-acknowledge each session if (userData.agencyStatus === 'Signed Agreement' && userData.userType !== 'Agent') { document.getElementById('agencyWarning').classList.add('visible'); document.getElementById('agencyAcknowledge').checked = false; // Force re-acknowledgment } } console.log('Returning user detected, contact form pre-filled'); } catch (e) { console.log('Could not parse saved user contact data'); } } // Helper: Update agency warning text based on user type function updateAgencyWarningText() { const userType = document.getElementById('userType').value; const warningTitle = document.getElementById('agencyWarningTitle'); const warningText = document.getElementById('agencyWarningText'); if (userType === 'Seller') { warningTitle.textContent = '⚠️ Listing Agreement Notice'; warningText.textContent = 'You indicated you may have a signed listing agreement with another real estate professional. This analysis is provided for informational purposes only and does not constitute an offer of representation. We encourage you to work with your current listing agent on any decisions regarding pricing or negotiation strategy.'; } else { warningTitle.textContent = 'Important Notice'; warningText.textContent = 'If you have a signed agency agreement with another agent, we encourage you to work with them on this property. This analysis is provided for informational purposes only and does not constitute an offer of representation. If you have questions about your current agreement, please consult with your agent or a licensed attorney.'; } } // Agency status warning - show for Buyer/Seller only, not Agent document.getElementById('agencyStatus').addEventListener('change', function(e) { const warning = document.getElementById('agencyWarning'); const acknowledgeCheckbox = document.getElementById('agencyAcknowledge'); const userType = document.getElementById('userType').value; // Agents don't need agency warning (they ARE the agent) if (e.target.value === 'Signed Agreement' && userType !== 'Agent') { updateAgencyWarningText(); warning.classList.add('visible'); acknowledgeCheckbox.checked = false; } else { warning.classList.remove('visible'); acknowledgeCheckbox.checked = false; } }); // Dynamic score label based on userType selection (even before unlock) document.getElementById('userType').addEventListener('change', function(e) { const likelihoodLabel = document.getElementById('likelihoodLabel'); if (likelihoodLabel) { const labelMap = { 'Buyer': 'Price Flexibility Likelihood', 'Seller': 'Buyer Negotiation Pressure', 'Agent': 'Negotiation Leverage' }; // Only update if a valid userType is selected, otherwise keep default if (labelMap[e.target.value]) { likelihoodLabel.textContent = labelMap[e.target.value]; } else { likelihoodLabel.textContent = 'Negotiation Snapshot'; } } // Update agency warning text if it's visible, or hide if switching to Agent const agencyWarning = document.getElementById('agencyWarning'); const currentAgencyStatus = document.getElementById('agencyStatus').value; if (e.target.value === 'Agent') { // Agents don't need agency warning - always hide it regardless of agency status agencyWarning.classList.remove('visible'); document.getElementById('agencyAcknowledge').checked = false; } else if (currentAgencyStatus === 'Signed Agreement') { // If switching FROM Agent to Buyer/Seller and they have Signed Agreement, show warning updateAgencyWarningText(); agencyWarning.classList.add('visible'); document.getElementById('agencyAcknowledge').checked = false; } else if (agencyWarning.classList.contains('visible')) { updateAgencyWarningText(); } // Show/hide agent client type selector const agentClientGroup = document.getElementById('agentClientGroup'); const agentClientType = document.getElementById('agentClientType'); if (e.target.value === 'Agent') { agentClientGroup.style.display = 'block'; agentClientType.required = true; } else { agentClientGroup.style.display = 'none'; agentClientType.required = false; agentClientType.value = ''; } }); // Format price input document.getElementById('listPrice').addEventListener('blur', function(e) { let value = e.target.value.replace(/[^0-9]/g, ''); if (value) { e.target.value = '$' + parseInt(value).toLocaleString(); } }); // Format reduction amount input document.getElementById('reductionAmount').addEventListener('blur', function(e) { let value = e.target.value.replace(/[^0-9]/g, ''); if (value) { e.target.value = '$' + parseInt(value).toLocaleString(); } }); // Show/hide reduction amount field document.getElementById('priorReductions').addEventListener('change', function(e) { const amountGroup = document.getElementById('reductionAmountGroup'); if (e.target.value === 'one' || e.target.value === 'multiple') { amountGroup.style.display = 'block'; } else { amountGroup.style.display = 'none'; document.getElementById('reductionAmount').value = ''; } }); // STEP 1: Property Details propertyForm.addEventListener('submit', function(e) { e.preventDefault(); // Clear previous errors step1Error.classList.remove('visible'); step1Error.textContent = ''; // Get form values const zip = document.getElementById('zipCode').value.trim(); const priceStr = document.getElementById('listPrice').value.replace(/[^0-9]/g, ''); const price = parseInt(priceStr); const dom = parseInt(document.getElementById('daysOnMarket').value); const condition = document.getElementById('propertyCondition').value; const motivation = document.getElementById('sellerMotivation').value; const priorReductions = document.getElementById('priorReductions').value; const reductionAmountStr = document.getElementById('reductionAmount').value.replace(/[^0-9]/g, ''); const reductionAmount = reductionAmountStr ? parseInt(reductionAmountStr) : null; // New market context fields const backOnMarket = document.getElementById('backOnMarket').value; const multipleOffers = document.getElementById('multipleOffers').value; const recentReduction = document.getElementById('recentReduction').value; // System age fields const roofAge = document.getElementById('roofAge').value; const furnaceAge = document.getElementById('furnaceAge').value; const acAge = document.getElementById('acAge').value; const waterHeaterAge = document.getElementById('waterHeaterAge').value; // Validate ZIP is in coverage area const districtKey = ZIP_MAP[zip]; const isMultiDistrict = MULTI_DISTRICT_ZIPS[zip]; // Determine which district to use let selectedDistrictKey; if (isMultiDistrict) { // Multi-district ZIP - use dropdown selection selectedDistrictKey = document.getElementById('districtSelect').value; if (!selectedDistrictKey) { step1Error.textContent = 'Please select your school district.'; step1Error.classList.add('visible'); return; } } else if (districtKey) { // Single district ZIP selectedDistrictKey = districtKey; } else { // Not in coverage area step1Section.style.display = 'none'; zipNotFound.classList.add('visible'); window.scrollTo({ top: 0, behavior: 'smooth' }); return; } const district = DISTRICTS[selectedDistrictKey]; if (!district) { step1Error.textContent = 'Unable to load district data. Please try again.'; step1Error.classList.add('visible'); return; } const propertyAddress = document.getElementById('propertyAddress').value; // Check for prior submission of this property (localStorage) const submissionKey = `oe_${propertyAddress}_${zip}`.replace(/\s+/g, '_').toLowerCase(); const priorSubmission = localStorage.getItem(submissionKey); if (priorSubmission) { const prior = JSON.parse(priorSubmission); const inputsChanged = prior.listPrice !== price || prior.dom !== dom; // Show notification but allow them to continue if (inputsChanged) { step1Error.innerHTML = `Note: You analyzed this property on ${prior.date}. Your inputs have changed since then. Click "Analyze" again to see updated results.`; } else { step1Error.innerHTML = `Note: You analyzed this property on ${prior.date} with similar inputs. Result was ${prior.likelihood}. Click "Analyze" again to continue.`; } step1Error.style.background = '#fef3c7'; step1Error.style.borderColor = '#f59e0b'; step1Error.style.color = '#92400e'; step1Error.classList.add('visible'); // Change button to indicate re-analysis document.getElementById('step1Btn').textContent = 'Re-Analyze Property →'; // Remove the check after showing - let them proceed on next click localStorage.setItem(submissionKey + '_notified', 'true'); // If they were already notified, let them proceed if (localStorage.getItem(submissionKey + '_notified')) { // Continue with analysis } else { return; } } // Calculate analysis (but don't show yet) analysisResult = calculateLikelihood(district, { listPrice: price, daysOnMarket: dom, condition: condition, motivation: motivation, priorReductions: priorReductions, reductionAmount: reductionAmount }); analysisResult.likelihood = getLikelihoodLevel(analysisResult.score); analysisResult.district = district; // Get selected neighborhood (if any) const selectedNeighborhood = getSelectedNeighborhood(district.name); analysisResult.neighborhood = selectedNeighborhood; // Store property data propertyData = { propertyAddress: document.getElementById('propertyAddress').value, propertyCity: document.getElementById('propertyCity').value, propertyState: document.getElementById('propertyState').value, propertyZip: zip, mlsNumber: document.getElementById('mlsNumber').value || null, listPrice: price, daysOnMarket: dom, propertyCondition: condition, sellerMotivation: motivation, priorReductions: priorReductions, reductionAmount: reductionAmount, // New market context fields backOnMarket: backOnMarket, multipleOffers: multipleOffers, recentReduction: recentReduction, // System age fields roofAge: roofAge, furnaceAge: furnaceAge, acAge: acAge, waterHeaterAge: waterHeaterAge, addressValidated: addressSelected, districtKey: selectedDistrictKey, neighborhoodId: selectedNeighborhood ? selectedNeighborhood.id : null, neighborhoodName: selectedNeighborhood ? selectedNeighborhood.name : null, isMultiDistrictZip: !!isMultiDistrict, // Data source tracking (BrightMLS compliance) dataAttestation: true, // User confirmed data accuracy mlsNumberSource: 'user_provided', domSource: 'user_provided', listPriceSource: 'user_provided', dataSourceTimestamp: new Date().toISOString() }; // Update step 2 summary const reductionSummary = priorReductions === 'none' ? '' : priorReductions === 'one' ? ' • 1 reduction' : priorReductions === 'multiple' ? ' • Multiple reductions' : ''; document.getElementById('summaryAddress').textContent = `${propertyData.propertyAddress}, ${propertyData.propertyCity}`; document.getElementById('summaryDetails').textContent = `$${price.toLocaleString()} • ${dom} days on market${reductionSummary} • ${district.name}`; // Show blurred preview of results const fullAddress = `${propertyData.propertyAddress}, ${propertyData.propertyCity}`; document.getElementById('propertyAddressDisplay').textContent = fullAddress; document.getElementById('districtName').textContent = `${district.name} • ${district.county} County, ${district.state}`; // Show likelihood level (unblurred as the hook) const likelihood = analysisResult.likelihood; const likelihoodDisplay = document.getElementById('likelihoodDisplay'); likelihoodDisplay.className = `likelihood-display ${likelihood.class}`; document.getElementById('likelihoodValue').textContent = likelihood.level; // Set label based on userType if already selected, otherwise use neutral label const likelihoodLabel = document.getElementById('likelihoodLabel'); if (likelihoodLabel) { const preSelectedUserType = document.getElementById('userType').value; const labelMap = { 'Buyer': 'Price Flexibility Likelihood', 'Seller': 'Buyer Negotiation Pressure', 'Agent': 'Negotiation Leverage' }; likelihoodLabel.textContent = labelMap[preSelectedUserType] || 'Negotiation Snapshot'; } // Show temperature gauge const tempData = TEMPERATURE_DATA[district.name] || { emoji: "⚖️", label: "Balanced", cssClass: "balanced" }; const tempGauge = document.getElementById('temperatureGauge'); document.getElementById('tempEmoji').textContent = tempData.emoji; document.getElementById('tempLabel').textContent = tempData.label; tempGauge.className = `temperature-gauge ${tempData.cssClass}`; // Show luxury badge if $2M+ const luxuryBadge = document.getElementById('luxuryBadge'); if (propertyData.listPrice >= 2000000) { luxuryBadge.style.display = 'inline-block'; } else { luxuryBadge.style.display = 'none'; } // ═══════════════════════════════════════════════════════════════════ // LOCKED VIEW: Show only Top 3 drivers, hide everything else // ═══════════════════════════════════════════════════════════════════ // Populate Top 3 drivers only (no additional factors in locked view) document.getElementById('factorPrice').textContent = analysisResult.factors.price; document.getElementById('factorDOM').textContent = analysisResult.factors.dom; document.getElementById('factorReductions').textContent = analysisResult.factors.priorReductions || 'No reductions yet'; // HIDE these sections in locked view (will show on unlock) const narrativeSection = document.getElementById('analysisNarrative'); if (narrativeSection) narrativeSection.style.display = 'none'; const neighborhoodInsights = document.getElementById('neighborhoodInsights'); if (neighborhoodInsights) neighborhoodInsights.style.display = 'none'; const additionalFactors = document.querySelector('.more-context-accordion'); if (additionalFactors) additionalFactors.style.display = 'none'; // Hide strategy content, show teaser instead document.getElementById('strategyTitle').textContent = 'Strategy Considerations'; const strategyContent = document.getElementById('strategyContent'); if (strategyContent) strategyContent.style.display = 'none'; // Create unlock teaser block const offerStrategy = document.getElementById('offerStrategy'); let unlockTeaser = document.getElementById('unlockTeaser'); if (!unlockTeaser && offerStrategy) { unlockTeaser = document.createElement('div'); unlockTeaser.id = 'unlockTeaser'; unlockTeaser.className = 'unlock-teaser'; unlockTeaser.innerHTML = `

Unlock your full analysis to see:

Enter your contact info above to continue.

`; offerStrategy.appendChild(unlockTeaser); } if (unlockTeaser) unlockTeaser.style.display = 'block'; // Add styles for locked view if (!document.getElementById('lockedViewStyles')) { const styleEl = document.createElement('style'); styleEl.id = 'lockedViewStyles'; styleEl.textContent = ` .unlock-teaser { margin-top: 20px; } .unlock-teaser-content { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border: 1px solid #dee2e6; border-radius: 12px; padding: 24px 28px; } .unlock-teaser-content h4 { margin: 0 0 16px 0; color: #1a2b3c; font-size: 1.1em; } .unlock-teaser-content ul { margin: 0 0 16px 0; padding-left: 20px; color: #4a5568; } .unlock-teaser-content li { margin-bottom: 8px; line-height: 1.5; } .unlock-teaser-cta { margin: 0; color: #6b7280; font-style: italic; font-size: 0.95em; } #resultsSection { position: relative; } `; document.head.appendChild(styleEl); } // Show step 2 AND results preview (no floating overlay - step2Section has the unlock header) console.log('Step 1 complete - transitioning to Step 2'); step1Section.style.display = 'none'; step2Section.style.display = 'block'; resultsSection.classList.add('visible'); console.log('Step 2 visible:', step2Section.style.display); console.log('Results visible:', resultsSection.classList.contains('visible')); window.scrollTo({ top: 0, behavior: 'smooth' }); }); // STEP 2: Contact Info → Show Results contactForm.addEventListener('submit', async function(e) { e.preventDefault(); // Clear previous errors step2Error.classList.remove('visible'); step2Error.textContent = ''; // Manual validation for required fields (since form has novalidate) const firstName = document.getElementById('firstName').value.trim(); const lastName = document.getElementById('lastName').value.trim(); const email = document.getElementById('email').value.trim(); const userType = document.getElementById('userType').value; const agencyStatus = document.getElementById('agencyStatus').value; const termsConsent = document.getElementById('termsConsent').checked; if (!firstName) { step2Error.textContent = 'Please enter your first name.'; step2Error.classList.add('visible'); document.getElementById('firstName').focus(); return; } if (!lastName) { step2Error.textContent = 'Please enter your last name.'; step2Error.classList.add('visible'); document.getElementById('lastName').focus(); return; } if (!email) { step2Error.textContent = 'Please enter your email address.'; step2Error.classList.add('visible'); document.getElementById('email').focus(); return; } // Basic email format check if (!email.includes('@') || !email.includes('.')) { step2Error.textContent = 'Please enter a valid email address.'; step2Error.classList.add('visible'); document.getElementById('email').focus(); return; } if (!userType) { step2Error.textContent = 'Please select who you are (Buyer, Seller, or Agent).'; step2Error.classList.add('visible'); document.getElementById('userType').focus(); return; } if (!agencyStatus) { step2Error.textContent = 'Please select your agency status.'; step2Error.classList.add('visible'); document.getElementById('agencyStatus').focus(); return; } if (!termsConsent) { step2Error.textContent = 'Please agree to the Terms of Service to continue.'; step2Error.classList.add('visible'); return; } // If Agent selected, validate client type if (userType === 'Agent') { const agentClientType = document.getElementById('agentClientType').value; if (!agentClientType) { step2Error.textContent = 'Please select who you are advising (Buyer client or Seller client).'; step2Error.classList.add('visible'); document.getElementById('agentClientType').focus(); return; } } // Validate agency acknowledgment if they have existing agreement (Buyer/Seller only, not Agent) if (agencyStatus === 'Signed Agreement' && userType !== 'Agent') { const acknowledged = document.getElementById('agencyAcknowledge').checked; if (!acknowledged) { step2Error.textContent = 'Please acknowledge the agency notice to continue.'; step2Error.classList.add('visible'); return; } } const district = analysisResult.district; const likelihood = analysisResult.likelihood; // Get current district stats for inventory-based guidance const districtStats = DISTRICT_STATS[district.name] || null; const isVeryLowInventory = districtStats && districtStats.monthsOfInventory < 2; // Seller's market - act fast const isLowInventory = districtStats && districtStats.monthsOfInventory >= 2 && districtStats.monthsOfInventory < 3; // Seller favored const isModeratingMarket = districtStats && districtStats.monthsOfInventory >= 3 && districtStats.monthsOfInventory < 4; // Slightly seller favored const isBalancedMarket = districtStats && districtStats.monthsOfInventory >= 4 && districtStats.monthsOfInventory <= 5; // Balanced, trending buyer const isBuyersMarket = districtStats && districtStats.monthsOfInventory > 6; // Buyer's market // Disable button during submission const submitBtn = document.getElementById('step2Btn'); const originalText = submitBtn.textContent; submitBtn.disabled = true; submitBtn.textContent = 'Submitting...'; // Build contact data const contactData = { firstName: document.getElementById('firstName').value, lastName: document.getElementById('lastName').value, email: document.getElementById('email').value, phone: document.getElementById('phone').value || null, userType: document.getElementById('userType').value, agentClientType: document.getElementById('agentClientType').value || null, agencyStatus: document.getElementById('agencyStatus').value, agencyAcknowledged: document.getElementById('agencyStatus').value === 'Signed Agreement' ? document.getElementById('agencyAcknowledge').checked : null }; // Check for repeat submission (localStorage) const submissionKey = `oe_${propertyData.propertyAddress}_${propertyData.propertyZip}`.replace(/\s+/g, '_').toLowerCase(); const priorSubmission = localStorage.getItem(submissionKey); let isRepeatSubmission = false; let priorSubmissionDate = null; if (priorSubmission) { const prior = JSON.parse(priorSubmission); isRepeatSubmission = true; priorSubmissionDate = prior.date; } // Build notes for FUB const hasExistingAgreement = contactData.agencyStatus === 'Signed Agreement'; const priorReductionsText = propertyData.priorReductions === 'none' ? 'No' : propertyData.priorReductions === 'one' ? 'Yes - 1 reduction' : propertyData.priorReductions === 'multiple' ? 'Yes - multiple reductions' : 'Unknown'; const reductionAmountText = propertyData.reductionAmount ? ' ($' + propertyData.reductionAmount.toLocaleString() + ' total)' : ''; const repeatNote = isRepeatSubmission ? `\n🔄 REPEAT SUBMISSION - Previously analyzed on ${priorSubmissionDate}` : ''; const fubNotes = ` OfferEdge Analysis - ${new Date().toLocaleDateString('en-US')}${repeatNote} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ CONTACT TYPE ${contactData.userType} - ${contactData.agencyStatus}${hasExistingAgreement ? '\n⚠️ HAS EXISTING AGENCY AGREEMENT - Agency disclosure was presented' : ''} PROPERTY ${propertyData.propertyAddress} ${propertyData.propertyCity}, ${propertyData.propertyState} ${propertyData.propertyZip} ${propertyData.mlsNumber ? 'MLS #: ' + propertyData.mlsNumber : 'MLS #: Not provided'} DETAILS List Price: $${propertyData.listPrice.toLocaleString()} Days on Market: ${propertyData.daysOnMarket} Prior Reductions: ${priorReductionsText}${reductionAmountText} Condition: ${propertyData.propertyCondition} Seller Motivation: ${propertyData.sellerMotivation} ANALYSIS RESULTS Likelihood Level: ${likelihood.level.toUpperCase()} Score: ${analysisResult.score}/85 District: ${district.name} (${district.county} County) Typical Adjustment: $${analysisResult.typicalAdjustment.toLocaleString()} DATA SOURCE (User Attestation) ✓ User confirmed data accuracy ✓ MLS#, DOM, Price: USER PROVIDED ✓ Attested: ${propertyData.dataSourceTimestamp} CONSENT Email Follow-up: ${document.getElementById('emailConsent').checked ? 'YES' : 'NO'} Phone/Text: ${document.getElementById('phoneConsent').checked ? 'YES' : 'NO'} `.trim(); // Build tags array const agencyTagMap = { 'No Agreement': 'OE-No-Agreement', 'Signed Agreement': 'OE-Under-Agreement', 'Agreement Expired': 'OE-Agreement-Expired', 'Exploring': 'OE-Exploring' }; const fubTags = [ 'OE-Lead', `OE-${likelihood.level}`, `OE-${contactData.userType}`, agencyTagMap[contactData.agencyStatus] || 'OE-Unknown-Agency' ]; if (isRepeatSubmission) { fubTags.push('OE-Repeat'); } // FUB Event Payload const fubPayload = { source: 'OfferEdge', system: 'OfferEdge Pricing Tool', type: 'Property Inquiry', message: fubNotes, person: { firstName: contactData.firstName, lastName: contactData.lastName, emails: [{ value: contactData.email }], phones: contactData.phone ? [{ value: contactData.phone }] : [], tags: fubTags }, property: { street: propertyData.propertyAddress, city: propertyData.propertyCity, state: propertyData.propertyState, code: propertyData.propertyZip, mlsNumber: propertyData.mlsNumber || undefined, price: propertyData.listPrice } }; // Reset button submitBtn.disabled = false; submitBtn.textContent = originalText; // Store submission in localStorage for repeat detection localStorage.setItem(submissionKey, JSON.stringify({ date: new Date().toLocaleDateString('en-US'), email: contactData.email, likelihood: likelihood.level, score: analysisResult.score, listPrice: propertyData.listPrice, dom: propertyData.daysOnMarket })); // Store user contact info for future visits (pre-fill) localStorage.setItem('oe_user_contact', JSON.stringify({ firstName: contactData.firstName, lastName: contactData.lastName, email: contactData.email, phone: contactData.phone || '', userType: contactData.userType, agencyStatus: contactData.agencyStatus, lastVisit: new Date().toISOString() })); // Data capture handled by Zapier webhook after results display // Update results UI - wrapped in try/catch to prevent silent failures try { console.log('Starting UI update...'); const fullAddress = `${propertyData.propertyAddress}, ${propertyData.propertyCity}`; document.getElementById('propertyAddressDisplay').textContent = fullAddress; document.getElementById('districtName').textContent = `${district.name} • ${district.county} County, ${district.state}`; // Update temperature gauge const tempData = TEMPERATURE_DATA[district.name] || { emoji: "⚖️", label: "Balanced", cssClass: "balanced" }; const tempGauge = document.getElementById('temperatureGauge'); document.getElementById('tempEmoji').textContent = tempData.emoji; document.getElementById('tempLabel').textContent = tempData.label; tempGauge.className = `temperature-gauge ${tempData.cssClass}`; // Show luxury badge if $2M+ const luxuryBadge = document.getElementById('luxuryBadge'); if (propertyData.listPrice >= 2000000) { luxuryBadge.style.display = 'inline-block'; } else { luxuryBadge.style.display = 'none'; } const likelihoodDisplay = document.getElementById('likelihoodDisplay'); likelihoodDisplay.className = `likelihood-display ${likelihood.class}`; document.getElementById('likelihoodValue').textContent = likelihood.level; // Generate and store narrative const narrativeText = generateNarrative(district, propertyData, analysisResult); document.getElementById('analysisNarrative').innerHTML = narrativeText; document.getElementById('factorMarket').textContent = analysisResult.factors.market; document.getElementById('factorDOM').textContent = analysisResult.factors.dom; document.getElementById('factorPrice').textContent = analysisResult.factors.price; document.getElementById('factorSeason').textContent = analysisResult.factors.season; document.getElementById('factorReductions').textContent = analysisResult.factors.priorReductions || 'N/A'; // Populate neighborhood insights (if neighborhood was selected) const neighborhoodInsights = document.getElementById('neighborhoodInsights'); const neighborhood = analysisResult.neighborhood; if (neighborhood) { // Show the section neighborhoodInsights.style.display = 'block'; // Populate name document.getElementById('neighborhoodInsightsName').textContent = neighborhood.name; // Price Tier (qualitative, not specific dollars - MLS compliant) document.getElementById('nhMedianPrice').textContent = neighborhood.priceTier; // vs. Neighborhood - property price position relative to neighborhood norms const listPrice = propertyData.listPrice; const medianPrice = neighborhood.medianSoldPrice; const priceDiffPct = ((listPrice - medianPrice) / medianPrice) * 100; const vsNeighborhoodEl = document.getElementById('nhVsNeighborhood'); if (vsNeighborhoodEl) { if (priceDiffPct > 10) { vsNeighborhoodEl.textContent = 'Above'; vsNeighborhoodEl.className = 'neighborhood-stat__value'; } else if (priceDiffPct < -10) { vsNeighborhoodEl.textContent = 'Below'; vsNeighborhoodEl.className = 'neighborhood-stat__value'; } else { vsNeighborhoodEl.textContent = 'In line'; vsNeighborhoodEl.className = 'neighborhood-stat__value'; } } // Competition level mapping let competitionLabel = 'Balanced'; if (neighborhood.marketSpeed === 'Fast' || neighborhood.marketSpeed === 'Fast Moving') competitionLabel = 'Higher'; else if (neighborhood.marketSpeed === 'Slow') competitionLabel = 'Lower'; document.getElementById('nhMarketSpeed').textContent = competitionLabel; // Buyer activity - DOM comparison const domVsDistrict = neighborhood.domVsDistrict; let activityLabel = 'Steady'; if (domVsDistrict < -7) activityLabel = 'Active'; else if (domVsDistrict > 7) activityLabel = 'Cautious'; document.getElementById('nhMedianDom').textContent = activityLabel; // Add clarifier line under metrics const clarifierEl = document.getElementById('nhClarifier'); if (clarifierEl) { clarifierEl.textContent = 'Even in balanced conditions overall, specific neighborhoods can still be competitive.'; } // Populate "More context" accordion with secondary drivers const moreContextEl = document.getElementById('moreContextContent'); if (moreContextEl) { // Determine market posture (with alignment rules) let marketPosture = 'Balanced'; if (district.marketPressure === 'tight') marketPosture = 'Seller-leaning'; else if (district.marketPressure === 'loose') marketPosture = 'Buyer-leaning'; // ALIGNMENT RULE: If price is Below local norms, posture cannot be Seller-leaning if (priceDiffPct < -10 && marketPosture === 'Seller-leaning') { marketPosture = 'Balanced'; } // Determine seasonal timing const currentMonth = new Date().getMonth(); let seasonalTiming = 'Neutral'; if (currentMonth === 11 || currentMonth === 0 || currentMonth === 1) seasonalTiming = 'Slower season'; else if (currentMonth >= 2 && currentMonth <= 4) seasonalTiming = 'Peak season'; // Determine seller adjustment const hasReduction = propertyData.priorReductions === 'one' || propertyData.priorReductions === 'multiple'; let contextHTML = `
Market posture: ${marketPosture}
`; contextHTML += `
Seasonal timing: ${seasonalTiming}
`; if (propertyData.priorReductions !== 'unknown') { contextHTML += `
Seller adjustment: ${hasReduction ? 'Yes' : 'No'}
`; } moreContextEl.innerHTML = contextHTML; } // Generate professional narrative - single paragraph format const userType = contactData.userType; const hasMultipleReductions = propertyData.priorReductions === 'multiple'; const hasOneReduction = propertyData.priorReductions === 'one'; const hasNoReductions = propertyData.priorReductions === 'none'; const likelihoodLevel = likelihood.level; // Determine price position label for narrative let pricePositionDesc = 'well-positioned for current neighborhood norms'; if (priceDiffPct > 10) pricePositionDesc = 'priced above typical neighborhood values'; else if (priceDiffPct < -10) pricePositionDesc = 'priced below typical neighborhood values'; // Build reduction clause let reductionClause = ''; if (hasMultipleReductions) { reductionClause = 'The seller has adjusted price multiple times'; } else if (hasOneReduction) { reductionClause = 'The seller has already adjusted once'; } else { reductionClause = 'The seller has not adjusted price yet'; } let narrative = ''; if (userType === 'Seller') { // Seller-focused narrative narrative = `${neighborhood.name} is a ${neighborhood.priceTier.toLowerCase()} neighborhood with ${competitionLabel.toLowerCase()} competition and ${activityLabel.toLowerCase()} buyer activity. `; if (hasMultipleReductions || propertyData.daysOnMarket > 60) { narrative += `Given the time on market, positioning against comparable sales in this area will be critical. In this environment, buyers may test the edges—being crisp on pricing and terms helps you control the negotiation.`; } else { narrative += `Strategic pricing aligned with neighborhood benchmarks can help attract serious buyers. In this environment, buyers may test the edges—being crisp on pricing and terms helps you control the negotiation.`; } } else if (userType === 'Agent') { // Agent-focused narrative - more human, less data-card const reductionInsight = hasMultipleReductions ? 'Multiple reductions suggest the seller is chasing the market.' : hasOneReduction ? 'The prior reduction suggests the seller is responding to feedback.' : 'No reductions yet—the seller may be testing their price point.'; const priceInsight = priceDiffPct > 10 ? 'priced above' : priceDiffPct < -10 ? 'in line with or below' : 'in line with'; narrative = `${neighborhood.name} is a ${neighborhood.priceTier.toLowerCase()} neighborhood with ${competitionLabel.toLowerCase()} competition and ${activityLabel.toLowerCase()} buyer activity. The home appears ${priceInsight} neighborhood norms. ${reductionInsight}`; } else { // Buyer-focused narrative - new single paragraph format narrative = `${neighborhood.name} is a ${neighborhood.priceTier.toLowerCase()} neighborhood with ${competitionLabel.toLowerCase()} competition and ${activityLabel.toLowerCase()} buyer activity. ${reductionClause}, and the home is ${pricePositionDesc}—so negotiation may come more through terms and timing than a major price move.`; } document.getElementById('nhNarrative').textContent = narrative; // Price context section removed - incorporated into main narrative // Benchmark disclosure (single instance) const benchmarkEl = document.getElementById('nhPriceContext'); if (benchmarkEl) { benchmarkEl.innerHTML = 'Benchmarks are calculated by The Cyr Team from publicly recorded property transfers and used for directional context. They can vary by property type (townhome vs. single-family) and micro-location.'; } } else { // Hide the section if no neighborhood selected neighborhoodInsights.style.display = 'none'; } // Populate Offer Strategy section with Leverage × Risk framework v2.1 const userType = contactData.userType; const likelihoodLevel = likelihood.level; const currentMonth = new Date().getMonth(); // 0-11 const dom = propertyData.daysOnMarket; const hasMultipleReductions = propertyData.priorReductions === 'multiple'; const hasOneReduction = propertyData.priorReductions === 'one'; const hasNoReductions = propertyData.priorReductions === 'none'; // tempData already declared above for temperature gauge const isHotMarket = tempData.label.includes('Seller') || tempData.label.includes('Premium'); const isColdMarket = tempData.label.includes('Buyer') || tempData.label.includes('Momentum') || tempData.label.includes('Discount'); // New market context signals const isBackOnMarket = propertyData.backOnMarket === 'yes'; const backOnMarketUnsure = propertyData.backOnMarket === 'unknown'; const hasMultipleOffersSignal = propertyData.multipleOffers === 'yes'; const multipleOffersUnsure = propertyData.multipleOffers === 'unknown'; const hasRecentReduction = propertyData.recentReduction === 'yes'; const recentReductionUnsure = propertyData.recentReduction === 'unknown'; // === DETERMINISTIC LEVERAGE CALCULATION (v2.2) === // Step 1: Derive BASE leverage from Likelihood score (eliminates double-counting) let baseLeverage = 'Moderate'; if (likelihoodLevel === 'High' || likelihoodLevel === 'Elevated') { baseLeverage = 'High'; } else if (likelihoodLevel === 'Moderate') { baseLeverage = 'Moderate'; } else { baseLeverage = 'Low'; } // Moderate is default // Step 2: Apply caps/adjustments in precedence order let leverage = baseLeverage; let leverageCapped = false; let leverageCapReason = ''; // Priority 1: Competition flag (hard cap to Low) if (hasMultipleOffersSignal) { if (leverage !== 'Low') { leverageCapped = true; leverageCapReason = 'competition'; } leverage = 'Low'; } // Priority 2: Recent reduction (drop one level, with exception for multiple reductions) // Exception: if multiple reductions = Yes AND DOM > typicalDOM, don't reduce (seller is chasing market, not testing) if (hasRecentReduction && !leverageCapped) { const skipRecentReductionPenalty = hasMultipleReductions && dom > district.typicalDOM; if (!skipRecentReductionPenalty) { if (leverage === 'High') { leverage = 'Moderate'; leverageCapped = true; leverageCapReason = 'recent reduction'; } else if (leverage === 'Moderate') { leverage = 'Low'; leverageCapped = true; leverageCapReason = 'recent reduction'; } // Low stays Low } } // === SYSTEM AGE ANALYSIS === // Thresholds based on real-world lifespan concerns: // - Roof 20+ years: insurance companies start questioning, borderline // - Furnace 20+ years: end of life (15+ is aging) // - A/C 12+ years: near end of life // - Water heater 10+ years: typical lifespan const roofOld = propertyData.roofAge === 'old'; // 20+ years const furnaceOld = propertyData.furnaceAge === 'old'; // 15+ years const furnaceEndOfLife = propertyData.furnaceAge === 'old'; // 15+ (likely 20+ in practice) const acNearEndOfLife = propertyData.acAge === 'mid2' || propertyData.acAge === 'old'; // 11+ years (12+ is concerning) const acOld = propertyData.acAge === 'old'; // 15+ years const waterHeaterOld = propertyData.waterHeaterAge === 'old'; // 10+ years // Build system concerns list for messaging const systemConcerns = []; if (roofOld) systemConcerns.push({ system: 'roof', note: '20+ years—insurance companies may question' }); if (furnaceOld) systemConcerns.push({ system: 'furnace', note: '15+ years—approaching end of life' }); if (acNearEndOfLife) systemConcerns.push({ system: 'A/C', note: acOld ? '15+ years—past typical lifespan' : '11-15 years—near end of life' }); if (waterHeaterOld) systemConcerns.push({ system: 'water heater', note: '10+ years—at typical lifespan' }); const oldSystemCount = systemConcerns.length; const hasOldSystems = oldSystemCount > 0; const hasMultipleOldSystems = oldSystemCount >= 2; // System ages can INCREASE leverage (not cap it) - older systems = more negotiating room // BUT only when listing is already showing weakness (not fresh/competitive) let systemLeverageBoost = false; const listingShowsWeakness = dom >= district.typicalDOM || hasMultipleReductions || hasOneReduction; // Don't boost Low→Moderate (fresh/competitive listings with old systems shouldn't get "negotiate hard" advice) // Only boost Moderate→High when listing already shows weakness if (hasMultipleOldSystems && leverage === 'Moderate' && listingShowsWeakness && !hasMultipleOffersSignal) { leverage = 'High'; systemLeverageBoost = true; } // Final clamp: ensure leverage stays within valid range leverage = clampLevel(leverage); // === RISK CALCULATION (v2.1 - back-on-market + system age) === let risk = 'Low'; let riskFactors = []; if (isBackOnMarket) { risk = 'High'; riskFactors.push('back on market'); } else if (backOnMarketUnsure) { risk = 'Moderate'; riskFactors.push('uncertain contract history'); } // Old systems increase risk (inspection protection important) if (hasMultipleOldSystems) { if (risk === 'Low') risk = 'Moderate'; else if (risk === 'Moderate') risk = 'High'; riskFactors.push('aging systems'); } else if (hasOldSystems) { if (risk === 'Low') risk = 'Moderate'; riskFactors.push('aging system'); } // Final clamp: ensure risk stays within valid range risk = clampLevel(risk); // === CONFIDENCE CALCULATION (from "Not sure" count) === let notSureCount = 0; let notSureItems = []; if (backOnMarketUnsure) { notSureCount++; notSureItems.push('contract history'); } if (multipleOffersUnsure) { notSureCount++; notSureItems.push('multiple offers'); } if (recentReductionUnsure) { notSureCount++; notSureItems.push('recent reduction timing'); } let confidence = 'High'; if (notSureCount >= 2) { confidence = 'Low'; } else if (notSureCount === 1) { confidence = 'Mixed'; } // === LIKELIHOOD as mirror of base leverage (for messaging) === // Shows underlying signal strength before caps let leverageNote = ''; if (leverageCapped && baseLeverage !== leverage) { if (leverageCapReason === 'competition') { leverageNote = 'Signals suggest flexibility, but competition is limiting how hard you can press.'; } else if (leverageCapReason === 'recent reduction') { leverageNote = 'Underlying signals are favorable, but the recent reduction means the seller will likely want to test the new price first.'; } } else if (systemLeverageBoost && systemConcerns.length > 0) { const systemList = systemConcerns.map(s => s.system).join(' and '); leverageNote = `Aging ${systemList} may give you additional negotiating leverage—repair credits or seller concessions are reasonable asks beyond just price.`; } // Seasonal determination let seasonName = ''; let seasonalAdvice = ''; if (currentMonth >= 11 || currentMonth <= 1) { seasonName = 'winter'; seasonalAdvice = `Winter Market Opportunity: Fewer buyers are active during the holiday season. Some sellers listing now have real timing needs, which can create negotiation opportunities.`; } else if (currentMonth >= 2 && currentMonth <= 4) { seasonName = 'spring'; seasonalAdvice = `Spring Market Reality: This is peak season with maximum buyer competition. Expect multiple offer situations on well-priced properties. Hesitation can cost you the house.`; } else if (currentMonth >= 5 && currentMonth <= 7) { seasonName = 'summer'; if (currentMonth === 6 || currentMonth === 7) { seasonalAdvice = `Summer Opportunity Window: Mid-summer often sees a dip as families vacation. Listings sitting through July/August may see motivated sellers by Labor Day.`; } else { seasonalAdvice = `Early Summer: Market remains active but beginning to slow as vacation season approaches.`; } } else { seasonName = 'fall'; seasonalAdvice = `Fall Market Dynamics: Sellers listing now often want to close before year-end. Motivation tends to build through November as the holiday window approaches.`; } let strategyTitle = 'Strategy Considerations'; let strategyPosture = ''; let strategyContingencies = ''; let strategyTiming = ''; if (userType === 'Buyer') { strategyTitle = 'Buyer Strategy Considerations'; // === OFFER POSTURE (based on Leverage + context signals) === if (hasMultipleOffersSignal) { strategyPosture = `Offer Posture: Multiple offers or a deadline has been indicated—competition is real. Lead with your best terms and consider what makes your offer stand out beyond just price.`; } else if (hasRecentReduction) { strategyPosture = `Offer Posture: The seller just reduced their price within the last week or so. They'll likely want to test this new number before conceding further. A fair offer now may get consideration, but don't expect immediate flexibility.`; } else if (isBackOnMarket && leverage === 'High') { strategyPosture = `Offer Posture: This property came back on market—a prior deal fell through. You may have negotiating leverage, but first understand why it fell out. Ask questions before assuming the seller is desperate.`; } else if (leverage === 'High') { strategyPosture = `Offer Posture: Time on market and price history suggest room to negotiate. An offer below asking is reasonable. Even if they don't engage immediately, they may circle back to you.`; } else if (leverage === 'Moderate') { strategyPosture = `Offer Posture: Start near asking with strong terms, or slightly below with room to move—keep it grounded in a clear rationale.`; } else { // Low leverage if (isHotMarket) { strategyPosture = `Offer Posture: Limited flexibility expected in this market. Strong offers get attention. If you want this property, come in close to asking with clean terms—or be prepared to lose it.`; } else { strategyPosture = `Offer Posture: This seller appears firm on price. A fair offer near asking is appropriate, but don't expect significant movement. Your terms and timing may matter as much as price.`; } } // === BELOW NORMS ADDENDUM === // When priced below local norms, flexibility comes from terms not price if (analysisResult.factors.price === "Below local norms") { strategyPosture += ` Because this property is priced below local norms, flexibility may come more from terms and timing than a significant price concession.`; } // === CONTINGENCY STRATEGY (based on Leverage × Risk) === // Build system warning if applicable let systemWarning = ''; if (systemConcerns.length > 0) { if (hasMultipleOldSystems) { // Build detailed system concerns message const concernsText = systemConcerns.map(s => `${s.system} (${s.note})`).join(', '); systemWarning = ` Given aging systems—${concernsText}—inspection protection is especially important. Consider requesting a home warranty or repair credits.`; } else { // Single system concern const concern = systemConcerns[0]; systemWarning = ` The ${concern.system} is ${concern.note}—worth attention during inspection.`; } } if (leverage === 'Low' && risk === 'Low') { // Competitive situation, normal property strategyContingencies = `Contingency Strategy: The type and number of contingencies may factor into the seller's decision. Consider an acceptable repair allowance—you retain inspection rights but agree to cover repairs up to an amount you're comfortable with. This gives the seller certainty while protecting your interests.`; } else if (leverage === 'Low' && risk === 'High') { // Competitive but risky - DON'T waive protections const riskReason = isBackOnMarket ? 'This property came back on market—understand why before weakening your position.' : 'There are factors here that warrant extra protection.'; strategyContingencies = `Contingency Strategy: Even in a competitive situation, protect yourself here. ${riskReason} Compete through timeline, earnest money, or personal appeal rather than waiving protections.${systemWarning}`; } else if (leverage === 'Low' && risk === 'Moderate') { // Competitive with uncertainty or aging systems strategyContingencies = `Contingency Strategy: Competition limits your leverage, but there are factors that warrant keeping your protections. Structure them strategically—an acceptable repair allowance can work here.${systemWarning}`; } else if (leverage === 'High' && risk === 'Low') { // Strong position, clean property strategyContingencies = `Contingency Strategy: You have leverage—include the contingencies you need. Inspection, financing, appraisal. A seller motivated to move will often prioritize deal certainty over perfect terms.`; } else if (leverage === 'High' && risk === 'High') { // Strong position but risky property strategyContingencies = `Contingency Strategy: Keep full contingencies AND do extra diligence. This situation warrants confirming permits, systems, and disclosures. Your leverage lets you negotiate, but the risk profile means protecting yourself is equally important.${systemWarning}`; } else if (leverage === 'High' && risk === 'Moderate') { // Strong position with some uncertainty or aging systems strategyContingencies = `Contingency Strategy: You have leverage and should use it. Include full contingencies.${systemWarning || ' Consider asking about the property\'s history if you haven\'t already.'}`; } else if (leverage === 'Moderate' && risk === 'High') { // Moderate leverage, high risk const riskReason = isBackOnMarket ? 'Given that this property came back on market' : 'Given the risk factors present'; strategyContingencies = `Contingency Strategy: ${riskReason}, keep your protections firmly in place. An acceptable repair allowance can balance seller certainty with your need for inspection rights.${systemWarning}`; } else if (leverage === 'Moderate' && risk === 'Moderate') { // Moderate both strategyContingencies = `Contingency Strategy: Standard contingencies are reasonable here. Consider an acceptable repair allowance to give the seller more certainty while retaining your inspection rights.${systemWarning}`; } else { // Moderate leverage, low risk (default) strategyContingencies = `Contingency Strategy: The type and number of contingencies may factor into the seller's decision. An acceptable repair allowance lets you keep inspection rights while giving the seller more certainty about the outcome.`; } // === TIMING GUIDANCE (with DOM middle ground) === if (isBackOnMarket) { strategyTiming = `Timing: Back-on-market properties warrant extra investigation. Ask your agent to find out what happened with the prior contract. That answer should shape your approach.`; } else if (hasMultipleOffersSignal) { strategyTiming = `Timing: With multiple offers or a deadline in play, decisions often need to happen quickly. Waiting may mean losing the opportunity.`; } else if (dom <= 10 && !hasMultipleReductions && isVeryLowInventory) { // Fresh listing in seller's market (< 2 months) strategyTiming = `⚡ Low Inventory Alert: ${district.name} currently has only ${districtStats.monthsOfInventory.toFixed(1)} months of supply—a seller's market. Well-priced homes are moving very quickly. If this property fits your needs, be prepared to act decisively.`; } else if (dom <= 14 && !hasMultipleReductions && isLowInventory) { // Fresh listing in seller-favored market (2-3 months) strategyTiming = `Timing: Fresh listing in a seller-favored market—${district.name} has ${districtStats.monthsOfInventory.toFixed(1)} months of inventory. Desirable properties often move quickly here.`; } else if (dom <= 14 && !hasMultipleReductions && isModeratingMarket) { // Fresh listing in moderating market (3-4 months) strategyTiming = `Timing: Fresh listing in a moderating market. ${district.name} has ${districtStats.monthsOfInventory.toFixed(1)} months of inventory—still slightly favoring sellers. If you like it, timely action is worth considering.`; } else if (dom <= 14 && !hasMultipleReductions && isBuyersMarket) { // Fresh listing in buyer's market (6+ months) strategyTiming = `Timing: Fresh listing, but ${district.name} has ${districtStats.monthsOfInventory.toFixed(1)} months of inventory—a buyer's market. You have time to evaluate carefully and negotiate thoughtfully.`; } else if (dom <= 14 && !hasMultipleReductions && isHotMarket) { strategyTiming = `Timing: Fresh listing in a competitive market. If you're serious, moving quickly is often wise—desirable properties tend not to last long here.`; } else if (dom >= 15 && dom <= 30) { strategyTiming = `Timing: Still relatively fresh on market. If you like it, act—but you likely have some room depending on how showings have gone.`; } else if (dom >= 31 && dom <= 60) { strategyTiming = `Timing: You're in a window where sellers often respond to clear terms. Worth asking why it hasn't moved: condition, terms, or buyer feedback. That answer should shape your approach.`; } else if (dom > 90) { strategyTiming = `Timing: Extended time on market often increases flexibility—but confirm why it hasn't sold. Take your time with due diligence.`; } else if (hasMultipleReductions) { strategyTiming = `Timing: Multiple reductions signal a seller adjusting to reality. They're motivated—but that doesn't mean desperate. Make a fair offer and be patient through negotiation.`; } else if (dom > 60) { strategyTiming = `Timing: Extended days on market often signals opportunity. This property hasn't found its buyer yet—could be price, condition, or timing. Your offer may be the engagement they need.`; } } else if (userType === 'Seller') { strategyTitle = 'Seller Strategy Considerations'; // ═══════════════════════════════════════════════════════════════════ // SELLER: PRICING POSTURE (based on DOM bucket × pressure level) // ═══════════════════════════════════════════════════════════════════ const domBucket = dom <= 14 ? 'fresh' : dom <= 30 ? 'active' : dom <= 60 ? 'stuck' : dom <= 90 ? 'extended' : 'veryExtended'; if (domBucket === 'fresh' && (likelihoodLevel === 'Low' || likelihoodLevel === 'Moderate')) { strategyPosture = `Pricing Posture: Hold — Your pricing is being tested by the market. Give it time to generate showings and feedback before considering any changes. Early adjustments can signal weakness.`; } else if (domBucket === 'fresh' && (likelihoodLevel === 'Elevated' || likelihoodLevel === 'High')) { strategyPosture = `Pricing Posture: Monitor — Even fresh listings can face pressure in this market. Watch showing activity closely. If traffic is light in the first two weeks, early repositioning may be smart.`; } else if (domBucket === 'active' && likelihoodLevel === 'Low') { strategyPosture = `Pricing Posture: Hold — You're still in a reasonable window. Continue gathering feedback from showings. If offers come in below expectations, that's data—not necessarily a call to action.`; } else if (domBucket === 'active' && likelihoodLevel === 'Moderate') { strategyPosture = `Pricing Posture: Monitor — Watch showing feedback carefully. If showings are strong but no offers, it may be terms or condition. If showings are light, it may be price.`; } else if (domBucket === 'active' && (likelihoodLevel === 'Elevated' || likelihoodLevel === 'High')) { strategyPosture = `Pricing Posture: Consider Adjusting — Market signals suggest buyers see room to negotiate. A proactive adjustment now may attract more serious buyers than waiting.`; } else if (domBucket === 'stuck' && likelihoodLevel === 'Low') { strategyPosture = `Pricing Posture: Evaluate — Extended time without strong interest warrants a strategic review. Is it price, presentation, or property-specific factors?`; } else if (domBucket === 'stuck' && likelihoodLevel === 'Moderate') { strategyPosture = `Pricing Posture: Hold value, improve response — With ${dom} days on market and moderate buyer pressure, focus on re-engaging attention: tighten presentation, improve showing access, and be proactive on terms. If activity stays flat after 7–10 days, consider a targeted adjustment rather than a series of small cuts.`; } else if (domBucket === 'stuck' && (likelihoodLevel === 'Elevated' || likelihoodLevel === 'High')) { strategyPosture = `Pricing Posture: Adjust — Market signals suggest buyers perceive room to negotiate. A meaningful adjustment—not a token gesture—may be needed to generate serious offers.`; } else if (domBucket === 'extended') { strategyPosture = `Pricing Posture: Reassess — Extended time on market requires honest evaluation. If you need to sell, a significant adjustment is likely required. If you can wait, consider temporarily withdrawing and relisting fresh.`; } else if (domBucket === 'veryExtended') { strategyPosture = `Pricing Posture: Reset — At this point, incremental reductions rarely work. Consider a substantial price reset or a strategic withdrawal and relist approach.`; } // Add-on for multiple reductions if (hasMultipleReductions) { strategyPosture += ` Multiple reductions haven't solved it. Before adjusting again, consider what else might be at play—condition issues, showing access, or marketing approach.`; } // Add-on for price position if (analysisResult.factors.price === "Above local norms") { strategyPosture += ` Your pricing is above neighborhood benchmarks. Buyers in this area have comparable options, which limits your negotiating position.`; } else if (analysisResult.factors.price === "Below local norms") { strategyPosture += ` Your property is positioned competitively against local benchmarks. Even so, buyers may still test the edges on terms—your goal is to control that negotiation.`; } // ═══════════════════════════════════════════════════════════════════ // SELLER: WHAT TO EXPECT (by pressure level) // ═══════════════════════════════════════════════════════════════════ if (likelihoodLevel === 'High') { strategyContingencies = `What to Expect: Market signals suggest buyers will push hard on price. Low offers are likely. Engaging constructively—even with disappointing offers—keeps your options open. Dismissing offers outright can leave you with no leverage at all.`; } else if (likelihoodLevel === 'Elevated') { strategyContingencies = `What to Expect: Buyers perceive room to negotiate. Expect offers meaningfully below asking, possibly with repair requests or extended timelines. Every serious offer deserves a thoughtful response.`; } else if (likelihoodLevel === 'Moderate') { strategyContingencies = `What to Expect: Expect offers somewhat below asking as an opening position. Buyers will likely request inspection protections and may ask for concessions. This is normal negotiation—not a sign of weakness.`; } else { strategyContingencies = `What to Expect: Buyers in this market typically come in near asking. Expect reasonable offers with standard contingencies. You're in a good position to hold firm on price.`; } // ═══════════════════════════════════════════════════════════════════ // SELLER: TIMING (by DOM bucket) // ═══════════════════════════════════════════════════════════════════ if (domBucket === 'fresh') { strategyTiming = `Timing: Early days. Focus on maximizing exposure and gathering showing feedback before considering any changes. What are agents saying after showings?`; } else if (domBucket === 'active') { strategyTiming = `Timing: You're in the critical evaluation window. If showings are strong but no offers, it may be terms or condition. If showings are light, it may be price. This is decision time.`; } else if (domBucket === 'stuck') { strategyTiming = `Timing: At this point, it's worth diagnosing what's holding it back—price, presentation, showing friction, or terms. Decide what you're willing to adjust first, and what you want to hold firm on. What's your ideal timing to be under contract?`; } else if (domBucket === 'extended') { strategyTiming = `Timing: The market has been speaking. If you need to sell, a meaningful adjustment—not a token reduction—is likely required to re-engage serious buyers.`; } else { strategyTiming = `Timing: At this point, the listing has gone "stale" in buyers' minds. Consider a strategic withdrawal and relist at a reset price, or a very significant reduction.`; } // ═══════════════════════════════════════════════════════════════════ // SELLER: SEASONAL CONTEXT // ═══════════════════════════════════════════════════════════════════ if (seasonName === 'winter') { seasonalAdvice = `Winter Market Reality: Fewer buyers are active, but those looking now are often serious and motivated. A well-positioned property can still move—but overpricing tends to get exposed faster in slower seasons.`; } else if (seasonName === 'spring') { seasonalAdvice = `Spring Market Advantage: This is peak selling season. More buyers means more competition for your property, but also more options for buyers. Pricing right is critical to capture the spring wave.`; } else if (seasonName === 'summer') { seasonalAdvice = `Summer Market Dynamics: Activity remains solid. Families want to close before school starts, creating some urgency. Use this window before the fall slowdown.`; } else { seasonalAdvice = `Fall Market Window: Buyer activity typically slows after Labor Day. If you're still on market, consider whether to push through the holidays or withdraw and relist in spring.`; } } else { // ═══════════════════════════════════════════════════════════════════ // AGENT MODE: Full playbook with buyer/seller client variants // ═══════════════════════════════════════════════════════════════════ strategyTitle = 'Agent Playbook'; const agentClientType = contactData.agentClientType; const domBucket = dom <= 14 ? 'fresh' : dom <= 30 ? 'active' : dom <= 60 ? 'stuck' : 'extended'; // Situation summary - exclude price position since it's already shown in "What influenced" factors const keySignals = []; if (domBucket === 'fresh') keySignals.push(`fresh listing (${dom} days)`); else if (domBucket === 'active') keySignals.push(`${dom} days on market`); else if (domBucket === 'stuck') keySignals.push(`${dom} days (the "stuck zone")`); else keySignals.push(`${dom} days on market (extended)`); if (hasMultipleReductions) keySignals.push('multiple reductions'); else if (hasOneReduction) keySignals.push('one reduction'); // Note: price position omitted here - already shown in "What influenced" factors if (hasMultipleOffersSignal) { strategyPosture = `Situation Summary: Competition present. Multiple offers reported—leverage diminished regardless of other factors. Focus on winning, not negotiating.`; } else { // Determine pressure rule based on price position AND client type const pricePosition = analysisResult.factors.price; let pressureRule = ''; if (agentClientType === 'sellerClient') { // Seller client pressure rules - conditional on feedback if (pricePosition === 'Above local norms') { pressureRule = '
Pressure rule (given above norms): expect price pressure; protect with terms unless feedback is clearly price-driven.'; } else if (pricePosition === 'Below local norms') { pressureRule = '
Pressure rule (given below norms): win on terms first; protect price unless feedback is clearly price-driven.'; } else { pressureRule = '
Pressure rule (pricing in line): hold price; be flexible on terms unless feedback is clearly price-driven.'; } } else { // Buyer client pressure rules if (pricePosition === 'Above local norms') { pressureRule = '
Pressure rule (given above norms): press on price first.'; } else if (pricePosition === 'Below local norms') { pressureRule = '
Pressure rule (given below norms): win on terms, not price.'; } else { pressureRule = '
Pressure rule (pricing in line): press selectively; win with terms.'; } } strategyPosture = `Situation Summary: ${leverage} leverage. ${keySignals.join(', ')}.${pressureRule}`; } // Client talk track let talkTrack = ''; if (agentClientType === 'buyerClient') { if (hasMultipleOffersSignal) { talkTrack = `Client Talk Track:`; } else if (leverage === 'High') { talkTrack = `Client Talk Track:`; } else if (leverage === 'Moderate') { talkTrack = `Client Talk Track:`; } else { talkTrack = `Client Talk Track:`; } // Questions to ask (buyer client) let questions = ''; if (hasMultipleOffersSignal) { questions = `Questions to Ask (Listing Agent):`; } else if (isBackOnMarket) { questions = `Questions to Ask (Listing Agent):`; } else { questions = `Questions to Ask (Listing Agent):`; } // Recommended next step (buyer client) let nextStep = ''; if (hasMultipleOffersSignal) { nextStep = `Recommended Next Step: Schedule showing ASAP. Prepare client to make best offer first—this is not a negotiation scenario.`; } else if (domBucket === 'fresh' && leverage === 'Low') { nextStep = `Recommended Next Step: Schedule showing immediately. If client is serious, prepare to write same-day if property fits their criteria.`; } else if (isBackOnMarket) { nextStep = `Recommended Next Step: Request disclosure package and any inspection reports from previous contract. Schedule showing after reviewing.`; } else if (domBucket === 'stuck' || domBucket === 'extended') { nextStep = `Recommended Next Steps:
  1. Call listing agent → confirm feedback, timeline, and preferred terms
  2. Choose posture → fair price + clean terms; press harder only if intel supports it
  3. Execute → showing + offer with a clear rationale
`; } else { nextStep = `Recommended Next Steps:
  1. Call listing agent → gauge motivation, timeline, and any term preferences
  2. Choose posture → fair price + clean terms; press harder only if intel supports it
  3. Execute → showing + offer with a clear rationale
`; } strategyContingencies = talkTrack; strategyTiming = questions; seasonalAdvice = nextStep; } else if (agentClientType === 'sellerClient') { // Seller client talk track if (likelihoodLevel === 'High') { talkTrack = `Client Talk Track:`; } else if (likelihoodLevel === 'Elevated') { talkTrack = `Client Talk Track:`; } else if (likelihoodLevel === 'Moderate') { talkTrack = `Client Talk Track:`; } else { talkTrack = `Client Talk Track:`; } // Questions to explore (seller client) - practical focus let questions = ''; if (domBucket === 'extended' || domBucket === 'stuck') { questions = `Questions to Explore with Client:`; } else { questions = `Questions to Explore with Client:`; } // Recommended next step (seller client) let nextStep = ''; if (domBucket === 'fresh') { nextStep = `Recommended Next Step: Review showing activity in one week. Adjust marketing strategy if traffic is below expectations.`; } else if (domBucket === 'active') { nextStep = `Recommended Next Step: Compile showing feedback from last 10 days. Schedule pricing strategy call to review.`; } else if (domBucket === 'stuck') { nextStep = `Recommended Next Steps:
  1. Review last 10–14 days of showing feedback + listing stats
  2. Confirm whether feedback is price-driven or friction-driven
  3. Decide: hold price + tighten terms, or one targeted adjustment
`; } else { nextStep = `Recommended Next Step: Prepare market update with recent comparable sales. Schedule in-person meeting to discuss price reset or withdrawal strategy.`; } strategyContingencies = talkTrack; strategyTiming = questions; seasonalAdvice = nextStep; } else { // No client type selected - show generic agent view strategyContingencies = `Select Client Type: Choose "Buyer client" or "Seller client" above to see tailored talk tracks and next steps.`; strategyTiming = ''; seasonalAdvice = ''; } } document.getElementById('strategyTitle').textContent = strategyTitle; document.getElementById('strategyPosture').innerHTML = strategyPosture; document.getElementById('strategyLeverageNote').innerHTML = leverageNote ? `${leverageNote}` : ''; document.getElementById('strategyContingencies').innerHTML = strategyContingencies; document.getElementById('strategyTiming').innerHTML = strategyTiming; document.getElementById('strategySeasonal').innerHTML = seasonalAdvice; // Confidence note (only show if not High) let confidenceNote = ''; if (confidence === 'Low') { const verifyList = notSureItems.map(item => `• ${item}`).join('
'); confidenceNote = `Confidence: Limited—several key details are uncertain.

What to verify:
${verifyList}

Confirming these details would significantly strengthen this analysis.`; } else if (confidence === 'Mixed') { confidenceNote = `Confidence: Mixed—you marked ${notSureItems.join(' and ')} as "not sure." Consider confirming with the listing agent for a sharper assessment.`; } document.getElementById('strategyConfidence').innerHTML = confidenceNote; // ═══════════════════════════════════════════════════════════════════════ // SEND WEBHOOK TO ZAPIER (after all display values are generated) // ═══════════════════════════════════════════════════════════════════════ const userTypeTagMap = { 'Buyer': 'OfferEdgeBuyer', 'Seller': 'OfferEdgeSeller', 'Agent': 'OfferEdgeAgent' }; // Strip HTML tags for plain text email fields const stripHtml = (html) => html.replace(/<[^>]*>/g, '').trim(); // ═══════════════════════════════════════════════════════════════════ // BUILD DYNAMIC TOP 3 DRIVERS (no blanks, plain English) // ═══════════════════════════════════════════════════════════════════ const buildTopDrivers = () => { const drivers = []; // Always include Price Position (slot 1) - Title Case to match UI if (analysisResult.factors.price && analysisResult.factors.price !== 'Within typical range') { drivers.push({ label: 'Price Position', value: analysisResult.factors.price }); } else { drivers.push({ label: 'Price Position', value: 'Within typical range' }); } // Always include DOM (slot 2) - with DOM band label - Title Case to match UI const domBand = propertyData.daysOnMarket <= 14 ? 'Fresh listing' : propertyData.daysOnMarket <= 30 ? 'Active range' : propertyData.daysOnMarket <= 60 ? 'the "stuck zone"' : 'Extended time on market'; drivers.push({ label: 'Days on Market', value: `${domBand} (${propertyData.daysOnMarket} days)` }); // Slot 3: Pick strongest signal from reductions, season, or posture const reductionSignal = analysisResult.factors.priorReductions; const seasonSignal = analysisResult.factors.season; // For Agent mode, use "Prior Reductions" label to match UI; for Buyer/Seller use "Seller Signal" const reductionLabel = contactData.userType === 'Agent' ? 'Prior Reductions' : 'Seller Signal'; // Priority: Multiple reductions > One reduction > Slow season > Peak season > Posture if (reductionSignal === 'Multiple reductions') { drivers.push({ label: reductionLabel, value: 'Multiple price reductions' }); } else if (reductionSignal === 'One prior reduction') { drivers.push({ label: reductionLabel, value: 'One prior reduction' }); } else if (seasonSignal && seasonSignal.includes('Slower')) { drivers.push({ label: 'Timing', value: 'Winter/slower season' }); } else if (seasonSignal && seasonSignal.includes('Peak')) { drivers.push({ label: 'Timing', value: 'Peak buying season' }); } else { // Default to reduction status even if "none" drivers.push({ label: reductionLabel, value: 'No reductions yet' }); } return drivers; }; const topDrivers = buildTopDrivers(); const driver1 = `${topDrivers[0].label}: ${topDrivers[0].value}`; const driver2 = `${topDrivers[1].label}: ${topDrivers[1].value}`; const driver3 = `${topDrivers[2].label}: ${topDrivers[2].value}`; // ═══════════════════════════════════════════════════════════════════ // BUILD EMAIL NARRATIVE (3 scannable paragraphs) - MODE-SPECIFIC // ═══════════════════════════════════════════════════════════════════ const emailMonth = new Date().getMonth(); const emailSeasonLabel = (emailMonth >= 11 || emailMonth <= 1) ? 'the slower winter season' : (emailMonth >= 2 && emailMonth <= 4) ? 'the active spring market' : (emailMonth >= 5 && emailMonth <= 7) ? 'summer' : 'the fall market'; const emailDomBand = propertyData.daysOnMarket <= 14 ? 'a fresh listing' : propertyData.daysOnMarket <= 30 ? 'in the active range' : propertyData.daysOnMarket <= 60 ? 'the "stuck zone"' : 'extended time on market'; // Mode-specific metric label for email const emailMetricLabel = contactData.userType === 'Seller' ? 'buyer negotiation pressure' : contactData.userType === 'Agent' ? 'negotiation leverage' : 'likelihood of price flexibility'; // Agent definition line (Agent mode only) - conditional for buyer/seller client let emailDefinition = ''; if (contactData.userType === 'Agent') { if (contactData.agentClientType === 'sellerClient') { emailDefinition = 'How firmly you can hold price vs. how flexible you need to be on terms/concessions.'; } else { emailDefinition = 'How hard you can press on price vs. how clean terms need to be.'; } } // Line 1: Conclusion (mode-specific) - skip for Agent mode since level shown in header let emailLine1 = ''; if (contactData.userType !== 'Agent') { emailLine1 = `Based on the details you entered and our local market experience, this property shows ${likelihood.level.toLowerCase()} ${emailMetricLabel}.`; } // Line 2: Context const emailLine2 = `It's listed in ${emailSeasonLabel} and has been on market ${propertyData.daysOnMarket} days (${emailDomBand}).`; // Neighborhood context (Agent mode only, if neighborhood was selected) let neighborhoodContext = ''; if (contactData.userType === 'Agent' && propertyData.neighborhoodName) { const hasReduction = propertyData.priorReductions === 'one' || propertyData.priorReductions === 'multiple'; const reductionNote = hasReduction ? '; the prior reduction suggests the seller is responding to feedback' : ''; neighborhoodContext = `Neighborhood context: ${propertyData.neighborhoodName} is mid-range with higher competition and steady activity; the home appears in line with neighborhood norms${reductionNote}.`; } // Line 3: Cyr Team perspective (mode-specific) const hasReductionForEmail = propertyData.priorReductions === 'one' || propertyData.priorReductions === 'multiple'; let emailLine3 = ''; if (contactData.userType === 'Seller') { // Seller perspective const reductionContext = hasReductionForEmail ? '—especially after a prior reduction' : ''; if (propertyData.daysOnMarket <= 30) { emailLine3 = `The Cyr Team's perspective: You're still in a reasonable window. Focus on gathering showing feedback and staying responsive to serious interest.`; } else { emailLine3 = `The Cyr Team's perspective: At this point in the listing, buyers often feel they have room to ask${reductionContext}. The goal is to hold your value while making it easy for the right buyer to say yes: tight presentation and clear terms that reduce friction. If activity stays flat over the next 7–10 days, that's your signal to consider a targeted adjustment rather than a series of small cuts.`; } } else if (contactData.userType === 'Agent') { // Agent perspective - conditional for buyer/seller client const agentClientType = contactData.agentClientType; const winterContext = emailSeasonLabel.includes('winter') ? 'Winter listings can reflect strategy or timing needs. ' : ''; const reductionContext = hasReductionForEmail ? '—especially after a prior reduction—' : ', '; const domContext = propertyData.daysOnMarket > 30 ? `At ${propertyData.daysOnMarket} days on market${reductionContext}` : ''; if (agentClientType === 'sellerClient') { // Seller client - protect price, reduce friction emailLine3 = `The Cyr Team's perspective: ${winterContext}${domContext}buyers often test the edges (inspection, credits, timeline). Protect price by reducing friction: tight presentation, crisp concession boundaries, and clear terms. If the last 7–10 days show flat activity or consistent price-driven feedback, that's your signal to consider one targeted adjustment.`; } else { // Buyer client - press on price emailLine3 = `The Cyr Team's perspective: ${winterContext}${domContext}there may be room for a reasonable conversation. Lead fair on price, win with clean terms, and use listing-agent intel to decide how hard to press.`; } } else { // Buyer perspective (default) const reductionContext = hasReductionForEmail ? '—especially since the seller has already adjusted once' : ''; if (emailSeasonLabel.includes('winter')) { emailLine3 = `The Cyr Team's perspective: Winter listings can indicate either strategy or timing needs. At ${propertyData.daysOnMarket} days on market, there may be room for a reasonable conversation${reductionContext}. Lead with a fair offer and clear terms; you'll likely get further than an aggressive opener.`; } else { emailLine3 = `The Cyr Team's perspective: At ${propertyData.daysOnMarket} days on market, there may be room for a reasonable conversation${reductionContext}. Lead with a fair offer and clear terms; you'll likely get further than an aggressive opener.`; } } // Mode-specific email subject const emailSubject = contactData.userType === 'Seller' ? `Your Pricing Intelligence Report: ${propertyData.propertyAddress}` : contactData.userType === 'Agent' ? `OfferEdge Analysis: ${propertyData.propertyAddress}` : `Your OfferEdge Analysis: ${propertyData.propertyAddress}`; // Mode-specific email CTA (agency-aware) const isRepresentedForEmail = contactData.agencyStatus === 'Signed Agreement'; let emailCTA; if (contactData.userType === 'Seller') { if (isRepresentedForEmail) { emailCTA = 'Reply with your timeline and goals (speed vs. price) and we\'ll share a few general positioning ideas you can review with your listing agent.'; } else { emailCTA = 'If helpful, reply with your ideal timeline, whether you\'d prefer a quick sale vs. maximum price, and any constraints (showings, settlement timing). I\'ll suggest a clean pricing-and-position plan you can implement right away.'; } } else if (contactData.userType === 'Agent') { if (contactData.agentClientType === 'sellerClient') { emailCTA = 'Reply with the seller\'s timeline + must-net (if known) and the last 2–3 feedback themes, and I\'ll suggest a clean positioning plan (price + concession boundaries + terms).'; } else { emailCTA = 'Reply with your client\'s goal (win vs value) and any constraints (timeline, appraisal risk, inspection posture), and I\'ll suggest a clean offer structure you can use.'; } } else { // Buyer if (isRepresentedForEmail) { emailCTA = 'Reply with questions about this analysis and we\'ll share general market context you can discuss with your agent.'; } else { emailCTA = 'If helpful, reply with your target timeline and any concerns, and I\'ll share a clean structure you can discuss with your agent.'; } } // ═══════════════════════════════════════════════════════════════════ // GENERATE MARKET CONTEXT FOR EMAIL (before payload) // ═══════════════════════════════════════════════════════════════════ let marketContextNote = ''; try { // Determine userMode for Market Context let mcUserMode = 'buyer'; if (contactData.userType === 'Buyer') { mcUserMode = contactData.agencyStatus === 'Signed Agreement' ? 'buyer_agreed' : 'buyer'; } else if (contactData.userType === 'Seller') { mcUserMode = 'seller'; } else if (contactData.userType === 'Agent') { mcUserMode = contactData.agentClientType === 'sellerClient' ? 'agent_seller' : 'agent_buyer'; } // Determine DOM band let mcDomBand = 'active'; if (propertyData.daysOnMarket <= 14) mcDomBand = 'fresh'; else if (propertyData.daysOnMarket <= 30) mcDomBand = 'active'; else if (propertyData.daysOnMarket <= 60) mcDomBand = 'stuck'; else mcDomBand = 'extended'; const mcResult = generateMarketContext(district.name, { dom: propertyData.daysOnMarket, domBand: mcDomBand, hasReduction: propertyData.priorReductions !== 'none', leverage: analysisResult.likelihood.level }, mcUserMode); if (mcResult && mcResult.note) { // Strip HTML tags for email plain text marketContextNote = mcResult.note.replace(/<[^>]*>/g, ''); } } catch (e) { console.log('Market context generation error (non-critical):', e); } const zapierPayload = { // Contact info firstName: contactData.firstName, lastName: contactData.lastName, email: contactData.email, phone: contactData.phone || '', userType: contactData.userType, agentClientType: contactData.agentClientType || '', tag: userTypeTagMap[contactData.userType] || 'OfferEdgeLead', agencyStatus: contactData.agencyStatus, agencyAcknowledged: contactData.agencyAcknowledged ? 'Yes' : 'N/A', // Property info propertyAddress: `${propertyData.propertyAddress}, ${propertyData.propertyCity}, ${propertyData.propertyState} ${propertyData.propertyZip}`, propertyZip: propertyData.propertyZip, mlsNumber: propertyData.mlsNumber || '', listPrice: propertyData.listPrice, daysOnMarket: propertyData.daysOnMarket, priorReductions: propertyData.priorReductions, district: district.name, neighborhood: propertyData.neighborhoodName || '', // Analysis results marketEnvironment: tempData.label, likelihoodLevel: likelihood.level, emailSubject: emailSubject, emailDefinition: emailDefinition, emailLine1: emailLine1, emailLine2: emailLine2, emailLine3: emailLine3, emailCTA: emailCTA, // Dynamic Top 3 Drivers (no blanks, plain English) driver1: driver1, driver2: driver2, driver3: driver3, driverOther: 'Other context: seasonal timing and market posture', neighborhoodContext: neighborhoodContext, marketContext: marketContextNote, // Strategy (keep HTML for Agent mode to preserve bullet formatting, strip for others) strategyPosture: contactData.userType === 'Agent' ? strategyPosture : stripHtml(strategyPosture), strategyContingencies: contactData.userType === 'Agent' ? strategyContingencies : stripHtml(strategyContingencies), strategyTiming: contactData.userType === 'Agent' ? strategyTiming : stripHtml(strategyTiming), strategySeasonal: contactData.userType === 'Agent' ? seasonalAdvice : stripHtml(seasonalAdvice), confidenceNote: stripHtml(confidenceNote), // Consent and metadata dataAttestation: 'Yes', emailConsent: document.getElementById('emailConsent').checked ? 'Yes' : 'No', phoneConsent: document.getElementById('phoneConsent').checked ? 'Yes' : 'No', isRepeatSubmission: isRepeatSubmission ? 'Yes' : 'No', submittedAt: new Date().toISOString() }; // Convert to form data for CORS compatibility const formData = new FormData(); Object.keys(zapierPayload).forEach(key => { formData.append(key, zapierPayload[key]); }); // Log new fields for debugging console.log('═══ ZAPIER PAYLOAD (New Fields) ═══'); console.log('userType:', zapierPayload.userType); console.log('agentClientType:', zapierPayload.agentClientType); console.log('emailSubject:', zapierPayload.emailSubject); console.log('emailLine1:', zapierPayload.emailLine1); console.log('emailLine3:', zapierPayload.emailLine3); console.log('emailCTA:', zapierPayload.emailCTA); console.log('════════════════════════════════════'); // Fire and forget - don't wait for response fetch('https://hooks.zapier.com/hooks/catch/5383194/ug3xx8u/', { method: 'POST', body: formData }).then(response => { console.log('Zapier webhook sent:', response.ok ? 'success' : 'failed'); }).catch(err => { console.error('Zapier webhook error (non-critical):', err); }); console.log('Full analysis sent to Zapier:', zapierPayload); // Show leverage/risk explanation for Agent users (and buyers who see leverage info) const leverageRiskKey = document.getElementById('leverageRiskKey'); if (userType === 'Agent' || userType === 'Buyer') { leverageRiskKey.style.display = 'block'; } else { leverageRiskKey.style.display = 'none'; } // Remove blur and show full results console.log('Showing results section...'); resultsSection.classList.remove('blurred-preview'); const unlockOverlay = document.getElementById('unlockOverlay'); if (unlockOverlay) unlockOverlay.style.display = 'none'; // ═══════════════════════════════════════════════════════════════════ // UNLOCK: Reveal hidden sections + regenerate mode-specific content // ═══════════════════════════════════════════════════════════════════ // Regenerate narrative with correct userType const narrativeSection = document.getElementById('analysisNarrative'); if (narrativeSection) { const modeNarrative = generateNarrative(district, propertyData, analysisResult, contactData.userType, contactData.agentClientType); narrativeSection.innerHTML = modeNarrative; narrativeSection.style.display = 'block'; } // ═══════════════════════════════════════════════════════════════════ // MARKET CONTEXT NOTE - Supporting district-level context // ═══════════════════════════════════════════════════════════════════ const marketContextSection = document.getElementById('marketContextSection'); if (marketContextSection) { // Determine userMode for Market Context CTA let userMode = 'buyer'; if (contactData.userType === 'Buyer') { userMode = contactData.agencyStatus === 'Signed Agreement' ? 'buyer_agreed' : 'buyer'; } else if (contactData.userType === 'Seller') { userMode = 'seller'; } else if (contactData.userType === 'Agent') { userMode = contactData.agentClientType === 'sellerClient' ? 'agent_seller' : 'agent_buyer'; } // Determine DOM band for conflict detection let domBand = 'active'; if (propertyData.daysOnMarket <= 14) domBand = 'fresh'; else if (propertyData.daysOnMarket <= 30) domBand = 'active'; else if (propertyData.daysOnMarket <= 60) domBand = 'stuck'; else domBand = 'extended'; const marketContext = generateMarketContext(district.name, { dom: propertyData.daysOnMarket, domBand: domBand, hasReduction: propertyData.priorReductions !== 'none', leverage: analysisResult.likelihood.level }, userMode); if (marketContext && marketContext.note) { document.getElementById('marketContextText').innerHTML = marketContext.note; marketContextSection.style.display = 'block'; } else { marketContextSection.style.display = 'none'; } } // REMOVED: Additional factors accordion (per decision to not show) // Keep it hidden - factors are baked into narrative instead const additionalFactors = document.querySelector('.more-context-accordion'); if (additionalFactors) additionalFactors.style.display = 'none'; const strategyContent = document.getElementById('strategyContent'); if (strategyContent) strategyContent.style.display = 'block'; const unlockTeaser = document.getElementById('unlockTeaser'); if (unlockTeaser) unlockTeaser.style.display = 'none'; // Swap CTA sections const ctaLocked = document.getElementById('ctaLocked'); const ctaUnlocked = document.getElementById('ctaUnlocked'); if (ctaLocked) ctaLocked.style.display = 'none'; if (ctaUnlocked) ctaUnlocked.style.display = 'block'; // Update CTA content by mode (agency-aware for Seller) const ctaTitle = document.getElementById('ctaTitle'); const ctaText = document.getElementById('ctaText'); const ctaButton = document.getElementById('ctaButton'); const isRepresented = contactData.agencyStatus === 'Signed Agreement'; if (ctaTitle && ctaText && ctaButton) { if (contactData.userType === 'Buyer') { if (isRepresented) { ctaTitle.textContent = 'Have Questions About This Analysis?'; ctaText.textContent = 'We can share general market context you can discuss with your agent.'; ctaButton.textContent = 'Get Market Context'; } else { ctaTitle.textContent = 'Ready to Make Your Move?'; ctaText.textContent = "Let's discuss strategy tailored to this specific property and your goals."; ctaButton.textContent = 'Schedule a Call'; } } else if (contactData.userType === 'Seller') { if (isRepresented) { ctaTitle.textContent = 'Want Positioning Ideas?'; ctaText.textContent = 'Reply with your timeline and goals (speed vs. price) and we\'ll share a few general positioning ideas you can review with your listing agent.'; ctaButton.textContent = 'Get Positioning Ideas'; } else { ctaTitle.textContent = 'Ready to Talk Pricing Strategy?'; ctaText.textContent = "Reply with your timeline and goals (speed vs. price) and we'll share a few general positioning ideas."; ctaButton.textContent = 'Schedule a Strategy Call'; } } else if (contactData.userType === 'Agent') { ctaTitle.textContent = 'Need Support on This One?'; if (contactData.agentClientType === 'sellerClient') { ctaText.textContent = 'Quick strategy check on pricing + positioning.'; } else { ctaText.textContent = 'Quick strategy check on posture + terms.'; } ctaButton.textContent = 'Get a Second Opinion'; } } // Show Listing Agreement Notice for represented sellers const listingAgreementNotice = document.getElementById('listingAgreementNotice'); if (listingAgreementNotice) { if (contactData.userType === 'Seller' && isRepresented) { listingAgreementNotice.style.display = 'block'; } else { listingAgreementNotice.style.display = 'none'; } } // Update score label to mode-specific const likelihoodLabel = document.getElementById('likelihoodLabel'); if (likelihoodLabel) { const labelMap = { 'Buyer': 'Price Flexibility Likelihood', 'Seller': 'Buyer Negotiation Pressure', 'Agent': 'Negotiation Leverage' }; likelihoodLabel.textContent = labelMap[contactData.userType] || 'Price Flexibility Likelihood'; } // Show metric definition for Agent mode only, with correct text based on client type const metricDefinition = document.getElementById('metricDefinition'); if (metricDefinition) { if (contactData.userType === 'Agent') { metricDefinition.style.display = 'block'; // Set text based on buyer vs seller client if (contactData.agentClientType === 'sellerClient') { metricDefinition.textContent = metricDefinition.dataset.seller; } else { metricDefinition.textContent = metricDefinition.dataset.buyer; } } else { metricDefinition.style.display = 'none'; } } // Populate Additional factors (now revealed) document.getElementById('factorMarket').textContent = analysisResult.factors.market; document.getElementById('factorSeason').textContent = analysisResult.factors.season; submitBtn.disabled = false; submitBtn.textContent = originalText; step2Section.style.display = 'none'; resultsSection.classList.add('visible'); window.scrollTo({ top: 0, behavior: 'smooth' }); console.log('Results displayed successfully'); } catch (uiError) { console.error('Error displaying results:', uiError); // Still show results even if some UI elements fail resultsSection.classList.remove('blurred-preview'); const unlockOverlay = document.getElementById('unlockOverlay'); if (unlockOverlay) unlockOverlay.style.display = 'none'; // Reveal hidden sections even on error const narrativeSection = document.getElementById('analysisNarrative'); if (narrativeSection) narrativeSection.style.display = 'block'; const additionalFactors = document.querySelector('.more-context-accordion'); if (additionalFactors) additionalFactors.style.display = 'block'; const strategyContent = document.getElementById('strategyContent'); if (strategyContent) strategyContent.style.display = 'block'; const unlockTeaser = document.getElementById('unlockTeaser'); if (unlockTeaser) unlockTeaser.style.display = 'none'; // Swap CTA sections const ctaLocked = document.getElementById('ctaLocked'); const ctaUnlocked = document.getElementById('ctaUnlocked'); if (ctaLocked) ctaLocked.style.display = 'none'; if (ctaUnlocked) ctaUnlocked.style.display = 'block'; submitBtn.disabled = false; submitBtn.textContent = originalText; step2Section.style.display = 'none'; resultsSection.classList.add('visible'); window.scrollTo({ top: 0, behavior: 'smooth' }); console.log('Results displayed (with errors)'); } }); function goBackToStep1() { step2Section.style.display = 'none'; step1Section.style.display = 'block'; window.scrollTo({ top: 0, behavior: 'smooth' }); } function resetForm() { propertyForm.reset(); contactForm.reset(); step1Section.style.display = 'block'; step2Section.style.display = 'none'; resultsSection.classList.remove('visible'); zipNotFound.classList.remove('visible'); // Reset blur state resultsSection.classList.remove('blurred-preview'); const unlockOverlay = document.getElementById('unlockOverlay'); if (unlockOverlay) unlockOverlay.style.display = 'none'; // Hide Market Context const marketContextSection = document.getElementById('marketContextSection'); if (marketContextSection) marketContextSection.style.display = 'none'; // Reset agency warning BEFORE pre-fill (so pre-fill has final say) document.getElementById('agencyWarning').classList.remove('visible'); document.getElementById('agencyAcknowledge').checked = false; // Hide listing agreement notice (results section) const listingAgreementNotice = document.getElementById('listingAgreementNotice'); if (listingAgreementNotice) listingAgreementNotice.style.display = 'none'; // Re-populate contact form for returning users const savedUserContact = localStorage.getItem('oe_user_contact'); if (savedUserContact) { try { const userData = JSON.parse(savedUserContact); if (userData.firstName) document.getElementById('firstName').value = userData.firstName; if (userData.lastName) document.getElementById('lastName').value = userData.lastName; if (userData.email) document.getElementById('email').value = userData.email; if (userData.phone) document.getElementById('phone').value = userData.phone; if (userData.userType) document.getElementById('userType').value = userData.userType; if (userData.agencyStatus) document.getElementById('agencyStatus').value = userData.agencyStatus; // Trigger agency warning if pre-filled status requires acknowledgment (Buyer/Seller only) if (userData.agencyStatus === 'Signed Agreement' && userData.userType !== 'Agent') { document.getElementById('agencyWarning').classList.add('visible'); } } catch (e) { // Ignore parse errors } } // Reset autocomplete state addressSelected = false; document.getElementById('addressHint').textContent = 'Select from dropdown to auto-fill city, state, ZIP'; document.getElementById('addressHint').style.color = ''; // Clear readonly fields document.getElementById('propertyCity').value = ''; document.getElementById('propertyState').value = ''; document.getElementById('zipCode').value = ''; // Hide district select hideDistrictSelect(); // Hide neighborhood select hideNeighborhoodSelect(); document.getElementById('neighborhoodInsights').style.display = 'none'; // Reset offer strategy document.getElementById('strategyPosture').innerHTML = ''; document.getElementById('strategyLeverageNote').innerHTML = ''; document.getElementById('strategyContingencies').innerHTML = ''; document.getElementById('strategyTiming').innerHTML = ''; document.getElementById('strategySeasonal').innerHTML = ''; document.getElementById('strategyConfidence').innerHTML = ''; // Reset likelihood key document.getElementById('likelihoodKeyContent').style.display = 'none'; document.querySelector('.likelihood-key__toggle').textContent = 'What does this mean? ▼'; // Reset leverage/risk key document.getElementById('leverageRiskKey').style.display = 'none'; document.getElementById('leverageRiskKeyContent').style.display = 'none'; document.querySelector('.leverage-risk-key__toggle').textContent = 'What do Leverage and Risk mean? ▼'; // Reset stored data propertyData = {}; analysisResult = {}; // (Agency warning reset moved above pre-fill block) // Reset attestation document.getElementById('dataAttestation').checked = false; // Reset prior reductions document.getElementById('reductionAmountGroup').style.display = 'none'; document.getElementById('reductionAmount').value = ''; // Reset luxury badge document.getElementById('luxuryBadge').style.display = 'none'; // Reset tips section document.getElementById('tipsContent').classList.remove('visible'); document.getElementById('tipsArrow').classList.remove('open'); // Reset step 1 button text document.getElementById('step1Btn').textContent = 'Analyze This Property →'; // Clear step 1 error styling step1Error.style.background = ''; step1Error.style.borderColor = ''; step1Error.style.color = ''; window.scrollTo({ top: 0, behavior: 'smooth' }); } // ═══════════════════════════════════════════════════════════════════════ // MODALS // ═══════════════════════════════════════════════════════════════════════ const modalContent = { terms: { title: "Terms of Service", content: `

1. Not an Appraisal or Valuation

OfferEdge provides market analysis and strategic recommendations based on user-submitted data. This is NOT an appraisal, broker price opinion, or formal property valuation. For official valuations, consult a licensed appraiser.

2. No Guarantee of Accuracy

Recommendations are based on historical market patterns and the information you provide. Real estate markets are unpredictable. OfferEdge does not guarantee that any property will or will not reduce in price, sell at any particular price, or perform as analyzed.

3. User Responsibility for Inputs

Analysis quality depends on accurate information. You are responsible for verifying the data you submit, including list price, days on market, and MLS number. OfferEdge is not liable for recommendations based on inaccurate, incomplete, or outdated inputs. All property data is provided by you, the user—we do not access MLS systems to retrieve this information.

4. Not a Substitute for Professional Advice

OfferEdge is an informational tool, not a replacement for professional real estate advice. Consult a licensed real estate professional before making buying, selling, or pricing decisions.

5. No Agency Relationship

Use of OfferEdge does not create an agency relationship, fiduciary duty, or representation agreement between you and The Cyr Team, Vincent Cyr Group LLC, Real Broker LLC, or any affiliated agent. If you are not currently represented and would like professional guidance, contact us to discuss representation.

6. For Represented Users

If you are currently under a Buyer Representation Agreement or Listing Agreement with another agent, this analysis is for informational purposes only. Please share and discuss any findings with your representing agent.

7. Limitation of Liability

IN NO EVENT SHALL VINCENT CYR GROUP LLC, THE CYR TEAM, OR THEIR AGENTS BE LIABLE FOR ANY DAMAGES ARISING FROM USE OF THIS TOOL. This includes direct, indirect, incidental, or consequential damages. Maximum liability is limited to $100 or the amount paid for the service, whichever is less.

8. Intellectual Property

OfferEdge and its algorithm are proprietary to Vincent Cyr Group LLC. Unauthorized reproduction or distribution is prohibited.

9. Data Storage

Your submission data is stored for accuracy tracking and service improvement. We may compare our analysis to actual market outcomes using public records to validate and improve our methodology. Your personal contact information is handled according to our Privacy Policy.

` }, privacy: { title: "Privacy Policy", content: `

Information We Collect

We collect the information you provide: name, email, phone (optional), and property details for analysis purposes.

How We Use It

Your information is used to generate your analysis and, with your consent, to follow up about the analysis or market updates.

We Don't Sell Your Data

We never sell, rent, or trade your personal information to third parties for marketing purposes.

Your Rights

You may request access to, correction of, or deletion of your personal data by contacting us. You may opt out of communications at any time.

Data Retention

We retain submission data for up to 3 years and consent records for 5 years for compliance purposes.

Contact

Questions? Contact The Cyr Team at privacy@thecyrteam.com

` } }; function showModal(type) { const content = modalContent[type]; document.getElementById('modalTitle').textContent = content.title; document.getElementById('modalContent').innerHTML = content.content; document.getElementById('modalOverlay').classList.add('visible'); document.body.style.overflow = 'hidden'; } function hideModal() { document.getElementById('modalOverlay').classList.remove('visible'); document.body.style.overflow = ''; } function closeModal(e) { if (e.target === document.getElementById('modalOverlay')) { hideModal(); } } // Close modal on escape document.addEventListener('keydown', function(e) { if (e.key === 'Escape') hideModal(); }); // Toggle likelihood key function toggleLikelihoodKey() { const content = document.getElementById('likelihoodKeyContent'); const toggle = document.querySelector('.likelihood-key__toggle'); if (content.style.display === 'none') { content.style.display = 'block'; toggle.textContent = 'What does this mean? ▲'; } else { content.style.display = 'none'; toggle.textContent = 'What does this mean? ▼'; } } // Toggle leverage/risk explanation function toggleLeverageRiskKey() { const content = document.getElementById('leverageRiskKeyContent'); const toggle = document.querySelector('.leverage-risk-key__toggle'); if (content.style.display === 'none') { content.style.display = 'block'; toggle.textContent = 'What do Leverage and Risk mean? ▲'; } else { content.style.display = 'none'; toggle.textContent = 'What do Leverage and Risk mean? ▼'; } } // Toggle sale type tips function toggleTips() { const content = document.getElementById('tipsContent'); const arrow = document.getElementById('tipsArrow'); content.classList.toggle('visible'); arrow.classList.toggle('open'); }