Files
boc/reports/design-audit-2026-06-20.md
T

609 lines
18 KiB
Markdown
Raw Normal View History

# Design Audit — quiXzoom × Landvex
**Date:** 2026-06-20
**Auditor:** Bernt (Principal Product Designer / Design Systems Lead)
**Scope:** quiXzoom.com + Landvex.com — production-grade responsive, accessibility, consistency, enterprise-readiness
---
## AUDIT HEADER
| Property | quiXzoom | Landvex |
|---|---|---|
| Stack | Vanilla CSS + Vanilla JS | Vanilla CSS + Vanilla JS |
| Stylesheet | /styles.css (single file) | Inline `<style>` (no external CSS file) |
| Framework | None | None |
| CSS approach | Custom properties + BEM-like classes | Custom properties + utility classes |
| Overflow elements (390px) | 3 real + 1 false positive | 15+ (footer = 2859px wide) |
| Overflow elements (1440px) | 0 | 2859px footer |
| Mobile-menu bug | `#mobile-menu` visible on desktop | N/A |
| Social link icons | ❌ quiXzoom: raw URLs | ✅ Landvex: SVG icons |
**Totals:**
- 🔴 Critical: 4
- 🟠 Major: 6
- 🟡 Minor: 4
---
## 🔴 CRITICAL DEFECTS
---
### C-001 — `#mobile-menu` visible on desktop (quiXzoom)
- **URL:** https://www.quixzoom.com/
- **Viewport:** 1440px desktop
- **Selector:** `#mobile-menu`
- **Computed CSS:** `display: block; height: 26px; position: static; top: 69px`
- **Rotorsak:** `#mobile-menu` lacks a `display: none` rule at viewport > 768px. The element renders as a second nav row below the sticky header, showing all nav links as unstyled plain text.
- **Visual impact:** Desktop nav appears broken — two rows, second row shows raw link text without styling.
**Fix:**
```css
/* In styles.css — add to desktop breakpoint (already exists for .hamburger) */
@media (min-width: 769px) {
#mobile-menu {
display: none !important;
}
}
```
---
### C-002 — Hamburger button 4×4px rendered on desktop (quiXzoom)
- **URL:** https://www.quixzoom.com/
- **Viewport:** 1440px desktop
- **Selector:** `button.hamburger`
- **Computed CSS:** `display: block; width: 4px; height: 4px`
- **Rotorsak:** `.hamburger` is not hidden at desktop breakpoints. It renders as a 4×4px invisible dot in the nav right area — a non-functional touch target that confuses layout.
**Fix:**
```css
@media (min-width: 769px) {
.hamburger {
display: none;
}
}
```
---
### C-003 — `.footer-links` 2859px overflow (Landvex)
- **URL:** https://www.landvex.com/
- **Viewport:** All viewports (780px measured, same at 390px and 1440px)
- **Selector:** `.footer-links`
- **Computed CSS:** `display: flex; flex-wrap: nowrap; gap: 24px; width: auto`
- **Rotorsak:** 34 `<a>` children in a single `flex nowrap` row. Total width: 34 × ~50px + 33 × 24px gap ≈ 2492px. Causes entire page horizontal scroll.
**Fix:**
```css
.footer-links {
flex-wrap: wrap;
gap: 12px 24px; /* row-gap 12px, column-gap 24px */
}
```
---
### C-004 — `.footer-legal-links` overflow on mobile (quiXzoom)
- **URL:** https://www.quixzoom.com/
- **Viewport:** 390px mobile (right: 476px, 86px outside viewport)
- **Selector:** `.footer-legal-links`
- **Computed CSS:** `display: flex; flex-wrap: nowrap; gap: 20px; width: auto`
- **Rotorsak:** 7 link items × ~50px + 6 × 20px gap = 470px in a 390px viewport. No wrapping.
**Fix:**
```css
.footer-legal-links {
flex-wrap: wrap;
gap: 8px 16px;
justify-content: center;
}
```
---
## 🟠 MAJOR DEFECTS
---
### M-001 — quiXzoom social footer links = raw URLs (quiXzoom)
- **URL:** https://www.quixzoom.com/
- **Viewport:** All
- **Selector:** `footer a[href*="linkedin"], footer a[href*="twitter"], footer a[href*="instagram"]`
- **Computed CSS:** N/A — content issue
- **Rotorsak:** Social links render as bare `https://www.linkedin.com/company/quixzoom` text — no icon, no label. Landvex has SVG icons for the same links. Cross-site inconsistency + enterprise credibility failure.
**Fix (HTML — quiXzoom footer social links):**
```html
<!-- Replace bare <a href="https://www.linkedin.com/company/quixzoom">https://...</a> with: -->
<a href="https://www.linkedin.com/company/quixzoom" aria-label="quiXzoom on LinkedIn" rel="noopener noreferrer" target="_blank">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/>
<rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/>
</svg>
<span class="sr-only">LinkedIn</span>
</a>
<a href="https://x.com/quixzoom" aria-label="quiXzoom on X" rel="noopener noreferrer" target="_blank">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.743l7.73-8.835L1.254 2.25H8.08l4.253 5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
<span class="sr-only">X (Twitter)</span>
</a>
```
```css
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
```
---
### M-002 — Landvex desktop nav has no navigation links
- **URL:** https://www.landvex.com/
- **Viewport:** 1440px desktop
- **Selector:** `nav` (Landvex)
- **Computed CSS:** Nav renders logo + "Get in touch" CTA only — no navigation links visible
- **Rotorsak:** Navigation items are not present in the rendered nav HTML on desktop. Either rendered only in a hidden mobile menu (not verified), or missing entirely from the template.
**Fix:** Add primary navigation links to Landvex desktop nav:
```html
<nav>
<a href="/" class="nav-logo">LandveX</a>
<ul class="nav-links">
<li><a href="/methodology/">Methodology</a></li>
<li><a href="/verticals/">Solutions</a></li>
<li><a href="/cities/">Cities</a></li>
<li><a href="/about/">About</a></li>
</ul>
<a href="/contact/" class="nav-cta">Get in touch</a>
</nav>
```
```css
@media (min-width: 769px) {
.nav-links {
display: flex;
gap: 32px;
list-style: none;
}
}
```
---
### M-003 — Stat rows do not wrap at < 480px (quiXzoom)
- **URL:** https://www.quixzoom.com/
- **Viewport:** 390px mobile
- **Selector:** `.hstat` parent container (hero stats: "1280 / 20+ / 0%")
- **Rotorsak:** Stat items in flex row, no wrap defined. At 390px the 3-column row is borderline; at 320px it breaks.
**Fix:**
```css
.hero-stats, /* or whichever parent wraps .hstat elements */
[class*="stat-row"] {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 16px;
}
```
---
### M-004 — `.community-pipeline-badge` white-space:nowrap on mobile (quiXzoom)
- **URL:** https://www.quixzoom.com/
- **Viewport:** 390px mobile
- **Selector:** `.community-pipeline-badge`
- **Computed CSS:** `white-space: nowrap; display: block; width: 315px`
- **Rotorsak:** Badge text "Field observation → Mission → Verified data" is `nowrap` and 315px wide. At 320px this overflows. Parent `.community-pipeline` is 343px — also exceeds 320px viewport.
**Fix:**
```css
.community-pipeline-badge {
white-space: normal;
overflow-wrap: anywhere;
}
.community-pipeline {
max-width: 100%;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
```
---
### M-005 — Focus styles absent on interactive elements (both sites)
- **URL:** https://www.quixzoom.com/ + https://www.landvex.com/
- **Viewport:** All
- **Selector:** `a, button, input, select, textarea` (all interactive elements)
- **Computed CSS at :focus-visible:** `outline-width: 0px; box-shadow: none`
- **Rotorsak:** No `:focus-visible` styles defined. WCAG 2.2 SC 2.4.11 (Focus Appearance) requires visible focus indicators.
**Fix:**
```css
/* Add to both sites' stylesheets */
:focus-visible {
outline: 2px solid #0066FF;
outline-offset: 2px;
border-radius: 4px;
}
/* Suppress for mouse users only */
:focus:not(:focus-visible) {
outline: none;
}
```
---
### M-006 — Footer 34-link flat list — no semantic grouping (Landvex)
- **URL:** https://www.landvex.com/
- **Viewport:** All
- **Selector:** `.footer-links`
- **Rotorsak:** 34 links in a single flat `<div>` with no grouping, no `<nav>` landmark, no section headers. Enterprise footers require grouped columns (Legal / Product / Company / Cities).
**Fix (HTML structure):**
```html
<footer>
<div class="footer-grid">
<nav aria-label="Legal" class="footer-col">
<h3 class="footer-col-title">Legal</h3>
<a href="/privacy/">Privacy</a>
<a href="/terms/">Terms</a>
<a href="/cookie-policy/">Cookies</a>
<a href="/dpa/">DPA</a>
<a href="/compliance/">Compliance</a>
</nav>
<nav aria-label="Product" class="footer-col">
<h3 class="footer-col-title">Product</h3>
<a href="/methodology/">Methodology</a>
<a href="/data-quality/">Data Quality</a>
<a href="/coverage/">Coverage</a>
<a href="/sla/">SLA</a>
</nav>
<nav aria-label="Cities" class="footer-col">
<h3 class="footer-col-title">Cities</h3>
<a href="/cities/stockholm/">Stockholm</a>
<!-- etc -->
</nav>
<nav aria-label="Company" class="footer-col">
<h3 class="footer-col-title">Company</h3>
<a href="/about/">About</a>
<a href="/careers/">Careers</a>
<a href="/blog/">Insights</a>
<a href="/press/">Press</a>
</nav>
</div>
</footer>
```
```css
.footer-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 40px;
}
.footer-col {
display: flex;
flex-direction: column;
gap: 8px;
}
.footer-col-title {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted, #6B6B6B);
margin-bottom: 8px;
}
```
---
## 🟡 MINOR DEFECTS
---
### m-001 — `html,body` missing `overflow-x: clip` safety net (both sites)
- **URL:** Both
- **Rotorsak:** After root-cause fixes, `overflow-x: clip` should be added as final protection.
**Fix (add AFTER root causes fixed above):**
```css
html,
body {
overflow-x: clip;
}
```
---
### m-002 — Milestone chain label mismatch (quiXzoom)
- **URL:** https://www.quixzoom.com/
- **Rotorsak:** DOM contains `.community-pipeline-badge` with text "10 → 25 → 50 → 100 → 250 → 500" (not "10K" as described in PDF). The text body references "World Builder at 10,000" — the badge does not show full chain.
**Fix:** Badge should show abbreviated chain or scroll on mobile:
```css
.community-pipeline-badge {
white-space: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
max-width: 100%;
}
```
---
### m-003 — Viewport scrollbar width causes 15px layout shift (quiXzoom 1440px)
- **URL:** https://www.quixzoom.com/
- **Viewport:** 1440px
- **Rotorsak:** `document.documentElement.scrollWidth: 1425` vs `window.innerWidth: 1440` — 15px difference = scrollbar width eating into layout. Container uses `max-width: 1160px + 24px padding` = 1208px. Not a real overflow, but can cause CLS.
**Fix:**
```css
.container {
padding-inline: max(24px, calc((100vw - 1160px) / 2));
}
/* Or use scrollbar-gutter to reserve space */
html {
scrollbar-gutter: stable;
}
```
---
### m-004 — Hero sub-text references old wording (quiXzoom)
- **URL:** https://www.quixzoom.com/
- **Selector:** `.hero-sub` (p tag in hero)
- **Computed content:** "Earn QX Credits per approved submission. Weekly payouts — no platform fee. Launching August 1, 2026."
- **Rotorsak:** "Weekly payouts" is the old wording — was updated in index.html but may not have been invalidated in CDN cache.
**Fix:** CDN cache invalidation for `/` and verify live origin serves updated copy.
---
## ACCESSIBILITY REPORT (WCAG 2.2 AA)
### A-001 — No :focus-visible on any interactive element (CRITICAL)
- **WCAG SC:** 2.4.11 Focus Appearance (AA, new in 2.2)
- **Selector:** All `a, button, input, select, textarea`
- **Fix:** See M-005 above
### A-002 — Hamburger button has no accessible label (quiXzoom)
- **Selector:** `button.hamburger`
- **Issue:** No `aria-label`, no visible text, no `aria-expanded`
- **Fix:**
```html
<button class="hamburger" aria-label="Open navigation" aria-expanded="false" aria-controls="mobile-menu">
<!-- SVG icon -->
</button>
```
```js
// Toggle aria-expanded on click
document.querySelector('.hamburger').addEventListener('click', function() {
const expanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', String(!expanded));
});
```
### A-003 — `<main>` id="main-content" present, skip-link present (quiXzoom) ✅
- Skip-to-main-content link exists (`top: -100px`, activated on focus). Correct.
### A-004 — Touch targets ≥ 44×44px
- Hamburger: 38×32px (quiXzoom) — below WCAG 2.2 minimum 44×44px
- **Fix:**
```css
.hamburger {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
```
---
## CONSISTENCY MATRIX
| Element | quiXzoom homepage | quiXzoom /earn/ | Landvex homepage | Status |
|---|---|---|---|---|
| Nav: desktop links | ✅ (.nav-links visible) | Unknown | ❌ No nav links | INCONSISTENT |
| Nav: mobile hamburger | ✅ 38×32px | Unknown | Unknown | — |
| Footer: social icons | ❌ Raw URLs | Unknown | ✅ SVG icons | INCONSISTENT |
| Footer: legal links | 7 links, flex nowrap | Unknown | 34 links, flex nowrap | SAME BUG |
| Footer: copyright | Not extracted | Unknown | "© 2026 Landvex Inc" | — |
| Credits value (hero) | 1280 (fixed) | 1280 | N/A | CONSISTENT ✅ |
| Currency | USD ($28 floating card) | Unknown | N/A | OK ✅ |
| Dark theme bg | #0a0a0a | Unknown | #0a0a0a | CONSISTENT ✅ |
| Accent blue | #0066FF | Unknown | #0066FF | CONSISTENT ✅ |
---
## DESIGN TOKEN SYSTEM
```css
/* =============================================
SHARED TOKEN SYSTEM — quiXzoom + Landvex
Apply to :root in both sites' stylesheets
============================================= */
:root {
/* === COLORS === */
--color-bg-primary: #0a0a0a;
--color-bg-secondary: #111111;
--color-bg-surface: #181818;
--color-bg-white: #FFFFFF;
--color-bg-off: #F7F7F5;
--color-accent: #0066FF;
--color-accent-dark: #0052CC;
--color-accent-pale: rgba(0, 102, 255, 0.07);
--color-success: #00C853;
--color-warning: #FF9800;
--color-error: #FF3B30;
--color-text-primary: #FFFFFF;
--color-text-body: #3a3a3a;
--color-text-muted: #6B6B6B;
--color-text-dim: #888888;
--color-text-dark: #0a0a0a;
--color-border: rgba(255, 255, 255, 0.07);
--color-border-light: rgba(0, 0, 0, 0.1);
/* === TYPOGRAPHY === */
--font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Helvetica Neue', sans-serif;
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.375rem; /* 22px */
--text-2xl: 1.75rem; /* 28px */
--text-3xl: 2.25rem; /* 36px */
--text-4xl: 3rem; /* 48px */
--text-hero: clamp(2.75rem, 6vw, 5rem);
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--font-weight-black: 900;
--line-height-tight: 1.1;
--line-height-snug: 1.3;
--line-height-normal: 1.65;
--line-height-relaxed: 1.8;
--measure: 65ch; /* max line length for body text */
/* === SPACING (4px base) === */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
--space-24: 96px;
/* === BORDER RADIUS === */
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 24px;
--radius-xl: 32px;
--radius-full: 100px;
/* === SHADOWS === */
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.12);
--shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.14);
--shadow-hover: 0 16px 48px rgba(0, 0, 0, 0.14);
/* === BREAKPOINTS (use in @media queries) === */
/* --bp-xs: 320px */
/* --bp-sm: 390px */
/* --bp-md: 768px */
/* --bp-lg: 1024px */
/* --bp-xl: 1280px */
/* --bp-2xl: 1440px */
/* --bp-3xl: 1920px */
/* === LAYOUT === */
--container-max: 1160px;
--container-pad: 24px;
--nav-height: 68px;
--section-pad: 96px;
/* === TRANSITIONS === */
--transition-base: 0.22s cubic-bezier(0.25, 0.46, 0.45, 0.94);
--transition-fast: 0.15s ease;
}
```
---
## IMPLEMENTATION ROADMAP
### Priority 1 — Overflow & Layout Crashes (fix immediately)
| ID | Fix | File | Est |
|---|---|---|---|
| C-001 | Hide `#mobile-menu` on desktop | quiXzoom/styles.css | 2 min |
| C-002 | Hide `.hamburger` on desktop | quiXzoom/styles.css | 1 min |
| C-003 | `flex-wrap: wrap` on `.footer-links` | Landvex/index.html inline style | 1 min |
| C-004 | `flex-wrap: wrap` on `.footer-legal-links` | quiXzoom/styles.css | 1 min |
| m-001 | Add `overflow-x: clip` to html/body | Both | 1 min |
**Total: ~6 minutes of CSS edits.**
### Priority 2 — Cross-site inconsistencies
| ID | Fix | File | Est |
|---|---|---|---|
| M-001 | Replace raw social URLs with SVG icons | quiXzoom footer | 20 min |
| M-002 | Add desktop nav links to Landvex | Landvex/index.html | 30 min |
| M-006 | Restructure Landvex footer into columns | Landvex/index.html | 45 min |
### Priority 3 — Accessibility failures (WCAG 2.2 AA)
| ID | Fix | File | Est |
|---|---|---|---|
| A-001/M-005 | Add `:focus-visible` styles globally | Both stylesheets | 5 min |
| A-002 | Add `aria-label` + `aria-expanded` to hamburger | quiXzoom/index.html | 5 min |
| A-004 | Expand hamburger touch target to 44×44px | quiXzoom/styles.css | 2 min |
### Priority 4 — Enterprise polish
| ID | Fix | Est |
|---|---|---|
| M-003 | Stat rows → auto-fit grid | 10 min |
| M-004 | Community pipeline badge white-space fix | 5 min |
| m-002/m-003 | Scrollbar gutter + layout polish | 10 min |
| Token system | Apply CSS variables to both sites | 2h |
| Footer columns | Landvex grouped footer nav | 45 min |
---
## GLOBAL SAFETY NET (add last, after all root causes fixed)
```css
/* Add to BOTH sites — last rule in stylesheet */
html,
body {
overflow-x: clip;
}
```
**Note:** `overflow-x: clip` (not `hidden`) preserves sticky positioning while preventing horizontal scroll. Apply only after root causes are fixed — never as primary solution.