My very First own bigboy config

This commit is contained in:
2026-03-11 17:46:45 -04:00
parent 1232b12961
commit bbe2b374a9
5 changed files with 484 additions and 193 deletions

View File

@@ -10,7 +10,6 @@
--ink: #1a1816; /* near-black for body text */
--paper: #f4f2ed; /* warm off-white page background */
--accent: #1a6a98; /* slightly darker blue for contrast */
--accent2: #a03050;
--muted: #6b6360; /* mid-grey for secondary text */
--border: #ccc9c3; /* light grey borders */
--card: #ebe8e2; /* off-white card background */
@@ -98,9 +97,10 @@ body {
background: rgba(0, 0, 0, 0.11) !important;
}
/* ── VS COMPARISON ROWS ──────────────────────────────────────────── */
.vs-save-green { background: #e8f5ee !important; }
.vs-save-amber { background: #fff8e1 !important; }
/* ── VS COMPARISON ───────────────────────────────────────────────── */
.vs-comparison-wrap { background: rgba(0, 0, 0, 0.025) !important; }
.vs-save-green td { background: rgba(39, 174, 96, 0.10) !important; }
.vs-save-amber td { background: rgba(210, 120, 30, 0.09) !important; }
/* ── SUMMARY BADGE ───────────────────────────────────────────────── */
.sec-summary-badge {

View File

@@ -21,13 +21,11 @@
}
/* ── DESIGN TOKENS ─────────────────────────────────────────────
Single source of truth for all colours. Edit here, not inline.
--accent2 (#c4526a) is defined but currently unused in UI.
─────────────────────────────────────────────────────────────── */
:root {
--ink: #ddd8d0;
--paper: #22201d;
--accent: #2d7aa8;
--accent2: #c4526a;
--muted: #a09890;
--border: #3a3630;
--card: #2a2722;
@@ -74,10 +72,11 @@
color: #555;
text-align: right;
line-height: 1.6;
margin-left: auto;
}
/* ── THEME TOGGLE BUTTON ────────────────────────────────────────
Positioned between logo and quote ref in .top-bar-inner.
Sits to the right of the quote ref/date in .top-bar-inner.
Slightly darker chip vs the cream top-bar bg so it reads as
a distinct control, not noise. Works on both theme top-bars.
─────────────────────────────────────────────────────────────── */
@@ -94,8 +93,7 @@
color: #3a3632;
transition: background 0.15s;
flex-shrink: 0;
margin-left: auto;
margin-right: 16px;
margin-left: 14px;
}
.theme-toggle-btn:hover { background: rgba(0, 0, 0, 0.17); }
.theme-toggle-btn:active { background: rgba(0, 0, 0, 0.23); }
@@ -119,7 +117,7 @@
align-items: start;
}
.main-col { display: flex; flex-direction: column; gap: 28px; }
.side-col { position: sticky; top: 72px; z-index: 10; align-self: start; }
.side-col { position: sticky; top: 35px; z-index: 10; align-self: start; }
/* ── CLIENT BAR ─────────────────────────────────────────────────
Lives inside .main-col, above section I.
@@ -178,6 +176,7 @@
gap: 12px;
margin-bottom: 32px;
}
.section:not(.sec-open) .section-header { margin-bottom: 0; }
.section-num {
font-family: 'Cinzel', serif;
font-weight: 700;
@@ -261,6 +260,42 @@
margin-top: 2px;
}
/* ── COLLAPSED SECTION COUNTER (sec-02/03/04 when collapsed) ───
Sits inside .section-title-block below .section-badge.
Two square .sec-count-btn buttons with a small gap between them.
Visible only when the section is collapsed (not .sec-open).
Buttons call stepCount() which stops propagation.
─────────────────────────────────────────────────────────────── */
.sec-collapsed-counter {
display: none;
align-items: center;
gap: 6px;
margin-top: 12px;
}
.section:not(.sec-open) .sec-collapsed-counter {
display: flex;
}
.sec-count-btn {
width: 44px;
height: 44px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--muted);
font-size: 22px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
flex-shrink: 0;
user-select: none;
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.sec-count-btn:hover { background: var(--border); color: var(--ink); }
.sec-count-btn:active { background: var(--accent); color: #fff; border-color: var(--accent); }
/* ── PILL TOGGLE (Section II — M365 vs BYOL) ───────────────────
CSS-only toggle using hidden radio inputs + adjacent label styling.
input:checked + label gets accent background.
@@ -500,7 +535,6 @@
transition: background 0.12s, border-color 0.12s;
}
.addon-row:hover { background: var(--card); }
.addon-row.selected { background: #1a2a38; border-color: #2a5070; }
.addon-row input[type=checkbox] { margin-top: 3px; accent-color: var(--accent); flex-shrink: 0; }
.addon-name { font-family: 'Lato', sans-serif; font-weight: 700; font-size: 15px; }
.addon-price {
@@ -681,7 +715,6 @@
line-height: 1;
margin-bottom: 16px;
}
.sidebar-divider { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
.sidebar-note {
font-size: 13px;
color: var(--muted);
@@ -697,19 +730,27 @@
costs MORE than the comparison (rare at low seat counts).
updateVsComparison(q) renders this section in update().
─────────────────────────────────────────────────────────────── */
.vs-table { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 8px; }
.vs-table td { padding: 7px 4px; }
.vs-table td:last-child { text-align: right; font-family: 'DM Mono', monospace; }
.vs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.vs-table td { padding: 8px 6px; vertical-align: middle; }
.vs-table td:last-child { text-align: right; font-family: 'DM Mono', monospace; white-space: nowrap; }
.vs-table tr:first-child td { padding-bottom: 14px; border-bottom: 1px solid var(--border); }
.vs-table tr:nth-child(2) td,
.vs-table tr:nth-child(4) td { padding-top: 14px; }
.vs-save-row td { padding: 9px 12px; font-size: 12px; font-family: 'DM Mono', monospace; letter-spacing: 0.05em; }
.vs-save-row td:first-child { border-radius: 6px 0 0 6px; }
.vs-save-row td:last-child { border-radius: 0 6px 6px 0; }
.vs-label {
font-family: 'DM Mono', monospace;
font-size: 12px;
letter-spacing: 0.09em;
font-size: 10px;
letter-spacing: 0.10em;
text-transform: uppercase;
color: var(--muted);
margin-top: 18px;
margin-bottom: 6px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.vs-save-row td { border-radius: 3px; padding: 5px 6px; font-size: 13px; }
.vs-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* ── INSIGHT NUDGE BANNER ───────────────────────────────────────
Contextual sales insight shown at bottom of sidebar.
@@ -730,6 +771,8 @@
font-size: 15px;
line-height: 1.7;
border-top: 1px solid var(--border);
min-height: 130px;
box-sizing: border-box;
}
.nudge-banner.amber {
background: #1f1500;
@@ -778,18 +821,52 @@
Right: HST checkbox + one-time fee input.
padding-left:96px aligns with section card edges.
─────────────────────────────────────────────────────────────── */
/* ── SECTIONS TOOLBAR (Collapse All / Expand All) ───────────────── */
.sections-toolbar {
display: flex;
justify-content: flex-end;
margin-left: 96px;
margin-bottom: 10px;
}
.btn-toggle-all {
font-family: 'DM Mono', monospace;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
background: none;
border: 1px solid var(--border);
border-radius: 6px;
padding: 5px 14px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.btn-toggle-all:hover {
color: var(--ink);
border-color: var(--accent);
background: rgba(45, 122, 168, 0.07);
}
.quote-settings-bar {
margin-left: 96px;
padding: 20px 28px 24px 28px;
padding: 18px 24px 20px 24px;
display: flex;
align-items: flex-start;
gap: 32px;
flex-wrap: wrap;
align-items: stretch;
gap: 0;
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.qs-group {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 260px;
padding: 0 24px 0 0;
}
.qs-group { display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 260px; }
.qs-label {
font-family: 'DM Mono', monospace;
font-size: 11px;
@@ -801,12 +878,65 @@
.qs-term-wrap .tier-seg { padding: 10px 8px; }
.qs-term-wrap .tier-name { font-size: 12px; }
.qs-term-wrap .tier-price { display: none; }
/* Best Value badge on 24-month */
.qs-best-badge {
display: inline-block;
font-family: 'DM Mono', monospace;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--green);
background: rgba(33, 112, 69, 0.13);
border: 1px solid rgba(33, 112, 69, 0.3);
border-radius: 3px;
padding: 1px 5px;
vertical-align: middle;
margin-left: 5px;
line-height: 1.5;
}
.tier-seg.active .qs-best-badge {
color: rgba(255,255,255,0.9);
background: rgba(255,255,255,0.18);
border-color: rgba(255,255,255,0.35);
}
/* Discount sub-text — green when not active */
.qs-discount-sub { color: var(--green) !important; }
.tier-seg.active .qs-discount-sub { color: rgba(255,255,255,0.8) !important; }
/* Dynamic savings row — appears below selector when discounted term active */
.qs-savings-row {
display: flex;
align-items: center;
gap: 6px;
font-family: 'DM Mono', monospace;
font-size: 11px;
letter-spacing: 0.05em;
color: var(--green);
margin-top: 2px;
}
.qs-savings-row.hidden { display: none; }
#qsSavingsAmt { font-weight: 700; }
/* Vertical divider between contract term and onboarding fee */
.qs-divider {
width: 1px;
background: var(--border);
margin: -18px 0 -20px;
flex-shrink: 0;
}
.qs-right {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 19px;
padding: 0 0 0 24px;
justify-content: center;
}
/* ── Custom toggle switch (replaces native checkbox for Waive) ── */
.qs-toggle-row {
display: flex;
align-items: center;
@@ -814,7 +944,33 @@
cursor: pointer;
user-select: none;
}
.qs-toggle-row input[type=checkbox] { accent-color: var(--accent); width: 16px; height: 16px; cursor: pointer; }
.qs-toggle-row input[type=checkbox] { display: none; }
.qs-switch {
width: 34px;
height: 20px;
background: var(--border);
border-radius: 10px;
position: relative;
transition: background 0.2s;
flex-shrink: 0;
}
.qs-switch::after {
content: '';
position: absolute;
width: 14px;
height: 14px;
background: #fff;
border-radius: 50%;
top: 3px;
left: 3px;
transition: left 0.2s, background 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.qs-toggle-row input:checked ~ .qs-switch { background: var(--accent); }
.qs-toggle-row input:checked ~ .qs-switch::after { left: 17px; }
.qs-fee-waive:has(input:disabled) { opacity: 0.5; cursor: default; }
.qs-fee-input:disabled { opacity: 0.4; cursor: not-allowed; }
.qs-toggle-label {
font-family: 'DM Mono', monospace;
font-size: 13px;
@@ -832,8 +988,6 @@
gap: 10px;
}
.qs-fee-waive { margin-left: 4px; }
.qs-fee-waive:has(input:disabled) { opacity: 0.5; cursor: default; }
.qs-fee-input:disabled { opacity: 0.4; cursor: not-allowed; }
.qs-fee-label {
font-family: 'DM Mono', monospace;
font-size: 13px;
@@ -863,8 +1017,8 @@
color: var(--ink);
font-family: 'DM Mono', monospace;
font-size: 14px;
width: 90px;
text-align: right;
width: 120px;
text-align: center;
padding: 6px 10px;
outline: none;
}
@@ -892,19 +1046,36 @@
.sidebar-note-mono { font-size: 12px; padding: 2px 0 6px; font-family: 'DM Mono', monospace; }
/* VS Comparison block */
.vs-comparison-wrap { margin-top: 16px; border-top: 1px solid var(--border); padding-top: 14px; }
.vs-inline-icon { margin-right: 5px; vertical-align: middle; }
.vs-svs-label { font-size: 15px; color: var(--ink); font-weight: 600; }
.vs-val-accent { color: var(--accent); font-weight: 600; }
.vs-td-muted { color: var(--muted); }
.vs-td-icon { margin-right: 5px; opacity: 0.6; vertical-align: middle; }
.vs-footnote { font-size: 13px; color: var(--muted); margin-top: 8px; line-height: 1.6; text-align: center; }
.vs-comparison-wrap {
margin-top: 16px;
margin-bottom: 15px;
padding: 24px 24px 22px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
border-radius: 10px;
}
.vs-inline-icon { margin-right: 6px; vertical-align: middle; }
.vs-svs-label { font-size: 14px; color: var(--ink); font-weight: 600; }
.vs-val-accent { color: var(--accent); font-weight: 600; font-size: 14px; }
.vs-td-muted { color: var(--muted); font-size: 12px; }
.vs-td-icon { margin-right: 5px; opacity: 0.55; vertical-align: middle; }
.vs-footnote {
font-size: 11px;
color: var(--muted);
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
line-height: 1.55;
font-style: italic;
}
/* Side note icons and savings highlight */
.note-icon { margin-right: 6px; vertical-align: middle; flex-shrink: 0; }
.savings-amount { color: var(--green); }
.sl-otf-waived > span:first-child { color: var(--green); }
.sl-otf-waived > span:first-child { color: var(--green); text-decoration: line-through; text-decoration-color: var(--green); }
.sl-otf-waived .val { color: var(--green); font-size: 12px; letter-spacing: 0.04em; }
.sl-otf-waived .otf-amt { text-decoration: line-through; text-decoration-color: var(--green); }
.sl-otf-waived .otf-waived-label { text-decoration: none; font-weight: 600; letter-spacing: 0.06em; }
/* Nudge banner internal flex rows */
.nudge-header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
@@ -939,8 +1110,8 @@
.vs-val-green — green text for savings value/label
.vs-val-amber — amber text for "costs more" value/label
─────────────────────────────────────────────────────────────── */
.vs-save-green { background: #1a2e20; }
.vs-save-amber { background: #2e1e0a; }
.vs-save-green td { background: rgba(39, 174, 96, 0.13); }
.vs-save-amber td { background: rgba(210, 120, 30, 0.13); }
.vs-val-green { color: var(--green) !important; }
.vs-val-amber { color: var(--amber) !important; }
@@ -1147,7 +1318,10 @@
.section { margin-left: 64px; }
.section-num { left: -64px; width: 56px; font-size: 52px; }
.client-bar { padding: 24px 0 24px 64px; }
.quote-settings-bar { margin-left: 64px; padding: 18px 22px 20px 22px; gap: 20px; }
.quote-settings-bar { margin-left: 64px; padding: 18px 22px 20px 22px; gap: 0; }
.qs-group { padding-right: 20px; }
.qs-right { padding-left: 20px; }
.sections-toolbar { margin-left: 64px; }
.top-bar-logo { margin-left: 38px; }
.pitch-inner { margin-left: 0; }
@@ -1155,6 +1329,24 @@
.pitch-item:nth-child(2) { border-right: none; }
.pitch-item:nth-child(3) { border-top: 1px solid var(--border); }
.pitch-item:nth-child(4) { border-top: 1px solid var(--border); border-right: none; }
/* Addon rows — price above title so text has full width */
.addon-row {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
column-gap: 10px;
row-gap: 6px;
padding: 12px 14px;
align-items: start;
}
.addon-row input[type=checkbox] { grid-column: 1; grid-row: 2; }
.addon-row > div { grid-column: 2; grid-row: 2; }
.addon-price {
grid-column: 1 / -1;
grid-row: 1;
margin-left: 0;
}
}
/* ── SMALL TABLET / LANDSCAPE PHONE (≤ 900px) ── */
@@ -1166,7 +1358,10 @@
.section-num { left: -52px; width: 44px; font-size: 42px; top: 24px; }
.top-bar-logo { margin-left: 26px; }
.client-bar { padding: 20px 0 20px 52px; }
.quote-settings-bar { margin-left: 52px; padding: 16px 18px 16px 18px; gap: 16px; }
.quote-settings-bar { margin-left: 52px; padding: 16px 18px; gap: 0; }
.qs-group { padding-right: 16px; }
.qs-right { padding-left: 16px; }
.sections-toolbar { margin-left: 52px; }
.pitch-wrap { padding: 0; }
.pitch-inner { margin-left: 0; }
@@ -1177,8 +1372,8 @@
margin-bottom: 24px;
}
.section-title-block { order: 2; flex: 0 0 100%; }
.sec-summary-badge { order: 1; margin-left: auto; }
.sec-chevron { order: 1; }
.sec-summary-badge { order: 1; }
.sec-chevron { order: 1; margin-left: auto; }
.section-title { font-size: 20px; }
.main-col { gap: 20px; }
@@ -1271,9 +1466,12 @@
.client-bar { padding: 20px 0 20px 0; }
.client-input { font-size: 22px; max-width: 100%; }
.quote-settings-bar { margin-left: 0; padding: 14px 16px 16px 16px; flex-direction: column; gap: 12px; }
.qs-group { min-width: 0; }
.qs-right { padding-top: 0; }
.quote-settings-bar { margin-left: 0; padding: 14px 16px 16px; flex-direction: column; gap: 0; }
.sections-toolbar { margin-left: 0; }
.qs-group { min-width: 0; padding-right: 0; }
/* Convert vertical divider to horizontal rule */
.qs-divider { width: auto; height: 1px; margin: 14px 0; }
.qs-right { padding-left: 0; }
.main-col { gap: 16px; }
.main-col > .section:first-of-type { margin-top: 8px; }
@@ -1300,8 +1498,9 @@
.num-input { width: 100%; font-size: 20px; padding: 12px; flex: 1; }
.step-btn { width: 48px; font-size: 22px; }
/* Addon rows — tighter */
.addon-row { padding: 12px 10px; gap: 10px; }
/* Addon rows — tighter font on phones (grid inherited from ≤1100px) */
.addon-row { padding: 12px 10px; }
.addon-price { font-size: 13px; }
.addon-name { font-size: 14px; }
.addon-desc { font-size: 12px; }
@@ -1393,7 +1592,7 @@
align-items: center;
gap: 10px;
position: fixed;
top: 62px;
top: 82px;
right: 14px;
z-index: 200;
background: var(--accent);
@@ -1591,6 +1790,7 @@
.collapsible-header { display: none !important; }
.sec-chevron { display: none !important; }
.sec-summary-badge { display: none !important; }
.sec-collapsed-counter { display: none !important; }
.quote-settings-bar { display: none !important; }
.section-badge { display: none !important; }
#savingsPrompt { display: none !important; }
@@ -1646,7 +1846,8 @@
.sidebar-line .val { color: #1a1a1a !important; }
/* ── VS comparison: clean for print ── */
.vs-save-green { background: #e8f5e9 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.vs-save-green td { background: #e8f5e9 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.vs-save-amber td { background: #fff3e0 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.vs-save-amber { background: #fff8e1 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* ── Feature cards: minimal ── */

View File

@@ -61,6 +61,8 @@
</div>
<div class="sidebar-body">
<div id="sidebarLines_m">
<div class="sidebar-note hidden" id="sideNote-m365_m"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="var(--green)" style="margin-right:6px;vertical-align:middle;flex-shrink:0;"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg> Bundled M365 saves client up to <strong id="m365SaveAmt_m" style="color:var(--green);"></strong>/mo vs retail licensing</div>
<div class="sidebar-note hidden" id="sideNote-byol_m"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="var(--amber)" class="note-icon"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zm-32 224a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg> BYOL selected — client handles their own Microsoft or Google licensing</div>
<div class="sidebar-line hidden" id="sl-users_m">
<span><span class="lbl-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="14" height="13" fill="currentColor" style="vertical-align:middle;"><path d="M144 0a80 80 0 1 1 0 160A80 80 0 1 1 144 0zM512 0a80 80 0 1 1 0 160A80 80 0 1 1 512 0zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM224 224a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z"/></svg></span> Users</span>
<span class="val" id="sl-users-val_m"></span>
@@ -87,7 +89,6 @@
<span><span class="lbl-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="12" height="13" fill="currentColor" style="vertical-align:middle;"><path d="M48 0C21.5 0 0 21.5 0 48V464c0 26.5 21.5 48 48 48h96V432c0-26.5 21.5-48 48-48s48 21.5 48 48v80h96c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48H48zM64 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V240zm112-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V240c0-8.8 7.2-16 16-16zm48-80v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16zm-144-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zm144 208h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H272c-8.8 0-16-7.2-16-16V352c0-8.8 7.2-16 16-16zm-144-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V352c0-8.8 7.2-16 16-16z"/></svg></span> Site Admin Fee</span>
<span class="val" id="sl-admin-val_m">$150</span>
</div>
<div class="sl-sub" id="sl-admin-sub_m"></div>
</div>
<!-- Discount line — hidden when no term discount -->
@@ -131,15 +132,10 @@
<span class="val" id="annualDisplay_m">$1,800</span>
</div>
<div class="sidebar-line" id="perUserRow_m" style="display:none;">
<span>Avg. Cost Per User<br><small class="per-user-cost-sub">(all services ÷ users)</small></span>
<span>Avg. Cost Per User<br><small id="perUserBreakdown_m" class="per-user-cost-sub sidebar-note-mono hidden"></small></span>
<span class="val" id="perUserDisplay_m"></span>
</div>
<div class="sidebar-note sidebar-note-mono hidden" id="perUserBreakdown_m"></div>
<hr class="sidebar-divider">
<div class="sidebar-note" id="sideNote-admin_m">Admin fee = <strong id="adminPct_m"></strong>% of total MRR</div>
<div class="sidebar-note hidden" id="sideNote-m365_m"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="var(--green)" style="margin-right:6px;vertical-align:middle;flex-shrink:0;"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg> Bundled M365 saves client up to <strong id="m365SaveAmt_m" style="color:var(--green);"></strong>/mo vs retail licensing</div>
<div class="sidebar-note hidden" id="sideNote-byol_m"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="var(--amber)" class="note-icon"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zm-32 224a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg> BYOL selected — client handles their own Microsoft or Google licensing</div>
<!-- VS Hiring In-House -->
<div id="vsComparison_m" class="hidden vs-comparison-wrap">
@@ -207,13 +203,13 @@
<path d="M364.36,47.93c-2.96,0-5.57-.33-7.84-.98-2.27-.66-4.25-1.58-5.96-2.78-1.71-1.2-3.23-2.6-4.58-4.22l7.03-7.87c1.87,2.4,3.83,3.98,5.87,4.75,2.04.77,3.96,1.15,5.76,1.15.71,0,1.35-.07,1.91-.2.56-.13.99-.35,1.29-.65.3-.3.45-.71.45-1.24,0-.49-.16-.9-.48-1.24s-.74-.63-1.26-.87c-.53-.24-1.1-.45-1.71-.62-.62-.17-1.22-.31-1.8-.42-.58-.11-1.1-.22-1.55-.34-2.25-.52-4.22-1.16-5.9-1.91-1.69-.75-3.09-1.65-4.22-2.7-1.12-1.05-1.96-2.25-2.5-3.6-.54-1.35-.81-2.87-.81-4.55,0-1.91.44-3.65,1.32-5.23.88-1.57,2.06-2.92,3.54-4.05s3.17-1.99,5.06-2.58,3.85-.9,5.87-.9c2.96,0,5.43.27,7.42.81,1.99.54,3.65,1.32,5,2.33,1.35,1.01,2.51,2.19,3.49,3.54l-7.08,6.8c-.83-.79-1.69-1.43-2.59-1.94s-1.83-.88-2.78-1.12c-.96-.24-1.9-.37-2.84-.37-.86,0-1.59.07-2.19.2-.6.13-1.07.34-1.41.62-.34.28-.51.67-.51,1.15s.21.89.65,1.21c.43.32.97.59,1.63.82.66.22,1.31.4,1.97.53.66.13,1.21.23,1.66.31,2.06.38,3.95.89,5.68,1.55,1.72.66,3.23,1.48,4.53,2.47s2.29,2.23,2.98,3.71c.69,1.48,1.04,3.23,1.04,5.26,0,2.89-.72,5.3-2.16,7.25-1.44,1.95-3.38,3.42-5.82,4.41-2.44.99-5.15,1.49-8.15,1.49Z" fill="#0c0c0c"/>
<path d="M386.61,47.36V8.02h17.71c2.7,0,5.1.58,7.2,1.74,2.1,1.16,3.75,2.75,4.95,4.78,1.2,2.02,1.8,4.35,1.8,6.97s-.6,5.17-1.8,7.31c-1.2,2.14-2.85,3.81-4.95,5.03-2.1,1.22-4.5,1.83-7.2,1.83h-5.56v11.69h-12.14ZM398.53,25.33h3.54c.71,0,1.35-.14,1.91-.42.56-.28,1-.68,1.32-1.21.32-.52.48-1.18.48-1.97s-.16-1.42-.48-1.91c-.32-.49-.76-.85-1.32-1.1-.56-.24-1.2-.37-1.91-.37h-3.54v6.97Z" fill="#0c0c0c"/>
</svg>
<button id="themeToggle" class="theme-toggle-btn" onclick="toggleTheme()" aria-label="Toggle light/dark theme" title="Switch to light theme">
<span id="themeToggleIcon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
</button>
<div class="top-bar-right">
<span id="quoteRef">SVS-00000000-0000</span><br>
<span id="headerDate"></span>
</div>
<button id="themeToggle" class="theme-toggle-btn" onclick="toggleTheme()" aria-label="Toggle light/dark theme" title="Switch to light theme">
<span id="themeToggleIcon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
</button>
</div>
</header>
@@ -246,21 +242,27 @@
<input type="radio" name="contractTerm" id="term12mo" value="12mo" onchange="update()">
<label for="term12mo" class="tier-seg" id="seg-term-12mo">
<div class="tier-name">12-Month</div>
<div class="tier-sub">3% off MRR</div>
<div class="tier-sub qs-discount-sub">3% off MRR</div>
</label>
<input type="radio" name="contractTerm" id="term24mo" value="24mo" onchange="update()">
<label for="term24mo" class="tier-seg" id="seg-term-24mo">
<div class="tier-name">24-Month</div>
<div class="tier-sub">5% off MRR</div>
<div class="tier-name">24-Month <span class="qs-best-badge">Best Value</span></div>
<div class="tier-sub qs-discount-sub">5% off MRR</div>
</label>
</div>
<div class="qs-savings-row hidden" id="qsSavingsDisplay">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="11" height="11" fill="currentColor"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>
Saving <span id="qsSavingsAmt">$0</span>/mo vs. month-to-month
</div>
</div>
<div class="qs-divider"></div>
<div class="qs-right">
<div class="qs-fee-row">
<div class="qs-fee-header">
<label class="qs-fee-label" for="oneTimeFee">Onboarding Fee</label>
<label class="qs-toggle-row qs-fee-waive">
<input type="checkbox" id="onboardingWaived" onchange="this.closest('.qs-fee-row').querySelector('#oneTimeFee').removeAttribute('data-manual'); update();">
<span class="qs-switch"></span>
<span class="qs-toggle-label">Waive</span>
</label>
</div>
@@ -272,6 +274,10 @@
</div>
</div>
<div class="sections-toolbar">
<button class="btn-toggle-all" id="toggleAllBtn" onclick="toggleAllSections()">Collapse All</button>
</div>
<!-- ────────────────────────────────────────────────────────────
SECTION I — SITE ADMIN FEE
id="sec-01" sec-open = starts expanded
@@ -364,6 +370,10 @@
<div class="section-title">User Package</div>
<div class="section-subtitle">Per-user monthly services — identity, email, security &amp; helpdesk</div>
<span class="section-badge">Per User / Month</span>
<div class="sec-collapsed-counter">
<button class="sec-count-btn" onclick="stepCount('userCount',-1,event)">&minus;</button>
<button class="sec-count-btn" onclick="stepCount('userCount',1,event)">+</button>
</div>
</div>
<span id="sec02-summary" class="sec-summary-badge"></span>
<div class="sec-chevron"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></div>
@@ -474,6 +484,10 @@
<div class="section-title">Endpoint Package</div>
<div class="section-subtitle">Per-device managed protection — workstations &amp; laptops</div>
<span class="section-badge">$35 / Endpoint / Month</span>
<div class="sec-collapsed-counter">
<button class="sec-count-btn" onclick="stepCount('endpointCount',-1,event)">&minus;</button>
<button class="sec-count-btn" onclick="stepCount('endpointCount',1,event)">+</button>
</div>
</div>
<span id="sec03-summary" class="sec-summary-badge"></span>
<div class="sec-chevron"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></div>
@@ -552,6 +566,10 @@
<div class="section-title">Server Management</div>
<div class="section-subtitle">Dedicated management for physical &amp; virtual servers</div>
<span class="section-badge">$120 / Server / Month</span>
<div class="sec-collapsed-counter">
<button class="sec-count-btn" onclick="stepCount('serverCount',-1,event)">&minus;</button>
<button class="sec-count-btn" onclick="stepCount('serverCount',1,event)">+</button>
</div>
</div>
<span id="sec04-summary" class="sec-summary-badge"></span>
<div class="sec-chevron"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></div>
@@ -762,6 +780,8 @@
toggled via style.display not .hidden class.
──────────────────────────────────────────────────────────── -->
<div id="sidebarLines">
<div class="sidebar-note hidden" id="sideNote-m365"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="var(--green)" class="note-icon"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg> Bundled M365 saves client up to <strong id="m365SaveAmt" class="savings-amount"></strong>/mo vs retail licensing</div>
<div class="sidebar-note hidden" id="sideNote-byol"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="var(--amber)" class="note-icon"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zm-32 224a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg> BYOL selected — client handles their own Microsoft or Google licensing</div>
<div class="sidebar-line hidden" id="sl-users">
<span><span class="lbl-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="14" height="13" fill="currentColor" style="vertical-align:middle;"><path d="M144 0a80 80 0 1 1 0 160A80 80 0 1 1 144 0zM512 0a80 80 0 1 1 0 160A80 80 0 1 1 512 0zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM224 224a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z"/></svg></span> Users</span>
<span class="val" id="sl-users-val"></span>
@@ -788,7 +808,6 @@
<span><span class="lbl-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="12" height="13" fill="currentColor" style="vertical-align:middle;"><path d="M48 0C21.5 0 0 21.5 0 48V464c0 26.5 21.5 48 48 48h96V432c0-26.5 21.5-48 48-48s48 21.5 48 48v80h96c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48H48zM64 240c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V240zm112-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V240c0-8.8 7.2-16 16-16zm48-80v32c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16h32c8.8 0 16 7.2 16 16zm-144-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16zm144 208h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H272c-8.8 0-16-7.2-16-16V352c0-8.8 7.2-16 16-16zm-144-16h32c8.8 0 16 7.2 16 16v32c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V352c0-8.8 7.2-16 16-16z"/></svg></span> Site Admin Fee</span>
<span class="val" id="sl-admin-val">$150</span>
</div>
<div class="sl-sub" id="sl-admin-sub"></div>
</div>
<!-- Discount line — hidden when no term discount -->
@@ -832,15 +851,9 @@
<span class="val" id="annualDisplay">$1,800</span>
</div>
<div class="sidebar-line" id="perUserRow" style="display:none;">
<span>Avg. Cost Per User<br><small class="per-user-cost-sub">(all services ÷ users)</small></span>
<span>Avg. Cost Per User<br><small id="perUserBreakdown" class="per-user-cost-sub sidebar-note-mono hidden"></small></span>
<span class="val" id="perUserDisplay"></span>
</div>
<div class="sidebar-note sidebar-note-mono hidden" id="perUserBreakdown"></div>
<hr class="sidebar-divider">
<div class="sidebar-note" id="sideNote-admin">Admin fee = <strong id="adminPct"></strong>% of total MRR</div>
<div class="sidebar-note hidden" id="sideNote-m365"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="var(--green)" class="note-icon"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg> Bundled M365 saves client up to <strong id="m365SaveAmt" class="savings-amount"></strong>/mo vs retail licensing</div>
<div class="sidebar-note hidden" id="sideNote-byol"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="var(--amber)" class="note-icon"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zm-32 224a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg> BYOL selected — client handles their own Microsoft or Google licensing</div>
<!-- ── VS HIRING IN-HOUSE ─────────────────────────────────────
Hidden (.hidden) until users>0 OR endpoints>0.

View File

@@ -1,26 +1,97 @@
// ── PRICING CONSTANTS ───────────────────────────────────────────
// All rates in CAD. Edit here — calcQuote() reads these only.
// ── PRICING VARIABLES ───────────────────────────────────────────
// All rates in CAD. Loaded from package-prices.csv at startup.
// Falls back to built-in defaults if CSV is unavailable.
// ADMIN_FEE_FLOOR = minimum site admin fee regardless of seat count
// ADMIN_FEE_MINIMUM = engagement threshold; admin = max(FLOOR, MIN-subtotal)
// ADMIN_FEE_ZT = supplement added to admin when ZT is active
// TOOL_COST_* = internal cost estimates (VS comparison only, not billed)
// IT_SALARY_1/5 = Ottawa benchmark salaries for VS comparison
// ─────────────────────────────────────────────────────────────────
const ADMIN_FEE_FLOOR = 150;
const ADMIN_FEE_MINIMUM = 650;
const ADMIN_FEE_ZT = 250;
const VOIP_RATES = { basic: 28, standard: 35, premium: 45 };
const VOIP_PHONE_RATE = 15;
const VOIP_FAX_RATE = 10;
const TOOL_COST_PER_USER = 42;
const TOOL_COST_PER_ENDPOINT = 23;
const TOOL_COST_MIN = 650;
const IT_SALARY_1 = 85000;
const IT_SALARY_5 = 420000;
let RATE_M365 = 130;
let RATE_BYOL = 110;
let ADDON_EXT_HOURS = 25;
let ADDON_1PASSWORD = 9;
let ADDON_INKY = 5;
let ADDON_ZERO_TRUST_USER= 55;
let RATE_ENDPOINT = 35;
let RATE_SERVER = 120;
let ADDON_USB_BLOCKING = 4;
let ADDON_BARE_METAL_BACKUP = 25;
let ZT_SEAT_RATE = 25;
let ZT_ROUTER_RATE = 100;
let ADMIN_FEE_FLOOR = 150;
let ADMIN_FEE_MINIMUM = 650;
let ADMIN_FEE_ZT = 250;
let ADMIN_1PWM_PCT = 0.10;
let VOIP_RATE_BASIC = 28;
let VOIP_RATE_STANDARD = 35;
let VOIP_RATE_PREMIUM = 45;
let VOIP_PHONE_RATE = 15;
let VOIP_FAX_RATE = 10;
let TOOL_COST_PER_USER = 42;
let TOOL_COST_PER_ENDPOINT = 23;
let TOOL_COST_MIN = 650;
let IT_SALARY_1 = 85000;
let IT_SALARY_5 = 420000;
// CONTRACT_DISCOUNT: discount applied to MRR based on contract length
// m2m = month-to-month (no discount), 12mo = 3% off, 24mo = 5% off
const CONTRACT_DISCOUNT = { 'm2m': 0, '12mo': 0.03, '24mo': 0.05 };
const HST_RATE = 0.13; // Ontario HST 13%
let DISCOUNT_M2M = 0;
let DISCOUNT_12MO = 0.03;
let DISCOUNT_24MO = 0.05;
let HST_RATE = 0.13; // Ontario HST 13%
// ── loadPricing() ────────────────────────────────────────────────
// Fetches package-prices.csv and overrides the pricing variables above.
// Silently falls back to built-in defaults if CSV is missing or malformed.
async function loadPricing() {
try {
const res = await fetch('package-prices.csv');
if (!res.ok) return;
const text = await res.text();
const lines = text.split('\n').slice(1); // skip header row
lines.forEach(line => {
const parts = line.split(',');
if (parts.length < 3) return;
const key = parts[1].trim();
const val = parseFloat(parts[2].trim());
if (isNaN(val)) return;
switch (key) {
case 'RATE_M365': RATE_M365 = val; break;
case 'RATE_BYOL': RATE_BYOL = val; break;
case 'ADDON_EXT_HOURS': ADDON_EXT_HOURS = val; break;
case 'ADDON_1PASSWORD': ADDON_1PASSWORD = val; break;
case 'ADDON_INKY': ADDON_INKY = val; break;
case 'ADDON_ZERO_TRUST_USER': ADDON_ZERO_TRUST_USER = val; break;
case 'RATE_ENDPOINT': RATE_ENDPOINT = val; break;
case 'RATE_SERVER': RATE_SERVER = val; break;
case 'ADDON_USB_BLOCKING': ADDON_USB_BLOCKING = val; break;
case 'ADDON_BARE_METAL_BACKUP': ADDON_BARE_METAL_BACKUP = val; break;
case 'ZT_SEAT_RATE': ZT_SEAT_RATE = val; break;
case 'ZT_ROUTER_RATE': ZT_ROUTER_RATE = val; break;
case 'ADMIN_FEE_FLOOR': ADMIN_FEE_FLOOR = val; break;
case 'ADMIN_FEE_MINIMUM': ADMIN_FEE_MINIMUM = val; break;
case 'ADMIN_FEE_ZT': ADMIN_FEE_ZT = val; break;
case 'ADMIN_1PWM_PCT': ADMIN_1PWM_PCT = val; break;
case 'VOIP_RATE_BASIC': VOIP_RATE_BASIC = val; break;
case 'VOIP_RATE_STANDARD': VOIP_RATE_STANDARD = val; break;
case 'VOIP_RATE_PREMIUM': VOIP_RATE_PREMIUM = val; break;
case 'VOIP_PHONE_RATE': VOIP_PHONE_RATE = val; break;
case 'VOIP_FAX_RATE': VOIP_FAX_RATE = val; break;
case 'TOOL_COST_PER_USER': TOOL_COST_PER_USER = val; break;
case 'TOOL_COST_PER_ENDPOINT':TOOL_COST_PER_ENDPOINT= val; break;
case 'TOOL_COST_MIN': TOOL_COST_MIN = val; break;
case 'IT_SALARY_1': IT_SALARY_1 = val; break;
case 'IT_SALARY_5': IT_SALARY_5 = val; break;
case 'DISCOUNT_M2M': DISCOUNT_M2M = val; break;
case 'DISCOUNT_12MO': DISCOUNT_12MO = val; break;
case 'DISCOUNT_24MO': DISCOUNT_24MO = val; break;
case 'HST_RATE': HST_RATE = val; break;
}
});
} catch (e) {
// CSV unavailable — built-in defaults remain active
}
}
// --- CALC ---
// ── calcQuote() ─────────────────────────────────────────────────
@@ -51,33 +122,36 @@ function calcQuote() {
const oneTimeFee = parseFloat(document.getElementById('oneTimeFee')?.value) || 0;
const ztActive = addZT || ztSeats > 0;
const baseUserRate = byol ? 110 : 130;
const addonRate = (addExtHours ? 25 : 0) + (addPWM ? 9 : 0) + (addINKY ? 5 : 0) + (addZT ? 55 : 0);
const VOIP_RATES = { basic: VOIP_RATE_BASIC, standard: VOIP_RATE_STANDARD, premium: VOIP_RATE_PREMIUM };
const CONTRACT_DISCOUNT = { 'm2m': DISCOUNT_M2M, '12mo': DISCOUNT_12MO, '24mo': DISCOUNT_24MO };
const baseUserRate = byol ? RATE_BYOL : RATE_M365;
const addonRate = (addExtHours ? ADDON_EXT_HOURS : 0) + (addPWM ? ADDON_1PASSWORD : 0) + (addINKY ? ADDON_INKY : 0) + (addZT ? ADDON_ZERO_TRUST_USER : 0);
const totalUserRate= baseUserRate + addonRate;
const userBase = users * baseUserRate;
const userExt = addExtHours ? users * 25 : 0;
const userPWM = addPWM ? users * 9 : 0;
const userINKY = addINKY ? users * 5 : 0;
const userZT = addZT ? users * 55 : 0;
const userExt = addExtHours ? users * ADDON_EXT_HOURS : 0;
const userPWM = addPWM ? users * ADDON_1PASSWORD : 0;
const userINKY = addINKY ? users * ADDON_INKY : 0;
const userZT = addZT ? users * ADDON_ZERO_TRUST_USER : 0;
const userTotal = userBase + userExt + userPWM + userINKY + userZT;
const endpointBase = endpoints * 35;
const serverBase = servers * 120;
const endpointUSB = addUSB ? endpoints * 4 : 0;
const endpointBMB = addBMB ? endpoints * 25 : 0;
const endpointBase = endpoints * RATE_ENDPOINT;
const serverBase = servers * RATE_SERVER;
const endpointUSB = addUSB ? endpoints * ADDON_USB_BLOCKING : 0;
const endpointBMB = addBMB ? endpoints * ADDON_BARE_METAL_BACKUP : 0;
const endpointTotal= endpointBase + serverBase + endpointUSB + endpointBMB;
const baseSubtotal = userBase + endpointBase + serverBase;
const siteAdminBase = Math.max(ADMIN_FEE_FLOOR, ADMIN_FEE_MINIMUM - baseSubtotal);
const admin1PWM = addPWM ? Math.round(userPWM * 0.10) : 0;
const admin1PWM = addPWM ? Math.round(userPWM * ADMIN_1PWM_PCT) : 0;
const adminFeeNet = siteAdminBase + (ztActive ? ADMIN_FEE_ZT : 0) + admin1PWM;
const ztNetSeats = ztSeats * 25;
const ztNetRouters = ztRouters * 100;
const ztNetSeats = ztSeats * ZT_SEAT_RATE;
const ztNetRouters = ztRouters * ZT_ROUTER_RATE;
const ztNetTotal = ztNetSeats + ztNetRouters;
const voipSeatRate = VOIP_RATES[voipTier] || 28;
const voipSeatRate = VOIP_RATES[voipTier] || VOIP_RATE_BASIC;
const voipSeatsAmt = voipSeats * voipSeatRate;
const voipPhoneAmt = addVoipPhone ? voipSeats * VOIP_PHONE_RATE : 0;
const voipFaxAmt = addVoipFax ? VOIP_FAX_RATE : 0;
@@ -149,9 +223,9 @@ function update() {
let oneTimeFee;
if (waived) {
oneTimeFee = 0;
if (feeEl) { feeEl.value = ''; feeEl.disabled = true; }
if (feeEl) { feeEl.value = ''; feeEl.disabled = true; feeEl.placeholder = 'Complimentary'; }
} else {
if (feeEl) feeEl.disabled = false;
if (feeEl) { feeEl.disabled = false; feeEl.placeholder = 'auto'; }
if (feeEl && !feeEl.dataset.manual) {
oneTimeFee = Math.round(q.MRR / 2);
feeEl.value = oneTimeFee > 0 ? oneTimeFee : '';
@@ -284,7 +358,7 @@ function update() {
if (_waived && _wouldBe > 0) {
row.classList.remove('hidden');
row.classList.add('sl-otf-waived');
if (valEl) valEl.textContent = fmt(_wouldBe) + ' WAIVED';
if (valEl) valEl.innerHTML = '<span class="otf-amt">' + fmt(_wouldBe) + '</span> <span class="otf-waived-label">WAIVED</span>';
} else if (oneTimeFee > 0) {
row.classList.remove('hidden', 'sl-otf-waived');
if (valEl) valEl.textContent = fmt(oneTimeFee) + ' (one-time)';
@@ -304,7 +378,6 @@ function update() {
}
// Sidebar notes
getEl('adminPct').textContent = MRR > 0 ? Math.round(adminFeeNet / MRR * 100) : '—';
getEl('sideNote-m365').classList.toggle('hidden', byol);
getEl('sideNote-byol').classList.toggle('hidden', !byol);
if (!byol && users > 0) getEl('m365SaveAmt').textContent = fmt(users * 15);
@@ -326,6 +399,18 @@ function update() {
if (seg) seg.classList.toggle('active', t === contractTerm);
});
// ── Contract savings display ─────────────────────────────────
const savingsRow = document.getElementById('qsSavingsDisplay');
const savingsAmt = document.getElementById('qsSavingsAmt');
if (savingsRow && savingsAmt) {
if (q.discountAmt > 0) {
savingsAmt.textContent = fmt(q.discountAmt);
savingsRow.classList.remove('hidden');
} else {
savingsRow.classList.add('hidden');
}
}
// Nudges — dynamic dollar values, context-sensitive conditions
const nudges = [];
if (!addZT && users > 0) nudges.push({
@@ -345,7 +430,7 @@ function update() {
color: 'amber'
});
if (voipSeats > 0 && voipTier === 'basic') nudges.push({
text: `Standard tier adds HD video, call analytics, and advanced call routing — only ${fmt(voipSeats * (VOIP_RATES.standard - VOIP_RATES.basic))}/mo more for ${voipSeats} seat${voipSeats !== 1 ? 's' : ''}. Most clients upgrade within 6 months.`,
text: `Standard tier adds HD video, call analytics, and advanced call routing — only ${fmt(voipSeats * (VOIP_RATE_STANDARD - VOIP_RATE_BASIC))}/mo more for ${voipSeats} seat${voipSeats !== 1 ? 's' : ''}. Most clients upgrade within 6 months.`,
color: 'green'
});
if (servers > 0 && !addBMB) nudges.push({
@@ -357,7 +442,7 @@ function update() {
color: 'amber'
});
window._nudges = nudges;
if (!window._nudgeIndex || window._nudgeIndex >= nudges.length) window._nudgeIndex = 0;
if (window._nudgeIndex == null || window._nudgeIndex >= nudges.length) window._nudgeIndex = 0;
renderNudge();
updateSavings(q);
@@ -378,6 +463,42 @@ function toggleSection(id) {
const isOpen = section.classList.toggle('sec-open');
body.style.display = isOpen ? '' : 'none';
updateSectionSummaries();
updateToggleAllBtn();
}
// ── toggleAllSections() / updateToggleAllBtn() ────────────────────
// Collapse all if any are open; expand all if all are closed.
// Button label reflects current state.
const _allSecIds = ['sec-01','sec-02','sec-03','sec-04','sec-05','sec-06'];
function toggleAllSections() {
const anyOpen = _allSecIds.some(id => document.getElementById(id)?.classList.contains('sec-open'));
_allSecIds.forEach(id => {
const section = document.getElementById(id);
const body = document.getElementById(id + '-body');
if (!section || !body) return;
if (anyOpen) { section.classList.remove('sec-open'); body.style.display = 'none'; }
else { section.classList.add('sec-open'); body.style.display = ''; }
});
updateSectionSummaries();
updateToggleAllBtn();
}
function updateToggleAllBtn() {
const anyOpen = _allSecIds.some(id => document.getElementById(id)?.classList.contains('sec-open'));
const btn = document.getElementById('toggleAllBtn');
if (btn) btn.textContent = anyOpen ? 'Collapse All' : 'Expand All';
}
// ── stepCount(inputId, delta, event) ─────────────────────────────
// Increments or decrements a num-input by delta (±1) from the
// collapsed section counter. Stops propagation so the click doesn't
// bubble up and toggle the section open/closed.
function stepCount(inputId, delta, event) {
if (event) event.stopPropagation();
const el = document.getElementById(inputId);
if (!el) return;
const min = parseInt(el.min) >= 0 ? parseInt(el.min) : 0;
el.value = Math.max(min, (parseInt(el.value) || 0) + delta);
update();
}
// ── updateSectionSummaries(q) ────────────────────────────────────
@@ -574,6 +695,7 @@ function updateSavings(q) {
const saving = bill - voipTotal;
if (saving > 0) {
comparator.textContent = `✓ Switching to SVS VoIP saves ~${fmt(saving)}/mo (${fmt(saving*12)}/yr) vs your current bill of ${fmt(bill)}/mo`;
comparator.style.color = '';
} else {
comparator.textContent = `Your current bill (${fmt(bill)}/mo) is lower than this VoIP quote (${fmt(voipTotal)}/mo) — consider reviewing the tier or seat count.`;
comparator.style.color = 'var(--amber)';
@@ -688,78 +810,6 @@ function restoreState() {
} catch(e) { return false; }
}
// ── exportQuote() ─────────────────────────────────────────────────
// Builds a plain-text quote summary from calcQuote() and triggers
// a browser file download: SVS_MSP_Quote_<ClientName>.txt
function exportQuote() {
const q = calcQuote();
const lines = [
'═══════════════════════════════════════════════════════',
' SVS MSP — Managed Services Quote',
'═══════════════════════════════════════════════════════',
` Client: ${q.clientName || '(not specified)'}`,
` Ref: ${document.getElementById('quoteRef')?.textContent || ''}`,
` Date: ${document.getElementById('headerDate')?.textContent || ''}`,
'───────────────────────────────────────────────────────',
'',
' SERVICES SUMMARY',
'',
];
if (q.users > 0) {
lines.push(` User Package (${q.users} users @ ${fmt(q.totalUserRate)}/user)`);
lines.push(` Base (${q.byol ? 'BYOL' : 'M365 Incl.'}): ${fmt(q.userBase)}/mo`);
if (q.userExt) lines.push(` Extended Hours: ${fmt(q.userExt)}/mo`);
if (q.userPWM) lines.push(` 1Password: ${fmt(q.userPWM)}/mo`);
if (q.userINKY) lines.push(` INKY Pro: ${fmt(q.userINKY)}/mo`);
if (q.userZT) lines.push(` Zero Trust: ${fmt(q.userZT)}/mo`);
lines.push(` Subtotal: ${fmt(q.userTotal)}/mo`);
lines.push('');
}
if (q.endpoints > 0 || q.servers > 0) {
lines.push(` Endpoint Package (${q.endpoints} endpoints @ $35)`);
if (q.endpointBMB) lines.push(` + Bare Metal Backup: ${fmt(q.endpointBMB)}/mo`);
if (q.endpointUSB) lines.push(` + USB Blocking: ${fmt(q.endpointUSB)}/mo`);
if (q.servers > 0) lines.push(` + ${q.servers} Server(s) @ $120: ${fmt(q.serverBase)}/mo`);
lines.push(` Subtotal: ${fmt(q.endpointTotal)}/mo`);
lines.push('');
}
lines.push(` Site Admin Fee: ${fmt(q.adminFeeNet)}/mo`);
lines.push('');
if (q.ztNetTotal > 0) {
lines.push(` Zero Trust Networking: ${fmt(q.ztNetTotal)}/mo`);
lines.push('');
}
if (q.voipTotal > 0) {
lines.push(` VoIP / UCaaS (${q.voipSeats} seats, ${q.voipTier}): ${fmt(q.voipTotal)}/mo`);
lines.push('');
}
lines.push('───────────────────────────────────────────────────────');
if (q.discountPct > 0) {
const termLabel = q.contractTerm === '12mo' ? '12-Month' : '24-Month';
lines.push(` BASE MRR: ${fmt(q.MRR)}`);
lines.push(` ${termLabel} DISCOUNT (${Math.round(q.discountPct*100)}%): ${fmt(q.discountAmt)}`);
}
lines.push(` MONTHLY RECURRING (MRR): ${fmt(q.effectiveMrr)}`);
if (q.hstEnabled) lines.push(` + HST (13%): ${fmt(q.hstAmt)}`);
lines.push(` ANNUAL PROJECTION: ${fmt(q.effectiveAnnual)}`);
if (q.oneTimeFee > 0) lines.push(` ONBOARDING FEE: ${fmt(q.oneTimeFee)} (one-time, not recurring)`);
if (q.users > 0) lines.push(` AVG. COST PER USER: ${fmt(q.effectiveMrr / q.users)}/user/mo (all services)`);
lines.push(` HST: ${q.hstEnabled ? 'Included in figures above' : 'Not included — HST applies at 13% on invoice'}`);
lines.push('───────────────────────────────────────────────────────');
lines.push('');
lines.push('Prepared by SVS MSP | This quote is valid for 30 days from date of issue.');
lines.push('Questions? Contact your SVS MSP account representative.');
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const client = (q.clientName || 'Quote').replace(/[^a-z0-9]/gi,'_');
a.href = url;
a.download = `SVS_MSP_Quote_${client}.txt`;
a.click();
URL.revokeObjectURL(url);
}
// ── printInvoice() ────────────────────────────────────────────────
// Generates a clean invoice-style HTML document in a new window
// and triggers the browser print dialog (Save as PDF works perfectly).
@@ -803,7 +853,7 @@ function printInvoice() {
}
if (q.voipTotal > 0) {
const tier = {basic:'Basic',standard:'Standard',premium:'Premium'}[q.voipTier] || 'Basic';
row(`VoIP / UCaaS — ${tier}`, `${q.voipSeats} seat${q.voipSeats!==1?'s':''} × $${VOIP_RATES[q.voipTier]}/mo`, fmt(q.voipSeatsAmt));
row(`VoIP / UCaaS — ${tier}`, `${q.voipSeats} seat${q.voipSeats!==1?'s':''} × $${q.voipSeatRate}/mo`, fmt(q.voipSeatsAmt));
if (q.voipPhoneAmt > 0) row('↳ Desk Phone HaaS (+$15/seat)', '', fmt(q.voipPhoneAmt), true);
if (q.voipFaxAmt > 0) row('↳ Virtual Fax (+$10/mo)', '', fmt(q.voipFaxAmt), true);
}
@@ -1042,7 +1092,8 @@ function initTheme() {
// Entry point. Sets quote ref (SVS-YYYYMMDD-XXXX) and date,
// restores saved state, then calls update() for initial render.
// Called once at bottom of script.
function initQuote() {
async function initQuote() {
await loadPricing();
const now = new Date();
const months = ['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER'];
const year = now.getFullYear();
@@ -1135,7 +1186,6 @@ initQuote();
syncEl('annualDisplay');
syncEl('perUserDisplay');
syncEl('perUserBreakdown');
syncEl('adminPct');
syncEl('m365SaveAmt');
syncEl('sl-discount-val');
syncEl('sl-base-mrr-val');
@@ -1162,7 +1212,6 @@ initQuote();
syncClass('sl-zt');
syncClass('sl-voip');
syncClass('sl-admin');
syncClass('sideNote-admin');
syncClass('sideNote-m365');
syncClass('sideNote-byol');
syncClass('vsComparison');
@@ -1173,9 +1222,6 @@ initQuote();
syncClass('sl-hst-row');
syncClass('sl-hst-total-row');
syncClass('sl-otf-row');
syncStyle('sl-discount-row');
syncStyle('sl-hst-row');
syncStyle('sl-otf-row');
syncClass('vs-1man-save-row');
syncClass('vs-1man-save');
syncClass('vs-1man-save-lbl');

31
package-prices.csv Normal file
View File

@@ -0,0 +1,31 @@
category,key,value,description
user_packages,RATE_M365,130,Per-user/mo rate — M365 included (standard package)
user_packages,RATE_BYOL,110,Per-user/mo rate — Bring Your Own License (client supplies M365)
user_addons,ADDON_EXT_HOURS,25,Per-user/mo — Extended support hours add-on
user_addons,ADDON_1PASSWORD,9,Per-user/mo — 1Password password manager add-on
user_addons,ADDON_INKY,5,Per-user/mo — Inky email security add-on
user_addons,ADDON_ZERO_TRUST_USER,55,Per-user/mo — Zero Trust security add-on (user seat)
endpoints,RATE_ENDPOINT,35,Per-endpoint/mo — Managed endpoint (workstation/laptop)
endpoints,RATE_SERVER,120,Per-server/mo — Managed server
endpoint_addons,ADDON_USB_BLOCKING,4,Per-endpoint/mo — USB blocking/device control add-on
endpoint_addons,ADDON_BARE_METAL_BACKUP,25,Per-endpoint/mo — Bare metal backup add-on
zero_trust_network,ZT_SEAT_RATE,25,Per-seat/mo — Zero Trust Network Access seat
zero_trust_network,ZT_ROUTER_RATE,100,Per-router/mo — Zero Trust Network Access router
voip,VOIP_RATE_BASIC,28,Per-seat/mo — VoIP Basic tier
voip,VOIP_RATE_STANDARD,35,Per-seat/mo — VoIP Standard tier
voip,VOIP_RATE_PREMIUM,45,Per-seat/mo — VoIP Premium tier
voip,VOIP_PHONE_RATE,15,Per-seat/mo — VoIP physical phone hardware add-on
voip,VOIP_FAX_RATE,10,Flat/mo — VoIP eFax add-on
site_admin,ADMIN_FEE_FLOOR,150,Minimum site admin fee regardless of seat count ($/mo)
site_admin,ADMIN_FEE_MINIMUM,650,Engagement threshold — admin = max(FLOOR, MINIMUM - subtotal)
site_admin,ADMIN_FEE_ZT,250,Additional site admin supplement when Zero Trust is active ($/mo)
site_admin,ADMIN_1PWM_PCT,0.10,Site admin surcharge as fraction of 1Password MRR (e.g. 0.10 = 10%)
contract_discounts,DISCOUNT_M2M,0,Month-to-month contract discount (0 = no discount)
contract_discounts,DISCOUNT_12MO,0.03,12-month contract discount as a decimal (0.03 = 3% off MRR)
contract_discounts,DISCOUNT_24MO,0.05,24-month contract discount as a decimal (0.05 = 5% off MRR)
tax,HST_RATE,0.13,Ontario HST rate as a decimal (0.13 = 13%)
vs_comparison,TOOL_COST_PER_USER,42,Internal tool cost per user (used in VS Hiring comparison only — not billed)
vs_comparison,TOOL_COST_PER_ENDPOINT,23,Internal tool cost per endpoint (used in VS Hiring comparison only — not billed)
vs_comparison,TOOL_COST_MIN,650,Minimum internal tool cost floor (used in VS Hiring comparison only — not billed)
vs_comparison,IT_SALARY_1,85000,Ottawa benchmark annual salary for 1 in-house IT staff (VS Hiring comparison)
vs_comparison,IT_SALARY_5,420000,Ottawa benchmark annual salary for 5 in-house IT staff (VS Hiring comparison)
1 category key value description
2 user_packages RATE_M365 130 Per-user/mo rate — M365 included (standard package)
3 user_packages RATE_BYOL 110 Per-user/mo rate — Bring Your Own License (client supplies M365)
4 user_addons ADDON_EXT_HOURS 25 Per-user/mo — Extended support hours add-on
5 user_addons ADDON_1PASSWORD 9 Per-user/mo — 1Password password manager add-on
6 user_addons ADDON_INKY 5 Per-user/mo — Inky email security add-on
7 user_addons ADDON_ZERO_TRUST_USER 55 Per-user/mo — Zero Trust security add-on (user seat)
8 endpoints RATE_ENDPOINT 35 Per-endpoint/mo — Managed endpoint (workstation/laptop)
9 endpoints RATE_SERVER 120 Per-server/mo — Managed server
10 endpoint_addons ADDON_USB_BLOCKING 4 Per-endpoint/mo — USB blocking/device control add-on
11 endpoint_addons ADDON_BARE_METAL_BACKUP 25 Per-endpoint/mo — Bare metal backup add-on
12 zero_trust_network ZT_SEAT_RATE 25 Per-seat/mo — Zero Trust Network Access seat
13 zero_trust_network ZT_ROUTER_RATE 100 Per-router/mo — Zero Trust Network Access router
14 voip VOIP_RATE_BASIC 28 Per-seat/mo — VoIP Basic tier
15 voip VOIP_RATE_STANDARD 35 Per-seat/mo — VoIP Standard tier
16 voip VOIP_RATE_PREMIUM 45 Per-seat/mo — VoIP Premium tier
17 voip VOIP_PHONE_RATE 15 Per-seat/mo — VoIP physical phone hardware add-on
18 voip VOIP_FAX_RATE 10 Flat/mo — VoIP eFax add-on
19 site_admin ADMIN_FEE_FLOOR 150 Minimum site admin fee regardless of seat count ($/mo)
20 site_admin ADMIN_FEE_MINIMUM 650 Engagement threshold — admin = max(FLOOR MINIMUM - subtotal)
21 site_admin ADMIN_FEE_ZT 250 Additional site admin supplement when Zero Trust is active ($/mo)
22 site_admin ADMIN_1PWM_PCT 0.10 Site admin surcharge as fraction of 1Password MRR (e.g. 0.10 = 10%)
23 contract_discounts DISCOUNT_M2M 0 Month-to-month contract discount (0 = no discount)
24 contract_discounts DISCOUNT_12MO 0.03 12-month contract discount as a decimal (0.03 = 3% off MRR)
25 contract_discounts DISCOUNT_24MO 0.05 24-month contract discount as a decimal (0.05 = 5% off MRR)
26 tax HST_RATE 0.13 Ontario HST rate as a decimal (0.13 = 13%)
27 vs_comparison TOOL_COST_PER_USER 42 Internal tool cost per user (used in VS Hiring comparison only — not billed)
28 vs_comparison TOOL_COST_PER_ENDPOINT 23 Internal tool cost per endpoint (used in VS Hiring comparison only — not billed)
29 vs_comparison TOOL_COST_MIN 650 Minimum internal tool cost floor (used in VS Hiring comparison only — not billed)
30 vs_comparison IT_SALARY_1 85000 Ottawa benchmark annual salary for 1 in-house IT staff (VS Hiring comparison)
31 vs_comparison IT_SALARY_5 420000 Ottawa benchmark annual salary for 5 in-house IT staff (VS Hiring comparison)