Cohesion Audit
Find every place a page drifts from the project's design system. The system has a canonical layer (base templates + base stylesheet + shared components + design-system docs); this skill scans everything else against that layer and reports drift, ranked by severity.
This is not a code-quality audit. It's specifically about visual + structural consistency. Different from:
- Dead-code audits (dead code / duplication)
- UX coverage audits (missing-feature gaps)
- Reuse / quality audits on changed code
- Accessibility audits
- Security review
Arguments
- No args: audit current working directory
- A project name or path (e.g.,
myapp) --live: also fetch live URLs and compare rendered output across pages--fix: auto-fix safe categories (rare; most fixes need design judgment — default off)
Project Registry
| Alias | Path | Canonical layer |
|---|---|---|
myapp |
(add your projects here) | (describe your canonical files: base templates, base stylesheet, components, design system docs) |
For React Native / Expo projects: the cohesion model is different (StyleSheet objects, Theme provider, native components). Skip this skill or adapt it heavily — flag and stop.
Process
Step 0 — Identify the source of truth
Resolve the project. Read in order:
CLAUDE.md— any documented design conventions- The canonical base template(s) — typically a
base.html(Jinja),app/layout.tsx(Next.js),Layout.astro(Astro), orindex.htmlfor static sites - The canonical stylesheet — typically
base.css,globals.css, or a theme file - Any shared components / macros —
components/ui/,src/components/shared/,_macros/, etc. - Design-system docs —
DESIGN_SYSTEM.md,BRAND_VOICE.md, design contract files
From these, extract the canonical inventory:
| Token type | What to extract |
|---|---|
| Color tokens | All --* CSS variables defined in the canonical stylesheet's :root block (or Tailwind theme.colors, or equivalent) |
| Font tokens | --font-display, --font-body, etc. — and the actual font families they map to |
| Component classes | Every class defined in the canonical stylesheet that's meant to be reused: .btn-primary, .btn-secondary, .card, .form-group, .empty-state, etc. |
| Allowed base templates / layouts | Which template / layout names child pages are allowed to extend |
Allowed <link> stylesheets |
Which external stylesheets are sanctioned (typically just one font provider for the canonical families) |
| Page-shell elements | What every page is expected to have: nav, footer, main landmark, lang attr, etc. |
Write this inventory down (mentally or in a scratch buffer); the audit compares against it.
Step 1 — Page-shape violations (RED)
Highest-severity drift. A page that doesn't extend the base shell shows up looking like a different site entirely.
For every page-level template / route (the ones a route handler actually renders), check:
1a. Extends an allowed base
- Jinja: does it have
{% extends "base.html" %}(or the project's canonical base)? - Next.js: is it inside the App Router root layout, or does it define its own
<html>/<body>? - Astro: does it wrap content in
<Layout>(or the project's canonical layout)? - Static HTML: does it include a canonical shared header/footer pattern?
A page that doesn't extend anything renders without nav/footer/global styles.
1b. Loads canonical stylesheet
- Does it inherit the canonical stylesheet from the base layout, OR does it load its own?
- Pages that load their own stylesheet bypass the design system.
1c. Has the canonical wordmark + nav
- If the base has a canonical
<nav>, does the page render with it? - Pages with their own custom
<nav>implementation = drift.
1d. Has the footer (specifically: includes the partial, not its own copy)
- Is the canonical footer included? Or does the page roll its own footer markup?
- Concrete detection: for every page-level template, check both:
- Does the file include the canonical footer (e.g.,
{% include "_footer.html" %}in Jinja,<Footer />in React, framework equivalent)? AND - Does the file contain a
<footer>tag NOT inside that include?
- Does the file include the canonical footer (e.g.,
- Either condition failing is RED. A page with its own inline
<footer>is the most common drift in old/orphaned pages — and when the inline copy ages, it falls behind the canonical (missing newer links like Home/Blog/social, browser-default link styling, broken indentation copy-pasted from a stale snapshot).
1e. Has the canonical background / hero layer
- If the base has a canonical background element (
.video-bg,.hero-bg, etc.), does the page rely on it or override it? - See Step 1.5 — overriding canonical background rules in page CSS is the load-bearing failure mode here.
1f. Has the canonical viewport + lang
<html lang="en">, viewport meta — usually inherited but worth checking pages with custom<head>.
Red flag heuristic: any page that defines its own <html>, <head>, <body>, or <footer> instead of inheriting from base is RED.
Step 1.5 — Global selector overrides in page CSS (RED)
The class of bug standard cohesion checks miss: a page-specific stylesheet (or inline <style> block) that redefines selectors which already exist in the canonical stylesheet. The page extends the base correctly, includes the footer correctly, but its own CSS silently overrides global rules — producing visible drift like wrong z-index on the background, wrong link colors in the footer, layout glitches in the nav.
The specific bug shape:
- Canonical stylesheet defines
.video-bg { z-index: -1 }(behind everything) - Page stylesheet redefines
.video-bg { z-index: 0 }(above default-z elements) - Browser cascade: page CSS loads after canonical, so page rule wins
- Result: the global background renders ON TOP of UI elements that don't have explicit z-index, breaking the layered visual stack
Detection (mechanical):
For every project-level .css file (NOT the canonical one):
- Extract every selector that has
display:,position:,z-index:, orfont-family:declarations - Cross-reference with the canonical stylesheet
- Any overlap is RED — even if the override "looks the same," the duplication means future canonical changes don't propagate
For every inline <style> block in a template:
4. Run the same check on its rules
5. RED if any global selectors are touched
Forbidden override list (extract from your canonical stylesheet):
For each project, the audit must build this list from the canonical layer. Common categories to extract:
- Root element selectors:
body,html - Background / shell selectors:
.video-bg,.hero-bg, etc. - Navigation: the project's nav class + any nav sub-elements
- Footer: bare
footerelement selector + any footer-specific classes - Cookie / consent banner selectors
- Skip-link selectors
- Main / landmark selectors:
main,#main-content
Each hit outside the canonical stylesheet is a RED finding. The fix is one of:
- Delete the override (let canonical drive — most common)
- Rename the selector to something page-scoped (e.g.,
.blueprint-video-overlayinstead of.video-bg) - Move the rule into the canonical stylesheet if it should apply globally
Step 1.6 — HTML well-formedness in templates (RED, mechanical)
The simplest-possible static check that catches a real class of catastrophic bugs: a page-level template has an unclosed <script> or <style> tag. The browser's HTML parser, while inside a <script> element, only exits on </script> — content type doesn't matter. So the parser stays in script-content mode through all subsequent <head> content (favicons, fonts, base stylesheet link, analytics — all swallowed as script content) until the next </script> it can find.
Detection (mechanical, per template):
# Per page-level template, count opens vs closes for tags that MUST balance.
# A mismatch is a RED finding — the page is broken HTML.
for tpl in src/templates/**/*.html; do
for tag in script style; do
opens=$(grep -cE "<$tag[[:space:]>]" "$tpl")
closes=$(grep -c "</$tag>" "$tpl")
if [ "$opens" != "$closes" ]; then
echo "RED $tpl: <$tag> $opens opens, $closes closes (mismatch)"
fi
done
done
Other tags worth balancing on page-level templates: <head>, <body>, <html>, <main>, <form>, <table>, <div> (last one is noisy — only flag mismatches > 5).
Common false-positive shape to ignore:
- Self-closing or void elements (
<meta>,<link>,<img>,<input>,<br>,<hr>) don't need closes. <script>withsrc=""is self-contained but still needs</script>per HTML spec.- Templating block boundaries inside a
<script>tag — fine, the rendered output collapses correctly UNLESS the templating block itself never closes the open<script>.
Auto-fix eligibility: balance violations are NOT generally auto-fixable — the missing close could go in many places, and inserting it wrong creates a worse bug. Always file as a follow-up with the exact line number of the unclosed open. Human inspects + adds the closing tag.
The exception: if the unclosed tag is OBVIOUSLY at the end of a templating block and the missing close is one line, an auto-fix is safe.
Step 1.7 — Shared UI-component class drift (RED)
The most insidious drift category — what we'll call parallel-definition drift:
- A canonical class like
.form-section-titleis defined inbase.csswithcolor: var(--secondary)(copper) - The SAME class is redefined in several page-level CSS files, all with subtly different colors
- A user notices visible drift across forms: section headers reading as a different shade than field labels
- Fix requires updating all definitions in lockstep AND patching files that USED the class but didn't define it (relying on browser defaults)
This is distinct from Step 3's component drift:
- Step 3 catches: page invents
.checkout-btninstead of using canonical.btn-primary(USAGE drift) - Step 1.7 catches: page redefines
.btn-primaryitself with different properties (DEFINITION drift)
Definition drift is worse because each copy is a subtle variation of the canonical; updating the canonical doesn't propagate. The user perceives it as "100 different shades of white" or "buttons that almost-but-don't match." Standard cohesion checks miss it because each individual rule looks valid in isolation.
Canonical UI-component class list (extract from your canonical stylesheet):
For each project, build this from the actual stylesheet. Common patterns:
- Form scaffolding:
.form-section-title,.form-section,.form-group,.field-label,.field-error,.field-help - Buttons:
.btn,.btn-primary,.btn-secondary,.btn-text - Cards:
.card,.card-* - States:
.empty-state,.error-state - Layout:
.skip-link,.video-bg,.sticky-nav,.nav-*
For each class, run three mechanical checks:
1.7a — Duplicate definitions (RED)
# For each canonical class, count definitions across the project:
for cls in form-section-title field-label btn-primary btn-secondary card empty-state; do
hits=$(rg -ln "^\\s*\\.${cls}\\s*\\{" src/)
count=$(echo "$hits" | grep -c .)
if [ "$count" -gt 1 ]; then
echo "RED .${cls} defined in $count files:"
echo "$hits" | sed 's/^/ - /'
fi
done
A canonical class should be defined ONCE, in your canonical stylesheet. Page-level redefinitions are RED, even if the values look identical — the duplication itself is the bug.
1.7b — Property-value mismatch across definitions (RED)
When a class IS defined in multiple places, compare property values:
# Pull every definition + its property block, diff against the canonical:
rg -A 15 "^\\s*\\.form-section-title\\s*\\{" src/
# → eyeball: do `color`, `font-family`, `font-size`, `padding`, `letter-spacing`
# match across all hits? Mismatches are RED.
Especially flag: color, font-family, font-size, font-weight, padding, letter-spacing, border. These produce the most visible cross-page drift.
1.7c — Used-but-undefined (RED)
A class used in markup with NO matching CSS rule reachable on the page renders with browser defaults — silently broken until someone notices.
# For each canonical class, find templates that USE it:
for cls in form-section-title field-label btn-primary card; do
rg -l "class=\"[^\"]*\\b${cls}\\b" src/templates/ | while read tpl; do
# Check if the template loads a stylesheet that defines the class.
# Simplest heuristic: does the canonical stylesheet load on this page
# AND does it define the class?
base_loads=$(grep -l "extends.*base.html\|/static/base.css" "$tpl")
base_defines=$(grep -c "^\\s*\\.${cls}\\s*\\{" src/static/base.css)
if [ -z "$base_loads" ] && [ "$base_defines" -gt 0 ]; then
echo "RED $tpl uses .${cls} but doesn't load base.css"
fi
done
done
Auto-fix bundle (per Step 7 pattern):
- 1.7a duplicates with verbatim copy → bundle PR that deletes the duplicates, leaves only the canonical
- 1.7b property mismatch on a single property (e.g., just
color) → bundle PR that flips all definitions to the canonical value - 1.7b property mismatch on multiple properties → file as follow-up, needs design judgment
- 1.7c orphan usage → file as follow-up; either add to canonical stylesheet or stop using
Step 2 — Token discipline (YELLOW)
Find every place that hard-codes a value that should be a token. These look fine in isolation but produce subtle drift over time.
2a. Hex color codes outside the canonical stylesheet
- Grep for
#[0-9a-fA-F]{3,6}across all templates + static files. - Filter out the canonical stylesheet itself + any acceptable surfaces (e.g., favicon manifest, social-card images).
- Every remaining hit is a candidate for tokenization. Cross-reference: does the hex match an existing token? If yes, it's a swap-in fix. If no, it's a new color the design system should know about.
2b. font-family: declarations outside the canonical stylesheet
- Inline
style="font-family: ..."and page-specific<style>blocks redefining font-family.
2c. Inline style attributes that redefine tokens
style="color: #B87333"should bestyle="color: var(--accent)"(or better, a class)style="background: rgba(26, 20, 24, 0.6)"should be a token
2d. External font imports beyond the canonical set
- Drift here is silent and brand-breaking.
Step 3 — Component drift (YELLOW)
Find every place that recreates a canonical component instead of using the macro/class.
3a. Button-like elements not using .btn-* classes
- Every
<button class="...">and<a class="...">whose visual is button-like - Group by class. If the same visual exists under 5 different class names, that's drift to consolidate.
- Especially flag: page-scoped classes like
.checkout-btn,.sample-report-link,.primary-actionthat recreate.btn-primarystyling.
3b. Card-like <div> clusters that should be the canonical card component
- Look for
<div class="*-card *-panel *-box">patterns with manual border + padding + border-radius - These should use the project's canonical card primitive.
3c. Form inputs that bypass the canonical form scaffolding
- Any
<div class="form-group"><label><input></div>markup that exactly mirrors what the canonical form macro renders should be migrated to the macro.
3d. Status/empty/error states bypassing the canonical state component
- Custom "no results" markup, custom error banners — fold into the canonical component.
Step 4 — Inline <style> block size (smell heuristic)
For each page template:
- Count lines inside
<style>blocks (excluding comments and whitespace) - A page with > ~50 lines of inline CSS is probably drifting from the system
- Rank pages by inline-CSS size; the worst offenders get top billing in the report
This isn't a violation per se — sometimes page-specific styling is correct. But it's a smell. Worth eyeballing the largest blocks for token drift, parallel components, or stuff that should be in the canonical stylesheet.
Step 5 — Cross-page rendering compare (--live flag)
When --live is passed, fetch every public route via WebFetch and parse the rendered HTML.
For each page, check the rendered output (not source) for:
<main>landmark present- Canonical
<nav>present <footer>present- Canonical background element present
- Canonical font preconnects + font provider link present
- No additional
<link rel="stylesheet">beyond the canonical set <html lang="...">present
Build a comparison matrix:
| Page | nav | footer | bg | base.css | extra stylesheets |
|---|---|---|---|---|---|
/ |
yes | PASS | yes | PASS | (none) |
/blog |
yes | PASS | yes | PASS | (none) |
/checkout |
yes | no (custom) | yes | PASS | own.css |
| ... |
Pages with any "no" are RED. Inconsistencies across the matrix are the visible drift.
Step 6 — Ranking
Compose findings into a single prioritized report:
## Cohesion Audit — [project]
### RED RED — Page-shape violations
1. **[file]** — extends base but overrides the content block with full custom layout including its own nav and footer. Drift visible to users.
2. **[file]** — does not extend any base; renders with bespoke `<head>`/`<body>`. Carries its own @font-face block + footer.
### YELLOW YELLOW — Component drift
1. `.checkout-btn` recreates `.btn-primary` (used in [N] places). Migrate or alias.
2. `.sample-report-link` recreates `.btn-text`. Migrate.
3. [N] inline `style="color: #B87333"` should be `var(--accent)`.
### YELLOW YELLOW — Token discipline
1. [N] hex colors outside canonical stylesheet ([list top 5 with file:line]).
2. [N] `font-family:` declarations outside canonical stylesheet.
### ORANGE SMELL — Inline `<style>` block size
1. `tools/blueprint.html` — [N] lines of inline CSS (largest)
2. `tools/time-travel.html` — [N] lines
3. `pages/index.html` — [N] lines (may be acceptable; landing pages tend to have hero-specific styling)
### Summary
- Top priority: [the worst offender]
- Next: [the next 2-3 priorities]
- Estimated cleanup effort: [rough size]
Each RED finding gets a follow-up issue or note (or one umbrella issue with sub-findings) per project conventions.
Fix Mode (--fix flag) and auto-fix bundle
When --fix is set, bundle the auto-fixable findings into a single PR.
Safe to auto-fix (auto_fix: true):
- Replace inline
<footer>block with the canonical footer include when the inline copy is a stale partial of the canonical footer - Delete page-CSS rules that override global selectors when the override doesn't add new behavior. If the override DOES change behavior, downgrade to
auto_fix: false(needs design review). - Replace inline
style="color: #B87333"withstyle="color: var(--accent)"when the hex unambiguously matches a known token - Add
rel="stylesheet"reference to the canonical stylesheet on a page that doesn't load it (only if the page already extends a base — bare-<head>rewrites are NOT auto-fixable) - Remove dead CSS rules (e.g., orphan selectors with no markup using them)
Requires confirmation (auto_fix: false):
- Migrating a page from custom layout to extending the canonical base (changes routing assumptions, may break tests)
- Replacing a parallel button class with
.btn-primary(may need CSS specificity adjustments + visual review) - Removing an inline
<style>block in favor of canonical equivalents (extraction first, then cleanup as separate PR) - Renaming a global-selector override to a page-scoped class (touches markup + CSS together)
Never auto-fix:
- Hex colors that don't match a known token (could be intentional brand exception or a new token to add)
- Font-family changes (could break ascenders/descenders/em sizing)
- Removing custom layouts (usually represents real design judgment that needs review)
- Page-CSS that overrides a global selector if the override is non-trivial (e.g., adds keyframes, redefines a media query block) — flag and file as follow-up, don't touch
Workflow: partition findings by auto_fix, build branch + commit + push + open PR with the auto-fixable set, file follow-ups for the rest. Cap at 5 fixes per PR; if more, split by category (footer-includes, override-deletions, hex-substitutions, etc.).
Rules
- Source of truth is the canonical layer. Don't invent a "what should be" from thin air; read the base layout, base stylesheet, components, and design docs to compute the inventory FIRST. Then audit against THAT.
- Severity by impact. A page without the global nav is RED — users see a different site. A hex color that should be a token is YELLOW — invisible drift today, brand-breaking when the brand color changes tomorrow.
- Per-token-substitution fixes are easy; layout migrations are hard. Default scope is detection + reporting. Layout migrations need a separate ticket per page.
- Watch for grandfathered violations. Existing cohesion lints often have an explicit grandfathered list. Read it; don't re-flag what's already known. Do flag if items have been on the list too long.
- Be specific. "tools/blueprint.html drifts from the design system" is useless. "tools/blueprint.html declares its own @font-face for Cormorant Garamond at line 47, has a 480-line inline
<style>block, uses.checkout-btninstead of.btn-primary, and includes a custom<footer>instead of the canonical footer include" is useful.
Skill Run Logging
If you keep a skill-run log, append an entry after each run:
Format: | YYYY-MM-DD | /cohesion | target (details) | one-line result |