<!-- Generated from /methodology at build time. Do not edit by hand. -->
<!-- Source of truth: the rendered page. A hand edit here is overwritten on the next build. -->

# Designesy — /methodology

Canonical page: https://www.designesy.org/methodology

---

designesy.org**/methodology** 45 checks 14 categories engine 1.5.0 contract v0.4.6 [contract JSON](/contracts/design-system.json)

## Methodology

How the 45-check engine turns a URL into a score, and how that method is tested. Deterministic: no language model and no human judgment, only regex, token-resolution and spec tests against the HTML and CSS a site serves.

On this page

1. How a score is made
2. Category weights
3. Grade bands
4. The cohort today
5. Accessibility floor
6. Anti-slop deduction
7. Originality lift
8. Hard-fail ceilings
9. What it measures
10. What it skips
11. How the method is tested
12. Every check
13. Data exports

## How a score is made

The engine fetches the page's HTML and every stylesheet it links, parses the `:root` custom properties, and runs **45 deterministic checks** across **14 weighted categories**. Each check returns `PASS`, `WARN`, `FAIL`, `MANUAL` or `N/A`. The score is a weighted average, then adjusted by an anti-slop deduction, an originality lift and two caps.

**It reads the delivered response.** The engine reads the HTML the server sends and the stylesheets it links. It runs no JavaScript and waits for no hydration, so on a site that renders in the browser every check reads the markup that arrives over the wire: a heading injected by script is absent to it, and a token set at runtime is not in the CSS it fetched.

That is a deliberate trade. The score stays deterministic, reproducible and cheap, free of headless rendering's timing flakiness: the same input yields the same output, which is what makes a score comparable week to week. It also bounds the claim: a low score on a client-rendered site describes its delivered HTML. Every result carries a `receipt` with `retrieved_at`, `engine_version` and a `digest` of the verdicts, so anyone can re-run the URL and confirm the answer.

**MANUAL** and **N/A** checks are left out of both numerator and denominator, as Lighthouse does with manual audits, so no site is penalised for what the static engine cannot run or what does not apply. The 3 manual checks need a browser viewport, a performance trace or live interaction; the conditional check applies only when a site serves what it reads.

### The pipeline, in order

1. Weigh the checks Each scored check carries its category’s weight, split across that category’s scored checks. Pass counts 1, warn 0.5, fail 0.
2. Subtract slop Twelve anti-slop rules take up to 20 points off for recognisable template patterns.
3. Add originality Seven kinds of craft add up to 8 points, halved when the slop is heavy.
4. Apply the floor Accessibility under 60 caps the score at 70, a C.
5. Apply the ceilings Any failing check caps the score at 89.9, a B. Seven severe failures cap it at 65, 70 or 75.

```
# Per-check weight: the category weight, split across its scored checks checkWeight = CATEGORY_WEIGHTS[category] / count(scored checks in category) # Status scoring PASS → 1.0 × checkWeight WARN → 0.5 × checkWeight FAIL → 0 MANUAL → excluded (not counted in the total) N/A → excluded (not counted in the total) # Step 1: weighted compliance weightedScore = round( Σ(weightedPoints) / Σ(weightedTotal) × 1000 ) / 10 # Step 2: anti-slop deduction, 12 rules, up to 20 points slopTotal = min( Σ(per-rule deductions), 20 ) score = max( 0, weightedScore − slopTotal ) # Step 3: originality lift, up to 8 points (halved when slop ≥ 12) originalityPoints = min( rawOriginality, 8 ) score = min( 100, score + originalityPoints ) # Step 4: accessibility floor if (a11yPct < 60) score = min( score, 70 ) # Step 5: hard-fail ceilings, after the lift (it never carries a score past one) if (any check FAILs) score = min( score, 89.9 ) # no A beside a FAIL if (v06 | v44 FAIL) score = min( score, 65 ) # contrast, status text if (v22 | v02 | v16 FAIL) score = min( score, 70 ) # CTA contrast, overflow, rem if (v24 | v25 FAIL) score = min( score, 75 ) # touch targets, headings # Per-category sub-score: step 1, scoped to one category categoryScore = round( Σ(catPoints) / Σ(catWeight) × 1000 ) / 10
```

round(… × 1000) / 10 keeps one decimal place: 95.2, never 95.2347. The per-category sub-scores use step 1 only; they are the category profiles on the leaderboard.

## Category weights

Weights follow the contract's own emphasis. They are relative and sum to **117**; the formula normalises them, so a category's real share of the score is its weight over 117, and only over the categories a site can be scored in. Performance and responsive have no scored checks in the static engine, so their weight is held in reserve.

Weights from lib/check-definitions, which mirrors the engine route.

| Category | Weight | Share | Checks | Scored |
| --- | --- | --- | --- | --- |
| Typography | 18 | 15.4% | 13 | 13 |
| Accessibility | 15 | 12.8% | 7 | 7 |
| Color roles | 12 | 10.3% | 2 | 2 |
| Motion | 10 | 8.5% | 5 | 5 |
| Tokens | 9 | 7.7% | 2 | 2 |
| Copywriting | 8 | 6.8% | 4 | 4 |
| Interface feel | 8 | 6.8% | 2 | 2 |
| Control polish | 7 | 6.0% | 3 | 2 |
| Page basics | 6 | 5.1% | 2 | 2 |
| Interaction | 6 | 5.1% | 1 | 1 |
| Performance | 6 | 5.1% | 1 | 0 |
| Security | 5 | 4.3% | 1 | 1 |
| DESIGN.md | 4 | 3.4% | 1 | 1 |
| Responsive | 3 | 2.6% | 1 | 0 |
| Total | 117 | 100% | 45 | 42 |

