Files
svsmspcalc/pre-alpha/docs/CHECKPOINT.md
2026-03-16 01:42:17 -04:00

401 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SVS MSP CALC — Beta Build Checkpoint
**Date:** 2026-03-15
**Status:** Phases 18 + Stage 8 complete. Beta + a11y/perf audit + code quality passes I & II + test expansion + print enhancements done.
**Tests:** 254/254 passing
**Build Prompt:** .claude/plans/STAGE2-BUILD-PROMPT.md
**Previous Stage Prompt:** docs/STAGE3-SESSION-PROMPT.md
**Previous Stage Prompt:** docs/STAGE5-SESSION-PROMPT.md
**Previous Stage Prompt:** docs/STAGE6-SESSION-PROMPT.md
**Previous Stage Prompt:** docs/STAGE7-SESSION-PROMPT.md
**Previous Stage Prompt:** docs/STAGE8-SESSION-PROMPT.md
---
## Completed
### Phase 1: Bug Fixes (6/6)
| # | Issue | File | Change |
|---|-------|------|--------|
| 1.1 | ADDON_INKY default $5 → $8 | quote-pricing.js:12 | `ADDON_INKY: 5``8` |
| 1.2 | Onboarding fee loses manual override on term switch | SVS-MSP-Calculator.js:41-70 | Store manual value in `data-manual-value` before 24mo clears it; restore on switch back to m2m/12mo |
| 1.3 | VoIP fax CSV comment misleading | package-prices.csv:18 | "Flat/mo" → "Per seat/mo" |
| 1.4 | Print forces HST on regardless of user toggle | quote-export.js:12 | Removed `state.hstEnabled = true;` — print now respects user's HST toggle |
| 1.5 | JSON export missing schema version | quote-export.js:229 | Added `version: '1.0'` as first field in payload |
| 1.6 | ZT admin supplement triggers with no warning | quote-render.js:494-499 | New amber nudge when `ztActive` warns about $250 admin supplement |
Test expectations updated in test-quote-engine.js for INKY $8 (4 values changed).
### Phase 2: Visual Polish (Sections IIII)
| # | Issue | File | Change |
|---|-------|------|--------|
| 2.1a | Hardcoded `#e06070` danger icon | components.css:41 | → `var(--text-danger)` — adapts per theme |
| 2.1b | Hardcoded `#86efac` pill-savings on checked state | components.css:442 | → `var(--text-pill-savings-active)` — new token |
| — | Token added to all 4 themes | tokens.css, light.css, glass.css, 70retro.css | Dark: `#86efac`, Light: `#d4f5e0`, Glass: `#a8f0c8`, Retro: `#e0f0d0` |
| 2.1c | QUICK-REF.md outdated | docs/QUICK-REF.md | Updated INKY $8, export desc, theme list, test count |
**Audit findings (no action needed):**
- All 4 themes fully token-covered for Sections IIII
- Glass theme uses `!important` selector overrides (valid for glassmorphism effects)
- Sidebar focus-toggle white rgba values sit on colored header — correct everywhere
- No remaining hardcoded colors in Sections IIII component CSS
- Sidebar renders correctly across all 4 themes at all breakpoints
### Phase 3: UX Hardening (Sections IIII)
#### 3.1 Interaction Refinements
| # | Change | Files | Details |
|---|--------|-------|---------|
| 3.1a | Smooth theme-switch transition | tokens.css, theme-manager.js | `body.theme-transitioning` class enables 0.25s color/bg/border fade; applied for 300ms during `toggleTheme()` |
| 3.1b | Nudge crossfade on rotation/nav | components.css, quote-render.js | `.nudge-fading` class fades opacity to 0; `cycleNudge()` does fade-out → swap → fade-in (180ms); auto-rotation now uses `cycleNudge(1)` for consistency |
| 3.1c | Summary badge fade-in on collapse | components.css | `@keyframes badgeFadeIn` — 0.25s opacity + translateY animation on `.sec-summary-badge` |
| 3.1d | Addon toggle micro-feedback | components.css | `@keyframes addonPulse` — 0.2s scale(1.015) pulse on `.addon-row.selected` |
#### 3.2 Responsive Edge Cases
| # | Change | Files | Details |
|---|--------|-------|---------|
| 3.2a | Touch targets ≥44px on mobile | responsive.css | `.mobile-panel-close-btn` 36→44px, `.nudge-nav-btn` 34→44px at ≤1100px; `.collapsible-header` and `.section-toggle` min-height 44px at ≤600px |
| 3.2b | Container query fallback verified | — | `@container (max-width: 760px)` for addon rows has adequate fallback via ≤600px media query; no change needed |
#### 3.3 Mobile Experience Completeness
| # | Change | Files | Details |
|---|--------|-------|---------|
| 3.3a | Focus trap in mobile panel | mobile-sync.js | `trapFocus()` function keeps Tab cycling within open panel; focus moves to close button on open, returns to pill on close |
| 3.3b | Safe-area insets for notch phones | responsive.css | `padding-bottom: env(safe-area-inset-bottom)` on `.mobile-panel-sheet`; `right: max(14px, env(safe-area-inset-right))` on `.mobile-quote-pill` |
**GATE: 88/88 tests pass. All JS syntax-checked. CSS brace balance verified.**
---
### Phase 4: Documentation & QA
| # | Task | Status |
|---|------|--------|
| 4.1 | Update all docs (README, code-verification, quote-rules, phase-roadmap, QUICK-REF, MASTER-SESSION-PROMPT, ai-session-brief) | COMPLETE |
| 4.2 | Full regression checklist walkthrough | COMPLETE — 88/88 automated, 15/15 manual items verified in code |
| 4.3 | Beta definition of done verification | COMPLETE — all 13 criteria pass |
**Docs updated:**
- README.md — phase status, 88 tests, 4 themes, export description, file map (70retro.css added)
- code-verification.md — date, test count, all Phase 1-3 changes as known-good baseline
- quote-rules.md — onboarding manual override persistence, HST print behavior, JSON export rules, admin nudge
- phase-roadmap.md — Phases 1-4 status, 88 tests
- QUICK-REF.md — test count in "Remind User", 70retro.css in CSS file map
- MASTER-SESSION-PROMPT.md — 88 tests (3 occurrences), 4 themes (6 occurrences), 70retro.css in tree, 4 theme override layers
- ai-session-brief.md — test count updated
**Regression checklist results:**
- Automated: 88/88 pass
- Manual: All 15 items verified via source code review (admin waive displays, term/onboarding logic, manual override persistence, sidebar sync, mobile panel sync, persistence round-trip, reset behavior, print HST, JSON export, section headers, theme transitions, nudge crossfade, focus trap, safe-area insets, touch targets)
**Beta Definition of Done: ALL 13 CRITERIA PASS**
**GATE: PASSED — Beta build for Sections IIII is complete.**
---
### Phase 5: Performance & Accessibility Audit
| # | Fix | File(s) | Details |
|---|-----|---------|---------|
| A2 | `aria-expanded` on section & collapsible toggles | HTML, SVS-MSP-Calculator.js | Added `aria-expanded="false"` to 12 toggle elements; JS updates dynamically on toggle |
| A3 | Focus trap on reset confirm modal | quote-persistence.js | `trapFocusInModal()` — Tab cycles within modal when open |
| A4 | `aria-label` on stepper buttons | HTML | All 12 step-btn elements have descriptive labels (e.g. "Decrease users") |
| P1 | Glass theme scroll jank on mobile | glass.css | `background-attachment: scroll` at ≤1100px — avoids fixed-bg repaint on iOS |
| P2 | Skip mobile sync on desktop | mobile-sync.js | Guard skips 35+ element sync when panel closed on desktop; forces full sync on `openMobilePanel()` |
| M1 | `sidebarFocusClientName` not in sync map | mobile-sync.js | Added to html sync list — client name now updates in mobile panel |
| M2 | `sl-discount-detail` + `sl-value-onboarding-label` not in sync map | mobile-sync.js | Added to html sync list — contract term label and onboarding label now sync |
**Not flagged (clean):** Token coverage, `:focus-visible`, mobile focus trap, escape handling, touch targets, `will-change` usage, print CSS isolation, no unused JS.
**GATE: 88/88 tests pass. All fixes verified.**
### Font Awesome Icon Fix
| # | Fix | File | Details |
|---|-----|------|---------|
| FA1 | Icons invisible on `file://` protocol | components.css:44-79 | All 36 FA Sharp Solid SVG file references converted to inline `data:image/svg+xml` URIs — eliminates CORS/`file://` restriction on `mask-image: url()` |
**Root cause:** CSS `mask-image: url("fontawesomekit/svgs/...")` is blocked by browser security on the `file://` protocol. Inline data URIs bypass this completely.
**GATE: 88/88 tests pass. Icons render on local file open.**
---
### Phase 6: Code Quality Pass (Stage 3)
| # | Fix | File(s) | Details |
|---|-----|---------|---------|
| CQ1 | New `--sky` color token | tokens.css, light.css, glass.css, 70retro.css | Per-theme sky/info accent: Dark `#38bdf8`, Light `#0e7490`, Glass `#7dd3fc`, Retro `#a34a14` |
| CQ2 | New `--transition-fast` token | tokens.css | `0.15s` — replaces hardcoded timing in layout.css button transitions |
| CQ3 | Consolidated duplicate button CSS | layout.css:25-47 | `.btn-reset-quote` and `.btn-import-quote` shared 10 identical properties → merged into grouped selector |
| CQ4 | Hardcoded amber hover → token-derived | layout.css:43-46 | `rgba(232,146,15,…)``color-mix(in srgb, var(--amber) …%, transparent)` |
| CQ5 | Hardcoded sky blue hover → token-derived | layout.css:47-50, light.css, glass.css, 70retro.css | All `rgba(56,189,248,…)` / `#38bdf8` / `#7dd3fc` / `#a34a14``var(--sky)` + `color-mix()` |
| CQ6 | Dead null-check removed | quote-render.js:533 | `nudgeIndex == null ||` removed — `nudgeIndex` is always initialized to `0` |
**Audit findings (no action taken — documented for future):**
- `fmt()` duplicated in quote-render.js and quote-export.js (both inside IIFEs — intentional isolation, one-liner)
- Spacing magic numbers (14px/16px/20px) used 95+ times — too many touchpoints for surgical migration
- `console.warn()` statements in pricing/persistence/import are intentional error reporting
- No dead functions, no unreachable code, no unused exports across all 8 JS modules
**GATE: 88/88 tests pass. All 4 themes verified tokenized.**
---
### Phase 7: Test Coverage Expansion (Stage 4)
| # | Test Group | Count | Details |
|---|-----------|-------|---------|
| T1 | Pricing DEFAULTS integrity | 34 | All required keys exist, types correct, values match spec, frozen, ordering invariants |
| T2 | Engine edge cases & boundaries | 55 | Admin fee thresholds, large counts (100u/100ep), string coercion, invalid inputs (NaN/null/empty), servers-only, VoIP-only, VoIP edge cases, ZT without user addon, admin waived, all addons combined, BYOL term independence, discount rounding |
| T3 | Export JSON schema validation | 18 | Payload structure, field types, version field, contract term labels, licensing labels, pricing sub-object, voip tier null handling |
| T4 | Persistence state shape | 6 | JSON round-trip for strings/numbers/booleans, engine compatibility, zero-state |
| T5 | Import payload mapping | 12 | Contract term reverse-map, full export→import→engine round-trip (MRR, effectiveMrr, mrrWithHst, userTotal, endpointTotal, voipTotal, adminFeeNet, effectiveAnnual) |
| T6 | Quote output invariants | 24 | 6 configs × 4 invariants (effectiveMrr, effectiveAnnual, mrrWithHst, non-negative values) |
**Total: 88 → 250 tests (162 new). All passing.**
**GATE: 250/250 tests pass.**
---
### Phase 8: Enhanced Print/PDF (Stage 4)
| # | Enhancement | File(s) | Details |
|---|------------|---------|---------|
| P1 | Quote notes field | HTML:920, components.css, quote-persistence.js, quote-export.js, quote-import.js | `<textarea id="quoteNotes">` in sidebar, persisted in localStorage, included in JSON export/import, rendered on print invoice |
| P2 | Explicit validity date | quote-export.js | Computes 30-day expiry: "Valid until [date]" in print footer instead of generic "30 days" |
| P3 | Page break control | quote-export.js (inline CSS) | `page-break-inside:avoid` on table rows + `.tots-wrap`; `break-inside:avoid` on notes section |
| P4 | Rep name field | HTML:100, layout.css, quote-persistence.js, quote-export.js, quote-import.js | `<input id="repName">` below client name, persisted, in JSON export/import, shown in print header + footer |
| P5 | CYA "Not Included" section | quote-export.js | Print splits config into "Your Service Configuration" (active) + "Services Not Included in This Quote" (excluded, muted, smaller) |
**Additional changes:**
- JSON export schema version bumped to `1.1` (new `repName`, `quoteNotes` fields)
- JSON import handles new fields gracefully (backward-compatible with `1.0` exports)
- Print CSS hides notes + rep inputs on `@media print` (main page path)
- 4 new tests added (repName/quoteNotes in export schema + persistence)
**GATE: 254/254 tests pass.**
---
## Key Files to Read on Resume
1. `docs/MASTER-SESSION-PROMPT.md` — full architecture and constraints
2. `docs/QUICK-REF.md` — compact file map, IDs, pricing
3. `docs/regression-checklist.md` — test procedures
4. `.claude/plans/STAGE2-BUILD-PROMPT.md` — the build prompt driving this work
5. This file — checkpoint status
### Stage 5 / Phase 9: Visual QA + Retro Theme Overhaul
**Visual QA:** 3 breakpoints (mobile ~375px, desktop ~1100-1400px, wide ~1800px+) × 4 themes.
| Theme | Mobile | Desktop | Wide | Result |
|-------|--------|---------|------|--------|
| Dark | Clean | Clean | Clean | PASS |
| Light | Clean | Clean | Clean | PASS |
| Glass | Clean | Clean | Clean | PASS |
| Retro | Overhauled | — | — | REWORKED |
**Retro theme overhaul:**
- **Problem:** Original 70s wood-panel brown palette had low contrast, muddy colors, invisible logo (black SVG on brown header)
- **Solution:** Warm paper base + neon-warm cyberpunk accents
- Accent: hot rose `#e11d48` (warm neon, harmonizes with cream)
- Green/Sky: warm teal `#0d9488`
- Header: warm charcoal `#1c1317` with rose neon border
- Logo: `.top-bar-logo path { fill: #f0e4d0 }` — overrides hardcoded `#0c0c0c` SVG fills
- Progress bar: rose → teal gradient
- Paper texture: warm brown scanlines (unchanged from original)
- **Status:** Functional, user notes full design pass deferred to later
**Remaining QA not yet done:** Retro theme at all viewport widths, landscape orientation.
**GATE: 254/254 tests pass. No visual bugs found on Dark/Light/Glass.**
---
### Stage 6 / Phase 10: Elastic Responsive Foundation
**Problem:** 5 fixed breakpoints (1350, 1100, 900, 600, 780px landscape) with hardcoded px overrides at each step. Max width capped at 1800px — wasted space on 1440p+ monitors.
**Solution:** Fluid `clamp()` tokens replace discrete breakpoint steps. Only structural breakpoints remain.
| # | Change | File(s) | Details |
|---|--------|---------|---------|
| E1 | Fluid layout tokens | tokens.css | `--page-max-width: clamp(1200px, 92vw, 2400px)`, `--page-gutter-x: clamp(16px, 3vw, 80px)`, `--layout-column-gap: clamp(24px, 3vw, 56px)`, sidebar min 400→360px |
| E2 | Fluid section tokens | tokens.css | `--section-offset: clamp(52px, 7vw, 104px)`, `--section-num-width/size` fluid, `--section-padding-*` fluid |
| E3 | Eliminated 1350px breakpoint | responsive.css | Removed — fluid tokens handle narrow desktop scaling |
| E4 | Eliminated 900px breakpoint | responsive.css | Removed — fluid tokens handle tablet spacing/numerals |
| E5 | Fluid logo margin | base.css | `margin-left: clamp(26px, 5.2vw, 78px)` replaces hardcoded 78px + breakpoint overrides |
| E6 | Fluid main-col gap | layout.css | `gap: clamp(16px, 1.5vw, 24px)` replaces hardcoded 24px + breakpoint override |
| E7 | Fluid client-bar padding | layout.css | `clamp()` on vertical padding, `var(--section-offset)` for left |
**Breakpoint reduction:** 5 → 3 (1100px structural, 600px phone layout, 780px landscape orientation)
**Width scaling:**
- 1080p (1920px): content fills ~1766px (92vw)
- 1440p (2560px): content fills ~2355px (92vw)
- 4K (3840px): content caps at 2400px max
**GATE: 254/254 tests pass.**
---
### Stage 7 / Phase 11: Feature Work (Option A)
#### 11.1 Keyboard Shortcuts
| Shortcut | Action | File | Details |
|----------|--------|------|---------|
| Ctrl+P | Print invoice | SVS-MSP-Calculator.js | `preventDefault()` blocks browser print dialog; calls `printInvoice()` |
| Ctrl+E | Export JSON | SVS-MSP-Calculator.js | Calls `exportQuoteJSON()` |
| Ctrl+R | Reset quote | SVS-MSP-Calculator.js | Opens confirm modal via `openResetConfirm()` — not a hard reset |
| Escape | Close overlays | mobile-sync.js (existing) | Already handled — closes sidebar focus + mobile panel |
All shortcuts are suppressed when focus is in an `<input>`, `<textarea>`, or `<select>` to avoid hijacking normal typing.
#### 11.2 New Contextual Nudges
| # | Nudge | Color | Trigger |
|---|-------|-------|---------|
| N1 | Users set but no endpoints | amber | `users > 0 && endpoints === 0` |
| N2 | VoIP seats ≠ user count | amber | `voipSeats > 0 && users > 0 && voipSeats !== users` |
| N3 | High admin-to-MRR ratio | amber | `adminFeeNet > MRR * 0.25` (and not waived) |
| N4 | Extended Hours upsell | green | `!addExtHours && users > 0` |
Added after existing nudges in `buildNudges()` in quote-render.js (lines 524551).
**GATE: 254/254 tests pass.**
---
### Stage 8 / Phase 12: Code Quality Pass II
#### 8.1 `--transition-fast` / `--transition-medium` Token Adoption
| # | Change | File(s) | Details |
|---|--------|---------|---------|
| T1 | New `--transition-medium` token | tokens.css | `0.25s` — for chevron/collapsible/nudge transforms |
| T2 | 10× `0.15s``var(--transition-fast)` | components.css | pill-toggle, tier-seg, addon-preview-pill, addon checkbox, sidebar-focus-toggle, nudge-nav-btn, btn-toggle-all, quote-notes-input, btn-export |
| T3 | 3× `0.25s``var(--transition-medium)` | components.css | sec-chevron transform, collapsible-toggle transform, nudge-banner bg/border |
| T4 | 1× `0.15s``var(--transition-fast)` | base.css | theme-toggle-btn |
| T5 | 2× `0.15s``var(--transition-fast)` | responsive.css | mobile-quote-pill, mobile-panel-close-btn |
**Left as-is:** 0.12s (stepper/addon micro-interactions), 0.18s (term tile tuned), 0.2s (switch/section/overlay), 0.3s (progress bar/accordion), 0.34s (section-body tuned bezier). No `0.15s` hardcodes remain outside the token definition.
#### 8.2 CSS Selector Specificity Audit
| # | Change | File(s) | Details |
|---|--------|---------|---------|
| S1 | `.sec-open``.section.sec-open` | components.css | Removed 2× `!important` — specificity now beats `.section:hover` via class count |
| S2 | Documented intentional `!important` | components.css | Added comments to `.qs-discount-sub`, sidebar utility classes (`.sl-muted`, `.sl-discount-val`, `.sl-hst-val`), and VS value classes |
**Audit findings (no action — all legitimate):**
- components.css: 13 remaining `!important` — all utility `display: none` or color overrides that must beat compound parent selectors
- 70retro.css: 37 `!important` — theme override pattern (same as glass.css with 97)
- responsive.css: 8 `!important` — mobile sidebar embedding
- tokens.css: 1 `!important``body.theme-transitioning` (intentional, per spec)
- print.css: All `!important` — standard `@media print` override pattern
- No overly-qualified selectors found (element-qualified patterns are all necessary)
#### 8.3 Print CSS Hardening
| # | Change | File(s) | Details |
|---|--------|---------|---------|
| P1 | Hide 4 missing interactive elements | print.css | Added `display: none !important` for `.sidebar-focus-toggle`, `.sidebar-utility`, `.qs-switch`, `.confirm-modal` |
| P2 | Theme-independent callout borders | tokens.css, print.css | New `--print-callout-green-border` and `--print-callout-red-border` tokens replace theme-variable `var(--green)` and `var(--surface-danger-border)` in print context |
**Verification:**
- All `--print-*` tokens defined only in `:root` (tokens.css) — no theme overrides
- Page-break rules unaffected by fluid layout tokens (`.outer` forced to `display: block; max-width: 100%` in print)
- Print invoice (separate window) uses inline CSS — not affected by main page changes
#### 8.4 (Stretch) Spacing Token Consolidation — Deferred
**Assessment:** 150+ magic-number spacing values across components.css (10px: 36, 12px: 35, 14px: 36, 16px: 24, 20px: 19). Existing `--space-stack-*` tokens used only 4× out of 150+. Migration scope too broad for surgical approach. Deferred to a dedicated spacing-focused stage.
**GATE: 254/254 tests pass.**
---
### Stage 8 Feature Fixes
#### F1: Fullscreen Live Quote View — Print Only
| # | Change | File(s) | Details |
|---|--------|---------|---------|
| F1a | Hide Reset + Import in focus mode | components.css | `.export-wrap` and `.sidebar-utility` now `display: none` in sidebar-focus-open |
| F1b | Print button inside sidebar header | HTML, components.css | New `.sidebar-focus-print-btn` in `.sidebar-header-row` — hidden by default, `display: inline-flex` in focus mode |
| F1c | Print button hidden in print/mobile | print.css, components.css | `display: none !important` in `@media print` and `.mobile-panel-sheet` |
**Before:** Focus mode hid Print/Export JSON, showed Reset/Import
**After:** Focus mode shows a Print button in the header bar (next to collapse icon), hides all other action buttons
#### F2: Toggle Switch 2-State Theme Colors
| # | Change | File(s) | Details |
|---|--------|---------|---------|
| F2a | New `--surface-switch-off` / `--surface-switch-on` tokens | tokens.css | Dark: off `#4a4540`, on `var(--green)` |
| F2b | Light theme switch tokens | light.css | Off `#b5ad9f`, on `var(--green)` |
| F2c | Glass theme switch tokens | glass.css | Off `rgba(255,255,255,0.15)`, on `var(--green)` |
| F2d | Retro theme switch tokens | 70retro.css | Off `#c0b4a0`, on `var(--green)` |
| F2e | Component CSS uses tokens | components.css | `.qs-switch` bg → `var(--surface-switch-off)`, checked → `var(--surface-switch-on)` |
| F2f | Glass checked override | glass.css | Added `.qs-toggle-row input:checked ~ .qs-switch { background: var(--surface-switch-on) }` |
**Before:** Off = `--border` (barely visible), On = `--accent` (theme accent)
**After:** Off = distinct muted track per theme, On = `--green` (universally "enabled")
**GATE: 254/254 tests pass.**
#### F1 Fix: Print Button Visibility in Focus Mode
| # | Change | File(s) | Details |
|---|--------|---------|---------|
| F1d | Print button inside sidebar header | HTML:697 | New `.sidebar-focus-print-btn` button in `.sidebar-header-row`, between title and collapse icon |
| F1e | Focus-only visibility | components.css | `display: none` by default; `display: inline-flex` when `body.sidebar-focus-open` |
| F1f | Hidden in print + mobile | print.css, components.css | `display: none !important` in `@media print` and `.mobile-panel-sheet` |
**Root cause:** `.sidebar-utility` is a sibling of `.sidebar`, not inside it. When `.sidebar` becomes `position: fixed`, the utility div is left behind the backdrop.
#### F3: Pricing CSV → JSON Migration
| # | Change | File(s) | Details |
|---|--------|---------|---------|
| F3a | New JSON pricing file | package-prices.json | Structured by category with `{ key: { value, description } }` format — human-readable + machine-parseable |
| F3b | Script-loaded pricing | package-prices-data.js, HTML | `window.SVS_PRICING_DATA` set via `<script>` tag — works on `file://` protocol, no web server needed |
| F3c | Loader updated | quote-pricing.js | `loadPricing()` checks `SVS_PRICING_DATA` global first (script path), then `fetch()` fallback (web server), then built-in defaults |
| F3d | CSV retained | package-prices.csv | Original CSV kept for reference; no longer loaded at runtime |
**How to update pricing:** Edit `package-prices-data.js` — change the `value` field for any key. No web server needed. The file is loaded via `<script>` tag before the pricing engine initializes.
**JSON format example:**
```json
{
"user_packages": {
"RATE_M365": { "value": 130, "description": "Per-user/mo rate — M365 included" }
}
}
```
**GATE: 254/254 tests pass.**
---
## Hard Constraints (reminder)
1. DOM IDs are a contract — no renaming
2. 254 tests must pass: `node svsmspcalc/tests/test-quote-engine.js`
3. localStorage keys unchanged
4. All 4 themes must work after every change
5. Mobile parity maintained
6. No frameworks, no npm — vanilla only
7. Surgical changes only
8. Sections IVVI unchanged (deferred)