GPT is about to go nuts with my project.

This commit is contained in:
2026-03-13 17:21:38 -04:00
parent 591e4155fc
commit bce93507cb
4 changed files with 712 additions and 255 deletions

View File

@@ -0,0 +1,502 @@
/* ══════════════════════════════════════════════════════════════
SVS MSP Calculator — Glass Dark Theme
Imported dynamically by the theme toggle as a third test theme.
Keeps the existing HTML structure intact and overrides presentation only.
══════════════════════════════════════════════════════════════ */
html {
color-scheme: dark;
}
:root {
--ink: #eef6ff;
--paper: #08111c;
--accent: #69c8ff;
--muted: #9fb3c9;
--border: rgba(143, 183, 221, 0.2);
--card: rgba(10, 18, 31, 0.62);
--green: #63d8a2;
--amber: #ffbe68;
--glass-header-text: #5f6d7f;
}
body {
background:
linear-gradient(138deg, #03070f 0%, #071120 14%, #10152a 30%, #1a1324 48%, #1a0d17 64%, #0a111b 82%, #050912 100%),
linear-gradient(125deg, rgba(44, 138, 255, 0.22) 0%, rgba(44, 138, 255, 0) 26%),
linear-gradient(142deg, rgba(18, 194, 152, 0.16) 18%, rgba(18, 194, 152, 0) 42%),
linear-gradient(154deg, rgba(118, 72, 224, 0.16) 34%, rgba(118, 72, 224, 0) 58%),
linear-gradient(164deg, rgba(198, 46, 86, 0.16) 52%, rgba(198, 46, 86, 0) 74%),
radial-gradient(circle at 8% 10%, rgba(72, 178, 255, 0.34), transparent 26%),
radial-gradient(circle at 26% 30%, rgba(32, 196, 144, 0.24), transparent 22%),
radial-gradient(circle at 58% 18%, rgba(116, 82, 222, 0.24), transparent 22%),
radial-gradient(circle at 88% 16%, rgba(200, 60, 92, 0.24), transparent 20%),
radial-gradient(circle at 76% 56%, rgba(42, 126, 255, 0.2), transparent 22%),
radial-gradient(circle at 24% 78%, rgba(112, 44, 138, 0.22), transparent 22%),
radial-gradient(circle at 92% 86%, rgba(18, 168, 132, 0.2), transparent 20%),
linear-gradient(160deg, rgba(7, 14, 25, 0.72) 0%, rgba(5, 10, 19, 0.8) 100%);
background-attachment: fixed;
color: var(--ink);
position: relative;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
linear-gradient(132deg, rgba(58, 154, 255, 0.16) 0%, rgba(58, 154, 255, 0) 34%),
linear-gradient(145deg, rgba(28, 201, 154, 0.12) 18%, rgba(28, 201, 154, 0) 44%),
linear-gradient(156deg, rgba(122, 84, 232, 0.12) 38%, rgba(122, 84, 232, 0) 60%),
linear-gradient(168deg, rgba(214, 68, 104, 0.12) 58%, rgba(214, 68, 104, 0) 78%);
opacity: 0.9;
z-index: 0;
}
.outer,
.pitch-wrap {
position: relative;
z-index: 1;
}
::selection {
background: rgba(105, 200, 255, 0.28);
color: #f8fbff;
}
.top-bar {
position: sticky !important;
top: 0 !important;
z-index: 100 !important;
background: linear-gradient(
180deg,
rgba(252, 255, 255, 0.96) 0%,
rgba(244, 249, 255, 0.93) 52%,
rgba(231, 240, 251, 0.91) 100%
) !important;
border-bottom: 1px solid rgba(118, 143, 171, 0.35) !important;
box-shadow: 0 8px 24px rgba(7, 18, 33, 0.1) !important;
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
}
.top-bar-right {
color: var(--glass-header-text) !important;
}
@media (min-width: 1101px) {
.outer {
padding-top: var(--sidebar-top-gap) !important;
}
.side-col {
top: var(--sidebar-sticky-top) !important;
}
}
.theme-toggle-btn {
background: linear-gradient(180deg, rgba(247, 250, 255, 0.88), rgba(217, 229, 242, 0.82)) !important;
border: 1px solid rgba(83, 117, 150, 0.24) !important;
color: #223142 !important;
box-shadow: 0 10px 24px rgba(6, 18, 31, 0.14) !important;
}
.theme-toggle-btn:hover {
background: linear-gradient(180deg, rgba(252, 254, 255, 0.94), rgba(226, 237, 248, 0.88)) !important;
}
.theme-toggle-btn:active {
background: linear-gradient(180deg, rgba(226, 236, 248, 0.95), rgba(205, 219, 235, 0.9)) !important;
}
.section,
.quote-settings-bar,
.sidebar,
.sidebar-utility .btn-reset-quote,
.mobile-panel-sheet,
.mobile-panel-close-row,
.mobile-panel-actions,
.confirm-modal-card,
.feature-card,
.addon-row,
.vs-comparison-wrap,
.export-wrap,
.pitch-inner {
background: linear-gradient(180deg, rgba(16, 27, 43, 0.76), rgba(9, 17, 29, 0.68)) !important;
border-color: rgba(143, 183, 221, 0.18) !important;
box-shadow:
0 18px 50px rgba(2, 8, 17, 0.32),
inset 0 1px 0 rgba(255, 255, 255, 0.06) !important;
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
}
.section {
background: linear-gradient(180deg, rgba(17, 29, 46, 0.74), rgba(9, 17, 29, 0.66)) !important;
}
.section:hover {
border-color: rgba(105, 200, 255, 0.34) !important;
box-shadow:
-4px 0 0 0 rgba(105, 200, 255, 0.36),
0 20px 54px rgba(2, 8, 17, 0.38),
inset 0 1px 0 rgba(255, 255, 255, 0.07) !important;
}
.sec-open {
border-color: rgba(105, 200, 255, 0.5) !important;
box-shadow:
-4px 0 0 0 rgba(105, 200, 255, 0.5),
0 22px 58px rgba(2, 8, 17, 0.42),
inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
}
.section-num {
color: rgba(226, 239, 255, 0.18) !important;
text-shadow: 0 0 26px rgba(105, 200, 255, 0.1);
}
.section-title,
.confirm-modal-title,
.sidebar-mrr,
.sidebar-line .val,
.vs-svs-label {
color: #f4f9ff !important;
}
.section-subtitle,
.feature-card-desc,
.sidebar-note,
.sidebar-note-mono,
.sl-sub,
.vs-label,
.vs-td-muted,
.savings-prompt,
.pitch-desc,
.qs-label,
.qs-toggle-label,
.qs-fee-label,
.qs-fee-dollar,
.sidebar-line,
.section-title-tag {
color: var(--muted) !important;
}
.client-input {
color: #f4f9ff !important;
border-bottom-color: rgba(143, 183, 221, 0.24) !important;
}
.client-input::placeholder {
color: rgba(159, 179, 201, 0.72) !important;
}
.sec-chevron,
.addon-preview-pill,
.btn-toggle-all,
.confirm-btn-secondary,
.btn-export-secondary,
.mobile-panel-close,
.nudge-nav-btn {
background: rgba(255, 255, 255, 0.04) !important;
border-color: rgba(143, 183, 221, 0.18) !important;
color: var(--muted) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.sec-open .sec-chevron,
.section-toggle:hover .sec-chevron,
.btn-toggle-all:hover,
.confirm-btn-secondary:hover,
.btn-export-secondary:hover,
.nudge-nav-btn:hover,
.mobile-panel-close:hover {
background: rgba(105, 200, 255, 0.12) !important;
border-color: rgba(105, 200, 255, 0.3) !important;
color: #f2f8ff !important;
}
.pill-toggle,
.tier-seg-wrap,
.qs-fee-input-wrap {
background: rgba(5, 11, 21, 0.3) !important;
border-color: rgba(143, 183, 221, 0.18) !important;
}
.pill-toggle label,
.tier-seg {
background: transparent !important;
}
.pill-toggle label:hover,
.tier-seg:hover,
.addon-row:hover {
background: rgba(255, 255, 255, 0.04) !important;
}
.tier-seg.active,
.btn-export,
.confirm-btn-danger,
.mobile-quote-pill,
.progress-fill {
background: linear-gradient(135deg, #7ad6ff 0%, #4da8ff 52%, #337dff 100%) !important;
color: #ffffff !important;
}
.pill-toggle input:checked + label {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0) 42%),
linear-gradient(135deg, rgba(103, 182, 248, 0.7) 0%, rgba(63, 122, 210, 0.76) 58%, rgba(45, 82, 155, 0.82) 100%) !important;
color: #ffffff !important;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.12),
inset 0 -1px 0 rgba(3, 10, 20, 0.2) !important;
}
.sidebar-header {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0) 44%),
linear-gradient(135deg, rgba(105, 188, 250, 0.82) 0%, rgba(71, 132, 224, 0.8) 54%, rgba(67, 72, 156, 0.76) 100%) !important;
color: #ffffff !important;
box-shadow:
inset 0 -1px 0 rgba(255, 255, 255, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.12) !important;
}
.pill-toggle input:checked + label .pill-desc,
.pill-toggle input:checked + label .pill-price,
.tier-seg.active .tier-name,
.tier-seg.active .tier-price,
.tier-seg.active .tier-sub {
color: #ffffff !important;
}
.num-input,
.qs-fee-input,
.qs-fee-dollar,
.qs-fee-input-wrap,
.mobile-panel-sheet .sidebar-body {
background: rgba(5, 11, 21, 0.34) !important;
}
.num-input,
.qs-fee-input {
border-color: rgba(143, 183, 221, 0.2) !important;
color: #f2f8ff !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.num-input:focus,
.qs-fee-input:focus,
.client-input:focus-visible {
border-color: rgba(105, 200, 255, 0.55) !important;
box-shadow: 0 0 0 3px rgba(105, 200, 255, 0.16) !important;
}
.qs-switch {
background: rgba(255, 255, 255, 0.12) !important;
}
.qs-switch::after {
background: rgba(250, 252, 255, 0.95) !important;
box-shadow: 0 2px 8px rgba(3, 9, 18, 0.28);
}
.addon-row.selected {
background: rgba(105, 200, 255, 0.12) !important;
border-color: rgba(105, 200, 255, 0.28) !important;
}
.addon-row.selected .addon-name,
.addon-row.selected .addon-price {
color: #9edcff !important;
}
.feature-card {
background: linear-gradient(180deg, rgba(19, 31, 49, 0.8), rgba(10, 18, 30, 0.72)) !important;
}
.callout-green,
.nudge-banner.green,
.admin-waive-savings {
background: linear-gradient(180deg, rgba(15, 48, 42, 0.82), rgba(10, 35, 30, 0.72)) !important;
border-color: rgba(99, 216, 162, 0.26) !important;
color: var(--green) !important;
}
.callout-red {
background: linear-gradient(180deg, rgba(62, 23, 34, 0.82), rgba(41, 14, 22, 0.74)) !important;
border-color: rgba(230, 117, 138, 0.26) !important;
color: #ffb7c6 !important;
}
.nudge-banner.amber,
.admin-fee-waived-badge {
background: linear-gradient(180deg, rgba(66, 41, 12, 0.84), rgba(43, 27, 8, 0.76)) !important;
border-color: rgba(255, 190, 104, 0.26) !important;
color: var(--amber) !important;
}
.admin-waive-savings,
.admin-fee-waived-badge,
.addon-preview-pill.active {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.addon-preview-pill.active {
background: rgba(105, 200, 255, 0.16) !important;
border-color: rgba(105, 200, 255, 0.28) !important;
color: #dff3ff !important;
}
.collapsible-header,
.sidebar-line,
.pitch-item,
.vs-label::after,
.sidebar-line-total,
.pitch-footer,
.export-wrap,
.mobile-panel-close-row,
.mobile-panel-actions {
border-color: rgba(143, 183, 221, 0.14) !important;
}
.sidebar-title,
.sidebar-client.placeholder {
color: rgba(255, 255, 255, 0.76) !important;
}
.sidebar-body,
.mobile-panel-sheet .sidebar,
.mobile-panel-sheet .sidebar-body {
background: transparent !important;
}
.sidebar-note strong,
.sl-discount-val,
.savings-amount {
color: var(--green) !important;
}
.sl-hst-toggle,
.sl-hst-val,
.sl-muted {
color: var(--muted) !important;
}
.vs-comparison-wrap {
background: linear-gradient(180deg, rgba(14, 24, 39, 0.72), rgba(9, 16, 29, 0.62)) !important;
}
.vs-save-green td {
background: rgba(99, 216, 162, 0.14) !important;
}
.vs-save-amber td {
background: rgba(255, 190, 104, 0.14) !important;
}
.export-wrap {
border-top: 1px solid rgba(143, 183, 221, 0.14) !important;
}
.btn-export {
box-shadow: 0 14px 28px rgba(29, 108, 186, 0.26) !important;
}
.btn-export:hover,
.mobile-quote-pill:hover {
filter: brightness(1.05);
}
.btn-reset-quote {
color: #dceefe !important;
}
.btn-reset-quote:hover {
background: rgba(105, 200, 255, 0.1) !important;
border-color: rgba(105, 200, 255, 0.32) !important;
color: #f2f8ff !important;
}
.confirm-modal-backdrop {
background: rgba(2, 7, 15, 0.72) !important;
backdrop-filter: blur(10px) saturate(125%);
-webkit-backdrop-filter: blur(10px) saturate(125%);
}
.confirm-modal-card {
background: linear-gradient(180deg, rgba(18, 29, 46, 0.86), rgba(10, 17, 29, 0.8)) !important;
}
.pitch-inner {
background: linear-gradient(180deg, rgba(14, 25, 40, 0.72), rgba(9, 16, 28, 0.68)) !important;
}
.pitch-title {
color: #f1f8ff !important;
}
.pitch-footer {
background: linear-gradient(135deg, rgba(11, 42, 34, 0.88), rgba(7, 28, 22, 0.86)) !important;
color: #8ee8bf !important;
}
.mobile-panel-sheet {
background: linear-gradient(180deg, rgba(12, 21, 34, 0.92), rgba(8, 14, 24, 0.9)) !important;
}
.mobile-panel-close-row,
.mobile-panel-actions {
background: rgba(10, 18, 30, 0.84) !important;
}
@media (max-width: 1100px) {
.top-bar {
background: linear-gradient(
180deg,
rgba(251, 255, 255, 0.95) 0%,
rgba(241, 247, 254, 0.91) 56%,
rgba(228, 238, 250, 0.89) 100%
) !important;
}
}
@media (max-width: 600px) {
body {
background:
linear-gradient(150deg, #03070f 0%, #0a1220 24%, #171226 54%, #110d18 76%, #070c14 100%),
linear-gradient(132deg, rgba(58, 154, 255, 0.16) 0%, rgba(58, 154, 255, 0) 34%),
linear-gradient(150deg, rgba(28, 201, 154, 0.12) 18%, rgba(28, 201, 154, 0) 44%),
linear-gradient(162deg, rgba(214, 68, 104, 0.11) 48%, rgba(214, 68, 104, 0) 72%),
radial-gradient(circle at 16% 12%, rgba(72, 178, 255, 0.24), transparent 24%),
radial-gradient(circle at 72% 20%, rgba(189, 58, 92, 0.18), transparent 20%),
radial-gradient(circle at 34% 46%, rgba(32, 196, 144, 0.18), transparent 22%),
radial-gradient(circle at 74% 60%, rgba(116, 82, 222, 0.16), transparent 20%);
background-attachment: fixed;
}
body::before {
background:
linear-gradient(138deg, rgba(58, 154, 255, 0.14) 0%, rgba(58, 154, 255, 0) 34%),
linear-gradient(154deg, rgba(28, 201, 154, 0.1) 18%, rgba(28, 201, 154, 0) 46%),
linear-gradient(166deg, rgba(122, 84, 232, 0.1) 36%, rgba(122, 84, 232, 0) 58%),
linear-gradient(174deg, rgba(214, 68, 104, 0.1) 56%, rgba(214, 68, 104, 0) 76%);
opacity: 0.82;
}
.theme-toggle-btn {
box-shadow: 0 8px 20px rgba(6, 18, 31, 0.12) !important;
}
.section,
.quote-settings-bar,
.sidebar,
.mobile-panel-sheet,
.confirm-modal-card {
box-shadow:
0 14px 36px rgba(2, 8, 17, 0.28),
inset 0 1px 0 rgba(255, 255, 255, 0.05) !important;
}
}

View File

@@ -31,6 +31,10 @@
--card: #272420; /* elevated surface — clear separation from paper */ --card: #272420; /* elevated surface — clear separation from paper */
--green: #3ab870; --green: #3ab870;
--amber: #e8920f; --amber: #e8920f;
--sidebar-stack-gap: 12px;
--sidebar-top-gap: calc(var(--sidebar-stack-gap) + 20px);
--top-bar-sticky-offset: 62px;
--sidebar-sticky-top: calc(var(--top-bar-sticky-offset) + var(--sidebar-top-gap));
} }
body { body {
background: var(--paper); background: var(--paper);
@@ -111,14 +115,14 @@
display: grid; display: grid;
grid-template-columns: 3fr 2fr; grid-template-columns: 3fr 2fr;
gap: 52px; gap: 52px;
padding: 50px clamp(20px,2vw,40px) 52px; padding: var(--sidebar-top-gap) clamp(20px,2vw,40px) 52px;
max-width: 1600px; max-width: 1600px;
margin: 0 auto; margin: 0 auto;
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: 102px; z-index: 10; align-self: start; } .side-col { position: sticky; top: var(--sidebar-sticky-top); z-index: 10; align-self: start; }
.sidebar-utility { margin-bottom: 12px; } .sidebar-utility { margin-bottom: var(--sidebar-stack-gap); }
.btn-reset-quote { .btn-reset-quote {
width: 100%; width: 100%;
background: var(--card); background: var(--card);
@@ -1459,7 +1463,7 @@
@media (max-width: 1350px) { @media (max-width: 1350px) {
.outer { .outer {
gap: 36px; gap: 36px;
padding: 52px clamp(48px,4vw,60px) 52px; padding: var(--sidebar-top-gap) clamp(48px,4vw,60px) 52px;
} }
.section { margin-left: 76px; } .section { margin-left: 76px; }
.section-num { left: -76px; width: 64px; font-size: 54px; top: 30px; } .section-num { left: -76px; width: 64px; font-size: 54px; top: 30px; }

View File

@@ -38,9 +38,9 @@
.mobile-panel-sheet — slides up from bottom, max-height:92vh .mobile-panel-sheet — slides up from bottom, max-height:92vh
.mobile-panel-handle — decorative drag indicator bar .mobile-panel-handle — decorative drag indicator bar
.mobile-panel-close-row — "QUOTE SUMMARY" label + × button .mobile-panel-close-row — "QUOTE SUMMARY" label + × button
#mobilePanelContent — contains duplicate sidebar (_m IDs) #mobilePanelContent — receives a JS-cloned sidebar with _m IDs
DUPLICATE SIDEBAR: All sidebar IDs duplicated with _m suffix. MOBILE SIDEBAR: Built from the desktop sidebar on boot.
update() calls syncEl/syncClass/syncStyle to keep _m in sync. update() syncs the cloned _m elements after each render.
Never DOM-move the real .sidebar here — it breaks desktop. Never DOM-move the real .sidebar here — it breaks desktop.
════════════════════════════════════════════════════════════ --> ════════════════════════════════════════════════════════════ -->
<div class="mobile-quote-panel" id="mobileQuotePanel"> <div class="mobile-quote-panel" id="mobileQuotePanel">
@@ -54,143 +54,10 @@
<div class="mobile-panel-actions"> <div class="mobile-panel-actions">
<button type="button" class="btn-reset-quote" onclick="openResetConfirm()">Reset Quote</button> <button type="button" class="btn-reset-quote" onclick="openResetConfirm()">Reset Quote</button>
</div> </div>
<!-- Sidebar content injected here by JS on first open --> <!-- Sidebar content injected by JS from the desktop sidebar markup -->
<div id="mobilePanelContent"> <div id="mobilePanelContent"></div>
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">SVS MSP — Live Quote</div>
<div class="sidebar-client" id="clientNameDisplay_m">Client Name</div>
</div>
<!-- Nudge Banner (mobile) -->
<div id="nudgeBanner_m" class="nudge-banner amber hidden">
<div class="nudge-header-row">
<span class="nudge-banner-label" style="margin-bottom:0;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="12" height="14" fill="currentColor" style="margin-right:6px;vertical-align:middle;"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2l0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4l0 0c19.8 27.1 39.7 54.4 49.2 86.2H272zM192 512c44.2 0 80-35.8 80-80V416H112v16c0 44.2 35.8 80 80 80zM112 352H272c0 0 0 0 0 0H112c0 0 0 0 0 0z"/></svg> Insight <span id="nudgeCounter_m" class="nudge-counter"></span></span>
<div class="nudge-nav-group">
<button onclick="cycleNudge(-1)" class="nudge-nav-btn" title="Previous"><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="15 18 9 12 15 6"/></svg></button>
<button onclick="cycleNudge(1)" class="nudge-nav-btn" title="Next"><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="9 18 15 12 9 6"/></svg></button>
</div> </div>
</div> </div>
<span id="nudgeText_m"></span>
</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>
</div>
<div class="sl-sub hidden" id="sl-users-sub_m"></div>
<div class="sidebar-line hidden" id="sl-endpoints_m">
<span><span class="lbl-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="14" height="13" fill="currentColor" style="vertical-align:middle;"><path d="M64 0C28.7 0 0 28.7 0 64V352c0 35.3 28.7 64 64 64H240l-10.7 32H160c-17.7 0-32 14.3-32 32s14.3 32 32 32H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H346.7L336 416H512c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H64zM512 64V352H64V64H512z"/></svg></span> Endpoints</span>
<span class="val" id="sl-endpoints-val_m"></span>
</div>
<div class="sl-sub hidden" id="sl-endpoints-sub_m"></div>
<div class="sidebar-line hidden" id="sl-servers_m">
<span><span class="lbl-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="13" height="13" fill="currentColor" style="vertical-align:middle;"><path d="M64 32C28.7 32 0 60.7 0 96v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm48 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM64 288c-35.3 0-64 28.7-64 64v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V352c0-35.3-28.7-64-64-64H64zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"/></svg></span> Servers</span>
<span class="val" id="sl-servers-val_m"></span>
</div>
<div class="sidebar-line hidden" id="sl-zt_m">
<span><span class="lbl-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="11" height="13" fill="currentColor" style="vertical-align:middle;"><path d="M144 144v48H304V144c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192V144C80 64.5 144.5 0 224 0s144 64.5 144 144v48h16c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V256c0-35.3 28.7-64 64-64H80z"/></svg></span> Zero Trust</span>
<span class="val" id="sl-zt-val_m"></span>
</div>
<div class="sidebar-line hidden" id="sl-voip_m">
<span><span class="lbl-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="13" height="13" fill="currentColor" style="vertical-align:middle;"><path d="M164.9 24.6c-7.7-18.6-28-28.5-47.4-23.2l-88 24C11.7 30.3 0 46.7 0 64C0 311.4 200.6 512 448 512c17.3 0 33.7-11.7 38.6-29.5l24-88c5.3-19.4-4.6-39.7-23.2-47.4l-96-40c-16.3-6.8-35.2-2.1-46.3 11.6L304.7 368C234.3 334.7 177.3 277.7 144 207.3L193.3 167c13.7-11.2 18.4-30 11.6-46.3l-40-96z"/></svg></span> VoIP</span>
<span class="val" id="sl-voip-val_m"></span>
</div>
<div class="sidebar-line" id="sl-admin_m">
<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 hidden" id="sl-admin-sub_m"></div>
</div>
<!-- Discount line — hidden when no term discount -->
<div class="sidebar-line sidebar-line-discount hidden" id="sl-base-mrr-row_m">
<span class="sl-muted">Base MRR</span>
<span class="val sl-muted" id="sl-base-mrr-val_m"></span>
</div>
<div class="sidebar-line sidebar-line-discount hidden" id="sl-discount-row_m">
<span class="sl-muted">Term Discount</span>
<span class="val sl-discount-val" id="sl-discount-val_m"></span>
</div>
<div class="sidebar-mrr-label">Monthly Recurring (MRR)</div>
<div class="sidebar-mrr" id="mrrDisplay_m">$150</div>
<label class="sl-hst-toggle">
<input type="checkbox" id="hstToggle_m" onchange="document.getElementById('hstToggle').checked=this.checked; update();">
<span>Include Ontario HST (13%)</span>
</label>
<!-- HST line -->
<div class="sidebar-line sidebar-line-hst hidden" id="sl-hst-row_m">
<span class="sl-muted">HST (13%)</span>
<span class="val sl-hst-val" id="sl-hst-val_m"></span>
</div>
<!-- Total inc. HST -->
<div class="sidebar-line sidebar-line-total hidden" id="sl-hst-total-row_m">
<span>Total (inc. HST)</span>
<span class="val" id="sl-hst-total-val_m"></span>
</div>
<!-- Onboarding fee line -->
<div class="sidebar-line hidden" id="sl-otf-row_m">
<span>Onboarding Fee</span>
<span class="val" id="sl-otf-val_m"></span>
</div>
<div class="sidebar-line">
<span>Annual Projection</span>
<span class="val" id="annualDisplay_m">$1,800</span>
</div>
<div class="sidebar-line hidden" id="perUserRow_m">
<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>
<!-- VS Hiring In-House -->
<div id="vsComparison_m" class="hidden vs-comparison-wrap">
<div class="vs-label">VS. Hiring In-House</div>
<table class="vs-table">
<tr>
<td>
<svg width="14" height="14" viewBox="0 0 72 98" class="vs-inline-icon" xmlns="http://www.w3.org/2000/svg">
<polyline points="7.32 8.88 62.11 8.88 34.72 58.22" fill="#1f75a6"/>
<polyline points="40.7 55.33 64.4 12.64 71.88 12.64 44.48 61.99 40.7 55.33" fill="#8d252f"/>
</svg>
<span class="vs-svs-label">SVS MSP</span>
</td>
<td class="vs-val-accent" id="vs-svs-annual_m"></td>
</tr>
<tr><td class="vs-td-muted"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="12" height="13" fill="currentColor" class="vs-td-icon"><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg> 1 IT person + tools</td><td class="vs-td-muted" id="vs-1man-cost_m"></td></tr>
<tr class="vs-save-row" id="vs-1man-save-row_m"><td><span id="vs-1man-save-lbl_m" class="vs-val-green">YOU SAVE</span></td><td id="vs-1man-save_m" class="vs-val-green"></td></tr>
<tr><td class="vs-td-muted"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="14" height="13" fill="currentColor" class="vs-td-icon"><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> 5-person team</td><td class="vs-td-muted" id="vs-5man-cost_m"></td></tr>
<tr class="vs-save-row" id="vs-5man-save-row_m"><td><span id="vs-5man-save-lbl_m" class="vs-val-green">YOU SAVE</span></td><td id="vs-5man-save_m" class="vs-val-green"></td></tr>
</table>
<div class="vs-footnote" id="vs-footnote_m"></div>
</div>
</div>
<div class="export-wrap">
<button class="btn-export" onclick="printInvoice()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14" fill="currentColor" style="margin-right:7px;vertical-align:middle;"><path d="M128 0C92.7 0 64 28.7 64 64v96h64V64H354.7L384 93.3V160h64V93.3c0-17-6.7-33.3-18.7-45.3L400 18.7C388 6.7 371.7 0 354.7 0H128zM384 352v32 64H128V384 352H384zm64 32h32c17.7 0 32-14.3 32-32V256c0-35.3-28.7-64-64-64H64c-35.3 0-64 28.7-64 64v96c0 17.7 14.3 32 32 32H64v64c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V384zm-16-88a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>
Print / Save PDF
</button>
<button class="btn-export btn-export-secondary" onclick="exportQuoteJSON()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="12" height="14" fill="currentColor" style="margin-right:7px;vertical-align:middle;"><path d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128h128L256 0zM216 232l-96 96 96 96 22.6-22.6L169.3 328l69.3-69.4L216 232zM168 232l-22.6 22.6 69.3 69.4-69.3 69.4L168 416l96-96-96-96z"/></svg>
Export JSON + Copy
</button>
</div>
</div>
</div>
</div>
</div>
<!-- TOP BAR --> <!-- TOP BAR -->
<!-- ── TOP BAR ── sticky, z-index:100, cream bg (#ddd8d0) ──────── --> <!-- ── TOP BAR ── sticky, z-index:100, cream bg (#ddd8d0) ──────── -->
@@ -754,7 +621,7 @@
<!-- SIDEBAR --> <!-- SIDEBAR -->
<!-- ── RIGHT COLUMN: sticky sidebar (desktop only ≥1100px) ───────── <!-- ── RIGHT COLUMN: sticky sidebar (desktop only ≥1100px) ─────────
Hidden on mobile via CSS. Duplicate exists in #mobilePanelContent. Hidden on mobile via CSS. Mobile clone is injected into #mobilePanelContent.
nudgeBanner must stay INSIDE .sidebar-body or it gets clipped. nudgeBanner must stay INSIDE .sidebar-body or it gets clipped.
──────────────────────────────────────────────────────────────── --> ──────────────────────────────────────────────────────────────── -->
<div class="side-col"> <div class="side-col">
@@ -769,7 +636,7 @@
<!-- ── INSIGHT NUDGE BANNER ────────────────────────────────────── <!-- ── INSIGHT NUDGE BANNER ──────────────────────────────────────
Sits flush under .sidebar-header, full width of .sidebar. Sits flush under .sidebar-header, full width of .sidebar.
Controlled entirely by renderNudge() via applyNudge(""). Controlled entirely by renderNudge() via applyNudge("").
Mobile duplicate: #nudgeBanner_m (synced via syncClass/syncEl). Mobile clone target: #nudgeBanner_m (synced via syncClass/syncEl).
.hidden class toggled by renderNudge() when nudges=[] .hidden class toggled by renderNudge() when nudges=[]
──────────────────────────────────────────────────────────────── --> ──────────────────────────────────────────────────────────────── -->
<div id="nudgeBanner" class="nudge-banner amber hidden"> <div id="nudgeBanner" class="nudge-banner amber hidden">
@@ -976,3 +843,4 @@
<script src="SVS-MSP-Calculator.js"></script> <script src="SVS-MSP-Calculator.js"></script>
</body> </body>
</html> </html>

View File

@@ -1275,50 +1275,94 @@ function exportQuoteJSON() {
} }
// ── THEME TOGGLE ───────────────────────────────────────────────── // ── THEME TOGGLE ─────────────────────────────────────────────────
// Light theme is a separate CSS file imported dynamically on demand. // Base CSS is the default dark theme.
// Dark mode = base stylesheet only (no extra link element). // Variant themes are imported dynamically on demand:
// Light mode = base + SVS-MSP-Calculator-light.css. // light = base + SVS-MSP-Calculator-light.css
// glass = base + SVS-MSP-Calculator-glass.css
// Preference persisted to localStorage under 'svs-theme'. // Preference persisted to localStorage under 'svs-theme'.
// initTheme() called first so the page never flashes the wrong theme. // initTheme() runs before initQuote() so the UI boots in the saved theme.
const THEME_STORAGE_KEY = 'svs-theme';
const THEME_STYLESHEET_ID = 'themeStylesheetLink';
const THEME_ASSET_VERSION = '20260313-02';
const SVG_SUN = '<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>'; const SVG_SUN = '<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>';
const SVG_MOON = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'; const SVG_MOON = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
const SVG_GLASS = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l2.7 5.3L20 11l-5.3 2.7L12 19l-2.7-5.3L4 11l5.3-2.7L12 3z"/><path d="M18.5 3.5l.8 1.7L21 6l-1.7.8-.8 1.7-.8-1.7L16 6l1.7-.8.8-1.7z"/></svg>';
const THEME_ORDER = ['dark', 'light', 'glass'];
const THEME_CONFIG = {
dark: {
icon: SVG_MOON,
href: null,
label: 'Dark'
},
light: {
icon: SVG_SUN,
href: 'SVS-MSP-Calculator-light.css',
label: 'Light'
},
glass: {
icon: SVG_GLASS,
href: 'SVS-MSP-Calculator-glass.css',
label: 'Glass'
}
};
function toggleTheme() { function getSavedTheme() {
const link = document.getElementById('lightThemeLink'); const saved = localStorage.getItem(THEME_STORAGE_KEY);
return THEME_ORDER.includes(saved) ? saved : 'dark';
}
function getCurrentTheme() {
const applied = document.documentElement.dataset.theme;
return THEME_ORDER.includes(applied) ? applied : getSavedTheme();
}
function updateThemeToggleUi(theme) {
const icon = document.getElementById('themeToggleIcon'); const icon = document.getElementById('themeToggleIcon');
const btn = document.getElementById('themeToggle'); const btn = document.getElementById('themeToggle');
if (link) { const currentIndex = THEME_ORDER.indexOf(theme);
// Currently light → switch to dark const nextTheme = THEME_ORDER[(currentIndex + 1) % THEME_ORDER.length];
link.remove(); const currentLabel = THEME_CONFIG[theme].label;
localStorage.setItem('svs-theme', 'dark'); const nextLabel = THEME_CONFIG[nextTheme].label;
if (icon) icon.innerHTML = SVG_SUN; if (icon) icon.innerHTML = THEME_CONFIG[theme].icon;
if (btn) btn.setAttribute('title', 'Switch to light theme'); if (btn) {
} else { const uiLabel = `Theme: ${currentLabel}. Click to switch to ${nextLabel}.`;
// Currently dark → switch to light btn.setAttribute('title', uiLabel);
const el = document.createElement('link'); btn.setAttribute('aria-label', uiLabel);
el.id = 'lightThemeLink';
el.rel = 'stylesheet';
el.href = 'SVS-MSP-Calculator-light.css';
document.head.appendChild(el);
localStorage.setItem('svs-theme', 'light');
if (icon) icon.innerHTML = SVG_MOON;
if (btn) btn.setAttribute('title', 'Switch to dark theme');
} }
} }
function applyTheme(theme) {
const nextTheme = THEME_ORDER.includes(theme) ? theme : 'dark';
const existing = document.getElementById(THEME_STYLESHEET_ID);
const legacyLight = document.getElementById('lightThemeLink');
if (existing) existing.remove();
if (legacyLight) legacyLight.remove();
const themeHref = THEME_CONFIG[nextTheme].href;
if (themeHref) {
const el = document.createElement('link');
el.id = THEME_STYLESHEET_ID;
el.rel = 'stylesheet';
el.href = `${themeHref}?v=${THEME_ASSET_VERSION}`;
el.dataset.theme = nextTheme;
document.head.appendChild(el);
}
document.documentElement.dataset.theme = nextTheme;
localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
updateThemeToggleUi(nextTheme);
}
function toggleTheme() {
const currentTheme = getCurrentTheme();
const currentIndex = THEME_ORDER.indexOf(currentTheme);
const nextTheme = THEME_ORDER[(currentIndex + 1) % THEME_ORDER.length];
applyTheme(nextTheme);
}
function initTheme() { function initTheme() {
if (localStorage.getItem('svs-theme') === 'light') { applyTheme(getSavedTheme());
const el = document.createElement('link');
el.id = 'lightThemeLink';
el.rel = 'stylesheet';
el.href = 'SVS-MSP-Calculator-light.css';
document.head.appendChild(el);
const icon = document.getElementById('themeToggleIcon');
const btn = document.getElementById('themeToggle');
if (icon) icon.innerHTML = SVG_MOON;
if (btn) btn.setAttribute('title', 'Switch to dark theme');
}
} }
// ── initQuote() ────────────────────────────────────────────────── // ── initQuote() ──────────────────────────────────────────────────
@@ -1361,33 +1405,54 @@ initTheme();
initQuote(); initQuote();
// ── MOBILE SIDEBAR SYNC CONTRACT ────────────────────────────────────── // ── MOBILE SIDEBAR SYNC CONTRACT ──────────────────────────────────────
// Every stateful sidebar element has a mirror ID with _m suffix. // The desktop sidebar is the single markup source of truth.
// The update() wrapper below syncs _m elements after each call to _origUpdate(). // On boot, we clone it into #mobilePanelContent and suffix all IDs with _m.
// The update() wrapper below then syncs dynamic values/classes into the clone.
// //
// WHEN ADDING A NEW SIDEBAR ELEMENT: // WHEN ADDING A NEW SIDEBAR ELEMENT:
// 1. Add the desktop element with its ID (e.g. #my-element) // 1. Add the desktop element with its ID (e.g. #my-element)
// 2. Add the mobile duplicate in #mobilePanelContent with ID #my-element_m // 2. Ensure the mobile sync map below includes it if it changes at runtime
// 3. Add the appropriate sync call in the update() wrapper: // 3. Avoid separate handwritten mobile markup for sidebar content
// syncEl(id) — copies innerHTML (text/HTML values)
// syncClass(id) — copies className (.hidden toggling via classList)
// syncStyle(id) — copies style.cssText (legacy inline display — avoid for new elements)
// syncChecked(id) — copies .checked state (checkboxes)
//
// NEVER DOM-move the real .sidebar into the panel.
// The duplicate HTML is intentional — moving breaks desktop layout on resize.
// ────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────
// ── MOBILE QUOTE PANEL IIFE ────────────────────────────────────── // ── MOBILE QUOTE PANEL IIFE ──────────────────────────────────────
// Encapsulates all mobile panel logic to avoid polluting global scope. // Encapsulates all mobile panel logic to avoid polluting global scope.
// ARCHITECTURE: // ARCHITECTURE:
// The real sidebar lives in .side-col (desktop). // The real sidebar lives in .side-col (desktop).
// The panel contains a STATIC DUPLICATE with _m suffixed IDs. // The mobile panel gets a JS-generated clone with _m suffixed IDs.
// update() is wrapped here to sync _m elements after every update. // update() is wrapped here to sync _m elements after every update.
// openMobilePanel / closeMobilePanel are exposed on window. // openMobilePanel / closeMobilePanel are exposed on window.
// Do NOT DOM-move the real sidebar into the panel — // Do NOT DOM-move the real desktop sidebar into the panel —
// it permanently breaks desktop layout on resize. // it permanently breaks desktop layout on resize.
(function() { (function() {
// Panel uses a static duplicate sidebar (_m IDs) — no DOM moving needed. function buildMobileSidebar() {
var container = document.getElementById('mobilePanelContent');
var desktopSidebar = document.querySelector('.side-col .sidebar');
if (!container || !desktopSidebar || container.children.length) return;
var mobileSidebar = desktopSidebar.cloneNode(true);
mobileSidebar.removeAttribute('id');
mobileSidebar.querySelectorAll('[id]').forEach(function(el) {
el.id = el.id + '_m';
});
var mobileHstToggle = mobileSidebar.querySelector('#hstToggle_m');
if (mobileHstToggle) {
mobileHstToggle.onchange = function() {
var desktopHstToggle = document.getElementById('hstToggle');
if (desktopHstToggle) desktopHstToggle.checked = this.checked;
update();
};
}
var mobileExportJson = mobileSidebar.querySelector('#btnExportJSON_m');
if (mobileExportJson) mobileExportJson.removeAttribute('id');
container.appendChild(mobileSidebar);
}
buildMobileSidebar();
window.openMobilePanel = function() { window.openMobilePanel = function() {
var panel = document.getElementById('mobileQuotePanel'); var panel = document.getElementById('mobileQuotePanel');
@@ -1437,6 +1502,84 @@ initQuote();
if (src && dst) dst.checked = src.checked; if (src && dst) dst.checked = src.checked;
} }
var sidebarSyncMap = {
html: [
'clientNameDisplay',
'sl-users-val',
'sl-endpoints-val',
'sl-servers-val',
'sl-zt-val',
'sl-voip-val',
'sl-admin-val',
'mrrDisplay',
'annualDisplay',
'perUserDisplay',
'perUserBreakdown',
'm365SaveAmt',
'sl-discount-val',
'sl-base-mrr-val',
'sl-hst-val',
'sl-hst-total-val',
'sl-otf-val',
'vs-svs-annual',
'vs-1man-cost',
'vs-1man-save',
'vs-1man-save-lbl',
'vs-5man-cost',
'vs-5man-save',
'vs-5man-save-lbl',
'vs-footnote',
'nudgeText',
'nudgeCounter',
'sl-users-sub',
'sl-endpoints-sub',
'sl-admin-sub',
'adminWaivedAmt'
],
class: [
'sl-users',
'sl-users-sub',
'sl-endpoints',
'sl-endpoints-sub',
'sl-admin-sub',
'sl-servers',
'sl-zt',
'sl-voip',
'sl-admin',
'sideNote-m365',
'sideNote-byol',
'vsComparison',
'perUserRow',
'perUserBreakdown',
'sl-discount-row',
'sl-base-mrr-row',
'sl-hst-row',
'sl-hst-total-row',
'sl-otf-row',
'vs-1man-save-row',
'vs-1man-save',
'vs-1man-save-lbl',
'vs-5man-save-row',
'vs-5man-save',
'vs-5man-save-lbl',
'nudgeBanner',
'adminWaivedSavings'
],
style: [
'sl-users-sub',
'sl-endpoints-sub',
'sl-admin-sub',
'perUserRow'
],
checked: [
'hstToggle'
]
};
function runSidebarSync(ids, syncFn) {
ids.forEach(syncFn);
}
// ── UPDATE WRAPPER ───────────────────────────────────────────── // ── UPDATE WRAPPER ─────────────────────────────────────────────
// Wraps the global update() to also sync the mobile panel. // Wraps the global update() to also sync the mobile panel.
// _origUpdate = the real update() defined above. // _origUpdate = the real update() defined above.
@@ -1445,70 +1588,10 @@ initQuote();
var _origUpdate = window.update; var _origUpdate = window.update;
window.update = function() { window.update = function() {
_origUpdate(); _origUpdate();
// Sync all mirrored sidebar elements runSidebarSync(sidebarSyncMap.html, syncEl);
syncEl('clientNameDisplay'); runSidebarSync(sidebarSyncMap.class, syncClass);
syncEl('sl-users-val'); runSidebarSync(sidebarSyncMap.style, syncStyle);
syncEl('sl-endpoints-val'); runSidebarSync(sidebarSyncMap.checked, syncChecked);
syncEl('sl-servers-val');
syncEl('sl-zt-val');
syncEl('sl-voip-val');
syncEl('sl-admin-val');
syncEl('mrrDisplay');
syncEl('annualDisplay');
syncEl('perUserDisplay');
syncEl('perUserBreakdown');
syncEl('m365SaveAmt');
syncEl('sl-discount-val');
syncEl('sl-base-mrr-val');
syncEl('sl-hst-val');
syncEl('sl-hst-total-val');
syncEl('sl-otf-val');
syncEl('vs-svs-annual');
syncEl('vs-1man-cost');
syncEl('vs-1man-save');
syncEl('vs-1man-save-lbl');
syncEl('vs-5man-cost');
syncEl('vs-5man-save');
syncEl('vs-5man-save-lbl');
syncEl('vs-footnote');
syncEl('nudgeText');
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');
syncClass('sl-admin');
syncClass('sideNote-m365');
syncClass('sideNote-byol');
syncClass('vsComparison');
syncClass('perUserRow');
syncClass('perUserBreakdown');
syncClass('sl-discount-row');
syncClass('sl-base-mrr-row');
syncClass('sl-hst-row');
syncClass('sl-hst-total-row');
syncClass('sl-otf-row');
syncClass('vs-1man-save-row');
syncClass('vs-1man-save');
syncClass('vs-1man-save-lbl');
syncClass('vs-5man-save-row');
syncClass('vs-5man-save');
syncClass('vs-5man-save-lbl');
syncClass('nudgeBanner');
syncClass('adminWaivedSavings');
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 // Pill MRR — show effective MRR with label
var mrr = document.getElementById('mrrDisplay'); var mrr = document.getElementById('mrrDisplay');
var pill = document.getElementById('mobilePillMrr'); var pill = document.getElementById('mobilePillMrr');