# Design Retrofit Audit Kit (LLM-ready)
**What this is**: the failure modes and verification procedure for applying a new design to a website that **already exists and already earns**. Every item was found by measuring a real retrofit: a live 106-page Eleventy site, three locales, 556 JSON-LD blocks, 23 machine-readable endpoints.
The headline result: a site-wide contrast sweep found **68 failures**, including every primary call to action at 4.0:1, below the AA minimum, on a site that had been live for months. Final state was **0 failures in both themes across all three locales, with 0 regressions in the machine-readable diff.**
**How to use it**: paste this entire file into Claude, ChatGPT, Gemini or any coding agent, then add one line: *"Retrofit this design onto my site at ./src. Work through the kit in order."* Pair it with **premium-design-system-spec.md** if you also need the design values themselves.
**License**: use it, adapt it, ship it. Originally published at https://jwatte.com/blog/retrofitting-design-system-what-broke/
---
## INSTRUCTIONS FOR THE ASSISTANT
Work through the phases in order. Do not skip Phase 1, and do not report the job finished until Phase 5 returns zero.
**Hard rules**: - Presentation only. Color, type, space, motion. Nothing else.
- Never change markup, structured data, canonicals, hreflang, meta tags, or any machine-readable output. If a change appears to require it, stop and ask.
- Never report a design as done based on how a screenshot looks. Phase 5 is the definition of done.
- When you deviate from a value, say which one, what you used, and why.
---
## PHASE 1: RECONNAISSANCE (before writing any CSS)
### 1.1 How does the site theme itself?
Grep for CSS custom property definitions and for theme selectors (`[data-theme=...]`, `.dark`, `prefers-color-scheme`).
- **Token-based** (a theme block redefines the *same* property names): the retrofit can be a single additive stylesheet loaded last that redefines those names. Reverting is deleting one ``. Scope the job at hours.
- **Hardcoded per component**: the retrofit is a rewrite. Scope it an order of magnitude higher and tell the user before starting.
### 1.2 Count the hardcoded literals inside theme blocks
This is the step that saves the most time, and skipping it cost three deploy cycles in the real retrofit.
```
grep -oE '\[data-theme="[a-z]+"\][^{]{0,90}\{[^}]*\}' style.css | grep -E '#[0-9a-fA-F]{3,6}'
```
One real stylesheet contained **64 rules** under its light-theme selector that hardcoded literal colors instead of referencing tokens. Written at specificity `(0,2,x)`, every one outranks a token-based override at `(0,1,0)`.
**Write ONE remap block at matching specificity covering all of them.** Do not fix these one component at a time. If you find yourself fixing the same class of bug in a third component, stop and count instead.
### 1.3 Inventory the assets you already have
- **Fonts.** If the display face is self-hosted, inspect the files. Variable fonts routinely cover 400 to 900 while the stylesheet declares only 700+. Adding `font-weight: 400 500` declarations pointing at the *same files* gives you light display type for **zero additional bytes**.
- **Images.** Check whether modern formats exist on disk but are unreferenced. In the real retrofit, WebP versions existed for every image and **none were served**, because templates used a bare ``. Serving what was already there cut 4.46 MB to 1.81 MB, a **59% reduction**.
- **Photography ceiling.** Record the largest available portrait and hero assets. If the only headshot is 200x200, no design can fill a large portrait block, and the layout must be capped rather than upscaling it.
### 1.4 Find the inline styles
```
grep -oE 'style="[^"]*(color|background)[^"]*"' **/*.html | sort -u
```
Inline styles beat every selector. You will need `!important` or an attribute selector for each. Knowing the list now prevents three rounds of surprise later.
---
## PHASE 2: BUILD THE PROOF HARNESS (still before any CSS)
"It is presentation-only" is a claim. On a site that earns, claims should be tests.
Write a script that walks the built output and records, per page:
- every page URL
- every JSON-LD block, and the full set of `@type` values inside it **recursing through all nested objects and arrays**, not just `@graph`
- whether each JSON-LD block still parses
- canonical URL
- every `rel="alternate"` hreflang and its href
- meta description, robots, OpenGraph and Twitter tags
- `h1` presence
- count of images missing `alt`
- every non-HTML machine-readable endpoint (`robots.txt`, sitemaps, `llms.txt`, feeds, `.well-known/*`)
Then: **capture before, capture after, diff. Regressions fail the run. Additions are reported and never fail.**
Run it after every change. In the real retrofit the baseline was 106 pages, 23 endpoints, 556 JSON-LD blocks, 33 distinct schema types, 0 unparseable blocks, 0 images missing alt, and it never moved across four rounds of work.
It is about fifty lines and it converts the scariest part of the job into a green check.
---
## PHASE 3: APPLY THE DESIGN
Write one new stylesheet, loaded after the existing one. Redefine the existing token names. Add the remap block from 1.2. Do not rewrite component rules.
**Order that surfaces problems earliest**: tokens → type → section rhythm → hero → cards/imagery → controls → header → footer → interior pages → motion.
**Do not forget interior pages.** In the real retrofit, roughly 100 of 106 pages rendered through a `.page-content` wrapper whose headings sit at specificity `(0,1,1)`, outranking bare element selectors. They kept the old weight-800 headings while the homepage looked completely redesigned. Put a homepage and a deep interior page side by side before believing the job is done.
---
## PHASE 4: THE ELEVEN FAILURE MODES
### 4.1 One color token cannot be both ink and fill: *the expensive one*
An accent dark enough to be readable **text** on a light ground is too dark to be a **background** behind dark text. If one token does both, fixing one role silently breaks the other everywhere.
Darkening an accent to reach 4.5:1 as text took the header button, the skip link, the cookie buttons and the card buttons in three locales all down to 2.88:1, in one edit.
Define `--accent`, `--accent-fill`, and `--on-accent-fill` separately. Also prefer **ink** for buttons: if the accent already carries eyebrows, numerals, rules and links, putting it on buttons overspends the one-accent budget. Ink means "do this", accent means "notice this".
### 4.2 Composite alpha when measuring contrast
A checker that reads `rgba(179, 146, 78, 0.14)` off a computed style and treats it as opaque will report failures that do not exist, and will miss real ones where a translucent overlay darkens something. Both directions matter.
Walk up the ancestor chain compositing each translucent layer onto the next until you hit an opaque background, then compare. A first sweep without this reported seven failures on pills and outline buttons that were all fine.
### 4.3 When several elements fail on the same background, fix the background
One panel nested four darkening layers: section, panel, inner panel, and a translucent zebra wash. Muted ink on the innermost ground was 4.06:1.
Darkening each failing element moved only the elements named while every sibling kept failing. **Raising the ground by a few points fixed all of them in one line.** Chase the ground before the ink.
### 4.4 A late `:root` declaration silently beats every earlier theme block
`:root` and `[data-theme="dark"]` have the **same specificity**, `(0,1,0)`. When the attribute is present both selectors match the same element, so **source order decides**.
Re-declaring a token in `:root` near the bottom of a stylesheet therefore overrides the dark palette declared above it. Result in the real retrofit: a light-colored panel with light-colored text in dark mode. **1.1:1, 81 failures on one page.**
Restate theme overrides **below** any late token declaration, and audit each theme independently.
### 4.5 A blanket color override must enumerate its dark contexts
Rewriting every instance of a hardcoded color to a corrected token will eventually hit an element sitting on a dark surface, inverting a fix into a failure. It happened to a credential band with an inline near-black background.
CSS cannot ask what color is behind an element. There is no clever selector, only an explicit list of dark surfaces to skip, and you find that list by measuring.
### 4.6 Do not globally style an element you did not author
Giving `blockquote` the display-serif pull-quote treatment is correct until you find the site uses `blockquote` for customer reviews, some past 700 characters, now rendering at 30px display serif.
Before styling any bare element selector on a codebase you did not write, list what actually uses that element.
### 4.7 Interior pages have their own specificity
See Phase 3. `.page-content h1` at `(0,1,1)` beats `h1` at `(0,0,1)` and beats `.section-title` at `(0,1,0)`.
### 4.8 Guard the hiding, not just the animating
Scroll-driven reveals set `opacity: 0` and animate to 1. In a browser without support that is a page whose content never appears.
```css
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
/* the opacity:0 rule lives ONLY in here */
}
}
```
Verify afterwards that zero elements sit below full opacity while in the viewport. **A progressive enhancement that can hide content is not an enhancement.**
### 4.9 Verification traps that produce false bug reports
- **Full-page screenshots do not trigger `loading="lazy"`.** Cards render as empty gray rectangles that look exactly like broken image paths. Scroll in steps, wait, then capture. Images inside collapsed `` legitimately never load.
- **Review screenshots at native resolution.** Downscaling turns fine-line SVG icons into things that read as emoji, and color emoji into things that read as icons. Both mistakes were made in one afternoon.
- **Things hide from grep.** Emoji frequently ship as **numeric HTML entities**, so searching for the literal character returns zero. Search entity forms (`[0-9]{5};`) too.
- **A live 200 is not proof.** Check the rendered value, not the status code.
### 4.10 Build-time data traps
- **Parallel API calls.** Two build-time fetches fired concurrently; one intermittently timed out, so that value rendered a fallback placeholder while its sibling rendered fine. It reads as a template bug. Sequential with one retry costs a second and is reliable.
- **A secret with no fallback.** A data file reading only `process.env.KEY` works on the host (where the variable is set) and silently renders placeholders in **every local build**. If a non-secret fallback already exists in the repo, use it. A local build that quietly renders wrong is worse than one that fails loudly.
### 4.11 Staging clones must not be indexable
If you preview the redesign on a separate deployment, its content duplicates a site that ranks. Before the first deploy:
- `X-Robots-Tag: noindex, nofollow` at the **header** level, so no template can defeat it
- `Disallow: /` in the preview's `robots.txt`
- **blank the analytics IDs and search-console verification tokens**, or staging pollutes production reporting and can be verified as its own property
- **remove anything that pings the outside world**: deploy scripts that submit sitemaps or ping indexing endpoints must never run from a preview
- leave canonicals pointing at production; that is correct for a staging clone
---
## PHASE 5: THE DEFINITION OF DONE
Run all four. The job is not finished until every one passes.
1. **Machine-readable diff: 0 regressions.** Pages, endpoints, JSON-LD block count, schema types, parse errors, images missing alt, canonicals, hreflang.
2. **Contrast sweep: 0 failures, in EVERY theme, in EVERY locale.** Walk every element that has its own text node. Composite alpha. Thresholds: 4.5:1 normal text; 3:1 large text (>= 24px, or >= 18.66px at weight >= 700); 3:1 non-text UI including **input borders** (WCAG 1.4.11).
3. **No horizontal overflow at 390, 768 and 1440.** Compare `documentElement.scrollWidth` against `clientWidth`. Ignore elements that are deliberately positioned off-canvas.
4. **No element stranded invisible.** Nothing below 0.9 opacity while inside the viewport.
Scoreboard from the real retrofit, as a sense of the shape:
| Stage | Contrast failures |
|---|---|
| First sweep (light) | 68 |
| After the token remap | 38 |
| After splitting ink from fill | 14 |
| After fixing the nested ground | 3 |
| **Final, light** | **0** |
| Dark theme, first audit | 81 |
| **Dark theme, final** | **0** |
---
## THE ELEVEN-LINE VERSION
1. Check how the site themes before you scope. Token-based means hours; hardcoded means a rewrite.
2. Count the hardcoded literals inside theme blocks and remap them in ONE pass.
3. Write the proof harness before the design.
4. Check whether your fonts already contain the weights you want.
5. Check whether modern image formats are on disk but unreferenced.
6. Never let one color token be both ink and fill.
7. Composite alpha when measuring contrast.
8. When several elements fail on one background, fix the background.
9. Re-declared tokens beat earlier theme blocks. Restate overrides below, audit themes separately.
10. Blanket color overrides must enumerate their dark contexts.
11. Guard the hiding, not just the animating.
---
## LIMITS
Measured on one real site in August 2026. Counts (68 failures, 64 hardcoded rules, 59% image reduction, 106 pages) are that site's numbers, not universal constants; the **failure modes** are the transferable part.
Specificity behavior follows the CSS Cascade specification. Contrast thresholds follow WCAG 2.2 SC 1.4.3 and 1.4.11. `animation-timeline: view()` support is partial at time of writing, which is the entire reason for the `@supports` guard.
**A passing automated contrast sweep is a floor, not proof of accessibility.** It does not test keyboard operability, screen-reader semantics, focus order, motion sensitivity beyond the reduced-motion query, or cognitive load. Treat it as the cheapest possible first gate, then get a real audit.
Companion file: **premium-design-system-spec.md**, for the measured design values themselves.