CSS cascade, specificity, and selector-weight discipline for large legacy stylesheets (50+ plain-CSS files, no build-time scoping). Use when adding, changing, or reviewing CSS in wordpress-theme/skyyrose-flagship — a color/typography override, a new component rule, a scoped reset, or a "the style isn't applying" investigation. Do NOT use for React/CSS-Modules/Tailwind work where the compiler already scopes selectors (use frontend-patterns), and do NOT use it as the .min build checklist (that ...
Scanned 9/11/2026
Install to Claude Code
npx -y skills add SkyyRoseLLC/DevSkyy --skill css-cascade-discipline --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Css Cascade Discipline?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/skyyrosellc-css-cascade-discipline)More formats (shields.io, HTML) on the badges page.
---
name: css-cascade-discipline
description: CSS cascade, specificity, and selector-weight discipline for large legacy stylesheets (50+ plain-CSS files, no build-time scoping). Use when adding, changing, or reviewing CSS in wordpress-theme/skyyrose-flagship — a color/typography override, a new component rule, a scoped reset, or a "the style isn't applying" investigation. Do NOT use for React/CSS-Modules/Tailwind work where the compiler already scopes selectors (use frontend-patterns), and do NOT use it as the .min build checklist (that is theme-min-build) — this skill is about which selector wins, not which bytes ship.
---
# CSS Cascade Discipline
Plain-CSS codebases have no scoping compiler. Every new rule fights every existing rule. This skill exists because a shipped fix (`#719`) was silently defeated: `.hero-cta-primary { color: var(--void,#050505) }` (specificity 0,1,0) lost to a scoped reset `.homepage-v2 a { color: inherit }` (0,1,1) — white-on-white CTA in production while the "fix" sat in the served file.
## When to use
**Observable events:**
* You are adding a rule that sets a property some other rule already sets on the same element — any color, `font-family`, `display`, or spacing override in `wordpress-theme/skyyrose-flagship/assets/css/`.
* A declaration is present in the served CSS file but the browser paints something else ("the fix is in the file and the page still looks wrong").
* You are about to land a *scoped reset* (`.some-section a { … }`, `.page-class button { … }`) — the highest-blast-radius change in a plain-CSS tree.
* Review of a PR that touches two or more stylesheets that both style the same component.
**When NOT to use:**
* **React with CSS Modules, styled-components, or Tailwind** — the toolchain generates unique class names, so cross-file specificity fights do not arise. Use `frontend-patterns`.
* **Pure build/minification questions** — "did the `.min` get rebuilt", "is the version bumped". That is `theme-min-build`. This skill only answers *which selector wins*; the `.min` step appears here because a cascade fix that never reaches the `.min` is invisible, not because this skill owns the build.
* **Choosing a color/font value.** Brand canon is locked elsewhere (`design-qc`, `docs/brand/`). This skill governs whether the value you chose actually applies.
## Inputs
| Required before you start | How to confirm | If absent |
|---|---|---|
| The exact element + property in dispute | name it: `.hero-cta-primary` / `color` | **STOP.** Without a named property you cannot compute a winner, and "restyle the hero" is not a cascade task. |
| The full list of stylesheets the page enqueues | read `wordpress-theme/skyyrose-flagship/inc/enqueue.php` | **STOP.** Load order decides specificity ties. Guessing it produces a fix that loses to a file you did not know was there. |
| Every competing rule for that property | `grep -rn '<selector-fragment>' wordpress-theme/skyyrose-flagship/assets/css/ --include='*.css' \| grep -v '\.min\.'` | **STOP — never proceed on the assumption that no competitor exists.** That assumption *is* bug #719. Absent census = do not write the rule. |
| A pristine baseline for the linter | see Verification rule 4 | **STOP** before attributing any stylelint error to your change — the tree already has 452. |
Always exclude `.min.` from greps. The minified siblings are generated; matching them doubles every
result and tempts you to "fix" a build artifact by hand.
## Specificity math (memorize)
`(inline, id, class/attr/pseudo-class, element)` — compare left to right, first difference wins. Ties → later in source order wins (across files: later-enqueued file wins).
- `.hero-cta-primary` → (0,1,0)
- `.homepage-v2 a` → (0,1,1) — class + ELEMENT beats a lone class for that element
- `.hero-cta.hero-cta-primary` → (0,2,0) — the standard escalation
- `[data-collection="love-hurts"] .v7card__add` → (0,2,0); attribute selectors count as class-weight
## Procedure — before writing ANY override rule
1. **Find the competition.** Grep every stylesheet the page loads for rules that could set the same property on the same element: the element's classes, its tag scoped under ancestors (`.section-scope a`, `.page-class button`), and `*` resets. In skyyrose-flagship, the known scoped resets are in `homepage-v2.css` (`.homepage-v2 a { color: inherit }`), `main.css` and `commercial-polish.css` (`* { … !important }` inside `@media print` — inactive on screen, don't panic-match them).
2. **Compute both weights.** Your rule must beat the strongest competitor OR come later at equal weight. When targeting an element (`a`, `button`, `input`) under a scoped reset, you need ≥ (0,1,1): double-class the component (`.x.x-variant`) or scope it (`.page-scope .x-variant`).
3. **Never solve with `!important`.** It wins today and creates the next un-overridable fight. Escalate specificity structurally instead.
4. **Custom-property fallbacks don't fix cascade losses.** `var(--token, #literal)` protects against a missing token, NOT against a competing selector. Diagnose which failure you actually have before choosing the fix.
5. **Rebuild the `.min`** — production serves it. `cd wordpress-theme && npm run build`.
6. **Run the Verification checks below** and paste real output.
## Probing a CSS fix in the browser (why `getComputedStyle` lies)
`getComputedStyle` probes lie to you in three specific ways:
- **Transitions mask inline probes**: `el.style.color = 'red'` read synchronously returns the transition's t=0 value when the element has `transition: all`. Set `el.style.setProperty('transition','none','important')` first.
- **CSSOM walkers**: `CSSStyleRule.cssRules` is always truthy (CSS nesting API) — `if (r.cssRules) walk(...); continue;` skips every style rule. Check `r.selectorText && r.style` FIRST, recurse `r.cssRules` separately.
- **Browser/CDN cache**: the page's CSSOM may be a stale copy even after deploy. Verify the RULE TEXT in the page's own CSSOM (`r.style.cssText`), not just the file fetched by curl — curl gets a fresh edge copy, the browser may not.
Decisive experiment when a declaration mysteriously loses: set `color: red !important` ON THE RULE via CSSOM. If red applies, cascade defeat confirmed — hunt the winner by weight, not by vibes.
## Landing a scoped reset safely (the other direction)
Adding a rule like `.my-section a { color: inherit }` retroactively beats every (0,1,0) component rule inside that section. Before landing one: grep for component classes used inside the section that style the same property at (0,1,0) and pre-escalate them. A scoped reset without this census is a time bomb.
## skyyrose-flagship specifics
- Production serves `.min` — after ANY css edit: `cd wordpress-theme && npm run build`, verify the fingerprint in the `.min` output, not just source.
- Load order = enqueue order in `inc/enqueue.php`. A later-enqueued file wins specificity ties against an earlier one.
- Prefer scoping new component rules under the template's body/section class (e.g. `.homepage-v2 .hero-strip`) — both for weight and for blast-radius control.
## Verification
Run from `/Users/theceo/DevSkyy/.claude/worktrees/glimmering-crafting-shannon`. Four independent checks;
each can return "no".
1. **Lint the source, not the build.** The tree has a large pre-existing baseline, so `exits 0` is the
wrong pass condition — it would fail every time and teach you to ignore it.
```bash
cd wordpress-theme && npx stylelint 'skyyrose-flagship/assets/css/**/*.css' --allow-empty-input
```
**PASS:** the *set of violation lines* is unchanged from the pristine baseline (see check 4) — no new
`file:line rule` entry attributable to your edit.
**Observed baseline 2026-07-28: `452 problems (452 errors, 0 warnings)`, 154 auto-fixable** `[repro]`.
A bare `--fix` across the tree would rewrite 154 unrelated sites — do not run it to "clean up".
2. **The shipped bytes match the source you edited.** Production serves `.min`; a source-only edit ships
nothing, so a cascade fix can be perfect and still invisible.
```bash
cd wordpress-theme && npm run build && npm run verify:aspect min-sync
```
**PASS:** `[PASS] min-sync every .min css/js byte-identical to a fresh build`.
**Observed 2026-07-28: PASS, `0 fail, 0 warn`** `[repro]`.
3. **The live document actually serves the version you think it does.** Batcache serves stale, so the
cache-buster is not optional, and **never `WebFetch`** — it strips `<script>` and misleads.
```bash
curl -s "https://skyyrose.co/?cb=$(date +%s)" | grep -o 'skyyrose-flagship/style[^"]*ver=[0-9.]*' | head -1
```
**PASS:** the `ver=` value equals `SKYYROSE_VERSION` in `functions.php`.
**Observed 2026-07-28: `skyyrose-flagship/style.min.css?ver=1.12.8`** `[live]`. If your fix is not in
1.12.8, the live page cannot contain it regardless of what the working tree says — that gap is exactly
the `[repo]` → `[live]` jump the evidence rules ban (bug-287).
4. **Attribution before you claim a finding is yours (rule 4).** With a 452-error baseline, "stylelint
is red" proves nothing. Extract the pristine tree and diff the violation *contents* — a new error
hides as one more line inside an already-red check.
```bash
mkdir -p /tmp/css-attr && git archive HEAD wordpress-theme/skyyrose-flagship/assets/css | tar -x -C /tmp/css-attr
ln -s "$PWD/wordpress-theme/node_modules" /tmp/css-attr/node_modules 2>/dev/null
(cd /tmp/css-attr && npx stylelint 'wordpress-theme/skyyrose-flagship/assets/css/**/*.css' \
--formatter unix 2>/dev/null | sed 's|^/tmp/css-attr/||' | sort) > /tmp/css-attr/before.txt
(cd wordpress-theme && npx stylelint 'skyyrose-flagship/assets/css/**/*.css' --formatter unix 2>/dev/null \
| sed 's|^|wordpress-theme/|' | sort) > /tmp/css-attr/after.txt
diff /tmp/css-attr/before.txt /tmp/css-attr/after.txt
```
**PASS:** `diff` prints nothing, or only `>` lines you intended. `[test]`
**Never `git stash`** to make a pristine tree — the stash stack is shared across worktrees and a pop can
destroy another session's work.
**Prove the check can fail (rule 3).** Before trusting check 2, break it once: touch a source `.css`
without rebuilding, re-run `npm run verify:aspect min-sync`, and confirm it reports the file stale;
then `npm run build` to restore green. A gate never observed failing is decoration.
**A gate that dies is not a gate that passed (rule 1).** `verify:aspect` and `stylelint` both exit
non-zero on internal error as well as on findings. If either aborts (missing `node_modules`, killed
process), its output is an *artifact* — re-run it; do not record silence as clean. (bug-230, ×6.)
**A SKIP is not a PASS (rule 2).** Whether the fixed element is *visually* correct at 390px and on
desktop is **not** checked by anything above — `verify-theme.sh --list` classifies `responsive` and
`a11y-interactive` as BROWSER aspects the script does not execute. Name the closer: a Playwright or
Chrome DevTools pass by the caller. Reporting a cascade fix as "done" on CLI evidence alone repeats
the #719 failure in the opposite direction.
## Worked example
**The defect this skill exists for**, read from the real file on 2026-07-28:
```bash
$ grep -n "homepage-v2 a\|hero-cta-primary" wordpress-theme/skyyrose-flagship/assets/css/homepage-v2.css
55:.homepage-v2 a { color: inherit; text-decoration: none; }
377:.hero-cta.hero-cta-primary {
379: .hero-cta-primary (0,1,0) loses that cascade and inherits white-on-white. */
383:.hero-cta-primary:hover {
388:.hero-cta:not(.hero-cta-primary):hover {
```
The two competitors, verbatim:
```css
/* homepage-v2.css:55 — the scoped reset, specificity (0,1,1) */
.homepage-v2 a { color: inherit; text-decoration: none; }
/* homepage-v2.css:377 — the landed fix, specificity (0,2,0) */
.hero-cta.hero-cta-primary {
/* Double-class beats `.homepage-v2 a { color: inherit }` (0,1,1) — a bare
.hero-cta-primary (0,1,0) loses that cascade and inherits white-on-white. */
background: #fff;
color: var(--void, #050505);
}
```
**The math, left to right:** `(0,1,1)` vs `(0,1,0)` → equal ids, equal classes, `1` element beats `0`
element, so the reset won and the original fix lost — a white CTA label on a white background, live,
while the "fix" sat in the served file. Escalating to `.hero-cta.hero-cta-primary` = `(0,2,0)`, which
beats `(0,1,1)` at the class column and never reaches the element column. Note what was *not* used:
no `!important`, and the `var(--void, #050505)` fallback was already there and never helped — a
custom-property fallback protects against a missing token, never against a competing selector.
Then the two gates that make the fix real:
```bash
$ cd wordpress-theme && npm run verify:aspect min-sync
[PASS] min-sync every .min css/js byte-identical to a fresh build
VERIFY PASS — 0 fail, 0 warn.
$ curl -s "https://skyyrose.co/?cb=$(date +%s)" | grep -o 'skyyrose-flagship/style[^"]*ver=[0-9.]*' | head -1
skyyrose-flagship/style.min.css?ver=1.12.8
```
**Honest scope:** those two prove the source and the build agree `[repro]`, and that production serves
theme 1.12.8 `[live]`. They do **not** prove the CTA renders dark-on-white for a real visitor — that is
a pixel claim needing a browser (rule 2 SKIP, closed by the caller's Playwright pass).
## Failure modes
| Symptom | Root cause | Fix |
|---|---|---|
| Declaration is in the served file, browser paints something else | Cascade defeat — a higher-weight or later-source rule wins | Census both selectors, compute weights, escalate structurally (bug #719) |
| Fix works locally, production unchanged | `.min` not rebuilt, or `SKYYROSE_VERSION` not bumped so visitors hold a cached asset | `npm run build`; bump the version triple (`functions.php`, `style.css`, `readme.txt`) |
| `getComputedStyle` returns the pre-animation value | `transition: all` — a synchronous read gets t=0 | `el.style.setProperty('transition','none','important')` first |
| A CSSOM walker skips every rule | `CSSStyleRule.cssRules` is always truthy under the nesting API | Test `r.selectorText && r.style` first; recurse `r.cssRules` separately |
| `!important` fixes it today, nothing can override it tomorrow | Specificity escalated with a hammer | Revert; double-class or scope instead |
| Adding `var(--token, #fallback)` doesn't fix the color | The failure is a cascade loss, not a missing token | Diagnose which failure you have before choosing the fix |
| A new scoped reset silently breaks components inside the section | Reset at (0,1,1) retroactively outranks every (0,1,0) component rule | Census component classes in that section and pre-escalate them before landing the reset |
| Stylelint "went red because of my change" | 452-error pre-existing baseline | Attribute via `git archive`; diff violation lines, not counts |
| curl shows the old CSS right after deploy | Batcache stale copy | Cache-bust: `?cb=$(date +%s)`; never `WebFetch` (strips `<script>`) |
| Grep returns twice the expected hits | `.min.css` siblings matched | Add `\| grep -v '\.min\.'` |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!