9.6 KiB
VERDICT
Hand-built static site that largely executes the "Sonoran Ledger" direction (warm paper #F7F3EB/#1C1814, sparse --accent orange as ink only, mono tabular numerals for money, 2px sharp corners, repeating-linear-gradient ledger rulings at 24px baseline). It is not generic AI-slop in the visual layer — custom tokens, no framework, hand-rolled selects/switches/steppers, sectional rulings, split trust numerals, and explicit "open book" copy differentiate it. However, the information architecture (hero/trust/story/services/pricing/calc/faq/contact/footer) is the standard MSP marketing skeleton, and several real defects exist, especially in JS behavior and mobile.
Prioritized by the requested hunt areas. All analysis from the three full file reads. No modifications performed.
index.html
[ERROR] Mobile navigation breakage. At <=880px, .nav .nav__link are hidden via CSS with no hamburger, no remaining in-nav anchors, and no alternative (e.g., footer or select). Only the phone number and theme toggle survive in the header. Sections #services, #pricing, #calculator, #faq become unreachable without scrolling the entire page on phones/tablets. (cross-ref css/styles.css:154)
[ERROR] "Send me this estimate" (id="sendEstimate") is a plain <a href="#contact">. It performs scroll only; calculator state (ledger lines, total) is never injected into #cf-msg or any hidden field. The contact form and estimator are completely disconnected. (index.html:362)
[WARN] Schema.org LocalBusiness "image" and the two <img> src values point to "assets/images/hero.png" / "assets/images/story.png" (relative, no root guarantee). If served from a subpath the images 404 and JSON-LD is invalid. Width/height present on imgs (good). (index.html:20,82,124)
[WARN] Accordion (FAQ) buttons carry aria-expanded correctly, but the answer containers lack role="region", id, or aria-labelledby wiring back to the button. Content remains in DOM (good for search), but grouping is weak for assistive tech. (index.html:377-407)
[INFO] Semantics otherwise strong: <main>, skip link, section+aria-labelledby, table+caption+scope="col", label+for on all form controls (including the custom switch and number inputs), tel/mailto, descriptive alts on the two photos, novalidate + custom validation on contact. Trust strip uses a labeled section + grid of cells (not a list or dl, but acceptable). Inline style tweaks exist on a few containers for spacing (minor hygiene).
[INFO] No obvious duplicate IDs, broken heading order, or missing lang/color-scheme. The <span id="top"> inside main is harmless but odd.
css/styles.css
[INFO] Tokens and dark override correctly implement the spec: --paper/--surface warm, --rule/--rule-soft for lines, --accent/#BD5A00 (light) and #F2A24E/#F4A85C (dark) used only for marks, flags, underlines (.ul), section-tag::before rules, brand mark, hover states, and selection. No heavy rounded corners (2px max), no large blurs except header. .mono + font-variant-numeric: tabular-nums on all money. Ledger ruling via precise repeating-linear-gradient (23px transparent + 1px at 24px) on .ledger sections only. (css/styles.css:9-46,184-189,254, etc.)
[WARN] Contrast. Tokens comment claims AA for --accent-ink. Core --ink on --paper is high-contrast. --ink-2 muted (#5A5148 light / #C4B8A3 dark) on body copy and small labels is borderline, especially at 0.78-0.92rem. Rule lines (rgba 0.16 / 0.08) and ledger rulings are intentionally faint; in dark mode or outdoors they become nearly invisible. This undercuts the "premium ledger" readability claim on real devices. (css/styles.css:13,38,101,186)
[WARN] Mobile risks beyond nav. Stepper buttons fixed at 34px wide (sub-44px touch minimum). Many breakpoints (880/860/820/720/680) but no container queries; tables and grids can squeeze (right-aligned .num columns in plan-table). Header sticky + backdrop-filter is modern but the surviving mobile nav content (phone + 40px toggle) can feel cramped. No focus-visible beyond the form-control outline rule. (css/styles.css:154,288-292,264-269,346-349,382)
[INFO] Distinctive execution, not templated. Custom chevron-less selects (bg-image linear gradients), custom .switch, custom .stepper (spinners explicitly killed), grid-gap + background-rule trick for trust separators, reveal using IntersectionObserver, section tags with accent rule, .tier--pop flag, .svc continuous ruled list instead of cards. The visual system fights generic. However, the overall page skeleton, "Most chosen" ribbon, trust strip, live two-pane estimator, and +/x FAQ are still recognizable marketing patterns.
[OK] Reduced-motion, box-sizing, and basic reset present. Body transitions limited to bg/color.
js/app.js
[ERROR] Stepper bounds + direct edit desync (core bug). Stepper click path (js/app.js:139) does Math.max(0, Math.min(500, v + dir)) and writes back. But intVal (67-72) only clamps the read value for calculations (if (v > 500) v = 500; return v;) and never mutates el.value. Typing 600 (or -5) into an endpoints/m365/voip field leaves the input showing the out-of-range number while every recalc, line, total, and per-endpoint uses the clamped 500/0. Subsequent stepper clicks read the lying input.value. (js/app.js:67-72,136-143)
[ERROR] Calculator "per endpoint" math/presentation. per = endpoints > 0 ? total / endpoints : 0 (125) then unconditionally renders "X per endpoint / mo". Total includes large fixed adders (support plan 200-850, equip 25, m365, voip, hosting). The displayed per-endpoint figure therefore amortizes the support plan etc. across seats. This contradicts the page claim "the same math we'd walk you through in person" and "per-endpoint pricing". Arithmetic is correct; the semantics of the output are misleading when any non-endpoint item is selected. (js/app.js:119,125-128)
[WARN] Support-plan line exactly as flagged. var support = numSelect("support"); (94) pulls the raw <option value="380"> price. It is passed verbatim as the cost argument to lineRow (114): lineRow((supportName || "Support plan") + " support", ..., support). The same raw value is added directly into total (119). For the "None" case (value 0) this produces the label "Support plan support" (supportName falls to "") with cost 0 (hidden by .is-zero). Functionally the total is correct (flat fee), but it is the only line that does not compute cost from a rate; it trusts the select value as both identifier and dollar amount. (js/app.js:94,105-107,114,119)
[WARN] FAQ max-height on resize is present but fragile. Click handler (159) sets panel.style.maxHeight = (inner.offsetHeight + 8) + "px". Resize listener (163-171) re-queries every open panel and does the same. Issues: magic +8, only updates currently open panels, measures at event time (font loading, subpixel, zoom, or post-open reflow can still clip or leave extra space), no removal of style on close beyond hard "0px", no MutationObserver for content changes. Works for static text on this page but is a classic source of cut-off answers. (js/app.js:152-171)
[INFO] Theme persistence. STORE="acg-theme", localStorage read on load, write on toggle, OS prefers listener that only applies when no explicit saved value (40-48). Logic is correct. Weaknesses: initial <html data-theme="light"> + script at very end of body guarantees a flash of the wrong theme on first paint for users with dark saved or OS dark. No early inline script or critical CSS. (js/app.js:12-49)
[INFO] Contact validation (demo level). Trims name + cf-contact only (181). No format check on the "phone or email" field. On error sets accent color + message + focus. On success sets good color + thanks + reset(). Note text/color survives the reset until next submit. No aria-live on #formNote (calc side has aria-live="polite" on the output shell). Correct for a static demo; insufficient for production. (js/app.js:173-192)
[OK] Remaining calculator paths: money() rounds, no NaN escapes in the hot path, form submit prevented, input+change both trigger recalc, steppers call recalc after write. Ledger hides zero lines. No other arithmetic defects found.
Short ranked list of the top fixes that matter (real defects only, highest impact first)
- Mobile nav: .nav__link {display:none} at 880px with no menu or persistent anchors — primary navigation is lost on phones.
- Stepper direct-edit desync: intVal clamps for math only; input.value can show values outside 0-500 while all outputs use the clamped number.
- Per-endpoint display math: total (with fixed support/equip/etc.) is divided by endpoints and labeled "per endpoint / mo" — misrepresents the per-endpoint claim.
- FAQ max-height: offsetHeight + 8 px snapshots on resize only for open items; content can clip after reflow, font load, or width change.
- Theme flash + late application: hardcoded light on <html>, script at bottom; dark users see wrong theme before JS runs.
- Touch targets: stepper +/- buttons locked at 34px wide.
- Estimate-to-contact handoff missing: the CTA only scrolls; no data transfer occurs.
- Support line label: raw price passed as cost produces "Support plan support" (value 0 case) and couples the select value to both identity and dollars.
- Contact status region has no aria-live; results are invisible to screen readers.
- Faint ledger rulings + muted text contrast: especially in dark mode; undercuts premium paper/ink readability.
The rest are nits (inline styles, brittle asset paths in JSON-LD, missing focus-visible on .btn, etc.). The site is closer to a deliberate craft artifact than a generic template, but the defects above are functional or accessibility issues that affect real use.