JustPaste.it

// ==UserScript==
// @name         Hosting Proration Calculator
// @namespace    hosting-proration-calc
// @version      2.0
// @description  Prorated extension/refund/plan-change/audit calculator for hosting support teams
// @author       rpqu + claude opus 4.6
// @match        https://YOUR-SITE-HERE.com/*
// @grant        none
// @license      GPL-3.0-or-later
// ==/UserScript==
/*
    Hosting Proration Calculator
    Copyright (C) 2026 rpqu

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/
(function () {
  "use strict";

  // ═══════════════════════════════════════════════════════════════════════════
  //  Storage & Config
  // ═══════════════════════════════════════════════════════════════════════════

  const SKEY = "hpc_cfg";
  const DEFAULTS = { feePct: 0, feeFlat: 0, cur: "$", penType: "none", penVal: 0 };

  function loadCfg() {
    try { return { ...DEFAULTS, ...JSON.parse(localStorage.getItem(SKEY)) }; }
    catch { return { ...DEFAULTS }; }
  }
  function saveCfg(c) { localStorage.setItem(SKEY, JSON.stringify(c)); }

  // ═══════════════════════════════════════════════════════════════════════════
  //  Date helpers
  // ═══════════════════════════════════════════════════════════════════════════

  function addMonths(d, n) {
    const r = new Date(d.getFullYear(), d.getMonth() + n, d.getDate());
    if (r.getDate() !== d.getDate()) r.setDate(0);
    return r;
  }
  function diffDays(a, b) { return Math.round((b - a) / 864e5); }
  function addDays(d, n) { return new Date(+d + n * 864e5); }
  function toDate(s) {
    if (!s) return null;
    const p = s.split("-").map(Number);
    if (p.length !== 3 || isNaN(p[0])) return null;
    return new Date(p[0], p[1] - 1, p[2]);
  }
  function fmtISO(d) {
    if (!d) return "";
    return d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2,"0") + "-" + String(d.getDate()).padStart(2,"0");
  }
  function fmtShort(d) {
    if (!d) return "?";
    return d.toLocaleDateString("en-US", { month:"short", day:"numeric", year:"numeric" });
  }
  function cycleEnd(start, cycle) {
    switch (cycle) {
      case "daily":       return addDays(start, 1);
      case "weekly":      return addDays(start, 7);
      case "fortnightly": return addDays(start, 14);
      case "monthly":     return addMonths(start, 1);
      case "quarterly":   return addMonths(start, 3);
      case "semi-annual": return addMonths(start, 6);
      case "yearly":      return addMonths(start, 12);
    }
    return addMonths(start, 12);
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  Math helpers
  // ═══════════════════════════════════════════════════════════════════════════

  function calcFee(amount, cfg) {
    if (amount <= 0) return 0;
    return Math.round((amount * (cfg.feePct / 100) + cfg.feeFlat) * 100) / 100;
  }
  function calcPenalty(basis, penType, penVal) {
    if (penType === "flat") return penVal;
    if (penType === "pct_remaining") return Math.round(basis * (penVal / 100) * 100) / 100;
    if (penType === "pct_total") return Math.round(basis * (penVal / 100) * 100) / 100;
    return 0;
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  State for invoice generation
  // ═══════════════════════════════════════════════════════════════════════════

  const state = { lastCalc: null };

  // ═══════════════════════════════════════════════════════════════════════════
  //  CSS
  // ═══════════════════════════════════════════════════════════════════════════

  const CSS = `
:host { all: initial; system-ui, -apple-system, sans-serif; }
*, *::before, *::after { box-sizing: border-box; }

/* Toggle button */
.hpc-toggle {
  position: fixed; bottom: 20px; right: 20px; z-index: 2147483647;
  width: 44px; height: 44px; border-radius: 50%;
  background: #0d6efd; color: #fff; border: none; cursor: pointer;
  font-size: 20px; display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 8px rgba(0,0,0,.25); transition: transform .15s;
}
.hpc-toggle:hover { transform: scale(1.1); }

/* Panel */
.hpc-panel {
  position: fixed; bottom: 74px; right: 20px; z-index: 2147483647;
  width: 440px; max-height: 88vh; overflow-y: auto;
  background: #fff; border: 1px solid #dee2e6; border-radius: 10px;
  box-shadow: 0 4px 24px rgba(0,0,0,.18); display: none; color: #212529;
  font-size: 13px; 1.45;
}
.hpc-panel.open { display: block; }
.hpc-panel::-webkit-scrollbar { width: 6px; }
.hpc-panel::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 3px; }

/* Header */
.hpc-hdr {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 14px; background: #0d6efd; color: #fff;
  border-radius: 10px 10px 0 0; cursor: move; user-select: none;
}
.hpc-hdr h3 { 0; font-size: 14px; font-weight: 600; }
.hpc-hdr button {
  background: none; border: none; color: #fff; font-size: 18px;
  cursor: pointer; padding: 0 4px; opacity: .8; 1;
}
.hpc-hdr button:hover { opacity: 1; }
.hpc-body { padding: 0 14px 14px; }