## Grade bands

The letter is a threshold on the number: F under 60, then a grade every ten points. An A needs 90 and no failing check: any FAIL caps the score at 89.9.

## The cohort today

point at a dot, or tap it, to read the site

Weekly run of 2026-10-12.

| Site | Rank | Grade | Score |
| --- | --- | --- | --- |
| Designesy | 1 | A | 100.0 |
| Atlassian Design System | 2 | B | 89.9 |
| GitHub Primer | 3 | B | 89.9 |
| X | 4 | B | 87.8 |
| Figma | 5 | B | 84.4 |
| Wikipedia | 6 | B | 83.8 |
| Apple | 7 | B | 83.1 |
| IBM Carbon | 8 | B | 82.8 |
| Roast by AI | 9 | B | 81.7 |
| Vercel | 10 | B | 81.1 |
| Designesy AI Studio | 11 | C | 74.5 |
| Awwwards | 12 | C | 72.1 |
| Linear | 13 | C | 70.0 |
| Stripe | 14 | C | 70.0 |
| zeroheight | 15 | C | 70.0 |
| Radix Colors | 16 | C | 70.0 |
| V&A Museum | 17 | C | 70.0 |
| Google Stitch | 18 | D | 68.5 |
| Material 3 | 19 | D | 66.4 |
| Notion | 20 | D | 65.5 |
| Adobe Spectrum | 21 | D | 65.1 |
| GitHub | 22 | D | 65.0 |
| Vercel Geist | 23 | D | 64.4 |
| Mozaika | 24 | D | 64.3 |
| IBM Plex | 25 | F | 56.8 |
| Pentagram | 26 | F | 56.6 |
| FWA | 27 | F | 54.6 |
| The New York Times | not ranked | C | 75.8 |
| Desy Guard | not ranked | C | 73.4 |
| CSS Design Awards | not ranked | F | 44.9 |

## Accessibility floor

If the accessibility category scores under 60, the score is capped at 70, a C, however high the weighted score. Perfect tokens and no accessibility cannot make an A.

The floor is a softer form of the DSAF enterprise-grade rule, which requires 75 in accessibility. Sixty is strict enough to stop the all-tokens, no-access failure and lenient enough that a site passing three of six accessibility checks is not capped. The cap binds only above 70: a score already under 70 is left as it is.

## Anti-slop deduction

A site can meet every contract rule and still look generic. A second pass runs **12 anti-slop rules** (S1 to S12) against the most recognisable template patterns, and each one found takes points off the weighted score directly. The deduction is flat, so a sparser site cannot game it.

**Per rule, at most 5 points; in all, at most 20.** A rule deducts `min(severity × min(instances, 3), 5)`: the fourth sighting of the same pattern adds nothing, three is enough to flag it.

| Rule | Pattern | Severity | Trigger |
| --- | --- | --- | --- |
| `S1` Overused fonts | Inter, Roboto, Open Sans, Montserrat, Poppins, Lato, Space Grotesk, Instrument Serif or Geist in `font-family` | 5 | 1+ match in any `font-family` |
| `S2` Full-page gradient | A multi-color `linear-gradient` on body or html, or a fixed or `inset: 0` overlay (1px hairlines excluded) | 5 | A gradient with `position: fixed`, `inset: 0` or `100vw`/`100vh` in the same block |
| `S3` Purple or violet gradient | `linear-gradient` with any of #615fff, #8e51ff, #4f39f6, #7f22fe, #a855f7, #9333ea, #7c3aed, #6d28d9, #5b21b6, #4c1d95 | 4 | 1+ match in a `background` with a gradient |
| `S4` Gradient text | `background-clip: text` over a gradient spanning two or more hue families (45° buckets; `var()` stops and neutrals ignored) | 4 | 2+ distinct non-neutral hue families in the stops |
| `S5` Default palette hexes | 3+ of #0f172a, #1e293b, #334155, #615fff, #8e51ff, #4f39f6, #7f22fe, #6366f1, #8b5cf6, #a78bfa, #0d6efd, #007bff | 5 | 3+ hex matches in CSS |
| `S6` Repeated card grid | 3+ classes matching `.(card|panel|tile|feature|item|box|cell|block)` on a repeating grid | 5 | 3+ card-like classes and `grid-template-columns: repeat(auto-fit|auto-fill|N)` |
| `S7` Emoji as icons | Emoji (U+1F300 to 1FAFF, 2600 to 27BF, 1F1E6 to 1F1FF) inside ` ` or a CTA link | 4 | 2+ emoji in buttons or CTAs |
| `S8` AI-pill badges | “AI-powered”, “Generate”, “Chat with AI”, “Powered by AI”, “Built with AI”, “AI-driven” | 3 | 1+ match in the HTML text |
| `S9` Lorem ipsum | “lorem ipsum”, “dolor sit amet”, “consectetur adipiscing”, “sed do eiusmod”, “tempor incididunt” | 5 | 1+ match in the HTML text |
| `S10` Single font family | One non-generic `font-family` on the whole page (serif, sans-serif, monospace and system-ui excluded) | 4 | Exactly one family name |
| `S11` Marketing buzzwords | 2+ of the rule's word list (streamline, empower, world-class, enterprise-grade, next-generation, disrupt and similar) | 3 | 2+ in the body text, tags stripped |
| `S12` Placeholder images | URLs from via.placeholder, placehold.co, placeholder.com, dummyimage, picsum.photos, loremflickr or unsplash.com/random | 4 | 1+ match in the HTML |

