# Cowork UI Kit — Final State Reflection (43 iterations)

> Distilled learnings from 43 iterations of self-critique + bar-raising. Captures patterns + gotchas surfaced beyond the kit's surface implementation. Companion to CHANGELOG-v0.5.md (which lists what shipped) — this lists what was *learned*.

Date: 2026-05-20
Version pinned: v0.5.11
Live: https://ui.dang.pm/

---

## At a glance

| Dimension | Final state |
|---|---|
| Archetypes covered | 6/6 dogfood PASS (A Dashboard / B Tool / C Landing / D Editorial / E Deck / F PWA) |
| Brands tested | 7 production (`annhien`, `khi`, `betterbuy`, `xanh-mkt`, `akr`, `hgg`, `ai-tools`) + 5 dogfood-only personas (Acme/Pulse/Wave/Slow Press/Forge/Calm) |
| Immutable versions | 8 (v0.5.3, .4, .5, .6, .7, .8, .9, .10, .11) |
| CI guards | **7** (token-audit, path-audit, contrast-check-brand, lighthouse-check, gradient-contrast-probe default-on Iter 41, visual-diff opt-in, ci-check orchestrator) |
| CI integration | strict + lenient modes (`BRAND_KNOWN_FAILURES` env var) |
| Tokens documented | **103+** across 14 sections (REFERENCE.md) — Iter 40A added 28 motion tokens |
| Motion system | **28 tokens + `prefers-reduced-motion` global + `.motion-essential` escape hatch + 15-pattern demo + MOTION.md** (Iter 40A) |
| State components | **4 loading atoms + 2 state molecules + toast organism + 2 JS utilities (focus-trap, keyboard) + showcase + UXSTATE.md** (Iter 40B) |
| **Form atoms (Iter 42)** | **DatePicker (4 variants, molecules/) + NumberInput (5 variants, atoms/) + Slider (6 variants, atoms/) + FileUploader (4 variants, atoms/)** = 4 new components / 19 demo variants shipped Iter 42B-D |
| Friction items closed | 25+ across V1-V5 friction logs |
| Lighthouse (v0.5.11) | **A11y 100/100 on 28/28 public URLs + /motion/ + /uxstate/** verified Iter 35a/b/c + Iter 41 (2026-05-20). All 46 URLs on the CI gate (6 archetypes + brands + USAGE + templates + mobile + atoms + molecules + organisms + icons + demo + variant-matrix + 9 previews + 7 showcase + motion + uxstate + 4 loading atoms + 2 state molecules + toast + **date-picker + number-input + slider + file-uploader (Iter 42→43K)**) pass strict threshold (perf≥90 / a11y≥95 / bp≥90 / seo≥90). CI `_build/lighthouse-check.{sh,ps1}` DEFAULT_URLS expanded 42 → 46 in Iter 43K. Trail: Iter 30=2307 / Iter 31a nginx gzip = perf 84-88 → 92-98 / Iter 31b page-B a11y 93→100 / Iter 32 = 2354/2400 archetype baseline / Iter 33 brands 80→100 / Iter 34 sweep surfaced 15 failing URLs / Iter 35a USAGE+templates+mobile / Iter 35b 7 previews / Iter 35c atoms+molecules+organisms+demo+matrix / Iter 41 motion+uxstate added / Iter 42 4 form atoms added / Iter 43K templates/ decorative aria-hidden + G19 codified. |
| Axe violations | 0/10 URLs (was 1063 at v0.4 baseline) |

---

## Trend evidence — friction declined as patches accumulated

| Dogfood iter | Archetype | Blocker/Major/minor | Time to first render |
|---|---|---|---|
| 16 W | C Landing | 0 / 3 / 4 | ~22 min |
| 19 AA | A Dashboard | 0 / 3 / 3 | ~22 min |
| 19 BB | F PWA | 0 / 2 / 5 | ~28 min |
| 21 M | B Tool | 0 / 1 / 3 | ~14 min |
| 22 O | D Editorial | 0 / 1 / 3 | ~8 min |
| 22 P | E Deck | 0 / 2 / 1 | ~14 min |

**Observation:** Friction trended downward (Major+minor 7 → 4) and time-to-first-render roughly halved (22 → 8 min) as later dogfoods benefited from earlier patches (REFERENCE.md token catalog, brand-aware CLI, fixed archetype starter relative paths, ADR-011 fallback clarity). Each blind dogfood found 0 Blockers — i.e. code shipped fine; gaps were always documentation, not implementation.

---

## Key gotchas surfaced (worth memorizing for future kits)

### G1. CSS `max-width: 100%` is useless when parent overflows
**Where:** Iter 28 B Tool truncate fix.
**What:** A span with `flex-1 truncate max-width: 100%` does NOT trigger ellipsis if the parent row is wider than viewport. The `100%` resolves against the overflowing parent — so the span itself is never narrower than its content. Outer `overflow-x: hidden` clips visually but gives a hard cut, not an ellipsis.
**Fix:** Hard pixel cap — `max-width: 220px !important; flex: 0 1 220px !important; display: inline-block` inside `@media (max-width: 600px)`. With span's own width < content width, `text-overflow: ellipsis` triggers correctly.
**Memory:** Use absolute caps, not percentages, when the parent constraint is unreliable. Verify by measuring the SPAN's width, not the parent's.

### G2. CSS custom property scope substitution
**Where:** Iter 11 M brand switcher, re-surfaced in Iter 20 K PWA F gotcha #6.
**What:** `var()` resolves at the declare site (computed time). Overriding `--brand-*` at a nested scope does NOT re-compute `:root`-defined derived vars (`--primary`, `--accent-bg`, etc.).
**Fix:** Redeclare derived vars at the component scope when multi-theme on the same page. See `page-F-pwa-app-flow.html` lines 40-50.
**Memory:** Theme switching by class override only works if the override is wide enough to redeclare ALL derived vars, not just the input HSL triplets.

### G3. Denylist exclude pattern misses files
**Where:** Iter 21 L v0.5.4 snapshot bug.
**What:** Snapshot denylist `exclude=_iter*` missed `_screenshots/` (749 MB), `_lighthouse/`, `_a11y/`, `_font-test*`, etc. Final snapshot bloated to 800 MB+.
**Fix:** Switched to allowlist — 16 dirs + N root .md files — final v0.5.4 snapshot 12.25 MB. Codified into VERSION.md snapshot recipe.
**Memory:** Always allowlist for snapshots. Never denylist. New scratch dirs added between releases will always leak through a denylist.

### G4. Agent context budget — single focused task
**Where:** Iters 25 V, 26 W (terminated mid-thought at 12-14 tool uses).
**What:** Briefs combining "audit + diagnose + fix + verify" exceed agent budget (~30 tool uses typical). Agent stops mid-analysis without shipping anything.
**Fix:** Each agent brief = 1 well-defined task with clear acceptance. Chain agents for multi-phase work.
**Memory:** When in doubt, split. Agent context is not unlimited.

### G5. PNG-hash baseline is brittle
**Where:** Iter 17 X visual baseline drift false alarm.
**What:** Font-load timing, Chrome version updates, content drift all change PNG byte hash without any real visual regression. Exact-match baseline produced constant false positives.
**Fix:** Iter 18 Z built 3-tier `_build/visual-diff.js`: IDENTICAL (byte equal) / EQUIVALENT (size delta < 2%) / DIFFERENT. Real layout shifts still caught; timing noise filtered.
**Memory:** Use thresholds, not exact-match, for visual regression.

### G6. Brand-relative tokens vs aesthetic-fixed
**Where:** Iter 23 R brand-relative mesh.
**What:** `--mesh-warm-1` was coral-tinted in foundation tokens (warm aesthetic default). When a brand swap happened, the mesh did NOT shift — so "brand swap complete" claims were FALSE for D archetype hex-hardcoded gradients (`#F5C9A8`, `#EEAE92`, etc. literally embedded).
**Fix:** Brand-aware mesh tokens `--brand-mesh-1/2/3` driven by HSL math from `--brand-primary-h` / `--brand-accent-h` / `--brand-highlight-h` chain. D editorial 6 hex literals → 0.
**Memory:** Any token tinted by aesthetic risks shadow-fixing a brand. Audit hex literals in archetype pages, not just brand presets.

### G7. Tailwind CDN runtime vs inline @media — don't chase wrong cause
**Where:** Iter 28 B Tool initial debug attempts.
**What:** Initial hypothesis: Tailwind utility classes (`truncate`) override inline `<style>` @media rules due to specificity. Spent agent cycles testing this. Real cause was G1 (max-width: 100% against overflowing parent).
**Fix:** Pixel-trace with Chrome DevTools / headless screenshot of the actual rendered span, not hypothesis chain.
**Memory:** Verify with measurement, not theory. Get the computed value before debating cascade rules.

### G8. Blind dogfood reveals doc gaps, not code bugs
**Where:** Iters 16 W, 19 AA, 19 BB, 21 M, 22 O, 22 P (six blind dogfoods).
**What:** Every blind dogfood found 0 Blockers + multiple doc-level Majors. The code worked; the instructions undertaught. External devs got stuck on "where do I edit?" and "which file is canonical?", not "this component is broken".
**Fix:** Each dogfood Major became a doc patch (FORK comment lines, REFERENCE.md token catalog, Option A/B README split, hue-band perceptual table, ADR-011 fallback role rewrite).
**Memory:** Docs are 50% of the kit. Test from outside-in (blind external dev persona) regularly, not just inside-out (developer reads own code).

### G9. Lighthouse perf fails can be infra (nginx gzip), not code
**Where:** Iter 31a perf remediation.
**What:** All 4 perf-failing archetype URLs (A/C/D/F) had identical bottleneck — nginx serving CSS/HTML uncompressed (`gzip` directive commented out in container config). Pre-built Tailwind was fine; CDN was not in use. Hypothesis "Tailwind CDN runtime cost" was wrong. Real cost: 24 KiB HTML + 70 KiB CSS uncompressed → ~600ms FCP/LCP penalty.
**Fix:** Enable `gzip on` (comp_level 6, types text/html text/css application/javascript image/svg+xml font/woff2) in nginx.conf. Persist nginx.conf inside `_build/` so config travels with the kit, not "lost on VPS".
**Memory:** Before deep-diving page-level perf optimization, verify infra layer fundamentals: gzip, HTTP cache headers, HTTP/2, font preconnect. A single nginx directive can lift 4-pt to 10-pt across every page. Check the obvious infra hygiene before instrumenting per-page hero LCP.

### G10. A11y audit must cover ALL public URLs, not just archetypes
**Where:** Iter 33 brand gallery a11y remediation.
**What:** The 6 archetype pages all scored a11y ≥95 after Iter 31b. But 📄 [brands/index.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/brands/index.html) (the brand showcase gallery — itself a *public* URL at `https://ui.dang.pm/brands/`) was never measured. Fresh audit landed at **a11y 80** with 23 color-contrast fails systemic to its demo components (white-on-primary failed for any brand with primary L ≥ 50%). User caught this visually ("sao màu chữ với màu nền y nhau vậy?") long before any CI gate would have. Lesson: the CI URL list reflected our *taxonomy* (archetypes) not our *attack surface* (every page served from `ui.dang.pm`).
**Fix:** Iter 33 added `https://ui.dang.pm/brands/` to `_build/lighthouse-check.{sh,ps1}` DEFAULT_URLS. Future kit pages exposed publicly get appended to DEFAULT_URLS at the same time they ship — codified in CI.md. Also introduced the `--primary-text-bg` token pattern (`hsl(... min(L-12%, 32%))`) for "any surface that hosts white text must use this, never raw `--primary`" — applicable to future demo components.
**Memory:** Public surface = a11y surface. "Showcase" / "demo" / "preview" pages are NOT exempt from the a11y bar just because they're internal-facing. If a URL is reachable without auth, it must be on the CI gate URL list. Add new URLs to the list at the same commit they go live, not "later".

### G11. `:root` beats `body` in CSS specificity for token redeclaration
**Where:** Iter 35c atoms/molecules a11y debug.
**What:** Token redeclarations placed on `body` selector (e.g. `body.aesthetic-warm { --text-2: ...; }`) lose to `:root` declarations because `:root` has specificity (0,1,0) AND comes first in cascade — `body` has same specificity (0,0,1) BUT only matches inside `<body>`. When `--text-2` is defined on `:root`, a `body` override only wins for descendants if specificity ties + later. Some tokens just disappeared silently — debug took ~60min before realizing.
**Fix:** Override tokens at `:root` level with body-class qualifier: `:root body.aesthetic-warm { --text-2: ...; }` (specificity (0,1,1) beats `:root` 's (0,1,0)). Or use CSS layers / cascade order to force.
**Memory:** When a token override "just doesn't apply", check selector specificity vs `:root` first. The fix is rarely the value — it's the place you wrote the rule.

### G12. Scoped `--muted-text` variable per skin — clean specificity collision fix
**Where:** Iter 35a templates/index.html debug.
**What:** Tailwind utility `opacity-50` applied to text in mixed-skin gallery: warm scope wanted dark muted, dark scope wanted light muted. Single rule couldn't satisfy both without `!important` (smell).
**Fix:** Define `--muted-text` as scoped variable that re-binds per skin:
```css
.skin-warm { --muted-text: hsl(28 20% 28%); }
.skin-dark { --muted-text: hsl(28 5% 78%); }
.mono.opacity-50, .label-muted { color: var(--muted-text); }
```
Single declaration on the utility, brand-relative + skin-aware via re-bind.
**Memory:** When same utility class needs different values across themes, define a SCOPED VARIABLE that re-binds per theme. Avoids `!important` and specificity wars.

### G13. Auto-aria-label IIFE for Atomic Design galleries
**Where:** Iter 35c atoms/molecules gallery fix.
**What:** Atomic Design gallery shows BARE input atoms intentionally — no labels by design (atoms in isolation). Lighthouse flags every input as missing `label`. Adding visible `<label>` breaks the "atom isolation" demo. Adding `aria-label` per input = 30+ markup edits + future drift risk.
**Fix:** Single IIFE script in gallery `<head>` walks all `<input>/<textarea>/<select>` on DOM ready, copies adjacent sibling text (heading/caption) into `aria-label`. Closes 29 violations without touching markup. Re-usable for any gallery.
**Memory:** "Convention via DOM walk" beats "discipline via markup edit" when violations follow a structural pattern. Same pattern works for image-alt (next-sibling caption), button-name (icon parent text), etc.

### G14. Brand-hued `--text-*` blends with brand-hued gradient bg (Lighthouse miss)
**Where:** Iter 37 brands page bug — Dang spotted by eye, Lighthouse passed.
**What:** `--neutral-h: var(--brand-primary-h)` makes `--warm-text-N` inherit brand hue. When that text sits on a `.brand-head` gradient built from `--primary-soft`/`--accent-soft` (same brand hue, lighter L), text + bg share hue family → low VISUAL contrast despite passing structural lightness math. Measured FAILs: A-Kryphan (h=220 L=30) brand-vibe = 3.0:1 on primary-soft; HaoGood (h=220 L=35) = 3.2:1; KHI/Xanh borderline ~3.4–4.2:1. **Lighthouse passed because automated axe `getBackgroundColor()` returns 'transparent' or first stop only for gradient parents — same-hue blend slips through.**
**Fix:** For text rendered on brand-hue gradient backgrounds, use `var(--primary-text-bg)` (clamped L≤32%) instead of `var(--text-2)`. Guarantees ≥4.5:1 across all 7 brand hues regardless of brand primary L. Re-bind for dark scope: `.brand-card[data-aesthetic="dark"] .brand-vibe { color: var(--text-2); }` (dark scope's text-2 is already grey 72% L, safe on dark gradient).
**Memory:** When text and bg both derive from brand HSL, structural lightness gap can pass while visual blend still fails — automated tools miss it. **Treat brand-hue text on brand-hue gradient as a known anti-pattern.** Use a clamped/decoupled token for guaranteed delta. Re-audit any other `--text-*` use on brand-tinted gradient backgrounds.

### G15. Brand-tinted `--primary` raw as 11px text — same hue blend on cream
**Where:** Iter 38a discovered `.eyebrow` in 📄 [demo/brand-switcher.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/demo/brand-switcher.html) — `color: var(--primary)` raw.
**What:** brand presets with primary L≥45% (BetterBuy 60, Xanh 45, AI Tools 60) give 11px text on cream surface 2.94-3.49:1 — fails WCAG AA. Same root cause as G14 but on a NEW surface (cream surface, not gradient).
**Fix:** use clamped formula `hsl(var(--brand-primary-h) var(--brand-primary-s) min(calc(var(--brand-primary-l) - 12%), 32%))` for text — guarantees ≥4.5:1. Dark scope override: `hsl(var(--brand-primary-h) 70% 72%)`.
**Memory:** anywhere `var(--primary)` is used as TEXT COLOR (not bg, not accent), check brand L. Raw primary works for L<45 brands but breaks for L≥45 mid-tone brands.

### G16. `color-mix(... transparent)` chip bg bleeds gradient through
**Where:** Iter 38c probe — `brands/.meta-tag` had `background: color-mix(in srgb, var(--surface) 88%, transparent)` = 88% opacity. Probe measured effective bg ≈ gradient underneath (12% bleed), not cream.
**What:** semi-transparent chip backgrounds composite with PARENT bg at render. Probe sampled visible pixel = mostly gradient color for warm cards (2.68-4.20:1 fail).
**Fix:** make chip bg fully opaque `var(--surface)` (no color-mix transparent). Border still works for visual separation. ONLY use color-mix-transparent when parent is known-flat (vd body bg).
**Memory:** when a chip/badge sits on a colored parent (gradient, brand-tinted, etc.), make bg fully opaque or chip text will fail contrast against the bleed-through.

### G17. `background-blend-mode: multiply` darkens gradient beyond static math
**Where:** Iter 38c `brands/.brand-head` — `linear-gradient(--primary-soft, --accent-soft), radial-gradient(--highlight-soft, transparent)` with `multiply` blend. Probe measured effective bg darker than `--primary-soft` alone.
**What:** Static contrast math using `--primary-soft` L+28% predicts ≥4.5:1, but multiply blend mode multiplies channel values → effective bg darker → real contrast lower. Iter 37 fix `min(L-12%, 32%)` was just-marginal (4.04-4.30:1).
**Fix:** When using blend modes on text containers, tighten clamp formula to `min(L-18%, 24%)` for ≥5:1 margin. Or eliminate blend modes from text-containing surfaces.
**Memory:** any `background-blend-mode` on a parent with text descendants requires +1-stop tighter contrast clamp.

### G19. Gradient parent + `::after` opaque mask — probe blind spot
**Where:** Iter 42A `.progress-circle__label` on 📁 [uxstate/](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/uxstate/) + 📄 [atoms/loading-progress.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/atoms/loading-progress.html). Probe saw conic-gradient first-stop `rgb(184,86,61)` as effective bg = 3.02:1 FAIL. Visually fine — `::after` pseudo-element covers gradient with `var(--surface)` cream.
**What:** probe's `getEffectiveBg()` walks parent chain looking for non-transparent `backgroundColor`. Does NOT introspect `::before`/`::after` pseudo-elements which may visually cover the parent. Probe FALSE NEGATIVE (fails on visually-passing element).
**Fix attempted (G15 clamp):** backfired — clamped color is same-hue family as gradient, contrast 1.81:1 (worse). Don't fight probe with same-hue clamp.
**Correct fix:** give the text element its OWN opaque bg matching the visual reality. `background: var(--surface); padding: 0 4px; border-radius: 999px;` — visually identical to "label floating on cream", probe-truthful (probe sees opaque parent as effective bg).
**Memory:** probe assumes worst-case parent bg. When a `::after` (or `::before`) pseudo masks a gradient under text, give the text element opaque self-bg to bypass probe ambiguity. This is structurally CORRECT — both probe AND user see the same color now, vs. relying on the probe to traverse pseudo-elements (which it can't from `getComputedStyle()` alone).

### G18. Tailwind prebuilt CSS — arbitrary shades silently dropped (JIT miss)
**Where:** Iter 39a 📄 [_previews/01-landing.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/_previews/01-landing.html) + 📄 [_previews/02-admin.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/_previews/02-admin.html) fixes — `from-rose-700`, `to-violet-800` not in 🐍 [tokens/tailwind-prebuilt.css](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/tokens/tailwind-prebuilt.css). Classes silently dropped → transparent gradient → catastrophic contrast fail.
**What:** kit uses prebuilt Tailwind CSS (not JIT runtime). Arbitrary shades (-700/-800/-900 if not pre-shipped) get NO CSS rule. Browser sees `from-rose-700` → no match → gradient default white→white.
**Fix (workaround):** inline `style="background: linear-gradient(to right, #9f1239, #c2410c);"` with hex literals — bypasses Tailwind entirely. ADR-001 deviation accepted for localized overrides.
**Fix (proper):** regen `tailwind-prebuilt.css` to include 700/800 shades for all base palettes. Track for next CI pass.
**Memory:** before using a Tailwind shade in kit context, grep `tokens/tailwind-prebuilt.css` first. If not present, either pick from available shades or use inline hex.

### G20. Standalone component page a11y — `<main>` landmark + progressbar/combobox accessible name
**Where:** Iter 43L (post-43K) 📄 [molecules/date-picker.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/molecules/date-picker.html) (Lighthouse a11y 92) + 📄 [atoms/file-uploader.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/atoms/file-uploader.html) (a11y 90) — both shipped Iter 42B/42D without `<main>` wrapper. Lighthouse fired `landmark-one-main` (-3 wt) + `aria-allowed-attr` (-10 wt, DatePicker only) + `aria-progressbar-name` (-7 wt, FileUploader only) + `label` (-10 wt, FileUploader only).
**What:** 3 distinct a11y traps a standalone component page can hit even when each sub-component is internally accessible:
  1. **`landmark-one-main`** — every public HTML page MUST have exactly one `<main>` landmark. A `.demo-wrap` `<div>` is not enough — Lighthouse counts `<main>` element OR `role="main"` only.
  2. **`aria-allowed-attr`** on `<input type="text">` carrying `aria-haspopup="dialog"` + `aria-expanded` — native `<input>` implicit role is `textbox` which does NOT allow `aria-haspopup`/`aria-expanded`. WAI-ARIA APG date picker dialog pattern requires `role="combobox"` on the input to license those attributes.
  3. **`aria-progressbar-name`** — every `role="progressbar"` MUST carry an accessible name (`aria-label` or `aria-labelledby`). `aria-valuetext` describes the VALUE, not the bar itself. JS-rendered progressbars need to interpolate filename into `aria-label` at construction time.
  4. **`label`** for visually-hidden `<input type="file">` — when the input is wrapped by `<div role="button">` (not by `<label for>`), there is NO programmatic label association. Use `aria-label` on the input directly.
**Fix:** (a) wrap content in `<main id="main">…</main>`, (b) add `role="combobox"` to every date-picker `<input>`, (c) add `aria-label="Tiến độ tải lên <filename>"` to every progressbar (hardcoded demo rows + JS `renderRow()` template literal), (d) add `aria-label="Chọn file…"` to every hidden `<input type="file">`.
**Verification:** Lighthouse a11y both pages 92/90 → **100**. Gradient-contrast probe still PASS (131 + 162 text elements WCAG AA). Visual screenshot 1280×1800 identical pre/post-fix — `<main>` is block-level default, no layout shift.
**Memory:** every standalone atomic component page (`atoms/*.html`, `molecules/*.html`, `organisms/*.html`) is a PUBLIC URL on Lighthouse a11y gate — same checks as archetype pages apply. Before shipping a new component page:
  - Wrap demo content in `<main>`.
  - Any `<input>` with `aria-haspopup`/`aria-expanded` needs `role="combobox"` (or be a `<button>`/`<select>` natively allowing it).
  - Every `role="progressbar"` needs `aria-label` — both hardcoded HTML + JS-generated.
  - Every hidden native form input (`<input type="file" class="visually-hidden">`) needs `aria-label` if not wrapped by `<label for>`.

---

## Patterns that worked

### P1. Immutable versioned URLs unlock production pinning
v0.5.3 → .4 → .5 → .6 → .7, each frozen at `https://ui.dang.pm/vX.Y.Z/`. External projects pin to immutable bytes; root continues iterating. No more "the kit moved, my layout broke" reports.

### P2. Parallel agents on independent work
Spawn 2-3 agents in the same message when tasks don't overlap files. Iter 22 ran N (path bug) + O (editorial dogfood) + P (deck dogfood) simultaneously. ~3x throughput on independent streams.

### P3. CI lenient mode for documented trade-offs
Some brand presets intentionally trade AA for fidelity (betterbuy `h:12 l:60` coral). `BRAND_KNOWN_FAILURES` env var lets CI accept those while still catching unexpected regressions on other brands. Documented in CI.md § Lenient mode.

### P4. Token catalog (REFERENCE.md) prevents reinvention
Iter 19 AA dogfood agent silently rewrote `hsl(... / 0.15)` inline because it didn't know `--primary-bg` existed. Iter 21 M dogfood (post REFERENCE.md ship) jumped straight to `--success-soft` and friends. Saved an estimated 10-15 min per dogfood. Token catalog is the highest-leverage doc the kit ships.

### P5. Self-critique loop with parallel agents (the meta pattern)
This 28-iter cycle exemplifies the loop: each `/loop` turn → reflect on current state → spawn focused fix agent(s) → wait for notification → reflect again → spawn next. The pattern scales because (a) each agent has bounded scope, (b) reflection accumulates between turns, (c) immutable snapshots let later iterations branch from known-good baselines without fear of breaking earlier work.

---

## What v0.5.10 added (Iter 37-41 arc)

- **4 a11y fixes**: 📁 [brands/](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/brands/) (Iter 37/38c — `.brand-vibe` clamp tighter + `.meta-tag` opaque bg + `.sale-timer` clamped, 0 fails); 📁 [_previews/](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/_previews/) 01-landing + 02-admin G14 elimination (Iter 39a — inline hex workaround, ADR-001 deviation noted); 📄 [demo/brand-switcher.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/demo/brand-switcher.html) `.eyebrow` raw `--primary` fix (Iter 38a); sale-timer clamped.
- **1 CI guard upgraded**: 🔧 [_build/gradient-contrast-probe.sh](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/_build/gradient-contrast-probe.sh) + 🔧 [_build/gradient-contrast-probe.ps1](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/_build/gradient-contrast-probe.ps1) — Iter 41 timeout fix (20s → 45s default), then flipped CI default-on (`CI_GRADIENT_PROBE=1` default in ci-check).
- **28 motion tokens + `prefers-reduced-motion` contract** in 📁 [motion/](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/motion/) — 📄 [motion/index.html](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/motion/index.html) 15-pattern demo + 📄 [MOTION.md](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/motion/MOTION.md) (Iter 40A).
- **11 UX state artifacts** in 📁 [uxstate/](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/uxstate/) + 📁 [atoms/](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/atoms/) + 📁 [molecules/](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/molecules/) + 📁 [organisms/](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/organisms/) — 4 loading atoms (skeleton/spinner/progress/dots) + 2 state molecules (empty/error) + toast organism + 🐍 [_assets/focus-trap.js](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/_assets/focus-trap.js) + 🐍 [_assets/keyboard.js](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/_assets/keyboard.js) + 📄 [UXSTATE.md](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/uxstate/UXSTATE.md) (Iter 40B).
- **2 reference docs**: 📄 [ROADMAP-COMPLETENESS.md](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/ROADMAP-COMPLETENESS.md) (kit 62% vs Tier-1, top 10 missing, v0.6→v1.0 roadmap — Iter 39b); 📄 [UX-PATTERNS.md](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/UX-PATTERNS.md) (20 patterns + gap matrix + AI-agent decision trees — Iter 39c).
- **4 codified gotchas**: G15 (brand-tinted `--primary` as 11px text), G16 (`color-mix(... transparent)` chip bg bleed), G17 (`background-blend-mode: multiply` darkens beyond static math), G18 (Tailwind prebuilt shades silently dropped).

---

## What's left for v0.6+

- **Real production adoption** — one of Dang's projects (KryDrive, KHI Studio, Cowork DNA landing) consuming v0.5.9 as a pinned dependency. Until that's done, the kit is theoretically production-ready but empirically unproven at scale.
- **Mobile breakpoint a11y** — 375 + 768 sweeps. Iter 32-35 measured desktop 1280 viewport only; mobile may surface new contrast/touch-target issues that desktop hides.
- **Dark-state a11y** — post-toggle audit. The brand-switcher + variant matrix host dark-aesthetic content but Lighthouse measures the default (warm) render. Programmatic toggle + re-measure needed.
- **Variant matrix programmatic test** — 6 archetype × 2 aesthetic × 7 brand = 84 combos. Currently spot-checked via dogfoods; needs grid capture + visual-diff sweep.
- **Screen reader testing** — NVDA / VoiceOver / TalkBack real-device testing (deferred from v0.5 known issues).
- **Component library extraction** — Atomic primitives (atoms/molecules) as standalone NPM package or zip. Currently coupled to the kit folder structure.
- **Migration guide v0.5 → v0.6** — when the next breaking change ships, devs pinned to v0.5.x need an explicit path forward.

---

## Acknowledgment

Built via `/loop` dynamic-pacing in Claude Code over 28 iterations on 2026-05-20. Each iteration: self-critique → fix → spawn agent(s) → verify → cut snapshot when stable. Agents averaged 30-60 tool uses each. Approximately 50-60 sub-agent invocations total. User input minimal post-Iter 0 — autonomous trajectory enforced by `/loop` re-invocations with the standing instruction "luôn đặt tiêu chuẩn cao hơn" (always raise the standard).

The kit at v0.5.7 reflects what AI agents + an autonomous loop can produce when given a clear quality bar (Lighthouse 100, axe 0, WCAG AA, 6-archetype × 7-brand coverage) and trusted to spawn sub-agents for focused fixes. No human pixel-pushing was required after Iter 0; every CSS rule, every doc paragraph, every guard script came from agent invocations + reflection.

The learnings above are the durable artifact. The code can be regenerated from these patterns + gotchas; the patterns + gotchas were earned over 28 iterations of trying, failing, fixing, and reflecting.
