Compute Which Rule Wins
Goal
CSS looks unpredictable not because there are no rules but because the rules have three stages. Importance → specificity → order.
Since there is no browser, it does not grade "does it look right." Instead it actually computes which rule wins — and that has a single answer even without a browser.
What to build
It is a single file, /root/work/css/styles.css. No HTML is needed.
The grader resolves the cascade assuming the elements below.
div#app > div.card
main.lessonLayout > aside.lessonSide
main.lessonLayout.sideHidden > aside.lessonSide
div.toolbar
The calculator
cd /root/work/css
python3 /opt/lab/checks/css-lab/cascade.py styles.css
For each rule it shows the order, the media condition, and the specificity. The scope it handles is
tags, classes, IDs, pseudo-classes, descendant/child combinators, comma groups, and
@media (max-width|min-width: Npx). If it meets any other selector, it does not
silently skip it but says "cannot interpret."
Steps
- 3
:roottokens +var()in 3 places - Predict the specificity →
02-spec.txt - Win without
!important - The order trap (put the base rule above the media query)
- A 3-column grid
- Collapse to 1 column
- A flex toolbar
- The calculator →
08-cascade.txt - Wrap-up →
09-notes.md
Notes
Steps 4 and 6 are bugs that actually happened in this service. The mobile table-of-contents overlay that wouldn't open, and the page growing to 33,000px when the table of contents was collapsed.
Put the tokens in :root
Create /root/work/css/styles.css, define at least three custom properties in :root, and then use them with var() in at least three places.
Run mkdir -p /root/work/css. The names are up to you, such as --accent, --gap, --fg. Unlike SCSS variables, custom properties are alive at runtime and are inherited — so you can override them only in a subtree.
Calculate the specificity
Write the specificity of the five selectors below in 02-spec.txt, one per line, in the format 번호=a,b,c (the placeholder is the selector number).
1) a
2) .btn.primary
3) #nav a:hover
4) ul > li::marker
5) :where(.card) .title
a = IDs, b = classes, attributes, and pseudo-classes, c = tags and pseudo-elements. Example: 1=0,0,1. Number 5 is the trap — :where() always counts as 0. Also remember that places don't carry over into each other: even 11 classes can't beat a single ID.
Win without !important
Write .card { color: ... } first, then make only the .card inside #app a different color. You must not use !important.
The grader actually computes which rule wins for the div#app > div.card element. #app .card is 1,1,0, so it beats .card (0,1,0). !important can only be beaten by another !important, so once you use it, you have to handle that property that way forever.
Avoid the order trap
Make the position of .lessonSide sticky by default and fixed at max-width: 900px. The two rules have the same specificity, so order decides the answer.
Put the base rule above the @media block. If you put it below, the specificity is the same (both 0,1,0), so the base rule written later wins on mobile too. The grader computes at two widths, 1280px and 500px. The bug in this service where the mobile overlay wouldn't open was exactly this.
The skeleton as a grid
Make .lessonLayout display: grid and give it a three-column grid-template-columns and a gap.
Example: grid-template-columns: 240px 1fr 200px. Use grid when you want to decide both rows and columns. If you are only dividing within a single row, flex is the right choice.
When it collapses, the column must go too
When it is .lessonLayout.sideHidden, make it one column. You must not leave a 0-width column such as 0 1fr 200px.
If a child is display: none, it drops out of the grid entirely. If you leave the column at 0, the body gets pushed into that 0px column and the line breaks after every character, and the page really did become 33,000px tall. Reduce the number of columns — grid-template-columns: 1fr.
A toolbar with flex
Make .toolbar display: flex and specify all of justify-content, align-items, and gap.
The most common combination is space-between + center + gap. If you want to push just one item to the right, you can also give that element margin-left: auto.
Check with the calculator
Resolve your stylesheet with /opt/lab/checks/css-lab/cascade.py and save the result as 08-cascade.txt.
python3 /opt/lab/checks/css-lab/cascade.py styles.css | tee 08-cascade.txt. It prints the order, media condition, and specificity for each rule. This is the calculation you do in your head when you look at a struck-through rule in the developer tools.
Explain why it lost
In 09-notes.md, write at least three lines: what happens if you flip the order in step 4, what happens if you leave a 0-width column in step 6, and what to look at first when you feel like using !important.
The text must contain 특정성, 순서, and 그리드 (the Korean words for "specificity", "order", and "grid"). The diagnosis is always four steps — does the selector match, is it losing, is it specificity or order, and are you expecting inheritance.