## Originality lift

The counterweight to slop: **seven kinds of craft** (O1 to O7), each detectable from CSS and HTML alone, add points. A compliant but generic site earns none; a bespoke one is credited. The lift is capped at **8 points**, so it nudges the score rather than steering it, and the score is clamped to 100.

**The slop gate.** With heavy slop (12 points or more), the lift is halved: craft on a heavily templated site is usually the framework's, and the lift is meant for the author's.

| Credit | Detection | Points |
| --- | --- | --- |
| `O1` Bespoke easing | Distinct `cubic-bezier()` curves outside the preset set (ease and its variants, Material, Tailwind v4, Bootstrap). A `linear()` spring with a body of 20+ characters counts as overshoot. | 1 for 1 to 2 curves · 3 for 3+ · +2 with overshoot · at most 5 |
| `O2` Modern layout | How many of `clamp()`, `container-type` or `@container`, and `subgrid` are present | 1 for one · 2 for two or more |
| `O3` Typographic detail | `font-feature-settings`, `font-variant-numeric`, `hanging-punctuation`, `text-underline-offset`, `font-optical-sizing` | 1 for one · 2 for two or more |
| `O4` Tiered reduced motion | A `prefers-reduced-motion` query that is targeted, where a blanket `* { animation: none }` earns nothing | 1, when targeted |
| `O5` Motion choreography | Scroll-driven animation (`animation-timeline`, `view-timeline`, `animation-range`), view transitions, or named `@keyframes` | 1 for 3+ named keyframes · 2 for scroll-driven or view transitions |
| `O6` Bespoke iconography | Inline ` ` or ` ` elements, placed by hand rather than from an icon font | 1 for 3+ inline SVGs or 2+ symbols |
| `O7` Semantic tokens | Role-named custom properties such as `--surface`, `--ink`, `--paper`, over hue names like `--color-blue-500`. The shadcn fingerprint (6+ of its default names) scores zero. | 2 for 4+ · 4 for 8+ · +2 layering · +2 theming · at most 6 |

## Hard-fail ceilings

A page with a failing check is never an A: any FAIL caps the score at 89.9, a B. Some failures cap it lower, whatever else is strong: a site that fails on contrast or overflows its viewport is held to a C or a D. Every cap applies after the originality lift and the accessibility floor, so the lift can carry a score up to a cap but never past it.

| Check | Cap | Why |
| --- | --- | --- |
| `any` Any failing check | 89.9 | A check the page fails cannot stand beside an A, so the grade is a B at most (engine 1.3.0). |
| `v06` Contrast readable | 65 | Text many readers cannot read: a failure of basic legibility. |
| `v44` Status text contrast | 65 | The words that report errors, warnings and success fall below WCAG contrast (engine 1.3.0). |
| `v22` CTA contrast | 70 | The primary call to action is hard to read. |
| `v02` Horizontal overflow | 70 | Content is cut off or scrolls sideways on small viewports. |
| `v16` Rem scale | 70 | A root font size under 16px sets off iOS Safari’s auto-zoom. |
| `v24` Touch targets | 75 | Controls under 44px are hard to use on touch screens. |
| `v25` Heading hierarchy | 75 | More than one h1, or skipped levels: the outline is broken. |

## What it measures

## What it skips

4 checks sit outside the score. 3 need a capability the static engine lacks (a rendered browser, a performance trace, live interaction) and are resolved by the full audit; one applies only when a site serves what it reads.

- `v02` manual Routes render without horizontal overflow at 375px, 720px, 860px, 1080px+ Requires a browser viewport trace: the engine fetches CSS, and overflow only exists in a rendered layout.
- `v04` manual Sound toggle flips aria-pressed and applies the audio preference Requires live DOM interaction; the engine does not execute JavaScript or click elements.
- `v37` conditional DESIGN.md spec-layer validation (DESIGN.md format lint) N/A if /DESIGN.md is not served at the target origin; no public convention requires it yet.
- `v21` manual Core Web Vitals plausible: LCP < 2.5s, INP < 200ms, CLS < 0.1 Requires a CDP trace: the engine fetches HTML and CSS, and vitals need a rendered page with timing data.

## How the method is tested

Four studies test the method against itself and against an outside instrument. Each figure below is read from the study's own report file, dated, and each report regenerates with one command.

Every leaderboard site's real check statuses are recomputed under 36 changes to the method: weight scaling, the WARN credit, the slop and originality layers, the bands, the floor and ceilings, and each category favoured one at a time.

**Uniform weight changes do nothing.** Scaling every weight by 10 or 20 per cent in either direction moves no score by more than 0.0 and no rank: the ratio cancels uniform scaling. **The judgment calls carry the sensitivity:** the WARN credit (15 grades), then the originality lift (9) and the grade bands (8). Favouring any single category by half again moves at most 2 grades and 2.6 points.

sensitivity-report.json, generated 2026-10-12 for contract v0.4.5. Left out, the engine could not read them on that run: nytimes.com, getdesy.com, cssdesignawards.com. Regenerate: node scripts/sensitivity-analysis.mjs.

