From 591e4155fc469f1b68b3e37111591c51f85d1f08 Mon Sep 17 00:00:00 2001 From: John OReilly Date: Fri, 13 Mar 2026 16:05:38 -0400 Subject: [PATCH] Glas Theme Coming Next --- SVS-MSP-Calculator-light.css | 106 ++++++++++++++++++--- SVS-MSP-Calculator.css | 177 +++++++++++++++++++++++++++++++---- SVS-MSP-Calculator.html | 91 ++++++++++++------ SVS-MSP-Calculator.js | 87 +++++++++++++---- 4 files changed, 388 insertions(+), 73 deletions(-) diff --git a/SVS-MSP-Calculator-light.css b/SVS-MSP-Calculator-light.css index 81e8fde..109c07e 100644 --- a/SVS-MSP-Calculator-light.css +++ b/SVS-MSP-Calculator-light.css @@ -8,11 +8,11 @@ /* ── DESIGN TOKENS (light overrides) ───────────────────────────── */ :root { --ink: #2c2825; /* soft near-black — readable without harshness */ - --paper: #f4f2ed; /* warm off-white page background */ + --paper: #ece7dc; /* warm sand/khaki page tone */ --accent: #1a6a98; /* blue — good for buttons, kept full strength */ --muted: #6b6360; /* mid-grey for secondary text */ - --border: #d0cdc7; /* soft warm-grey borders */ - --card: #ebe8e2; /* off-white card background */ + --border: #d2cbc0; /* warm border to match khaki palette */ + --card: #f1ebdf; /* general light-mode utility surface */ --green: #217045; /* darker green — accessible on white */ --amber: #a05f00; /* darker amber — accessible on white */ } @@ -29,13 +29,71 @@ body { to keep the SVG logo (fill="#0c0c0c" text) legible. ─────────────────────────────────────────────────────────────────── */ .top-bar { - background: #e8e4db !important; + background: #e3ddd2 !important; border-bottom-color: rgba(0, 0, 0, 0.09) !important; /* replace strong blue stripe with soft warm separator */ } /* ── SECTION CARDS ───────────────────────────────────────────────── */ .section { - background: #faf9f5 !important; /* warm white — less stark than pure white on cream background */ + background: #f5f2ea !important; /* warm cream — floats above khaki paper */ +} + +/* ── SPLIT LIGHT-MODE SURFACES ───────────────────────────────────── + Keep sections as the main content cards. + Quote settings = warmer utility panel. + Live quote = cooler summary panel, including the mobile sheet. +*/ +.quote-settings-bar { + background: #ebe1d2 !important; + border-color: #d8cab8 !important; +} +.qs-divider { + background: #d6c8b7 !important; +} +.qs-fee-input-wrap, +.qs-fee-dollar, +.qs-fee-input { + background: #f6f0e6 !important; +} +.sidebar { + background: #edf3f6 !important; + border-color: #cfdae2 !important; +} +.sidebar-utility .btn-reset-quote, +.mobile-panel-actions .btn-reset-quote { + background: #edf3f6 !important; + border-color: #cfdae2 !important; +} +.sidebar-body { + background: #f3f7f9 !important; +} +.export-wrap { + background: #edf3f6 !important; + border-top: 1px solid #d8e2e8 !important; +} +.mobile-panel-sheet { + background: #edf3f6 !important; + border-top-color: #cfdae2 !important; +} +.mobile-panel-close-row { + background: #edf3f6 !important; + border-bottom-color: #d8e2e8 !important; +} +.mobile-panel-actions { + background: #edf3f6 !important; + border-bottom-color: #d8e2e8 !important; +} +.mobile-panel-sheet .sidebar { + background: transparent !important; +} +.mobile-panel-sheet .sidebar-body { + background: #f3f7f9 !important; +} +.confirm-modal-card { + background: #f5f2ea !important; +} +.confirm-btn-secondary:hover { + background: rgba(0, 0, 0, 0.04) !important; } /* ── CHEVRON PILL — swap white-alpha tint for dark-alpha ─────────── */ @@ -73,12 +131,12 @@ body { /* Dark mode steppers blend in naturally. Light mode needs explicit lift: white surfaces stand off the warm card bg; accent symbols tie to brand. */ .step-btn { - background: #ffffff !important; - border-color: #b8b4ae !important; /* stronger than --border #d0cdc7 for crispness */ + background: #faf8f3 !important; /* soft cream — not stark white */ + border-color: #b8b4ae !important; /* stronger than --border for crispness */ color: var(--accent) !important; /* accent blue +/- symbols instead of muted grey */ } .step-btn:hover { - background: #f0ede7 !important; /* warm tint matches section card on hover */ + background: #ede8de !important; /* warm tint matches khaki family on hover */ border-color: var(--accent) !important; color: var(--accent) !important; } @@ -88,7 +146,7 @@ body { color: #fff !important; } .num-input { - background: #ffffff !important; + background: #faf8f3 !important; /* soft cream — matches step buttons */ border-color: #b8b4ae !important; } .num-input:focus { @@ -98,7 +156,7 @@ body { /* ── ADDON ROW SELECTED ──────────────────────────────────────────── */ .addon-row.selected { - background: #dff0fb !important; + background: #daedf8 !important; border-color: var(--accent) !important; box-shadow: inset 3px 0 0 0 var(--accent) !important; } @@ -111,7 +169,28 @@ body { /* ── FEATURE CARDS ───────────────────────────────────────────────── */ .feature-card { - background: #f0ede7 !important; + background: #e9e4da !important; +} + +/* ── SIDEBAR / SUMMARY TEXT ──────────────────────────────────────── + Base dark theme hardcodes a few bright values for money and labels. + In light mode those need to return to dark text so the desktop + sidebar and the responsive mobile sheet remain readable. +*/ +.sidebar-line .val { + color: var(--ink) !important; +} +.sidebar-mrr { + color: var(--ink) !important; +} +.vs-svs-label { + color: var(--ink) !important; +} +.vs-val-accent { + color: var(--accent) !important; +} +.vs-td-muted { + color: var(--muted) !important; } /* ── NUDGE BANNER ────────────────────────────────────────────────── */ @@ -131,7 +210,10 @@ body { } /* ── VS COMPARISON ───────────────────────────────────────────────── */ -.vs-comparison-wrap { background: rgba(0, 0, 0, 0.025) !important; } +.vs-comparison-wrap { + background: #e5edf2 !important; + border-color: #cfdae2 !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; } diff --git a/SVS-MSP-Calculator.css b/SVS-MSP-Calculator.css index 5d1d712..88ea41b 100644 --- a/SVS-MSP-Calculator.css +++ b/SVS-MSP-Calculator.css @@ -23,12 +23,12 @@ Single source of truth for all colours. Edit here, not inline. ─────────────────────────────────────────────────────────────── */ :root { - --ink: #ddd8d0; - --paper: #22201d; - --accent: #2d7aa8; - --muted: #b0a898; /* lifted slightly — #a09890 was too faint on dark cards */ - --border: #3a3630; - --card: #2a2722; + --ink: #e8e3da; /* warm beige-white — brighter for legibility */ + --paper: #1c1a17; /* darker base — widens gap vs card for panel float */ + --accent: #3d8aba; /* lifted blue — pops on dark backgrounds */ + --muted: #9e9588; /* softer secondary — clearly subordinate but readable */ + --border: #35322c; /* subtler dividers */ + --card: #272420; /* elevated surface — clear separation from paper */ --green: #3ab870; --amber: #e8920f; } @@ -118,6 +118,110 @@ } .main-col { display: flex; flex-direction: column; gap: 28px; } .side-col { position: sticky; top: 102px; z-index: 10; align-self: start; } + .sidebar-utility { margin-bottom: 12px; } + .btn-reset-quote { + width: 100%; + background: var(--card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 11px 14px; + color: var(--muted); + font-family: 'DM Mono', monospace; + font-size: 12px; + letter-spacing: 0.09em; + text-transform: uppercase; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.1s; + } + .btn-reset-quote:hover { + background: rgba(232, 146, 15, 0.08); + border-color: rgba(232, 146, 15, 0.38); + color: var(--amber); + } + .btn-reset-quote:active { transform: translateY(1px); } + + .confirm-modal { + position: fixed; + inset: 0; + z-index: 400; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; + } + .confirm-modal.open { + opacity: 1; + pointer-events: auto; + } + .confirm-modal-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.62); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + } + .confirm-modal-card { + position: relative; + width: min(460px, calc(100% - 32px)); + margin: 12vh auto 0; + background: var(--card); + border: 1px solid var(--border); + border-radius: 14px; + padding: 22px 22px 20px; + box-shadow: 0 16px 50px rgba(0,0,0,0.35); + } + .confirm-modal-eyebrow { + font-family: 'DM Mono', monospace; + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--amber); + margin-bottom: 10px; + } + .confirm-modal-title { + font-family: 'Poppins', sans-serif; + font-size: 24px; + line-height: 1.3; + color: var(--ink); + margin-bottom: 10px; + } + .confirm-modal-copy { + font-size: 14px; + line-height: 1.7; + color: var(--muted); + margin-bottom: 18px; + } + .confirm-modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + } + .confirm-btn { + border-radius: 8px; + padding: 11px 14px; + font-family: 'DM Mono', monospace; + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.1s; + } + .confirm-btn:active { transform: translateY(1px); } + .confirm-btn-secondary { + background: transparent; + color: var(--muted); + border: 1px solid var(--border); + } + .confirm-btn-secondary:hover { + background: rgba(255,255,255,0.05); + color: var(--ink); + border-color: var(--accent); + } + .confirm-btn-danger { + background: var(--amber); + color: #fff; + border: 1px solid transparent; + } + .confirm-btn-danger:hover { filter: brightness(1.05); } /* ── CLIENT BAR ───────────────────────────────────────────────── Lives inside .main-col, above section I. @@ -706,7 +810,7 @@ } .sidebar-line .val { font-family: 'DM Mono', monospace; - color: var(--ink); + color: #f2ede4; /* brighter warm white — money figures pop vs labels */ font-size: 14px; } .sidebar-line .lbl-icon { margin-right: 5px; } @@ -723,7 +827,7 @@ font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 48px; - color: var(--ink); + color: #f5f0e8; /* brightest text — hero MRR number */ line-height: 1; margin-bottom: 16px; } @@ -1065,15 +1169,15 @@ margin-top: 16px; margin-bottom: 15px; padding: 24px 24px 22px; - background: rgba(255, 255, 255, 0.04); + background: rgba(255, 255, 255, 0.06); 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-svs-label { font-size: 14px; color: #f2ede4; font-weight: 600; } + .vs-val-accent { color: #5aaedc; font-weight: 600; font-size: 14px; } + .vs-td-muted { color: #b5ab9e; font-size: 13px; } + .vs-td-icon { margin-right: 5px; opacity: 0.7; vertical-align: middle; } .vs-footnote { font-size: 11px; color: var(--muted); @@ -1135,8 +1239,8 @@ .vs-val-green — green text for savings value/label .vs-val-amber — amber text for "costs more" value/label ─────────────────────────────────────────────────────────────── */ - .vs-save-green td { background: rgba(39, 174, 96, 0.13); } - .vs-save-amber td { background: rgba(210, 120, 30, 0.13); } + .vs-save-green td { background: rgba(39, 174, 96, 0.16); } + .vs-save-amber td { background: rgba(210, 120, 30, 0.16); } .vs-val-green { color: var(--green) !important; } .vs-val-amber { color: var(--amber) !important; } @@ -1160,7 +1264,7 @@ the checkbox's native accent-color. No pseudo-element needed. ─────────────────────────────────────────────────────────────── */ .addon-row.selected { - background: #1a2a38; + background: #1d2d3a; border-color: var(--accent); box-shadow: inset 3px 0 0 0 var(--accent); } @@ -1284,8 +1388,28 @@ border-right: 1px solid var(--border); } .pitch-item:last-child { border-right: none; } - .pitch-icon { font-size: 20px; margin-bottom: 12px; color: var(--accent); } - .pitch-title { font-family: 'Poppins', sans-serif; font-weight: 600; font-size: 16px; margin-bottom: 8px; } + .pitch-head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 10px; + min-height: 28px; + } + .pitch-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + font-size: 20px; + color: var(--accent); + } + .pitch-title { + font-family: 'Poppins', sans-serif; + font-weight: 600; + font-size: 16px; + line-height: 1.3; + margin: 0; + } .pitch-desc { font-size: 14px; color: var(--muted); line-height: 1.7; } .pitch-footer { background: #162e22; /* match green callout family — was #1a2e20 */ @@ -1588,6 +1712,13 @@ /* Nudge banner */ .nudge-banner { padding: 14px 16px; font-size: 14px; } .export-wrap { padding: 16px 16px 20px; } + .confirm-modal-card { + margin-top: 8vh; + padding: 20px 18px 18px; + } + .confirm-modal-title { font-size: 21px; } + .confirm-modal-actions { flex-direction: column-reverse; } + .confirm-btn { width: 100%; } /* Fee table */ .fee-table td { padding: 7px 0; font-size: 13px; } @@ -1626,6 +1757,7 @@ ─────────────────────────────────────────────────────────────── */ .mobile-quote-pill { display: none; } .mobile-quote-panel { display: none; } + .mobile-panel-actions { display: none; } /* ═══════════════════════════════════════ MOBILE QUOTE PILL + FULL-SCREEN PANEL @@ -1751,6 +1883,15 @@ padding: 16px 20px 12px; border-bottom: 1px solid var(--border); } + .mobile-panel-actions { + display: block; + padding: 0 20px 12px; + border-bottom: 1px solid var(--border); + background: var(--card); + } + .mobile-panel-actions .btn-reset-quote { + margin-top: 12px; + } .mobile-panel-close-title { font-family: 'DM Mono', monospace; font-size: 12px; diff --git a/SVS-MSP-Calculator.html b/SVS-MSP-Calculator.html index 774093f..e12cc05 100644 --- a/SVS-MSP-Calculator.html +++ b/SVS-MSP-Calculator.html @@ -51,6 +51,9 @@ Quote Summary +
+ +
@@ -100,6 +103,7 @@ Site Admin Fee $150
+ @@ -344,10 +348,10 @@
- + What's Covered by the Admin Fee
-
+
Tenant & Identity Management
Microsoft 365 / Entra ID tenant administration, user lifecycle, MFA enforcement, and conditional access policies.
Network & Infrastructure Oversight
Firewall configuration reviews, DNS management, VLAN segmentation oversight, and network performance monitoring.
@@ -425,13 +429,19 @@
    -
  • Microsoft 365 Business Premium (M365 tier) — Word, Excel, PowerPoint, Teams, Exchange
  • -
  • Entra ID & MFA — identity protection, conditional access, and SSO
  • -
  • Microsoft Defender for Business — endpoint + email threat protection
  • -
  • Helpdesk support (business hours) — tickets, remote sessions, escalations
  • -
  • Onboarding & offboarding — provisioning, access revocation, equipment checklists
  • -
  • Security awareness training (SAT) — phishing simulations & training modules
  • -
  • User-level documentation — accounts, devices, access tracked per user
  • +
  • Microsoft 365 Business Premium
  • +
  • Word, Excel, PowerPoint, Teams, Exchange
  • +
  • Entra ID / conditional access / SSO
  • +
  • Defender for Business
  • +
  • MFA enforcement
  • +
  • SaaS alerting / geo-location lock
  • +
  • CORK cyber warranty with $100,000 annual coverage
  • +
  • INKY advanced mail protection
  • +
  • Security awareness training and phishing simulation
  • +
  • SaaS backup for business email, OneDrive, and Google Workspace
  • +
  • Dark web ID scanning
  • +
  • SOC account monitoring
  • +
  • Unlimited remote bilingual help desk support (9am-5pm / Mon-Fri)
@@ -443,7 +453,7 @@
@@ -461,7 +471,7 @@
@@ -746,6 +758,9 @@ nudgeBanner must stay INSIDE .sidebar-body or it gets clipped. ──────────────────────────────────────────────────────────────── -->
+ @@ -907,25 +923,33 @@
-
-
-
-
Security-First MSP
+
+
+
+
+
Security-First MSP
+
Every engagement is built on a security baseline — EDR, MFA, patch management, and cyber warranty included.
-
-
Ottawa-Based Team
+
+
+
Ottawa-Based Team
+
Local presence, Canadian data sovereignty, and an account team that knows your business and your region.
-
-
Flat-Rate, No Surprises
+
+
+
Flat-Rate, No Surprises
+
Predictable monthly billing with no per-ticket charges — aligned incentives mean we fix things right the first time.
-
-
Scales With You
+
+
+
Scales With You
+
Add users, endpoints, servers, ZT networking, or VoIP as you grow — one vendor, one invoice, one relationship.
@@ -936,6 +960,19 @@
+ + diff --git a/SVS-MSP-Calculator.js b/SVS-MSP-Calculator.js index 5ef1a41..f0cc86d 100644 --- a/SVS-MSP-Calculator.js +++ b/SVS-MSP-Calculator.js @@ -351,7 +351,7 @@ function update() { const subParts = [`${users} × ${fmt(baseUserRate)}/user (${byol ? 'BYOL' : 'M365 Incl.'})`]; if (addExtHours) subParts.push(`+ ${fmt(userExt)}/mo ext. hrs`); if (addPWM) subParts.push(`+ ${fmt(userPWM)}/mo 1Password`); - if (addINKY) subParts.push(`+ ${fmt(userINKY)}/mo INKY`); + if (addINKY) subParts.push(`+ ${fmt(userINKY)}/mo INKY Pro upgrade`); if (addZT) subParts.push(`+ ${fmt(userZT)}/mo Zero Trust`); sub.innerHTML = subParts.join('
'); } @@ -376,6 +376,7 @@ function update() { if (voipTotal > 0) getEl('sl-voip-val').textContent = fmt(voipTotal); const slAdminEl = getEl('sl-admin'); const slAdminValEl = getEl('sl-admin-val'); + const slAdminSubEl = getEl('sl-admin-sub'); if (adminWaived) { slAdminEl?.classList.add('sl-admin-waived'); if (slAdminValEl) slAdminValEl.innerHTML = @@ -384,6 +385,13 @@ function update() { slAdminEl?.classList.remove('sl-admin-waived'); if (slAdminValEl) slAdminValEl.textContent = fmt(adminFeeNet); } + if (slAdminSubEl) { + const adminParts = [`Base ${fmt(siteAdminBase)}/mo`]; + if (ztActive) adminParts.push(`+ ${fmt(ADMIN_FEE_ZT)}/mo Zero Trust supplement`); + if (addPWM && admin1PWM > 0) adminParts.push(`+ ${fmt(admin1PWM)}/mo 1Password admin`); + slAdminSubEl.classList.remove('hidden'); + slAdminSubEl.innerHTML = adminParts.join('
'); + } // MRR + totals — show effective MRR (after term discount) as the headline number getEl('mrrDisplay').textContent = fmt(effectiveMrr); @@ -546,7 +554,6 @@ function onWaiveToggle() { // Calls updateSectionSummaries() to show/hide summary badges. // Map: section ID → collapsible IDs that should auto-expand when section opens const _sectionCollapsibles = { - 'sec-01': ['adminCovered'], 'sec-02': ['userIncluded', 'addonsA'], 'sec-03': ['endpointIncluded', 'addonsB'], 'sec-04': ['serverIncluded'], @@ -849,6 +856,7 @@ function stepInput(id, delta) { // --- AUTO-SAVE / RESTORE --- const SAVE_KEY = 'svs-msp-quote-v1'; +const QUOTE_REF_KEY = 'svs-msp-quote-ref'; function saveState() { try { @@ -886,6 +894,50 @@ function debouncedSave() { _saveTimer = setTimeout(saveState, 400); } +function syncBodyScrollLock() { + const panelOpen = document.getElementById('mobileQuotePanel')?.classList.contains('open'); + const modalOpen = document.getElementById('resetConfirmModal')?.classList.contains('open'); + document.body.style.overflow = (panelOpen || modalOpen) ? 'hidden' : ''; +} + +function openResetConfirm() { + const modal = document.getElementById('resetConfirmModal'); + if (!modal) return; + modal.classList.add('open'); + modal.setAttribute('aria-hidden', 'false'); + syncBodyScrollLock(); + document.getElementById('resetConfirmCancel')?.focus(); +} + +function closeResetConfirm() { + const modal = document.getElementById('resetConfirmModal'); + if (!modal) return; + modal.classList.remove('open'); + modal.setAttribute('aria-hidden', 'true'); + syncBodyScrollLock(); +} + +function confirmResetQuote() { + clearTimeout(_saveTimer); + try { + localStorage.removeItem(SAVE_KEY); + localStorage.removeItem(QUOTE_REF_KEY); + } catch (e) { + console.warn('confirmResetQuote: failed to clear saved quote state', e); + } + closeResetConfirm(); + window.location.reload(); +} + +document.addEventListener('keydown', function(e) { + const modalOpen = document.getElementById('resetConfirmModal')?.classList.contains('open'); + if (e.key === 'Escape' && modalOpen) { + closeResetConfirm(); + e.preventDefault(); + e.stopImmediatePropagation(); + } +}); + // ── restoreState() ─────────────────────────────────────────────── // Restores form state from localStorage on page load. // SAVE_KEY = 'svs-msp-quote-v1'. @@ -966,7 +1018,7 @@ function printInvoice() { row(`User Package — ${pkg}`, `${q.users} user${q.users!==1?'s':''} × ${fmt(q.baseUserRate)}/mo`, fmt(q.userBase)); if (q.userExt > 0) row(`↳ Extended Hours (+${fmt(ADDON_EXT_HOURS)}/user)`, '', fmt(q.userExt), true); if (q.userPWM > 0) row(`↳ 1Password Business (+${fmt(ADDON_1PASSWORD)}/user)`, '', fmt(q.userPWM), true); - if (q.userINKY > 0) row(`↳ Inky Email Security (+${fmt(ADDON_INKY)}/user)`, '', fmt(q.userINKY), true); + if (q.userINKY > 0) row(`↳ INKY Pro Upgrade (+${fmt(ADDON_INKY)}/user)`, '', fmt(q.userINKY), true); if (q.userZT > 0) row(`↳ Zero Trust User (+${fmt(ADDON_ZERO_TRUST_USER)}/user)`, '', fmt(q.userZT), true); } if (q.endpoints > 0) { @@ -1014,7 +1066,7 @@ function printInvoice() { feat('Licensing Model', true, q.byol ? 'BYOL — Bring Your Own License' : 'M365 Premium Included'); feat('Extended Help Desk Hours', q.addExtHours, q.addExtHours ? `+${fmt(ADDON_EXT_HOURS)}/user/mo` : ''); feat('1Password Business', q.addPWM, q.addPWM ? `+${fmt(ADDON_1PASSWORD)}/user/mo` : ''); - feat('INKY Pro Email Security', q.addINKY, q.addINKY ? `+${fmt(ADDON_INKY)}/user/mo` : ''); + feat('INKY Pro Upgrade', q.addINKY, q.addINKY ? `+${fmt(ADDON_INKY)}/user/mo` : ''); feat('Zero Trust User Access', q.addZT, q.addZT ? `+${fmt(ADDON_ZERO_TRUST_USER)}/user/mo` : ''); feat('USB Device Blocking', q.addUSB, q.addUSB ? `+${fmt(ADDON_USB_BLOCKING)}/endpoint/mo` : ''); feat('Bare Metal Backup', q.addBMB, q.addBMB ? `+${fmt(ADDON_BARE_METAL_BACKUP)}/endpoint/mo` : ''); @@ -1280,22 +1332,22 @@ async function initQuote() { const year = now.getFullYear(); const month = months[now.getMonth()]; const dateStr = `${year}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`; - const savedRef = localStorage.getItem('svs-msp-quote-ref'); + const savedRef = localStorage.getItem(QUOTE_REF_KEY); let quoteRef; if (savedRef) { // Regenerate if the baked-in date is older than 30 days const m = savedRef.match(/^SVS-(\d{4})(\d{2})(\d{2})-/); - const refDate = m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; - const ageMs = refDate ? now - refDate : Infinity; - if (ageMs > 30 * 24 * 60 * 60 * 1000) { - quoteRef = `SVS-${dateStr}-${String(Math.floor(Math.random()*9000)+1000)}`; - localStorage.setItem('svs-msp-quote-ref', quoteRef); - } else { - quoteRef = savedRef; - } + const refDate = m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; + const ageMs = refDate ? now - refDate : Infinity; + if (ageMs > 30 * 24 * 60 * 60 * 1000) { + quoteRef = `SVS-${dateStr}-${String(Math.floor(Math.random()*9000)+1000)}`; + localStorage.setItem(QUOTE_REF_KEY, quoteRef); + } else { + quoteRef = savedRef; + } } else { quoteRef = `SVS-${dateStr}-${String(Math.floor(Math.random()*9000)+1000)}`; - localStorage.setItem('svs-msp-quote-ref', quoteRef); + localStorage.setItem(QUOTE_REF_KEY, quoteRef); } const quoteRefEl = document.getElementById('quoteRef'); if (quoteRefEl) quoteRefEl.textContent = quoteRef; @@ -1341,7 +1393,7 @@ initQuote(); var panel = document.getElementById('mobileQuotePanel'); if (panel) { panel.classList.add('open'); - document.body.style.overflow = 'hidden'; + syncBodyScrollLock(); } }; @@ -1349,7 +1401,7 @@ initQuote(); var panel = document.getElementById('mobileQuotePanel'); if (panel) { panel.classList.remove('open'); - document.body.style.overflow = ''; + syncBodyScrollLock(); } }; @@ -1423,10 +1475,12 @@ initQuote(); syncEl('nudgeCounter'); syncEl('sl-users-sub'); syncEl('sl-endpoints-sub'); + syncEl('sl-admin-sub'); syncClass('sl-users'); syncClass('sl-users-sub'); syncClass('sl-endpoints'); syncClass('sl-endpoints-sub'); + syncClass('sl-admin-sub'); syncClass('sl-servers'); syncClass('sl-zt'); syncClass('sl-voip'); @@ -1452,6 +1506,7 @@ initQuote(); syncEl('adminWaivedAmt'); syncStyle('sl-users-sub'); syncStyle('sl-endpoints-sub'); + syncStyle('sl-admin-sub'); syncStyle('perUserRow'); syncChecked('hstToggle'); // Pill MRR — show effective MRR with label