`;
} 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:
The Cyr Team's perspective for this property
Neighborhood context and benchmarks
A tailored strategy plan (buyer, seller, or agent)
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:
"I know it's been sitting, but the seller just indicated multiple offers. That changes things—we need to bring our best offer now."
"In a competitive situation, focus on winning—not extracting. Best offer first."
"Consider what makes your offer stand out beyond price: timeline, contingencies, earnest money."
"This property has been on market longer than typical—often a sign there may be room to negotiate, depending on feedback and seller timeline."
${hasOneReduction ? '"The seller has already adjusted once, which suggests they\'re responding to market signals."' : '"No price adjustments yet, so the seller is testing the market."'}
"I'd lead fair on price and win with clean terms rather than a low opener."
`;
} else {
talkTrack = `Client Talk Track:
"This property is fresh on market in a competitive area—expect limited negotiating room."
"Sellers in this position typically hold firm. Our strongest play is a clean, competitive offer."
"I'd recommend coming in at or near asking with minimal contingencies to be taken seriously."
`;
}
// Questions to ask (buyer client)
let questions = '';
if (hasMultipleOffersSignal) {
questions = `Questions to Ask (Listing Agent):
"What's the deadline for best and final?"
"Is the seller prioritizing price, terms, or timeline?"
"Will you be presenting all offers simultaneously?"
`;
} else if (isBackOnMarket) {
questions = `Questions to Ask (Listing Agent):
"Are there any terms the seller values most (settlement date, rent-back, inspection sensitivity)?"
"Any prior offers or a contract that fell apart?"
`;
}
// 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:
Call listing agent → confirm feedback, timeline, and preferred terms
Choose posture → fair price + clean terms; press harder only if intel supports it
Execute → showing + offer with a clear rationale
`;
} else {
nextStep = `Recommended Next Steps:
Call listing agent → gauge motivation, timeline, and any term preferences
Choose posture → fair price + clean terms; press harder only if intel supports it
"At ${dom} days on market, expect buyers to test the edges (inspection, credits, timeline). Our job is to reduce friction and be crisp on terms so we can protect price."
${hasOneReduction ? '"You\'ve already adjusted once, which signals responsiveness—use that to define clear boundaries now."' : '"We haven\'t adjusted price yet, so buyers will test our positioning. Stay crisp on concession boundaries."'}
"If the last 7–10 days show low activity or consistent price-driven feedback, consider one targeted adjustment (not a series of small cuts)."
`;
} else {
talkTrack = `Client Talk Track:
"Based on current market data, you're in a strong position. Buyers are likely to come in near asking."
"I'd recommend holding firm on price and evaluating any offers carefully before responding."
"Your property is well-positioned—there's no need to preemptively adjust."
`;
}
// Questions to explore (seller client) - practical focus
let questions = '';
if (domBucket === 'extended' || domBucket === 'stuck') {
questions = `Questions to Explore with Client:
"What's your timeline and must-net number?"
"What concession boundaries should we establish (inspection, credits, closing costs)?"
"What themes are we hearing from feedback—price, condition, terms, or showing friction?"
"Any closing or possession constraints we need to work around?"
`;
} else {
questions = `Questions to Explore with Client:
"What's your ideal timeline for closing?"
"Is there a floor price we should establish now?"
"What concession boundaries should we set (inspection, credits)?"
"Any possession or closing constraints?"
`;
}
// 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:
Review last 10–14 days of showing feedback + listing stats
Confirm whether feedback is price-driven or friction-driven
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.