| Perturbation | Grades changed | Rank moves | Largest change |
| --- | --- | --- | --- |
| weights −10% (uniform) | 0 | 0 | 0.0 |
| weights +10% (uniform) | 0 | 0 | 0.0 |
| weights −20% (uniform) | 0 | 0 | 0.0 |
| weights +20% (uniform) | 0 | 0 | 0.0 |
| WARN credit 0.5 → 0.25 | 15 | 20 | 14.4 |
| WARN credit 0.5 → 0.75 | 15 | 25 | 19.9 |
| slop deduction +5 | 4 | 24 | 15.0 |
| originality lift +5 | 9 | 19 | 5.0 |
| grade bands tightened 2pts (92/82/72/62) | 8 | 0 | 0.0 |
| grade bands loosened 2pts (88/78/68/58) | 3 | 0 | 0.0 |
| a11y floor 60% → 50% | 1 | 13 | 14.9 |
| a11y floor 60% → 70% | 3 | 6 | 14.4 |
| hard-fail ceilings removed | 2 | 7 | 5.0 |
| any-FAIL cap removed (the rule before engine 1.3.0) | 1 | 2 | 0.5 |
| INTERACTION: WARN 0.75 × slop +5 | 15 | 25 | 25.1 |
| INTERACTION: WARN 0.25 × slop +5 | 15 | 26 | 19.1 |
| INTERACTION: WARN 0.75 × a11y floor 70% | 13 | 26 | 14.1 |
| INTERACTION: WARN 0.25 × a11y floor 70% | 17 | 20 | 19.9 |
| INTERACTION: slop +5 × originality +5 | 6 | 20 | 16.0 |
| INTERACTION: WARN 0.75 × originality +5 | 14 | 24 | 19.1 |
| INTERACTION: WARN 0.75 × ceilings removed | 20 | 23 | 21.6 |
| INTERACTION: slop +5 × ceilings removed | 5 | 24 | 15.0 |
| OAT: cadence weight ×1.5 | 0 | 10 | 2.6 |
| OAT: accessibility weight ×1.5 | 2 | 10 | 1.9 |
| OAT: semantic weight ×1.5 | 2 | 9 | 2.1 |
| OAT: motion weight ×1.5 | 2 | 13 | 1.9 |
| OAT: tokens weight ×1.5 | 0 | 2 | 1.7 |
| OAT: takt weight ×1.5 | 0 | 8 | 1.3 |
| OAT: poise weight ×1.5 | 0 | 8 | 0.9 |
| OAT: identity weight ×1.5 | 0 | 2 | 0.9 |
| OAT: interaction weight ×1.5 | 0 | 12 | 2.6 |
| OAT: performance weight ×1.5 | 0 | 0 | 0.0 |
| OAT: responsive weight ×1.5 | 0 | 0 | 0.0 |
| OAT: security weight ×1.5 | 0 | 5 | 1.3 |
| OAT: spec weight ×1.5 | 0 | 2 | 1.4 |
| OAT: copywriting weight ×1.5 | 1 | 12 | 2.0 |

A method release should never be a silent regression. Each contract version is diffed: the same check statuses scored under the previous version's profile, rebuilt from the changelog, and the change reported per site. From v0.3.0 to v0.4.0, 14 grades moved (6 up, 8 down); the largest rise was apple.com at +6.9, the largest fall pentagram.com at -15.7.

Every score response pins its `contractVersion`. Before adopting a new version in CI, run the diff: if a grade moves, it says why.

| Site | v0.3.0 | v0.4.0 | Change |
| --- | --- | --- | --- |
| designesy.org | 100.0 A | 93.0 A | −7.0 |
| atlassian.design | 79.6 C | 86.1 B | +6.5 |
| x.com | 79.2 C | 84.2 B | +5.0 |
| primer.style | 82.3 B | 82.4 B | +0.1 |
| apple.com | 76.2 C | 83.1 B | +6.9 |
| vercel.com | 86.2 B | 81.7 B | −4.5 |
| wikipedia.org | 76.9 C | 80.7 B | +3.8 |
| figma.com | 81.1 B | 76.6 C | −4.5 |
| designesy.ai.studio | 70.2 C | 74.8 C | +4.6 |
| awwwards.com | 66.0 D | 71.6 C | +5.6 |
| roastbyai.com | 80.7 B | 71.2 C | −9.5 |
| zeroheight.com | 70.0 C | 70.0 C | 0.0 |
| vam.ac.uk | 70.0 C | 70.0 C | 0.0 |
| linear.app | 70.0 C | 69.5 D | −0.5 |
| github.com | 81.0 B | 68.5 D | −12.5 |
| m3.material.io | 61.4 D | 67.6 D | +6.2 |
| stripe.com | 70.0 C | 66.9 D | −3.1 |
| notion.so | 70.0 C | 65.0 D | −5.0 |
| radix-ui.com | 68.8 D | 63.2 D | −5.6 |
| mozaika.design | 59.2 F | 62.8 D | +3.6 |
| geist.dev | 62.0 D | 62.0 D | 0.0 |
| stitch.withgoogle.com | 62.0 D | 61.2 D | −0.8 |
| carbondesignsystem.com | 71.2 C | 60.7 D | −10.5 |
| spectrum.adobe.com | 59.3 F | 58.9 F | −0.4 |
| plex.ibm.com | 53.8 F | 58.3 F | +4.5 |
| pentagram.com | 70.9 C | 55.2 F | −15.7 |
| fwa.org | 59.4 F | 48.0 F | −11.4 |

score-diff-report.json, generated 2026-09-28. Left out, the engine could not read them on that run: nytimes.com, getdesy.com, cssdesignawards.com. Regenerate: node scripts/score-diff.mjs --all.

