qualia-framework 4.5.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/AGENTS.md +24 -0
  2. package/CLAUDE.md +12 -75
  3. package/README.md +23 -16
  4. package/agents/builder.md +9 -21
  5. package/agents/planner.md +8 -0
  6. package/agents/verifier.md +8 -0
  7. package/agents/visual-evaluator.md +132 -0
  8. package/bin/cli.js +54 -18
  9. package/bin/install.js +369 -29
  10. package/bin/qualia-ui.js +208 -1
  11. package/bin/slop-detect.mjs +5 -0
  12. package/bin/state.js +34 -1
  13. package/docs/install-redesign-builder-prompt.md +290 -0
  14. package/docs/install-redesign-pilot.md +234 -0
  15. package/docs/playwright-loop-builder-prompt.md +185 -0
  16. package/docs/playwright-loop-design-notes.md +108 -0
  17. package/docs/playwright-loop-pilot-results.md +170 -0
  18. package/docs/playwright-loop-tester-prompt.md +213 -0
  19. package/docs/polish-loop-supervised-run.md +111 -0
  20. package/docs/reviews/matt-pocock-skills-analysis.md +300 -0
  21. package/guide.md +9 -5
  22. package/hooks/env-empty-guard.js +74 -0
  23. package/hooks/pre-compact.js +19 -9
  24. package/hooks/pre-deploy-gate.js +8 -2
  25. package/hooks/pre-push.js +26 -12
  26. package/hooks/supabase-destructive-guard.js +62 -0
  27. package/hooks/vercel-account-guard.js +91 -0
  28. package/package.json +2 -1
  29. package/rules/design-brand.md +4 -0
  30. package/rules/design-laws.md +4 -0
  31. package/rules/design-product.md +4 -0
  32. package/rules/design-rubric.md +4 -0
  33. package/rules/grounding.md +4 -0
  34. package/skills/qualia-build/SKILL.md +40 -46
  35. package/skills/qualia-discuss/SKILL.md +51 -68
  36. package/skills/qualia-handoff/SKILL.md +1 -0
  37. package/skills/qualia-hook-gen/SKILL.md +206 -0
  38. package/skills/qualia-issues/SKILL.md +151 -0
  39. package/skills/qualia-map/SKILL.md +78 -35
  40. package/skills/qualia-new/REFERENCE.md +139 -0
  41. package/skills/qualia-new/SKILL.md +45 -121
  42. package/skills/qualia-optimize/REFERENCE.md +265 -0
  43. package/skills/qualia-optimize/SKILL.md +92 -232
  44. package/skills/qualia-plan/SKILL.md +58 -65
  45. package/skills/qualia-polish-loop/REFERENCE.md +265 -0
  46. package/skills/qualia-polish-loop/SKILL.md +201 -0
  47. package/skills/qualia-polish-loop/fixtures/broken.html +117 -0
  48. package/skills/qualia-polish-loop/fixtures/clean.html +196 -0
  49. package/skills/qualia-polish-loop/scripts/loop.mjs +323 -0
  50. package/skills/qualia-polish-loop/scripts/playwright-capture.mjs +206 -0
  51. package/skills/qualia-polish-loop/scripts/score.mjs +176 -0
  52. package/skills/qualia-prd/SKILL.md +199 -0
  53. package/skills/qualia-report/SKILL.md +141 -200
  54. package/skills/qualia-research/SKILL.md +28 -33
  55. package/skills/qualia-road/SKILL.md +103 -0
  56. package/skills/qualia-ship/SKILL.md +1 -0
  57. package/skills/qualia-task/SKILL.md +1 -1
  58. package/skills/qualia-test/SKILL.md +50 -2
  59. package/skills/qualia-triage/SKILL.md +152 -0
  60. package/skills/qualia-verify/SKILL.md +63 -104
  61. package/skills/qualia-zoom/SKILL.md +51 -0
  62. package/skills/zoho-workflow/SKILL.md +1 -1
  63. package/templates/CONTEXT.md +36 -0
  64. package/templates/decisions/ADR-template.md +30 -0
  65. package/tests/bin.test.sh +598 -7
  66. package/tests/state.test.sh +58 -0
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: qualia-polish-loop
3
+ description: "Autonomous visual-polish loop — screenshots a live URL at three viewports, scores 8 design dimensions against the rubric using vision, fixes issues in the codebase, re-screenshots, loops until every dimension scores ≥ 3 or the kill-switch fires. Trigger on 'polish loop', 'visual loop', 'screenshot polish', 'visual QA loop', 'fix what I see', 'make it look right', 'iterate on the design until it's correct'. v5.1 flagship — closes the design-iteration churn friction."
4
+ allowed-tools:
5
+ - Bash
6
+ - Read
7
+ - Write
8
+ - Edit
9
+ - Grep
10
+ - Glob
11
+ - Agent
12
+ argument-hint: "{url} [--brief PATH] [--max 8] [--viewports 375,768,1440] [--ref PATH] [--budget 100000]"
13
+ ---
14
+
15
+ # /qualia-polish-loop — Autonomous Visual-Polish Loop
16
+
17
+ See its own work. Fix its own work. Stop only when correct.
18
+
19
+ ## What it does
20
+
21
+ Takes a URL + design brief. Screenshots at 3 viewports (mobile / tablet / desktop). Spawns a vision evaluator that scores 8 dimensions of `rules/design-rubric.md` against the brief with cited evidence. Spawns up to 3 fix-builders in parallel for the top issues. Re-screenshots. Loops until all dimensions ≥ 3 or the kill-switch trips (regression, budget, or max iterations).
22
+
23
+ Different from `/qualia-polish`: that one is read+edit+slop-detect, single pass. This one is **see+edit+verify+repeat** with a real loop and real screenshots.
24
+
25
+ ## When to use
26
+
27
+ - After `/qualia-build` or `/qualia-verify` when visual issues remain that text-based slop-detect can't catch (mobile-only breakage, motion missing, hero-video framing)
28
+ - When the user says "it doesn't look right" / "fix what I see" / "the mobile version is broken"
29
+ - As an opt-in upgrade to `/qualia-polish --redesign` Stage 4 (vision loop)
30
+ - Before `/qualia-ship` for a final visual QA pass
31
+
32
+ ## Pre-flight (sequential, every invocation)
33
+
34
+ ```bash
35
+ node ~/.claude/bin/qualia-ui.js banner polish
36
+ ```
37
+
38
+ Run these in order. Halt on the first failure.
39
+
40
+ | Gate | Check | If fail |
41
+ |---|---|---|
42
+ | Substrate | `rules/design-rubric.md`, `rules/design-laws.md` exist | Run `npx qualia install` |
43
+ | Brief | `--brief` PATH if provided, else `.planning/DESIGN.md`, else PRODUCT.md | If none, HALT: "No design brief found. Pass --brief or run /qualia-new." |
44
+ | Browser | `node ~/.claude/skills/qualia-polish-loop/scripts/playwright-capture.mjs --url about:blank --out /tmp/qpl-preflight` exits 0 | HALT with the script's setup hint |
45
+ | URL reachable | `curl -fsS -o /dev/null -w '%{http_code}' "$URL"` returns 2xx/3xx | HALT — start the dev server first |
46
+ | Working tree | `git status --porcelain` is empty | HALT — "Loop commits per iteration. Stash or commit pending changes first." |
47
+ | Budget | Estimate iters × ~14.5K tokens; warn if > 100K | If `--budget` not set, default 100K. Surface estimate to user. |
48
+
49
+ State the preflight explicitly:
50
+
51
+ ```
52
+ QPL_PREFLIGHT: substrate=pass brief={path} browser=pass url=200 git=clean budget=100K
53
+ ```
54
+
55
+ ## Loop (max 8 iterations, default 6)
56
+
57
+ Single state file at `/tmp/qpl-{timestamp}/state.json` keeps the deterministic counters (iteration, fingerprints, fixes, verdict) out of the LLM context.
58
+
59
+ ```bash
60
+ RUN_ID="qpl-$(date +%s)"
61
+ STATE="/tmp/${RUN_ID}/state.json"
62
+ node ~/.claude/skills/qualia-polish-loop/scripts/loop.mjs init \
63
+ --state "$STATE" --url "$URL" --brief "$BRIEF" \
64
+ --max "${MAX:-8}" --budget "${BUDGET:-100000}"
65
+ ```
66
+
67
+ Each iteration:
68
+
69
+ ### 1. Capture (deterministic, ~3-5s)
70
+
71
+ ```bash
72
+ ITER_DIR="/tmp/${RUN_ID}/iter-${N}"
73
+ node ~/.claude/skills/qualia-polish-loop/scripts/playwright-capture.mjs \
74
+ --url "$URL" --out "$ITER_DIR" --viewports 375,768,1440 --wait 1500
75
+ ```
76
+
77
+ Three PNGs land in `$ITER_DIR/{mobile,tablet,desktop}-*.png`. The capture script auto-selects a backend: Playwright if `import('playwright')` resolves, else the cached `~/.cache/ms-playwright/chromium-*` binary, else `google-chrome` / `chromium` on PATH.
78
+
79
+ ### 2. Evaluate (vision — single Agent spawn per iteration)
80
+
81
+ Spawn `Agent` with `subagent_type="qualia-visual-evaluator"`. Inline the rubric, the brief, the screenshots paths, the viewport meta, and the previous iteration's issues (if any). The agent reads the screenshots itself and returns a single JSON envelope per the contract in `agents/visual-evaluator.md`.
82
+
83
+ Save the agent's JSON to `$ITER_DIR/eval.json`. The exact spawn template lives in REFERENCE.md (so the SKILL.md stays under 250 lines per progressive-disclosure discipline).
84
+
85
+ ### 3. Decide (deterministic)
86
+
87
+ ```bash
88
+ node ~/.claude/skills/qualia-polish-loop/scripts/loop.mjs record \
89
+ --state "$STATE" --eval "$ITER_DIR/eval.json"
90
+ ```
91
+
92
+ Exit codes: `0` = SUCCESS (all dims ≥ 3), `1` = CONTINUE (more iterations), `3` = KILLED (regression / budget / max).
93
+
94
+ The orchestrator computes the verdict per `rules/design-rubric.md`:
95
+
96
+ - **all aggregate scores ≥ 3 AND no critical issues remain** → SUCCESS, exit loop
97
+ - **same issue fingerprint recurred 3 consecutive iterations** → KILL, `LOOP_REGRESSION_DETECTED`
98
+ - **tokens used exceeds budget** → KILL, `OUT_OF_BUDGET`
99
+ - **iteration count = max** → KILL, `MAX_ITERATIONS_REACHED`
100
+ - **else** → CONTINUE
101
+
102
+ ### 4. Fix (parallel, up to 3 builders)
103
+
104
+ For the top 3 issues from `eval.json.top_issues`, spawn `qualia-builder` agents IN THE SAME RESPONSE TURN (parallel). Each builder receives the issue, the design tokens from DESIGN.md, and explicit "fix only this dimension, do not refactor" rules. Template in REFERENCE.md.
105
+
106
+ Each builder commits its fix via the orchestrator's slop-detect-gated commit:
107
+
108
+ ```bash
109
+ node ~/.claude/skills/qualia-polish-loop/scripts/loop.mjs commit-fix \
110
+ --state "$STATE" --file "$AFFECTED_FILE" --slug "{issue-slug}"
111
+ ```
112
+
113
+ This stages the file, runs `slop-detect` first (critical findings BLOCK and the builder must retry), then commits with `qpl-{N}: {slug}`. Every fix is a real revertable git commit. Failed fixes leave the working tree clean — no half-edits.
114
+
115
+ ### 5. Redeploy (default: dev-localhost)
116
+
117
+ Default mode does NOT redeploy. The loop assumes the dev server is running with HMR; the next capture re-screenshots the changed page. Verify the dev server is still up:
118
+
119
+ ```bash
120
+ curl -fsS -o /dev/null "$URL" || HALT "dev server died at iteration $N"
121
+ ```
122
+
123
+ For Vercel-preview mode (opt-in via `--deploy preview`), run `vercel deploy` (NOT `--prod`) and update `URL` to the deploy URL before the next capture. Slower (~30-60s/iter), but works against a fully-built environment when HMR isn't reliable.
124
+
125
+ ### 6. Loop back to Capture, increment N
126
+
127
+ ## Post-loop
128
+
129
+ ```bash
130
+ node ~/.claude/skills/qualia-polish-loop/scripts/loop.mjs report --state "$STATE" \
131
+ > .planning/visual-polish-loop.md
132
+ git add .planning/visual-polish-loop.md
133
+ git -c user.name="Qualia Solutions" -c user.email="info@qualiasolutions.net" \
134
+ commit -m "qpl-final: visual-polish-loop report"
135
+ ```
136
+
137
+ Then a closing card via `qualia-ui`:
138
+
139
+ ```bash
140
+ node ~/.claude/bin/qualia-ui.js divider
141
+ node ~/.claude/bin/qualia-ui.js ok "Iterations: {N}/{max}"
142
+ node ~/.claude/bin/qualia-ui.js ok "Final aggregate: {sum}/40 (avg {avg})"
143
+ node ~/.claude/bin/qualia-ui.js ok "Fixes committed: {count}"
144
+ node ~/.claude/bin/qualia-ui.js ok "Tokens used: {N}K of {budget}K"
145
+ node ~/.claude/bin/qualia-ui.js end "VISUAL POLISH LOOP — {SUCCESS|KILLED-AT-N|OUT-OF-BUDGET}" "/qualia-verify or /qualia-ship"
146
+ ```
147
+
148
+ ## Hard rules
149
+
150
+ 1. **Vision-eval is anchored.** The rubric criteria are inlined in the eval prompt verbatim. Never spawn the evaluator with "tell me what you think" — that's how you get "looks great!" hallucinations. The verbatim contract is in REFERENCE.md.
151
+ 2. **Per-iteration commits.** Every fix gets `qpl-{N}: {slug}` prefix so the user can `git revert` any iteration cleanly. The orchestrator enforces this via `loop.mjs commit-fix`.
152
+ 3. **Slop-detect gate.** Critical findings BLOCK the commit. The fix-builder must retry (or the loop kills if the same fingerprint recurs 3x).
153
+ 4. **No silent destruction.** All edits go through git. Failed iterations leave clear `qpl-N:` commit trail. No working-tree side effects.
154
+ 5. **`prefers-reduced-motion` honored.** If reduced motion is set, the evaluator scores motion on CSS-declaration quality, not visible animation. Don't penalize "no motion" when reduced motion is on.
155
+ 6. **Budget discipline.** Token usage is tracked in state. KILL at cap. Surface partial progress.
156
+ 7. **Cleanup on exit.** No orphan browser processes (the capture script spawns short-lived headless processes, but verify nothing lingers via `pgrep -f chrome --headless`). Temp dirs left for forensic debugging until the next run.
157
+
158
+ ## Failure modes
159
+
160
+ | Symptom | Likely cause | Action |
161
+ |---|---|---|
162
+ | `playwright-capture.mjs` exits 2 with `no_browser_backend` | No Chrome/Chromium found anywhere | `npx playwright install chromium` or install Google Chrome |
163
+ | Screenshot is blank | Page not done rendering | Increase `--wait` to 3000ms; retry once |
164
+ | Vision agent says "looks great!" to everything | Rubric not inlined / prompt drift | Verify REFERENCE.md prompt is being used verbatim. Check `agents/visual-evaluator.md` is on disk. |
165
+ | Same issue every iteration | Fix-builder not addressing root cause | Kill at 3 recurrences (automatic). Read `state.json.fingerprints[*]` for diagnosis. Hand-fix and resume. |
166
+ | Dev server died mid-loop | Port conflict / crash | Detect via curl heartbeat in step 5. HALT — restart server, run loop with `--resume STATE`. |
167
+ | Token budget blown | Too many iterations needed | KILL at cap (automatic). Report partial progress. Consider tightening DESIGN.md or pre-running `/qualia-polish` once first. |
168
+
169
+ ## Setup notes for users
170
+
171
+ The loop requires headless Chrome/Chromium. Two ways to get it:
172
+
173
+ ```bash
174
+ # A. Playwright (recommended — best stability + waiting semantics)
175
+ npx playwright install chromium
176
+
177
+ # B. System Chrome (fallback — works if google-chrome or chromium is on PATH)
178
+ # Already installed on most dev machines; nothing to do.
179
+ ```
180
+
181
+ The capture script tries Playwright first, then the cached chromium binary, then PATH lookups. No npm dependency on Playwright is added to your project — it's optional.
182
+
183
+ ## Self-tests (the spec mandates 3 scenarios)
184
+
185
+ `docs/playwright-loop-pilot-results.md` records the outcome of running the loop against:
186
+ 1. `fixtures/clean.html` (well-designed page) — expect SUCCESS in 1-2 iterations
187
+ 2. `fixtures/broken.html` (deliberately bad page) — expect SUCCESS in 4-6 iterations after fixes
188
+ 3. Synthetic kill-switch test — verify regression detection fires at iteration 4
189
+
190
+ Run them with `bash skills/qualia-polish-loop/scripts/_self-tests.sh` (if present) or follow the manual instructions in REFERENCE.md.
191
+
192
+ ## Token-budget discipline
193
+
194
+ | Max iterations | Estimated tokens | Notes |
195
+ |---|---|---|
196
+ | 2 | ~30K | tight — only for known-clean pages |
197
+ | 4 | ~60K | standard |
198
+ | 6 | ~90K | default |
199
+ | 8 | ~120K | maximum — set `--budget 150000` to allow |
200
+
201
+ Each iteration costs roughly: 3 PNG vision reads (~9K), rubric prompt (~2K), 3 fix-builder spawns with file reads (~3.5K). Plus the orchestrator's deterministic CLI calls (~negligible). The state file persists outside Claude's context entirely — only the per-iteration eval JSON and fix outcomes flow back in.
@@ -0,0 +1,117 @@
1
+ <!doctype html>
2
+ <!--
3
+ Deliberately broken page used by /qualia-polish-loop self-test Scenario 2.
4
+ Hits multiple absolute-ban patterns from rules/design-laws.md and
5
+ rules/design-brand.md so the vision evaluator has to identify them all.
6
+ Banned font (Inter), pure white + pure black, blue-purple gradient,
7
+ gradient text, identical 3-column card grid, "Get Started" / "Learn More"
8
+ generic CTAs, side-stripe border-left:4px decorative, max-width:1280
9
+ fixed container, outline:none without focus replacement. This fixture
10
+ is intentional shipped slop. Do not lift patterns from it.
11
+ -->
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8">
15
+ <meta name="viewport" content="width=device-width,initial-scale=1">
16
+ <title>BrandX</title>
17
+ <style>
18
+ body {
19
+ margin: 0;
20
+ background: #ffffff;
21
+ color: #000000;
22
+ font-family: Inter, sans-serif;
23
+ font-weight: 400;
24
+ }
25
+ .container { max-width: 1280px; margin: 0 auto; padding: 24px; }
26
+ header { display: flex; justify-content: space-between; align-items: center; padding: 16px 0; }
27
+ .logo { font-size: 18px; font-weight: 700; }
28
+ nav a { color: #000; margin-left: 24px; text-decoration: none; outline: none; }
29
+ nav a:hover { text-decoration: underline; }
30
+
31
+ .hero {
32
+ text-align: center;
33
+ padding: 80px 16px;
34
+ background: linear-gradient(135deg, #2563eb 0%, #9333ea 100%);
35
+ color: #ffffff;
36
+ }
37
+ .hero h1 {
38
+ font-size: 56px;
39
+ font-family: Inter, sans-serif;
40
+ font-weight: 800;
41
+ margin: 0 0 16px;
42
+ background: linear-gradient(90deg, #fff, #c4b5fd);
43
+ -webkit-background-clip: text;
44
+ background-clip: text;
45
+ color: transparent;
46
+ }
47
+ .hero p { font-size: 18px; max-width: 620px; margin: 0 auto 32px; }
48
+
49
+ .cta-row { display: flex; gap: 12px; justify-content: center; }
50
+ .btn {
51
+ background: #ffffff;
52
+ color: #000;
53
+ padding: 12px 24px;
54
+ border: 0;
55
+ font-family: Inter, sans-serif;
56
+ font-size: 16px;
57
+ cursor: pointer;
58
+ outline: none;
59
+ }
60
+ .btn-secondary { background: transparent; color: #fff; border: 1px solid #fff; }
61
+
62
+ .features { padding: 80px 0; }
63
+ .features h2 { font-size: 32px; text-align: center; margin: 0 0 48px; font-family: Inter, sans-serif; }
64
+ .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
65
+ .card {
66
+ background: #f8f9fa;
67
+ padding: 32px;
68
+ border-left: 4px solid #2563eb;
69
+ }
70
+ .card h3 { font-size: 18px; margin: 0 0 8px; font-family: Inter, sans-serif; }
71
+ .card p { color: #666; font-size: 14px; margin: 0; }
72
+
73
+ footer { background: #000; color: #fff; padding: 32px 0; text-align: center; font-size: 14px; }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <div class="container">
78
+ <header>
79
+ <span class="logo">BrandX</span>
80
+ <nav>
81
+ <a href="#">Features</a>
82
+ <a href="#">Pricing</a>
83
+ <a href="#">About</a>
84
+ </nav>
85
+ </header>
86
+ </div>
87
+
88
+ <section class="hero">
89
+ <h1>Welcome to BrandX</h1>
90
+ <p>The AI-powered platform that helps you do more with less, built for modern teams who care about results.</p>
91
+ <div class="cta-row">
92
+ <button class="btn">Get Started</button>
93
+ <button class="btn btn-secondary">Learn More</button>
94
+ </div>
95
+ </section>
96
+
97
+ <div class="container features">
98
+ <h2>Everything you need to succeed</h2>
99
+ <div class="grid">
100
+ <div class="card">
101
+ <h3>Fast</h3>
102
+ <p>Lightning-fast performance that scales with your business needs and grows alongside you.</p>
103
+ </div>
104
+ <div class="card">
105
+ <h3>Secure</h3>
106
+ <p>Bank-grade security with end-to-end encryption keeping your data safe from prying eyes.</p>
107
+ </div>
108
+ <div class="card">
109
+ <h3>Easy</h3>
110
+ <p>Intuitive interface designed for everyone, from beginners to power users.</p>
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <footer>2026 BrandX. All rights reserved.</footer>
116
+ </body>
117
+ </html>
@@ -0,0 +1,196 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>Lumen Audit · case study</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300..900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg: oklch(0.16 0.012 220);
13
+ --bg-2: oklch(0.20 0.014 220);
14
+ --surface: oklch(0.24 0.014 220);
15
+ --text: oklch(0.94 0.006 220);
16
+ --muted: oklch(0.66 0.010 220);
17
+ --dim: oklch(0.50 0.012 220);
18
+ --line: oklch(0.32 0.012 220);
19
+ --accent: oklch(0.78 0.14 196);
20
+ --accent-2: oklch(0.70 0.13 196);
21
+
22
+ --shadow-sm: 0 1px 2px oklch(0.10 0.020 220 / 0.40);
23
+ --shadow-md: 0 8px 24px -8px oklch(0.10 0.030 220 / 0.50);
24
+ --shadow-lg: 0 20px 60px -20px oklch(0.10 0.035 220 / 0.60);
25
+
26
+ --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
27
+ }
28
+ * { box-sizing: border-box; }
29
+ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); }
30
+ body {
31
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
32
+ font-size: clamp(0.95rem, 0.5rem + 0.5vw, 1.0625rem);
33
+ line-height: 1.55;
34
+ font-feature-settings: "tnum" 1, "ss01" 1;
35
+ -webkit-font-smoothing: antialiased;
36
+ }
37
+ header, main, footer {
38
+ padding-inline: clamp(1.25rem, 5vw, 4rem);
39
+ padding-block: clamp(1.5rem, 4vw, 3rem);
40
+ }
41
+ a { color: var(--text); text-decoration: none; transition: color 150ms var(--ease-out); }
42
+ a:hover { color: var(--accent); }
43
+ a:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 2px; }
44
+
45
+ header {
46
+ display: grid;
47
+ grid-template-columns: auto 1fr auto;
48
+ align-items: baseline;
49
+ gap: clamp(1rem, 3vw, 2.5rem);
50
+ border-bottom: 1px solid var(--line);
51
+ }
52
+ .logo {
53
+ font-family: 'Fraunces', serif;
54
+ font-weight: 600;
55
+ font-size: 1.25rem;
56
+ letter-spacing: -0.02em;
57
+ }
58
+ .logo span { color: var(--accent); }
59
+ nav { display: flex; gap: clamp(1rem, 2vw, 2rem); justify-self: end; font-size: 0.875rem; color: var(--muted); }
60
+ nav a:hover { color: var(--text); }
61
+
62
+ .hero {
63
+ display: grid;
64
+ grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
65
+ gap: clamp(1.5rem, 4vw, 4rem);
66
+ align-items: end;
67
+ padding-block: clamp(3rem, 8vw, 7rem);
68
+ border-bottom: 1px solid var(--line);
69
+ }
70
+ @media (max-width: 720px) { .hero { grid-template-columns: 1fr; } }
71
+ h1 {
72
+ font-family: 'Fraunces', serif;
73
+ font-weight: 350;
74
+ font-variation-settings: "opsz" 144;
75
+ font-size: clamp(2.25rem, 1rem + 6vw, 5.25rem);
76
+ line-height: 1.02;
77
+ letter-spacing: -0.035em;
78
+ margin: 0 0 1.5rem;
79
+ max-width: 16ch;
80
+ }
81
+ h1 em {
82
+ font-style: italic;
83
+ font-weight: 400;
84
+ color: var(--accent);
85
+ }
86
+ .lede { color: var(--muted); max-width: 38ch; font-size: 1rem; }
87
+ .meta {
88
+ align-self: end;
89
+ display: grid;
90
+ gap: 0.5rem;
91
+ font-size: 0.8125rem;
92
+ color: var(--dim);
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.08em;
95
+ }
96
+ .meta dt { color: var(--muted); }
97
+ .meta dd { margin: 0; color: var(--text); font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
98
+
99
+ .work { padding-block: clamp(3rem, 7vw, 6rem); }
100
+ .work h2 {
101
+ font-family: 'Fraunces', serif;
102
+ font-weight: 400;
103
+ font-size: clamp(1.5rem, 0.5rem + 2.5vw, 2.5rem);
104
+ letter-spacing: -0.02em;
105
+ margin: 0 0 2.5rem;
106
+ }
107
+ .work-grid {
108
+ display: grid;
109
+ grid-template-columns: repeat(12, 1fr);
110
+ gap: 1.25rem;
111
+ }
112
+ .work-grid > article { background: var(--surface); border: 1px solid var(--line); padding: 1.5rem; transition: transform 200ms var(--ease-out), box-shadow 200ms var(--ease-out); }
113
+ .work-grid > article:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
114
+ .item-1 { grid-column: span 7; min-height: 18rem; display: grid; align-content: end; }
115
+ .item-2 { grid-column: span 5; min-height: 18rem; }
116
+ .item-3 { grid-column: span 5; min-height: 14rem; }
117
+ .item-4 { grid-column: span 7; min-height: 14rem; }
118
+ @media (max-width: 720px) { .item-1, .item-2, .item-3, .item-4 { grid-column: span 12; min-height: 11rem; } }
119
+ .work-grid h3 { font-family: 'Fraunces', serif; font-weight: 400; font-size: 1.5rem; letter-spacing: -0.02em; margin: 0 0 0.5rem; }
120
+ .work-grid p { color: var(--muted); margin: 0; font-size: 0.9375rem; }
121
+ .work-grid .num { color: var(--dim); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 1rem; }
122
+
123
+ footer { border-top: 1px solid var(--line); display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: end; color: var(--dim); font-size: 0.8125rem; }
124
+ footer a:hover { color: var(--accent); }
125
+
126
+ @media (prefers-reduced-motion: reduce) {
127
+ *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
128
+ }
129
+ </style>
130
+ </head>
131
+ <body>
132
+ <header>
133
+ <span class="logo">Lumen<span>·</span></span>
134
+ <nav>
135
+ <a href="#work">Work</a>
136
+ <a href="#process">Process</a>
137
+ <a href="#contact">Get in touch</a>
138
+ </nav>
139
+ <a class="logo" href="#contact" style="color: var(--accent); font-size: 0.875rem;">Audit a brand</a>
140
+ </header>
141
+
142
+ <main>
143
+ <section class="hero">
144
+ <div>
145
+ <h1>Brands that age <em>well</em>, on the second glance.</h1>
146
+ <p class="lede">Lumen audits the visual language of design-led companies. We score against a 64-point rubric and ship the rewrite, not the slide deck.</p>
147
+ </div>
148
+ <dl class="meta">
149
+ <dt>Engagements</dt>
150
+ <dd>148 since 2019</dd>
151
+ <dt>Average lead time</dt>
152
+ <dd>11 days</dd>
153
+ <dt>Repeat-customer rate</dt>
154
+ <dd>72%</dd>
155
+ </dl>
156
+ </section>
157
+
158
+ <section class="work" id="work">
159
+ <h2>Recent audits, picked because they read</h2>
160
+ <div class="work-grid">
161
+ <article class="item-1">
162
+ <span class="num">01 / Fintech, NL</span>
163
+ <h3>Onyx Treasury</h3>
164
+ <p>Replaced a navy-and-gold palette saturated with sector clichés. Shipped a single-hue OKLCH system, restored 12 of 14 contrast pairs, gave the dashboard six months of breathing room.</p>
165
+ </article>
166
+ <article class="item-2">
167
+ <span class="num">02 / Healthcare, IE</span>
168
+ <h3>Verge Diagnostics</h3>
169
+ <p>Removed pastel teal. Rebuilt around editorial Fraunces and a single accent pulled from the diagnostic chart. Saved 41kb of icon font weight.</p>
170
+ </article>
171
+ <article class="item-3">
172
+ <span class="num">03 / Voice AI, US</span>
173
+ <h3>Hum.</h3>
174
+ <p>Pulled motion back from elastic-spring chaos to one signature reveal per page. Kept the personality, lost the seasickness.</p>
175
+ </article>
176
+ <article class="item-4">
177
+ <span class="num">04 / Restaurant group, CY</span>
178
+ <h3>Olive &amp; Bone</h3>
179
+ <p>One typeface (Fraunces) across the brand, the menu, the wayfinding, the receipts. The legibility budget went to text, not chrome.</p>
180
+ </article>
181
+ </div>
182
+ </section>
183
+ </main>
184
+
185
+ <footer id="contact">
186
+ <div>
187
+ <span class="logo" style="font-size: 1rem;">Lumen<span>·</span></span>
188
+ <p style="margin: 0.5rem 0 0; max-width: 40ch;">Brand audits with a real opinion. We say no to projects we can&rsquo;t move forward.</p>
189
+ </div>
190
+ <div style="text-align: right;">
191
+ <a href="mailto:hello@lumenaudit.studio">hello@lumenaudit.studio</a><br>
192
+ <span>2026 Lumen Audit, Nicosia + Amsterdam</span>
193
+ </div>
194
+ </footer>
195
+ </body>
196
+ </html>