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

View File

@@ -21,13 +21,11 @@
} }
/* ── DESIGN TOKENS ───────────────────────────────────────────── /* ── DESIGN TOKENS ─────────────────────────────────────────────
Single source of truth for all colours. Edit here, not inline. Single source of truth for all colours. Edit here, not inline.
--accent2 (#c4526a) is defined but currently unused in UI.
─────────────────────────────────────────────────────────────── */ ─────────────────────────────────────────────────────────────── */
:root { :root {
--ink: #ddd8d0; --ink: #ddd8d0;
--paper: #22201d; --paper: #22201d;
--accent: #2d7aa8; --accent: #2d7aa8;
--accent2: #c4526a;
--muted: #a09890; --muted: #a09890;
--border: #3a3630; --border: #3a3630;
--card: #2a2722; --card: #2a2722;
@@ -74,10 +72,11 @@
color: #555; color: #555;
text-align: right; text-align: right;
line-height: 1.6; line-height: 1.6;
margin-left: auto;
} }
/* ── THEME TOGGLE BUTTON ──────────────────────────────────────── /* ── 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 Slightly darker chip vs the cream top-bar bg so it reads as
a distinct control, not noise. Works on both theme top-bars. a distinct control, not noise. Works on both theme top-bars.
─────────────────────────────────────────────────────────────── */ ─────────────────────────────────────────────────────────────── */
@@ -94,8 +93,7 @@
color: #3a3632; color: #3a3632;
transition: background 0.15s; transition: background 0.15s;
flex-shrink: 0; flex-shrink: 0;
margin-left: auto; margin-left: 14px;
margin-right: 16px;
} }
.theme-toggle-btn:hover { background: rgba(0, 0, 0, 0.17); } .theme-toggle-btn:hover { background: rgba(0, 0, 0, 0.17); }
.theme-toggle-btn:active { background: rgba(0, 0, 0, 0.23); } .theme-toggle-btn:active { background: rgba(0, 0, 0, 0.23); }
@@ -119,7 +117,7 @@
align-items: start; align-items: start;
} }
.main-col { display: flex; flex-direction: column; gap: 28px; } .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 ───────────────────────────────────────────────── /* ── CLIENT BAR ─────────────────────────────────────────────────
Lives inside .main-col, above section I. Lives inside .main-col, above section I.
@@ -178,6 +176,7 @@
gap: 12px; gap: 12px;
margin-bottom: 32px; margin-bottom: 32px;
} }
.section:not(.sec-open) .section-header { margin-bottom: 0; }
.section-num { .section-num {
font-family: 'Cinzel', serif; font-family: 'Cinzel', serif;
font-weight: 700; font-weight: 700;
@@ -261,6 +260,42 @@
margin-top: 2px; 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) ─────────────────── /* ── PILL TOGGLE (Section II — M365 vs BYOL) ───────────────────
CSS-only toggle using hidden radio inputs + adjacent label styling. CSS-only toggle using hidden radio inputs + adjacent label styling.
input:checked + label gets accent background. input:checked + label gets accent background.
@@ -500,7 +535,6 @@
transition: background 0.12s, border-color 0.12s; transition: background 0.12s, border-color 0.12s;
} }
.addon-row:hover { background: var(--card); } .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-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-name { font-family: 'Lato', sans-serif; font-weight: 700; font-size: 15px; }
.addon-price { .addon-price {
@@ -681,7 +715,6 @@
line-height: 1; line-height: 1;
margin-bottom: 16px; margin-bottom: 16px;
} }
.sidebar-divider { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
.sidebar-note { .sidebar-note {
font-size: 13px; font-size: 13px;
color: var(--muted); color: var(--muted);
@@ -697,19 +730,27 @@
costs MORE than the comparison (rare at low seat counts). costs MORE than the comparison (rare at low seat counts).
updateVsComparison(q) renders this section in update(). updateVsComparison(q) renders this section in update().
─────────────────────────────────────────────────────────────── */ ─────────────────────────────────────────────────────────────── */
.vs-table { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 8px; } .vs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.vs-table td { padding: 7px 4px; } .vs-table td { padding: 8px 6px; vertical-align: middle; }
.vs-table td:last-child { text-align: right; font-family: 'DM Mono', monospace; } .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 { .vs-label {
font-family: 'DM Mono', monospace; font-family: 'DM Mono', monospace;
font-size: 12px; font-size: 10px;
letter-spacing: 0.09em; letter-spacing: 0.10em;
text-transform: uppercase; text-transform: uppercase;
color: var(--muted); color: var(--muted);
margin-top: 18px; margin-bottom: 10px;
margin-bottom: 6px; 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 ─────────────────────────────────────── /* ── INSIGHT NUDGE BANNER ───────────────────────────────────────
Contextual sales insight shown at bottom of sidebar. Contextual sales insight shown at bottom of sidebar.
@@ -730,6 +771,8 @@
font-size: 15px; font-size: 15px;
line-height: 1.7; line-height: 1.7;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
min-height: 130px;
box-sizing: border-box;
} }
.nudge-banner.amber { .nudge-banner.amber {
background: #1f1500; background: #1f1500;
@@ -778,18 +821,52 @@
Right: HST checkbox + one-time fee input. Right: HST checkbox + one-time fee input.
padding-left:96px aligns with section card edges. 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 { .quote-settings-bar {
margin-left: 96px; margin-left: 96px;
padding: 20px 28px 24px 28px; padding: 18px 24px 20px 24px;
display: flex; display: flex;
align-items: flex-start; align-items: stretch;
gap: 32px; gap: 0;
flex-wrap: wrap;
background: var(--card); background: var(--card);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 12px; 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 { .qs-label {
font-family: 'DM Mono', monospace; font-family: 'DM Mono', monospace;
font-size: 11px; font-size: 11px;
@@ -801,12 +878,65 @@
.qs-term-wrap .tier-seg { padding: 10px 8px; } .qs-term-wrap .tier-seg { padding: 10px 8px; }
.qs-term-wrap .tier-name { font-size: 12px; } .qs-term-wrap .tier-name { font-size: 12px; }
.qs-term-wrap .tier-price { display: none; } .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 { .qs-right {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
padding-top: 19px; padding: 0 0 0 24px;
justify-content: center;
} }
/* ── Custom toggle switch (replaces native checkbox for Waive) ── */
.qs-toggle-row { .qs-toggle-row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -814,7 +944,33 @@
cursor: pointer; cursor: pointer;
user-select: none; 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 { .qs-toggle-label {
font-family: 'DM Mono', monospace; font-family: 'DM Mono', monospace;
font-size: 13px; font-size: 13px;
@@ -832,8 +988,6 @@
gap: 10px; gap: 10px;
} }
.qs-fee-waive { margin-left: 4px; } .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 { .qs-fee-label {
font-family: 'DM Mono', monospace; font-family: 'DM Mono', monospace;
font-size: 13px; font-size: 13px;
@@ -863,8 +1017,8 @@
color: var(--ink); color: var(--ink);
font-family: 'DM Mono', monospace; font-family: 'DM Mono', monospace;
font-size: 14px; font-size: 14px;
width: 90px; width: 120px;
text-align: right; text-align: center;
padding: 6px 10px; padding: 6px 10px;
outline: none; outline: none;
} }
@@ -892,19 +1046,36 @@
.sidebar-note-mono { font-size: 12px; padding: 2px 0 6px; font-family: 'DM Mono', monospace; } .sidebar-note-mono { font-size: 12px; padding: 2px 0 6px; font-family: 'DM Mono', monospace; }
/* VS Comparison block */ /* VS Comparison block */
.vs-comparison-wrap { margin-top: 16px; border-top: 1px solid var(--border); padding-top: 14px; } .vs-comparison-wrap {
.vs-inline-icon { margin-right: 5px; vertical-align: middle; } margin-top: 16px;
.vs-svs-label { font-size: 15px; color: var(--ink); font-weight: 600; } margin-bottom: 15px;
.vs-val-accent { color: var(--accent); font-weight: 600; } padding: 24px 24px 22px;
.vs-td-muted { color: var(--muted); } background: rgba(255, 255, 255, 0.04);
.vs-td-icon { margin-right: 5px; opacity: 0.6; vertical-align: middle; } border: 1px solid var(--border);
.vs-footnote { font-size: 13px; color: var(--muted); margin-top: 8px; line-height: 1.6; text-align: center; } 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 */ /* Side note icons and savings highlight */
.note-icon { margin-right: 6px; vertical-align: middle; flex-shrink: 0; } .note-icon { margin-right: 6px; vertical-align: middle; flex-shrink: 0; }
.savings-amount { color: var(--green); } .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 .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 banner internal flex rows */
.nudge-header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; } .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-green — green text for savings value/label
.vs-val-amber — amber text for "costs more" value/label .vs-val-amber — amber text for "costs more" value/label
─────────────────────────────────────────────────────────────── */ ─────────────────────────────────────────────────────────────── */
.vs-save-green { background: #1a2e20; } .vs-save-green td { background: rgba(39, 174, 96, 0.13); }
.vs-save-amber { background: #2e1e0a; } .vs-save-amber td { background: rgba(210, 120, 30, 0.13); }
.vs-val-green { color: var(--green) !important; } .vs-val-green { color: var(--green) !important; }
.vs-val-amber { color: var(--amber) !important; } .vs-val-amber { color: var(--amber) !important; }
@@ -1147,7 +1318,10 @@
.section { margin-left: 64px; } .section { margin-left: 64px; }
.section-num { left: -64px; width: 56px; font-size: 52px; } .section-num { left: -64px; width: 56px; font-size: 52px; }
.client-bar { padding: 24px 0 24px 64px; } .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; } .top-bar-logo { margin-left: 38px; }
.pitch-inner { margin-left: 0; } .pitch-inner { margin-left: 0; }
@@ -1155,6 +1329,24 @@
.pitch-item:nth-child(2) { border-right: none; } .pitch-item:nth-child(2) { border-right: none; }
.pitch-item:nth-child(3) { border-top: 1px solid var(--border); } .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; } .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) ── */ /* ── SMALL TABLET / LANDSCAPE PHONE (≤ 900px) ── */
@@ -1166,7 +1358,10 @@
.section-num { left: -52px; width: 44px; font-size: 42px; top: 24px; } .section-num { left: -52px; width: 44px; font-size: 42px; top: 24px; }
.top-bar-logo { margin-left: 26px; } .top-bar-logo { margin-left: 26px; }
.client-bar { padding: 20px 0 20px 52px; } .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-wrap { padding: 0; }
.pitch-inner { margin-left: 0; } .pitch-inner { margin-left: 0; }
@@ -1177,8 +1372,8 @@
margin-bottom: 24px; margin-bottom: 24px;
} }
.section-title-block { order: 2; flex: 0 0 100%; } .section-title-block { order: 2; flex: 0 0 100%; }
.sec-summary-badge { order: 1; margin-left: auto; } .sec-summary-badge { order: 1; }
.sec-chevron { order: 1; } .sec-chevron { order: 1; margin-left: auto; }
.section-title { font-size: 20px; } .section-title { font-size: 20px; }
.main-col { gap: 20px; } .main-col { gap: 20px; }
@@ -1271,9 +1466,12 @@
.client-bar { padding: 20px 0 20px 0; } .client-bar { padding: 20px 0 20px 0; }
.client-input { font-size: 22px; max-width: 100%; } .client-input { font-size: 22px; max-width: 100%; }
.quote-settings-bar { margin-left: 0; padding: 14px 16px 16px 16px; flex-direction: column; gap: 12px; } .quote-settings-bar { margin-left: 0; padding: 14px 16px 16px; flex-direction: column; gap: 0; }
.qs-group { min-width: 0; } .sections-toolbar { margin-left: 0; }
.qs-right { padding-top: 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 { gap: 16px; }
.main-col > .section:first-of-type { margin-top: 8px; } .main-col > .section:first-of-type { margin-top: 8px; }
@@ -1300,8 +1498,9 @@
.num-input { width: 100%; font-size: 20px; padding: 12px; flex: 1; } .num-input { width: 100%; font-size: 20px; padding: 12px; flex: 1; }
.step-btn { width: 48px; font-size: 22px; } .step-btn { width: 48px; font-size: 22px; }
/* Addon rows — tighter */ /* Addon rows — tighter font on phones (grid inherited from ≤1100px) */
.addon-row { padding: 12px 10px; gap: 10px; } .addon-row { padding: 12px 10px; }
.addon-price { font-size: 13px; }
.addon-name { font-size: 14px; } .addon-name { font-size: 14px; }
.addon-desc { font-size: 12px; } .addon-desc { font-size: 12px; }
@@ -1393,7 +1592,7 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
position: fixed; position: fixed;
top: 62px; top: 82px;
right: 14px; right: 14px;
z-index: 200; z-index: 200;
background: var(--accent); background: var(--accent);
@@ -1579,7 +1778,7 @@
.nudge-banner, .nudge-banner,
.pitch-wrap, .pitch-wrap,
.quote-settings-bar, .quote-settings-bar,
.section-header .section-toggle { pointer-events: none; } .section-header.section-toggle { pointer-events: none; }
.mobile-quote-pill { display: none !important; } .mobile-quote-pill { display: none !important; }
.mobile-quote-panel { display: none !important; } .mobile-quote-panel { display: none !important; }
@@ -1591,6 +1790,7 @@
.collapsible-header { display: none !important; } .collapsible-header { display: none !important; }
.sec-chevron { display: none !important; } .sec-chevron { display: none !important; }
.sec-summary-badge { display: none !important; } .sec-summary-badge { display: none !important; }
.sec-collapsed-counter { display: none !important; }
.quote-settings-bar { display: none !important; } .quote-settings-bar { display: none !important; }
.section-badge { display: none !important; } .section-badge { display: none !important; }
#savingsPrompt { display: none !important; } #savingsPrompt { display: none !important; }
@@ -1646,7 +1846,8 @@
.sidebar-line .val { color: #1a1a1a !important; } .sidebar-line .val { color: #1a1a1a !important; }
/* ── VS comparison: clean for print ── */ /* ── 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; } .vs-save-amber { background: #fff8e1 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* ── Feature cards: minimal ── */ /* ── Feature cards: minimal ── */

View File

@@ -61,6 +61,8 @@
</div> </div>
<div class="sidebar-body"> <div class="sidebar-body">
<div id="sidebarLines_m"> <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"> <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><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> <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><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> <span class="val" id="sl-admin-val_m">$150</span>
</div> </div>
<div class="sl-sub" id="sl-admin-sub_m"></div>
</div> </div>
<!-- Discount line — hidden when no term discount --> <!-- Discount line — hidden when no term discount -->
@@ -131,15 +132,10 @@
<span class="val" id="annualDisplay_m">$1,800</span> <span class="val" id="annualDisplay_m">$1,800</span>
</div> </div>
<div class="sidebar-line" id="perUserRow_m" style="display:none;"> <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> <span class="val" id="perUserDisplay_m"></span>
</div> </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 --> <!-- VS Hiring In-House -->
<div id="vsComparison_m" class="hidden vs-comparison-wrap"> <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="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"/> <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> </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"> <div class="top-bar-right">
<span id="quoteRef">SVS-00000000-0000</span><br> <span id="quoteRef">SVS-00000000-0000</span><br>
<span id="headerDate"></span> <span id="headerDate"></span>
</div> </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> </div>
</header> </header>
@@ -246,21 +242,27 @@
<input type="radio" name="contractTerm" id="term12mo" value="12mo" onchange="update()"> <input type="radio" name="contractTerm" id="term12mo" value="12mo" onchange="update()">
<label for="term12mo" class="tier-seg" id="seg-term-12mo"> <label for="term12mo" class="tier-seg" id="seg-term-12mo">
<div class="tier-name">12-Month</div> <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> </label>
<input type="radio" name="contractTerm" id="term24mo" value="24mo" onchange="update()"> <input type="radio" name="contractTerm" id="term24mo" value="24mo" onchange="update()">
<label for="term24mo" class="tier-seg" id="seg-term-24mo"> <label for="term24mo" class="tier-seg" id="seg-term-24mo">
<div class="tier-name">24-Month</div> <div class="tier-name">24-Month <span class="qs-best-badge">Best Value</span></div>
<div class="tier-sub">5% off MRR</div> <div class="tier-sub qs-discount-sub">5% off MRR</div>
</label> </label>
</div> </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>
<div class="qs-divider"></div>
<div class="qs-right"> <div class="qs-right">
<div class="qs-fee-row"> <div class="qs-fee-row">
<div class="qs-fee-header"> <div class="qs-fee-header">
<label class="qs-fee-label" for="oneTimeFee">Onboarding Fee</label> <label class="qs-fee-label" for="oneTimeFee">Onboarding Fee</label>
<label class="qs-toggle-row qs-fee-waive"> <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();"> <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> <span class="qs-toggle-label">Waive</span>
</label> </label>
</div> </div>
@@ -272,6 +274,10 @@
</div> </div>
</div> </div>
<div class="sections-toolbar">
<button class="btn-toggle-all" id="toggleAllBtn" onclick="toggleAllSections()">Collapse All</button>
</div>
<!-- ──────────────────────────────────────────────────────────── <!-- ────────────────────────────────────────────────────────────
SECTION I — SITE ADMIN FEE SECTION I — SITE ADMIN FEE
id="sec-01" sec-open = starts expanded id="sec-01" sec-open = starts expanded
@@ -364,6 +370,10 @@
<div class="section-title">User Package</div> <div class="section-title">User Package</div>
<div class="section-subtitle">Per-user monthly services — identity, email, security &amp; helpdesk</div> <div class="section-subtitle">Per-user monthly services — identity, email, security &amp; helpdesk</div>
<span class="section-badge">Per User / Month</span> <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> </div>
<span id="sec02-summary" class="sec-summary-badge"></span> <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> <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-title">Endpoint Package</div>
<div class="section-subtitle">Per-device managed protection — workstations &amp; laptops</div> <div class="section-subtitle">Per-device managed protection — workstations &amp; laptops</div>
<span class="section-badge">$35 / Endpoint / Month</span> <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> </div>
<span id="sec03-summary" class="sec-summary-badge"></span> <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> <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-title">Server Management</div>
<div class="section-subtitle">Dedicated management for physical &amp; virtual servers</div> <div class="section-subtitle">Dedicated management for physical &amp; virtual servers</div>
<span class="section-badge">$120 / Server / Month</span> <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> </div>
<span id="sec04-summary" class="sec-summary-badge"></span> <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> <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. toggled via style.display not .hidden class.
──────────────────────────────────────────────────────────── --> ──────────────────────────────────────────────────────────── -->
<div id="sidebarLines"> <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"> <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><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> <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><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> <span class="val" id="sl-admin-val">$150</span>
</div> </div>
<div class="sl-sub" id="sl-admin-sub"></div>
</div> </div>
<!-- Discount line — hidden when no term discount --> <!-- Discount line — hidden when no term discount -->
@@ -832,15 +851,9 @@
<span class="val" id="annualDisplay">$1,800</span> <span class="val" id="annualDisplay">$1,800</span>
</div> </div>
<div class="sidebar-line" id="perUserRow" style="display:none;"> <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> <span class="val" id="perUserDisplay"></span>
</div> </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 ───────────────────────────────────── <!-- ── VS HIRING IN-HOUSE ─────────────────────────────────────
Hidden (.hidden) until users>0 OR endpoints>0. Hidden (.hidden) until users>0 OR endpoints>0.

View File

@@ -1,26 +1,97 @@
// ── PRICING CONSTANTS ─────────────────────────────────────────── // ── PRICING VARIABLES ───────────────────────────────────────────
// All rates in CAD. Edit here — calcQuote() reads these only. // 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_FLOOR = minimum site admin fee regardless of seat count
// ADMIN_FEE_MINIMUM = engagement threshold; admin = max(FLOOR, MIN-subtotal) // ADMIN_FEE_MINIMUM = engagement threshold; admin = max(FLOOR, MIN-subtotal)
// ADMIN_FEE_ZT = supplement added to admin when ZT is active // ADMIN_FEE_ZT = supplement added to admin when ZT is active
// TOOL_COST_* = internal cost estimates (VS comparison only, not billed) // TOOL_COST_* = internal cost estimates (VS comparison only, not billed)
// IT_SALARY_1/5 = Ottawa benchmark salaries for VS comparison // IT_SALARY_1/5 = Ottawa benchmark salaries for VS comparison
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
const ADMIN_FEE_FLOOR = 150; let RATE_M365 = 130;
const ADMIN_FEE_MINIMUM = 650; let RATE_BYOL = 110;
const ADMIN_FEE_ZT = 250; let ADDON_EXT_HOURS = 25;
const VOIP_RATES = { basic: 28, standard: 35, premium: 45 }; let ADDON_1PASSWORD = 9;
const VOIP_PHONE_RATE = 15; let ADDON_INKY = 5;
const VOIP_FAX_RATE = 10; let ADDON_ZERO_TRUST_USER= 55;
const TOOL_COST_PER_USER = 42; let RATE_ENDPOINT = 35;
const TOOL_COST_PER_ENDPOINT = 23; let RATE_SERVER = 120;
const TOOL_COST_MIN = 650; let ADDON_USB_BLOCKING = 4;
const IT_SALARY_1 = 85000; let ADDON_BARE_METAL_BACKUP = 25;
const IT_SALARY_5 = 420000; 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 // CONTRACT_DISCOUNT: discount applied to MRR based on contract length
// m2m = month-to-month (no discount), 12mo = 3% off, 24mo = 5% off // m2m = month-to-month (no discount), 12mo = 3% off, 24mo = 5% off
const CONTRACT_DISCOUNT = { 'm2m': 0, '12mo': 0.03, '24mo': 0.05 }; let DISCOUNT_M2M = 0;
const HST_RATE = 0.13; // Ontario HST 13% 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 --- // --- CALC ---
// ── calcQuote() ───────────────────────────────────────────────── // ── calcQuote() ─────────────────────────────────────────────────
@@ -51,33 +122,36 @@ function calcQuote() {
const oneTimeFee = parseFloat(document.getElementById('oneTimeFee')?.value) || 0; const oneTimeFee = parseFloat(document.getElementById('oneTimeFee')?.value) || 0;
const ztActive = addZT || ztSeats > 0; const ztActive = addZT || ztSeats > 0;
const baseUserRate = byol ? 110 : 130; const VOIP_RATES = { basic: VOIP_RATE_BASIC, standard: VOIP_RATE_STANDARD, premium: VOIP_RATE_PREMIUM };
const addonRate = (addExtHours ? 25 : 0) + (addPWM ? 9 : 0) + (addINKY ? 5 : 0) + (addZT ? 55 : 0); 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 totalUserRate= baseUserRate + addonRate;
const userBase = users * baseUserRate; const userBase = users * baseUserRate;
const userExt = addExtHours ? users * 25 : 0; const userExt = addExtHours ? users * ADDON_EXT_HOURS : 0;
const userPWM = addPWM ? users * 9 : 0; const userPWM = addPWM ? users * ADDON_1PASSWORD : 0;
const userINKY = addINKY ? users * 5 : 0; const userINKY = addINKY ? users * ADDON_INKY : 0;
const userZT = addZT ? users * 55 : 0; const userZT = addZT ? users * ADDON_ZERO_TRUST_USER : 0;
const userTotal = userBase + userExt + userPWM + userINKY + userZT; const userTotal = userBase + userExt + userPWM + userINKY + userZT;
const endpointBase = endpoints * 35; const endpointBase = endpoints * RATE_ENDPOINT;
const serverBase = servers * 120; const serverBase = servers * RATE_SERVER;
const endpointUSB = addUSB ? endpoints * 4 : 0; const endpointUSB = addUSB ? endpoints * ADDON_USB_BLOCKING : 0;
const endpointBMB = addBMB ? endpoints * 25 : 0; const endpointBMB = addBMB ? endpoints * ADDON_BARE_METAL_BACKUP : 0;
const endpointTotal= endpointBase + serverBase + endpointUSB + endpointBMB; const endpointTotal= endpointBase + serverBase + endpointUSB + endpointBMB;
const baseSubtotal = userBase + endpointBase + serverBase; const baseSubtotal = userBase + endpointBase + serverBase;
const siteAdminBase = Math.max(ADMIN_FEE_FLOOR, ADMIN_FEE_MINIMUM - baseSubtotal); 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 adminFeeNet = siteAdminBase + (ztActive ? ADMIN_FEE_ZT : 0) + admin1PWM;
const ztNetSeats = ztSeats * 25; const ztNetSeats = ztSeats * ZT_SEAT_RATE;
const ztNetRouters = ztRouters * 100; const ztNetRouters = ztRouters * ZT_ROUTER_RATE;
const ztNetTotal = ztNetSeats + ztNetRouters; const ztNetTotal = ztNetSeats + ztNetRouters;
const voipSeatRate = VOIP_RATES[voipTier] || 28; const voipSeatRate = VOIP_RATES[voipTier] || VOIP_RATE_BASIC;
const voipSeatsAmt = voipSeats * voipSeatRate; const voipSeatsAmt = voipSeats * voipSeatRate;
const voipPhoneAmt = addVoipPhone ? voipSeats * VOIP_PHONE_RATE : 0; const voipPhoneAmt = addVoipPhone ? voipSeats * VOIP_PHONE_RATE : 0;
const voipFaxAmt = addVoipFax ? VOIP_FAX_RATE : 0; const voipFaxAmt = addVoipFax ? VOIP_FAX_RATE : 0;
@@ -149,9 +223,9 @@ function update() {
let oneTimeFee; let oneTimeFee;
if (waived) { if (waived) {
oneTimeFee = 0; oneTimeFee = 0;
if (feeEl) { feeEl.value = ''; feeEl.disabled = true; } if (feeEl) { feeEl.value = ''; feeEl.disabled = true; feeEl.placeholder = 'Complimentary'; }
} else { } else {
if (feeEl) feeEl.disabled = false; if (feeEl) { feeEl.disabled = false; feeEl.placeholder = 'auto'; }
if (feeEl && !feeEl.dataset.manual) { if (feeEl && !feeEl.dataset.manual) {
oneTimeFee = Math.round(q.MRR / 2); oneTimeFee = Math.round(q.MRR / 2);
feeEl.value = oneTimeFee > 0 ? oneTimeFee : ''; feeEl.value = oneTimeFee > 0 ? oneTimeFee : '';
@@ -284,7 +358,7 @@ function update() {
if (_waived && _wouldBe > 0) { if (_waived && _wouldBe > 0) {
row.classList.remove('hidden'); row.classList.remove('hidden');
row.classList.add('sl-otf-waived'); 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) { } else if (oneTimeFee > 0) {
row.classList.remove('hidden', 'sl-otf-waived'); row.classList.remove('hidden', 'sl-otf-waived');
if (valEl) valEl.textContent = fmt(oneTimeFee) + ' (one-time)'; if (valEl) valEl.textContent = fmt(oneTimeFee) + ' (one-time)';
@@ -304,7 +378,6 @@ function update() {
} }
// Sidebar notes // Sidebar notes
getEl('adminPct').textContent = MRR > 0 ? Math.round(adminFeeNet / MRR * 100) : '—';
getEl('sideNote-m365').classList.toggle('hidden', byol); getEl('sideNote-m365').classList.toggle('hidden', byol);
getEl('sideNote-byol').classList.toggle('hidden', !byol); getEl('sideNote-byol').classList.toggle('hidden', !byol);
if (!byol && users > 0) getEl('m365SaveAmt').textContent = fmt(users * 15); if (!byol && users > 0) getEl('m365SaveAmt').textContent = fmt(users * 15);
@@ -326,6 +399,18 @@ function update() {
if (seg) seg.classList.toggle('active', t === contractTerm); 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 // Nudges — dynamic dollar values, context-sensitive conditions
const nudges = []; const nudges = [];
if (!addZT && users > 0) nudges.push({ if (!addZT && users > 0) nudges.push({
@@ -345,7 +430,7 @@ function update() {
color: 'amber' color: 'amber'
}); });
if (voipSeats > 0 && voipTier === 'basic') nudges.push({ 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' color: 'green'
}); });
if (servers > 0 && !addBMB) nudges.push({ if (servers > 0 && !addBMB) nudges.push({
@@ -357,7 +442,7 @@ function update() {
color: 'amber' color: 'amber'
}); });
window._nudges = nudges; 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(); renderNudge();
updateSavings(q); updateSavings(q);
@@ -378,6 +463,42 @@ function toggleSection(id) {
const isOpen = section.classList.toggle('sec-open'); const isOpen = section.classList.toggle('sec-open');
body.style.display = isOpen ? '' : 'none'; body.style.display = isOpen ? '' : 'none';
updateSectionSummaries(); 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) ──────────────────────────────────── // ── updateSectionSummaries(q) ────────────────────────────────────
@@ -574,6 +695,7 @@ function updateSavings(q) {
const saving = bill - voipTotal; const saving = bill - voipTotal;
if (saving > 0) { 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.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 { } 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.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)'; comparator.style.color = 'var(--amber)';
@@ -688,78 +810,6 @@ function restoreState() {
} catch(e) { return false; } } 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() ──────────────────────────────────────────────── // ── printInvoice() ────────────────────────────────────────────────
// Generates a clean invoice-style HTML document in a new window // Generates a clean invoice-style HTML document in a new window
// and triggers the browser print dialog (Save as PDF works perfectly). // and triggers the browser print dialog (Save as PDF works perfectly).
@@ -803,7 +853,7 @@ function printInvoice() {
} }
if (q.voipTotal > 0) { if (q.voipTotal > 0) {
const tier = {basic:'Basic',standard:'Standard',premium:'Premium'}[q.voipTier] || 'Basic'; 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.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); 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, // Entry point. Sets quote ref (SVS-YYYYMMDD-XXXX) and date,
// restores saved state, then calls update() for initial render. // restores saved state, then calls update() for initial render.
// Called once at bottom of script. // Called once at bottom of script.
function initQuote() { async function initQuote() {
await loadPricing();
const now = new Date(); const now = new Date();
const months = ['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER']; const months = ['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER'];
const year = now.getFullYear(); const year = now.getFullYear();
@@ -1135,7 +1186,6 @@ initQuote();
syncEl('annualDisplay'); syncEl('annualDisplay');
syncEl('perUserDisplay'); syncEl('perUserDisplay');
syncEl('perUserBreakdown'); syncEl('perUserBreakdown');
syncEl('adminPct');
syncEl('m365SaveAmt'); syncEl('m365SaveAmt');
syncEl('sl-discount-val'); syncEl('sl-discount-val');
syncEl('sl-base-mrr-val'); syncEl('sl-base-mrr-val');
@@ -1162,7 +1212,6 @@ initQuote();
syncClass('sl-zt'); syncClass('sl-zt');
syncClass('sl-voip'); syncClass('sl-voip');
syncClass('sl-admin'); syncClass('sl-admin');
syncClass('sideNote-admin');
syncClass('sideNote-m365'); syncClass('sideNote-m365');
syncClass('sideNote-byol'); syncClass('sideNote-byol');
syncClass('vsComparison'); syncClass('vsComparison');
@@ -1173,9 +1222,6 @@ initQuote();
syncClass('sl-hst-row'); syncClass('sl-hst-row');
syncClass('sl-hst-total-row'); syncClass('sl-hst-total-row');
syncClass('sl-otf-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-row');
syncClass('vs-1man-save'); syncClass('vs-1man-save');
syncClass('vs-1man-save-lbl'); 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)