Each site is rated by two instruments that never see each other's results: the engine (accessibility category at 60 or more passes) and axe-core 4.10.2, the WCAG engine, run in a real browser (no serious or critical violation passes). Across 27 sites, κ = 0.208 (95% interval -0.15 to 0.57): slight agreement.

The two measure different layers. The engine reads what shipped CSS declares (tokens, focus-visible, reduced motion); axe reads what renders (computed contrast, ARIA, button names, alt text). Neither contains the other, which is why the full audit at `/api/score/audit` runs a real browser.

| Engine verdict | axe-core 4.10.2 passes | axe-core 4.10.2 fails |
| --- | --- | --- |
| Engine passes | 9 | 8 |
| Engine fails | 3 | 7 |

blind-comparison-report.json, generated 2026-09-28. Left out, with no verdict from one of the two raters: nytimes.com, getdesy.com, cssdesignawards.com.

After sensitivity comes the adversarial test: can a chosen weighting put a site on top? (An analysis of the OECD Better Life Index found 19 of 36 countries could be ranked first.) Every site is recomputed under 30 weightings (the published one, uniform, and each category doubled or halved) and its best and worst rank reported.

On 2026-09-28, the top five held under every scenario. The widest band was awwwards.com, ranks 10 to 16. Ranks here are that run's, which can differ from today's leaderboard.

rank-bounds-report.json, generated 2026-09-28. Left out, the engine could not read them on that run: nytimes.com, getdesy.com, cssdesignawards.com. Regenerate: node scripts/rank-bounds.mjs.

| Site | Best | Published | Worst | Band |
| --- | --- | --- | --- | --- |
| designesy.org | 1 | 1 | 1 | 0 |
| atlassian.design | 2 | 2 | 4 | 2 |
| x.com | 2 | 3 | 6 | 4 |
| apple.com | 3 | 4 | 6 | 3 |
| primer.style | 3 | 5 | 7 | 4 |
| vercel.com | 4 | 6 | 7 | 3 |
| wikipedia.org | 3 | 7 | 8 | 5 |
| figma.com | 7 | 8 | 9 | 2 |
| designesy.ai.studio | 8 | 9 | 10 | 2 |
| awwwards.com | 10 | 10 | 16 | 6 |
| roastbyai.com | 9 | 11 | 15 | 6 |
| zeroheight.com | 10 | 12 | 13 | 3 |
| vam.ac.uk | 12 | 13 | 14 | 2 |
| linear.app | 13 | 14 | 16 | 3 |
| github.com | 11 | 15 | 16 | 5 |
| m3.material.io | 13 | 16 | 19 | 6 |
| stripe.com | 15 | 17 | 19 | 4 |
| notion.so | 17 | 18 | 21 | 4 |
| radix-ui.com | 18 | 19 | 24 | 6 |
| mozaika.design | 17 | 20 | 21 | 4 |
| geist.dev | 19 | 21 | 22 | 3 |
| stitch.withgoogle.com | 20 | 22 | 25 | 5 |
| carbondesignsystem.com | 20 | 23 | 25 | 5 |
| spectrum.adobe.com | 22 | 24 | 26 | 4 |
| plex.ibm.com | 23 | 25 | 26 | 3 |
| pentagram.com | 23 | 26 | 26 | 3 |
| fwa.org | 27 | 27 | 27 | 0 |

## Every check

All 45, by category, heaviest first. Open a category to read how each of its checks is decided.

Typography rendering discipline: font smoothing, rem scales, line-height, text-wrap, tabular figures, selection styling, font synthesis, underline position and skip-ink.

