Enter your deal details below. You get a Holdback Score out of 100, a payment reality check showing your true cost, and a clear breakdown of where your deal is strong and where there's money left.
Monthly payments are the dealer's preferred unit of deception. Here's the real math.
True APR, total interest over term, cost-per-month breakdown, and a 100-point Holdback Score.
Quick Check
The dealer quoted you a payment. Is it reasonable?
Enter the payment they quoted and we'll show you what's behind the number.
Ask about any deal
Describe the vehicle and price you were quoted. The tool checks it against MSRP data for 30 popular vehicles and gives you context.
Is $42,000 for a 2026 RAV4 XLE a good deal?
I was quoted $38,500 for a new Civic Touring
Is $62,000 reasonable for a 2026 F-150 Lariat?
Tesla Model Y for $58,000 in Ontario
1
Vehicle & Deal
2
Financing
3
Add-ons
Tell us about the vehicle & deal
Enter what the dealer quoted. If you're missing a number, leave it blank. We'll work with what you have.
The negotiated price for the vehicle itself. Not including HST, licensing, or admin fees.
Enter a selling price to continue
Where do I find this? ↓
The MSRP is on the window sticker of any new vehicle. For used vehicles, check the manufacturer’s website or AutoTrader for the original sticker price. If you can’t find it, leave this blank. Your score will still work.
Trade-in vs. private sale? ↓
Dealers typically offer 10–20% below private sale value. The advantage of a trade-in is HST savings: you only pay tax on the difference between your new vehicle price and trade-in value. Whether to trade or sell privately depends on your vehicle’s market value.
Amount you'll be financing
~$0
How are you paying?
Most buyers overpay here. A 1% rate difference on a $45,000 vehicle costs $900–$1,800 over the loan.
I don’t know my rate ↓
If the dealer only gave you a monthly payment, enter that below instead. We’ll calculate your implied interest rate, which is often higher than you’d expect. If you haven’t discussed rate yet, try checking with your bank or credit union for a pre-approval to use as a benchmark.
If you enter this AND a rate, we’ll cross-check for hidden fees rolled into your loan.
Most Ontario dealers charge $300–$900. Enter if known.
The price to buy the vehicle at end of lease. Found on your lease quote.
Cash purchases avoid financing markup entirely. We’ll still analyze your price, trade-in, and any add-ons offered.
Estimated payment
~$0/month
What add-ons were you offered?
Add-on markups range from 50% to 300%. Check everything you were presented with, and enter the quoted price if you have it.
Total add-on cost: $0 · Estimated dealer cost: $0–$0
Extended Warranty / Service PlanDealer cost: $300–$800 · Typically sold for $1,500–$4,000+
High margin
GAP InsuranceCovers write-off shortfall · Dealer cost ~$200, sold for $400–$1,200
High margin
Paint Protection / Ceramic CoatingDealer cost $150–$400 · Sold for $800–$2,500+
Very high margin
Fabric / Leather ProtectionScotchgard equivalent · Dealer cost $50–$150, sold for $300–$800
Highest margin
Rust Proofing / UndercoatModern vehicles have factory corrosion protection · Often unnecessary
Questionable value
Tire & Wheel ProtectionRoad hazard coverage · Dealer cost $200–$400, sold for $700–$1,800
High margin
Credit / Life Insurance on LoanAlmost always overpriced vs. term life insurance purchased independently
Very high margin
Did the dealer quote an admin fee? Most charge $300–$900. You can go back to Step 2 to enter it.
Your Holdback Score Is Ready
Enter your email to see your full analysis: Holdback Score, payment reality check, financing breakdown, and negotiation tips.
By submitting, you agree to receive your analysis plus occasional emails from Holdback (Toronto, Ontario) with car-buying tips. No dealer sharing, ever. Unsubscribe any time. See our Privacy Policy.
A Holdback consultation goes deeper. We review your specific deal line by line, give you exact negotiation language, and tell you what to take and what to decline in the finance office.
Flat fee · Remote via Zoom · Evening & weekend slots available
Compare
Got another quote? Compare side by side.
Enter the key numbers from the second dealer and see which deal is stronger.
How We Calculate Your Score
Your score starts at a base of 30 and is adjusted across three categories, totalling up to 100 points.
Price (up to 35 points)
Compares your negotiated price to MSRP. New vehicles: we look for 2–5%+ below sticker. Used vehicles: we flag pricing relative to original MSRP but note that market comps are needed for a complete picture.
Financing (up to 35 points)
Compares your rate to the current Ontario market benchmark (). Penalizes terms of 72+ months due to interest cost and depreciation risk.
Add-ons (up to 30 points)
Scores based on total add-on cost, weighted by quoted prices when available. Dealer cost on these products is typically 15–25% of retail.
About this analysis: Holdback Scores are estimates based on Ontario market averages and publicly available pricing data. They are educational tools, not financial or legal advice. Actual dealer margins, regional conditions, manufacturer incentives, and individual credit profiles vary. For a deal-specific analysis using your exact numbers, book a Holdback consultation.
Common Questions
Straight Answers
The score benchmarks your deal against Ontario market averages for pricing, financing rates, and add-on costs. It flags where your deal has room to improve: pricing relative to MSRP, rate relative to market, and add-on margins. It does not replace a deal-specific consultation, but if the score is low, there is almost certainly money on the table.
No. You can enter estimated numbers to see how a potential deal stacks up. The analysis is most useful when you have actual figures from a dealer: a quoted price, interest rate, or list of offered add-ons. Not sure what the numbers on your quote mean? Learn how to read a dealer quote before entering your details.
We use your email to deliver your complete deal analysis and negotiation tips. We never share it with dealers, third parties, or use it for unrelated marketing. See the Privacy Policy for details.
A low score means the dealer likely has significant margin in one or more areas: price, financing rate, or add-on products. The detailed breakdown identifies which areas to push back on. A Deal Review ($147) or Buyer’s Brief ($297) gives you the specific language and strategy to address each one before you sign.
Want Expert Eyes on Your Deal?
The Deal Analyzer flags where you may be leaving money on the table. A Holdback consultation tells you exactly how much, and gives you the strategy to get it back.
';
}
}
document.getElementById('leaseStats').innerHTML = statsHtml;
// Buyout analysis
var insightBox = document.getElementById('leaseInsightBox');
var insight = document.getElementById('leaseInsight');
if (hasResidual) {
var totalIfBuyout = totalLeaseCost + state.residualValue;
var premium = totalIfBuyout - state.sellingPrice;
insight.innerHTML = 'If you lease for ' + state.leaseTerm + ' months and then buy out at the residual of $' + fmt(state.residualValue) + ', your total cost to own would be $' + fmt(totalIfBuyout) + '. That is $' + fmt(premium) + ' more than buying outright at the current selling price. This premium covers the flexibility of the lease, but it\'s worth knowing.';
insightBox.style.display = 'block';
} else {
insightBox.style.display = 'none';
}
leaseCard.style.display = 'block';
}
/* ── Render back-calculation insights ── */
function renderBackCalc() {
var bcCard = document.getElementById('backCalcCard');
var bcText = document.getElementById('backCalcText');
var parts = [];
if (state.paymentType === 'finance' && state.sellingPrice > 0 && state.loanTerm > 0) {
var taxableAmt = Math.max(0, state.sellingPrice - state.tradeValue);
var hstAmt = taxableAmt * 0.13;
var totalCost = state.sellingPrice + hstAmt;
var princ = totalCost - state.downPayment - state.tradeValue;
if (princ > 0) {
// Case A: monthly payment but no rate
if (state.monthlyPayment > 0 && state.interestRate <= 0 && state.impliedRate > 0) {
var benchmarkRate = getBenchmark(state.vehicleType);
var benchLabel = state.vehicleType === 'new' ? 'new' : 'used';
var impliedCalc = calcAmort(princ, state.impliedRate, state.loanTerm);
var benchCalc = calcAmort(princ, benchmarkRate, state.loanTerm);
var extraInterest = impliedCalc.interest - benchCalc.interest;
parts.push('Your quoted payment of $' + fmt(state.monthlyPayment) + '/month over ' + state.loanTerm + ' months implies a rate of approximately ' + state.impliedRate.toFixed(1) + '%. The Ontario market benchmark for ' + benchLabel + ' vehicle financing is ' + benchmarkRate + '%.' + (extraInterest > 100 ? ' At that implied rate, you would pay $' + fmt(extraInterest) + ' more in interest than at benchmark.' : ''));
}
// Case B: both monthly payment AND rate
if (state.monthlyPayment > 0 && state.interestRate > 0) {
var expectedCalc = calcAmort(princ, state.interestRate, state.loanTerm);
var paymentDiff = state.monthlyPayment - expectedCalc.payment;
if (paymentDiff > 20) {
var overTerm = paymentDiff * state.loanTerm;
parts.push('Based on your rate of ' + state.interestRate + '% and term of ' + state.loanTerm + ' months, your monthly payment should be approximately $' + fmt(expectedCalc.payment) + '. The dealer quoted $' + fmt(state.monthlyPayment) + '. That is $' + fmt(paymentDiff) + '/month higher, or $' + fmt(overTerm) + ' over the loan. The difference is likely add-ons or fees rolled into the financed amount.');
}
}
}
}
// Case C: trade-in HST insight
if (state.tradeValue > 0 && state.sellingPrice > 0) {
var hstSavings = state.tradeValue * 0.13;
var netAmount = state.sellingPrice - state.tradeValue;
parts.push('Your trade-in of $' + fmt(state.tradeValue) + ' saves you $' + fmt(hstSavings) + ' in Ontario HST versus selling privately. At a dealer, HST is charged on the net amount ($' + fmt(state.sellingPrice) + ' - $' + fmt(state.tradeValue) + ' = $' + fmt(netAmount) + '). If you sold privately, you\'d pay HST on the full $' + fmt(state.sellingPrice) + '.');
}
if (parts.length > 0) {
bcText.innerHTML = parts.join('
');
bcCard.style.display = 'block';
} else {
bcCard.style.display = 'none';
}
}
/* ── Render breakdown cards (all ungated) ── */
function renderBreakdowns(sc) {
// PRICE
if (state.sellingPrice > 0 && state.msrp > 0) {
var diff = state.msrp - state.sellingPrice;
var pct = ((diff / state.msrp) * 100).toFixed(1);
if (state.vehicleType === 'new') {
if (diff > 0) {
var priceMsg = 'You negotiated $' + esc(fmt(diff)) + ' (' + esc(pct) + '%) below MSRP. For a new vehicle in Ontario, anything above 3% is solid. ';
if (parseFloat(pct) >= 5) {
priceMsg += 'This is a strong price result.';
} else {
priceMsg += 'There may still be room. A consultation can confirm using live incentive data.';
}
setBreakdown('price', '#059669', 'badge-green', 'Below MSRP', priceMsg, Math.min(95, 40 + parseFloat(pct) * 8));
} else if (diff === 0) {
setBreakdown('price', 'var(--warn-ink)', 'badge-yellow', 'Full MSRP', 'You\'re paying full MSRP with no discount. For most new vehicles in this market, there is room below sticker on price or through manufacturer incentives you may not be using.', 50);
} else {
setBreakdown('price', 'var(--danger-deep)', 'badge-red', 'Above MSRP', 'You\'re being asked to pay $' + esc(fmt(Math.abs(diff))) + ' above MSRP. Unless this vehicle has genuinely zero market alternatives, this is either negotiable or you should be comparing multiple dealers.', 20);
}
} else {
if (diff > 0) {
setBreakdown('price', '#059669', 'badge-green', 'Below Original MSRP', 'The selling price is $' + esc(fmt(diff)) + ' (' + esc(pct) + '%) below the original MSRP. For a used vehicle, the relationship to original MSRP is only one factor. Market value depends on age, mileage, condition, and local supply. Getting current market comps would give a more complete picture.', Math.min(85, 30 + parseFloat(pct) * 3));
} else {
setBreakdown('price', 'var(--danger-deep)', 'badge-red', 'At or Above Original MSRP', 'The selling price is at or above the original MSRP for a used vehicle. Unless this is a rare or in-demand model with very low mileage, this warrants investigation. Getting current market comps would help confirm.', 20);
}
}
} else if (state.sellingPrice > 0) {
var noMsrpMsg = '';
if (state.vehicleType === 'used') {
noMsrpMsg = 'No MSRP to compare against, so we can\'t assess pricing relative to original sticker. For used vehicles, market comps matter more than MSRP. Check AutoTrader, CarGurus, or a Holdback consultation for current market pricing on your specific vehicle.';
} else {
noMsrpMsg = 'We didn\'t have an MSRP to compare against. Enter the MSRP for a full price analysis, or a Holdback consultation pulls current market pricing for your specific vehicle and trim.';
}
setBreakdown('price', '#7c8a98', 'badge-grey', 'Needs Data', noMsrpMsg, 35);
} else {
setBreakdown('price', '#7c8a98', 'badge-grey', 'Needs Data', 'No pricing data was entered. Go back and enter at least a selling price for a price analysis.', 10);
}
// FINANCE
var rateToUse = state.interestRate;
if (rateToUse <= 0 && state.impliedRate > 0) rateToUse = state.impliedRate;
if (state.paymentType === 'cash') {
setBreakdown('finance', '#059669', 'badge-green', 'Cash Purchase', 'Cash purchases avoid financing markup entirely. Make sure you negotiated on the out-the-door price, not a "cash discount." Dealers sometimes add fees to cash deals to recoup lost finance profit.', 90);
} else if (state.paymentType === 'lease') {
if (state.impliedLeaseAPR > 0) {
var leaseBench = getLeaseBenchmark(state.vehicleType);
var leaseDiff = state.impliedLeaseAPR - leaseBench;
if (leaseDiff <= 0) {
setBreakdown('finance', '#059669', 'badge-green', 'Strong Lease Rate', 'Your lease implies an equivalent APR of ' + state.impliedLeaseAPR.toFixed(2) + '% (money factor: ' + state.moneyFactor.toFixed(5) + '). This is at or below the ' + leaseBench + '% benchmark. The rate side of your lease looks solid.', 80);
} else if (leaseDiff <= 2) {
setBreakdown('finance', 'var(--warn-ink)', 'badge-yellow', 'Lease Rate Above Benchmark', 'Your lease implies an equivalent APR of ' + state.impliedLeaseAPR.toFixed(2) + '% (money factor: ' + state.moneyFactor.toFixed(5) + '). The benchmark is ' + leaseBench + '%. Dealers can mark up the money factor, and that markup is not always disclosed. Ask the dealer for the base (buy) rate from the manufacturer.', 50);
} else {
setBreakdown('finance', 'var(--danger-deep)', 'badge-red', 'Lease Rate Well Above Benchmark', 'Your lease implies an equivalent APR of ' + state.impliedLeaseAPR.toFixed(2) + '% (money factor: ' + state.moneyFactor.toFixed(5) + '). The benchmark is ' + leaseBench + '%. This gap suggests significant money factor markup. Ask the dealer what the manufacturer base rate is and negotiate from there. Do not sign this lease without an independent review.', 20);
}
} else {
setBreakdown('finance', '#7c8a98', 'badge-grey', 'Lease (Incomplete Data)', 'Enter your monthly payment, selling price, residual value, and lease term to get a full lease rate analysis. Without these numbers, we cannot calculate the implied money factor.', 50);
}
} else if (rateToUse > 0) {
var benchmark = getBenchmark(state.vehicleType);
var rateDiff = rateToUse - benchmark;
var termForCalc = state.loanTerm || 60;
var princForCalc = Math.max(0, state.sellingPrice + (Math.max(0, state.sellingPrice - state.tradeValue) * 0.13) - state.downPayment - state.tradeValue);
var extraCost = Math.round(princForCalc * (rateDiff / 100) * (termForCalc / 12));
var rateLabel = (state.interestRate > 0) ? state.interestRate : state.impliedRate;
var rateNote = (state.interestRate <= 0 && state.impliedRate > 0) ? ' (implied from your quoted payment)' : '';
var benchSource = ' Ontario market benchmark as of ' + BENCHMARKS.lastUpdated + '.';
// Include admin fee in analysis
var adminNote = '';
if (state.adminFee > 0) {
adminNote = ' The dealer admin fee of $' + esc(fmt(state.adminFee)) + ' is included in the financed amount.';
}
if (rateDiff <= 0) {
setBreakdown('finance', '#059669', 'badge-green', 'Competitive Rate', 'Your rate of ' + esc(rateLabel.toString()) + '%' + rateNote + ' is at or below the current Ontario market benchmark of ' + benchmark + '% for ' + (state.vehicleType === 'new' ? 'new' : 'used') + ' vehicles.' + benchSource + (state.loanTerm >= 84 ? ' Note: An 84+ month term significantly increases total interest cost and depreciation risk. Consider whether a shorter term is possible at a similar payment.' : ' The rate is solid.') + adminNote, 80);
} else if (rateDiff <= 2) {
setBreakdown('finance', 'var(--warn-ink)', 'badge-yellow', 'Rate Has Room', 'Your rate of ' + esc(rateLabel.toString()) + '%' + rateNote + ' is approximately ' + esc(rateDiff.toFixed(2)) + '% above the ' + (state.vehicleType === 'new' ? 'new' : 'used') + ' vehicle benchmark' + (extraCost > 0 ? ', an estimated $' + esc(fmt(extraCost)) + ' in additional interest over your term' : '') + '.' + benchSource + ' This gap is negotiable. Dealers mark up the bank\'s rate by 1 to 3%. A Holdback consultation shows you what rate you likely qualify for.' + adminNote, 50);
} else {
setBreakdown('finance', 'var(--danger-deep)', 'badge-red', 'Rate Is High', 'Your rate of ' + esc(rateLabel.toString()) + '%' + rateNote + ' is ' + esc(rateDiff.toFixed(2)) + '% above the ' + (state.vehicleType === 'new' ? 'new' : 'used') + ' vehicle benchmark' + (extraCost > 0 ? ', approximately $' + esc(fmt(Math.abs(extraCost))) + ' in extra interest over your term' : '') + '.' + benchSource + ' Unless your credit score is below 640, this rate has significant room to move. A consultation targets this directly.' + adminNote, 25);
}
} else {
setBreakdown('finance', '#7c8a98', 'badge-grey', 'Rate Unknown', 'Enter your interest rate or monthly payment on Step 2 for a full financing analysis. If the dealer only gave you a monthly payment without a rate, that\'s worth investigating before signing.', 35);
}
// ADD-ONS
var ac = state.addons.length;
if (ac === 0) {
setBreakdown('addon', '#059669', 'badge-green', 'No Add-ons Checked', 'You either weren\'t offered add-ons or haven\'t reached that stage yet. If you\'re buying new, the finance manager will present these products when you sit down to sign. A Holdback consultation prepares you for what to expect and what to decline.');
} else {
var total = 0;
var hasQuotedPrices = false;
var dealerCostLowTotal = 0;
var dealerCostHighTotal = 0;
for (var i = 0; i < state.addons.length; i++) {
var aid = state.addons[i];
var quotedPrice = state.addonPrices[aid];
if (quotedPrice > 0) {
total += quotedPrice;
hasQuotedPrices = true;
} else {
total += addonData[aid].defaultCost;
}
dealerCostLowTotal += addonData[aid].dealerCostLow;
dealerCostHighTotal += addonData[aid].dealerCostHigh;
}
var badgeC = ac >= 3 ? 'badge-red' : 'badge-yellow';
var ic = ac >= 3 ? 'var(--danger-deep)' : 'var(--warn-ink)';
var addonMsg = 'You were offered ' + esc(ac.toString()) + ' add-on product' + (ac > 1 ? 's' : '') + ' with a combined ' + (hasQuotedPrices ? 'quoted' : 'estimated retail') + ' value of approximately $' + esc(fmt(total)) + '. The dealer\'s cost on these items is typically $' + esc(fmt(dealerCostLowTotal)) + '\u2013$' + esc(fmt(dealerCostHighTotal)) + '. Some may be worth keeping depending on your situation. A Holdback consultation reviews each one and gives you the language to decline the rest.';
setBreakdown('addon', ic, badgeC, ac + ' Add-on' + (ac > 1 ? 's' : '') + ' Offered', addonMsg);
// Per-addon breakdown table
var addonBreakdownWrap = document.getElementById('addonBreakdownWrap');
if (hasQuotedPrices) {
var tableHtml = '
Product
Quoted
Est. Dealer Cost
Markup
';
for (var j = 0; j < state.addons.length; j++) {
var addonId = state.addons[j];
var price = state.addonPrices[addonId];
if (price > 0) {
var dLow = addonData[addonId].dealerCostLow;
var dHigh = addonData[addonId].dealerCostHigh;
var markupLow = price > 0 && dHigh > 0 ? Math.round(price / dHigh) : 0;
var markupHigh = price > 0 && dLow > 0 ? Math.round(price / dLow) : 0;
tableHtml += '
' + esc(addonData[addonId].label) + '
$' + fmt(price) + '
$' + fmt(dLow) + '\u2013$' + fmt(dHigh) + '
~' + markupLow + '-' + markupHigh + 'x
';
}
}
tableHtml += '
';
addonBreakdownWrap.innerHTML = tableHtml;
} else {
addonBreakdownWrap.innerHTML = '';
}
}
}
/* ── Render negotiation playbook (gated) ── */
function renderNegotiationPlaybook(sc) {
var talkingPoints = document.getElementById('talkingPoints');
var html = '';
// Price talking point
if (sc.priceScore < sc.priceMax * 0.5) {
html += '
What to say about the price
"I\'ve done my research and comparable ' + (state.vehicleDesc ? esc(state.vehicleDesc) + 's' : 'vehicles') + ' in the Canadian market are listing for less. I\'d like to discuss how we can get closer to a competitive range."
';
}
// Rate talking point
if (sc.financeScore < sc.financeMax * 0.5 && state.paymentType === 'finance') {
html += '
What to say about the rate
"I\'d like to see the buy rate from the lender before any dealer markup. I\'m also getting quotes from my bank and credit union to compare."
';
}
// Add-on talking point
if (sc.addonScore < sc.addonMax * 0.5) {
html += '
What to say at the finance desk
"I\'d like to see each product individually with the cost separated from the vehicle financing. I\'ll review them and let you know which ones I\'d like to include."
';
}
// Always include general tip
html += '
General tip
"I\'m not signing today. I\'d like to take the paperwork home to review it."
';
talkingPoints.innerHTML = html;
// CTA card (dynamic based on weakest area)
var ctaH, ctaB;
var weakestArea = 'price';
var weakestPct = sc.priceMax > 0 ? sc.priceScore / sc.priceMax : 1;
var financePct = sc.financeMax > 0 ? sc.financeScore / sc.financeMax : 1;
var addonPct = sc.addonMax > 0 ? sc.addonScore / sc.addonMax : 1;
if (financePct < weakestPct) { weakestArea = 'financing'; weakestPct = financePct; }
if (addonPct < weakestPct) { weakestArea = 'addons'; }
if (sc.total >= 80) {
ctaH = 'Confirm before you commit.';
ctaB = 'A Deal Review ($147) validates your numbers and catches anything hidden in the fine print before you sign.';
} else if (sc.total >= 65) {
if (weakestArea === 'financing') {
ctaH = 'Your rate is the biggest opportunity here.';
} else if (weakestArea === 'addons') {
ctaH = 'The finance office products are where you\'re leaving money.';
} else {
ctaH = 'There\'s money left on the table.';
}
ctaB = 'A Buyer\'s Brief ($297) identifies every dollar of avoidable cost and gives you the exact language to address each one with the dealer.';
} else if (sc.total >= 50) {
ctaH = 'This deal has serious flags.';
ctaB = 'A Buyer\'s Brief ($297) covers the specific issues flagged above and gives you a game plan for the conversation with the dealer.';
} else {
ctaH = 'Do not sign this deal yet.';
ctaB = 'A Buyer\'s Brief ($297) identifies where this deal is structured against you and gives you the language and strategy to fix it before you commit.';
}
document.getElementById('ctaHeadline').textContent = ctaH;
document.getElementById('ctaBody').textContent = ctaB;
}
/* ── Email gate submission ── */
function submitGate() {
var email = document.getElementById('gateEmail').value.trim();
var name = document.getElementById('gateFirstName').value.trim();
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) {
var f = document.getElementById('gateEmail');
f.classList.add('error');
f.placeholder = 'Please enter a valid email';
return;
}
// Send lead data
var sc = score();
var hbUtms = (window.Holdback && window.Holdback.getUTMs) ? window.Holdback.getUTMs() : {};
fetch('/api/lead', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.assign({
email: email,
name: name,
source: 'Deal Analyzer',
score: sc.total,
vehicle: state.vehicleDesc,
vehicleType: state.vehicleType,
sellingPrice: state.sellingPrice,
msrp: state.msrp,
paymentType: state.paymentType,
tradeValue: state.tradeValue,
downPayment: state.downPayment,
monthlyPayment: state.monthlyPayment,
loanTerm: state.loanTerm,
interestRate: state.interestRate,
paymentFrequency: state.paymentFrequency,
adminFee: state.adminFee,
leasePayment: state.leasePayment,
leaseTerm: state.leaseTerm,
residualValue: state.residualValue,
addons: state.addons,
addonPrices: state.addonPrices,
_hp: (document.getElementById('confirmAnalysis') || {}).value || '',
_t0: window.__holdbackPageT0 || 0
}, hbUtms))
}).then(function(res) {
if (!res || !res.ok) throw new Error();
}).catch(function() {
var gate = document.getElementById('emailGate');
if (gate) {
var notice = document.createElement('p');
notice.style.cssText = 'font-size: var(--fs-sm);color:var(--danger);margin-top:10px;';
notice.textContent = 'Your results are ready, but we couldn\u2019t save your email. Reach us at hello@holdback.ca';
gate.appendChild(notice);
}
});
trackEvent('da_email_submitted', {});
// Reveal all results
revealResults();
}
/* ── Change My Numbers ── */
function changeNumbers() {
// Hide results and email gate
document.getElementById('resultsSection').classList.remove('active');
document.getElementById('emailGateSection').classList.remove('active');
document.getElementById('emailGateSection').style.display = '';
// Show hero, progress, form
document.getElementById('pageHero').style.display = '';
document.getElementById('progressContainer').style.display = '';
document.getElementById('formContainer').style.display = '';
// Navigate to step 1
goToStep(1, true);
// Repopulate form
populateFormFromState();
trackEvent('da_numbers_changed', {});
window.scrollTo({ top: 0, behavior: 'smooth' });
}
/* ── Addon checkbox toggle ── */
function setupAddonToggles() {
for (var i = 0; i < addonIds.length; i++) {
(function(id) {
var checkbox = document.getElementById('addon_' + id);
var priceRow = document.getElementById('addonPriceRow_' + id);
var item = checkbox.closest('.addon-item');
checkbox.addEventListener('change', function() {
priceRow.style.display = this.checked ? 'block' : 'none';
if (this.checked) {
item.classList.add('checked');
} else {
item.classList.remove('checked');
}
updateAddonTotal();
});
// Addon price input change
var priceInput = document.getElementById('addonPrice_' + id);
priceInput.addEventListener('input', function() {
updateAddonTotal();
});
// Setup dollar formatting for addon price inputs
setupDollarInput('addonPrice_' + id);
})(addonIds[i]);
}
}
/* ── FAQ accordion ── */
function setupFaq() {
var faqItems = document.querySelectorAll('.faq-question');
for (var i = 0; i < faqItems.length; i++) {
(function(item) {
item.addEventListener('click', function() {
var parent = this.parentElement;
var isActive = parent.classList.contains('active');
var allItems = document.querySelectorAll('.faq-item');
for (var j = 0; j < allItems.length; j++) {
allItems[j].classList.remove('active');
allItems[j].querySelector('.faq-icon').textContent = '+';
allItems[j].querySelector('.faq-question').setAttribute('aria-expanded', 'false');
}
if (!isActive) {
parent.classList.add('active');
this.querySelector('.faq-icon').textContent = '\u2212';
this.setAttribute('aria-expanded', 'true');
}
});
item.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
});
})(faqItems[i]);
}
}
/* ── Event listeners ── */
// Toggle buttons (vehicle type, payment type, payment frequency)
var toggleBtns = document.querySelectorAll('.type-toggle [data-field][data-value]');
for (var i = 0; i < toggleBtns.length; i++) {
(function(btn) {
btn.addEventListener('click', function() {
selectToggle(this.getAttribute('data-field'), this.getAttribute('data-value'), this);
});
})(toggleBtns[i]);
}
// Term button group
var termBtns = document.querySelectorAll('.term-btn');
for (var t = 0; t < termBtns.length; t++) {
(function(btn) {
btn.addEventListener('click', function() {
for (var j = 0; j < termBtns.length; j++) {
termBtns[j].classList.remove('selected');
}
this.classList.add('selected');
document.getElementById('loanTerm').value = this.getAttribute('data-term');
state.loanTerm = parseInt(this.getAttribute('data-term'), 10);
updateStep2Preview();
});
})(termBtns[t]);
}
// Info-tip toggle links
var tipLinks = document.querySelectorAll('[data-tip]');
for (var k = 0; k < tipLinks.length; k++) {
(function(link) {
link.addEventListener('click', function() {
toggleTip(this.getAttribute('data-tip'));
});
})(tipLinks[k]);
}
// Step navigation
document.getElementById('btnNextStep2').addEventListener('click', function() { goToStep(2); });
document.getElementById('btnNextStep3').addEventListener('click', function() { goToStep(3); });
document.getElementById('btnBackStep1').addEventListener('click', function() { goToStep(1); });
document.getElementById('btnBackStep2').addEventListener('click', function() { goToStep(2); });
// Clear selling price error on input
document.getElementById('sellingPrice').addEventListener('input', function() {
this.classList.remove('error');
document.getElementById('sellingPriceError').classList.remove('visible');
updateStep1Preview();
});
// Live preview updates for step 1
document.getElementById('tradeValue').addEventListener('input', updateStep1Preview);
document.getElementById('downPayment').addEventListener('input', updateStep1Preview);
// Live preview updates for step 2
document.getElementById('interestRate').addEventListener('input', updateStep2Preview);
// Show results
document.getElementById('btnShowScore').addEventListener('click', function() { showEmailGate(); });
// Email gate submit
document.getElementById('btnSubmitGate').addEventListener('click', function() { submitGate(); });
// Allow Enter key in email field to submit
document.getElementById('gateEmail').addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
submitGate();
}
});
// Change my numbers
document.getElementById('btnChangeNumbers').addEventListener('click', function() { changeNumbers(); });
document.getElementById('btnChangeTop').addEventListener('click', function() { changeNumbers(); });
document.getElementById('btnPrintDeal').addEventListener('click', function() { window.print(); });
// Share deal link
document.getElementById('btnShareDeal').addEventListener('click', function() {
var p = new URLSearchParams();
if (state.vehicleType) p.set('type', state.vehicleType);
if (state.vehicleDesc) p.set('vehicle', state.vehicleDesc);
if (state.sellingPrice > 0) p.set('sp', state.sellingPrice);
if (state.msrp > 0) p.set('msrp', state.msrp);
if (state.tradeValue > 0) p.set('tv', state.tradeValue);
if (state.downPayment > 0) p.set('dp', state.downPayment);
if (state.paymentType !== 'finance') p.set('pt', state.paymentType);
if (state.interestRate > 0) p.set('rate', state.interestRate);
if (state.loanTerm > 0) p.set('term', state.loanTerm);
if (state.paymentFrequency !== 'monthly') p.set('freq', state.paymentFrequency);
if (state.monthlyPayment > 0) p.set('pmt', state.monthlyPayment);
if (state.adminFee > 0) p.set('af', state.adminFee);
if (state.leasePayment > 0) p.set('lp', state.leasePayment);
if (state.leaseTerm > 0) p.set('lt', state.leaseTerm);
if (state.residualValue > 0) p.set('rv', state.residualValue);
if (state.addons.length > 0) p.set('addons', state.addons.join(','));
var url = window.location.origin + window.location.pathname + '?' + p.toString();
navigator.clipboard.writeText(url).then(function() {
var el = document.getElementById('shareDealCopied');
el.style.display = 'inline';
setTimeout(function() { el.style.display = 'none'; }, 2000);
});
});
// CTA tracking
document.getElementById('ctaButton').addEventListener('click', function() {
trackEvent('da_cta_clicked', {});
});
// Methodology link smooth scroll
document.getElementById('methodologyLink').addEventListener('click', function(e) {
e.preventDefault();
var target = document.getElementById('methodology');
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
// Save state on input blur for all fields
var allInputs = document.querySelectorAll('input, select');
for (var b = 0; b < allInputs.length; b++) {
allInputs[b].addEventListener('blur', function() {
collectStep(currentStep);
});
}
// Setup dollar input formatting
setupDollarInput('sellingPrice');
setupDollarInput('msrp');
setupDollarInput('tradeValue');
setupDollarInput('downPayment');
setupDollarInput('monthlyPayment');
setupDollarInput('adminFee');
setupDollarInput('leasePayment');
setupDollarInput('residualValue');
// Setup addon toggles and FAQ
setupAddonToggles();
setupFaq();
// Populate benchmark rates in methodology section
var methodRates = document.getElementById('methodologyRates');
if (methodRates) methodRates.textContent = BENCHMARKS.newRate + '% for new, ' + BENCHMARKS.usedRate + '% for used as of ' + BENCHMARKS.lastUpdated;
// Restore state from session storage
restoreState();
// Load from URL parameters (shared deal links)
function loadDealFromURL() {
var p = new URLSearchParams(window.location.search);
if (!p.has('sp')) return;
if (p.has('type')) state.vehicleType = p.get('type');
if (p.has('vehicle')) state.vehicleDesc = p.get('vehicle');
if (p.has('sp')) state.sellingPrice = parseInt(p.get('sp'), 10);
if (p.has('msrp')) state.msrp = parseInt(p.get('msrp'), 10);
if (p.has('tv')) state.tradeValue = parseInt(p.get('tv'), 10);
if (p.has('dp')) state.downPayment = parseInt(p.get('dp'), 10);
if (p.has('pt')) state.paymentType = p.get('pt');
if (p.has('rate')) state.interestRate = parseFloat(p.get('rate'));
if (p.has('term')) state.loanTerm = parseInt(p.get('term'), 10);
if (p.has('freq')) state.paymentFrequency = p.get('freq');
if (p.has('pmt')) state.monthlyPayment = parseInt(p.get('pmt'), 10);
if (p.has('af')) state.adminFee = parseInt(p.get('af'), 10);
if (p.has('lp')) state.leasePayment = parseInt(p.get('lp'), 10);
if (p.has('lt')) state.leaseTerm = parseInt(p.get('lt'), 10);
if (p.has('rv')) state.residualValue = parseInt(p.get('rv'), 10);
if (p.has('addons')) state.addons = p.get('addons').split(',');
// Populate form fields from state
restoreState();
// Hide quick check, skip to full analysis
document.getElementById('quickCheckContainer').style.display = 'none';
}
loadDealFromURL();
// GA4: deal analyzer start
// --- INCOME AFFORDABILITY ---
setupDollarInput('incomeInput');
document.getElementById('incomeInput').addEventListener('input', function() {
// Re-render payment reality to update affordability
if (document.getElementById('resultsSection').classList.contains('active')) {
renderPaymentReality();
}
});
// --- DEAL COMPARISON ---
setupDollarInput('cmpPrice');
setupDollarInput('cmpPayment');
document.getElementById('btnCompare').addEventListener('click', function() {
var cmpPrice = getDollarValue('cmpPrice');
if (cmpPrice <= 0) {
document.getElementById('cmpPrice').classList.add('error');
return;
}
document.getElementById('cmpPrice').classList.remove('error');
var cmpRate = parseFloat(document.getElementById('cmpRate').value) || 0;
var cmpTerm = parseInt(document.getElementById('cmpTerm').value, 10) || state.loanTerm || 60;
var cmpPmt = getDollarValue('cmpPayment');
// Calculate Deal B
var cmpHst = Math.max(0, cmpPrice - state.tradeValue) * 0.13;
var cmpPrincipal = (cmpPrice + cmpHst + state.adminFee) - state.tradeValue - state.downPayment;
if (cmpPrincipal <= 0) cmpPrincipal = 1;
var cmpImplied = 0;
if (cmpPmt > 0 && cmpRate <= 0) {
// Solve for implied rate
var lo = 0, hi = 30;
for (var i = 0; i < 50; i++) {
var mid = (lo + hi) / 2;
var mr = mid / 12 / 100;
var calc = mr === 0 ? cmpPrincipal / cmpTerm : cmpPrincipal * (mr * Math.pow(1 + mr, cmpTerm)) / (Math.pow(1 + mr, cmpTerm) - 1);
if (calc < cmpPmt) lo = mid; else hi = mid;
}
cmpImplied = Math.round((lo + hi) / 2 * 100) / 100;
cmpRate = cmpImplied;
}
var cmpRes = cmpRate > 0 ? calcAmort(cmpPrincipal, cmpRate, cmpTerm) : { payment: cmpPrincipal / cmpTerm, totalPaid: cmpPrincipal, totalInterest: 0 };
var cmpMonthly = cmpPmt > 0 ? cmpPmt : cmpRes.payment;
// Deal A values
var aHst = Math.max(0, state.sellingPrice - state.tradeValue) * 0.13;
var aPrincipal = (state.sellingPrice + aHst + state.adminFee) - state.tradeValue - state.downPayment;
var aRate = state.interestRate > 0 ? state.interestRate : (state.impliedRate > 0 ? state.impliedRate : 0);
var aTerm = state.loanTerm || 60;
var aRes = aRate > 0 ? calcAmort(aPrincipal, aRate, aTerm) : { payment: aPrincipal / aTerm, totalPaid: aPrincipal, totalInterest: 0 };
var aMonthly = state.monthlyPayment > 0 ? state.monthlyPayment : aRes.payment;
var benchmark = getBenchmark(state.vehicleType);
// Render comparison
function winner(a, b, lowerBetter) {
if (a === b) return ['', ''];
if (lowerBetter) return a < b ? ['color:var(--success);font-weight:700;', 'color: var(--danger-deep);'] : ['color: var(--danger-deep);', 'color:var(--success);font-weight:700;'];
return a > b ? ['color:var(--success);font-weight:700;', 'color: var(--danger-deep);'] : ['color: var(--danger-deep);', 'color:var(--success);font-weight:700;'];
}
function row(label, aVal, bVal, fmt2, lowerBetter) {
var w = winner(parseFloat(aVal) || 0, parseFloat(bVal) || 0, lowerBetter);
var aStr = fmt2 ? fmt2(aVal) : aVal;
var bStr = fmt2 ? fmt2(bVal) : bVal;
return '
' + label + '
' +
'
' + aStr + '
' +
'
' + bStr + '
';
}
function fmtD(v) { return '$' + Math.round(v).toLocaleString('en-CA'); }
function fmtP(v) { return v.toFixed(2) + '%'; }
var html = '
';
html += '
';
html += '
';
html += '
Deal A (yours)
';
html += '
Deal B
';
html += '
';
html += row('Selling Price', state.sellingPrice, cmpPrice, fmtD, true);
html += row('Interest Rate', aRate, cmpRate, fmtP, true);
html += row('Term', aTerm, cmpTerm, function(v) { return v + ' mo'; }, true);
html += row('Monthly Payment', aMonthly, cmpMonthly, fmtD, true);
html += row('Total Interest', aRes.totalInterest, cmpRes.totalInterest, fmtD, true);
html += row('Total Paid', aRes.totalPaid, cmpRes.totalPaid, fmtD, true);
html += '
';
// Summary
var diff = cmpRes.totalPaid - aRes.totalPaid;
if (diff > 0) {
html += '
Deal A saves you ' + fmtD(Math.abs(diff)) + ' over the life of the loan compared to Deal B.
';
} else if (diff < 0) {
html += '
Deal B saves you ' + fmtD(Math.abs(diff)) + ' over the life of the loan. Consider negotiating Deal A closer to these terms.
';
} else {
html += '
Both deals have similar total cost. Look at the rate and term structure to decide which works better for your situation.
';
dcMessages.appendChild(div);
dcMessages.scrollTop = dcMessages.scrollHeight;
}
function dcFindVehicle(text) {
var lower = text.toLowerCase();
for (var i = 0; i < dcVehicleKeys.length; i++) {
if (lower.indexOf(dcVehicleKeys[i]) !== -1) {
return dcVehicles[dcVehicleKeys[i]];
}
}
return null;
}
function dcFindPrice(text) {
var patterns = [
/\$\s?([\d,]+(?:\.\d{1,2})?)/,
/(\d{2,3},?\d{3}(?:\.\d{1,2})?)\s*(?:dollars?|cad|cdn)?/i,
/(?:for|at|price|quoted|paying|costs?)\s+\$?\s?([\d,]+)/i
];
for (var i = 0; i < patterns.length; i++) {
var m = text.match(patterns[i]);
if (m) {
var val = parseFloat(m[1].replace(/,/g, ''));
if (val >= 10000 && val <= 250000) return val;
}
}
return null;
}
function dcFindYear(text) {
var m = text.match(/\b(202[0-9]|203[0-9])\b/);
return m ? parseInt(m[1], 10) : null;
}
function dcBuildResponse(input) {
var vehicle = dcFindVehicle(input);
var price = dcFindPrice(input);
var year = dcFindYear(input);
if (!vehicle && !price) {
return '
I can help check a deal. Tell me the vehicle and the price you were quoted.
' +
'
For example: "Is $42,000 for a 2026 RAV4 XLE a good deal?"
';
}
if (vehicle && !price) {
return '
The ' + vehicle.name + ' (' + vehicle.segment + ') has a Canadian MSRP range of ' + dcFmt(vehicle.msrpLow) + ' to ' + dcFmt(vehicle.msrpHigh) + ' depending on trim.
' +
'
What price were you quoted? Include the dollar amount and I can check it against market data.
';
}
if (!vehicle && price) {
return '
You mentioned a price of ' + dcFmt(price) + ', but I could not identify the vehicle model.
' +
'
We do not have pricing data for that model yet. Use the full Deal Analyzer below for a detailed analysis of any deal.
';
}
var msrpMid = (vehicle.msrpLow + vehicle.msrpHigh) / 2;
var diffFromLow = ((price - vehicle.msrpLow) / vehicle.msrpLow * 100).toFixed(1);
var diffFromHigh = ((price - vehicle.msrpHigh) / vehicle.msrpHigh * 100).toFixed(1);
var verdict = '';
var detail = '';
var yearLabel = year ? year + ' ' : '';
if (price < vehicle.msrpLow) {
var belowPct = Math.abs(parseFloat(diffFromLow));
verdict = 'Below MSRP Range';
detail = '
That price of ' + dcFmt(price) + ' for a ' + yearLabel + vehicle.name + ' is ' + belowPct + '% below the starting MSRP of ' + dcFmt(vehicle.msrpLow) + '. For a ' + vehicle.segment.toLowerCase() + ', that is a solid starting point.
';
detail += '
The MSRP range for this model is ' + dcFmt(vehicle.msrpLow) + ' to ' + dcFmt(vehicle.msrpHigh) + ' depending on trim level.
';
detail += '
Confirm this price includes all fees, not just the base vehicle. Verify that freight, PDI, A/C tax, and dealer documentation fees are accounted for.
';
} else if (price <= vehicle.msrpHigh) {
if (price <= msrpMid) {
verdict = 'Within MSRP Range (Lower Half)';
detail = '
That price of ' + dcFmt(price) + ' for a ' + yearLabel + vehicle.name + ' falls in the lower half of the MSRP range (' + dcFmt(vehicle.msrpLow) + ' to ' + dcFmt(vehicle.msrpHigh) + ').
';
detail += '
That is at sticker price. Most buyers negotiate 2-5% below MSRP on this model. On a ' + vehicle.segment.toLowerCase() + ', that could mean ' + dcFmt(price * 0.03) + ' to ' + dcFmt(price * 0.05) + ' in savings.
That price of ' + dcFmt(price) + ' for a ' + yearLabel + vehicle.name + ' falls in the upper half of the MSRP range (' + dcFmt(vehicle.msrpLow) + ' to ' + dcFmt(vehicle.msrpHigh) + ').
';
detail += '
That is at sticker price for a higher trim. Most buyers negotiate 2-5% below MSRP. Make sure the trim and options justify the price point.
';
}
detail += '
Run the numbers through the full Deal Analyzer below to see the complete picture including financing costs.
That price of ' + dcFmt(price) + ' for a ' + yearLabel + vehicle.name + ' is ' + abovePct + '% above the top MSRP of ' + dcFmt(vehicle.msrpHigh) + '.
';
detail += '
Unless this is a high-demand model with limited inventory, there is room to negotiate. The full MSRP range for this ' + vehicle.segment.toLowerCase() + ' is ' + dcFmt(vehicle.msrpLow) + ' to ' + dcFmt(vehicle.msrpHigh) + '.
';
detail += '
Check for dealer markups, added accessories, or protection packages that may be inflating the price. A Holdback consultation can identify exactly where the overage is.
';
}
function dcProcessInput() {
var text = dcInput.value.trim();
if (!text) return;
dcAddMessage('user', text.replace(//g, '>'));
dcInput.value = '';
dcSend.disabled = true;
setTimeout(function() {
var response = dcBuildResponse(text);
dcAddMessage('bot', response);
dcSend.disabled = false;
dcInput.focus();
if (typeof gtag === 'function') {
var v = dcFindVehicle(text);
var p = dcFindPrice(text);
gtag('event', 'deal_chat_query', {
vehicle: v ? v.name : 'unknown',
price: p || 0,
query: text.substring(0, 100)
});
}
}, 400);
}
dcSend.addEventListener('click', dcProcessInput);
dcInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); dcProcessInput(); }
});
document.getElementById('dcExampleList').addEventListener('click', function(e) {
var li = e.target.closest('li');
if (li && li.dataset.q) {
dcInput.value = li.dataset.q;
dcProcessInput();
}
});
// ===========================
// END DEAL CHAT
// ===========================
// Quick-check uses the canonical solveForRate() defined above (line ~1777).
// Both call sites here pass (payment, principal, months) to match that signature.
document.getElementById('qcBtn').addEventListener('click', function() {
var paymentRaw = getDollarValue('qcPayment');
if (paymentRaw <= 0) {
document.getElementById('qcPayment').classList.add('error');
return;
}
document.getElementById('qcPayment').classList.remove('error');
// Convert to monthly
var monthlyPmt = paymentRaw;
if (qcFreq === 'biweekly') monthlyPmt = paymentRaw * 26 / 12;
if (qcFreq === 'weekly') monthlyPmt = paymentRaw * 52 / 12;
var price = getDollarValue('qcPrice');
var termVal = document.getElementById('qcTerm').value;
var term = termVal ? parseInt(termVal, 10) : 0;
var results = document.getElementById('qcResults');
var html = '';
// CASE 1: Price + Payment (+ optional term)
if (price > 0) {
var hst = Math.max(0, price) * 0.13;
var principal = price + hst;
var benchmarkRate = BENCHMARKS.newRate;
if (term > 0) {
// We have price + payment + term: solve for implied rate
var implied = solveForRate(monthlyPmt, principal, term);
// Canada Interest Act §4 — fixed-rate loans compound semi-annually,
// not in advance. Effective monthly rate from a stated annual rate
// is `(1 + r/2)^(1/6) - 1`, NOT `r/12`. The US-style monthly form
// (formerly used here) under-stated the benchmark payment by ~$10/mo
// on a 60-month $30k loan — making any quoted deal look closer to
// benchmark than it actually was.
var benchR = Math.pow(1 + benchmarkRate / 200, 1/6) - 1;
var benchPmt = principal * (benchR * Math.pow(1 + benchR, term)) / (Math.pow(1 + benchR, term) - 1);
var totalPaid = monthlyPmt * term;
var benchTotal = benchPmt * term;
var excess = totalPaid - benchTotal;
html += '
';
if (implied > 0 && implied > benchmarkRate + 0.5) {
html += '
';
html += '
What this means
';
if (excess > 500) {
html += '
Your quoted payment implies a rate of ' + implied.toFixed(2) + '%, which is above the ' + benchmarkRate + '% Ontario market benchmark. Over ' + term + ' months, that gap costs you roughly $' + Math.round(excess).toLocaleString('en-CA') + ' in extra interest. This could be a rate markup, or it could mean add-ons or fees have been rolled into the payment.
';
} else {
html += '
Your rate is slightly above the ' + benchmarkRate + '% benchmark, but the difference is relatively small. Still worth confirming there are no add-ons rolled into the payment.
';
}
html += '
';
} else if (implied > 0 && implied <= benchmarkRate) {
html += '
';
html += '
Your implied rate of ' + implied.toFixed(2) + '% is at or below the market benchmark. The payment looks reasonable for this vehicle and term. Run the full analysis below to check the add-on and pricing details.
';
html += '
';
}
} else {
// Price + payment, no term: show across common terms
html += '
If any rate looks higher than expected, add-ons or fees may be rolled into the payment. Enter your term above for a specific analysis, or run the full deal analyzer below.
';
}
} else {
// CASE 2: Payment only, no price
html += '