{"id":3656,"date":"2026-08-15T13:10:29","date_gmt":"2026-08-15T03:10:29","guid":{"rendered":"https:\/\/chipkie.com\/uk\/?page_id=3656"},"modified":"2026-08-15T13:14:15","modified_gmt":"2026-08-15T03:14:15","slug":"family-loan-calculators-uk","status":"publish","type":"page","link":"https:\/\/chipkie.com\/uk\/family-loan-calculators-uk\/","title":{"rendered":"Family Loan Calculators UK"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"3656\" class=\"elementor elementor-3656\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-2189e9a e-con-full e-flex e-con e-parent\" data-id=\"2189e9a\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t<div class=\"elementor-element elementor-element-7e9461f e-con-full elementor-hidden-tablet elementor-hidden-mobile e-flex e-con e-child\" data-id=\"7e9461f\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-97869d9 e-con-full e-flex e-con e-child\" data-id=\"97869d9\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t<div class=\"elementor-element elementor-element-5e6aa69 e-flex e-con-boxed e-con e-child\" data-id=\"5e6aa69\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-7d5e065 elementor-widget elementor-widget-heading\" data-id=\"7d5e065\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h1 class=\"elementor-heading-title elementor-size-default\">UK Family Loan Calculator for Friends and Family<\/h1>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-6b3c0d0 elementor-widget elementor-widget-heading\" data-id=\"6b3c0d0\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Work out a clear repayment plan in pounds before any money changes hands \u2014 with no account needed.<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-6f18409 elementor-align-center elementor-widget elementor-widget-button\" data-id=\"6f18409\" data-element_type=\"widget\" data-e-type=\"widget\" id=\"loan_start_wp\" data-widget_type=\"button.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<div class=\"elementor-button-wrapper\">\n\t\t\t\t\t<a class=\"elementor-button elementor-button-link elementor-size-sm\" href=\"#family-loan-calculator\">\n\t\t\t\t\t\t<span class=\"elementor-button-content-wrapper\">\n\t\t\t\t\t\t<span class=\"elementor-button-icon\">\n\t\t\t\t<i aria-hidden=\"true\" class=\"fas fa-play-circle\"><\/i>\t\t\t<\/span>\n\t\t\t\t\t\t\t\t\t<span class=\"elementor-button-text\">Calculate my repayments<\/span>\n\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-a58a069 e-con-full elementor-hidden-tablet elementor-hidden-mobile e-flex e-con e-child\" data-id=\"a58a069\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-7edd473 e-flex e-con-boxed e-con e-parent\" data-id=\"7edd473\" data-element_type=\"container\" data-e-type=\"container\" id=\"intro\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-bc7cbbd e-con-full e-flex e-con e-child\" data-id=\"bc7cbbd\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-597cb53 elementor-widget elementor-widget-heading\" data-id=\"597cb53\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Compare repayments, interest and timing before you agree<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-ea3335c elementor-widget elementor-widget-text-editor\" data-id=\"ea3335c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>Enter the loan amount, annual interest rate, term and repayment frequency to see each repayment, total interest, total repaid and estimated finish date.<\/p><p>Chipkie&#8217;s calculator supports interest-free and interest-bearing family loans, with monthly, fortnightly or weekly repayments. It uses the same reducing-balance calculation method as Chipkie&#8217;s loan schedule, so you can carry a scenario into loan setup without starting again.<\/p><p><strong>No account is needed to use the calculator.<\/strong><\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-d0f7cbe e-flex e-con-boxed e-con e-parent\" data-id=\"d0f7cbe\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-62f13ed e-con-full e-flex e-con e-child\" data-id=\"62f13ed\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-72aa3ca elementor-widget elementor-widget-html\" data-id=\"72aa3ca\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!--\n  Chipkie Family Loan Calculator \u2014 GB embed (self-contained).\n\n  HOW TO USE (WordPress\/Elementor): paste this whole block into an HTML\n  widget on the GB calculator page. No external scripts, fonts or CSS.\n\n  This file is GENERATED \u2014 one widget codebase serves every market. Edit\n  resources\/marketing\/family-loan-calculator\/src\/ and re-run `node build.mjs`;\n  do not hand-edit or fork this file per market (the market attribute below\n  is the only intended difference).\n\n  Attributes on <chipkie-loan-calc>:\n    market=\"GB\"                       drives currency, wording and the\n                                         market's tax-guidance module\n    config-url=\"...\/api\/calculator\/config\"  live AFR\/usury\/PSA\/pricing data\n    cta-url=\"...\/loans\/create\"           where \"Create this loan\" deep-links\n    comparison-apr=\"12\"                  fallback bank APR if config is unreachable\n    analytics=\"off\"                      add to disable dataLayer events\n\n  Theme overrides (optional) \u2014 set on the page:\n    chipkie-loan-calc { --ck-primary:#004053; --ck-accent:#6dc4bc; }\n-->\n<section aria-label=\"Family loan calculator\">\n  <p style=\"position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)\">\n    Free family loan calculator for the UK: work out repayments, total\n    interest and an amortisation schedule for family loans \u2014 with guidance on\n    HMRC savings income and the Personal Savings Allowance \u2014 then create a\n    signed loan agreement with Chipkie.\n  <\/p>\n  <chipkie-loan-calc\n    id=\"family-loan-calculator\"\n    style=\"scroll-margin-top:140px\"\n    market=\"GB\"\n    config-url=\"https:\/\/my.chipkie.com\/api\/calculator\/config\"\n    cta-url=\"https:\/\/my.chipkie.com\/loans\/create\"\n    comparison-apr=\"12\"\n  ><\/chipkie-loan-calc>\n<\/section>\n<script>\n(() => {\n'use strict';\n\/**\n * Chipkie family-loan calculator \u2014 amortisation engine.\n *\n * EXACT JavaScript port of the app's loan maths so the marketing widget quotes\n * the same numbers the app will (docs\/specs\/family-loan-calculator-scope.md\n * makes widget\/app divergence a P1 bug):\n *   - app\/Services\/LoanService.php      (numInstalments, periodicRate,\n *                                        instalmentAmount, getInstalments,\n *                                        instalmentDueDate)\n *   - app\/Services\/AmortisationService.php (buildSchedule reconcile walk,\n *                                        simulatePayoff, suggestExtra)\n *   - app\/Support\/Afr.php \/ Usury.php   (assess\/check, data-driven)\n *\n * Parity is enforced two ways: tests\/Feature\/CalculatorParityTest.php\n * regenerates golden-vectors.json from the real PHP services, and\n * test\/math.test.mjs asserts this port reproduces it to the cent.\n *\/\n\n\/\/ \u2500\u2500 PHP-compatible rounding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ PHP's round() pre-corrects float representation (round(2.675, 2) === 2.68);\n\/\/ a naive Math.round(x*100)\/100 gives 2.67. Snap to 14 significant digits\n\/\/ before rounding, half away from zero, to mirror PHP.\nfunction phpRound(x, places = 2) {\n  if (!isFinite(x)) return x;\n  const f = Math.pow(10, places);\n  const scaled = Number((Math.abs(x) * f).toPrecision(14));\n  return (Math.sign(x) || 1) * Math.round(scaled) \/ f;\n}\n\nconst round2 = (x) => phpRound(x, 2);\n\n\/\/ \u2500\u2500 Dates (UTC-noon internally: no DST\/locale drift) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction dateParts(iso) {\n  const [y, m, d] = String(iso).slice(0, 10).split('-').map(Number);\n  return { y, m, d };\n}\n\nfunction toUtc(iso) {\n  const { y, m, d } = dateParts(iso);\n  return new Date(Date.UTC(y, m - 1, d, 12));\n}\n\nfunction isoOf(dt) {\n  return dt.toISOString().slice(0, 10);\n}\n\n\/** Whole days from a to b (negative when b is earlier). *\/\nfunction diffDays(aIso, bIso) {\n  return Math.round((toUtc(bIso) - toUtc(aIso)) \/ 86400000);\n}\n\n\/**\n * Calendar-month add with END-OF-MONTH OVERFLOW (Jan 31 + 1mo = Mar 3) \u2014\n * the same normalisation PHP's DateTime\/Carbon::addMonths and JS setMonth\n * apply, and what CreateLoan.vue's addMonths() produces for the form's\n * default end date.\n *\/\nfunction addMonthsIso(iso, months) {\n  const { y, m, d } = dateParts(iso);\n  return isoOf(new Date(Date.UTC(y, m - 1 + months, d, 12)));\n}\n\nfunction addDaysIso(iso, days) {\n  const { y, m, d } = dateParts(iso);\n  return isoOf(new Date(Date.UTC(y, m - 1, d + days, 12)));\n}\n\n\/**\n * Full calendar months from a to b \u2014 PHP DateInterval semantics (what\n * Carbon::diffInMonths returns for two date-only values): whole-of-calendar\n * months, minus one when b's day-of-month hasn't reached a's yet.\n *\/\nfunction diffInMonths(aIso, bIso) {\n  const a = dateParts(aIso);\n  const b = dateParts(bIso);\n  let months = (b.y - a.y) * 12 + (b.m - a.m);\n  if (b.d < a.d) months -= 1;\n  return Math.max(0, months);\n}\n\n\/\/ \u2500\u2500 LoanService port \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst FREQUENCIES = ['weekly', 'fortnightly', 'monthly'];\n\n\/** LoanService::numInstalments \u2014 floors the span; end <= start \u2192 0. *\/\nfunction numInstalments(startIso, endIso, frequency) {\n  if (diffDays(startIso, endIso) <= 0) return 0;\n  const days = diffDays(startIso, endIso);\n  switch (frequency) {\n    case 'weekly':\n      return Math.floor(days \/ 7);\n    case 'fortnightly':\n      \/\/ Carbon: diffInWeeks (int) first, THEN halve and floor.\n      return Math.floor(Math.floor(days \/ 7) \/ 2);\n    case 'monthly':\n      return diffInMonths(startIso, endIso);\n    default:\n      throw new Error('Invalid frequency');\n  }\n}\n\n\/** LoanService::periodicRate \u2014 daily = annual\/365.25; weekly \u00d77, fortnightly \u00d714, monthly = annual\/12. *\/\nfunction periodicRate(interestPct, frequency) {\n  if (interestPct <= 0) return 0;\n  const annual = interestPct \/ 100;\n  const daily = annual \/ 365.25;\n  switch (frequency) {\n    case 'weekly':\n      return daily * 7;\n    case 'fortnightly':\n      return daily * 14;\n    case 'monthly':\n      return annual \/ 12;\n    default:\n      throw new Error('Invalid frequency');\n  }\n}\n\n\/** LoanService::instalmentAmount \u2014 standard reducing-balance annuity, 2 dp. *\/\nfunction instalmentAmount(amount, interestPct, startIso, endIso, frequency) {\n  const n = numInstalments(startIso, endIso, frequency);\n  if (n < 1) return round2(amount);\n  if (interestPct <= 0) return round2(amount \/ n);\n  const r = periodicRate(interestPct, frequency);\n  const x = Math.pow(1 + r, n);\n  return round2((amount * r * x) \/ (x - 1));\n}\n\n\/** LoanService::instalmentDueDate \u2014 index is 0-based; the first payment is one period after start. *\/\nfunction instalmentDueDate(startIso, index, frequency) {\n  const periods = index + 1;\n  switch (frequency) {\n    case 'weekly':\n      return addDaysIso(startIso, periods * 7);\n    case 'fortnightly':\n      return addDaysIso(startIso, periods * 14);\n    case 'monthly':\n      return addMonthsIso(startIso, periods);\n    default:\n      throw new Error('Invalid frequency');\n  }\n}\n\n\/**\n * LoanService::getInstalments \u2014 the schedule total distributed across n\n * instalments in WHOLE CENTS (leftover cents spread one each across the\n * earliest instalments) so the schedule sums exactly to the total.\n *\/\nfunction getInstalments(startIso, endIso, frequency, amount, interestPct) {\n  const n = numInstalments(startIso, endIso, frequency);\n  if (n < 1) return [];\n\n  const per = instalmentAmount(amount, interestPct, startIso, endIso, frequency);\n  const total = interestPct > 0 ? round2(per * n) : round2(amount);\n\n  const totalCents = Math.round(total * 100);\n  const base = Math.trunc(totalCents \/ n);\n  const remainder = totalCents - base * n;\n\n  const out = [];\n  for (let i = 0; i < n; i++) {\n    const cents = base + (i < remainder ? 1 : 0);\n    out.push({ amount: cents \/ 100, due_date: instalmentDueDate(startIso, i, frequency) });\n  }\n  return out;\n}\n\n\/**\n * AmortisationService::buildSchedule \u2014 per-payment interest\/principal split on\n * the reducing balance, then the reconcile walk (payments fixed; the rounding\n * remainder reallocated between interest and principal from the last row\n * backward) so principal sums EXACTLY to the loan amount and the final\n * balance is exactly zero.\n *\/\nfunction buildSchedule({ amount, interest, startDate, endDate, frequency }) {\n  const principal = round2(Number(amount));\n  const rate = Number(interest);\n  const instalments = getInstalments(startDate, endDate, frequency, principal, rate);\n  const periodic = periodicRate(rate, frequency);\n\n  const rows = [];\n  let balance = principal;\n  instalments.forEach((inst, i) => {\n    const payment = round2(inst.amount);\n    let interestPart = periodic > 0 ? round2(balance * periodic) : 0;\n    if (interestPart > payment) interestPart = payment;\n    const principalPart = round2(payment - interestPart);\n    balance = round2(balance - principalPart);\n    rows.push({\n      number: i + 1,\n      due_date: inst.due_date,\n      payment,\n      interest: interestPart,\n      principal: principalPart,\n    });\n  });\n\n  const n = rows.length;\n  if (n > 0) {\n    const sumP = round2(rows.reduce((s, r) => s + r.principal, 0));\n    let residual = round2(principal - sumP);\n    for (let i = n - 1; i >= 0 && Math.abs(residual) >= 0.005; i--) {\n      const room = residual > 0 ? round2(rows[i].interest) : round2(-rows[i].principal);\n      const add = residual > 0 ? Math.min(residual, room) : Math.max(residual, room);\n      rows[i].principal = round2(rows[i].principal + add);\n      rows[i].interest = round2(rows[i].payment - rows[i].principal);\n      residual = round2(residual - add);\n    }\n  }\n\n  let bal = principal;\n  for (const r of rows) {\n    bal = round2(bal - r.principal);\n    r.balance = bal;\n  }\n  const principalSum = round2(principal - bal);\n  const repaid = round2(rows.reduce((s, r) => s + r.payment, 0));\n  const totalInterest = round2(rows.reduce((s, r) => s + r.interest, 0));\n\n  return {\n    interest_rate: round2(rate),\n    frequency,\n    projected: true,\n    rows,\n    totals: {\n      principal: round2(principalSum),\n      interest: totalInterest,\n      repaid,\n      instalments: n,\n    },\n  };\n}\n\n\/\/ \u2500\u2500 Early payoff (AmortisationService::earlyPayoff, 'recurring' mode, from start) \u2500\u2500\n\/** AmortisationService::simulatePayoff \u2014 one reducing-balance walk. *\/\nfunction simulatePayoff(balance, payment, periodic, guard) {\n  let interest = 0;\n  let periods = 0;\n  let bal = balance;\n  while (bal > 0.01 && periods < guard) {\n    const charge = periodic <= 0 ? 0 : round2(bal * periodic);\n    const principalPart = round2(payment - charge);\n    if (principalPart <= 0) break;\n    interest = round2(interest + charge);\n    bal = round2(bal - principalPart);\n    periods++;\n  }\n  return { interest, periods };\n}\n\n\/** AmortisationService::suggestExtra \u2014 round the payment up to a friendly increment. *\/\nfunction suggestExtra(regularPayment) {\n  if (regularPayment <= 0) return 0;\n  const increment = regularPayment < 50 ? 5 : regularPayment < 500 ? 10 : 50;\n  let target = Math.ceil(regularPayment \/ increment) * increment;\n  if (target <= regularPayment + 0.005) target += increment;\n  return round2(target - regularPayment);\n}\n\n\/**\n * Recurring-extra projection from the loan's start (the widget's case: no\n * payments made yet, balance = full principal) \u2014 the same walk the app's\n * premium early-payoff tool runs, so the teaser numbers match it.\n *\/\nfunction earlyPayoff(schedule, principal, extra) {\n  const rows = schedule.rows;\n  const baselineCount = rows.length;\n  if (baselineCount === 0 || principal <= 0.01) {\n    return { state: 'complete', payments_saved: 0, interest_saved: 0, new_payoff: null };\n  }\n  const periodic = periodicRate(schedule.interest_rate, schedule.frequency);\n  const regular = round2(rows[0].payment);\n  const guard = baselineCount + 5;\n  const baseline = simulatePayoff(principal, regular, periodic, guard);\n  const boosted = round2(regular + Math.max(0, round2(extra)));\n\n  \/\/ Boosted payment big enough to clear everything in one hit.\n  const firstCharge = periodic <= 0 ? 0 : round2(principal * periodic);\n  if (boosted >= principal + firstCharge) {\n    return {\n      state: 'pays_off_now',\n      regular_payment: regular,\n      payments_saved: Math.max(0, baselineCount - 1),\n      interest_saved: round2(Math.max(0, baseline.interest - firstCharge)),\n      new_interest: firstCharge,\n      new_payoff: rows[0].due_date,\n      suggested_extra: suggestExtra(regular),\n    };\n  }\n\n  const accel = simulatePayoff(principal, boosted, periodic, guard);\n  const periods = Math.max(1, Math.min(accel.periods, baselineCount));\n  return {\n    state: 'projected',\n    regular_payment: regular,\n    payments_saved: Math.max(0, baselineCount - periods),\n    interest_saved: round2(Math.max(0, baseline.interest - accel.interest)),\n    new_interest: accel.interest,\n    new_payoff: rows[periods - 1].due_date,\n    suggested_extra: suggestExtra(regular),\n  };\n}\n\n\/\/ \u2500\u2500 US tax intelligence (App\\Support\\Afr \/ Usury ports; data-driven) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/**\n * Afr::applicableRate + assess. `afr` is the payload from the calculator\n * config endpoint: { rates: {short, mid, long}, short_max_months,\n * mid_max_months, month }. Missing\/partial data \u2192 dormant\n * (has_data false) \u2014 never a guessed 0%.\n *\/\nfunction afrAssess(ratePct, termMonths, amount, afr) {\n  const none = { has_data: false, applicable_afr: null, band: null, below: false, de_minimis_exempt: false };\n  const r = afr && afr.rates;\n  if (!r || !isFinite(r.short) || !isFinite(r.mid) || !isFinite(r.long)) return none;\n\n  const shortMax = Number(afr.short_max_months ?? 36);\n  const midMax = Number(afr.mid_max_months ?? 108);\n  const band = termMonths <= shortMax ? 'short' : termMonths <= midMax ? 'mid' : 'long';\n  const applicable = Number(r[band]);\n  return {\n    has_data: true,\n    applicable_afr: applicable,\n    band,\n    below: ratePct != null && ratePct < applicable - 0.0001,\n    \/\/ Public shape mirrors Afr::assess. Chipkie models term loans, for which\n    \/\/ IRS Publication 550 says the $10,000 gift-loan exception does not apply.\n    de_minimis_exempt: false,\n  };\n}\n\n\/**\n * Usury::check. `states` is the config map (lowercase state name\/code \u2192\n * number | 'none'; absent = not assessed). Conservative: unknown state \u2192\n * 'unknown' (\"check your state\"), never a guessed cap.\n *\/\nfunction usuryCheck(ratePct, state, states) {\n  const empty = { status: 'unknown', cap: null, state: state || null };\n  if (!state || !states) return empty;\n  const v = states[String(state).trim().toLowerCase()];\n  if (v === undefined || v === null) return empty;\n  if (typeof v === 'string' && v.toLowerCase() === 'none') return { status: 'none', cap: null, state };\n  const cap = Number(v);\n  if (!isFinite(cap)) return empty;\n  const over = ratePct != null && ratePct > cap + 0.0001;\n  return { status: over ? 'over' : 'ok', cap, state };\n}\n\n\/**\n * <chipkie-loan-calc> \u2014 Chipkie's family loan calculator (marketing embed).\n *\n * Self-contained custom element (shadow DOM: no style\/ID collisions inside\n * WordPress\/Elementor). All amortisation maths is the exact LoanService port\n * in math.mjs, so the numbers here are the numbers the app quotes.\n *\n * Attributes (all optional):\n *   market=\"US\"            \u2014 market code; US enables the tax-intelligence panel\n *   config-url=\"\u2026\"         \u2014 GET endpoint (api.calculator.config); live AFR \/\n *                            usury \/ prices. Unreachable \u2192 tax panel dormant.\n *   lead-url=\"\u2026\"           \u2014 POST endpoint (api.calculator.lead)\n *   cta-url=\"\u2026\"            \u2014 loan-create deep-link base (\u2026\/loans\/create)\n *   comparison-apr=\"12\"    \u2014 fallback bank APR if the config fetch fails\n *   analytics=\"off\"        \u2014 disable dataLayer events\n *\n * Theming: override any --ck-* custom property from page CSS, e.g.\n *   chipkie-loan-calc { --ck-primary: #004053; }\n *\/\n\n\nconst WIDGET_CSS = `\/* Chipkie family-loan calculator \u2014 shadow-DOM styles.\n   Brand tokens (tailwind.config.js) as custom properties on :host; pages can\n   override any --ck-* token from outside (custom props pierce the shadow). *\/\n\n:host {\n  \/* Brand (defaults = Chipkie palette) *\/\n  --ck-primary: #004053;        \/* navy *\/\n  --ck-accent: #6dc4bc;         \/* brand teal *\/\n  --ck-accent-dark: #007c89;    \/* dark teal *\/\n  --ck-highlight: rgb(245, 131, 103); \/* salmon *\/\n  --ck-bg: #ffffff;\n  --ck-panel: #f2f8f8;          \/* light teal wash *\/\n  --ck-text: #1a202c;\n  --ck-muted: #52606d;\n  --ck-border: #d8e4e6;\n  --ck-good-bg: #e3f5ee;\n  --ck-good-text: #14532d;\n  --ck-warn-bg: #fdf3e3;\n  --ck-warn-border: #eab308;\n  --ck-warn-text: #713f12;\n  --ck-alert-bg: #fdeceb;\n  --ck-alert-border: #dc6d5a;\n  --ck-alert-text: #7f1d1d;\n  --ck-radius: 14px;\n  --ck-font: \"Nexa Book\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n\n  display: block;\n  font-family: var(--ck-font);\n  color: var(--ck-text);\n  line-height: 1.45;\n  container-type: inline-size;\n}\n\n@media (prefers-color-scheme: dark) {\n  :host {\n    --ck-bg: #0f1e24;\n    --ck-panel: #16303a;\n    --ck-text: #e8f1f2;\n    --ck-muted: #9fb6bc;\n    --ck-border: #2a4650;\n    --ck-good-bg: #123527;\n    --ck-good-text: #a7e3c4;\n    --ck-warn-bg: #38300f;\n    --ck-warn-text: #f5e3a3;\n    --ck-alert-bg: #3a1a17;\n    --ck-alert-text: #f3b8ae;\n  }\n}\n\n*, *::before, *::after { box-sizing: border-box; }\n\n.calc {\n  background: var(--ck-bg);\n  border: 1px solid var(--ck-border);\n  border-top: 6px solid var(--ck-accent);\n  border-radius: var(--ck-radius);\n  box-shadow: 0 12px 30px -12px rgba(0, 32, 42, 0.25);\n  padding: 26px;\n  max-width: 920px;\n  margin: 0 auto;\n}\n\n.calc-header { text-align: center; margin-bottom: 22px; }\n.calc-header h3 { margin: 0 0 6px; font-size: 1.55rem; font-weight: 700; color: var(--ck-primary); }\n@media (prefers-color-scheme: dark) { .calc-header h3 { color: var(--ck-accent); } }\n.calc-header p { margin: 0; color: var(--ck-muted); font-size: 0.95rem; }\n\n\/* The headline answer (imputed-interest mode). Stacks on narrow screens \u2014\n   the rate is the thing they came for, so it must never be the part that\n   gets clipped. *\/\n.answer {\n  margin: 14px auto 0;\n  display: flex;\n  flex-wrap: wrap;\n  align-items: baseline;\n  justify-content: center;\n  gap: 4px 10px;\n  padding: 12px 14px;\n  border: 1px solid color-mix(in srgb, var(--ck-primary) 25%, transparent);\n  border-radius: 12px;\n  background: color-mix(in srgb, var(--ck-primary) 5%, transparent);\n}\n\/* \\`display:flex\\`\/\\`grid\\` BEAT the hidden attribute's UA \\`display:none\\`, so a\n   hidden box still paints \u2014 an empty bordered rectangle above the calculator\n   on every page that is not in that mode. Only the screenshot showed it. *\/\n.answer[hidden], .compare[hidden] { display: none; }\n.answer-label { color: var(--ck-muted); font-size: 0.9rem; }\n.answer-value { font-size: 1.9rem; font-weight: 700; line-height: 1.1; color: var(--ck-primary); }\n@media (prefers-color-scheme: dark) { .answer-value { color: var(--ck-accent); } }\n.answer-note { color: var(--ck-muted); font-size: 0.82rem; flex-basis: 100%; }\n\n\/* Gift-or-loan comparison. Two cards, side by side where there is room and\n   stacked where there isn't \u2014 at 390px a two-column grid would shrink both to\n   unreadable. Gift first: it is the choice people drift into by accident. *\/\n.compare { margin-top: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; text-align: left; }\n@container (max-width: 700px) { .compare { grid-template-columns: 1fr; } }\n.compare-card {\n  padding: 14px;\n  border: 1px solid var(--ck-border);\n  border-radius: 12px;\n  background: var(--ck-panel);\n}\n.compare-card--loan { border-color: color-mix(in srgb, var(--ck-primary) 35%, transparent); }\n.compare-card h4 {\n  margin: 0 0 8px; font-size: 1rem; font-weight: 700; color: var(--ck-primary);\n}\n@media (prefers-color-scheme: dark) { .compare-card h4 { color: var(--ck-accent); } }\n.compare-card ul { margin: 0; padding-left: 18px; }\n.compare-card li { margin-bottom: 7px; font-size: 0.9rem; line-height: 1.45; color: var(--ck-text); }\n.compare-card li:last-child { margin-bottom: 0; }\n\n.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; }\n@container (max-width: 700px) { .cols { grid-template-columns: 1fr; } }\n\n.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }\n\n.field { margin-bottom: 14px; }\n.field label {\n  display: block; margin-bottom: 6px;\n  font-weight: 600; font-size: 0.86rem; color: var(--ck-muted);\n}\n.field input[type=\"number\"], .field input[type=\"text\"], .field input[type=\"date\"],\n.field input[type=\"email\"], .field select {\n  width: 100%; padding: 11px 12px;\n  border: 2px solid var(--ck-border); border-radius: 8px;\n  font-size: 1rem; font-family: inherit;\n  background: var(--ck-bg); color: var(--ck-text);\n}\n.field input:focus-visible, .field select:focus-visible, button:focus-visible,\ninput[type=\"range\"]:focus-visible, a:focus-visible, details summary:focus-visible {\n  outline: 3px solid var(--ck-accent); outline-offset: 1px;\n}\n.field .hint { font-size: 0.78rem; color: var(--ck-muted); margin-top: 4px; }\n.field .error { font-size: 0.82rem; color: var(--ck-alert-text); background: var(--ck-alert-bg); border-radius: 6px; padding: 4px 8px; margin-top: 5px; }\n\ninput[type=\"range\"] { width: 100%; accent-color: var(--ck-accent-dark); margin: 6px 0 0; }\n\n.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }\n.chip {\n  border: 1.5px solid var(--ck-accent-dark); border-radius: 999px;\n  background: transparent; color: var(--ck-accent-dark);\n  padding: 5px 13px; font-size: 0.85rem; font-weight: 600; font-family: inherit;\n  cursor: pointer;\n}\n.chip[aria-pressed=\"true\"] { background: var(--ck-accent-dark); color: #fff; }\n.chip:hover { background: var(--ck-accent); border-color: var(--ck-accent); color: #06272e; }\n\n\/* Results *\/\n.results {\n  background: var(--ck-panel);\n  border: 1px solid var(--ck-border);\n  border-radius: var(--ck-radius);\n  padding: 20px;\n  min-height: 200px; \/* reserve space \u2014 no layout shift on first calc *\/\n}\n.headline { text-align: center; padding-bottom: 14px; border-bottom: 2px solid var(--ck-border); margin-bottom: 14px; }\n.headline .label { color: var(--ck-muted); font-size: 0.9rem; }\n.headline .value { font-size: 2.1rem; font-weight: 800; color: var(--ck-primary); }\n@media (prefers-color-scheme: dark) { .headline .value { color: var(--ck-accent); } }\n.headline .per { font-size: 1rem; font-weight: 600; color: var(--ck-muted); }\n\n.rrow { display: flex; justify-content: space-between; gap: 12px; margin: 7px 0; font-size: 0.95rem; }\n.rrow .k { color: var(--ck-muted); }\n.rrow .v { font-weight: 700; color: var(--ck-text); font-variant-numeric: tabular-nums; }\n\n.savings {\n  background: var(--ck-good-bg); color: var(--ck-good-text);\n  border-radius: 10px; padding: 13px 15px; margin-top: 14px;\n  font-size: 0.93rem;\n}\n.savings strong { font-size: 1.05rem; }\n.savings .src { display: block; margin-top: 5px; font-size: 0.76rem; opacity: 0.85; }\n\n\/* Advisory panels (AFR \/ usury \/ gift tax) *\/\n.advice { border-radius: 10px; padding: 12px 14px; margin-top: 12px; font-size: 0.9rem; }\n.advice.info { background: var(--ck-panel); border: 1px solid var(--ck-border); color: var(--ck-text); }\n.advice.warn { background: var(--ck-warn-bg); border-left: 4px solid var(--ck-warn-border); color: var(--ck-warn-text); }\n.advice.alert { background: var(--ck-alert-bg); border-left: 4px solid var(--ck-alert-border); color: var(--ck-alert-text); }\n.advice .fix { margin-top: 8px; }\n.advice .title { font-weight: 700; display: block; margin-bottom: 3px; }\n.smallprint { font-size: 0.74rem; color: var(--ck-muted); margin-top: 10px; }\n\n\/* Charts *\/\n.chart-wrap { margin-top: 16px; }\n.chart-wrap h4 { margin: 0 0 6px; font-size: 0.9rem; color: var(--ck-muted); font-weight: 600; }\n.chart-wrap svg { width: 100%; height: auto; display: block; }\n.legend { display: flex; gap: 16px; font-size: 0.78rem; color: var(--ck-muted); margin-top: 4px; flex-wrap: wrap; }\n.legend .sw { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }\n\n\/* Early payoff *\/\n.payoff { margin-top: 18px; padding: 14px; border: 1px dashed var(--ck-accent-dark); border-radius: 10px; }\n.payoff h4 { margin: 0 0 4px; font-size: 0.95rem; color: var(--ck-primary); }\n@media (prefers-color-scheme: dark) { .payoff h4 { color: var(--ck-accent); } }\n.payoff .out { font-size: 0.92rem; margin-top: 8px; }\n.payoff .teaser { font-size: 0.78rem; color: var(--ck-muted); margin-top: 6px; }\n\n\/* Scenarios *\/\n.scenarios { margin-top: 18px; }\n.scenarios table, .schedule table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }\n.scenarios th, .scenarios td, .schedule th, .schedule td {\n  padding: 6px 8px; text-align: right; border-bottom: 1px solid var(--ck-border);\n  font-variant-numeric: tabular-nums;\n}\n.scenarios th:first-child, .scenarios td:first-child,\n.schedule th:first-child, .schedule td:first-child { text-align: left; }\n.scenarios th, .schedule th { color: var(--ck-muted); font-weight: 600; }\n.table-scroll { overflow-x: auto; }\n\n\/* Schedule *\/\n.schedule { margin-top: 18px; }\n.schedule summary { cursor: pointer; font-weight: 700; color: var(--ck-accent-dark); }\n.schedule .actions { margin: 10px 0; display: flex; gap: 10px; flex-wrap: wrap; }\n\nbutton.btn {\n  font-family: inherit; font-size: 0.9rem; font-weight: 700;\n  border-radius: 8px; padding: 9px 16px; cursor: pointer;\n  border: 2px solid var(--ck-accent-dark);\n  background: transparent; color: var(--ck-accent-dark);\n}\nbutton.btn:hover { background: var(--ck-accent-dark); color: #fff; }\nbutton.btn.primary { background: var(--ck-primary); border-color: var(--ck-primary); color: #fff; }\nbutton.btn.primary:hover { background: var(--ck-accent-dark); border-color: var(--ck-accent-dark); }\n\n\/* CTA + capture *\/\n.cta {\n  margin-top: 22px; padding: 20px;\n  background: var(--ck-primary); color: #fff;\n  border-radius: var(--ck-radius); text-align: center;\n}\n.cta h4 { margin: 0 0 6px; font-size: 1.2rem; }\n.cta p { margin: 0 0 12px; font-size: 0.9rem; opacity: 0.92; }\n.cta a.go {\n  display: inline-block; background: var(--ck-highlight); color: #3b1006;\n  font-weight: 800; text-decoration: none; border-radius: 10px;\n  padding: 12px 26px; font-size: 1.02rem;\n}\n.cta a.go:hover { filter: brightness(1.08); }\n.cta ul { list-style: none; display: flex; gap: 7px 18px; flex-wrap: wrap; justify-content: center; padding: 0; margin: 14px 0 0; font-size: 0.82rem; opacity: 0.95; }\n.cta ul li::before { content: \"\u2713 \"; color: var(--ck-accent); font-weight: 800; }\n\n.capture { margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; align-items: center; }\n.capture input[type=\"email\"] {\n  flex: 1 1 220px; max-width: 300px; padding: 10px 12px;\n  border-radius: 8px; border: none; font-family: inherit; font-size: 0.92rem;\n}\n.capture .msg { flex-basis: 100%; font-size: 0.85rem; }\n.hp { position: absolute; left: -5000px; width: 1px; height: 1px; overflow: hidden; } \/* honeypot *\/\n\n.visually-hidden {\n  position: absolute; width: 1px; height: 1px; margin: -1px;\n  clip-path: inset(50%); overflow: hidden; white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .savings, .advice { animation: ck-fade 0.35s ease-in; }\n  @keyframes ck-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }\n}\n`;\n\nconst US_STATES = {\n  AL: 'Alabama', AK: 'Alaska', AZ: 'Arizona', AR: 'Arkansas', CA: 'California',\n  CO: 'Colorado', CT: 'Connecticut', DE: 'Delaware', DC: 'District of Columbia',\n  FL: 'Florida', GA: 'Georgia', HI: 'Hawaii', ID: 'Idaho', IL: 'Illinois',\n  IN: 'Indiana', IA: 'Iowa', KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana',\n  ME: 'Maine', MD: 'Maryland', MA: 'Massachusetts', MI: 'Michigan',\n  MN: 'Minnesota', MS: 'Mississippi', MO: 'Missouri', MT: 'Montana',\n  NE: 'Nebraska', NV: 'Nevada', NH: 'New Hampshire', NJ: 'New Jersey',\n  NM: 'New Mexico', NY: 'New York', NC: 'North Carolina', ND: 'North Dakota',\n  OH: 'Ohio', OK: 'Oklahoma', OR: 'Oregon', PA: 'Pennsylvania',\n  RI: 'Rhode Island', SC: 'South Carolina', SD: 'South Dakota',\n  TN: 'Tennessee', TX: 'Texas', UT: 'Utah', VT: 'Vermont', VA: 'Virginia',\n  WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', WY: 'Wyoming',\n};\n\nconst LIMITS = { amount: [500, 10_000_000], rate: [0, 100], term: [1, 360] };\nconst BAND_LABELS = { short: 'short-term', mid: 'mid-term', long: 'long-term' };\n\nconst esc = (s) => String(s).replace(\/[&<>\"']\/g, (c) => ({\n  '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;',\n}[c]));\n\nfunction todayIso() {\n  const d = new Date();\n  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n}\n\n\/** utm\/click params on the CURRENT (marketing) page, forwarded on the CTA + lead. *\/\nfunction pageAttribution() {\n  const keep = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid', 'msclkid'];\n  const out = {};\n  try {\n    const q = new URLSearchParams(window.location.search);\n    for (const k of keep) {\n      const v = q.get(k);\n      if (v) out[k] = v.slice(0, 255);\n    }\n    if (document.referrer) out.referrer = document.referrer.slice(0, 255);\n  } catch { \/* SSR\/sandbox: no attribution *\/ }\n  return out;\n}\n\nclass ChipkieLoanCalc extends HTMLElement {\n  connectedCallback() {\n    if (this._ready) return;\n    this._ready = true;\n\n    this.market = (this.getAttribute('market') || 'US').toUpperCase();\n    \/\/ MODE \u2014 same engine, different question. One page cannot rank for \"family\n    \/\/ loan calculator\" AND \"how much interest must I charge\", so each search\n    \/\/ intent gets its own page; but a second calculator (or a second AFR read)\n    \/\/ would be the two-paths problem this codebase keeps paying for. The mode\n    \/\/ only reframes what is ALREADY computed.\n    \/\/   calculator        the general tool (default, unchanged)\n    \/\/   imputed-interest  US: leads with the minimum rate the IRS expects\n    this.mode = (this.getAttribute('mode') || 'calculator').toLowerCase();\n    this.configUrl = this.getAttribute('config-url') || 'https:\/\/my.chipkie.com\/api\/calculator\/config';\n    this.leadUrl = this.getAttribute('lead-url')\n      || this.configUrl.replace(\/\\\/config.*$\/, '\/lead');\n    this.ctaUrl = this.getAttribute('cta-url') || 'https:\/\/my.chipkie.com\/loans\/create';\n    this.analyticsOn = this.getAttribute('analytics') !== 'off';\n\n    \/\/ Config payload (AFR\/usury\/prices). Starts with safe fallbacks; the\n    \/\/ fetch upgrades it. Everything tax-related stays DORMANT without data \u2014\n    \/\/ never a guessed rate (mirrors App\\Support\\Afr's philosophy).\n    this.cfg = {\n      market: this.market,\n      currency: { code: this.market === 'GB' ? 'GBP' : this.market === 'AU' ? 'AUD' : 'USD',\n        locale: this.market === 'GB' ? 'en-GB' : this.market === 'AU' ? 'en-AU' : 'en-US' },\n      comparison: {\n        apr: Number(this.getAttribute('comparison-apr')) || 12,\n        source: 'typical bank personal-loan APR',\n        as_of: '',\n      },\n      contract: { available: false, price: null },\n      afr: null, usury: null, gift_tax: null,\n    };\n\n    this.s = { \/\/ scenario state\n      amount: 20000, rate: 0, term: 36, frequency: 'monthly',\n      start: todayIso(), state: '', extra: 0,\n    };\n    this.pinned = [];\n    this._eventsSent = {};\n\n    this.root = this.attachShadow({ mode: 'open' });\n    this.render();\n    this.bind();\n    this.loadConfig();\n    this.recalc();\n  }\n\n  \/\/ \u2500\u2500 static shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  render() {\n    const us = this.market === 'US';\n    const freqOptions = us\n      ? '<option value=\"monthly\">Monthly<\/option><option value=\"fortnightly\">Every 2 weeks<\/option><option value=\"weekly\">Weekly<\/option>'\n      : '<option value=\"monthly\">Monthly<\/option><option value=\"fortnightly\">Fortnightly<\/option><option value=\"weekly\">Weekly<\/option>';\n    const stateOptions = ['<option value=\"\">Select a state (optional)<\/option>']\n      .concat(Object.entries(US_STATES).map(([c, n]) => `<option value=\"${c}\">${esc(n)}<\/option>`)).join('');\n\n    this.root.innerHTML = `\n      <style>${WIDGET_CSS}<\/style>\n      <div class=\"calc\">\n        <div class=\"calc-header\">\n          ${this.mode === 'minimum-rate' ? `\n          <h3>What's the minimum interest rate for a family loan?<\/h3>\n          <p>${{ US: 'In the US there is one, and it moves every month. Enter the amount and term to see it.',\n                 AU: 'In Australia there isn\\u2019t one \\u2014 but a 0% loan has consequences worth knowing about.',\n                 GB: 'In the UK there isn\\u2019t one \\u2014 but what you charge changes what you owe.' }[this.market]\n              || 'It depends where you are. Here\\u2019s what applies to you.'}<\/p>`\n          : this.mode === 'imputed-interest' ? `\n          <h3>How much interest do I have to charge?<\/h3>\n          <p>Enter the amount and how long the loan runs. The minimum rate is the\n            IRS applicable federal rate for that term \u2014 charge less and the\n            difference can be treated as interest you never received.<\/p>`\n          : this.mode === 'gift-or-loan' ? `\n          <h3>Is it a gift or a loan?<\/h3>\n          <p>Same money, two very different sets of consequences. Enter the amount\n            to see what each choice means${{ US: ' for the IRS', AU: ' for the ATO and Centrelink', GB: ' for HMRC' }[this.market] || ''},\n            and what it takes to make a loan actually count as one.<\/p>`\n          : `\n          <h3>Family Loan Calculator<\/h3>\n          <p>Work out repayments for the Bank of ${us ? 'Mom' : 'Mum'} &amp; Dad \u2014 and keep it\n            ${{ US: 'right with the IRS', AU: 'tax-smart with the ATO', GB: 'tax-smart with HMRC' }[this.market] || 'fair for everyone'}.<\/p>`}\n          <div id=\"answer\" class=\"answer\" hidden aria-live=\"polite\"><\/div>\n          <div id=\"compare\" class=\"compare\" hidden aria-live=\"polite\"><\/div>\n          <div id=\"nominimum\" class=\"answer\" hidden aria-live=\"polite\"><\/div>\n        <\/div>\n        <div class=\"cols\">\n          <div class=\"inputs\">\n            <div class=\"field\">\n              <label for=\"amount\">Loan amount<\/label>\n              <input id=\"amount\" type=\"number\" inputmode=\"decimal\" min=\"${LIMITS.amount[0]}\" max=\"${LIMITS.amount[1]}\" step=\"100\" value=\"20000\" autocomplete=\"off\">\n              <input id=\"amount-slider\" type=\"range\" min=\"500\" max=\"250000\" step=\"500\" value=\"20000\" aria-label=\"Loan amount slider\">\n              <div class=\"error\" id=\"amount-err\" hidden><\/div>\n            <\/div>\n            <div class=\"grid2\">\n              <div class=\"field\">\n                <label for=\"rate\">Interest rate (% \/ year)<\/label>\n                <input id=\"rate\" type=\"number\" inputmode=\"decimal\" min=\"0\" max=\"100\" step=\"0.1\" value=\"0\" autocomplete=\"off\">\n                <div class=\"chips\" role=\"group\" aria-label=\"Rate quick picks\">\n                  <button type=\"button\" class=\"chip\" data-rate=\"0\" aria-pressed=\"true\">0%<\/button>\n                  <button type=\"button\" class=\"chip\" id=\"afr-chip\" hidden aria-pressed=\"false\"><\/button>\n                <\/div>\n                <div class=\"error\" id=\"rate-err\" hidden><\/div>\n              <\/div>\n              <div class=\"field\">\n                <label for=\"term\">Term (months)<\/label>\n                <input id=\"term\" type=\"number\" inputmode=\"numeric\" min=\"1\" max=\"360\" step=\"1\" value=\"36\" autocomplete=\"off\">\n                <div class=\"chips\" role=\"group\" aria-label=\"Term quick picks\">\n                  <button type=\"button\" class=\"chip\" data-term=\"12\">1 yr<\/button>\n                  <button type=\"button\" class=\"chip\" data-term=\"24\">2 yrs<\/button>\n                  <button type=\"button\" class=\"chip\" data-term=\"36\" aria-pressed=\"true\">3 yrs<\/button>\n                  <button type=\"button\" class=\"chip\" data-term=\"60\">5 yrs<\/button>\n                <\/div>\n                <div class=\"error\" id=\"term-err\" hidden><\/div>\n              <\/div>\n            <\/div>\n            <div class=\"grid2\">\n              <div class=\"field\">\n                <label for=\"frequency\">Repayment frequency<\/label>\n                <select id=\"frequency\">${freqOptions}<\/select>\n              <\/div>\n              <div class=\"field\">\n                <label for=\"start\">First payment period starts<\/label>\n                <input id=\"start\" type=\"date\" value=\"${todayIso()}\">\n              <\/div>\n            <\/div>\n            ${us ? `\n            <div class=\"field\">\n              <label for=\"state\">Borrower's state<\/label>\n              <select id=\"state\">${stateOptions}<\/select>\n              <div class=\"hint\">Used for state interest-limit guidance \u2014 Chipkie's US loan agreements are governed by the borrower's state.<\/div>\n            <\/div>` : ''}\n            <div id=\"tax-panel\" aria-live=\"polite\"><\/div>\n          <\/div>\n\n          <div class=\"outputs\">\n            <div class=\"results\" aria-live=\"polite\">\n              <div class=\"headline\">\n                <div class=\"label\">Each repayment<\/div>\n                <div class=\"value\" id=\"per\">\u2014<\/div>\n                <div class=\"per\" id=\"per-unit\"><\/div>\n              <\/div>\n              <div class=\"rrow\"><span class=\"k\">Number of payments<\/span><span class=\"v\" id=\"npay\">\u2014<\/span><\/div>\n              <div class=\"rrow\"><span class=\"k\">Total interest<\/span><span class=\"v\" id=\"tint\">\u2014<\/span><\/div>\n              <div class=\"rrow\"><span class=\"k\">Total repaid<\/span><span class=\"v\" id=\"trep\">\u2014<\/span><\/div>\n              <div class=\"rrow\"><span class=\"k\">Debt-free by<\/span><span class=\"v\" id=\"payoff\">\u2014<\/span><\/div>\n              <div class=\"savings\" id=\"savings\" hidden><\/div>\n            <\/div>\n\n            <div class=\"chart-wrap\" id=\"chart-balance\"><\/div>\n            <div class=\"chart-wrap\" id=\"chart-split\"><\/div>\n\n            <div class=\"payoff\">\n              <h4>Pay it off sooner<\/h4>\n              <label for=\"extra\" style=\"font-size:0.85rem;color:var(--ck-muted)\">Extra on every payment: <strong id=\"extra-label\">+0<\/strong><\/label>\n              <input id=\"extra\" type=\"range\" min=\"0\" max=\"500\" step=\"10\" value=\"0\">\n              <div class=\"out\" id=\"payoff-out\"><\/div>\n              <div class=\"teaser\">Chipkie's early-payoff planner tracks this against real payments once your loan is set up.<\/div>\n            <\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"scenarios\" id=\"scenarios\" hidden>\n          <h4 style=\"margin:0 0 6px;font-size:0.95rem;\">Compare scenarios<\/h4>\n          <div class=\"table-scroll\" id=\"scenario-table\"><\/div>\n        <\/div>\n        <div class=\"chips\" style=\"margin-top:12px\">\n          <button type=\"button\" class=\"chip\" id=\"pin\">\uff0b Pin this scenario to compare<\/button>\n          <button type=\"button\" class=\"chip\" id=\"unpin\" hidden>Clear pinned<\/button>\n        <\/div>\n\n        <details class=\"schedule\" id=\"schedule\">\n          <summary>Full payment schedule<\/summary>\n          <div class=\"actions\">\n            <button type=\"button\" class=\"btn\" id=\"csv\">Download CSV<\/button>\n          <\/div>\n          <div class=\"table-scroll\" id=\"schedule-table\"><\/div>\n        <\/details>\n\n        <div class=\"cta\">\n          <h4>Make it official<\/h4>\n          <p>Turn this into a signed agreement with payment tracking, reminders, and a tax-ready annual summary \u2014 set up in minutes.<\/p>\n          <a class=\"go\" id=\"cta-link\" href=\"${esc(this.ctaUrl)}\" target=\"_blank\" rel=\"noopener\">Create this loan with Chipkie \u2192<\/a>\n          <ul id=\"value-stack\">\n            <li>Written contract with recorded acceptance<\/li>\n            <li>Payment tracking &amp; friendly reminders<\/li>\n            <li>Tax-ready annual summary<\/li>\n          <\/ul>\n          <form class=\"capture\" id=\"capture\" novalidate>\n            <label class=\"visually-hidden\" for=\"lead-email\">Email address<\/label>\n            <input id=\"lead-email\" type=\"email\" placeholder=\"you@example.com\" autocomplete=\"email\">\n            <input class=\"hp\" type=\"text\" name=\"website\" tabindex=\"-1\" autocomplete=\"off\" aria-hidden=\"true\">\n            <button type=\"submit\" class=\"btn\" style=\"border-color:#fff;color:#fff\">Save my breakdown<\/button>\n            <span class=\"msg\" id=\"capture-msg\" role=\"status\"><\/span>\n          <\/form>\n        <\/div>\n        <p class=\"smallprint\" id=\"footnote\"><\/p>\n      <\/div>`;\n  }\n\n  $(id) { return this.root.getElementById(id); }\n\n  \/\/ \u2500\u2500 events \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  bind() {\n    const on = (id, ev, fn) => this.$(id) && this.$(id).addEventListener(ev, fn);\n\n    on('amount', 'input', () => { this.s.amount = Number(this.$('amount').value); this.syncSlider(); this.recalc(); });\n    on('amount-slider', 'input', () => {\n      this.s.amount = Number(this.$('amount-slider').value);\n      this.$('amount').value = String(this.s.amount);\n      this.recalc();\n    });\n    on('rate', 'input', () => { this.s.rate = Number(this.$('rate').value); this.markRateChips(); this.recalc(); });\n    on('term', 'input', () => { this.s.term = Number(this.$('term').value); this.markTermChips(); this.recalc(); });\n    on('frequency', 'change', () => { this.s.frequency = this.$('frequency').value; this.recalc(); });\n    on('start', 'change', () => { this.s.start = this.$('start').value || todayIso(); this.recalc(); });\n    on('state', 'change', () => { this.s.state = this.$('state').value; this.recalc(); });\n    on('extra', 'input', () => { this.s.extra = Number(this.$('extra').value); this.renderPayoff(); });\n\n    this.root.querySelectorAll('.chip[data-rate]').forEach((b) =>\n      b.addEventListener('click', () => { this.setRate(Number(b.dataset.rate)); }));\n    this.root.querySelectorAll('.chip[data-term]').forEach((b) =>\n      b.addEventListener('click', () => {\n        this.s.term = Number(b.dataset.term);\n        this.$('term').value = String(this.s.term);\n        this.markTermChips();\n        this.recalc();\n      }));\n\n    on('afr-chip', 'click', () => {\n      const a = this.currentAfr();\n      if (a?.has_data) {\n        this.setRate(a.applicable_afr);\n        this.track('afr_fix', { rate: a.applicable_afr });\n      }\n    });\n\n    on('pin', 'click', () => this.pinScenario());\n    on('unpin', 'click', () => { this.pinned = []; this.renderScenarios(); });\n    on('csv', 'click', () => this.downloadCsv());\n    on('schedule', 'toggle', () => { if (this.$('schedule').open) this.track('schedule_open'); });\n    on('cta-link', 'click', () => this.track('cta_click', { amount: this.s.amount, rate: this.s.rate }));\n    on('capture', 'submit', (e) => { e.preventDefault(); this.submitLead(); });\n  }\n\n  setRate(r) {\n    this.s.rate = phpRound(Number(r), 2);\n    this.$('rate').value = String(this.s.rate);\n    this.markRateChips();\n    this.recalc();\n  }\n\n  syncSlider() {\n    const v = Math.min(250000, Math.max(500, this.s.amount || 500));\n    this.$('amount-slider').value = String(v);\n  }\n\n  markRateChips() {\n    this.root.querySelectorAll('.chip[data-rate]').forEach((b) =>\n      b.setAttribute('aria-pressed', String(Number(b.dataset.rate) === this.s.rate)));\n    const a = this.currentAfr();\n    this.$('afr-chip')?.setAttribute('aria-pressed', String(!!a?.has_data && a.applicable_afr === this.s.rate));\n  }\n\n  markTermChips() {\n    this.root.querySelectorAll('.chip[data-term]').forEach((b) =>\n      b.setAttribute('aria-pressed', String(Number(b.dataset.term) === this.s.term)));\n  }\n\n  \/\/ \u2500\u2500 live config (AFR \/ usury \/ prices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  async loadConfig() {\n    const cacheKey = `ck-calc-config-${this.market}`;\n    try {\n      const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null');\n      if (cached && Date.now() - cached.at < 24 * 3600 * 1000) {\n        this.applyConfig(cached.data);\n      }\n    } catch { \/* ignore bad cache *\/ }\n\n    try {\n      \/\/ data: URLs (demo\/tests) carry the payload inline \u2014 no query appended.\n      const url = this.configUrl.startsWith('data:')\n        ? this.configUrl\n        : `${this.configUrl}?market=${encodeURIComponent(this.market)}`;\n      const res = await fetch(url, { mode: 'cors' });\n      if (!res.ok) return;\n      const data = await res.json();\n      this.applyConfig(data);\n      try { localStorage.setItem(cacheKey, JSON.stringify({ at: Date.now(), data })); } catch { \/* full\/blocked *\/ }\n    } catch { \/* offline \u2192 widget stays fully functional, tax panel dormant *\/ }\n  }\n\n  applyConfig(data) {\n    if (!data || typeof data !== 'object') return;\n    this.cfg = { ...this.cfg, ...data };\n\n    \/\/ In imputed-interest mode the page's whole job is to answer \"what rate?\",\n    \/\/ so answer it on arrival rather than starting at 0% and waiting for the\n    \/\/ visitor to find the AFR chip. ONCE only, and only if they have not\n    \/\/ already typed a rate \u2014 never overwrite a person's own input.\n    if ((this.mode === 'imputed-interest' || this.mode === 'minimum-rate')\n        && !this._afrPrefilled && !this.s.rate) {\n      const a = this.currentAfr();\n      if (a?.has_data && a.applicable_afr) {\n        this._afrPrefilled = true;\n        this.setRate(a.applicable_afr);\n\n        return; \/\/ setRate recalcs\n      }\n    }\n\n    this.recalc();\n  }\n\n  \/**\n   * The headline answer, in imputed-interest mode only.\n   *\n   * Says nothing when there is no AFR data \u2014 the same rule as the rest of the\n   * tax layer and as App\\Support\\Afr: dormant beats guessed. A page that\n   * invents \"0%\" here would be telling a stranger they can lend interest-free.\n   *\/\n  updateAnswer() {\n    const el = this.$('answer');\n    if (!el) return;\n    \/\/ Serves BOTH the imputed-interest page and the minimum-rate page. The US\n    \/\/ answer to \"what's the minimum\" IS the AFR, so this deliberately reuses the\n    \/\/ one AFR read rather than growing a second path to keep in step.\n    if (this.mode !== 'imputed-interest' && this.mode !== 'minimum-rate') { el.hidden = true; return; }\n\n    const a = this.currentAfr();\n    if (!a?.has_data || !a.applicable_afr) {\n      el.hidden = true;\n\n      return;\n    }\n\n    \/\/ \"June 2026\", never the raw \"2026-06\" the API ships. An ISO month in front\n    \/\/ of a customer is the Loan Pulse defect again \u2014 caught by the browser pass\n    \/\/ both times, because it looks fine in a string assertion.\n    const month = this.monthName(this.cfg.afr?.month);\n    const monthTxt = month ? ` for ${esc(month)}` : '';\n    const stale = this.cfg.afr?.stale\n      ? ' <span class=\"answer-note\">\u2014 verify this month\\'s figure before you rely on it<\/span>'\n      : '';\n    \/\/ A cleared Term field is Number('') === 0 \u2014 never headline an AFR \"for a\n    \/\/ 0-month loan\"; ask for the term instead of asserting nonsense.\n    if (!Number.isInteger(this.s.term) || this.s.term < 1) {\n      el.hidden = false;\n      el.innerHTML = `<span class=\"answer-label\">Enter a loan term to see the minimum rate<\/span>`;\n      return;\n    }\n    el.hidden = false;\n    el.innerHTML = `<span class=\"answer-label\">Minimum rate for a ${this.s.term}-month loan<\/span>\n      <strong class=\"answer-value\">${esc(String(a.applicable_afr))}%<\/strong>\n      <span class=\"answer-note\">IRS applicable federal rate${monthTxt}<\/span>${stale}`;\n  }\n\n  \/**\n   * \"There is no minimum\" \\u2014 the AU and GB answer (minimum-rate mode only).\n   *\n   * ONLY THE US HAS A STATUTORY MINIMUM. Australia and the UK have no minimum\n   * interest rate for a private loan at all: 0% is allowed. Inventing a number\n   * for those markets to make the page symmetrical would be the worst kind of\n   * wrong \\u2014 confident, plausible, and acting on it costs the reader money.\n   * (Division 7A's benchmark rate is company-to-shareholder, not friend-to-\n   * friend, and is deliberately not cited here.)\n   *\n   * So the honest answer is \"there isn't one\" plus the constraint that DOES\n   * bite at 0%, taken from the reviewed figures the config already serves. If\n   * we hold no figure, the caveat degrades to qualitative rather than guessing.\n   *\/\n  updateMinimum() {\n    const el = this.$('nominimum');\n    if (!el) return;\n    if (this.mode !== 'minimum-rate' || this.market === 'US') { el.hidden = true; return; }\n\n    const au = this.cfg.au_tax, gb = this.cfg.gb_tax;\n    let caveat = '';\n\n    if (this.market === 'AU' && au?.gifting_free_area) {\n      const asOf = au.as_of ? ` (${esc(String(au.as_of))})` : '';\n      caveat = `But if you are on \\u2014 or heading for \\u2014 an Age Pension, an interest-free\n        loan can be treated as a gift, and Centrelink counts gifts above\n        ${this.money(au.gifting_free_area, 0)} a year${au.gifting_free_area_5yr\n          ? ` or ${this.money(au.gifting_free_area_5yr, 0)} over five years` : ''}${asOf}.\n        Charging interest, and keeping a record of repayments, is what keeps it a loan.`;\n    } else if (this.market === 'GB' && gb?.psa_basic) {\n      const asOf = gb.as_of ? ` (${esc(String(gb.as_of))})` : '';\n      caveat = `But interest you DO receive is savings income. Your Personal Savings\n        Allowance \\u2014 ${this.money(gb.psa_basic, 0)} at the basic rate${gb.psa_higher\n          ? `, ${this.money(gb.psa_higher, 0)} at the higher rate` : ''}${asOf} \\u2014\n        may cover it; anything above that is reportable to HMRC.`;\n    } else {\n      caveat = `What matters instead is that it is clearly a loan: agreed in writing,\n        with a repayment schedule you both keep to.`;\n    }\n\n    el.hidden = false;\n    el.innerHTML = `<span class=\"answer-label\">Minimum interest rate<\/span>\n      <strong class=\"answer-value\">There isn\\u2019t one<\/strong>\n      <span class=\"answer-note\">${caveat}<\/span>`;\n  }\n\n  \/**\n   * The gift-or-loan comparison (gift-or-loan mode only).\n   *\n   * Two cards, same money. Built ENTIRELY from figures the config endpoint\n   * already serves \u2014 gift_tax (US), au_tax (AU), gb_tax (GB) \u2014 because those\n   * are the reviewed ones on the Tax & compliance board. Where we do not hold a\n   * number we stay QUALITATIVE rather than reaching for one from memory: GB gets\n   * no IHT figures here for exactly that reason, matching how the existing GB\n   * advisory already hedges.\n   *\n   * Careful with the gift side. The common overstatement is \"you'll owe gift\n   * tax\" \u2014 for almost everyone the annual exclusion and then the lifetime\n   * exemption mean no tax is actually due, just a form. Saying otherwise would\n   * frighten people out of the right choice.\n   *\/\n  updateCompare() {\n    const el = this.$('compare');\n    if (!el) return;\n    if (this.mode !== 'gift-or-loan') { el.hidden = true; return; }\n\n    const amt = this.s.amount;\n    const gift = [];\n    const loan = [];\n\n    if (this.market === 'US') {\n      const ex = this.cfg.gift_tax?.annual_exclusion;\n      const asOf = this.cfg.gift_tax?.as_of ? ` (${esc(this.cfg.gift_tax.as_of)})` : '';\n      if (ex) {\n        gift.push(amt > ex\n          ? `${this.money(amt, 0)} is over the ${this.money(ex, 0)} annual exclusion${asOf}, so the excess\n             is reportable on Form 709 and counts against your lifetime exemption. Most people owe\n             <strong>no tax<\/strong> \u2014 it is paperwork, not a bill.`\n          : `${this.money(amt, 0)} is within the ${this.money(ex, 0)} annual exclusion${asOf} \u2014 nothing to\n             report, nothing to pay.`);\n      }\n      gift.push('You cannot ask for it back. A gift is final.');\n      const a = this.currentAfr();\n      loan.push(a?.has_data && a.applicable_afr\n        ? `Charge at least the applicable federal rate (<strong>${esc(String(a.applicable_afr))}%<\/strong> for\n           this term) or the IRS can treat the shortfall as interest you received anyway.`\n        : 'Charge at least the IRS applicable federal rate for the term, or the shortfall can be treated as interest you received anyway.');\n      loan.push('Put it in writing. Without a written agreement and a repayment record, the IRS can treat the whole thing as a gift.');\n    }\n\n    if (this.market === 'AU') {\n      const t = this.cfg.au_tax || {};\n      if (t.gifting_free_area) {\n        gift.push(`Counts toward the Centrelink gifting free areas (${this.money(t.gifting_free_area, 0)} a year \/\n          ${this.money(t.gifting_free_area_5yr, 0)} over 5 years${t.as_of ? `, ${esc(t.as_of)}` : ''}), which can\n          affect an Age Pension. This is an <strong>assessment<\/strong> question, not a tax bill.`);\n      }\n      gift.push('You cannot ask for it back. A gift is final.');\n      loan.push('<strong>0% is fine<\/strong> \u2014 the ATO sets no minimum rate for private family loans.');\n      loan.push('If you do charge interest, it is assessable income for you as the lender.');\n      loan.push('Put it in writing. A documented loan with repayments is what keeps it a loan rather than a gift.');\n    }\n\n    if (this.market === 'GB') {\n      const t = this.cfg.gb_tax || {};\n      gift.push('Can matter for inheritance tax depending on the amount and when it was given \u2014 worth checking your own position.');\n      gift.push('You cannot ask for it back. A gift is final.');\n      loan.push('<strong>0% is fine<\/strong> \u2014 HMRC sets no minimum rate for family loans.');\n      if (t.psa_basic) {\n        loan.push(`If you charge interest it is savings income; your Personal Savings Allowance\n          (${this.money(t.psa_basic, 0)} basic \/ ${this.money(t.psa_higher, 0)} higher${t.as_of ? `, ${esc(t.as_of)}` : ''})\n          may cover it.`);\n      }\n      loan.push('Put it in writing. A documented loan stays clearly distinct from a gift.');\n    }\n\n    if (!gift.length && !loan.length) { el.hidden = true; return; }\n\n    el.hidden = false;\n    el.innerHTML = `\n      <div class=\"compare-card\">\n        <h4>If you call it a gift<\/h4>\n        <ul>${gift.map((s) => `<li>${s}<\/li>`).join('')}<\/ul>\n      <\/div>\n      <div class=\"compare-card compare-card--loan\">\n        <h4>If you call it a loan<\/h4>\n        <ul>${loan.map((s) => `<li>${s}<\/li>`).join('')}<\/ul>\n      <\/div>`;\n  }\n\n  \/** 'YYYY-MM' \u2192 'June 2026'. Null when absent or malformed \u2014 never guessed. *\/\n  monthName(iso) {\n    if (!\/^\\d{4}-\\d{2}$\/.test(String(iso || ''))) return null;\n    const [y, m] = String(iso).split('-').map(Number);\n    if (m < 1 || m > 12) return null;\n    try {\n      return new Date(Date.UTC(y, m - 1, 15)).toLocaleDateString(\n        this.cfg.currency?.locale || 'en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' });\n    } catch { return null; }\n  }\n\n  currentAfr() {\n    return afrAssess(this.s.rate, this.s.term, this.s.amount, this.cfg.afr);\n  }\n\n  \/\/ \u2500\u2500 validation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  validate() {\n    const errs = {};\n    const [aMin, aMax] = LIMITS.amount;\n    if (!isFinite(this.s.amount) || this.s.amount < aMin || this.s.amount > aMax) {\n      errs.amount = `Enter an amount between ${this.money(aMin, 0)} and ${this.money(aMax, 0)}.`;\n    }\n    if (!isFinite(this.s.rate) || this.s.rate < LIMITS.rate[0] || this.s.rate > LIMITS.rate[1]) {\n      errs.rate = 'Enter a rate between 0 and 100%.';\n    }\n    if (!Number.isInteger(this.s.term) || this.s.term < LIMITS.term[0] || this.s.term > LIMITS.term[1]) {\n      errs.term = 'Enter a term between 1 and 360 months.';\n    }\n    for (const f of ['amount', 'rate', 'term']) {\n      const el = this.$(`${f}-err`);\n      if (!el) continue;\n      el.hidden = !errs[f];\n      el.textContent = errs[f] || '';\n      this.$(f)?.setAttribute('aria-invalid', String(!!errs[f]));\n    }\n    return Object.keys(errs).length === 0;\n  }\n\n  \/\/ \u2500\u2500 formatting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  money(n, digits = 2) {\n    const c = this.cfg.currency || {};\n    try {\n      return new Intl.NumberFormat(c.locale || 'en-US', {\n        style: 'currency', currency: c.code || 'USD',\n        minimumFractionDigits: digits, maximumFractionDigits: digits,\n      }).format(n);\n    } catch {\n      return `$${Number(n).toFixed(digits)}`;\n    }\n  }\n\n  dateLabel(iso) {\n    try {\n      return new Date(`${iso}T12:00:00Z`).toLocaleDateString(this.cfg.currency?.locale || 'en-US', {\n        year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC',\n      });\n    } catch { return iso; }\n  }\n\n  perUnit() {\n    return { weekly: 'per week', fortnightly: this.market === 'US' ? 'every 2 weeks' : 'per fortnight', monthly: 'per month' }[this.s.frequency];\n  }\n\n  \/\/ \u2500\u2500 the pipeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  scheduleFor(ratePct) {\n    const end = addMonthsIso(this.s.start, this.s.term);\n    return buildSchedule({\n      amount: this.s.amount, interest: ratePct,\n      startDate: this.s.start, endDate: end, frequency: this.s.frequency,\n    });\n  }\n\n  recalc() {\n    \/\/ Before the validity gate: the answer depends on TERM and the AFR data,\n    \/\/ not on the whole scenario being valid, and it is the reason the page\n    \/\/ exists. A typo in the amount must not blank out the number they came for.\n    this.updateAnswer();\n    this.updateCompare();\n    this.updateMinimum();\n\n    if (!this.validate()) {\n      this.root.querySelector('.results').style.opacity = '0.45';\n      return;\n    }\n    this.root.querySelector('.results').style.opacity = '';\n\n    const end = addMonthsIso(this.s.start, this.s.term);\n    const n = numInstalments(this.s.start, end, this.s.frequency);\n    if (n < 1) {\n      this.$('per').textContent = '\u2014';\n      this.$('per-unit').textContent = 'Term too short for this frequency';\n      return;\n    }\n\n    const sched = this.scheduleFor(this.s.rate);\n    const per = instalmentAmount(this.s.amount, this.s.rate, this.s.start, end, this.s.frequency);\n    this.current = { sched, per, end, n };\n\n    this.$('per').textContent = this.money(per);\n    this.$('per-unit').textContent = this.perUnit();\n    this.$('npay').textContent = String(n);\n    this.$('tint').textContent = this.money(sched.totals.interest);\n    this.$('trep').textContent = this.money(sched.totals.repaid);\n    this.$('payoff').textContent = this.dateLabel(sched.rows[n - 1].due_date);\n\n    this.renderSavings(sched);\n    this.renderTaxPanel();\n    this.renderCharts(sched);\n    this.renderPayoff();\n    this.renderSchedule(sched);\n    this.updateCta();\n    this.renderFootnote();\n    this.trackDebounced('used', { amount: this.s.amount, rate: this.s.rate, term: this.s.term, frequency: this.s.frequency });\n  }\n\n  renderSavings(sched) {\n    const cmp = this.cfg.comparison;\n    const box = this.$('savings');\n    if (!cmp || !(this.s.rate < cmp.apr)) { box.hidden = true; this._bankSched = null; return; }\n    \/\/ Always refresh the comparison schedule (the balance chart overlays it)\n    \/\/ even when the savings are too small to celebrate.\n    const bank = this.scheduleFor(cmp.apr);\n    this._bankSched = bank;\n    const saved = phpRound(bank.totals.repaid - sched.totals.repaid, 2);\n    if (saved <= 10) { box.hidden = true; return; }\n    box.hidden = false;\n    box.innerHTML = `\ud83c\udf89 A family loan at ${esc(String(this.s.rate))}% saves <strong>${esc(this.money(saved))}<\/strong>\n      vs a bank loan at ${esc(String(cmp.apr))}% APR.\n      <span class=\"src\">Comparison: ${esc(cmp.source || 'bank personal-loan APR')}${cmp.as_of ? `, ${esc(cmp.as_of)}` : ''}.<\/span>`;\n  }\n\n  \/\/ \u2500\u2500 US tax intelligence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  renderTaxPanel() {\n    const panel = this.$('tax-panel');\n    if (!panel) return;\n    if (this.market !== 'US') { this.renderNonUsTaxPanel(panel); return; }\n\n    const parts = [];\n    const a = this.currentAfr();\n    const chip = this.$('afr-chip');\n\n    if (a.has_data) {\n      const afrPct = `${a.applicable_afr}%`;\n      \/\/ \"June 2026\", never the raw \"2026-06\" the API ships. This shipped LIVE\n      \/\/ reading \"\u2026for this term (short-term (2026-06))\" while the answer block\n      \/\/ on the SAME page formatted it correctly \u2014 one call site was fixed and\n      \/\/ its sibling was not. Found by a browser pass, because an ISO month\n      \/\/ passes every string assertion you would think to write.\n      const monthName = this.monthName(this.cfg.afr?.month);\n      const month = monthName ? ` (${monthName})` : '';\n      const stale = this.cfg.afr?.stale ? ' \u2014 may be out of date, verify the current month' : '';\n      if (chip) {\n        chip.hidden = false;\n        chip.textContent = `AFR ${afrPct}`;\n        chip.title = 'Set the rate to the IRS Applicable Federal Rate for this term';\n      }\n      parts.push(`<div class=\"advice info\">IRS Applicable Federal Rate for this term\n        (${BAND_LABELS[a.band]}${month}): <strong>${esc(afrPct)}<\/strong>${esc(stale)}.<\/div>`);\n\n      if (a.below) {\n        const gt = this.cfg.gift_tax;\n        const gtLine = gt?.annual_exclusion\n          ? ` Forgone interest may also count toward the annual gift-tax exclusion (${this.money(gt.annual_exclusion, 0)} per person${gt.as_of ? `, ${esc(gt.as_of)}` : ''}).`\n          : '';\n        parts.push(`<div class=\"advice warn\"><span class=\"title\">Below-market rate \u2014 the IRS may impute interest<\/span>\n          For a fixed-term loan, charging less than the AFR can mean the IRS treats the difference as taxable\n          \"imputed\" interest to the lender (IRC \u00a77872), even when the principal is $10,000 or less.${gtLine}\n          <div class=\"fix\"><button type=\"button\" class=\"chip\" id=\"afr-fix\">Use the AFR rate (${esc(afrPct)})<\/button><\/div>\n        <\/div>`);\n        this.trackOnce(`afr_warning-${this.s.rate}-${this.s.amount}`, 'afr_warning', { rate: this.s.rate, amount: this.s.amount });\n      }\n    } else if (chip) {\n      chip.hidden = true;\n    }\n\n    if (this.s.state && this.cfg.usury?.states) {\n      const u = usuryCheck(this.s.rate, this.s.state, this.cfg.usury.states);\n      if (u.status === 'over') {\n        parts.push(`<div class=\"advice alert\"><span class=\"title\">Above ${esc(US_STATES[this.s.state] || this.s.state)}'s commonly-cited interest cap<\/span>\n          ${esc(US_STATES[this.s.state] || this.s.state)}'s general usury limit is commonly cited at <strong>${esc(String(u.cap))}%<\/strong>\n          \u2014 this rate may not be enforceable there.\n          <div class=\"fix\"><button type=\"button\" class=\"chip\" id=\"usury-fix\">Reduce to ${esc(String(u.cap))}%<\/button><\/div>\n        <\/div>`);\n        this.trackOnce(`usury-${this.s.state}`, 'usury_warning', { state: this.s.state, rate: this.s.rate });\n      } else if (u.status === 'unknown' && this.s.rate >= 10) {\n        parts.push(`<div class=\"advice info\">We don't have ${esc(US_STATES[this.s.state] || this.s.state)}'s interest limit on file \u2014\n          at ${esc(String(this.s.rate))}% it's worth checking your state's usury cap.<\/div>`);\n      }\n    }\n\n    \/\/ Re-rendering destroys the element holding keyboard focus (activating\n    \/\/ \"Use the AFR rate\" via Enter dropped focus to <body>, restarting the Tab\n    \/\/ order from the page top). Remember which fix button had focus and give\n    \/\/ it back to its replacement.\n    const focusedFix = (panel.contains(document.activeElement) && document.activeElement.id) || null;\n    panel.innerHTML = parts.join('');\n    if (focusedFix) panel.querySelector('#' + focusedFix)?.focus();\n    panel.querySelector('#afr-fix')?.addEventListener('click', () => {\n      this.setRate(a.applicable_afr);\n      this.track('afr_fix', { rate: a.applicable_afr });\n    });\n    panel.querySelector('#usury-fix')?.addEventListener('click', () => {\n      const u = usuryCheck(this.s.rate, this.s.state, this.cfg.usury.states);\n      if (u.cap != null) this.setRate(u.cap);\n    });\n    this.markRateChips();\n  }\n\n  \/**\n   * AU \/ GB advisory modules \u2014 same hedged, guidance-only voice as the app's\n   * tax-summary notes. Figures come from the config endpoint (au_tax\/gb_tax)\n   * so the embed never hard-codes a reviewable number; without data the\n   * panel stays dormant, exactly like the US AFR panel.\n   *\/\n  renderNonUsTaxPanel(panel) {\n    const parts = [];\n\n    if (this.market === 'AU' && this.cfg.au_tax) {\n      const t = this.cfg.au_tax;\n      if (this.s.rate > 0) {\n        parts.push(`<div class=\"advice info\">Interest you receive is generally <strong>assessable income<\/strong>\n          for you as the lender \u2014 Chipkie's tax-ready annual summary keeps the record for tax time.<\/div>`);\n      } else if (t.gifting_free_area) {\n        parts.push(`<div class=\"advice info\">No ATO minimum rate applies to private family loans \u2014 <strong>0% is fine<\/strong>.\n          A written agreement keeps it clearly a loan rather than a gift, which can matter for Age Pension\n          gifting rules (gifting free areas: ${this.money(t.gifting_free_area, 0)} a year \/\n          ${this.money(t.gifting_free_area_5yr, 0)} over 5 years${t.as_of ? `, ${esc(t.as_of)}` : ''}).<\/div>`);\n      }\n    }\n\n    if (this.market === 'GB' && this.cfg.gb_tax) {\n      const t = this.cfg.gb_tax;\n      if (this.s.rate > 0 && t.psa_basic) {\n        parts.push(`<div class=\"advice info\">Interest you receive counts as <strong>savings income<\/strong>.\n          Your Personal Savings Allowance (${this.money(t.psa_basic, 0)} basic rate \/\n          ${this.money(t.psa_higher, 0)} higher rate${t.as_of ? `, ${esc(t.as_of)}` : ''}) may cover some or\n          all of it \u2014 anything above that is reportable to HMRC.<\/div>`);\n      } else if (this.s.rate <= 0) {\n        parts.push(`<div class=\"advice info\">No HMRC minimum rate applies to family loans \u2014 <strong>0% is fine<\/strong>.\n          A written agreement keeps the loan clearly distinct from a gift, which can matter for\n          inheritance-tax purposes.<\/div>`);\n      }\n    }\n\n    panel.innerHTML = parts.join('');\n  }\n\n  \/\/ \u2500\u2500 charts (inline SVG, no deps) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  renderCharts(sched) {\n    const bal = this.$('chart-balance');\n    const rows = sched.rows;\n    const bank = (this.s.rate < (this.cfg.comparison?.apr ?? Infinity)) ? this._bankSched : null;\n    const W = 600; const H = 200; const P = { l: 8, r: 8, t: 8, b: 22 };\n    const n = rows.length;\n    const maxY = this.s.amount;\n    const x = (i) => P.l + (i \/ n) * (W - P.l - P.r);\n    const y = (v) => P.t + (1 - v \/ maxY) * (H - P.t - P.b);\n    const line = (rs) => `M ${x(0)} ${y(this.s.amount)} ` + rs.map((r, i) => `L ${x(i + 1)} ${y(Math.max(0, r.balance))}`).join(' ');\n\n    const bankPath = bank && bank.rows.length === n\n      ? `<path d=\"${line(bank.rows)}\" fill=\"none\" stroke=\"var(--ck-highlight)\" stroke-width=\"2\" stroke-dasharray=\"5 4\"\/>`\n      : '';\n    bal.innerHTML = `\n      <h4 id=\"chart-balance-h\">Balance over time<\/h4>\n      <svg viewBox=\"0 0 ${W} ${H}\" role=\"img\" aria-labelledby=\"chart-balance-h\">\n        <line x1=\"${P.l}\" y1=\"${y(0)}\" x2=\"${W - P.r}\" y2=\"${y(0)}\" stroke=\"var(--ck-border)\"\/>\n        <line x1=\"${P.l}\" y1=\"${y(maxY \/ 2)}\" x2=\"${W - P.r}\" y2=\"${y(maxY \/ 2)}\" stroke=\"var(--ck-border)\" stroke-dasharray=\"2 4\"\/>\n        ${bankPath}\n        <path d=\"${line(rows)}\" fill=\"none\" stroke=\"var(--ck-accent-dark)\" stroke-width=\"2.5\"\/>\n        <text x=\"${P.l}\" y=\"${H - 6}\" font-size=\"11\" fill=\"var(--ck-muted)\">start<\/text>\n        <text x=\"${W - P.r}\" y=\"${H - 6}\" font-size=\"11\" fill=\"var(--ck-muted)\" text-anchor=\"end\">${esc(this.dateLabel(rows[n - 1].due_date))}<\/text>\n      <\/svg>\n      <div class=\"legend\">\n        <span><span class=\"sw\" style=\"background:var(--ck-accent-dark)\"><\/span>This family loan<\/span>\n        ${bankPath ? `<span><span class=\"sw\" style=\"background:var(--ck-highlight)\"><\/span>Bank loan at ${esc(String(this.cfg.comparison.apr))}% APR<\/span>` : ''}\n      <\/div>`;\n\n    const split = this.$('chart-split');\n    const ti = sched.totals.interest; const tp = sched.totals.principal;\n    const total = ti + tp;\n    const pw = total > 0 ? (tp \/ total) * 100 : 100;\n    split.innerHTML = `\n      <h4 id=\"chart-split-h\">Where each ${this.market === 'US' ? 'dollar' : 'payment'} goes<\/h4>\n      <svg viewBox=\"0 0 600 44\" role=\"img\" aria-labelledby=\"chart-split-h\">\n        <rect x=\"0\" y=\"6\" width=\"${(pw \/ 100) * 600}\" height=\"22\" rx=\"4\" fill=\"var(--ck-accent-dark)\"\/>\n        <rect x=\"${(pw \/ 100) * 600}\" y=\"6\" width=\"${600 - (pw \/ 100) * 600}\" height=\"22\" rx=\"4\" fill=\"var(--ck-highlight)\"\/>\n      <\/svg>\n      <div class=\"legend\">\n        <span><span class=\"sw\" style=\"background:var(--ck-accent-dark)\"><\/span>Principal ${esc(this.money(tp))}<\/span>\n        <span><span class=\"sw\" style=\"background:var(--ck-highlight)\"><\/span>Interest ${esc(this.money(ti))}<\/span>\n      <\/div>`;\n  }\n\n  \/\/ \u2500\u2500 early payoff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  renderPayoff() {\n    if (!this.current) return;\n    const { sched, per } = this.current;\n    const inc = per < 50 ? 5 : per < 500 ? 10 : 50;\n    const slider = this.$('extra');\n    slider.max = String(inc * 20);\n    slider.step = String(inc);\n    if (this.s.extra > Number(slider.max)) this.s.extra = 0;\n    slider.value = String(this.s.extra);\n    this.$('extra-label').textContent = `+${this.money(this.s.extra, 0)} ${this.perUnit()}`;\n\n    const out = this.$('payoff-out');\n    if (this.s.extra <= 0) {\n      out.innerHTML = `<span style=\"color:var(--ck-muted)\">Drag to see how a little extra shortens the loan.<\/span>`;\n      return;\n    }\n    const p = earlyPayoff(sched, this.s.amount, this.s.extra);\n    if (p.state === 'pays_off_now') {\n      out.innerHTML = `That extra clears the whole loan on the first payment \u2014 debt-free by <strong>${esc(this.dateLabel(p.new_payoff))}<\/strong>.`;\n      return;\n    }\n    const saved = p.interest_saved > 0 ? ` and save <strong>${esc(this.money(p.interest_saved))}<\/strong> in interest` : '';\n    out.innerHTML = `Pay <strong>${esc(this.money(this.s.extra, 0))}<\/strong> extra ${esc(this.perUnit())} \u2192\n      debt-free <strong>${p.payments_saved} payment${p.payments_saved === 1 ? '' : 's'} sooner<\/strong>\n      (${esc(this.dateLabel(p.new_payoff))})${saved}.`;\n  }\n\n  \/\/ \u2500\u2500 scenarios \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  pinScenario() {\n    if (!this.current) {\n      \/\/ A silent no-op reads as a broken button (UX audit): say what's needed.\n      const wrap = this.$('scenarios');\n      if (wrap) {\n        wrap.hidden = false;\n        wrap.innerHTML = '<p class=\"answer-note\">Fix the highlighted fields above, then pin the scenario to compare.<\/p>';\n      }\n      return;\n    }\n    if (this.pinned.length >= 3) this.pinned.shift();\n    const { sched, per, n } = this.current;\n    this.pinned.push({\n      amount: this.s.amount, rate: this.s.rate, term: this.s.term,\n      frequency: this.s.frequency, per, n,\n      interest: sched.totals.interest, repaid: sched.totals.repaid,\n      payoff: sched.rows[n - 1].due_date,\n    });\n    this.renderScenarios();\n    this.track('pin', { count: this.pinned.length });\n  }\n\n  renderScenarios() {\n    const wrap = this.$('scenarios');\n    const unpin = this.$('unpin');\n    if (this.pinned.length === 0) { wrap.hidden = true; unpin.hidden = true; return; }\n    wrap.hidden = false; unpin.hidden = false;\n    const rows = this.pinned.map((p, i) => `<tr>\n      <td>Scenario ${String.fromCharCode(65 + i)}<\/td>\n      <td>${esc(this.money(p.amount, 0))}<\/td>\n      <td>${esc(String(p.rate))}%<\/td>\n      <td>${p.term} mo<\/td>\n      <td>${esc(this.money(p.per))} ${esc({ weekly: '\/wk', fortnightly: '\/2wk', monthly: '\/mo' }[p.frequency])}<\/td>\n      <td>${esc(this.money(p.interest))}<\/td>\n      <td>${esc(this.money(p.repaid))}<\/td>\n    <\/tr>`).join('');\n    this.$('scenario-table').innerHTML = `<table>\n      <thead><tr><th scope=\"col\">Pinned<\/th><th scope=\"col\">Amount<\/th><th scope=\"col\">Rate<\/th><th scope=\"col\">Term<\/th>\n      <th scope=\"col\">Repayment<\/th><th scope=\"col\">Interest<\/th><th scope=\"col\">Total repaid<\/th><\/tr><\/thead>\n      <tbody>${rows}<\/tbody><\/table>`;\n  }\n\n  \/\/ \u2500\u2500 schedule table + CSV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  renderSchedule(sched) {\n    const rows = sched.rows;\n    const cells = (r) => `<tr><td>${r.number}<\/td><td>${esc(this.dateLabel(r.due_date))}<\/td>\n      <td>${esc(this.money(r.payment))}<\/td><td>${esc(this.money(r.interest))}<\/td>\n      <td>${esc(this.money(r.principal))}<\/td><td>${esc(this.money(r.balance))}<\/td><\/tr>`;\n    \/\/ Very long weekly schedules: elide the middle in the TABLE (CSV has it all).\n    let body;\n    if (rows.length > 120) {\n      body = rows.slice(0, 100).map(cells).join('')\n        + `<tr><td colspan=\"6\" style=\"text-align:center;color:var(--ck-muted)\">\u2026 ${rows.length - 112} payments elided \u2014 download the CSV for every row \u2026<\/td><\/tr>`\n        + rows.slice(-12).map(cells).join('');\n    } else {\n      body = rows.map(cells).join('');\n    }\n    this.$('schedule-table').innerHTML = `<table>\n      <thead><tr><th scope=\"col\">#<\/th><th scope=\"col\">Due<\/th><th scope=\"col\">Payment<\/th>\n      <th scope=\"col\">Interest<\/th><th scope=\"col\">Principal<\/th><th scope=\"col\">Balance<\/th><\/tr><\/thead>\n      <tbody>${body}<\/tbody><\/table>`;\n  }\n\n  downloadCsv() {\n    if (!this.current) return;\n    const { sched } = this.current;\n    const lines = ['number,due_date,payment,interest,principal,balance'];\n    for (const r of sched.rows) {\n      lines.push([r.number, r.due_date, r.payment.toFixed(2), r.interest.toFixed(2), r.principal.toFixed(2), r.balance.toFixed(2)].join(','));\n    }\n    lines.push('', `# Chipkie family loan calculator \u2014 ${this.s.amount} at ${this.s.rate}% over ${this.s.term} months (${this.s.frequency})`);\n    const blob = new Blob([lines.join('\\n')], { type: 'text\/csv' });\n    const a = document.createElement('a');\n    a.href = URL.createObjectURL(blob);\n    a.download = 'chipkie-family-loan-schedule.csv';\n    a.click();\n    setTimeout(() => URL.revokeObjectURL(a.href), 5000);\n    this.track('csv');\n  }\n\n  \/\/ \u2500\u2500 funnel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  updateCta() {\n    const q = new URLSearchParams({\n      mode: 'chat',\n      market: this.market,\n      amount: String(this.s.amount),\n      rate: String(this.s.rate),\n      frequency: this.s.frequency,\n      term_months: String(this.s.term),\n      ck_src: 'calculator',\n      ck_calc: `a${this.s.amount}-r${this.s.rate}-t${this.s.term}-${this.s.frequency}`,\n    });\n    if (this.s.state) q.set('state', this.s.state);\n    for (const [k, v] of Object.entries(pageAttribution())) {\n      if (k !== 'referrer') q.set(k, v);\n    }\n    this.$('cta-link').href = `${this.ctaUrl}?${q.toString()}`;\n\n    const price = this.cfg.contract?.available && this.cfg.contract?.price;\n    const stack = this.$('value-stack');\n    if (price && !stack.querySelector('.price-li')) {\n      const li = document.createElement('li');\n      li.className = 'price-li';\n      li.textContent = `Contract from ${this.money(price)} \u2014 no subscription needed`;\n      stack.appendChild(li);\n    }\n    \/\/ Full markets (AU) also sell the automated-payments subscription.\n    const premium = this.cfg.premium?.available && this.cfg.premium?.price;\n    if (premium && !stack.querySelector('.premium-li')) {\n      const li = document.createElement('li');\n      li.className = 'premium-li';\n      li.textContent = `Optional automated payments from ${this.money(premium)}\/mo`;\n      stack.appendChild(li);\n    }\n  }\n\n  async submitLead() {\n    const email = this.$('lead-email').value.trim();\n    const msg = this.$('capture-msg');\n    if (!\/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$\/.test(email)) {\n      msg.textContent = 'Please enter a valid email.';\n      return;\n    }\n    const { sched, per, n } = this.current || {};\n    const body = {\n      email,\n      market: this.market,\n      state: this.s.state || null,\n      website: this.root.querySelector('.hp').value, \/\/ honeypot\n      scenario: {\n        amount: this.s.amount, rate: this.s.rate, term_months: this.s.term,\n        frequency: this.s.frequency, start: this.s.start,\n        per_payment: per ?? null, payments: n ?? null,\n        total_interest: sched?.totals.interest ?? null,\n        total_repaid: sched?.totals.repaid ?? null,\n      },\n      attribution: pageAttribution(),\n    };\n    msg.textContent = 'Saving\u2026';\n    try {\n      const res = await fetch(this.leadUrl, {\n        method: 'POST', mode: 'cors',\n        headers: { 'Content-Type': 'application\/json', Accept: 'application\/json' },\n        body: JSON.stringify(body),\n      });\n      if (!res.ok) throw new Error(String(res.status));\n      msg.textContent = 'Saved! Grab the CSV above for a copy now \u2014 and watch your inbox.';\n      this.$('lead-email').value = '';\n      this.track('email_capture');\n    } catch {\n      msg.textContent = 'Couldn\u2019t save right now \u2014 the Download CSV button above works offline.';\n    }\n  }\n\n  \/\/ \u2500\u2500 analytics (dataLayer only; silent no-op without it) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  track(event, params = {}) {\n    if (!this.analyticsOn) return;\n    try { (window.dataLayer = window.dataLayer || []).push({ event: `ck_calc_${event}`, ...params }); } catch { \/* no-op *\/ }\n  }\n\n  trackOnce(key, event, params) {\n    if (this._eventsSent[key]) return;\n    this._eventsSent[key] = true;\n    this.track(event, params);\n  }\n\n  trackDebounced(event, params) {\n    clearTimeout(this._tdb);\n    this._tdb = setTimeout(() => this.track(event, params), 800);\n  }\n\n  renderFootnote() {\n    const bits = ['Figures are a guide only and match how Chipkie schedules loans (reducing-balance).'];\n    if (this.market === 'US') {\n      bits.push('AFR, imputed-interest, gift-tax and state interest-limit notes are general guidance, not tax or legal advice \u2014 please verify current IRS and state rules before relying on them.');\n      if (this.cfg.usury?.as_of) bits.push(`State limits last reviewed ${this.cfg.usury.as_of}.`);\n    } else if (this.market === 'AU') {\n      bits.push('ATO and Age Pension notes are general guidance, not tax or financial advice \u2014 please verify current ATO and Services Australia rules before relying on them.');\n    } else if (this.market === 'GB') {\n      bits.push('Savings-income and inheritance-tax notes are general guidance, not tax advice \u2014 please verify current HMRC rules before relying on them.');\n    }\n    this.$('footnote').textContent = bits.join(' ');\n  }\n}\n\nif (!customElements.get('chipkie-loan-calc')) {\n  customElements.define('chipkie-loan-calc', ChipkieLoanCalc);\n}\n\n})();\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-8f9d1f7 e-flex e-con-boxed e-con e-parent\" data-id=\"8f9d1f7\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-7b9ae67 elementor-widget elementor-widget-text-editor\" data-id=\"7b9ae67\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<style>#family-loan-calculator{scroll-margin-top:140px}\n\/* Chipkie country calculator guide \u2014 add once above the post-calculator blocks. *\/\n.ck-calc-guide {\n  --ck-ink: #073b4c;\n  --ck-ink-soft: #315866;\n  --ck-aqua: #67c8c5;\n  --ck-aqua-pale: #e9f8f7;\n  --ck-coral: #ef6f51;\n  --ck-sand: #fff8eb;\n  --ck-white: #ffffff;\n  --ck-line: #cfe6e5;\n  color: var(--ck-ink);\n  font-family: inherit;\n  font-size: 17px;\n  line-height: 1.65;\n}\n\n.ck-calc-guide *,\n.ck-calc-guide *::before,\n.ck-calc-guide *::after {\n  box-sizing: border-box;\n}\n\n.ck-calc-guide h2,\n.ck-calc-guide h3 {\n  color: var(--ck-ink);\n  line-height: 1.18;\n  margin-top: 0;\n}\n\n.ck-calc-guide h2 {\n  font-size: clamp(1.75rem, 4vw, 2.5rem);\n  margin-bottom: 0.6rem;\n}\n\n.ck-calc-guide h3 {\n  font-size: 1.2rem;\n  margin-bottom: 0.45rem;\n}\n\n.ck-calc-guide p,\n.ck-calc-guide ul,\n.ck-calc-guide ol {\n  margin-top: 0;\n}\n\n.ck-calc-guide a {\n  color: #006a7c;\n  text-decoration-thickness: 0.1em;\n  text-underline-offset: 0.15em;\n}\n\n.ck-calc-guide a:hover {\n  color: #004a59;\n}\n\n.ck-calc-guide a:focus-visible,\n.ck-calc-guide summary:focus-visible {\n  outline: 3px solid var(--ck-coral);\n  outline-offset: 3px;\n}\n\n.ck-guide-section {\n  margin: 0 auto;\n  max-width: 1120px;\n  padding: clamp(2.25rem, 6vw, 4.75rem) clamp(1rem, 3vw, 1.5rem);\n}\n\n.ck-guide-section + .ck-guide-section {\n  border-top: 1px solid var(--ck-line);\n}\n\n.ck-guide-intro {\n  color: var(--ck-ink-soft);\n  font-size: 1.1rem;\n  max-width: 760px;\n}\n\n.ck-card-grid {\n  display: grid;\n  gap: 1rem;\n  grid-template-columns: repeat(3, minmax(0, 1fr));\n  margin-top: 1.75rem;\n}\n\n.ck-card {\n  background: var(--ck-aqua-pale);\n  border: 1px solid var(--ck-line);\n  border-radius: 18px;\n  padding: 1.35rem;\n}\n\n.ck-step-list {\n  counter-reset: ck-step;\n  display: grid;\n  gap: 0.9rem;\n  list-style: none;\n  margin: 1.5rem 0 0;\n  padding: 0;\n}\n\n.ck-step-list li {\n  counter-increment: ck-step;\n  display: grid;\n  gap: 0.15rem 0.9rem;\n  grid-template-columns: 2.5rem 1fr;\n}\n\n.ck-step-list li::before {\n  align-items: center;\n  background: var(--ck-ink);\n  border-radius: 999px;\n  color: var(--ck-white);\n  content: counter(ck-step);\n  display: flex;\n  font-weight: 700;\n  grid-row: 1 \/ span 2;\n  height: 2.5rem;\n  justify-content: center;\n  width: 2.5rem;\n}\n\n.ck-step-list strong {\n  color: var(--ck-ink);\n}\n\n.ck-example {\n  background: var(--ck-sand);\n  border-left: 6px solid var(--ck-coral);\n  border-radius: 16px;\n  padding: clamp(1.25rem, 4vw, 2rem);\n}\n\n.ck-result-grid {\n  display: grid;\n  gap: 0.75rem;\n  grid-template-columns: repeat(4, minmax(0, 1fr));\n  margin: 1.25rem 0;\n}\n\n.ck-result {\n  background: var(--ck-white);\n  border: 1px solid #eadfc9;\n  border-radius: 12px;\n  padding: 1rem;\n}\n\n.ck-result strong,\n.ck-result span {\n  display: block;\n}\n\n.ck-result strong {\n  font-size: 1.25rem;\n}\n\n.ck-result span {\n  color: var(--ck-ink-soft);\n  font-size: 0.88rem;\n}\n\n.ck-context-grid {\n  display: grid;\n  gap: 1rem 1.5rem;\n  grid-template-columns: repeat(2, minmax(0, 1fr));\n  margin-top: 1.5rem;\n}\n\n.ck-context-card {\n  border-top: 4px solid var(--ck-aqua);\n  padding-top: 1rem;\n}\n\n.ck-notice {\n  background: var(--ck-aqua-pale);\n  border-radius: 14px;\n  margin-top: 1.5rem;\n  padding: 1.25rem;\n}\n\n.ck-faq-list {\n  display: grid;\n  gap: 0.75rem;\n  margin-top: 1.5rem;\n}\n\n.ck-faq-list details {\n  background: var(--ck-white);\n  border: 1px solid var(--ck-line);\n  border-radius: 14px;\n  overflow: clip;\n}\n\n.ck-faq-list summary {\n  align-items: center;\n  cursor: pointer;\n  display: flex;\n  font-weight: 700;\n  justify-content: space-between;\n  list-style: none;\n  min-height: 52px;\n  padding: 1rem 3.2rem 1rem 1.1rem;\n  position: relative;\n}\n\n.ck-faq-list summary::-webkit-details-marker {\n  display: none;\n}\n\n.ck-faq-list summary::after {\n  color: var(--ck-coral);\n  content: \"+\";\n  font-size: 1.65rem;\n  font-weight: 500;\n  line-height: 1;\n  position: absolute;\n  right: 1.1rem;\n  top: 50%;\n  transform: translateY(-50%);\n}\n\n.ck-faq-list details[open] summary::after {\n  content: \"\u2212\";\n}\n\n.ck-faq-answer {\n  border-top: 1px solid var(--ck-line);\n  color: var(--ck-ink-soft);\n  padding: 1rem 1.1rem 1.2rem;\n}\n\n.ck-faq-answer p:last-child {\n  margin-bottom: 0;\n}\n\n.ck-source-list {\n  font-size: 0.94rem;\n  margin-bottom: 0;\n}\n\n.ck-review-note {\n  color: var(--ck-ink-soft);\n  font-size: 0.9rem;\n  margin-top: 1.25rem;\n}\n\n.ck-final-cta {\n  background: var(--ck-ink);\n  border-radius: 22px;\n  color: var(--ck-white);\n  margin: 0 auto clamp(2rem, 6vw, 5rem);\n  max-width: 1072px;\n  padding: clamp(1.5rem, 5vw, 3rem);\n  text-align: center;\n}\n\n.ck-final-cta h2,\n.ck-final-cta p {\n  color: var(--ck-white);\n}\n\n.ck-final-cta p {\n  margin-left: auto;\n  margin-right: auto;\n  max-width: 730px;\n}\n\n.ck-button-row {\n  align-items: center;\n  display: flex;\n  flex-wrap: wrap;\n  gap: 0.75rem;\n  justify-content: center;\n  margin-top: 1.25rem;\n}\n\n.ck-button {\n  align-items: center;\n  border: 2px solid var(--ck-aqua);\n  border-radius: 999px;\n  display: inline-flex;\n  font-weight: 700;\n  justify-content: center;\n  min-height: 48px;\n  padding: 0.65rem 1.25rem;\n  text-decoration: none;\n}\n\n.ck-button-primary {\n  background: var(--ck-aqua);\n  color: var(--ck-ink) !important;\n}\n\n.ck-button-secondary {\n  color: var(--ck-white) !important;\n}\n\n.ck-supporting-links {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 0.6rem 1.2rem;\n  justify-content: center;\n  margin: 1.3rem 0 0;\n}\n\n.ck-supporting-links a {\n  color: var(--ck-white);\n}\n\n.ck-visually-hidden {\n  clip: rect(0 0 0 0);\n  clip-path: inset(50%);\n  height: 1px;\n  overflow: hidden;\n  position: absolute;\n  white-space: nowrap;\n  width: 1px;\n}\n\n@media (max-width: 900px) {\n  .ck-card-grid,\n  .ck-result-grid {\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n  }\n}\n\n@media (max-width: 640px) {\n  .ck-calc-guide {\n    font-size: 16px;\n  }\n\n  .ck-card-grid,\n  .ck-context-grid,\n  .ck-result-grid {\n    grid-template-columns: 1fr;\n  }\n\n  .ck-guide-section {\n    padding-left: 0.75rem;\n    padding-right: 0.75rem;\n  }\n\n  .ck-button-row,\n  .ck-supporting-links {\n    align-items: stretch;\n    flex-direction: column;\n  }\n\n  .ck-button {\n    width: 100%;\n  }\n}\n<\/style>\n<div class=\"ck-calc-guide ck-market-uk\">\n  <section class=\"ck-guide-section\" aria-labelledby=\"ck-uk-features-title\">\n    <h2 id=\"ck-uk-features-title\">Plan a family loan with fewer awkward surprises<\/h2>\n    <p class=\"ck-guide-intro\">A family loan may begin with trust, but a clear plan helps protect that trust. Chipkie lets both people explore the numbers before agreeing what is realistic.<\/p>\n\n    <div class=\"ck-card-grid\">\n      <article class=\"ck-card\">\n        <h3>See the complete cost<\/h3>\n        <p>View the repayment amount, number of payments, total interest, total repaid and projected finish date together.<\/p>\n      <\/article>\n      <article class=\"ck-card\">\n        <h3>Compare 0% and interest-bearing loans<\/h3>\n        <p>Model an interest-free loan or enter an annual rate chosen by the family. Compare scenarios before deciding what feels fair and affordable.<\/p>\n      <\/article>\n      <article class=\"ck-card\">\n        <h3>Choose a practical repayment rhythm<\/h3>\n        <p>Calculate monthly, fortnightly or weekly repayments and choose when the first payment period begins.<\/p>\n      <\/article>\n      <article class=\"ck-card\">\n        <h3>Explore earlier repayment<\/h3>\n        <p>Use the extra-payment control to estimate how additional payments could shorten the loan and reduce interest.<\/p>\n      <\/article>\n      <article class=\"ck-card\">\n        <h3>Open the complete schedule<\/h3>\n        <p>Review how each payment is divided between principal and interest. Expand the full repayment schedule or download it as a CSV for your records.<\/p>\n      <\/article>\n      <article class=\"ck-card\">\n        <h3>Continue without re-entering the figures<\/h3>\n        <p>When you are ready, carry the amount, rate, term and frequency into Chipkie to record the loan, choose any optional agreement features you need and begin tracking repayments.<\/p>\n      <\/article>\n    <\/div>\n\n    <p class=\"ck-notice\"><strong>About bank-rate comparisons:<\/strong> any comparison is illustrative. Check the source and review date shown beside the rate; it is not a credit offer or a prediction of the rate available to either person.<\/p>\n  <\/section>\n\n  <section class=\"ck-guide-section\" aria-labelledby=\"ck-uk-howto-title\">\n    <h2 id=\"ck-uk-howto-title\">How to calculate a UK family loan<\/h2>\n    <ol class=\"ck-step-list\">\n      <li><strong>Enter the amount being lent.<\/strong><span>Use the amount the borrower will actually receive, in pounds.<\/span><\/li>\n      <li><strong>Choose the annual interest rate.<\/strong><span>Enter 0% for an interest-free arrangement or add the annual rate you have agreed.<\/span><\/li>\n      <li><strong>Set the repayment term.<\/strong><span>Choose the total number of months. The quick options make it easy to compare one-, two-, three- and five-year plans.<\/span><\/li>\n      <li><strong>Select the repayment frequency.<\/strong><span>Compare monthly, fortnightly and weekly payments using the same loan amount, rate and term.<\/span><\/li>\n      <li><strong>Choose the first payment-period date.<\/strong><span>This helps the calculator estimate when the loan will finish.<\/span><\/li>\n      <li><strong>Review the full result.<\/strong><span>Look beyond the headline repayment. Check the total interest, total repaid, number of payments and projected finish date.<\/span><\/li>\n      <li><strong>Stress-test the arrangement.<\/strong><span>Compare a longer term, a lower rate or an extra regular payment before both people commit.<\/span><\/li>\n      <li><strong>Create the loan when the plan feels workable.<\/strong><span>Chipkie can carry the scenario into loan setup and help both sides keep a shared repayment record.<\/span><\/li>\n    <\/ol>\n  <\/section>\n<\/div>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-86f00e5 e-flex e-con-boxed e-con e-parent\" data-id=\"86f00e5\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-034853b e-con-full e-flex e-con e-child\" data-id=\"034853b\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-550fa23 elementor-widget elementor-widget-text-editor\" data-id=\"550fa23\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<div class=\"ck-calc-guide ck-market-uk\">\n  <section class=\"ck-guide-section\" aria-labelledby=\"ck-uk-example-title\">\n    <div class=\"ck-example\">\n      <h2 id=\"ck-uk-example-title\">Worked example: \u00a320,000 over three years at 5%<\/h2>\n      <p>For a <strong>\u00a320,000<\/strong> family loan repaid monthly over <strong>36 months<\/strong> at a fixed annual rate of <strong>5%<\/strong>, the calculator currently shows:<\/p>\n\n      <div class=\"ck-result-grid\" aria-label=\"Illustrative repayment results\">\n        <div class=\"ck-result\"><strong>\u00a3599.42<\/strong><span>approximate monthly repayment<\/span><\/div>\n        <div class=\"ck-result\"><strong>36<\/strong><span>payments<\/span><\/div>\n        <div class=\"ck-result\"><strong>\u00a31,579.12<\/strong><span>total interest<\/span><\/div>\n        <div class=\"ck-result\"><strong>\u00a321,579.12<\/strong><span>total repaid<\/span><\/div>\n      <\/div>\n\n      <p>At 0% interest, the same \u00a320,000 over 36 monthly payments would be approximately <strong>\u00a3555.56 per month<\/strong>, with no interest added.<\/p>\n      <p>The projected finish date depends on the selected first-payment date. A final payment may also be adjusted by a few pence to clear the remaining balance precisely.<\/p>\n      <p><a href=\"#family-loan-calculator\">Try these figures in the calculator<\/a><\/p>\n    <\/div>\n  <\/section>\n\n  <section class=\"ck-guide-section\" aria-labelledby=\"ck-uk-results-title\">\n    <h2 id=\"ck-uk-results-title\">Understanding the result<\/h2>\n    <ul>\n      <li>The calculator provides a planning estimate based on a fixed annual rate and a reducing-balance repayment schedule. It does not model variable rates, arrangement fees, late charges, missed payments or every possible change to a real loan.<\/li>\n      <li>The bank-rate comparison is illustrative. It should always show its source and the date reviewed; it is not a credit offer or a prediction of the rate available to either person.<\/li>\n      <li>Interest received on a private loan can be taxable savings income. The Personal Savings Allowance may reduce the amount on which tax is paid, but the correct treatment depends on the lender&#8217;s complete tax position.<\/li>\n      <li>An outstanding loan can be relevant when either person dies. HMRC&#8217;s estate forms specifically ask about personal loans owed to a deceased lender. Keep evidence of the original amount, terms, interest and repayments, and obtain professional advice where necessary.<\/li>\n      <li>This calculator provides general information, not financial, legal, mortgage or tax advice. Results are not a loan offer or a guarantee that an agreement will be enforceable.<\/li>\n    <\/ul>\n  <\/section>\n<\/div>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-f697cce e-con-full e-flex e-con e-child\" data-id=\"f697cce\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t<div class=\"elementor-element elementor-element-51ad7d4 e-con-full e-flex e-con e-child\" data-id=\"51ad7d4\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-12adb0a elementor-widget elementor-widget-text-editor\" data-id=\"12adb0a\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<div class=\"ck-calc-guide ck-market-uk\">\n  <section class=\"ck-guide-section\" aria-labelledby=\"ck-uk-context-title\">\n    <h2 id=\"ck-uk-context-title\">Before lending to a family member<\/h2>\n    <div class=\"ck-context-grid\">\n      <article class=\"ck-context-card\">\n        <h3>Check that both sides can afford the plan<\/h3>\n        <p>The borrower should budget for the repayment alongside essential bills and other debts. The lender should consider what would happen if repayments were delayed or the money was not returned when expected.<\/p>\n      <\/article>\n\n      <article class=\"ck-context-card\">\n        <h3>Agree whether the money is a loan or a gift<\/h3>\n        <p>A loan is intended to be repaid; a gift is not. Record the intention clearly before transferring the money and keep an accurate history of repayments.<\/p>\n      <\/article>\n\n      <article class=\"ck-context-card\">\n        <h3>Decide what happens if circumstances change<\/h3>\n        <p>Discuss missed payments, temporary financial difficulty, repayment holidays, early repayment and whether the lender might need the money back sooner. Changes should be agreed and recorded by both people.<\/p>\n      <\/article>\n\n      <article class=\"ck-context-card\">\n        <h3>Consider interest and tax<\/h3>\n        <p>HMRC treats interest received on private loans as interest income. Depending on the lender&#8217;s other income and allowances, some or all of it may be taxable.<\/p>\n        <p>For the 2026\/27 tax year, the Personal Savings Allowance is \u00a31,000 for basic-rate taxpayers and \u00a3500 for higher-rate taxpayers. Additional-rate taxpayers do not receive this allowance. These figures apply to qualifying savings income overall, not separately to each loan.<\/p>\n      <\/article>\n\n      <article class=\"ck-context-card\">\n        <h3>Be open about property deposits<\/h3>\n        <p>If the money will help fund a home purchase, tell the mortgage lender, broker and conveyancer whether it is a loan or a gift. They may ask for evidence of the source of funds and may treat a repayable loan differently when assessing affordability.<\/p>\n      <\/article>\n\n      <article class=\"ck-context-card\">\n        <h3>Get advice for significant or unusual arrangements<\/h3>\n        <p>Consider independent legal or tax advice when a loan is large, secured against property, connected with an estate or trust, made across borders, or part of repeated or commercial lending.<\/p>\n      <\/article>\n    <\/div>\n  <\/section>\n\n  <section class=\"ck-guide-section\" aria-labelledby=\"ck-uk-sources-title\">\n    <h2 id=\"ck-uk-sources-title\">UK guidance used for this page<\/h2>\n    <p class=\"ck-guide-intro\">Use the official and public-service guidance below to check the rules that apply to your circumstances.<\/p>\n    <ul class=\"ck-source-list\">\n      <li>HMRC: <a href=\"https:\/\/www.gov.uk\/hmrc-internal-manuals\/savings-and-investment-manual\/saim2010\" target=\"_blank\" rel=\"noopener\">Interest on private loans is taxable interest<\/a><\/li>\n      <li>GOV.UK: <a href=\"https:\/\/www.gov.uk\/government\/publications\/budget-2025-overview-of-tax-legislation-and-rates-ootlar\/annex-a-rates-and-allowances\" target=\"_blank\" rel=\"noopener\">2026\/27 Personal Savings Allowance<\/a><\/li>\n      <li>MoneyHelper: <a href=\"https:\/\/www.moneyhelper.org.uk\/en\/everyday-money\/credit\/paying-back-money-to-family-and-friends\" target=\"_blank\" rel=\"noopener\">Borrowing from or lending to family<\/a><\/li>\n      <li>HMRC: <a href=\"https:\/\/www.gov.uk\/government\/publications\/inheritance-tax-debts-due-to-the-estate-iht416\" target=\"_blank\" rel=\"noopener\">Personal loans owed to a deceased lender<\/a> and <a href=\"https:\/\/www.gov.uk\/hmrc-internal-manuals\/inheritance-tax-manual\/ihtm19050\" target=\"_blank\" rel=\"noopener\">evidence retained for personal loans<\/a><\/li>\n      <li>MoneyHelper: <a href=\"https:\/\/www.moneyhelper.org.uk\/en\/blog\/buy-or-rent-a-home\/what-is-the-mortgage-loan-application-process\" target=\"_blank\" rel=\"noopener\">Mortgage application and deposit evidence<\/a><\/li>\n      <li>Financial Conduct Authority: <a href=\"https:\/\/handbook.fca.org.uk\/handbook\/perg2\" target=\"_blank\" rel=\"noopener\">Regulated activities and the &#8220;by way of business&#8221; test<\/a><\/li>\n    <\/ul>\n    <p class=\"ck-review-note\"><strong>Last reviewed: 14 August 2026.<\/strong> UK tax and regulatory guidance can change. Verify current rules at the linked sources before relying on this information.<\/p>\n  <\/section>\n<\/div>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-18c90f7 e-con-full e-flex e-con e-child\" data-id=\"18c90f7\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-a11fdcd elementor-widget elementor-widget-text-editor\" data-id=\"a11fdcd\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<div class=\"ck-calc-guide ck-market-uk\">\n  <section class=\"ck-guide-section\" aria-labelledby=\"ck-uk-faq-title\">\n    <h2 id=\"ck-uk-faq-title\">UK family loan calculator FAQs<\/h2>\n    <p class=\"ck-guide-intro\">Clear answers about repayments, interest, tax context and recording a loan between family or friends in the UK.<\/p>\n\n    <div class=\"ck-faq-list\">\n      <details>\n        <summary>How do I calculate repayments on a family loan?<\/summary>\n        <div class=\"ck-faq-answer\"><p>Enter the amount, annual interest rate, repayment term and payment frequency. Chipkie calculates the payment for each period, number of payments, total interest, total repaid and projected finish date using a reducing-balance schedule.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Can a UK family loan be interest-free?<\/summary>\n        <div class=\"ck-faq-answer\"><p>A private family loan can be agreed without interest in many ordinary circumstances, and Chipkie supports a 0% rate. A zero-interest arrangement should still state the amount, repayment expectations and what happens if circumstances change. Get advice where the amount, purpose or structure makes the arrangement significant or unusual.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Is there a minimum HMRC interest rate for family loans?<\/summary>\n        <div class=\"ck-faq-answer\"><p>The UK does not use the US Applicable Federal Rate system for ordinary private family loans. There is no general AFR-style rate that this calculator requires you to charge. That does not mean every interest-free arrangement has the same tax, estate, mortgage or legal consequences.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Is interest from a family loan taxable in the UK?<\/summary>\n        <div class=\"ck-faq-answer\"><p>Interest received on a private loan is generally taxable interest income for the lender. HMRC identifies interest on loans made privately to individuals or companies as interest within the Income Tax rules. Whether tax is payable depends on the lender\u2019s total income, allowances and circumstances.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Does the Personal Savings Allowance cover family-loan interest?<\/summary>\n        <div class=\"ck-faq-answer\"><p>It may. For 2026\/27, the allowance is \u00a31,000 for a basic-rate taxpayer and \u00a3500 for a higher-rate taxpayer; additional-rate taxpayers receive no Personal Savings Allowance. The allowance applies across qualifying savings income, so bank interest and other interest may use some or all of it.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Should a family loan be put in writing?<\/summary>\n        <div class=\"ck-faq-answer\"><p>MoneyHelper recommends recording how much will be repaid and when, and keeping repayment records. A written agreement can also cover the interest rate, early repayment, missed payments and how changes will be agreed. For a substantial or secured loan, ask a solicitor whether a more formal agreement is appropriate.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>What is the difference between a family loan and a gift?<\/summary>\n        <div class=\"ck-faq-answer\"><p>A loan creates an expectation that the money will be repaid; a gift does not. The wording, conduct of both people and actual repayments can all be relevant. Record the intention before transferring the money and do not describe a repayable loan as a gift to a mortgage lender or another third party.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Can a family loan affect Inheritance Tax?<\/summary>\n        <div class=\"ck-faq-answer\"><p>It can. If a lender dies while money remains outstanding, HMRC treats personal loans owed to the deceased as debts due to the estate and asks for their value and supporting evidence. Forgiving or releasing a loan may have different consequences, so estate-planning advice can be important.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Can I use a family loan for a house deposit?<\/summary>\n        <div class=\"ck-faq-answer\"><p>Potentially, but the borrower should disclose the arrangement to the mortgage lender, broker and conveyancer. Mortgage applications commonly require evidence of the deposit and distinguish between money given as a gift and money that must be repaid. A family repayment may also affect mortgage affordability.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Can I calculate weekly or fortnightly repayments?<\/summary>\n        <div class=\"ck-faq-answer\"><p>Yes. Chipkie supports monthly, fortnightly and weekly repayment schedules. Changing the frequency recalculates the schedule rather than simply dividing a monthly figure, helping the calculator remain consistent with Chipkie\u2019s loan setup.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Can the calculator show the effect of extra repayments?<\/summary>\n        <div class=\"ck-faq-answer\"><p>Yes. The early-payment control estimates how an additional amount on each payment could shorten the schedule and reduce interest. It is a projection only; any right or obligation to make extra payments should be recorded in the agreement.<\/p><\/div>\n      <\/details>\n      <details>\n        <summary>Is this calculator financial, legal or tax advice?<\/summary>\n        <div class=\"ck-faq-answer\"><p>No. It is an informational planning tool. Tax treatment, enforceability, mortgage requirements and FCA regulation depend on the facts. FCA authorisation questions are particularly relevant where lending is carried on by way of business; anyone lending repeatedly or commercially should obtain appropriate advice.<\/p><\/div>\n      <\/details>\n    <\/div>\n  <\/section>\n<\/div>\n\n<script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How do I calculate repayments on a family loan?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Enter the amount, annual interest rate, repayment term and payment frequency. Chipkie calculates the payment for each period, number of payments, total interest, total repaid and projected finish date using a reducing-balance schedule.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can a UK family loan be interest-free?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"A private family loan can be agreed without interest in many ordinary circumstances, and Chipkie supports a 0% rate. A zero-interest arrangement should still state the amount, repayment expectations and what happens if circumstances change. Get advice where the amount, purpose or structure makes the arrangement significant or unusual.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Is there a minimum HMRC interest rate for family loans?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"The UK does not use the US Applicable Federal Rate system for ordinary private family loans. There is no general AFR-style rate that this calculator requires you to charge. That does not mean every interest-free arrangement has the same tax, estate, mortgage or legal consequences.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Is interest from a family loan taxable in the UK?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Interest received on a private loan is generally taxable interest income for the lender. HMRC identifies interest on loans made privately to individuals or companies as interest within the Income Tax rules. Whether tax is payable depends on the lender\u2019s total income, allowances and circumstances.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Does the Personal Savings Allowance cover family-loan interest?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"It may. For 2026\/27, the allowance is \u00a31,000 for a basic-rate taxpayer and \u00a3500 for a higher-rate taxpayer; additional-rate taxpayers receive no Personal Savings Allowance. The allowance applies across qualifying savings income, so bank interest and other interest may use some or all of it.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Should a family loan be put in writing?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"MoneyHelper recommends recording how much will be repaid and when, and keeping repayment records. A written agreement can also cover the interest rate, early repayment, missed payments and how changes will be agreed. For a substantial or secured loan, ask a solicitor whether a more formal agreement is appropriate.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What is the difference between a family loan and a gift?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"A loan creates an expectation that the money will be repaid; a gift does not. The wording, conduct of both people and actual repayments can all be relevant. Record the intention before transferring the money and do not describe a repayable loan as a gift to a mortgage lender or another third party.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can a family loan affect Inheritance Tax?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"It can. If a lender dies while money remains outstanding, HMRC treats personal loans owed to the deceased as debts due to the estate and asks for their value and supporting evidence. Forgiving or releasing a loan may have different consequences, so estate-planning advice can be important.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can I use a family loan for a house deposit?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Potentially, but the borrower should disclose the arrangement to the mortgage lender, broker and conveyancer. Mortgage applications commonly require evidence of the deposit and distinguish between money given as a gift and money that must be repaid. A family repayment may also affect mortgage affordability.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can I calculate weekly or fortnightly repayments?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes. Chipkie supports monthly, fortnightly and weekly repayment schedules. Changing the frequency recalculates the schedule rather than simply dividing a monthly figure, helping the calculator remain consistent with Chipkie\u2019s loan setup.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can the calculator show the effect of extra repayments?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes. The early-payment control estimates how an additional amount on each payment could shorten the schedule and reduce interest. It is a projection only; any right or obligation to make extra payments should be recorded in the agreement.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Is this calculator financial, legal or tax advice?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"No. It is an informational planning tool. Tax treatment, enforceability, mortgage requirements and FCA regulation depend on the facts. FCA authorisation questions are particularly relevant where lending is carried on by way of business; anyone lending repeatedly or commercially should obtain appropriate advice.\"\n      }\n    }\n  ]\n}\n<\/script>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-b2fb5c2 e-flex e-con-boxed e-con e-parent\" data-id=\"b2fb5c2\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-9c228ee e-con-full e-flex e-con e-child\" data-id=\"9c228ee\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-12a94f7 elementor-widget elementor-widget-heading\" data-id=\"12a94f7\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Ready to turn the plan into a shared record?<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-606c919 elementor-widget elementor-widget-text-editor\" data-id=\"606c919\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>When both people are comfortable with the figures, return to the result and select <strong>Create this loan with Chipkie<\/strong>. That calculator action carries the amount, rate, term and repayment frequency into loan setup so you can review the plan, choose any optional agreement features you need and track repayments together.<\/p><p><a href=\"https:\/\/chipkie.com\/uk\/how-chipkie-works\/\">See how Chipkie works<\/a> \u00b7 <a href=\"https:\/\/chipkie.com\/uk\/pricing\/\">See UK pricing<\/a> \u00b7 <a href=\"https:\/\/chipkie.com\/uk\/faqs\/\">Read UK FAQs<\/a> \u00b7 <a href=\"https:\/\/chipkie.com\/uk\/contact\/\">Contact Chipkie<\/a><\/p><p><strong>You can continue using the calculator without creating an account.<\/strong><\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-7229bf4 elementor-align-left elementor-mobile-align-center elementor-widget elementor-widget-button\" data-id=\"7229bf4\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"button.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<div class=\"elementor-button-wrapper\">\n\t\t\t\t\t<a class=\"elementor-button elementor-button-link elementor-size-sm\" href=\"#family-loan-calculator\">\n\t\t\t\t\t\t<span class=\"elementor-button-content-wrapper\">\n\t\t\t\t\t\t\t\t\t<span class=\"elementor-button-text\">Review my calculation<\/span>\n\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-680bc08 e-con-full e-flex e-con e-child\" data-id=\"680bc08\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-d2f2888 elementor-widget elementor-widget-image\" data-id=\"d2f2888\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"image.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<img fetchpriority=\"high\" decoding=\"async\" width=\"600\" height=\"463\" src=\"https:\/\/chipkie.com\/uk\/wp-content\/uploads\/sites\/9\/2023\/12\/high-five.png\" class=\"attachment-medium_large size-medium_large wp-image-1204\" alt=\"High five for Chipkie!\" srcset=\"https:\/\/chipkie.com\/uk\/wp-content\/uploads\/sites\/9\/2023\/12\/high-five.png 600w, https:\/\/chipkie.com\/uk\/wp-content\/uploads\/sites\/9\/2023\/12\/high-five-300x232.png 300w\" sizes=\"(max-width: 600px) 100vw, 600px\" \/>\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>UK Family Loan Calculator for Friends and Family Work out a clear repayment plan in pounds before any money changes hands \u2014 with no account needed. Calculate my repayments Compare repayments, interest and timing before you agree Enter the loan amount, annual interest rate, term and repayment frequency to see each repayment, total interest, total &#8230; <a title=\"Family Loan Calculators UK\" class=\"read-more\" href=\"https:\/\/chipkie.com\/uk\/family-loan-calculators-uk\/\" aria-label=\"Read more about Family Loan Calculators UK\">Read more<\/a><\/p>\n","protected":false},"author":3,"featured_media":0,"parent":0,"menu_order":10,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-3656","page","type-page","status-publish"],"_links":{"self":[{"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/pages\/3656","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/comments?post=3656"}],"version-history":[{"count":3,"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/pages\/3656\/revisions"}],"predecessor-version":[{"id":3661,"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/pages\/3656\/revisions\/3661"}],"wp:attachment":[{"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/media?parent=3656"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}