- `v14` Cadence typography rules match live CSS and contract.cadence Checks for the Cadence rule set: font-synthesis: none, text-underline-position: from-font, text-decoration-skip-ink: auto, -webkit-font-smoothing: antialiased, -moz-osx-font-smoothing: grayscale, root font-size: 16px, all sizes in rem. PASS if all present. These are Designesy Cadence rules, so on an external site (scope=universal) their absence is N/A.
- `v15` Font smoothing: antialiased + grayscale on :root confirmed Searches :root or html for -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale. PASS if both are present. Prevents subpixel rendering artifacts on dark backgrounds.
- `v16` Rem-based scale: all text sizes in rem, root at 16px confirmed Counts rem-based vs px-based font-size declarations. PASS if the majority are rem and root is 16px. The 16px root is the Cadence floor: iOS Safari auto-zooms inputs below 16px.
- `v17` Line-height by role: headings 1.08, body 1.55 confirmed Extracts line-height values from heading and body selectors. PASS if heading line-heights cluster near 1.08 and body line-heights near 1.55. Tight headings read as deliberate; relaxed body copy reads as confident.
- `v18` text-wrap: balance + pretty both present in live CSS Searches for text-wrap: balance (headings) and text-wrap: pretty (paragraphs). PASS if both are present. Progressive enhancement: unsupported browsers ignore them. A Designesy Cadence rule, so on an external site (scope=universal) its absence is N/A.
- `v19` tabular-nums: 8 instances across the live CSS Counts font-feature-settings: "tnum" or font-variant-numeric: tabular-nums declarations. PASS if ≥ 8 instances (threshold for a site that takes numeric display seriously). Prevents digits from shifting width as values change.
- `v20` ::selection styled with var( --signal ) instead of the browser default Searches for ::selection rules using var( --signal ). PASS if the selection color is the signal token instead of the browser default. The selection color is a small but loud brand surface.
- `v26` Font family count ≤3 (body + heading + mono) Parses all font-family declarations and counts distinct families. PASS if ≤ 3. WARN if 4 or 5. FAIL if 6+. More than 3 families suggests inconsistency and hurts performance.
- `v46` Type sizes come from the scale Reads the type scale from the page's own :root type tokens (names such as --text -*, --font-size -*, --fs -* or --type -* whose value is a static rem or px length; a colour token such as --text-muted never counts), takes their values plus their median ratio as the steps, and the smallest token as the floor. Every static rem or px font-size is judged: off the scale when it sits more than 2.5% from every step, under the floor when it is smaller than the smallest token. A clamp(), min() or max() that moves with a viewport or container unit is deliberate fluid type and is accepted as written; only its lower bound is held to the floor. Relative sizes (em, %, keywords) follow their parent and are not judged. WARN only; SKIP with fewer than three type tokens. v16 asks for rem; this asks whether each rem sits on the scale.
- `v28` Reading width 45-75ch on prose containers Parses each CSS rule and keeps its selector alongside its ch value, then asks whether that selector actually targets prose: paragraph-like elements (p, article, li, blockquote) or prose-named classes (.prose, .lede, .measure, .note). Rules on structural selectors (grid, table, row, flex, pre, code) are excluded, because a measure on a grid narrows one track rather than fixing line length. PASS requires a prose-targeting rule in 45 to 75ch (66ch ideal). WARN covers three distinct states, reported separately: ch rules exist but none reach prose; prose rules exist but all are outside the band; or no ch rule at all. Lines longer than 75ch are hard to track; shorter than 45ch feels choppy. Method change 2026-09-17 : v28 previously scanned the stylesheet for any max-width in ch units and passed if one value was in range, without checking which selector carried it, which let this site measure 108.6ch on three pages while scoring zero v28 warnings. The check now requires the measure to reach prose. Two leaderboard sites (X, GitHub Primer) moved PASS to WARN under the corrected method, about 0.6 points each; recorded because a method change that moves published grades should be disclosed rather than applied silently.
- `x01` font-synthesis: none set (Cadence resolved tension) Searches for font-synthesis: none. PASS if declared. WARN if font-synthesis is declared but not set to none, or if no rule is found. Prevents the browser from synthesizing bold/italic faces when the real weights are not loaded, a common cause of blurry headlines on Windows.
- `x02` text-underline-position: from-font set (Cadence resolved tension) Searches for text-underline-position: from-font or under. PASS if declared. Uses the font designer’s built-in underline position rather than the browser default, which is usually too low and clips descenders.
- `x03` text-decoration-skip-ink: auto set Searches for text-decoration-skip-ink: auto or none. PASS if declared. Makes underlines skip the rounded parts of letters (g, j, p, q, y), a small typographic refinement that shows attention to craft.

WCAG 2.2 AA primitives: contrast, touch targets, heading hierarchy, the input font floor, button text contrast, forced-colors readiness and status colors used as text in every theme. It carries the accessibility floor.

- `v06` Contrast remains readable for ink, muted, and accent on paper (WCAG 2.1 + APCA) Resolves --paper, --ink, --muted, --muted-dim to RGB and computes WCAG 2.1 contrast ratios. PASS if all clear 4.5:1 (AA body text). WARN if any clear 3:1 but not 4.5:1. FAIL if any below 3:1. APCA Lc values are reported alongside.
- `v22` Primary button text passes WCAG AA contrast against --signal fill Resolves --signal to RGB, then tests --ink and --paper against it. PASS if the best ratio ≥ 4.5:1. WARN if ≥ 3:1 (large-text pass). FAIL if below 3:1. N/A if --signal is not declared or unresolvable.
- `v24` Touch targets ≥44px on interactive elements (WCAG 2.5.5 Enhanced) Searches CSS for min-height or min-width ≥ 44px on button/a/input/select selectors. PASS if found. 44px is WCAG 2.2 SC 2.5.5 Target Size (Enhanced), level AAA; the AA minimum, SC 2.5.8, is 24px, and the contract holds the stricter bar. Full verification needs a browser to measure rendered dimensions.
- `v25` Heading hierarchy: single h1, no skipped levels Parses the HTML for h1-h6 elements. PASS if exactly one h1 and no skipped levels (no h1→h3 jumps). Screen readers and SEO both rely on a logical heading outline.
- `v27` Input font-size ≥16px (prevents iOS Safari auto-zoom) Searches CSS for input/textarea/select font-size declarations. FAIL if one is below 16px, PASS if the 16px (1rem) floor is declared. With neither, WARN if the page has a text input, textarea or select; N/A if it has none, since nothing can zoom. Inputs below 16px trigger a layout-shift zoom on iPhone that breaks mobile UX.
- `v35` Forced-colors readiness: @media (forced-colors: active) block present Searches CSS for @media (forced-colors: active) and forced-color-adjust. PASS if both are present. Windows High Contrast Mode and Chrome forced-colors recolor the page; without this media query, critical UI becomes illegible.
- `v44` Status colors used as text meet contrast in every declared theme Finds the status colors (custom properties named ok, success, warn, warning, error, danger, info, grade-a to grade-f and the like, or a hue the stylesheet also paints as a mark) that a rule uses as text color, directly, through one alias or at reduced alpha. Resolves each in every declared theme (:root, [data-theme] or.dark/.light blocks, prefers-color-scheme blocks; a theme named by part of a compound theme selector also reads that compound’s tokens), including var() chains and color-mix() rounded to the 8-bit color a browser paints. Measures it on a surface the stylesheet attests: the background the rule or another rule for the same element sets, else the nearest ancestor’s in the same selector (a hovered button’s own fill, else its fill at rest), translucent fills composited, else the page background of that theme. PASS if every use clears 4.5:1 (3:1 for 24px, or 18.66px bold; 3:1 for a graphic: an svg, an icon or glyph, a progress bar, a color named for an icon, a currentColor fill) in every theme. FAIL if a color named as a status falls short where it has a hue (Oklch chroma 0.06 or more). WARN if white or grey text under a status name falls short (a general text-contrast miss: the status lives in the fill behind it), if a hue that counts only because the sheet also paints it as a mark falls short (its real surface may be a fill the static engine cannot see), if an ancestor in a hover, active, focus, expanded or open state paints no fill the sheet declares (the engine never FAILs a pairing it cannot attest), if a background cannot be resolved, or if alpha-reduced text clears only at full alpha. Disabled states are exempt, as WCAG 1.4.3 exempts inactive controls. N/A if no status color is used as text. Found on this site: light --warn as text measured 3.51:1.

