{"id":2986,"date":"2025-12-06T17:56:03","date_gmt":"2025-12-06T06:56:03","guid":{"rendered":"https:\/\/chipkie.com\/?page_id=2986"},"modified":"2026-07-05T13:10:36","modified_gmt":"2026-07-05T03:10:36","slug":"family-loan-calculator","status":"publish","type":"page","link":"https:\/\/chipkie.com\/uk\/family-loan-calculator\/","title":{"rendered":"Family Loan Calculator"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"2986\" class=\"elementor elementor-2986\" 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\">Family Loan Repayment Calculator by Chipkie<\/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\">Planning a loan between friends or family members? Standard bank calculators often fail because they assume high interest rates and rigid terms.<\/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=\"https:\/\/my.chipkie.com\/loans\/create\/\">\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\">Get Started<\/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\">The Chipkie Family Loan Calculator is designed specifically for private lending<\/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 data-path-to-node=\"12\">Whether you are charging commercial interest or providing an interest-free loan (0% interest) to help a child enter the property market, this tool will calculate your exact weekly or monthly repayments instantly.<\/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<!--\r\n  Chipkie Family Loan Calculator \u2014 GB embed (self-contained).\r\n\r\n  HOW TO USE (WordPress\/Elementor): paste this whole block into an HTML\r\n  widget on the GB calculator page. No external scripts, fonts or CSS.\r\n\r\n  This file is GENERATED \u2014 one widget codebase serves every market. Edit\r\n  resources\/marketing\/family-loan-calculator\/src\/ and re-run `node build.mjs`;\r\n  do not hand-edit or fork this file per market (the market attribute below\r\n  is the only intended difference).\r\n\r\n  Attributes on <chipkie-loan-calc>:\r\n    market=\"GB\"                       drives currency, wording and the\r\n                                         market's tax-guidance module\r\n    config-url=\"...\/api\/calculator\/config\"  live AFR\/usury\/PSA\/pricing data\r\n    cta-url=\"...\/loans\/create\"           where \"Create this loan\" deep-links\r\n    comparison-apr=\"12\"                  fallback bank APR if config is unreachable\r\n    analytics=\"off\"                      add to disable dataLayer events\r\n\r\n  Theme overrides (optional) \u2014 set on the page:\r\n    chipkie-loan-calc { --ck-primary:#004053; --ck-accent:#6dc4bc; }\r\n-->\r\n<section aria-label=\"Family loan calculator\">\r\n  <p style=\"position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)\">\r\n    Free family loan calculator for the UK: work out repayments, total\r\n    interest and an amortisation schedule for family loans \u2014 with guidance on\r\n    HMRC savings income and the Personal Savings Allowance \u2014 then create a\r\n    signed loan agreement with Chipkie.\r\n  <\/p>\r\n  <chipkie-loan-calc\r\n    market=\"GB\"\r\n    config-url=\"https:\/\/my.chipkie.com\/api\/calculator\/config\"\r\n    cta-url=\"https:\/\/my.chipkie.com\/loans\/create\"\r\n    comparison-apr=\"12\"\r\n  ><\/chipkie-loan-calc>\r\n<\/section>\r\n<script>\r\n(() => {\r\n'use strict';\r\n\/**\r\n * Chipkie family-loan calculator \u2014 amortisation engine.\r\n *\r\n * EXACT JavaScript port of the app's loan maths so the marketing widget quotes\r\n * the same numbers the app will (docs\/specs\/family-loan-calculator-scope.md\r\n * makes widget\/app divergence a P1 bug):\r\n *   - app\/Services\/LoanService.php      (numInstalments, periodicRate,\r\n *                                        instalmentAmount, getInstalments,\r\n *                                        instalmentDueDate)\r\n *   - app\/Services\/AmortisationService.php (buildSchedule reconcile walk,\r\n *                                        simulatePayoff, suggestExtra)\r\n *   - app\/Support\/Afr.php \/ Usury.php   (assess\/check, data-driven)\r\n *\r\n * Parity is enforced two ways: tests\/Feature\/CalculatorParityTest.php\r\n * regenerates golden-vectors.json from the real PHP services, and\r\n * test\/math.test.mjs asserts this port reproduces it to the cent.\r\n *\/\r\n\r\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\r\n\/\/ PHP's round() pre-corrects float representation (round(2.675, 2) === 2.68);\r\n\/\/ a naive Math.round(x*100)\/100 gives 2.67. Snap to 14 significant digits\r\n\/\/ before rounding, half away from zero, to mirror PHP.\r\nfunction phpRound(x, places = 2) {\r\n  if (!isFinite(x)) return x;\r\n  const f = Math.pow(10, places);\r\n  const scaled = Number((Math.abs(x) * f).toPrecision(14));\r\n  return (Math.sign(x) || 1) * Math.round(scaled) \/ f;\r\n}\r\n\r\nconst round2 = (x) => phpRound(x, 2);\r\n\r\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\r\nfunction dateParts(iso) {\r\n  const [y, m, d] = String(iso).slice(0, 10).split('-').map(Number);\r\n  return { y, m, d };\r\n}\r\n\r\nfunction toUtc(iso) {\r\n  const { y, m, d } = dateParts(iso);\r\n  return new Date(Date.UTC(y, m - 1, d, 12));\r\n}\r\n\r\nfunction isoOf(dt) {\r\n  return dt.toISOString().slice(0, 10);\r\n}\r\n\r\n\/** Whole days from a to b (negative when b is earlier). *\/\r\nfunction diffDays(aIso, bIso) {\r\n  return Math.round((toUtc(bIso) - toUtc(aIso)) \/ 86400000);\r\n}\r\n\r\n\/**\r\n * Calendar-month add with END-OF-MONTH OVERFLOW (Jan 31 + 1mo = Mar 3) \u2014\r\n * the same normalisation PHP's DateTime\/Carbon::addMonths and JS setMonth\r\n * apply, and what CreateLoan.vue's addMonths() produces for the form's\r\n * default end date.\r\n *\/\r\nfunction addMonthsIso(iso, months) {\r\n  const { y, m, d } = dateParts(iso);\r\n  return isoOf(new Date(Date.UTC(y, m - 1 + months, d, 12)));\r\n}\r\n\r\nfunction addDaysIso(iso, days) {\r\n  const { y, m, d } = dateParts(iso);\r\n  return isoOf(new Date(Date.UTC(y, m - 1, d + days, 12)));\r\n}\r\n\r\n\/**\r\n * Full calendar months from a to b \u2014 PHP DateInterval semantics (what\r\n * Carbon::diffInMonths returns for two date-only values): whole-of-calendar\r\n * months, minus one when b's day-of-month hasn't reached a's yet.\r\n *\/\r\nfunction diffInMonths(aIso, bIso) {\r\n  const a = dateParts(aIso);\r\n  const b = dateParts(bIso);\r\n  let months = (b.y - a.y) * 12 + (b.m - a.m);\r\n  if (b.d < a.d) months -= 1;\r\n  return Math.max(0, months);\r\n}\r\n\r\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\r\nconst FREQUENCIES = ['weekly', 'fortnightly', 'monthly'];\r\n\r\n\/** LoanService::numInstalments \u2014 floors the span; end <= start \u2192 0. *\/\r\nfunction numInstalments(startIso, endIso, frequency) {\r\n  if (diffDays(startIso, endIso) <= 0) return 0;\r\n  const days = diffDays(startIso, endIso);\r\n  switch (frequency) {\r\n    case 'weekly':\r\n      return Math.floor(days \/ 7);\r\n    case 'fortnightly':\r\n      \/\/ Carbon: diffInWeeks (int) first, THEN halve and floor.\r\n      return Math.floor(Math.floor(days \/ 7) \/ 2);\r\n    case 'monthly':\r\n      return diffInMonths(startIso, endIso);\r\n    default:\r\n      throw new Error('Invalid frequency');\r\n  }\r\n}\r\n\r\n\/** LoanService::periodicRate \u2014 daily = annual\/365.25; weekly \u00d77, fortnightly \u00d714, monthly = annual\/12. *\/\r\nfunction periodicRate(interestPct, frequency) {\r\n  if (interestPct <= 0) return 0;\r\n  const annual = interestPct \/ 100;\r\n  const daily = annual \/ 365.25;\r\n  switch (frequency) {\r\n    case 'weekly':\r\n      return daily * 7;\r\n    case 'fortnightly':\r\n      return daily * 14;\r\n    case 'monthly':\r\n      return annual \/ 12;\r\n    default:\r\n      throw new Error('Invalid frequency');\r\n  }\r\n}\r\n\r\n\/** LoanService::instalmentAmount \u2014 standard reducing-balance annuity, 2 dp. *\/\r\nfunction instalmentAmount(amount, interestPct, startIso, endIso, frequency) {\r\n  const n = numInstalments(startIso, endIso, frequency);\r\n  if (n < 1) return round2(amount);\r\n  if (interestPct <= 0) return round2(amount \/ n);\r\n  const r = periodicRate(interestPct, frequency);\r\n  const x = Math.pow(1 + r, n);\r\n  return round2((amount * r * x) \/ (x - 1));\r\n}\r\n\r\n\/** LoanService::instalmentDueDate \u2014 index is 0-based; the first payment is one period after start. *\/\r\nfunction instalmentDueDate(startIso, index, frequency) {\r\n  const periods = index + 1;\r\n  switch (frequency) {\r\n    case 'weekly':\r\n      return addDaysIso(startIso, periods * 7);\r\n    case 'fortnightly':\r\n      return addDaysIso(startIso, periods * 14);\r\n    case 'monthly':\r\n      return addMonthsIso(startIso, periods);\r\n    default:\r\n      throw new Error('Invalid frequency');\r\n  }\r\n}\r\n\r\n\/**\r\n * LoanService::getInstalments \u2014 the schedule total distributed across n\r\n * instalments in WHOLE CENTS (leftover cents spread one each across the\r\n * earliest instalments) so the schedule sums exactly to the total.\r\n *\/\r\nfunction getInstalments(startIso, endIso, frequency, amount, interestPct) {\r\n  const n = numInstalments(startIso, endIso, frequency);\r\n  if (n < 1) return [];\r\n\r\n  const per = instalmentAmount(amount, interestPct, startIso, endIso, frequency);\r\n  const total = interestPct > 0 ? round2(per * n) : round2(amount);\r\n\r\n  const totalCents = Math.round(total * 100);\r\n  const base = Math.trunc(totalCents \/ n);\r\n  const remainder = totalCents - base * n;\r\n\r\n  const out = [];\r\n  for (let i = 0; i < n; i++) {\r\n    const cents = base + (i < remainder ? 1 : 0);\r\n    out.push({ amount: cents \/ 100, due_date: instalmentDueDate(startIso, i, frequency) });\r\n  }\r\n  return out;\r\n}\r\n\r\n\/**\r\n * AmortisationService::buildSchedule \u2014 per-payment interest\/principal split on\r\n * the reducing balance, then the reconcile walk (payments fixed; the rounding\r\n * remainder reallocated between interest and principal from the last row\r\n * backward) so principal sums EXACTLY to the loan amount and the final\r\n * balance is exactly zero.\r\n *\/\r\nfunction buildSchedule({ amount, interest, startDate, endDate, frequency }) {\r\n  const principal = round2(Number(amount));\r\n  const rate = Number(interest);\r\n  const instalments = getInstalments(startDate, endDate, frequency, principal, rate);\r\n  const periodic = periodicRate(rate, frequency);\r\n\r\n  const rows = [];\r\n  let balance = principal;\r\n  instalments.forEach((inst, i) => {\r\n    const payment = round2(inst.amount);\r\n    let interestPart = periodic > 0 ? round2(balance * periodic) : 0;\r\n    if (interestPart > payment) interestPart = payment;\r\n    const principalPart = round2(payment - interestPart);\r\n    balance = round2(balance - principalPart);\r\n    rows.push({\r\n      number: i + 1,\r\n      due_date: inst.due_date,\r\n      payment,\r\n      interest: interestPart,\r\n      principal: principalPart,\r\n    });\r\n  });\r\n\r\n  const n = rows.length;\r\n  if (n > 0) {\r\n    const sumP = round2(rows.reduce((s, r) => s + r.principal, 0));\r\n    let residual = round2(principal - sumP);\r\n    for (let i = n - 1; i >= 0 && Math.abs(residual) >= 0.005; i--) {\r\n      const room = residual > 0 ? round2(rows[i].interest) : round2(-rows[i].principal);\r\n      const add = residual > 0 ? Math.min(residual, room) : Math.max(residual, room);\r\n      rows[i].principal = round2(rows[i].principal + add);\r\n      rows[i].interest = round2(rows[i].payment - rows[i].principal);\r\n      residual = round2(residual - add);\r\n    }\r\n  }\r\n\r\n  let bal = principal;\r\n  for (const r of rows) {\r\n    bal = round2(bal - r.principal);\r\n    r.balance = bal;\r\n  }\r\n  const principalSum = round2(principal - bal);\r\n  const repaid = round2(rows.reduce((s, r) => s + r.payment, 0));\r\n  const totalInterest = round2(rows.reduce((s, r) => s + r.interest, 0));\r\n\r\n  return {\r\n    interest_rate: round2(rate),\r\n    frequency,\r\n    projected: true,\r\n    rows,\r\n    totals: {\r\n      principal: round2(principalSum),\r\n      interest: totalInterest,\r\n      repaid,\r\n      instalments: n,\r\n    },\r\n  };\r\n}\r\n\r\n\/\/ \u2500\u2500 Early payoff (AmortisationService::earlyPayoff, 'recurring' mode, from start) \u2500\u2500\r\n\/** AmortisationService::simulatePayoff \u2014 one reducing-balance walk. *\/\r\nfunction simulatePayoff(balance, payment, periodic, guard) {\r\n  let interest = 0;\r\n  let periods = 0;\r\n  let bal = balance;\r\n  while (bal > 0.01 && periods < guard) {\r\n    const charge = periodic <= 0 ? 0 : round2(bal * periodic);\r\n    const principalPart = round2(payment - charge);\r\n    if (principalPart <= 0) break;\r\n    interest = round2(interest + charge);\r\n    bal = round2(bal - principalPart);\r\n    periods++;\r\n  }\r\n  return { interest, periods };\r\n}\r\n\r\n\/** AmortisationService::suggestExtra \u2014 round the payment up to a friendly increment. *\/\r\nfunction suggestExtra(regularPayment) {\r\n  if (regularPayment <= 0) return 0;\r\n  const increment = regularPayment < 50 ? 5 : regularPayment < 500 ? 10 : 50;\r\n  let target = Math.ceil(regularPayment \/ increment) * increment;\r\n  if (target <= regularPayment + 0.005) target += increment;\r\n  return round2(target - regularPayment);\r\n}\r\n\r\n\/**\r\n * Recurring-extra projection from the loan's start (the widget's case: no\r\n * payments made yet, balance = full principal) \u2014 the same walk the app's\r\n * premium early-payoff tool runs, so the teaser numbers match it.\r\n *\/\r\nfunction earlyPayoff(schedule, principal, extra) {\r\n  const rows = schedule.rows;\r\n  const baselineCount = rows.length;\r\n  if (baselineCount === 0 || principal <= 0.01) {\r\n    return { state: 'complete', payments_saved: 0, interest_saved: 0, new_payoff: null };\r\n  }\r\n  const periodic = periodicRate(schedule.interest_rate, schedule.frequency);\r\n  const regular = round2(rows[0].payment);\r\n  const guard = baselineCount + 5;\r\n  const baseline = simulatePayoff(principal, regular, periodic, guard);\r\n  const boosted = round2(regular + Math.max(0, round2(extra)));\r\n\r\n  \/\/ Boosted payment big enough to clear everything in one hit.\r\n  const firstCharge = periodic <= 0 ? 0 : round2(principal * periodic);\r\n  if (boosted >= principal + firstCharge) {\r\n    return {\r\n      state: 'pays_off_now',\r\n      regular_payment: regular,\r\n      payments_saved: Math.max(0, baselineCount - 1),\r\n      interest_saved: round2(Math.max(0, baseline.interest - firstCharge)),\r\n      new_interest: firstCharge,\r\n      new_payoff: rows[0].due_date,\r\n      suggested_extra: suggestExtra(regular),\r\n    };\r\n  }\r\n\r\n  const accel = simulatePayoff(principal, boosted, periodic, guard);\r\n  const periods = Math.max(1, Math.min(accel.periods, baselineCount));\r\n  return {\r\n    state: 'projected',\r\n    regular_payment: regular,\r\n    payments_saved: Math.max(0, baselineCount - periods),\r\n    interest_saved: round2(Math.max(0, baseline.interest - accel.interest)),\r\n    new_interest: accel.interest,\r\n    new_payoff: rows[periods - 1].due_date,\r\n    suggested_extra: suggestExtra(regular),\r\n  };\r\n}\r\n\r\n\/\/ \u2500\u2500 US tax intelligence (App\\Support\\Afr \/ Usury ports; data-driven) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\/**\r\n * Afr::applicableRate + assess. `afr` is the payload from the calculator\r\n * config endpoint: { rates: {short, mid, long}, short_max_months,\r\n * mid_max_months, de_minimis, month }. Missing\/partial data \u2192 dormant\r\n * (has_data false) \u2014 never a guessed 0%.\r\n *\/\r\nfunction afrAssess(ratePct, termMonths, amount, afr) {\r\n  const none = { has_data: false, applicable_afr: null, band: null, below: false, de_minimis_exempt: false };\r\n  const r = afr && afr.rates;\r\n  if (!r || !isFinite(r.short) || !isFinite(r.mid) || !isFinite(r.long)) return none;\r\n\r\n  const shortMax = Number(afr.short_max_months ?? 36);\r\n  const midMax = Number(afr.mid_max_months ?? 108);\r\n  const band = termMonths <= shortMax ? 'short' : termMonths <= midMax ? 'mid' : 'long';\r\n  const applicable = Number(r[band]);\r\n  const deMinimis = Number(afr.de_minimis ?? 10000);\r\n\r\n  return {\r\n    has_data: true,\r\n    applicable_afr: applicable,\r\n    band,\r\n    below: ratePct != null && ratePct < applicable - 0.0001,\r\n    de_minimis_exempt: amount != null && amount <= deMinimis,\r\n  };\r\n}\r\n\r\n\/**\r\n * Usury::check. `states` is the config map (lowercase state name\/code \u2192\r\n * number | 'none'; absent = not assessed). Conservative: unknown state \u2192\r\n * 'unknown' (\"check your state\"), never a guessed cap.\r\n *\/\r\nfunction usuryCheck(ratePct, state, states) {\r\n  const empty = { status: 'unknown', cap: null, state: state || null };\r\n  if (!state || !states) return empty;\r\n  const v = states[String(state).trim().toLowerCase()];\r\n  if (v === undefined || v === null) return empty;\r\n  if (typeof v === 'string' && v.toLowerCase() === 'none') return { status: 'none', cap: null, state };\r\n  const cap = Number(v);\r\n  if (!isFinite(cap)) return empty;\r\n  const over = ratePct != null && ratePct > cap + 0.0001;\r\n  return { status: over ? 'over' : 'ok', cap, state };\r\n}\r\n\r\n\/**\r\n * <chipkie-loan-calc> \u2014 Chipkie's family loan calculator (marketing embed).\r\n *\r\n * Self-contained custom element (shadow DOM: no style\/ID collisions inside\r\n * WordPress\/Elementor). All amortisation maths is the exact LoanService port\r\n * in math.mjs, so the numbers here are the numbers the app quotes.\r\n *\r\n * Attributes (all optional):\r\n *   market=\"US\"            \u2014 market code; US enables the tax-intelligence panel\r\n *   config-url=\"\u2026\"         \u2014 GET endpoint (api.calculator.config); live AFR \/\r\n *                            usury \/ prices. Unreachable \u2192 tax panel dormant.\r\n *   lead-url=\"\u2026\"           \u2014 POST endpoint (api.calculator.lead)\r\n *   cta-url=\"\u2026\"            \u2014 loan-create deep-link base (\u2026\/loans\/create)\r\n *   comparison-apr=\"12\"    \u2014 fallback bank APR if the config fetch fails\r\n *   analytics=\"off\"        \u2014 disable dataLayer events\r\n *\r\n * Theming: override any --ck-* custom property from page CSS, e.g.\r\n *   chipkie-loan-calc { --ck-primary: #004053; }\r\n *\/\r\n\r\n\r\nconst WIDGET_CSS = `\/* Chipkie family-loan calculator \u2014 shadow-DOM styles.\r\n   Brand tokens (tailwind.config.js) as custom properties on :host; pages can\r\n   override any --ck-* token from outside (custom props pierce the shadow). *\/\r\n\r\n:host {\r\n  \/* Brand (defaults = Chipkie palette) *\/\r\n  --ck-primary: #004053;        \/* navy *\/\r\n  --ck-accent: #6dc4bc;         \/* brand teal *\/\r\n  --ck-accent-dark: #007c89;    \/* dark teal *\/\r\n  --ck-highlight: rgb(245, 131, 103); \/* salmon *\/\r\n  --ck-bg: #ffffff;\r\n  --ck-panel: #f2f8f8;          \/* light teal wash *\/\r\n  --ck-text: #1a202c;\r\n  --ck-muted: #52606d;\r\n  --ck-border: #d8e4e6;\r\n  --ck-good-bg: #e3f5ee;\r\n  --ck-good-text: #14532d;\r\n  --ck-warn-bg: #fdf3e3;\r\n  --ck-warn-border: #eab308;\r\n  --ck-warn-text: #713f12;\r\n  --ck-alert-bg: #fdeceb;\r\n  --ck-alert-border: #dc6d5a;\r\n  --ck-alert-text: #7f1d1d;\r\n  --ck-radius: 14px;\r\n  --ck-font: \"Nexa Book\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\r\n\r\n  display: block;\r\n  font-family: var(--ck-font);\r\n  color: var(--ck-text);\r\n  line-height: 1.45;\r\n  container-type: inline-size;\r\n}\r\n\r\n@media (prefers-color-scheme: dark) {\r\n  :host {\r\n    --ck-bg: #0f1e24;\r\n    --ck-panel: #16303a;\r\n    --ck-text: #e8f1f2;\r\n    --ck-muted: #9fb6bc;\r\n    --ck-border: #2a4650;\r\n    --ck-good-bg: #123527;\r\n    --ck-good-text: #a7e3c4;\r\n    --ck-warn-bg: #38300f;\r\n    --ck-warn-text: #f5e3a3;\r\n    --ck-alert-bg: #3a1a17;\r\n    --ck-alert-text: #f3b8ae;\r\n  }\r\n}\r\n\r\n*, *::before, *::after { box-sizing: border-box; }\r\n\r\n.calc {\r\n  background: var(--ck-bg);\r\n  border: 1px solid var(--ck-border);\r\n  border-top: 6px solid var(--ck-accent);\r\n  border-radius: var(--ck-radius);\r\n  box-shadow: 0 12px 30px -12px rgba(0, 32, 42, 0.25);\r\n  padding: 26px;\r\n  max-width: 920px;\r\n  margin: 0 auto;\r\n}\r\n\r\n.calc-header { text-align: center; margin-bottom: 22px; }\r\n.calc-header h3 { margin: 0 0 6px; font-size: 1.55rem; font-weight: 700; color: var(--ck-primary); }\r\n@media (prefers-color-scheme: dark) { .calc-header h3 { color: var(--ck-accent); } }\r\n.calc-header p { margin: 0; color: var(--ck-muted); font-size: 0.95rem; }\r\n\r\n.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; }\r\n@container (max-width: 700px) { .cols { grid-template-columns: 1fr; } }\r\n\r\n.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }\r\n\r\n.field { margin-bottom: 14px; }\r\n.field label {\r\n  display: block; margin-bottom: 6px;\r\n  font-weight: 600; font-size: 0.86rem; color: var(--ck-muted);\r\n}\r\n.field input[type=\"number\"], .field input[type=\"text\"], .field input[type=\"date\"],\r\n.field input[type=\"email\"], .field select {\r\n  width: 100%; padding: 11px 12px;\r\n  border: 2px solid var(--ck-border); border-radius: 8px;\r\n  font-size: 1rem; font-family: inherit;\r\n  background: var(--ck-bg); color: var(--ck-text);\r\n}\r\n.field input:focus-visible, .field select:focus-visible, button:focus-visible,\r\ninput[type=\"range\"]:focus-visible, a:focus-visible, details summary:focus-visible {\r\n  outline: 3px solid var(--ck-accent); outline-offset: 1px;\r\n}\r\n.field .hint { font-size: 0.78rem; color: var(--ck-muted); margin-top: 4px; }\r\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; }\r\n\r\ninput[type=\"range\"] { width: 100%; accent-color: var(--ck-accent-dark); margin: 6px 0 0; }\r\n\r\n.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }\r\n.chip {\r\n  border: 1.5px solid var(--ck-accent-dark); border-radius: 999px;\r\n  background: transparent; color: var(--ck-accent-dark);\r\n  padding: 5px 13px; font-size: 0.85rem; font-weight: 600; font-family: inherit;\r\n  cursor: pointer;\r\n}\r\n.chip[aria-pressed=\"true\"] { background: var(--ck-accent-dark); color: #fff; }\r\n.chip:hover { background: var(--ck-accent); border-color: var(--ck-accent); color: #06272e; }\r\n\r\n\/* Results *\/\r\n.results {\r\n  background: var(--ck-panel);\r\n  border: 1px solid var(--ck-border);\r\n  border-radius: var(--ck-radius);\r\n  padding: 20px;\r\n  min-height: 200px; \/* reserve space \u2014 no layout shift on first calc *\/\r\n}\r\n.headline { text-align: center; padding-bottom: 14px; border-bottom: 2px solid var(--ck-border); margin-bottom: 14px; }\r\n.headline .label { color: var(--ck-muted); font-size: 0.9rem; }\r\n.headline .value { font-size: 2.1rem; font-weight: 800; color: var(--ck-primary); }\r\n@media (prefers-color-scheme: dark) { .headline .value { color: var(--ck-accent); } }\r\n.headline .per { font-size: 1rem; font-weight: 600; color: var(--ck-muted); }\r\n\r\n.rrow { display: flex; justify-content: space-between; gap: 12px; margin: 7px 0; font-size: 0.95rem; }\r\n.rrow .k { color: var(--ck-muted); }\r\n.rrow .v { font-weight: 700; color: var(--ck-text); font-variant-numeric: tabular-nums; }\r\n\r\n.savings {\r\n  background: var(--ck-good-bg); color: var(--ck-good-text);\r\n  border-radius: 10px; padding: 13px 15px; margin-top: 14px;\r\n  font-size: 0.93rem;\r\n}\r\n.savings strong { font-size: 1.05rem; }\r\n.savings .src { display: block; margin-top: 5px; font-size: 0.76rem; opacity: 0.85; }\r\n\r\n\/* Advisory panels (AFR \/ usury \/ gift tax) *\/\r\n.advice { border-radius: 10px; padding: 12px 14px; margin-top: 12px; font-size: 0.9rem; }\r\n.advice.info { background: var(--ck-panel); border: 1px solid var(--ck-border); color: var(--ck-text); }\r\n.advice.warn { background: var(--ck-warn-bg); border-left: 4px solid var(--ck-warn-border); color: var(--ck-warn-text); }\r\n.advice.alert { background: var(--ck-alert-bg); border-left: 4px solid var(--ck-alert-border); color: var(--ck-alert-text); }\r\n.advice .fix { margin-top: 8px; }\r\n.advice .title { font-weight: 700; display: block; margin-bottom: 3px; }\r\n.smallprint { font-size: 0.74rem; color: var(--ck-muted); margin-top: 10px; }\r\n\r\n\/* Charts *\/\r\n.chart-wrap { margin-top: 16px; }\r\n.chart-wrap h4 { margin: 0 0 6px; font-size: 0.9rem; color: var(--ck-muted); font-weight: 600; }\r\n.chart-wrap svg { width: 100%; height: auto; display: block; }\r\n.legend { display: flex; gap: 16px; font-size: 0.78rem; color: var(--ck-muted); margin-top: 4px; flex-wrap: wrap; }\r\n.legend .sw { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }\r\n\r\n\/* Early payoff *\/\r\n.payoff { margin-top: 18px; padding: 14px; border: 1px dashed var(--ck-accent-dark); border-radius: 10px; }\r\n.payoff h4 { margin: 0 0 4px; font-size: 0.95rem; color: var(--ck-primary); }\r\n@media (prefers-color-scheme: dark) { .payoff h4 { color: var(--ck-accent); } }\r\n.payoff .out { font-size: 0.92rem; margin-top: 8px; }\r\n.payoff .teaser { font-size: 0.78rem; color: var(--ck-muted); margin-top: 6px; }\r\n\r\n\/* Scenarios *\/\r\n.scenarios { margin-top: 18px; }\r\n.scenarios table, .schedule table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }\r\n.scenarios th, .scenarios td, .schedule th, .schedule td {\r\n  padding: 6px 8px; text-align: right; border-bottom: 1px solid var(--ck-border);\r\n  font-variant-numeric: tabular-nums;\r\n}\r\n.scenarios th:first-child, .scenarios td:first-child,\r\n.schedule th:first-child, .schedule td:first-child { text-align: left; }\r\n.scenarios th, .schedule th { color: var(--ck-muted); font-weight: 600; }\r\n.table-scroll { overflow-x: auto; }\r\n\r\n\/* Schedule *\/\r\n.schedule { margin-top: 18px; }\r\n.schedule summary { cursor: pointer; font-weight: 700; color: var(--ck-accent-dark); }\r\n.schedule .actions { margin: 10px 0; display: flex; gap: 10px; flex-wrap: wrap; }\r\n\r\nbutton.btn {\r\n  font-family: inherit; font-size: 0.9rem; font-weight: 700;\r\n  border-radius: 8px; padding: 9px 16px; cursor: pointer;\r\n  border: 2px solid var(--ck-accent-dark);\r\n  background: transparent; color: var(--ck-accent-dark);\r\n}\r\nbutton.btn:hover { background: var(--ck-accent-dark); color: #fff; }\r\nbutton.btn.primary { background: var(--ck-primary); border-color: var(--ck-primary); color: #fff; }\r\nbutton.btn.primary:hover { background: var(--ck-accent-dark); border-color: var(--ck-accent-dark); }\r\n\r\n\/* CTA + capture *\/\r\n.cta {\r\n  margin-top: 22px; padding: 20px;\r\n  background: var(--ck-primary); color: #fff;\r\n  border-radius: var(--ck-radius); text-align: center;\r\n}\r\n.cta h4 { margin: 0 0 6px; font-size: 1.2rem; }\r\n.cta p { margin: 0 0 12px; font-size: 0.9rem; opacity: 0.92; }\r\n.cta a.go {\r\n  display: inline-block; background: var(--ck-highlight); color: #3b1006;\r\n  font-weight: 800; text-decoration: none; border-radius: 10px;\r\n  padding: 12px 26px; font-size: 1.02rem;\r\n}\r\n.cta a.go:hover { filter: brightness(1.08); }\r\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; }\r\n.cta ul li::before { content: \"\u2713 \"; color: var(--ck-accent); font-weight: 800; }\r\n\r\n.capture { margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; align-items: center; }\r\n.capture input[type=\"email\"] {\r\n  flex: 1 1 220px; max-width: 300px; padding: 10px 12px;\r\n  border-radius: 8px; border: none; font-family: inherit; font-size: 0.92rem;\r\n}\r\n.capture .msg { flex-basis: 100%; font-size: 0.85rem; }\r\n.hp { position: absolute; left: -5000px; width: 1px; height: 1px; overflow: hidden; } \/* honeypot *\/\r\n\r\n.visually-hidden {\r\n  position: absolute; width: 1px; height: 1px; margin: -1px;\r\n  clip-path: inset(50%); overflow: hidden; white-space: nowrap;\r\n}\r\n\r\n@media (prefers-reduced-motion: no-preference) {\r\n  .savings, .advice { animation: ck-fade 0.35s ease-in; }\r\n  @keyframes ck-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }\r\n}\r\n`;\r\n\r\nconst US_STATES = {\r\n  AL: 'Alabama', AK: 'Alaska', AZ: 'Arizona', AR: 'Arkansas', CA: 'California',\r\n  CO: 'Colorado', CT: 'Connecticut', DE: 'Delaware', DC: 'District of Columbia',\r\n  FL: 'Florida', GA: 'Georgia', HI: 'Hawaii', ID: 'Idaho', IL: 'Illinois',\r\n  IN: 'Indiana', IA: 'Iowa', KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana',\r\n  ME: 'Maine', MD: 'Maryland', MA: 'Massachusetts', MI: 'Michigan',\r\n  MN: 'Minnesota', MS: 'Mississippi', MO: 'Missouri', MT: 'Montana',\r\n  NE: 'Nebraska', NV: 'Nevada', NH: 'New Hampshire', NJ: 'New Jersey',\r\n  NM: 'New Mexico', NY: 'New York', NC: 'North Carolina', ND: 'North Dakota',\r\n  OH: 'Ohio', OK: 'Oklahoma', OR: 'Oregon', PA: 'Pennsylvania',\r\n  RI: 'Rhode Island', SC: 'South Carolina', SD: 'South Dakota',\r\n  TN: 'Tennessee', TX: 'Texas', UT: 'Utah', VT: 'Vermont', VA: 'Virginia',\r\n  WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', WY: 'Wyoming',\r\n};\r\n\r\nconst LIMITS = { amount: [500, 10_000_000], rate: [0, 100], term: [1, 360] };\r\nconst BAND_LABELS = { short: 'short-term', mid: 'mid-term', long: 'long-term' };\r\n\r\nconst esc = (s) => String(s).replace(\/[&<>\"']\/g, (c) => ({\r\n  '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;',\r\n}[c]));\r\n\r\nfunction todayIso() {\r\n  const d = new Date();\r\n  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\r\n}\r\n\r\n\/** utm\/click params on the CURRENT (marketing) page, forwarded on the CTA + lead. *\/\r\nfunction pageAttribution() {\r\n  const keep = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid', 'msclkid'];\r\n  const out = {};\r\n  try {\r\n    const q = new URLSearchParams(window.location.search);\r\n    for (const k of keep) {\r\n      const v = q.get(k);\r\n      if (v) out[k] = v.slice(0, 255);\r\n    }\r\n    if (document.referrer) out.referrer = document.referrer.slice(0, 255);\r\n  } catch { \/* SSR\/sandbox: no attribution *\/ }\r\n  return out;\r\n}\r\n\r\nclass ChipkieLoanCalc extends HTMLElement {\r\n  connectedCallback() {\r\n    if (this._ready) return;\r\n    this._ready = true;\r\n\r\n    this.market = (this.getAttribute('market') || 'US').toUpperCase();\r\n    this.configUrl = this.getAttribute('config-url') || 'https:\/\/my.chipkie.com\/api\/calculator\/config';\r\n    this.leadUrl = this.getAttribute('lead-url')\r\n      || this.configUrl.replace(\/\\\/config.*$\/, '\/lead');\r\n    this.ctaUrl = this.getAttribute('cta-url') || 'https:\/\/my.chipkie.com\/loans\/create';\r\n    this.analyticsOn = this.getAttribute('analytics') !== 'off';\r\n\r\n    \/\/ Config payload (AFR\/usury\/prices). Starts with safe fallbacks; the\r\n    \/\/ fetch upgrades it. Everything tax-related stays DORMANT without data \u2014\r\n    \/\/ never a guessed rate (mirrors App\\Support\\Afr's philosophy).\r\n    this.cfg = {\r\n      market: this.market,\r\n      currency: { code: this.market === 'GB' ? 'GBP' : this.market === 'AU' ? 'AUD' : 'USD',\r\n        locale: this.market === 'GB' ? 'en-GB' : this.market === 'AU' ? 'en-AU' : 'en-US' },\r\n      comparison: {\r\n        apr: Number(this.getAttribute('comparison-apr')) || 12,\r\n        source: 'typical bank personal-loan APR',\r\n        as_of: '',\r\n      },\r\n      contract: { available: false, price: null },\r\n      afr: null, usury: null, gift_tax: null,\r\n    };\r\n\r\n    this.s = { \/\/ scenario state\r\n      amount: 20000, rate: 0, term: 36, frequency: 'monthly',\r\n      start: todayIso(), state: '', extra: 0,\r\n    };\r\n    this.pinned = [];\r\n    this._eventsSent = {};\r\n\r\n    this.root = this.attachShadow({ mode: 'open' });\r\n    this.render();\r\n    this.bind();\r\n    this.loadConfig();\r\n    this.recalc();\r\n  }\r\n\r\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\r\n  render() {\r\n    const us = this.market === 'US';\r\n    const freqOptions = us\r\n      ? '<option value=\"monthly\">Monthly<\/option><option value=\"fortnightly\">Every 2 weeks<\/option><option value=\"weekly\">Weekly<\/option>'\r\n      : '<option value=\"monthly\">Monthly<\/option><option value=\"fortnightly\">Fortnightly<\/option><option value=\"weekly\">Weekly<\/option>';\r\n    const stateOptions = ['<option value=\"\">Select a state (optional)<\/option>']\r\n      .concat(Object.entries(US_STATES).map(([c, n]) => `<option value=\"${c}\">${esc(n)}<\/option>`)).join('');\r\n\r\n    this.root.innerHTML = `\r\n      <style>${WIDGET_CSS}<\/style>\r\n      <div class=\"calc\">\r\n        <div class=\"calc-header\">\r\n          <h3>Family Loan Calculator<\/h3>\r\n          <p>Work out repayments for the Bank of ${us ? 'Mom' : 'Mum'} &amp; Dad \u2014 and keep it\r\n            ${{ US: 'right with the IRS', AU: 'tax-smart with the ATO', GB: 'tax-smart with HMRC' }[this.market] || 'fair for everyone'}.<\/p>\r\n        <\/div>\r\n        <div class=\"cols\">\r\n          <div class=\"inputs\">\r\n            <div class=\"field\">\r\n              <label for=\"amount\">Loan amount<\/label>\r\n              <input id=\"amount\" type=\"number\" inputmode=\"decimal\" min=\"${LIMITS.amount[0]}\" max=\"${LIMITS.amount[1]}\" step=\"100\" value=\"20000\" autocomplete=\"off\">\r\n              <input id=\"amount-slider\" type=\"range\" min=\"500\" max=\"250000\" step=\"500\" value=\"20000\" aria-label=\"Loan amount slider\">\r\n              <div class=\"error\" id=\"amount-err\" hidden><\/div>\r\n            <\/div>\r\n            <div class=\"grid2\">\r\n              <div class=\"field\">\r\n                <label for=\"rate\">Interest rate (% \/ year)<\/label>\r\n                <input id=\"rate\" type=\"number\" inputmode=\"decimal\" min=\"0\" max=\"100\" step=\"0.1\" value=\"0\" autocomplete=\"off\">\r\n                <div class=\"chips\" role=\"group\" aria-label=\"Rate quick picks\">\r\n                  <button type=\"button\" class=\"chip\" data-rate=\"0\" aria-pressed=\"true\">0%<\/button>\r\n                  <button type=\"button\" class=\"chip\" id=\"afr-chip\" hidden aria-pressed=\"false\"><\/button>\r\n                <\/div>\r\n                <div class=\"error\" id=\"rate-err\" hidden><\/div>\r\n              <\/div>\r\n              <div class=\"field\">\r\n                <label for=\"term\">Term (months)<\/label>\r\n                <input id=\"term\" type=\"number\" inputmode=\"numeric\" min=\"1\" max=\"360\" step=\"1\" value=\"36\" autocomplete=\"off\">\r\n                <div class=\"chips\" role=\"group\" aria-label=\"Term quick picks\">\r\n                  <button type=\"button\" class=\"chip\" data-term=\"12\">1 yr<\/button>\r\n                  <button type=\"button\" class=\"chip\" data-term=\"24\">2 yrs<\/button>\r\n                  <button type=\"button\" class=\"chip\" data-term=\"36\" aria-pressed=\"true\">3 yrs<\/button>\r\n                  <button type=\"button\" class=\"chip\" data-term=\"60\">5 yrs<\/button>\r\n                <\/div>\r\n                <div class=\"error\" id=\"term-err\" hidden><\/div>\r\n              <\/div>\r\n            <\/div>\r\n            <div class=\"grid2\">\r\n              <div class=\"field\">\r\n                <label for=\"frequency\">Repayment frequency<\/label>\r\n                <select id=\"frequency\">${freqOptions}<\/select>\r\n              <\/div>\r\n              <div class=\"field\">\r\n                <label for=\"start\">First payment period starts<\/label>\r\n                <input id=\"start\" type=\"date\" value=\"${todayIso()}\">\r\n              <\/div>\r\n            <\/div>\r\n            ${us ? `\r\n            <div class=\"field\">\r\n              <label for=\"state\">Borrower's state<\/label>\r\n              <select id=\"state\">${stateOptions}<\/select>\r\n              <div class=\"hint\">Used for state interest-limit guidance \u2014 Chipkie's US loan agreements are governed by the borrower's state.<\/div>\r\n            <\/div>` : ''}\r\n            <div id=\"tax-panel\" aria-live=\"polite\"><\/div>\r\n          <\/div>\r\n\r\n          <div class=\"outputs\">\r\n            <div class=\"results\" aria-live=\"polite\">\r\n              <div class=\"headline\">\r\n                <div class=\"label\">Each repayment<\/div>\r\n                <div class=\"value\" id=\"per\">\u2014<\/div>\r\n                <div class=\"per\" id=\"per-unit\"><\/div>\r\n              <\/div>\r\n              <div class=\"rrow\"><span class=\"k\">Number of payments<\/span><span class=\"v\" id=\"npay\">\u2014<\/span><\/div>\r\n              <div class=\"rrow\"><span class=\"k\">Total interest<\/span><span class=\"v\" id=\"tint\">\u2014<\/span><\/div>\r\n              <div class=\"rrow\"><span class=\"k\">Total repaid<\/span><span class=\"v\" id=\"trep\">\u2014<\/span><\/div>\r\n              <div class=\"rrow\"><span class=\"k\">Debt-free by<\/span><span class=\"v\" id=\"payoff\">\u2014<\/span><\/div>\r\n              <div class=\"savings\" id=\"savings\" hidden><\/div>\r\n            <\/div>\r\n\r\n            <div class=\"chart-wrap\" id=\"chart-balance\"><\/div>\r\n            <div class=\"chart-wrap\" id=\"chart-split\"><\/div>\r\n\r\n            <div class=\"payoff\">\r\n              <h4>Pay it off sooner<\/h4>\r\n              <label for=\"extra\" style=\"font-size:0.85rem;color:var(--ck-muted)\">Extra on every payment: <strong id=\"extra-label\">+0<\/strong><\/label>\r\n              <input id=\"extra\" type=\"range\" min=\"0\" max=\"500\" step=\"10\" value=\"0\">\r\n              <div class=\"out\" id=\"payoff-out\"><\/div>\r\n              <div class=\"teaser\">Chipkie's early-payoff planner tracks this against real payments once your loan is set up.<\/div>\r\n            <\/div>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"scenarios\" id=\"scenarios\" hidden>\r\n          <h4 style=\"margin:0 0 6px;font-size:0.95rem;\">Compare scenarios<\/h4>\r\n          <div class=\"table-scroll\" id=\"scenario-table\"><\/div>\r\n        <\/div>\r\n        <div class=\"chips\" style=\"margin-top:12px\">\r\n          <button type=\"button\" class=\"chip\" id=\"pin\">\uff0b Pin this scenario to compare<\/button>\r\n          <button type=\"button\" class=\"chip\" id=\"unpin\" hidden>Clear pinned<\/button>\r\n        <\/div>\r\n\r\n        <details class=\"schedule\" id=\"schedule\">\r\n          <summary>Full payment schedule<\/summary>\r\n          <div class=\"actions\">\r\n            <button type=\"button\" class=\"btn\" id=\"csv\">Download CSV<\/button>\r\n          <\/div>\r\n          <div class=\"table-scroll\" id=\"schedule-table\"><\/div>\r\n        <\/details>\r\n\r\n        <div class=\"cta\">\r\n          <h4>Make it official<\/h4>\r\n          <p>Turn this into a signed agreement with payment tracking, reminders, and a tax-ready annual summary \u2014 set up in minutes.<\/p>\r\n          <a class=\"go\" id=\"cta-link\" href=\"${esc(this.ctaUrl)}\" target=\"_blank\" rel=\"noopener\">Create this loan with Chipkie \u2192<\/a>\r\n          <ul id=\"value-stack\">\r\n            <li>Legally-binding contract, e-signed<\/li>\r\n            <li>Payment tracking &amp; friendly reminders<\/li>\r\n            <li>Tax-ready annual summary<\/li>\r\n          <\/ul>\r\n          <form class=\"capture\" id=\"capture\" novalidate>\r\n            <label class=\"visually-hidden\" for=\"lead-email\">Email address<\/label>\r\n            <input id=\"lead-email\" type=\"email\" placeholder=\"you@example.com\" autocomplete=\"email\">\r\n            <input class=\"hp\" type=\"text\" name=\"website\" tabindex=\"-1\" autocomplete=\"off\" aria-hidden=\"true\">\r\n            <button type=\"submit\" class=\"btn\" style=\"border-color:#fff;color:#fff\">Save my breakdown<\/button>\r\n            <span class=\"msg\" id=\"capture-msg\" role=\"status\"><\/span>\r\n          <\/form>\r\n        <\/div>\r\n        <p class=\"smallprint\" id=\"footnote\"><\/p>\r\n      <\/div>`;\r\n  }\r\n\r\n  $(id) { return this.root.getElementById(id); }\r\n\r\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\r\n  bind() {\r\n    const on = (id, ev, fn) => this.$(id) && this.$(id).addEventListener(ev, fn);\r\n\r\n    on('amount', 'input', () => { this.s.amount = Number(this.$('amount').value); this.syncSlider(); this.recalc(); });\r\n    on('amount-slider', 'input', () => {\r\n      this.s.amount = Number(this.$('amount-slider').value);\r\n      this.$('amount').value = String(this.s.amount);\r\n      this.recalc();\r\n    });\r\n    on('rate', 'input', () => { this.s.rate = Number(this.$('rate').value); this.markRateChips(); this.recalc(); });\r\n    on('term', 'input', () => { this.s.term = Number(this.$('term').value); this.markTermChips(); this.recalc(); });\r\n    on('frequency', 'change', () => { this.s.frequency = this.$('frequency').value; this.recalc(); });\r\n    on('start', 'change', () => { this.s.start = this.$('start').value || todayIso(); this.recalc(); });\r\n    on('state', 'change', () => { this.s.state = this.$('state').value; this.recalc(); });\r\n    on('extra', 'input', () => { this.s.extra = Number(this.$('extra').value); this.renderPayoff(); });\r\n\r\n    this.root.querySelectorAll('.chip[data-rate]').forEach((b) =>\r\n      b.addEventListener('click', () => { this.setRate(Number(b.dataset.rate)); }));\r\n    this.root.querySelectorAll('.chip[data-term]').forEach((b) =>\r\n      b.addEventListener('click', () => {\r\n        this.s.term = Number(b.dataset.term);\r\n        this.$('term').value = String(this.s.term);\r\n        this.markTermChips();\r\n        this.recalc();\r\n      }));\r\n\r\n    on('afr-chip', 'click', () => {\r\n      const a = this.currentAfr();\r\n      if (a?.has_data) {\r\n        this.setRate(a.applicable_afr);\r\n        this.track('afr_fix', { rate: a.applicable_afr });\r\n      }\r\n    });\r\n\r\n    on('pin', 'click', () => this.pinScenario());\r\n    on('unpin', 'click', () => { this.pinned = []; this.renderScenarios(); });\r\n    on('csv', 'click', () => this.downloadCsv());\r\n    on('schedule', 'toggle', () => { if (this.$('schedule').open) this.track('schedule_open'); });\r\n    on('cta-link', 'click', () => this.track('cta_click', { amount: this.s.amount, rate: this.s.rate }));\r\n    on('capture', 'submit', (e) => { e.preventDefault(); this.submitLead(); });\r\n  }\r\n\r\n  setRate(r) {\r\n    this.s.rate = phpRound(Number(r), 2);\r\n    this.$('rate').value = String(this.s.rate);\r\n    this.markRateChips();\r\n    this.recalc();\r\n  }\r\n\r\n  syncSlider() {\r\n    const v = Math.min(250000, Math.max(500, this.s.amount || 500));\r\n    this.$('amount-slider').value = String(v);\r\n  }\r\n\r\n  markRateChips() {\r\n    this.root.querySelectorAll('.chip[data-rate]').forEach((b) =>\r\n      b.setAttribute('aria-pressed', String(Number(b.dataset.rate) === this.s.rate)));\r\n    const a = this.currentAfr();\r\n    this.$('afr-chip')?.setAttribute('aria-pressed', String(!!a?.has_data && a.applicable_afr === this.s.rate));\r\n  }\r\n\r\n  markTermChips() {\r\n    this.root.querySelectorAll('.chip[data-term]').forEach((b) =>\r\n      b.setAttribute('aria-pressed', String(Number(b.dataset.term) === this.s.term)));\r\n  }\r\n\r\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\r\n  async loadConfig() {\r\n    const cacheKey = `ck-calc-config-${this.market}`;\r\n    try {\r\n      const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null');\r\n      if (cached && Date.now() - cached.at < 24 * 3600 * 1000) {\r\n        this.applyConfig(cached.data);\r\n      }\r\n    } catch { \/* ignore bad cache *\/ }\r\n\r\n    try {\r\n      \/\/ data: URLs (demo\/tests) carry the payload inline \u2014 no query appended.\r\n      const url = this.configUrl.startsWith('data:')\r\n        ? this.configUrl\r\n        : `${this.configUrl}?market=${encodeURIComponent(this.market)}`;\r\n      const res = await fetch(url, { mode: 'cors' });\r\n      if (!res.ok) return;\r\n      const data = await res.json();\r\n      this.applyConfig(data);\r\n      try { localStorage.setItem(cacheKey, JSON.stringify({ at: Date.now(), data })); } catch { \/* full\/blocked *\/ }\r\n    } catch { \/* offline \u2192 widget stays fully functional, tax panel dormant *\/ }\r\n  }\r\n\r\n  applyConfig(data) {\r\n    if (!data || typeof data !== 'object') return;\r\n    this.cfg = { ...this.cfg, ...data };\r\n    this.recalc();\r\n  }\r\n\r\n  currentAfr() {\r\n    return afrAssess(this.s.rate, this.s.term, this.s.amount, this.cfg.afr);\r\n  }\r\n\r\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\r\n  validate() {\r\n    const errs = {};\r\n    const [aMin, aMax] = LIMITS.amount;\r\n    if (!isFinite(this.s.amount) || this.s.amount < aMin || this.s.amount > aMax) {\r\n      errs.amount = `Enter an amount between ${this.money(aMin, 0)} and ${this.money(aMax, 0)}.`;\r\n    }\r\n    if (!isFinite(this.s.rate) || this.s.rate < LIMITS.rate[0] || this.s.rate > LIMITS.rate[1]) {\r\n      errs.rate = 'Enter a rate between 0 and 100%.';\r\n    }\r\n    if (!Number.isInteger(this.s.term) || this.s.term < LIMITS.term[0] || this.s.term > LIMITS.term[1]) {\r\n      errs.term = 'Enter a term between 1 and 360 months.';\r\n    }\r\n    for (const f of ['amount', 'rate', 'term']) {\r\n      const el = this.$(`${f}-err`);\r\n      if (!el) continue;\r\n      el.hidden = !errs[f];\r\n      el.textContent = errs[f] || '';\r\n      this.$(f)?.setAttribute('aria-invalid', String(!!errs[f]));\r\n    }\r\n    return Object.keys(errs).length === 0;\r\n  }\r\n\r\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\r\n  money(n, digits = 2) {\r\n    const c = this.cfg.currency || {};\r\n    try {\r\n      return new Intl.NumberFormat(c.locale || 'en-US', {\r\n        style: 'currency', currency: c.code || 'USD',\r\n        minimumFractionDigits: digits, maximumFractionDigits: digits,\r\n      }).format(n);\r\n    } catch {\r\n      return `$${Number(n).toFixed(digits)}`;\r\n    }\r\n  }\r\n\r\n  dateLabel(iso) {\r\n    try {\r\n      return new Date(`${iso}T12:00:00Z`).toLocaleDateString(this.cfg.currency?.locale || 'en-US', {\r\n        year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC',\r\n      });\r\n    } catch { return iso; }\r\n  }\r\n\r\n  perUnit() {\r\n    return { weekly: 'per week', fortnightly: this.market === 'US' ? 'every 2 weeks' : 'per fortnight', monthly: 'per month' }[this.s.frequency];\r\n  }\r\n\r\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\r\n  scheduleFor(ratePct) {\r\n    const end = addMonthsIso(this.s.start, this.s.term);\r\n    return buildSchedule({\r\n      amount: this.s.amount, interest: ratePct,\r\n      startDate: this.s.start, endDate: end, frequency: this.s.frequency,\r\n    });\r\n  }\r\n\r\n  recalc() {\r\n    if (!this.validate()) {\r\n      this.root.querySelector('.results').style.opacity = '0.45';\r\n      return;\r\n    }\r\n    this.root.querySelector('.results').style.opacity = '';\r\n\r\n    const end = addMonthsIso(this.s.start, this.s.term);\r\n    const n = numInstalments(this.s.start, end, this.s.frequency);\r\n    if (n < 1) {\r\n      this.$('per').textContent = '\u2014';\r\n      this.$('per-unit').textContent = 'Term too short for this frequency';\r\n      return;\r\n    }\r\n\r\n    const sched = this.scheduleFor(this.s.rate);\r\n    const per = instalmentAmount(this.s.amount, this.s.rate, this.s.start, end, this.s.frequency);\r\n    this.current = { sched, per, end, n };\r\n\r\n    this.$('per').textContent = this.money(per);\r\n    this.$('per-unit').textContent = this.perUnit();\r\n    this.$('npay').textContent = String(n);\r\n    this.$('tint').textContent = this.money(sched.totals.interest);\r\n    this.$('trep').textContent = this.money(sched.totals.repaid);\r\n    this.$('payoff').textContent = this.dateLabel(sched.rows[n - 1].due_date);\r\n\r\n    this.renderSavings(sched);\r\n    this.renderTaxPanel();\r\n    this.renderCharts(sched);\r\n    this.renderPayoff();\r\n    this.renderSchedule(sched);\r\n    this.updateCta();\r\n    this.renderFootnote();\r\n    this.trackDebounced('used', { amount: this.s.amount, rate: this.s.rate, term: this.s.term, frequency: this.s.frequency });\r\n  }\r\n\r\n  renderSavings(sched) {\r\n    const cmp = this.cfg.comparison;\r\n    const box = this.$('savings');\r\n    if (!cmp || !(this.s.rate < cmp.apr)) { box.hidden = true; this._bankSched = null; return; }\r\n    \/\/ Always refresh the comparison schedule (the balance chart overlays it)\r\n    \/\/ even when the savings are too small to celebrate.\r\n    const bank = this.scheduleFor(cmp.apr);\r\n    this._bankSched = bank;\r\n    const saved = phpRound(bank.totals.repaid - sched.totals.repaid, 2);\r\n    if (saved <= 10) { box.hidden = true; return; }\r\n    box.hidden = false;\r\n    box.innerHTML = `\ud83c\udf89 A family loan at ${esc(String(this.s.rate))}% saves <strong>${esc(this.money(saved))}<\/strong>\r\n      vs a bank loan at ${esc(String(cmp.apr))}% APR.\r\n      <span class=\"src\">Comparison: ${esc(cmp.source || 'bank personal-loan APR')}${cmp.as_of ? `, ${esc(cmp.as_of)}` : ''}.<\/span>`;\r\n  }\r\n\r\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\r\n  renderTaxPanel() {\r\n    const panel = this.$('tax-panel');\r\n    if (!panel) return;\r\n    if (this.market !== 'US') { this.renderNonUsTaxPanel(panel); return; }\r\n\r\n    const parts = [];\r\n    const a = this.currentAfr();\r\n    const chip = this.$('afr-chip');\r\n\r\n    if (a.has_data) {\r\n      const afrPct = `${a.applicable_afr}%`;\r\n      const month = this.cfg.afr?.month ? ` (${this.cfg.afr.month})` : '';\r\n      const stale = this.cfg.afr?.stale ? ' \u2014 may be out of date, verify the current month' : '';\r\n      if (chip) {\r\n        chip.hidden = false;\r\n        chip.textContent = `AFR ${afrPct}`;\r\n        chip.title = 'Set the rate to the IRS Applicable Federal Rate for this term';\r\n      }\r\n      parts.push(`<div class=\"advice info\">IRS Applicable Federal Rate for this term\r\n        (${BAND_LABELS[a.band]}${month}): <strong>${esc(afrPct)}<\/strong>${esc(stale)}.<\/div>`);\r\n\r\n      if (a.below && !a.de_minimis_exempt) {\r\n        const gt = this.cfg.gift_tax;\r\n        const gtLine = gt?.annual_exclusion\r\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)}` : ''}).`\r\n          : '';\r\n        parts.push(`<div class=\"advice warn\"><span class=\"title\">Below-market rate \u2014 the IRS may impute interest<\/span>\r\n          On loans over ${this.money(this.cfg.afr?.de_minimis ?? 10000, 0)}, charging less than the AFR can mean the IRS\r\n          treats the difference as taxable \"imputed\" interest to the lender (IRC \u00a77872).${gtLine}\r\n          <div class=\"fix\"><button type=\"button\" class=\"chip\" id=\"afr-fix\">Use the AFR rate (${esc(afrPct)})<\/button><\/div>\r\n        <\/div>`);\r\n        this.trackOnce(`afr_warning-${this.s.rate}-${this.s.amount}`, 'afr_warning', { rate: this.s.rate, amount: this.s.amount });\r\n      } else if (a.below && a.de_minimis_exempt) {\r\n        parts.push(`<div class=\"advice info\">At ${this.money(this.cfg.afr?.de_minimis ?? 10000, 0)} or less, family \"gift loans\"\r\n          are generally exempt from the IRS imputed-interest rules (IRC \u00a77872) \u2014 low-rate lending is usually fine at this size.<\/div>`);\r\n      }\r\n    } else if (chip) {\r\n      chip.hidden = true;\r\n    }\r\n\r\n    if (this.s.state && this.cfg.usury?.states) {\r\n      const u = usuryCheck(this.s.rate, this.s.state, this.cfg.usury.states);\r\n      if (u.status === 'over') {\r\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>\r\n          ${esc(US_STATES[this.s.state] || this.s.state)}'s general usury limit is commonly cited at <strong>${esc(String(u.cap))}%<\/strong>\r\n          \u2014 this rate may not be enforceable there.\r\n          <div class=\"fix\"><button type=\"button\" class=\"chip\" id=\"usury-fix\">Reduce to ${esc(String(u.cap))}%<\/button><\/div>\r\n        <\/div>`);\r\n        this.trackOnce(`usury-${this.s.state}`, 'usury_warning', { state: this.s.state, rate: this.s.rate });\r\n      } else if (u.status === 'unknown' && this.s.rate >= 10) {\r\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\r\n          at ${esc(String(this.s.rate))}% it's worth checking your state's usury cap.<\/div>`);\r\n      }\r\n    }\r\n\r\n    panel.innerHTML = parts.join('');\r\n    panel.querySelector('#afr-fix')?.addEventListener('click', () => {\r\n      this.setRate(a.applicable_afr);\r\n      this.track('afr_fix', { rate: a.applicable_afr });\r\n    });\r\n    panel.querySelector('#usury-fix')?.addEventListener('click', () => {\r\n      const u = usuryCheck(this.s.rate, this.s.state, this.cfg.usury.states);\r\n      if (u.cap != null) this.setRate(u.cap);\r\n    });\r\n    this.markRateChips();\r\n  }\r\n\r\n  \/**\r\n   * AU \/ GB advisory modules \u2014 same hedged, guidance-only voice as the app's\r\n   * tax-summary notes. Figures come from the config endpoint (au_tax\/gb_tax)\r\n   * so the embed never hard-codes a reviewable number; without data the\r\n   * panel stays dormant, exactly like the US AFR panel.\r\n   *\/\r\n  renderNonUsTaxPanel(panel) {\r\n    const parts = [];\r\n\r\n    if (this.market === 'AU' && this.cfg.au_tax) {\r\n      const t = this.cfg.au_tax;\r\n      if (this.s.rate > 0) {\r\n        parts.push(`<div class=\"advice info\">Interest you receive is generally <strong>assessable income<\/strong>\r\n          for you as the lender \u2014 Chipkie's tax-ready annual summary keeps the record for tax time.<\/div>`);\r\n      } else if (t.gifting_free_area) {\r\n        parts.push(`<div class=\"advice info\">No ATO minimum rate applies to private family loans \u2014 <strong>0% is fine<\/strong>.\r\n          A written agreement keeps it clearly a loan rather than a gift, which can matter for Age Pension\r\n          gifting rules (gifting free areas: ${this.money(t.gifting_free_area, 0)} a year \/\r\n          ${this.money(t.gifting_free_area_5yr, 0)} over 5 years${t.as_of ? `, ${esc(t.as_of)}` : ''}).<\/div>`);\r\n      }\r\n    }\r\n\r\n    if (this.market === 'GB' && this.cfg.gb_tax) {\r\n      const t = this.cfg.gb_tax;\r\n      if (this.s.rate > 0 && t.psa_basic) {\r\n        parts.push(`<div class=\"advice info\">Interest you receive counts as <strong>savings income<\/strong>.\r\n          Your Personal Savings Allowance (${this.money(t.psa_basic, 0)} basic rate \/\r\n          ${this.money(t.psa_higher, 0)} higher rate${t.as_of ? `, ${esc(t.as_of)}` : ''}) may cover some or\r\n          all of it \u2014 anything above that is reportable to HMRC.<\/div>`);\r\n      } else if (this.s.rate <= 0) {\r\n        parts.push(`<div class=\"advice info\">No HMRC minimum rate applies to family loans \u2014 <strong>0% is fine<\/strong>.\r\n          A written agreement keeps the loan clearly distinct from a gift, which can matter for\r\n          inheritance-tax purposes.<\/div>`);\r\n      }\r\n    }\r\n\r\n    panel.innerHTML = parts.join('');\r\n  }\r\n\r\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\r\n  renderCharts(sched) {\r\n    const bal = this.$('chart-balance');\r\n    const rows = sched.rows;\r\n    const bank = (this.s.rate < (this.cfg.comparison?.apr ?? Infinity)) ? this._bankSched : null;\r\n    const W = 600; const H = 200; const P = { l: 8, r: 8, t: 8, b: 22 };\r\n    const n = rows.length;\r\n    const maxY = this.s.amount;\r\n    const x = (i) => P.l + (i \/ n) * (W - P.l - P.r);\r\n    const y = (v) => P.t + (1 - v \/ maxY) * (H - P.t - P.b);\r\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(' ');\r\n\r\n    const bankPath = bank && bank.rows.length === n\r\n      ? `<path d=\"${line(bank.rows)}\" fill=\"none\" stroke=\"var(--ck-highlight)\" stroke-width=\"2\" stroke-dasharray=\"5 4\"\/>`\r\n      : '';\r\n    bal.innerHTML = `\r\n      <h4 id=\"chart-balance-h\">Balance over time<\/h4>\r\n      <svg viewBox=\"0 0 ${W} ${H}\" role=\"img\" aria-labelledby=\"chart-balance-h\">\r\n        <line x1=\"${P.l}\" y1=\"${y(0)}\" x2=\"${W - P.r}\" y2=\"${y(0)}\" stroke=\"var(--ck-border)\"\/>\r\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\"\/>\r\n        ${bankPath}\r\n        <path d=\"${line(rows)}\" fill=\"none\" stroke=\"var(--ck-accent-dark)\" stroke-width=\"2.5\"\/>\r\n        <text x=\"${P.l}\" y=\"${H - 6}\" font-size=\"11\" fill=\"var(--ck-muted)\">start<\/text>\r\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>\r\n      <\/svg>\r\n      <div class=\"legend\">\r\n        <span><span class=\"sw\" style=\"background:var(--ck-accent-dark)\"><\/span>This family loan<\/span>\r\n        ${bankPath ? `<span><span class=\"sw\" style=\"background:var(--ck-highlight)\"><\/span>Bank loan at ${esc(String(this.cfg.comparison.apr))}% APR<\/span>` : ''}\r\n      <\/div>`;\r\n\r\n    const split = this.$('chart-split');\r\n    const ti = sched.totals.interest; const tp = sched.totals.principal;\r\n    const total = ti + tp;\r\n    const pw = total > 0 ? (tp \/ total) * 100 : 100;\r\n    split.innerHTML = `\r\n      <h4 id=\"chart-split-h\">Where each ${this.market === 'US' ? 'dollar' : 'payment'} goes<\/h4>\r\n      <svg viewBox=\"0 0 600 44\" role=\"img\" aria-labelledby=\"chart-split-h\">\r\n        <rect x=\"0\" y=\"6\" width=\"${(pw \/ 100) * 600}\" height=\"22\" rx=\"4\" fill=\"var(--ck-accent-dark)\"\/>\r\n        <rect x=\"${(pw \/ 100) * 600}\" y=\"6\" width=\"${600 - (pw \/ 100) * 600}\" height=\"22\" rx=\"4\" fill=\"var(--ck-highlight)\"\/>\r\n      <\/svg>\r\n      <div class=\"legend\">\r\n        <span><span class=\"sw\" style=\"background:var(--ck-accent-dark)\"><\/span>Principal ${esc(this.money(tp))}<\/span>\r\n        <span><span class=\"sw\" style=\"background:var(--ck-highlight)\"><\/span>Interest ${esc(this.money(ti))}<\/span>\r\n      <\/div>`;\r\n  }\r\n\r\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\r\n  renderPayoff() {\r\n    if (!this.current) return;\r\n    const { sched, per } = this.current;\r\n    const inc = per < 50 ? 5 : per < 500 ? 10 : 50;\r\n    const slider = this.$('extra');\r\n    slider.max = String(inc * 20);\r\n    slider.step = String(inc);\r\n    if (this.s.extra > Number(slider.max)) this.s.extra = 0;\r\n    slider.value = String(this.s.extra);\r\n    this.$('extra-label').textContent = `+${this.money(this.s.extra, 0)} ${this.perUnit()}`;\r\n\r\n    const out = this.$('payoff-out');\r\n    if (this.s.extra <= 0) {\r\n      out.innerHTML = `<span style=\"color:var(--ck-muted)\">Drag to see how a little extra shortens the loan.<\/span>`;\r\n      return;\r\n    }\r\n    const p = earlyPayoff(sched, this.s.amount, this.s.extra);\r\n    if (p.state === 'pays_off_now') {\r\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>.`;\r\n      return;\r\n    }\r\n    const saved = p.interest_saved > 0 ? ` and save <strong>${esc(this.money(p.interest_saved))}<\/strong> in interest` : '';\r\n    out.innerHTML = `Pay <strong>${esc(this.money(this.s.extra, 0))}<\/strong> extra ${esc(this.perUnit())} \u2192\r\n      debt-free <strong>${p.payments_saved} payment${p.payments_saved === 1 ? '' : 's'} sooner<\/strong>\r\n      (${esc(this.dateLabel(p.new_payoff))})${saved}.`;\r\n  }\r\n\r\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\r\n  pinScenario() {\r\n    if (!this.current) return;\r\n    if (this.pinned.length >= 3) this.pinned.shift();\r\n    const { sched, per, n } = this.current;\r\n    this.pinned.push({\r\n      amount: this.s.amount, rate: this.s.rate, term: this.s.term,\r\n      frequency: this.s.frequency, per, n,\r\n      interest: sched.totals.interest, repaid: sched.totals.repaid,\r\n      payoff: sched.rows[n - 1].due_date,\r\n    });\r\n    this.renderScenarios();\r\n    this.track('pin', { count: this.pinned.length });\r\n  }\r\n\r\n  renderScenarios() {\r\n    const wrap = this.$('scenarios');\r\n    const unpin = this.$('unpin');\r\n    if (this.pinned.length === 0) { wrap.hidden = true; unpin.hidden = true; return; }\r\n    wrap.hidden = false; unpin.hidden = false;\r\n    const rows = this.pinned.map((p, i) => `<tr>\r\n      <td>Scenario ${String.fromCharCode(65 + i)}<\/td>\r\n      <td>${esc(this.money(p.amount, 0))}<\/td>\r\n      <td>${esc(String(p.rate))}%<\/td>\r\n      <td>${p.term} mo<\/td>\r\n      <td>${esc(this.money(p.per))} ${esc({ weekly: '\/wk', fortnightly: '\/2wk', monthly: '\/mo' }[p.frequency])}<\/td>\r\n      <td>${esc(this.money(p.interest))}<\/td>\r\n      <td>${esc(this.money(p.repaid))}<\/td>\r\n    <\/tr>`).join('');\r\n    this.$('scenario-table').innerHTML = `<table>\r\n      <thead><tr><th scope=\"col\">Pinned<\/th><th scope=\"col\">Amount<\/th><th scope=\"col\">Rate<\/th><th scope=\"col\">Term<\/th>\r\n      <th scope=\"col\">Repayment<\/th><th scope=\"col\">Interest<\/th><th scope=\"col\">Total repaid<\/th><\/tr><\/thead>\r\n      <tbody>${rows}<\/tbody><\/table>`;\r\n  }\r\n\r\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\r\n  renderSchedule(sched) {\r\n    const rows = sched.rows;\r\n    const cells = (r) => `<tr><td>${r.number}<\/td><td>${esc(this.dateLabel(r.due_date))}<\/td>\r\n      <td>${esc(this.money(r.payment))}<\/td><td>${esc(this.money(r.interest))}<\/td>\r\n      <td>${esc(this.money(r.principal))}<\/td><td>${esc(this.money(r.balance))}<\/td><\/tr>`;\r\n    \/\/ Very long weekly schedules: elide the middle in the TABLE (CSV has it all).\r\n    let body;\r\n    if (rows.length > 120) {\r\n      body = rows.slice(0, 100).map(cells).join('')\r\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>`\r\n        + rows.slice(-12).map(cells).join('');\r\n    } else {\r\n      body = rows.map(cells).join('');\r\n    }\r\n    this.$('schedule-table').innerHTML = `<table>\r\n      <thead><tr><th scope=\"col\">#<\/th><th scope=\"col\">Due<\/th><th scope=\"col\">Payment<\/th>\r\n      <th scope=\"col\">Interest<\/th><th scope=\"col\">Principal<\/th><th scope=\"col\">Balance<\/th><\/tr><\/thead>\r\n      <tbody>${body}<\/tbody><\/table>`;\r\n  }\r\n\r\n  downloadCsv() {\r\n    if (!this.current) return;\r\n    const { sched } = this.current;\r\n    const lines = ['number,due_date,payment,interest,principal,balance'];\r\n    for (const r of sched.rows) {\r\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(','));\r\n    }\r\n    lines.push('', `# Chipkie family loan calculator \u2014 ${this.s.amount} at ${this.s.rate}% over ${this.s.term} months (${this.s.frequency})`);\r\n    const blob = new Blob([lines.join('\\n')], { type: 'text\/csv' });\r\n    const a = document.createElement('a');\r\n    a.href = URL.createObjectURL(blob);\r\n    a.download = 'chipkie-family-loan-schedule.csv';\r\n    a.click();\r\n    setTimeout(() => URL.revokeObjectURL(a.href), 5000);\r\n    this.track('csv');\r\n  }\r\n\r\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\r\n  updateCta() {\r\n    const q = new URLSearchParams({\r\n      mode: 'chat',\r\n      market: this.market,\r\n      amount: String(this.s.amount),\r\n      rate: String(this.s.rate),\r\n      frequency: this.s.frequency,\r\n      term_months: String(this.s.term),\r\n      ck_src: 'calculator',\r\n      ck_calc: `a${this.s.amount}-r${this.s.rate}-t${this.s.term}-${this.s.frequency}`,\r\n    });\r\n    if (this.s.state) q.set('state', this.s.state);\r\n    for (const [k, v] of Object.entries(pageAttribution())) {\r\n      if (k !== 'referrer') q.set(k, v);\r\n    }\r\n    this.$('cta-link').href = `${this.ctaUrl}?${q.toString()}`;\r\n\r\n    const price = this.cfg.contract?.available && this.cfg.contract?.price;\r\n    const stack = this.$('value-stack');\r\n    if (price && !stack.querySelector('.price-li')) {\r\n      const li = document.createElement('li');\r\n      li.className = 'price-li';\r\n      li.textContent = `Contract from ${this.money(price)} \u2014 no subscription needed`;\r\n      stack.appendChild(li);\r\n    }\r\n    \/\/ Full markets (AU) also sell the automated-payments subscription.\r\n    const premium = this.cfg.premium?.available && this.cfg.premium?.price;\r\n    if (premium && !stack.querySelector('.premium-li')) {\r\n      const li = document.createElement('li');\r\n      li.className = 'premium-li';\r\n      li.textContent = `Optional automated payments from ${this.money(premium)}\/mo`;\r\n      stack.appendChild(li);\r\n    }\r\n  }\r\n\r\n  async submitLead() {\r\n    const email = this.$('lead-email').value.trim();\r\n    const msg = this.$('capture-msg');\r\n    if (!\/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$\/.test(email)) {\r\n      msg.textContent = 'Please enter a valid email.';\r\n      return;\r\n    }\r\n    const { sched, per, n } = this.current || {};\r\n    const body = {\r\n      email,\r\n      market: this.market,\r\n      state: this.s.state || null,\r\n      website: this.root.querySelector('.hp').value, \/\/ honeypot\r\n      scenario: {\r\n        amount: this.s.amount, rate: this.s.rate, term_months: this.s.term,\r\n        frequency: this.s.frequency, start: this.s.start,\r\n        per_payment: per ?? null, payments: n ?? null,\r\n        total_interest: sched?.totals.interest ?? null,\r\n        total_repaid: sched?.totals.repaid ?? null,\r\n      },\r\n      attribution: pageAttribution(),\r\n    };\r\n    msg.textContent = 'Saving\u2026';\r\n    try {\r\n      const res = await fetch(this.leadUrl, {\r\n        method: 'POST', mode: 'cors',\r\n        headers: { 'Content-Type': 'application\/json', Accept: 'application\/json' },\r\n        body: JSON.stringify(body),\r\n      });\r\n      if (!res.ok) throw new Error(String(res.status));\r\n      msg.textContent = 'Saved! Grab the CSV above for a copy now \u2014 and watch your inbox.';\r\n      this.$('lead-email').value = '';\r\n      this.track('email_capture');\r\n    } catch {\r\n      msg.textContent = 'Couldn\u2019t save right now \u2014 the Download CSV button above works offline.';\r\n    }\r\n  }\r\n\r\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\r\n  track(event, params = {}) {\r\n    if (!this.analyticsOn) return;\r\n    try { (window.dataLayer = window.dataLayer || []).push({ event: `ck_calc_${event}`, ...params }); } catch { \/* no-op *\/ }\r\n  }\r\n\r\n  trackOnce(key, event, params) {\r\n    if (this._eventsSent[key]) return;\r\n    this._eventsSent[key] = true;\r\n    this.track(event, params);\r\n  }\r\n\r\n  trackDebounced(event, params) {\r\n    clearTimeout(this._tdb);\r\n    this._tdb = setTimeout(() => this.track(event, params), 800);\r\n  }\r\n\r\n  renderFootnote() {\r\n    const bits = ['Figures are a guide only and match how Chipkie schedules loans (reducing-balance).'];\r\n    if (this.market === 'US') {\r\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.');\r\n      if (this.cfg.usury?.as_of) bits.push(`State limits last reviewed ${this.cfg.usury.as_of}.`);\r\n    } else if (this.market === 'AU') {\r\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.');\r\n    } else if (this.market === 'GB') {\r\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.');\r\n    }\r\n    this.$('footnote').textContent = bits.join(' ');\r\n  }\r\n}\r\n\r\nif (!customElements.get('chipkie-loan-calc')) {\r\n  customElements.define('chipkie-loan-calc', ChipkieLoanCalc);\r\n}\r\n\r\n})();\r\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-773117a elementor-widget__width-initial elementor-widget elementor-widget-image\" data-id=\"773117a\" 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=\"768\" height=\"960\" src=\"https:\/\/chipkie.com\/uk\/wp-content\/uploads\/sites\/9\/2024\/04\/how-to-calculate-interest-rates-768x960.jpg\" class=\"attachment-medium_large size-medium_large wp-image-2425\" alt=\"how to calculate interest rates\" srcset=\"https:\/\/chipkie.com\/uk\/wp-content\/uploads\/sites\/9\/2024\/04\/how-to-calculate-interest-rates-768x960.jpg 768w, https:\/\/chipkie.com\/uk\/wp-content\/uploads\/sites\/9\/2024\/04\/how-to-calculate-interest-rates-240x300.jpg 240w, https:\/\/chipkie.com\/uk\/wp-content\/uploads\/sites\/9\/2024\/04\/how-to-calculate-interest-rates-819x1024.jpg 819w, https:\/\/chipkie.com\/uk\/wp-content\/uploads\/sites\/9\/2024\/04\/how-to-calculate-interest-rates.jpg 1080w\" sizes=\"(max-width: 768px) 100vw, 768px\" \/>\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\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<h3><b>How to use this calculator<\/b><\/h3><ul data-path-to-node=\"18\"><li><p data-path-to-node=\"18,0,0\"><b>Loan Amount:<\/b> Enter the total principal amount being lent (e.g., $50,000).<\/p><\/li><li><p data-path-to-node=\"18,1,0\"><b>Interest Rate:<\/b> Enter the agreed annual interest rate. If this is a &#8220;Bank of Mum and Dad&#8221; gift or soft loan, simply enter <b>0<\/b>.<\/p><\/li><li><p data-path-to-node=\"18,2,0\"><b>Loan Term:<\/b> Enter the total number of months the borrower has to pay the money back (e.g., 60 months for a 5-year loan).<\/p><\/li><\/ul>\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<h3><b>Why do I need to calculate repayments for a family loan?<\/b><\/h3>\n<p data-path-to-node=\"19\">Even if you trust your family member implicitly, a verbal agreement often leads to confusion. &#8220;Pay me back when you can&#8221; is the most dangerous phrase in family finance.<\/p>\n<p data-path-to-node=\"20\">Using a calculator to set a fixed repayment amount creates:<\/p>\n<ol start=\"1\" data-path-to-node=\"21\">\n<li>\n<p data-path-to-node=\"21,0,0\"><b>Discipline:<\/b> The borrower can budget for the expense.<\/p>\n<\/li>\n<li>\n<p data-path-to-node=\"21,1,0\"><b>Transparency:<\/b> Both parties know exactly when the debt will be cleared.<\/p>\n<\/li>\n<li>\n<p data-path-to-node=\"21,2,0\"><b>Fairness:<\/b> If there are siblings, documenting the loan ensures no accusations of favoritism later.<\/p>\n<\/li>\n<\/ol>\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<h3><b>Can I lend money to family at 0% interest?<\/b><\/h3>\n<p data-path-to-node=\"22\">Yes. In Australia, you are generally free to lend money to family members at whatever interest rate you choose, including 0%. This is often called an interest-free loan.<\/p>\n<p data-path-to-node=\"23\">However, if you do charge interest, that interest is considered <b>taxable income<\/b> by the ATO and must be declared on your tax return. The borrower generally cannot claim a tax deduction for the interest unless the loan is used for income-producing purposes (like buying an investment property).<\/p>\n<p data-path-to-node=\"24\"><i>&gt; <b>Note:<\/b> Chipkie does not provide tax advice. Please consult a qualified accountant regarding your specific situation.<\/i><\/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<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<h3><b>Turning these numbers into a legal agreement<\/b><\/h3>\n<p data-path-to-node=\"25\">Calculating the repayment is step one. Step two is documenting it.<\/p>\n<p data-path-to-node=\"26\">Without a written agreement, a transfer of money can be mistaken for a gift by the courts (in family law disputes) or by the ATO. A formal <b>Family Loan Agreement<\/b> protects both the lender&#8217;s capital and the borrower&#8217;s relationship.<\/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<\/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\">Chipkie is free for everyone!<\/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>At Chipkie, it\u2019s our mission to see your communities flourish through uncertain times. And as a country renowned for looking out for our mates, we want to make it easier to lend a hand when the people we love need it most. That\u2019s why it\u2019s 100% free to join, no hidden catches, no hidden fee\u2019s. Just good Karma.<\/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=\"https:\/\/chipkie.com\/uk\/pricing\/\">\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\">Find out more<\/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 decoding=\"async\" width=\"600\" height=\"463\" data-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 lazyload\" alt=\"High five for Chipkie!\" data-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\" data-sizes=\"(max-width: 600px) 100vw, 600px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 600px; --smush-placeholder-aspect-ratio: 600\/463;\" \/>\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>Family Loan Repayment Calculator by Chipkie Planning a loan between friends or family members? Standard bank calculators often fail because they assume high interest rates and rigid terms. Get Started The Chipkie Family Loan Calculator is designed specifically for private lending Whether you are charging commercial interest or providing an interest-free loan (0% interest) to &#8230; <a title=\"Family Loan Calculator\" class=\"read-more\" href=\"https:\/\/chipkie.com\/uk\/family-loan-calculator\/\" aria-label=\"Read more about Family Loan Calculator\">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-2986","page","type-page","status-publish"],"_links":{"self":[{"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/pages\/2986","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=2986"}],"version-history":[{"count":62,"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/pages\/2986\/revisions"}],"predecessor-version":[{"id":3517,"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/pages\/2986\/revisions\/3517"}],"wp:attachment":[{"href":"https:\/\/chipkie.com\/uk\/wp-json\/wp\/v2\/media?parent=2986"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}