CSS is a rule engine: each rule is a selector (which
elements) plus declarations (which properties get which
values). The engine applies every matching rule to every element and
resolves conflicts by specificity and order โ "the cascade". All of
Ndiro's CSS lives in two places: the shared skin in
base.html's <style>, and per-page rules
each template adds via a block.
.card { โฆ } /* any element with class="card" */
.btn.primary { โฆ } /* both classes on the same element */
.menu-panel a { โฆ } /* any <a> anywhere inside .menu-panel */
.btn:hover { โฆ } /* while the pointer is over it */
.btn:disabled { โฆ } /* state-based */
:root[data-theme="light"] { โฆ } /* attribute match โ the theme switch */
When two rules set the same property on the same element, the more specific selector wins (id beats class beats element), and among equals, the later one in the file. You can see the whole resolution in devtools: the Styles pane lists every matching rule with the losers struck through โ it's the query plan explainer for CSS.
Every element is a rectangle: content, wrapped in
padding, wrapped in border, wrapped in
margin. Ndiro opts the whole page into the sane sizing mode
where width means the visible box, border and padding
included:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
For arranging boxes, this site needs only two tools:
.container { max-width: 640px;
margin: 0 auto; } (auto side margins = centering).display: flex..form-row {
display: flex; /* children become flex items in a row */
gap: 10px; /* space between them */
flex-wrap: wrap; /* overflow wraps instead of squishing */
}
.form-row > div {
flex: 1; /* share leftover width equally */
min-width: 130px; /* below this, wrap to the next line */
}
Those five lines are also the entire "responsive design" of the form: on a phone the three fields wrap into a column, on a laptop they sit in a row, with no phone-specific code. Add the viewport meta from the HTML chapter and a fluid max-width container, and small screens mostly take care of themselves.
The third tool, positioning, is used sparingly:
position: fixed pins the menu hamburger to the viewport
(.corner-controls) and stretches the photo lightbox over
everything (inset: 0 plus a z-index to win the
stacking order).
The gem of this stylesheet. CSS variables ("custom properties") are defined once on the root element and referenced everywhere:
templates/base.html โ:root { /* dark theme โ the default */
--bg-0: #282828; --card: #3c3836; --border: #504945;
--muted: #a89984; --text: #ebdbb2; --gold: #fabd2f;
--green: #b8bb26; --red: #fb4934; /* โฆ14 tokens total */
}
:root[data-theme="light"] { /* same 14 names, light values */
--bg-0: #fbf1c7; --text: #3c3836; --gold: #b57614; โฆ
}
.card {
background: var(--card);
border: 1px solid var(--border);
}
Because colors in the app are spelled var(--token),
the entire theme switch is one attribute flip on
<html>: set data-theme="light" and the
second :root block wins, every
var() re-resolves, and the whole page restyles. No CSS is
swapped, no classes are toggled per element. (There is one deliberate
exception that proves the rule: .btn.primary hard-codes
its dark text color, because dark-on-gold is the right contrast in
both themes โ a color that shouldn't follow the theme is
written literally, so the token system stays honest.)
One wrinkle worth knowing by name: FOUC (flash of
unstyled/wrong content). The theme choice lives in
localStorage, which only JavaScript can read โ so a tiny
script runs in <head>, before the first
paint, to set the attribute. Do it at the bottom of the page
instead and dark-theme users see a cream flash on every load. The
try/catch handles private browsing, where
localStorage may throw.
Two kinds, both present in miniature. Transitions animate a property
change: .brand { transition: border-color .2s, transform .2s
} makes the hover lift smooth. Keyframe animations run on their
own โ the loading indicator is a broccoli spinning forever:
.spin { display: inline-block; animation: spin 1.1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
The token block is a config layer, exactly like a well-factored
*-site.xml: code references names, one file binds
names to values, and swapping the binding reconfigures everything.
The review smell is identical too โ a hard-coded
#fabd2f in a template is the same defect as a
hard-coded hostname in a job: it works until the environment (here,
the theme) changes underneath it.
Open devtools โ Elements, select the <html>
element, and add the attribute data-theme="light" by
hand โ the site restyles instantly. Then find
:root in the Styles pane and edit
--gold to hotpink; watch every heading,
button and chart accent change at once. That's the token system
proving itself.