Whether the color system speaks in roles (ink, surface, danger) or in hues (blue-500), and covers the status states. Wired on 2026-08-30, so batch runs before that date score it empty.

- `v42` Semantic color vocabulary: tokens named by role rather than hue Classifies every color-valued token in :root by name: role words (ink, paper, surface, muted, danger, success, accent, border…) vs hue words (blue, slate, amber…) and numeric scale suffixes (-500). Brand coinages matching neither list are excluded from the share. PASS if ≥60% role-named with ≥3 distinct roles; WARN otherwise (WARN-only, because it is style craft with no user harm). SKIP if fewer than 4 color tokens. Mirrors the contract's own role-named palette.
- `v43` Semantic status colors: ok/warn/error/info state roles present Scans color token names for the four status families: ok/success, warn/warning/caution, error/danger/fail, info/notice. PASS if ≥3 of 4 families are present; WARN otherwise (WARN-only). SKIP if the site declares no color tokens. The contract ships --ok, --warn, and --error for exactly this purpose.

Motion hygiene: no transition: all, will-change kept to transform and opacity, a reduced-motion block, duration tokens, and content that stays visible when motion is paused.

- `v05` prefers-reduced-motion disables entrance and wordmark breath Reads each @media (prefers-reduced-motion) block by its value. PASS if a reduce block declares rules, or if a no-preference block opts into motion (an animation, a transition, smooth scrolling or a view transition), which keeps that motion from anyone who asked for less. WARN if neither: an empty block, or a no-preference block that only switches motion off, honours nothing. This is the vestibular-safety primitive: without it, motion-sensitive users cannot use the site.
- `v11` No transition:all in the live stylesheet Regex-searches for transition: all (case-insensitive). FAIL if found. transition: all causes layout-thrash and surprises; the contract requires named properties only.
- `v12` will-change restricted to transform and opacity only Parses every will-change declaration. If any value contains anything other than transform or opacity (including auto), it WARNs. will-change on non-transform/opacity properties forces unnecessary layer promotion.
- `v23` Duration tokens --duration-quick through --duration-slow present in :root Checks :root for the 5 duration tokens: --duration, --duration-quick, --duration-fast, --duration-medium, --duration-slow. PASS if all 5 are declared. Hardcoded ms values in component CSS are the anti-pattern.
- `v45` Pausing motion keeps content visible Finds a pause that holds every element: animation-play-state: paused on * under an attribute or class on the document (html[data-motion="paused"]), or inside prefers-reduced-motion: reduce. Then finds each one-shot entrance whose first keyframe sets opacity 0 or visibility hidden, which the pause would hold invisible. PASS if every such entrance has an override under the pause (animation: none, animation-name: none, animation-play-state: running, or a negative animation-delay at least as long as the animation) for its selector or a compound it contains. FAIL if an entrance has none. WARN if the override exists but its selector or scope does not clearly match. N/A if no rule pauses every element. Found on this site: paused visitors got empty pages.

Token architecture: the --paper foundation, and whether tokens are layered from primitive to semantic to component.

- `v01` Token values match live site :root foundation Parses :root custom properties from the fetched CSS. Looks for --paper specifically: the contract names --paper, --ink, --muted, --surface, --surface-raised, --line, --signal, --signal-light, --signal-dim as the required foundation. PASS if --paper resolves to a value.
- `v29` Token architecture: primitive → semantic → component layers Counts how many tokens are referenced via var() (aliasing) vs. raw values. A 2-tier or 3-tier aliasing structure (primitive → semantic → component) indicates a mature token system. PASS if at least 2 layers are detected.

UX copy discipline: verb-led buttons, no trailing periods, descriptive link text, no all-caps. Heuristics grounded in NN/g, Microsoft Fluent, IBM Carbon and WCAG 2.4.4.

- `v38` Button text is a verb phrase or recognized command (not a bare noun) Parses button elements and checks if text starts with a verb or recognized command (Save, Cancel, Delete, Edit, Share, Close, Back, Next). WARN if buttons don't lead with a verb. N/A if no buttons found. Heuristic: review flagged buttons manually. Grounded in NN/g: "Lead with verbs or verb phrases that clearly outline what will happen after the command is selected."
- `v39` No trailing period on button text, labels, or tab text Searches button/label/tab elements for trailing periods (excludes ellipsis...). WARN if found. N/A if no relevant elements. Microsoft Fluent: "Don't end text for buttons, radio buttons, labels, or checkboxes with a period." Periods are for full sentences in tooltips, error messages, and dialog bodies only.
- `v40` Link text is descriptive (not bare "click here", "learn more", "here") Parses anchor elements and checks link text against a blocklist of non-descriptive patterns (click here, here, learn more, read more, more, link, this, that, continue, see more, view details). WARN if matched. N/A if no anchors. WCAG 2.4.4 Link Purpose: link text should describe the destination.
- `v41` No ALL CAPS UI text except eyebrow labels Searches button/a/label/td/th/p/li/h1-h6 for ALL CAPS text (>3 letters), excluding elements with class containing "eyebrow"/"meta-label" or inline text-transform: uppercase. WARN if found. IBM Carbon: "All caps has been shown to be slower to read." Only eyebrow labels and acronyms should be uppercase.