/* Settings */
.hpc-settings { display: none; padding: 8px 0; border-bottom: 1px solid #e9ecef; }
.hpc-settings.open { display: block; }
.hpc-settings .s-row { display: flex; align-items: center; gap: 6px; 5px; }
.hpc-settings label { font-size: 12px; color: #6c757d; min-width: 120px; }
.hpc-settings input {
  width: 72px; padding: 3px 6px; border: 1px solid #ced4da; border-radius: 4px;
  font-size: 12px; text-align: right;
}
.hpc-settings .s-btns { display: flex; gap: 6px; 6px; }
.hpc-settings .s-btns button, .hpc-btn-sm {
  padding: 3px 10px; font-size: 11px; border: 1px solid #ced4da; border-radius: 4px;
  background: #fff; color: #495057; cursor: pointer;
}
.hpc-settings .s-btns button:hover, .hpc-btn-sm:hover { background: #e9ecef; }

/* Tabs */
.hpc-tabs { display: flex; flex-wrap: wrap; gap: 0; 10px 0 8px; }
.hpc-tab {
  flex: 1 1 auto; min-width: 0; padding: 6px 2px; text-align: center; cursor: pointer;
  border: 1px solid #dee2e6; background: #f8f9fa; color: #495057;
  font-size: 12px; font-weight: 500; transition: all .12s; white-space: nowrap;
}
.hpc-tab:first-child { border-radius: 6px 0 0 6px; }
.hpc-tab:last-child { border-radius: 0 6px 6px 0; }
.hpc-tab.active { background: #0d6efd; color: #fff; border-color: #0d6efd; }

/* Form */
.hpc-section { 10px; }
.hpc-stitle {
  font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
  color: #6c757d; 6px; font-weight: 600;
}
.hpc-f { display: flex; align-items: center; 6px; }
.hpc-f label { min-width: 125px; font-size: 12px; color: #495057; flex-shrink: 0; }
.hpc-f input, .hpc-f select {
  flex: 1; padding: 5px 8px; border: 1px solid #ced4da; border-radius: 5px;
  font-size: 13px; background: #fff; color: #212529; min-width: 0;
}
.hpc-f input[type="number"] { text-align: right; }
.hpc-f select { cursor: pointer; }
.hpc-f .auto {
  font-size: 11px; color: #6c757d; 6px; white-space: nowrap; flex-shrink: 0;
}
.hpc-chk { display: flex; align-items: center; gap: 6px; 6px; }
.hpc-chk input[type="checkbox"] { width: auto; 0; flex-shrink: 0; }
.hpc-chk label { font-size: 12px; color: #495057; cursor: pointer; }
.hpc-note { font-size: 11px; color: #6c757d; 2px 0 6px 125px; }
.hpc-adv-btn {
  font-size: 11px; color: #6c757d; cursor: pointer; border: none;
  background: none; padding: 2px 0; 4px;
}
.hpc-adv-btn:hover { color: #0d6efd; }
.hpc-adv { display: none; }
.hpc-adv.open { display: block; }

/* Radio group */
.hpc-radio { display: flex; gap: 12px; 6px; 125px; }
.hpc-radio label { font-size: 12px; display: flex; align-items: center; gap: 4px; cursor: pointer; color: #495057; }
.hpc-radio input { 0; }

/* Results */
.hpc-res {
  10px; padding: 10px; background: #f8f9fa;
  border: 1px solid #e9ecef; border-radius: 6px;
  "SF Mono","Cascadia Code","Consolas",monospace;
  font-size: 11.5px; 1.6; white-space: pre-wrap;
  word-break: break-word; min-height: 36px;
}
.hpc-res .hl { color: #0d6efd; font-weight: 600; }
.hpc-res .ok { color: #198754; font-weight: 600; }
.hpc-res .w { color: #dc3545; font-weight: 600; }
.hpc-res .d { color: #6c757d; }
.hpc-res .s { color: #adb5bd; }
.hpc-res-btns { display: flex; gap: 6px; justify-content: flex-end; 6px; }
.hpc-res-btns button {
  padding: 3px 10px; font-size: 11px; border: 1px solid #ced4da; border-radius: 4px;
  background: #fff; color: #495057; cursor: pointer;
}
.hpc-res-btns button:hover { background: #e9ecef; }

.hpc-pane { display: none; }
.hpc-pane.active { display: block; }

/* Batch cards */
.bc { border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; 8px; background: #fafbfc; }
.bc-hdr { display: flex; align-items: center; justify-content: space-between; 6px; }
.bc-hdr input { border: none; background: transparent; font-weight: 600; font-size: 13px; color: #212529; flex: 1; padding: 2px 0; }
.bc-hdr button { background: none; border: none; color: #adb5bd; font-size: 16px; cursor: pointer; padding: 0 4px; }
.bc-hdr button:hover { color: #dc3545; }
.bc-row { display: flex; gap: 6px; 4px; align-items: center; }
.bc-row label { font-size: 11px; color: #6c757d; min-width: 32px; }
.bc-row input { flex: 1; padding: 3px 6px; border: 1px solid #ced4da; border-radius: 4px; font-size: 12px; min-width: 0; }
.bc-row input[type="number"] { text-align: right; }
.bc-res { font-size: 11px; padding: 4px 0; monospace; color: #495057; }

/* Invoice overlay */
.hpc-overlay {
  display: none; position: fixed; inset: 0; z-index: 2147483647;
  background: rgba(0,0,0,.4); align-items: center; justify-content: center;
}
.hpc-overlay.open { display: flex; }
.hpc-modal {
  background: #fff; border-radius: 10px; padding: 16px; width: 460px; max-height: 80vh;
  overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,.25);
}
.hpc-modal h4 { 0 0 10px; font-size: 14px; }
.hpc-modal textarea {
  width: 100%; height: 300px; "SF Mono","Consolas",monospace;
  font-size: 12px; padding: 10px; border: 1px solid #ced4da; border-radius: 6px;
  resize: vertical; color: #212529; 1.5;
}
.hpc-modal .m-btns { display: flex; gap: 8px; justify-content: flex-end; 10px; }
.hpc-modal .m-btns button {
  padding: 6px 14px; font-size: 12px; border-radius: 5px; cursor: pointer; border: 1px solid #ced4da;
}
.hpc-modal .m-btns .m-primary { background: #0d6efd; color: #fff; border-color: #0d6efd; }
.hpc-modal .m-btns .m-secondary { background: #fff; color: #495057; }

/* FX locked badge */
.fx-lock { display: inline-block; font-size: 10px; background: #d1e7dd; color: #0f5132; padding: 1px 6px; border-radius: 3px; 6px; }

/* Case context bar */
.hpc-ctx {
  display: flex; gap: 6px; padding: 7px 14px; border-bottom: 1px solid #e9ecef;
  background: #f8f9fa;
}
.hpc-ctx input {
  flex: 1; padding: 4px 8px; border: 1px solid #dee2e6; border-radius: 4px;
  font-size: 12px; background: #fff; color: #212529; min-width: 0;
}
.hpc-ctx input::placeholder { color: #adb5bd; }

/* Template picker row */
.hpc-tmpl-row { display: flex; gap: 6px; 8px; align-items: center; }
.hpc-tmpl-row label { min-width: 125px; font-size: 12px; color: #6c757d; flex-shrink: 0; }
.hpc-tmpl-row select {
  flex: 1; padding: 4px 6px; border: 1px solid #dee2e6; border-radius: 4px;
  font-size: 12px; background: #fff; color: #212529; cursor: pointer;
}
.hpc-tmpl-row button {
  padding: 3px 8px; font-size: 11px; border: 1px solid #ced4da; border-radius: 4px;
  background: #fff; color: #6c757d; cursor: pointer; white-space: nowrap; flex-shrink: 0;
}
.hpc-tmpl-row button:hover { background: #e9ecef; color: #212529; }

/* Template list in settings */
.hpc-tmpl-list { 4px 0 6px; }
.hpc-tmpl-item {
  display: flex; align-items: center; gap: 6px; padding: 3px 0;
  font-size: 12px; border-bottom: 1px solid #f1f3f5;
}
.hpc-tmpl-item span { flex: 1; color: #212529; cursor: pointer; }
.hpc-tmpl-item span:hover { color: #0d6efd; text-decoration: underline; }
.hpc-tmpl-item .td { font-size: 11px; color: #6c757d; }
.hpc-tmpl-item button { background: none; border: none; color: #adb5bd; cursor: pointer; font-size: 14px; padding: 0 2px; }
.hpc-tmpl-item button:hover { color: #dc3545; }

/* History modal entries */
.hpc-hist-entry {
  display: flex; align-items: flex-start; gap: 8px; padding: 8px 0;
  border-bottom: 1px solid #f1f3f5; cursor: default;
}
.hpc-hist-entry:last-child { border-bottom: none; }
.hpc-hist-badge {
  font-size: 10px; font-weight: 700; padding: 2px 5px; border-radius: 3px;
  color: #fff; flex-shrink: 0; 1px; text-transform: uppercase;
}
.badge-ext   { background: #0d6efd; }
.badge-ref   { background: #198754; }
.badge-plan  { background: #6f42c1; }
.badge-audit { background: #fd7e14; }
.badge-batch { background: #0dcaf0; color: #000; }
.badge-fx    { background: #6c757d; }
.hpc-hist-info { flex: 1; min-width: 0; }
.hpc-hist-ts { font-size: 10px; color: #6c757d; }
.hpc-hist-ctx { font-size: 11px; color: #495057; font-weight: 500; }
.hpc-hist-lbl { font-size: 12px; color: #212529; 1px; }
.hpc-hist-restore {
  padding: 2px 8px; font-size: 11px; border: 1px solid #dee2e6; border-radius: 4px;
  background: #fff; color: #0d6efd; cursor: pointer; flex-shrink: 0;
}
.hpc-hist-restore:hover { background: #e7f1ff; }
.hpc-hist-empty { font-size: 12px; color: #6c757d; padding: 12px 0; text-align: center; }

/* Wider modal for history */
.hpc-modal-wide { width: 500px; }
`;

  // ═══════════════════════════════════════════════════════════════════════════
  //  Cycle options HTML (reusable)
  // ═══════════════════════════════════════════════════════════════════════════

  const CYCLE_OPTS = `
    <option value="daily">Daily</option>
    <option value="weekly">Weekly</option>
    <option value="fortnightly">Fortnightly</option>
    <option value="monthly">Monthly</option>
    <option value="quarterly">Quarterly</option>
    <option value="semi-annual">Semi-annual</option>
    <option value="yearly" selected>Yearly</option>`;

  const PEN_OPTS = `
    <option value="none">None</option>
    <option value="flat">Flat fee</option>
    <option value="pct_remaining">% of refund</option>
    <option value="pct_total">% of total paid</option>`;

  // ═══════════════════════════════════════════════════════════════════════════
  //  HTML
  // ═══════════════════════════════════════════════════════════════════════════

  const HTML = `
<button class="hpc-toggle" id="hpcBtn" title="Proration Calculator">&#9874;</button>

<div class="hpc-panel" id="hpcPanel">
  <div class="hpc-hdr" id="hpcHdr">
    <h3>Proration Calculator</h3>
    <div><button id="btnHist" title="History">&#9203;</button> <button id="btnCfg" title="Settings">&#9881;</button> <button id="btnX">&times;</button></div>
  </div>
  <!-- case context bar -->
  <div class="hpc-ctx">
    <input type="text" id="ctxTicket" placeholder="Ticket / Case ID" maxlength="50">
    <input type="text" id="ctxCustomer" placeholder="Customer name" maxlength="80">
  </div>
  <div class="hpc-body">

    <!-- ═══ Settings ═══ -->
    <div class="hpc-settings" id="secCfg">
      <div class="s-row"><label>Processor fee %</label><input type="number" step="0.01" min="0" id="cfgPct"></div>
      <div class="s-row"><label>Processor flat fee</label><input type="number" step="0.01" min="0" id="cfgFlat"></div>
      <div class="s-row"><label>Currency symbol</label><input type="text" maxlength="4" id="cfgCur" style="width:40px;text-align:center"></div>
      <div class="s-row"><label>Default penalty</label><select id="cfgPenType">${PEN_OPTS}</select></div>
      <div class="s-row" id="cfgPenRow" style="display:none"><label>Penalty amount/%</label><input type="number" step="0.01" min="0" id="cfgPenVal"></div>
      <div class="s-btns">
        <button id="btnExport">Export Config</button>
        <button id="btnImport">Import Config</button>
        <input type="file" id="fileImport" accept=".json" style="display:none">
      </div>
      <div style="8px;border-top:1px solid #e9ecef;padding-top:6px">
        <div style="display:flex;align-items:center;justify-content:space-between;4px">
          <span style="font-size:12px;font-weight:600;color:#6c757d">Templates</span>
          <button class="hpc-btn-sm" id="tmplAddBtn">+ Add</button>
        </div>
        <div class="hpc-tmpl-list" id="tmplList"></div>
      </div>
    </div>

    <!-- ═══ Tabs ═══ -->
    <div class="hpc-tabs">
      <div class="hpc-tab active" data-tab="ext">Extend</div>
      <div class="hpc-tab" data-tab="ref">Refund</div>
      <div class="hpc-tab" data-tab="plan">Plan&nbsp;&Delta;</div>
      <div class="hpc-tab" data-tab="audit">Audit</div>
      <div class="hpc-tab" data-tab="batch">Batch</div>
      <div class="hpc-tab" data-tab="fx">FX</div>
    </div>

    <!-- ═══════════ EXTEND ═══════════ -->
    <div class="hpc-pane active" id="paneExt">
      <div class="hpc-section">
        <div class="hpc-stitle">Contract</div>
        <div class="hpc-tmpl-row"><label>Template</label><select id="extTmplSel"><option value="">— load template —</option></select><button id="extTmplSave" title="Save current as template">Save as…</button></div>
        <div class="hpc-f"><label>Price per cycle</label><input type="number" step="0.01" min="0" id="extPrice" placeholder="45.00"></div>
        <div class="hpc-f"><label>Billing cycle</label><select id="extCycle">${CYCLE_OPTS}</select></div>
        <div class="hpc-f"><label>Current billing end</label><input type="date" id="extEnd"></div>
        <div class="hpc-f"><label>New target end</label><input type="date" id="extTarget"></div>
      </div>
      <button class="hpc-adv-btn" data-for="extAdv">&#9662; Advanced</button>
      <div class="hpc-adv" id="extAdv">
        <div class="hpc-f"><label>Proration divisor</label><input type="number" min="1" id="extDiv" placeholder="auto"><span class="auto" id="extDivA"></span></div>
        <div class="hpc-f"><label>Pre-existing credit</label><input type="number" step="0.01" min="0" id="extCred" value="0"></div>
      </div>
      <div class="hpc-res" id="extR"><span class="d">Fill in the fields above.</span></div>
      <div class="hpc-res-btns"><button data-copy="extR">Copy</button><button data-note="ext">Draft Invoice</button></div>
    </div>

    <!-- ═══════════ REFUND ═══════════ -->
    <div class="hpc-pane" id="paneRef">
      <div class="hpc-section">
        <div class="hpc-stitle">Service period</div>
        <div class="hpc-f"><label>Period start</label><input type="date" id="refStart"></div>
        <div class="hpc-f"><label>Billing end</label><input type="date" id="refEnd"></div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">Payment</div>
        <div class="hpc-tmpl-row"><label>Template</label><select id="refTmplSel"><option value="">— load template —</option></select><button id="refTmplSave">Save as…</button></div>
        <div class="hpc-f"><label>Payment amount</label><input type="number" step="0.01" min="0" id="refPay" placeholder="32.42"></div>
        <div class="hpc-f"><label>Credit applied</label><input type="number" step="0.01" min="0" id="refCred" value="0"></div>
        <div class="hpc-note">Credit = pre-existing balance (no processor fee on it)</div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">Cutoff</div>
        <div class="hpc-f"><label>Cutoff date</label><input type="date" id="refCut"></div>
        <div class="hpc-chk"><input type="checkbox" id="refCutInc" checked><label for="refCutInc">Include cutoff date in unused days</label></div>
      </div>
      <button class="hpc-adv-btn" data-for="refAdv">&#9662; Advanced</button>
      <div class="hpc-adv" id="refAdv">
        <div class="hpc-f"><label>Proration divisor</label><input type="number" min="1" id="refDiv" placeholder="auto"><span class="auto" id="refDivA"></span></div>
        <div class="hpc-f"><label>Processor fee override</label><input type="number" step="0.01" min="0" id="refFee" placeholder="auto"><span class="auto" id="refFeeA"></span></div>
        <div class="hpc-chk"><input type="checkbox" id="refRfFee"><label for="refRfFee">Refund itself incurs processor fee</label></div>
        <div class="hpc-stitle" style="8px">Early termination penalty</div>
        <div class="hpc-f"><label>Penalty type</label><select id="refPenT">${PEN_OPTS}</select></div>
        <div class="hpc-f" id="refPenR" style="display:none"><label>Penalty amount/%</label><input type="number" step="0.01" min="0" id="refPenV" value="0"></div>
      </div>
      <div class="hpc-res" id="refR"><span class="d">Fill in the fields above.</span></div>
      <div class="hpc-res-btns"><button data-copy="refR">Copy</button><button data-note="ref">Draft Credit Note</button></div>
    </div>

    <!-- ═══════════ PLAN CHANGE ═══════════ -->
    <div class="hpc-pane" id="panePlan">
      <div class="hpc-section">
        <div class="hpc-stitle">Old plan</div>
        <div class="hpc-tmpl-row"><label>Template</label><select id="plOldTmplSel"><option value="">— load template —</option></select><button id="plOldTmplSave">Save as…</button></div>
        <div class="hpc-f"><label>Price per cycle</label><input type="number" step="0.01" min="0" id="plOldP" placeholder="10.00"></div>
        <div class="hpc-f"><label>Billing cycle</label><select id="plOldC">${CYCLE_OPTS}</select></div>
        <div class="hpc-f"><label>Current period start</label><input type="date" id="plOldS"></div>
        <div class="hpc-f"><label>Current period end</label><input type="date" id="plOldE"></div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">Change</div>
        <div class="hpc-f"><label>Change date</label><input type="date" id="plChg"></div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">New plan</div>
        <div class="hpc-tmpl-row"><label>Template</label><select id="plNewTmplSel"><option value="">— load template —</option></select><button id="plNewTmplSave">Save as…</button></div>
        <div class="hpc-f"><label>Price per cycle</label><input type="number" step="0.01" min="0" id="plNewP" placeholder="25.00"></div>
        <div class="hpc-f"><label>Billing cycle</label><select id="plNewC">${CYCLE_OPTS}</select></div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">Cycle handling</div>
        <div class="hpc-radio">
          <label><input type="radio" name="plMode" value="continue" checked> Continue current cycle</label>
          <label><input type="radio" name="plMode" value="fresh"> Fresh cycle</label>
        </div>
        <div class="hpc-note" style="0">Continue = new plan inherits old end date. Fresh = new full cycle from change date.</div>
      </div>
      <button class="hpc-adv-btn" data-for="plAdv">&#9662; Advanced</button>
      <div class="hpc-adv" id="plAdv">
        <div class="hpc-f"><label>Old plan divisor</label><input type="number" min="1" id="plOldDiv" placeholder="auto"><span class="auto" id="plOldDivA"></span></div>
        <div class="hpc-f"><label>New plan divisor</label><input type="number" min="1" id="plNewDiv" placeholder="auto"><span class="auto" id="plNewDivA"></span></div>
      </div>
      <div class="hpc-res" id="plR"><span class="d">Fill in the fields above.</span></div>
      <div class="hpc-res-btns"><button data-copy="plR">Copy</button><button data-note="plan">Draft Adjustment Note</button></div>
    </div>

    <!-- ═══════════ AUDIT ═══════════ -->
    <div class="hpc-pane" id="paneAudit">
      <div class="hpc-section">
        <div class="hpc-stitle">Service period</div>
        <div class="hpc-f"><label>Period start</label><input type="date" id="audStart"></div>
        <div class="hpc-f"><label>Billing end</label><input type="date" id="audEnd"></div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">Payment</div>
        <div class="hpc-tmpl-row"><label>Template</label><select id="audTmplSel"><option value="">— load template —</option></select><button id="audTmplSave">Save as…</button></div>
        <div class="hpc-f"><label>Payment amount</label><input type="number" step="0.01" min="0" id="audPay"></div>
        <div class="hpc-f"><label>Credit applied</label><input type="number" step="0.01" min="0" id="audCred" value="0"></div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">Cutoff</div>
        <div class="hpc-f"><label>Cutoff date</label><input type="date" id="audCut"></div>
        <div class="hpc-chk"><input type="checkbox" id="audCutInc" checked><label for="audCutInc">Include cutoff date in unused days</label></div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">Actual refund received</div>
        <div class="hpc-f"><label>Amount received</label><input type="number" step="0.01" min="0" id="audActual" placeholder="3.35"></div>
      </div>
      <button class="hpc-adv-btn" data-for="audAdv">&#9662; Advanced</button>
      <div class="hpc-adv" id="audAdv">
        <div class="hpc-f"><label>Proration divisor</label><input type="number" min="1" id="audDiv" placeholder="auto"><span class="auto" id="audDivA"></span></div>
        <div class="hpc-f"><label>Processor fee override</label><input type="number" step="0.01" min="0" id="audFee" placeholder="auto"><span class="auto" id="audFeeA"></span></div>
        <div class="hpc-stitle" style="8px">Early termination penalty</div>
        <div class="hpc-f"><label>Penalty type</label><select id="audPenT">${PEN_OPTS}</select></div>
        <div class="hpc-f" id="audPenR" style="display:none"><label>Penalty amount/%</label><input type="number" step="0.01" min="0" id="audPenV" value="0"></div>
      </div>
      <div class="hpc-res" id="audR"><span class="d">Fill in the fields above.</span></div>
      <div class="hpc-res-btns"><button data-copy="audR">Copy</button><button data-note="audit">Draft Discrepancy Report</button></div>
    </div>

    <!-- ═══════════ BATCH ═══════════ -->
    <div class="hpc-pane" id="paneBatch">
      <div class="hpc-section">
        <div class="hpc-stitle">Common parameters</div>
        <div class="hpc-f"><label>Cutoff date</label><input type="date" id="batCut"></div>
        <div class="hpc-chk"><input type="checkbox" id="batCutInc" checked><label for="batCutInc">Include cutoff date in unused days</label></div>
      </div>
      <div class="hpc-section">
        <div class="hpc-stitle">Services <button class="hpc-btn-sm" id="batAdd">+ Add</button></div>
        <div id="batList"></div>
      </div>
      <div class="hpc-res" id="batR"><span class="d">Add services and fill in the cutoff date.</span></div>
      <div class="hpc-res-btns"><button data-copy="batR">Copy</button><button data-note="batch">Draft Credit Note</button></div>
    </div>

    <!-- ═══════════ FX (Currency) ═══════════ -->
    <div class="hpc-pane" id="paneFx">
      <div class="hpc-section">
        <div class="hpc-stitle">Currency conversion</div>
        <div class="hpc-f"><label>Amount</label><input type="number" step="0.01" id="fxAmt" placeholder="100.00"></div>
        <div class="hpc-f"><label>From currency</label><input type="text" id="fxFrom" value="USD" maxlength="5" style="text-transform:uppercase"></div>
        <div class="hpc-f"><label>Exchange rate</label><input type="number" step="0.000001" id="fxRate" placeholder="1.00"></div>
        <div class="hpc-f"><label>To currency</label><input type="text" id="fxTo" value="EUR" maxlength="5" style="text-transform:uppercase"></div>
      </div>
      <div style="display:flex;gap:8px;8px">
        <button class="hpc-btn-sm" id="fxLock">Lock Rate</button>
        <span id="fxLockInfo" style="font-size:11px;color:#6c757d;26px"></span>
      </div>
      <div class="hpc-res" id="fxR"><span class="d">Enter amount and rate.</span></div>
      <div class="hpc-res-btns"><button data-copy="fxR">Copy</button></div>
    </div>

  </div><!-- .hpc-body -->
</div><!-- .hpc-panel -->

<!-- Template add/edit overlay -->
<div class="hpc-overlay" id="tmplOvr">
  <div class="hpc-modal" style="width:360px">
    <h4 id="tmplOvrTitle">Add Template</h4>
    <div class="hpc-f" style="8px"><label style="min-width:80px;font-size:12px">Name</label><input type="text" id="tmplName" placeholder="e.g. VPS Basic" maxlength="40"></div>
    <div class="hpc-f" style="8px"><label style="min-width:80px;font-size:12px">Price</label><input type="number" step="0.01" min="0" id="tmplPrice" placeholder="45.00"></div>
    <div class="hpc-f" style="8px"><label style="min-width:80px;font-size:12px">Cycle</label><select id="tmplCycle">${CYCLE_OPTS}</select></div>
    <div class="hpc-f" style="8px"><label style="min-width:80px;font-size:12px">Divisor</label><input type="number" min="1" id="tmplDiv" placeholder="auto (leave blank)"></div>
    <input type="hidden" id="tmplEditId">
    <div class="m-btns">
      <button class="m-secondary" id="tmplOvrClose">Cancel</button>
      <button class="m-primary" id="tmplOvrSave">Save Template</button>
    </div>
  </div>
</div>

<!-- History overlay -->
<div class="hpc-overlay" id="histOvr">
  <div class="hpc-modal hpc-modal-wide">
    <div style="display:flex;align-items:center;justify-content:space-between;10px">
      <h4 style="0">Calculation History</h4>
      <button class="m-secondary" id="histClear" style="font-size:11px;padding:3px 8px;border:1px solid #ced4da;border-radius:4px;background:#fff;cursor:pointer">Clear all</button>
    </div>
    <div id="histList"></div>
    <div class="m-btns"><button class="m-secondary" id="histClose">Close</button></div>
  </div>
</div>

<!-- Invoice overlay -->
<div class="hpc-overlay" id="noteOvr">
  <div class="hpc-modal">
    <h4 id="noteTitle">Document</h4>
    <textarea id="noteTxt"></textarea>
    <div class="m-btns">
      <button class="m-secondary" id="noteClose">Close</button>
      <button class="m-primary" id="noteCopy">Copy to Clipboard</button>
    </div>
  </div>
</div>
`;

  // ═══════════════════════════════════════════════════════════════════════════
  //  Init UI (Shadow DOM)
  // ═══════════════════════════════════════════════════════════════════════════

  const host = document.createElement("div");
  document.body.appendChild(host);
  const shadow = host.attachShadow({ mode: "closed" });

  const styleEl = document.createElement("style");
  styleEl.textContent = CSS;
  shadow.appendChild(styleEl);

  const wrap = document.createElement("div");
  wrap.innerHTML = HTML;
  shadow.appendChild(wrap);

  const $ = (id) => shadow.getElementById(id);
  const $v = (id) => { const e = $(id); return e ? e.value : ""; };
  const $f = (id) => parseFloat($v(id)) || 0;
  const $d = (id) => toDate($v(id));
  const $ck = (id) => { const e = $(id); return e ? e.checked : false; };

  // ═══════════════════════════════════════════════════════════════════════════
  //  Load config into UI
  // ═══════════════════════════════════════════════════════════════════════════

  let cfg = loadCfg();
  function cfgToUI() {
    $("cfgPct").value = cfg.feePct;
    $("cfgFlat").value = cfg.feeFlat;
    $("cfgCur").value = cfg.cur;
    $("cfgPenType").value = cfg.penType || "none";
    $("cfgPenVal").value = cfg.penVal || 0;
    $("cfgPenRow").style.display = cfg.penType !== "none" ? "" : "none";
    $("refPenT").value = cfg.penType || "none";
    $("refPenV").value = cfg.penVal || 0;
    $("refPenR").style.display = cfg.penType !== "none" ? "" : "none";
    $("audPenT").value = cfg.penType || "none";
    $("audPenV").value = cfg.penVal || 0;
    $("audPenR").style.display = cfg.penType !== "none" ? "" : "none";
  }
  cfgToUI();

  function readCfg() {
    cfg.feePct = parseFloat($("cfgPct").value) || 0;
    cfg.feeFlat = parseFloat($("cfgFlat").value) || 0;
    cfg.cur = $("cfgCur").value || "$";
    cfg.penType = $v("cfgPenType");
    cfg.penVal = parseFloat($("cfgPenVal").value) || 0;
    saveCfg(cfg);
  }
  function C(n) { return cfg.cur + n.toFixed(2); }

  // ═══════════════════════════════════════════════════════════════════════════
  //  Config export / import
  // ═══════════════════════════════════════════════════════════════════════════

  $("btnExport").addEventListener("click", () => {
    readCfg();
    const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = "hpc-config.json"; a.click();
    URL.revokeObjectURL(url);
  });

  $("btnImport").addEventListener("click", () => $("fileImport").click());
  $("fileImport").addEventListener("change", (e) => {
    const f = e.target.files[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = (ev) => {
      try {
        const data = JSON.parse(ev.target.result);
        cfg = { ...DEFAULTS, ...data };
        saveCfg(cfg);
        cfgToUI();
        recalcActive();
      } catch { alert("Invalid config file."); }
    };
    reader.readAsText(f);
    e.target.value = "";
  });

  // Penalty type toggles visibility of value field
  ["cfgPenType"].forEach(id => {
    $(id).addEventListener("change", () => {
      $("cfgPenRow").style.display = $v(id) !== "none" ? "" : "none";
      readCfg();
    });
  });
  ["refPenT"].forEach(id => {
    $(id).addEventListener("change", () => {
      $("refPenR").style.display = $v(id) !== "none" ? "" : "none";
      calcRef();
    });
  });
  ["audPenT"].forEach(id => {
    $(id).addEventListener("change", () => {
      $("audPenR").style.display = $v(id) !== "none" ? "" : "none";
      calcAudit();
    });
  });

  // ═══════════════════════════════════════════════════════════════════════════
  //  Panel toggle, drag, tabs
  // ═══════════════════════════════════════════════════════════════════════════

  $("hpcBtn").addEventListener("click", () => $("hpcPanel").classList.toggle("open"));
  $("btnX").addEventListener("click", () => $("hpcPanel").classList.remove("open"));
  $("btnCfg").addEventListener("click", () => $("secCfg").classList.toggle("open"));

  // Drag
  {
    const hdr = $("hpcHdr"), panel = $("hpcPanel");
    let dx, dy, dragging = false;
    hdr.addEventListener("mousedown", (e) => {
      if (e.target.tagName === "BUTTON") return;
      dragging = true;
      const r = panel.getBoundingClientRect();
      dx = e.clientX - r.left; dy = e.clientY - r.top;
      e.preventDefault();
    });
    document.addEventListener("mousemove", (e) => {
      if (!dragging) return;
      panel.style.left = (e.clientX - dx) + "px";
      panel.style.top = (e.clientY - dy) + "px";
      panel.style.right = "auto"; panel.style.bottom = "auto";
    });
    document.addEventListener("mouseup", () => dragging = false);
  }

  // Tabs
  let activeTab = "ext";
  const PANE_MAP = { ext:"paneExt", ref:"paneRef", plan:"panePlan", audit:"paneAudit", batch:"paneBatch", fx:"paneFx" };
  shadow.querySelectorAll(".hpc-tab").forEach(tab => {
    tab.addEventListener("click", () => {
      shadow.querySelectorAll(".hpc-tab").forEach(t => t.classList.remove("active"));
      shadow.querySelectorAll(".hpc-pane").forEach(p => p.classList.remove("active"));
      tab.classList.add("active");
      activeTab = tab.dataset.tab;
      $(PANE_MAP[activeTab]).classList.add("active");
    });
  });

  // Advanced toggles
  shadow.querySelectorAll(".hpc-adv-btn").forEach(btn => {
    btn.addEventListener("click", () => {
      const tgt = $(btn.dataset.for);
      const open = tgt.classList.toggle("open");
      btn.innerHTML = (open ? "&#9652; " : "&#9662; ") + "Advanced";
    });
  });

  // Copy buttons
  shadow.querySelectorAll("[data-copy]").forEach(btn => {
    btn.addEventListener("click", () => {
      const txt = $(btn.dataset.copy).textContent;
      navigator.clipboard.writeText(txt).then(() => {
        const orig = btn.textContent;
        btn.textContent = "Copied!";
        setTimeout(() => btn.textContent = orig, 1200);
      });
    });
  });

  // Escape
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape") {
      if ($("noteOvr").classList.contains("open")) $("noteOvr").classList.remove("open");
      else $("hpcPanel").classList.remove("open");
    }
  });

  // ═══════════════════════════════════════════════════════════════════════════
  //  EXTENSION calculation
  // ═══════════════════════════════════════════════════════════════════════════

  function calcExt() {
    readCfg();
    const out = $("extR");
    const price = $f("extPrice"), endDate = $d("extEnd"), target = $d("extTarget");
    if (!price || !endDate || !target) { out.innerHTML = '<span class="d">Fill in the fields above.</span>'; state.lastCalc = null; return; }

    const daysAdded = diffDays(endDate, target);
    if (daysAdded <= 0) { out.innerHTML = '<span class="w">Target must be after billing end.</span>'; state.lastCalc = null; return; }

    const cycle = $v("extCycle");
    const natEnd = cycleEnd(endDate, cycle);
    const natDays = diffDays(endDate, natEnd);
    $("extDivA").textContent = `(cycle: ${natDays}d)`;
    const divisor = $f("extDiv") || natDays;
    const credit = $f("extCred");

    const gross = (daysAdded / divisor) * price;
    const toCharge = Math.max(0, gross - credit);
    const fee = toCharge > 0 ? calcFee(toCharge, cfg) : 0;
    const hostNet = toCharge - fee;
    const daily = price / divisor;

    let s = "";
    const SEP = (c, n) => `<span class="s">${c.repeat(n)}</span>\n`;
    s += SEP("=", 42);
    s += `  Extension: <span class="hl">${daysAdded} days</span>\n`;
    s += `  ${fmtShort(endDate)} --> ${fmtShort(target)}\n`;
    s += SEP("-", 42);
    s += `  Cycle price: ${C(price)} / ${divisor} days\n`;
    s += `  Daily rate:  ${cfg.cur}${daily.toFixed(4)}/day\n`;
    s += SEP("-", 42);
    s += `  <span class="d">(${daysAdded} / ${divisor}) x ${C(price)}</span>\n`;
    s += `  Gross cost:  <span class="hl">${C(gross)}</span>\n`;

    if (credit > 0) {
      s += `  Credit:     -${C(credit)}\n`;
      if (toCharge <= 0) {
        s += `  <span class="ok">Fully covered by credit.</span>\n`;
        const leftover = credit - gross;
        if (leftover > 0) s += `  Remaining:   ${C(leftover)}\n`;
        out.innerHTML = s;
        state.lastCalc = { type:"ext", daysAdded, divisor, price, gross, credit, toCharge:0, fee:0, hostNet:0, endDate, target, cycle };
        return;
      }
      s += `  To charge:   ${C(toCharge)}\n`;
    }

    if (cfg.feePct > 0 || cfg.feeFlat > 0) {
      const fd = feeDesc();
      s += `  Proc. fee:   ${C(fee)} <span class="d">(${fd})</span>\n`;
      s += `  Host gets:   <span class="ok">${C(hostNet)}</span>\n`;
    }
    s += `  Customer pays: <span class="hl">${C(toCharge)}</span>\n`;
    out.innerHTML = s;

    state.lastCalc = { type:"ext", daysAdded, divisor, price, gross, credit, toCharge, fee, hostNet, endDate, target, cycle };
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  REFUND calculation
  // ═══════════════════════════════════════════════════════════════════════════

  function calcRef() {
    readCfg();
    const out = $("refR");
    const start = $d("refStart"), end = $d("refEnd"), payment = $f("refPay"), credit = $f("refCred"), cutoff = $d("refCut");
    if (!start || !end || !cutoff || (!payment && !credit)) { out.innerHTML = '<span class="d">Fill in the fields above.</span>'; state.lastCalc = null; return; }

    const totalDays = diffDays(start, end);
    if (totalDays <= 0) { out.innerHTML = '<span class="w">End must be after start.</span>'; state.lastCalc = null; return; }

    $("refDivA").textContent = `(period: ${totalDays}d)`;
    const divisor = $f("refDiv") || totalDays;
    const countCut = $ck("refCutInc");
    const remFrom = countCut ? cutoff : addDays(cutoff, 1);
    const remaining = diffDays(remFrom, end);
    if (remaining <= 0) { out.innerHTML = '<span class="d">No remaining days — no refund due.</span>'; state.lastCalc = null; return; }

    const feeOvr = $("refFee").value ? $f("refFee") : calcFee(payment, cfg);
    $("refFeeA").textContent = `(auto: ${C(calcFee(payment, cfg))})`;

    const totalGross = credit + payment;
    const totalNet = credit + (payment - feeOvr);
    const refGross = (remaining / divisor) * totalGross;
    const refNet = (remaining / divisor) * totalNet;
    const pct = ((remaining / divisor) * 100).toFixed(2);

    const penT = $v("refPenT"), penV = $f("refPenV");
    const penGross = calcPenalty(penT === "pct_total" ? totalGross : refGross, penT, penV);
    const penNet = calcPenalty(penT === "pct_total" ? totalNet : refNet, penT, penV);
    const finalGross = Math.max(0, refGross - penGross);
    const finalNet = Math.max(0, refNet - penNet);

    const rfFee = $ck("refRfFee");
    const s = renderRefundResult({ start, end, divisor, payment, credit, totalGross, totalNet, feeOvr, cutoff, countCut, remFrom, remaining, pct, refGross, refNet, penT, penV, penGross, penNet, finalGross, finalNet, rfFee });
    out.innerHTML = s;

    state.lastCalc = { type:"ref", start, end, divisor, payment, credit, totalGross, totalNet, cutoff, remaining, remFrom, refGross, refNet, penT, penGross, penNet, finalGross, finalNet, pct };
  }

  function renderRefundResult(d) {
    let s = "";
    const SEP = (c, n) => `<span class="s">${c.repeat(n)}</span>\n`;
    s += SEP("=", 42);
    s += `  Period: ${fmtShort(d.start)} --> ${fmtShort(d.end)}\n`;
    s += `  Divisor: ${d.divisor} days\n`;
    s += SEP("-", 42);

    if (d.credit > 0) {
      s += `  Payment:    ${C(d.payment)}\n`;
      s += `  Credit:     ${C(d.credit)}\n`;
      s += `  Total paid: ${C(d.totalGross)} <span class="d">(gross)</span>\n`;
      s += `  Proc. fee:  ${C(d.feeOvr)} <span class="d">(on payment only)</span>\n`;
      s += `  Host net:   ${C(d.totalNet)}\n`;
    } else {
      s += `  Total paid: ${C(d.totalGross)} <span class="d">(gross)</span>\n`;
      if (d.feeOvr > 0) { s += `  Proc. fee:  ${C(d.feeOvr)}\n`; s += `  Host net:   ${C(d.totalNet)}\n`; }
    }

    s += SEP("-", 42);
    s += `  Cutoff:     ${fmtShort(d.cutoff)}`;
    s += d.countCut ? ` <span class="d">(counted as unused)</span>\n` : ` <span class="d">(last used day)</span>\n`;
    s += `  Remaining:  <span class="hl">${d.remaining} days</span> <span class="d">(${fmtShort(d.remFrom)} --> ${fmtShort(d.end)})</span>\n`;
    s += `  Unused:     ${d.pct}%\n`;

    s += SEP("=", 42);
    s += `  <span class="d">(${d.remaining}/${d.divisor}) x ${C(d.totalGross)}</span>\n`;
    s += `  Refund (gross): <span class="hl">${C(d.refGross)}</span>\n`;

    if (d.totalGross !== d.totalNet) {
      s += `\n  <span class="d">(${d.remaining}/${d.divisor}) x ${C(d.totalNet)}</span>\n`;
      s += `  Refund (net):   <span class="ok">${C(d.refNet)}</span>\n`;
    }

    if (d.penT !== "none" && (d.penGross > 0 || d.penNet > 0)) {
      s += SEP("-", 42);
      const pLabel = d.penT === "flat"
        ? `flat ${C(d.penV)}`
        : `${d.penV}% of ${d.penT === "pct_total" ? "total paid" : "refund"}`;
      s += `  Penalty:    -${C(d.penGross)} <span class="d">(${pLabel})</span>\n`;
      s += `  After penalty (gross): <span class="hl">${C(d.finalGross)}</span>\n`;
      if (d.totalGross !== d.totalNet) {
        s += `  After penalty (net):   <span class="ok">${C(d.finalNet)}</span>\n`;
      }
    }

    if (d.rfFee) {
      const rfg = calcFee(d.finalGross, cfg), rfn = calcFee(d.finalNet, cfg);
      s += `\n` + SEP("-", 42);
      s += `  <span class="d">Refund transaction fee:</span>\n`;
      s += `    On gross: ${C(rfg)}`;
      if (d.totalGross !== d.totalNet) s += ` | On net: ${C(rfn)}`;
      s += `\n`;
      s += `  <span class="d">Host subsidizes (customer gets full):</span>\n`;
      s += `    Gross: gets ${C(d.finalGross)}, host pays ${C(d.finalGross + rfg)}\n`;
      if (d.totalGross !== d.totalNet)
        s += `    Net:   gets ${C(d.finalNet)}, host pays ${C(d.finalNet + rfn)}\n`;
      s += `  <span class="d">Customer absorbs fee:</span>\n`;
      s += `    Gross: gets ${C(d.finalGross - rfg)}, host pays ${C(d.finalGross)}\n`;
      if (d.totalGross !== d.totalNet)
        s += `    Net:   gets ${C(d.finalNet - rfn)}, host pays ${C(d.finalNet)}\n`;
    }

    return s;
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  PLAN CHANGE calculation
  // ═══════════════════════════════════════════════════════════════════════════

  function calcPlan() {
    readCfg();
    const out = $("plR");
    const oldP = $f("plOldP"), oldS = $d("plOldS"), oldE = $d("plOldE"), chgDate = $d("plChg");
    const newP = $f("plNewP"), newCycle = $v("plNewC");
    const mode = shadow.querySelector('input[name="plMode"]:checked');
    if (!oldP || !oldS || !oldE || !chgDate || !newP || !mode) { out.innerHTML = '<span class="d">Fill in the fields above.</span>'; state.lastCalc = null; return; }

    const pm = mode.value;
    const oldTotal = diffDays(oldS, oldE);
    if (oldTotal <= 0) { out.innerHTML = '<span class="w">Old plan end must be after start.</span>'; state.lastCalc = null; return; }

    const oldRemaining = diffDays(chgDate, oldE);
    if (oldRemaining <= 0) { out.innerHTML = '<span class="w">Change date must be before period end.</span>'; state.lastCalc = null; return; }

    $("plOldDivA").textContent = `(period: ${oldTotal}d)`;
    const oldDiv = $f("plOldDiv") || oldTotal;
    const oldCredit = (oldRemaining / oldDiv) * oldP;

    let newCharge, newEnd, newDiv, newDays;
    if (pm === "continue") {
      newDays = oldRemaining;
      newEnd = oldE;
      const natNewCycle = diffDays(chgDate, cycleEnd(chgDate, newCycle));
      $("plNewDivA").textContent = `(cycle: ${natNewCycle}d)`;
      newDiv = $f("plNewDiv") || natNewCycle;
      newCharge = (newDays / newDiv) * newP;
    } else {
      newEnd = cycleEnd(chgDate, newCycle);
      newDays = diffDays(chgDate, newEnd);
      $("plNewDivA").textContent = `(full cycle: ${newDays}d)`;
      newDiv = $f("plNewDiv") || newDays;
      newCharge = newP;
    }

    const net = newCharge - oldCredit;
    const fee = net > 0 ? calcFee(net, cfg) : 0;
    const hostNet = net > 0 ? net - fee : net;

    let s = "";
    const SEP = (c, n) => `<span class="s">${c.repeat(n)}</span>\n`;
    s += SEP("=", 42);
    s += `  Plan change on ${fmtShort(chgDate)}\n`;
    s += SEP("-", 42);
    s += `  <span class="d">Old plan: ${C(oldP)} / ${$v("plOldC")}</span>\n`;
    s += `  <span class="d">Period: ${fmtShort(oldS)} --> ${fmtShort(oldE)} (${oldDiv}d)</span>\n`;
    s += `  Remaining: ${oldRemaining} days\n`;
    s += `  <span class="d">(${oldRemaining}/${oldDiv}) x ${C(oldP)}</span>\n`;
    s += `  Credit:     <span class="ok">${C(oldCredit)}</span>\n`;
    s += SEP("-", 42);
    s += `  <span class="d">New plan: ${C(newP)} / ${newCycle}</span>\n`;
    if (pm === "continue") {
      s += `  <span class="d">Continues to ${fmtShort(newEnd)} (${newDays}d remaining)</span>\n`;
      s += `  <span class="d">(${newDays}/${newDiv}) x ${C(newP)}</span>\n`;
    } else {
      s += `  <span class="d">New cycle: ${fmtShort(chgDate)} --> ${fmtShort(newEnd)} (${newDays}d)</span>\n`;
      s += `  <span class="d">Full cycle price</span>\n`;
    }
    s += `  Charge:     <span class="hl">${C(newCharge)}</span>\n`;
    s += SEP("=", 42);
    s += `  Charge:   ${C(newCharge)}\n`;
    s += `  Credit:  -${C(oldCredit)}\n`;
    s += SEP("-", 42);
    if (net >= 0) {
      s += `  Net due:    <span class="hl">${C(net)}</span>\n`;
      if (fee > 0) {
        s += `  Proc. fee:  ${C(fee)} <span class="d">(${feeDesc()})</span>\n`;
        s += `  Host gets:  <span class="ok">${C(hostNet)}</span>\n`;
      }
    } else {
      s += `  Refund owed: <span class="ok">${C(Math.abs(net))}</span>\n`;
    }

    out.innerHTML = s;
    state.lastCalc = { type:"plan", oldP, oldS, oldE, chgDate, newP, newCycle, pm, oldDiv, oldCredit, oldRemaining, newCharge, newEnd, newDays, newDiv, net, fee, hostNet };
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  AUDIT calculation
  // ═══════════════════════════════════════════════════════════════════════════

  function calcAudit() {
    readCfg();
    const out = $("audR");
    const start = $d("audStart"), end = $d("audEnd"), payment = $f("audPay"), credit = $f("audCred"), cutoff = $d("audCut"), actual = $f("audActual");
    if (!start || !end || !cutoff || (!payment && !credit)) { out.innerHTML = '<span class="d">Fill in the fields above.</span>'; state.lastCalc = null; return; }

    const totalDays = diffDays(start, end);
    if (totalDays <= 0) { out.innerHTML = '<span class="w">End must be after start.</span>'; state.lastCalc = null; return; }

    $("audDivA").textContent = `(period: ${totalDays}d)`;
    const divisor = $f("audDiv") || totalDays;
    const countCut = $ck("audCutInc");
    const remFrom = countCut ? cutoff : addDays(cutoff, 1);
    const remaining = diffDays(remFrom, end);
    if (remaining <= 0) { out.innerHTML = '<span class="d">No remaining days.</span>'; state.lastCalc = null; return; }

    const feeOvr = $("audFee").value ? $f("audFee") : calcFee(payment, cfg);
    $("audFeeA").textContent = `(auto: ${C(calcFee(payment, cfg))})`;

    const totalGross = credit + payment;
    const totalNet = credit + (payment - feeOvr);
    const refGross = (remaining / divisor) * totalGross;
    const refNet = (remaining / divisor) * totalNet;
    const pct = ((remaining / divisor) * 100).toFixed(2);

    const penT = $v("audPenT"), penV = $f("audPenV");
    const penGross = calcPenalty(penT === "pct_total" ? totalGross : refGross, penT, penV);
    const finalGross = Math.max(0, refGross - penGross);
    const finalNet = Math.max(0, refNet - calcPenalty(penT === "pct_total" ? totalNet : refNet, penT, penV));

    let s = "";
    const SEP = (c, n) => `<span class="s">${c.repeat(n)}</span>\n`;
    s += SEP("=", 42);
    s += `  DISCREPANCY AUDIT\n`;
    s += SEP("=", 42);
    s += `  Period: ${fmtShort(start)} --> ${fmtShort(end)}\n`;
    s += `  Divisor: ${divisor}d | Remaining: ${remaining}d (${pct}%)\n`;
    s += SEP("-", 42);

    if (credit > 0) {
      s += `  Payment: ${C(payment)} + Credit: ${C(credit)}\n`;
      s += `  Total gross: ${C(totalGross)}\n`;
    } else {
      s += `  Total paid: ${C(totalGross)}\n`;
    }
    if (feeOvr > 0) s += `  Host net: ${C(totalNet)}\n`;

    s += SEP("-", 42);
    s += `  Correct (gross): <span class="hl">${C(refGross)}</span>\n`;
    if (totalGross !== totalNet) s += `  Correct (net):   <span class="ok">${C(refNet)}</span>\n`;
    if (penT !== "none" && penGross > 0) {
      s += `  Penalty:        -${C(penGross)}\n`;
      s += `  After penalty:   <span class="hl">${C(finalGross)}</span>\n`;
    }

    if (actual > 0) {
      s += SEP("=", 42);
      s += `  Actual received: ${C(actual)}\n`;
      const diffG = finalGross - actual;
      const diffN = finalNet - actual;
      if (Math.abs(diffG) < 0.005) {
        s += `  <span class="ok">Matches gross calculation.</span>\n`;
      } else if (Math.abs(diffN) < 0.005) {
        s += `  <span class="ok">Matches net calculation.</span>\n`;
      } else if (diffG > 0) {
        s += `  Shortfall (gross): <span class="w">${C(diffG)}</span>\n`;
        if (totalGross !== totalNet) s += `  Shortfall (net):   <span class="w">${C(diffN)}</span>\n`;
      } else {
        s += `  Overpaid by: <span class="w">${C(Math.abs(diffG))}</span>\n`;
      }

      // Reverse-engineer what base the host likely used
      if (remaining > 0 && Math.abs(diffG) >= 0.005) {
        const likelyBase = (actual * divisor) / remaining;
        s += `\n  <span class="d">Likely error: host prorated on</span>\n`;
        s += `  <span class="d">  ${C(likelyBase)} instead of ${C(totalGross)}</span>\n`;
        if (credit > 0 && Math.abs(likelyBase - payment) < 0.02) {
          s += `  <span class="w">  ^ Matches payment amount — credit was excluded!</span>\n`;
        }
      }
    } else {
      s += `\n  <span class="d">Enter "Amount received" to compare.</span>\n`;
    }

    out.innerHTML = s;
    state.lastCalc = { type:"audit", start, end, divisor, payment, credit, totalGross, totalNet, cutoff, remaining, remFrom, refGross, refNet, actual, finalGross, finalNet, penT, penGross, pct };
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  BATCH calculation
  // ═══════════════════════════════════════════════════════════════════════════

  let batIdx = 0;

  function addBatchCard() {
    batIdx++;
    const card = document.createElement("div");
    card.className = "bc";
    card.dataset.idx = batIdx;
    card.innerHTML = `
      <div class="bc-hdr"><input type="text" class="bc-lbl" value="Service ${batIdx}" placeholder="Label"><button class="bc-rm" title="Remove">&times;</button></div>
      <div class="bc-row"><label>Start</label><input type="date" class="bc-s"><label>End</label><input type="date" class="bc-e"></div>
      <div class="bc-row"><label>Paid</label><input type="number" step="0.01" class="bc-pay" placeholder="0.00"><label>Cr.</label><input type="number" step="0.01" class="bc-cr" value="0"><label>Div</label><input type="number" class="bc-div" placeholder="auto" style="width:55px"></div>
      <div class="bc-res"></div>`;
    $("batList").appendChild(card);
    card.querySelector(".bc-rm").addEventListener("click", () => { card.remove(); calcBatch(); });
    card.querySelectorAll("input").forEach(inp => { inp.addEventListener("input", calcBatch); inp.addEventListener("change", calcBatch); });
    calcBatch();
  }

  $("batAdd").addEventListener("click", addBatchCard);

  function calcBatch() {
    readCfg();
    const out = $("batR");
    const cutoff = $d("batCut");
    const countCut = $ck("batCutInc");
    const cards = $("batList").querySelectorAll(".bc");

    if (!cutoff || cards.length === 0) {
      out.innerHTML = '<span class="d">Add services and set cutoff date.</span>';
      state.lastCalc = null;
      return;
    }

    let totGross = 0, totNet = 0;
    const items = [];
    const SEP = (c, n) => `<span class="s">${c.repeat(n)}</span>\n`;

    cards.forEach(card => {
      const label = card.querySelector(".bc-lbl").value || "?";
      const s = toDate(card.querySelector(".bc-s").value);
      const e = toDate(card.querySelector(".bc-e").value);
      const pay = parseFloat(card.querySelector(".bc-pay").value) || 0;
      const cr = parseFloat(card.querySelector(".bc-cr").value) || 0;
      const divOvr = parseFloat(card.querySelector(".bc-div").value) || 0;
      const resEl = card.querySelector(".bc-res");

      if (!s || !e || (!pay && !cr)) { resEl.textContent = ""; return; }

      const totalDays = diffDays(s, e);
      if (totalDays <= 0) { resEl.textContent = "Invalid dates"; return; }

      const divisor = divOvr || totalDays;
      const remFrom = countCut ? cutoff : addDays(cutoff, 1);
      const remaining = diffDays(remFrom, e);

      if (remaining <= 0) { resEl.textContent = "No remaining days"; return; }

      const tGross = cr + pay;
      const fee = calcFee(pay, cfg);
      const tNet = cr + (pay - fee);
      const rG = (remaining / divisor) * tGross;
      const rN = (remaining / divisor) * tNet;

      resEl.textContent = `${remaining}d remaining | Gross: ${C(rG)} | Net: ${C(rN)}`;
      totGross += rG;
      totNet += rN;
      items.push({ label, start: s, end: e, remaining, divisor, totalGross: tGross, totalNet: tNet, refGross: rG, refNet: rN });
    });

    let s = SEP("=", 42);
    s += `  BATCH REFUND SUMMARY\n`;
    s += `  Cutoff: ${fmtShort(cutoff)}`;
    s += countCut ? ` (included)\n` : ` (excluded)\n`;
    s += SEP("=", 42);

    items.forEach(it => {
      s += `  <span class="hl">${it.label}</span>\n`;
      s += `    ${fmtShort(it.start)} --> ${fmtShort(it.end)}\n`;
      s += `    ${it.remaining}d remaining, paid ${C(it.totalGross)}\n`;
      s += `    Gross: ${C(it.refGross)} | Net: ${C(it.refNet)}\n`;
    });

    s += SEP("=", 42);
    s += `  Total (gross): <span class="hl">${C(totGross)}</span>\n`;
    if (Math.abs(totGross - totNet) > 0.005) {
      s += `  Total (net):   <span class="ok">${C(totNet)}</span>\n`;
    }

    out.innerHTML = s;
    state.lastCalc = { type:"batch", cutoff, countCut, items, totGross, totNet };
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  FX (Currency conversion)
  // ═══════════════════════════════════════════════════════════════════════════

  let fxLocked = null;

  function calcFx() {
    const amt = $f("fxAmt"), rate = $f("fxRate");
    const from = $v("fxFrom").toUpperCase() || "???";
    const to = $v("fxTo").toUpperCase() || "???";
    const out = $("fxR");

    if (!amt || !rate) { out.innerHTML = '<span class="d">Enter amount and rate.</span>'; return; }

    const converted = amt * rate;
    let s = "";
    s += `  ${amt.toFixed(2)} ${from}\n`;
    s += `  x ${rate}\n`;
    s += `  <span class="s">${"-".repeat(30)}</span>\n`;
    s += `  <span class="hl">${converted.toFixed(2)} ${to}</span>\n`;

    if (fxLocked) {
      s += `\n  <span class="d">Rate locked: ${fxLocked.rate} @ ${fxLocked.time}</span>\n`;
    }

    out.innerHTML = s;
  }

  $("fxLock").addEventListener("click", () => {
    const rate = $f("fxRate");
    if (!rate) return;
    const now = new Date();
    fxLocked = {
      rate: rate,
      time: now.toLocaleString("en-US", { month:"short", day:"numeric", year:"numeric", hour:"2-digit", minute:"2-digit" }),
    };
    $("fxLockInfo").innerHTML = `<span class="fx-lock">Locked @ ${fxLocked.time}</span>`;
    calcFx();
  });

  // ═══════════════════════════════════════════════════════════════════════════
  //  Invoice / Credit Note / Document generator
  // ═══════════════════════════════════════════════════════════════════════════

  function feeDesc() {
    if (cfg.feePct > 0 && cfg.feeFlat > 0) return `${cfg.feePct}% + ${C(cfg.feeFlat)}`;
    if (cfg.feePct > 0) return `${cfg.feePct}%`;
    if (cfg.feeFlat > 0) return C(cfg.feeFlat);
    return "none";
  }

  const NOTE_TITLES = {
    ext: "INVOICE", ref: "CREDIT NOTE", plan: "ADJUSTMENT NOTE",
    audit: "DISCREPANCY REPORT", batch: "CREDIT NOTE (BATCH)",
  };

  function generateNote(type) {
    const lc = state.lastCalc;
    if (!lc) return "No calculation to generate from.";

    const today = new Date().toLocaleDateString("en-US", { month:"long", day:"numeric", year:"numeric" });
    const HR = "=".repeat(46);
    const hr = "-".repeat(46);
    let t = "";
    t += `${HR}\n`;
    t += `  ${NOTE_TITLES[type] || "DOCUMENT"}\n`;
    t += `${HR}\n`;
    t += `  Date:     ${today}\n`;
    t += `  Ref:      [EDIT]\n`;
    t += `  Customer: [EDIT]\n`;
    t += `${hr}\n\n`;

    if (type === "ext" && lc.type === "ext") {
      t += `  Service Extension\n`;
      t += `  From:  ${fmtShort(lc.endDate)}\n`;
      t += `  To:    ${fmtShort(lc.target)} (${lc.daysAdded} days)\n`;
      t += `  Rate:  ${C(lc.price)} / ${lc.divisor} days\n\n`;
      t += `  Gross cost:       ${C(lc.gross)}\n`;
      if (lc.credit > 0) t += `  Credit applied:  -${C(lc.credit)}\n`;
      t += `  Amount due:       ${C(lc.toCharge)}\n`;
      if (lc.fee > 0) t += `  Processor fee:    ${C(lc.fee)} (${feeDesc()})\n`;
      t += `  ${hr}\n`;
      t += `  TOTAL DUE:        ${C(lc.toCharge)}\n`;
    }

    else if (type === "ref" && lc.type === "ref") {
      t += `  Service: [EDIT]\n`;
      t += `  Period:  ${fmtShort(lc.start)} -> ${fmtShort(lc.end)}\n`;
      t += `  Cutoff:  ${fmtShort(lc.cutoff)} (${lc.remaining} days remaining)\n\n`;
      t += `  Total paid:       ${C(lc.totalGross)}\n`;
      t += `  Prorated refund:  (${lc.remaining}/${lc.divisor}) x ${C(lc.totalGross)}\n\n`;
      t += `  Refund (gross):   ${C(lc.refGross)}\n`;
      if (lc.totalGross !== lc.totalNet) t += `  Refund (net):     ${C(lc.refNet)}\n`;
      if (lc.penT !== "none" && lc.penGross > 0) {
        t += `  Penalty:         -${C(lc.penGross)}\n`;
        t += `  After penalty:    ${C(lc.finalGross)}\n`;
      }
      t += `  ${hr}\n`;
      t += `  TOTAL REFUND:     ${C(lc.finalGross)}\n`;
    }

    else if (type === "plan" && lc.type === "plan") {
      t += `  Plan Change on ${fmtShort(lc.chgDate)}\n\n`;
      t += `  Old plan: ${C(lc.oldP)} / ${$v("plOldC")}\n`;
      t += `  Period:   ${fmtShort(lc.oldS)} -> ${fmtShort(lc.oldE)}\n`;
      t += `  Remaining: ${lc.oldRemaining} days\n`;
      t += `  Credit:    ${C(lc.oldCredit)}\n\n`;
      t += `  New plan: ${C(lc.newP)} / ${lc.newCycle}\n`;
      if (lc.pm === "continue") {
        t += `  Continues to: ${fmtShort(lc.newEnd)}\n`;
      } else {
        t += `  New cycle: ${fmtShort(lc.chgDate)} -> ${fmtShort(lc.newEnd)}\n`;
      }
      t += `  Charge:    ${C(lc.newCharge)}\n\n`;
      t += `  ${hr}\n`;
      t += `  Charge:   ${C(lc.newCharge)}\n`;
      t += `  Credit:  -${C(lc.oldCredit)}\n`;
      t += `  ${hr}\n`;
      if (lc.net >= 0) {
        t += `  NET DUE:  ${C(lc.net)}\n`;
      } else {
        t += `  REFUND:   ${C(Math.abs(lc.net))}\n`;
      }
    }

    else if (type === "audit" && lc.type === "audit") {
      t += `  Service: [EDIT]\n`;
      t += `  Period:  ${fmtShort(lc.start)} -> ${fmtShort(lc.end)}\n`;
      t += `  Cutoff:  ${fmtShort(lc.cutoff)} (${lc.remaining}d remaining)\n\n`;
      t += `  Total paid (gross): ${C(lc.totalGross)}\n`;
      if (lc.totalGross !== lc.totalNet) t += `  Total host net:     ${C(lc.totalNet)}\n`;
      t += `\n  Correct refund (gross): ${C(lc.refGross)}\n`;
      if (lc.totalGross !== lc.totalNet) t += `  Correct refund (net):   ${C(lc.refNet)}\n`;
      if (lc.actual > 0) {
        t += `\n  Actual received:    ${C(lc.actual)}\n`;
        const diff = lc.finalGross - lc.actual;
        if (Math.abs(diff) >= 0.005) {
          t += `  Shortfall (gross):  ${C(diff)}\n`;
        } else {
          t += `  (Matches gross calculation)\n`;
        }
      }
      t += `  ${hr}\n`;
      if (lc.actual > 0 && lc.finalGross - lc.actual >= 0.005) {
        t += `  AMOUNT OWED:  ${C(lc.finalGross - lc.actual)}\n`;
      }
    }

    else if (type === "batch" && lc.type === "batch") {
      t += `  Service retirement: ${fmtShort(lc.cutoff)}\n\n`;
      let i = 0;
      lc.items.forEach(it => {
        i++;
        t += `  ${i}. ${it.label}\n`;
        t += `     Period: ${fmtShort(it.start)} -> ${fmtShort(it.end)}\n`;
        t += `     Remaining: ${it.remaining}d | Paid: ${C(it.totalGross)}\n`;
        t += `     Refund (gross): ${C(it.refGross)}\n`;
        if (Math.abs(it.refGross - it.refNet) > 0.005) t += `     Refund (net):   ${C(it.refNet)}\n`;
        t += `\n`;
      });
      t += `  ${hr}\n`;
      t += `  TOTAL REFUND (gross): ${C(lc.totGross)}\n`;
      if (Math.abs(lc.totGross - lc.totNet) > 0.005) {
        t += `  TOTAL REFUND (net):   ${C(lc.totNet)}\n`;
      }
    }

    t += `\n  Notes: [EDIT]\n`;
    t += `${HR}\n`;
    return t;
  }

  // Note buttons
  shadow.querySelectorAll("[data-note]").forEach(btn => {
    btn.addEventListener("click", () => {
      const type = btn.dataset.note;
      if (!state.lastCalc) { alert("Run a calculation first."); return; }
      const txt = generateNote(type);
      $("noteTitle").textContent = NOTE_TITLES[type] || "Document";
      $("noteTxt").value = txt;
      $("noteOvr").classList.add("open");
    });
  });

  $("noteClose").addEventListener("click", () => $("noteOvr").classList.remove("open"));
  $("noteCopy").addEventListener("click", () => {
    navigator.clipboard.writeText($("noteTxt").value).then(() => {
      $("noteCopy").textContent = "Copied!";
      setTimeout(() => $("noteCopy").textContent = "Copy to Clipboard", 1200);
    });
  });
  $("noteOvr").addEventListener("click", (e) => {
    if (e.target === $("noteOvr")) $("noteOvr").classList.remove("open");
  });

  // ═══════════════════════════════════════════════════════════════════════════
  //  Event wiring — recalculate on input changes
  // ═══════════════════════════════════════════════════════════════════════════

  function recalcActive() {
    switch (activeTab) {
      case "ext":   calcExt();   break;
      case "ref":   calcRef();   break;
      case "plan":  calcPlan();  break;
      case "audit": calcAudit(); break;
      case "batch": calcBatch(); break;
      case "fx":    calcFx();    break;
    }
  }

  const cfgIds = ["cfgPct", "cfgFlat", "cfgCur", "cfgPenVal"];
  cfgIds.forEach(id => {
    $(id).addEventListener("input", recalcActive);
    $(id).addEventListener("change", recalcActive);
  });

  function wirePane(ids, fn) {
    ids.forEach(id => {
      const el = $(id);
      if (!el) return;
      el.addEventListener("input", fn);
      el.addEventListener("change", fn);
    });
  }

  wirePane(["extPrice","extCycle","extEnd","extTarget","extDiv","extCred"], calcExt);
  wirePane(["refStart","refEnd","refPay","refCred","refCut","refCutInc","refDiv","refFee","refRfFee","refPenT","refPenV"], calcRef);
  wirePane(["plOldP","plOldC","plOldS","plOldE","plChg","plNewP","plNewC","plOldDiv","plNewDiv"], calcPlan);
  wirePane(["audStart","audEnd","audPay","audCred","audCut","audCutInc","audDiv","audFee","audActual","audPenT","audPenV"], calcAudit);
  wirePane(["batCut","batCutInc"], calcBatch);
  wirePane(["fxAmt","fxRate","fxFrom","fxTo"], calcFx);

  // Plan mode radios
  shadow.querySelectorAll('input[name="plMode"]').forEach(r => r.addEventListener("change", calcPlan));

  // ═══════════════════════════════════════════════════════════════════════════
  //  Case context
  // ═══════════════════════════════════════════════════════════════════════════

  const ctx = { ticketId: "", customerName: "" };
  $("ctxTicket").addEventListener("input", () => { ctx.ticketId = $v("ctxTicket"); });
  $("ctxCustomer").addEventListener("input", () => { ctx.customerName = $v("ctxCustomer"); });

  // Patch generateNote to use ctx values
  const _origGenNote = generateNote;
  // (ctx is already in scope when generateNote runs — the function reads ctx directly)

  // ═══════════════════════════════════════════════════════════════════════════
  //  Templates
  // ═══════════════════════════════════════════════════════════════════════════

  if (!cfg.templates) cfg.templates = [];

  function refreshTemplateSelectors() {
    const selIds = ["extTmplSel","refTmplSel","plOldTmplSel","plNewTmplSel","audTmplSel"];
    selIds.forEach(id => {
      const sel = $(id);
      if (!sel) return;
      const prev = sel.value;
      sel.innerHTML = '<option value="">— load template —</option>';
      cfg.templates.slice().sort((a,b) => a.name.localeCompare(b.name)).forEach(t => {
        const opt = document.createElement("option");
        opt.value = t.id;
        opt.textContent = `${t.name} (${C(t.price)}/${t.cycle})`;
        sel.appendChild(opt);
      });
      if (prev) sel.value = prev;
    });
    renderTmplList();
  }

  function renderTmplList() {
    const list = $("tmplList");
    if (!list) return;
    if (cfg.templates.length === 0) {
      list.innerHTML = '<div class="hpc-hist-empty">No templates yet.</div>';
      return;
    }
    list.innerHTML = "";
    cfg.templates.slice().sort((a,b) => a.name.localeCompare(b.name)).forEach(t => {
      const item = document.createElement("div");
      item.className = "hpc-tmpl-item";
      item.innerHTML = `
        <span title="Click to edit">${t.name}</span>
        <span class="td">${C(t.price)}/${t.cycle}${t.divisor ? " div:"+t.divisor : ""}</span>
        <button title="Delete">&times;</button>`;
      item.querySelector("span").addEventListener("click", () => openTmplModal(t));
      item.querySelector("button").addEventListener("click", () => {
        cfg.templates = cfg.templates.filter(x => x.id !== t.id);
        saveCfg(cfg);
        refreshTemplateSelectors();
      });
      list.appendChild(item);
    });
  }

  function openTmplModal(t) {
    $("tmplOvrTitle").textContent = t ? "Edit Template" : "Add Template";
    $("tmplName").value = t ? t.name : "";
    $("tmplPrice").value = t ? t.price : "";
    $("tmplCycle").value = t ? t.cycle : "yearly";
    $("tmplDiv").value = t ? (t.divisor || "") : "";
    $("tmplEditId").value = t ? t.id : "";
    $("tmplOvr").classList.add("open");
    setTimeout(() => $("tmplName").focus(), 50);
  }

  $("tmplAddBtn").addEventListener("click", () => openTmplModal(null));
  $("tmplOvrClose").addEventListener("click", () => $("tmplOvr").classList.remove("open"));
  $("tmplOvr").addEventListener("click", e => { if (e.target === $("tmplOvr")) $("tmplOvr").classList.remove("open"); });

  $("tmplOvrSave").addEventListener("click", () => {
    const name = $v("tmplName").trim();
    const price = parseFloat($v("tmplPrice"));
    const cycle = $v("tmplCycle");
    const divisor = parseInt($v("tmplDiv")) || 0;
    if (!name || !price) { $("tmplName").focus(); return; }

    const editId = $v("tmplEditId");
    if (editId) {
      const t = cfg.templates.find(x => x.id === editId);
      if (t) { t.name = name; t.price = price; t.cycle = cycle; t.divisor = divisor; }
    } else {
      cfg.templates.push({ id: Date.now().toString(36), name, price, cycle, divisor });
    }
    saveCfg(cfg);
    refreshTemplateSelectors();
    $("tmplOvr").classList.remove("open");
  });

  // Load template into a tab's fields
  function applyTemplate(t, priceId, cycleId, divId) {
    if (!t) return;
    $(priceId).value = t.price;
    $(cycleId).value = t.cycle;
    if (divId && t.divisor) $(divId).value = t.divisor;
    else if (divId) $(divId).value = "";
  }

  function buildSaveHandler(priceId, cycleId, divId) {
    return () => {
      const price = parseFloat($v(priceId));
      const cycle = $v(cycleId);
      const divisor = parseInt($v(divId)) || 0;
      openTmplModal(null);
      $("tmplPrice").value = price || "";
      $("tmplCycle").value = cycle;
      $("tmplDiv").value = divisor || "";
    };
  }

  // Wire template selectors
  const tmplSelMap = [
    { sel:"extTmplSel",    p:"extPrice",  c:"extCycle",  d:"extDiv",    fn: calcExt  },
    { sel:"refTmplSel",    p:"refPay",    c:null,        d:"refDiv",    fn: calcRef  },
    { sel:"plOldTmplSel",  p:"plOldP",    c:"plOldC",    d:"plOldDiv",  fn: calcPlan },
    { sel:"plNewTmplSel",  p:"plNewP",    c:"plNewC",    d:"plNewDiv",  fn: calcPlan },
    { sel:"audTmplSel",    p:"audPay",    c:null,        d:"audDiv",    fn: calcAudit},
  ];

  tmplSelMap.forEach(({ sel, p, c, d, fn }) => {
    $(sel).addEventListener("change", () => {
      const id = $v(sel);
      if (!id) return;
      const t = cfg.templates.find(x => x.id === id);
      if (t) applyTemplate(t, p, c, d);
      $(sel).value = "";
      fn();
    });
  });

  $("extTmplSave").addEventListener("click",    buildSaveHandler("extPrice","extCycle","extDiv"));
  $("refTmplSave").addEventListener("click",    buildSaveHandler("refPay","extCycle","refDiv"));
  $("plOldTmplSave").addEventListener("click",  buildSaveHandler("plOldP","plOldC","plOldDiv"));
  $("plNewTmplSave").addEventListener("click",  buildSaveHandler("plNewP","plNewC","plNewDiv"));
  $("audTmplSave").addEventListener("click",    buildSaveHandler("audPay","extCycle","audDiv"));

  refreshTemplateSelectors();

  // ═══════════════════════════════════════════════════════════════════════════
  //  History
  // ═══════════════════════════════════════════════════════════════════════════

  const HKEY = "hpc_hist";
  const HIST_MAX = 20;

  function loadHistory() {
    try { return JSON.parse(localStorage.getItem(HKEY)) || []; }
    catch { return []; }
  }
  function saveHistory(h) { localStorage.setItem(HKEY, JSON.stringify(h)); }

  function addToHistory(tab, label, inputs) {
    const h = loadHistory();
    h.unshift({
      id: Date.now(),
      ts: new Date().toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),
      tab,
      label,
      ticketId: ctx.ticketId,
      customerName: ctx.customerName,
      inputs,
    });
    saveHistory(h.slice(0, HIST_MAX));
  }

  function captureInputs(ids) {
    const map = {};
    ids.forEach(id => {
      const el = $(id);
      if (!el) return;
      map[id] = el.type === "checkbox" ? el.checked : el.value;
    });
    return map;
  }

  function restoreInputs(inputs) {
    Object.entries(inputs).forEach(([id, val]) => {
      const el = $(id);
      if (!el) return;
      if (el.type === "checkbox") el.checked = !!val;
      else if (el.type === "radio") {
        shadow.querySelectorAll(`input[name="${el.name}"]`).forEach(r => r.checked = (r.value === val));
      }
      else el.value = val;
    });
  }

  function renderHistoryModal() {
    const h = loadHistory();
    const list = $("histList");
    if (h.length === 0) {
      list.innerHTML = '<div class="hpc-hist-empty">No calculations yet.</div>';
      return;
    }
    list.innerHTML = "";
    h.forEach(entry => {
      const row = document.createElement("div");
      row.className = "hpc-hist-entry";
      const ctxLine = [entry.ticketId, entry.customerName].filter(Boolean).join(" · ");
      row.innerHTML = `
        <span class="hpc-hist-badge badge-${entry.tab}">${entry.tab}</span>
        <div class="hpc-hist-info">
          <div class="hpc-hist-ts">${entry.ts}</div>
          ${ctxLine ? `<div class="hpc-hist-ctx">${ctxLine}</div>` : ""}
          <div class="hpc-hist-lbl">${entry.label}</div>
        </div>
        <button class="hpc-hist-restore">Restore</button>`;
      row.querySelector(".hpc-hist-restore").addEventListener("click", () => {
        // Switch to the right tab
        shadow.querySelectorAll(".hpc-tab").forEach(t => t.classList.remove("active"));
        shadow.querySelectorAll(".hpc-pane").forEach(p => p.classList.remove("active"));
        const PANE_MAP2 = { ext:"paneExt",ref:"paneRef",plan:"panePlan",audit:"paneAudit",batch:"paneBatch",fx:"paneFx" };
        shadow.querySelector(`[data-tab="${entry.tab}"]`).classList.add("active");
        activeTab = entry.tab;
        $(PANE_MAP2[entry.tab]).classList.add("active");
        // Restore inputs
        restoreInputs(entry.inputs);
        // Recalc
        if (entry.inputs._batCards) restoreBatchCards(entry.inputs._batCards);
        recalcActive();
        $("histOvr").classList.remove("open");
      });
      list.appendChild(row);
    });
  }

  // Batch card restoration helper
  function restoreBatchCards(cards) {
    $("batList").innerHTML = "";
    batIdx = 0;
    cards.forEach(cd => {
      addBatchCard();
      const card = $("batList").lastElementChild;
      card.querySelector(".bc-lbl").value = cd.label;
      card.querySelector(".bc-s").value = cd.start;
      card.querySelector(".bc-e").value = cd.end;
      card.querySelector(".bc-pay").value = cd.pay;
      card.querySelector(".bc-cr").value = cd.cr;
      card.querySelector(".bc-div").value = cd.div;
    });
  }

  // History button
  $("btnHist").addEventListener("click", () => {
    renderHistoryModal();
    $("histOvr").classList.add("open");
  });
  $("histClose").addEventListener("click", () => $("histOvr").classList.remove("open"));
  $("histOvr").addEventListener("click", e => { if (e.target === $("histOvr")) $("histOvr").classList.remove("open"); });
  $("histClear").addEventListener("click", () => {
    if (confirm("Clear all history?")) { saveHistory([]); renderHistoryModal(); }
  });

  // ═══════════════════════════════════════════════════════════════════════════
  //  Auto-save hooks — patch each calc fn to save history on success
  // ═══════════════════════════════════════════════════════════════════════════

  const EXT_IDS  = ["extPrice","extCycle","extEnd","extTarget","extDiv","extCred"];
  const REF_IDS  = ["refStart","refEnd","refPay","refCred","refCut","refCutInc","refDiv","refFee","refRfFee","refPenT","refPenV"];
  const PLAN_IDS = ["plOldP","plOldC","plOldS","plOldE","plChg","plNewP","plNewC","plOldDiv","plNewDiv"];
  const AUD_IDS  = ["audStart","audEnd","audPay","audCred","audCut","audCutInc","audDiv","audFee","audActual","audPenT","audPenV"];
  const BAT_IDS  = ["batCut","batCutInc"];
  const FX_IDS   = ["fxAmt","fxFrom","fxRate","fxTo"];

  function histLabel(tab) {
    const lc = state.lastCalc;
    if (!lc) return "";
    switch (tab) {
      case "ext":   return `Extend ${lc.daysAdded}d → ${C(lc.toCharge)}`;
      case "ref":   return `Refund: ${C(lc.finalGross)} gross${lc.totalGross!==lc.totalNet?" / "+C(lc.finalNet)+" net":""}`;
      case "plan":  return `Plan change: ${lc.net>=0?"due "+C(lc.net):"refund "+C(Math.abs(lc.net))}`;
      case "audit": return `Audit: correct ${C(lc.refGross)}${lc.actual>0?", actual "+C(lc.actual):""}`;
      case "batch": return `Batch ${lc.items.length} services: ${C(lc.totGross)} gross`;
      case "fx": {
        const amt=$f("fxAmt"),rate=$f("fxRate"),from=$v("fxFrom"),to=$v("fxTo");
        return `${amt.toFixed(2)} ${from} = ${(amt*rate).toFixed(2)} ${to}`;
      }
    }
    return "";
  }

  function maybeSaveHistory(tab) {
    if (!state.lastCalc) return;
    let ids, extra = {};
    switch (tab) {
      case "ext":   ids = EXT_IDS; break;
      case "ref":   ids = REF_IDS; break;
      case "plan":  ids = PLAN_IDS; extra._plMode = shadow.querySelector('input[name="plMode"]:checked')?.value; break;
      case "audit": ids = AUD_IDS; break;
      case "batch":
        ids = BAT_IDS;
        extra._batCards = [...$("batList").querySelectorAll(".bc")].map(card => ({
          label: card.querySelector(".bc-lbl").value,
          start: card.querySelector(".bc-s").value,
          end:   card.querySelector(".bc-e").value,
          pay:   card.querySelector(".bc-pay").value,
          cr:    card.querySelector(".bc-cr").value,
          div:   card.querySelector(".bc-div").value,
        }));
        break;
      case "fx": ids = FX_IDS; break;
      default: return;
    }
    addToHistory(tab, histLabel(tab), { ...captureInputs(ids), ...extra });
  }

  // Debounced history save (only save after 2s of inactivity, not on every keystroke)
  const histTimers = {};
  function scheduleHistSave(tab) {
    clearTimeout(histTimers[tab]);
    histTimers[tab] = setTimeout(() => maybeSaveHistory(tab), 2000);
  }

  // Patch the calc functions to also trigger history save
  const _calcExt = calcExt, _calcRef = calcRef, _calcPlan = calcPlan,
        _calcAudit = calcAudit, _calcBatch = calcBatch, _calcFx = calcFx;

  // Override by wrapping — use the existing references already bound to events
  // Instead, hook into recalcActive which is called after every input
  const _recalcActive = recalcActive;
  // Re-assign the tab-specific wires to also schedule history
  ["extPrice","extCycle","extEnd","extTarget","extDiv","extCred"].forEach(id => {
    const el = $(id); if (!el) return;
    el.addEventListener("input",  () => scheduleHistSave("ext"));
    el.addEventListener("change", () => scheduleHistSave("ext"));
  });
  REF_IDS.forEach(id => { const el=$(id); if(!el)return; el.addEventListener("input",()=>scheduleHistSave("ref")); el.addEventListener("change",()=>scheduleHistSave("ref")); });
  PLAN_IDS.forEach(id => { const el=$(id); if(!el)return; el.addEventListener("input",()=>scheduleHistSave("plan")); el.addEventListener("change",()=>scheduleHistSave("plan")); });
  AUD_IDS.forEach(id => { const el=$(id); if(!el)return; el.addEventListener("input",()=>scheduleHistSave("audit")); el.addEventListener("change",()=>scheduleHistSave("audit")); });
  BAT_IDS.forEach(id => { const el=$(id); if(!el)return; el.addEventListener("input",()=>scheduleHistSave("batch")); el.addEventListener("change",()=>scheduleHistSave("batch")); });
  FX_IDS.forEach(id => { const el=$(id); if(!el)return; el.addEventListener("input",()=>scheduleHistSave("fx")); el.addEventListener("change",()=>scheduleHistSave("fx")); });
  shadow.querySelectorAll('input[name="plMode"]').forEach(r => r.addEventListener("change",()=>scheduleHistSave("plan")));

  // ═══════════════════════════════════════════════════════════════════════════
  //  Patch generateNote to use case context
  // ═══════════════════════════════════════════════════════════════════════════

  // generateNote already runs after state.lastCalc is set; patch the header lines
  const origGenNote = generateNote;
  // Shadow the function so ctx flows in:
  /* generateNote is a plain function in scope — we monkey-patch by redefining the
     reference used by the note buttons. Since data-note buttons call origGenNote
     via the event listener already added, we update the listener instead.        */
  shadow.querySelectorAll("[data-note]").forEach(btn => {
    // The original listener was added in the earlier block; add a second listener
    // that patches the output before showing it.
    btn.addEventListener("click", () => {
      // Short-circuit: the first listener already set noteTxt.value.
      // We just need to patch the Ref/Customer lines.
      const ta = $("noteTxt");
      if (!ta) return;
      if (ctx.ticketId)    ta.value = ta.value.replace(/Ref:\s+\[EDIT\]/,    `Ref:      ${ctx.ticketId}`);
      if (ctx.customerName) ta.value = ta.value.replace(/Customer:\s+\[EDIT\]/, `Customer: ${ctx.customerName}`);
    });
  });

})();