My very First own bigboy config
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 ── */
|
||||
|
||||
@@ -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 & 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)">−</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 & 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)">−</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 & 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)">−</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.
|
||||
|
||||
@@ -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
31
package-prices.csv
Normal 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)
|
||||
|
Reference in New Issue
Block a user