Interaction feel: press scales above the 0.95 floor (0.96 for cells, 0.985 for cards, 0.995 for surfaces). Named for the German word for precise, musical timing.

- `v10` Takt interface-feel rules match live CSS and contract.takt Verifies the static half: stagger enter animation-delay, soften exit transform ease-out, concentric border-radius set. Press-behavior and hit-area require a browser.
- `v13` Press scale 0.96 on cells, 0.985 on cards/rows (both above the 0.95 floor) Extracts every transform: scale() value in :active contexts. FAIL if any scale is 0 (a glitch rather than a press) or below 0.95. PASS if real press scales are found above 0.95. The 0.95 floor is the contract minimum; lower reads as a glitch.

Interaction poise: hover lifts, press-settle, keyboard-path documentation and the sound toggle. The static half is read from CSS; the interaction half needs a browser.

- `v04` manual Sound toggle flips aria-pressed and applies the audio preference Requires clicking a sound toggle and verifying aria-pressed flips and a [data-audio] attribute is applied. The static engine cannot interact with the DOM.
- `v08` Poise interaction rules match live /labs/poise and contract.interaction Verifies the static half: fine-pointer hover guard (@media hover: hover), press-settle scale ~0.97, opacity-only mark breath. The interaction-feel half requires a browser.
- `v09` Poise keyboard-path verification remains published and current Verifies the static half: 4 keyboard-affordance signals in the HTML/CSS (tabindex, accesskey, key bindings, focus management). Tab-order traversal requires a browser.

Document identity: semantic landmarks (h1, title, meta description, main, header, nav) and AI-disclosure readiness (EU AI Act, Article 50).

- `v07` Semantic HTML foundation: single h1, title, meta description, landmark Parses HTML for: exactly one h1, a descriptive <title>, <meta name="description">, and at least one <main>/<header>/<nav> landmark. PASS if all 4 are present. WARN if 1-2 missing. FAIL if 3+ missing.
- `v34` AI-Disclosure Readiness (EU AI Act Art 50, effective 2026-08-02 ) Detects AI-interactive surfaces (chatbots, AI assistants) in the HTML. If detected, checks for disclosure signals (visible "AI" text, aria-label, meta generator, C2PA). PASS if no AI surface is detected (disclosure not required) or if disclosure is present. FAIL if an AI surface is detected without disclosure.

Focus visibility: :focus-visible rings declared.

- `v03` Primary interactive elements show focus-visible rings Regex-searches the CSS for :focus-visible declarations. PASS if any :focus-visible rule is found. This is the keyboard-navigation visibility primitive: without it, Tab users cannot see where they are.

Core Web Vitals (LCP, INP, CLS). They need a CDP or Playwright trace, so the static engine marks them manual; the weight is held in reserve.

- `v21` manual Core Web Vitals plausible: LCP < 2.5s, INP < 200ms, CLS < 0.1 Requires a CDP/Playwright trace to measure LCP, INP, and CLS against the Google thresholds. The static engine cannot do this.

Unicode security: UTS #39 confusable detection in token names and CSS identifiers, against Cyrillic and Greek homoglyphs shadowing Latin names.

- `v36` Unicode Security: no UTS #39 confusable characters in token names or CSS identifiers Scans token names, CSS class/id selectors, and url() refs for non-ASCII confusable characters (Cyrillic, Greek, fullwidth) using a Unicode confusable detector. PASS when 0 confusables. FAIL when token-name confusables found (shadowing risk, e.g. --соlor-bg with Cyrillic с vs --color-bg ). WARN for class/id/url confusables. Provenance: Unicode Technical Standard #39, Unicode 16.0.0.

The DESIGN.md spec layer: the DESIGN.md format’s linter, run when a site serves /DESIGN.md and not applicable otherwise.

- `v37` conditional DESIGN.md spec-layer validation (DESIGN.md format lint) Fetches /DESIGN.md from the target origin and runs the DESIGN.md format's linter (11 lint rules: broken token refs, missing primary colors, WCAG contrast, orphaned tokens, section order). PASS on clean lint. WARN on lint warnings. FAIL on lint errors. N/A if /DESIGN.md is not served; this is expected, as no public convention requires it yet.

Horizontal overflow at 375, 720, 860 and 1080 px and up. It needs a browser viewport, so the static engine marks it manual; the weight is held in reserve.

- `v02` manual Routes render without horizontal overflow at 375px, 720px, 860px, 1080px+ Requires rendering the page at four viewport widths and measuring scrollWidth > clientWidth. The static engine cannot do this.

## Data exports

Contract v0.4.6 · engine 1.5.0 · 45 checks in 14 categories · 12 slop rules, up to −20 · originality, up to +8 · 8 hard-fail ceilings · engine at [/api/score](/api/score) · contract at [/contracts/design-system.json](/contracts/design-system.json)
