๐ŸฅฆNdiro
Chapter 5 of 13

CSS: style, layout, theming

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.

Selectors you'll actually meet here

.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.

The box model and the layout primitives

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:

templates/base.html โ†—
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

For arranging boxes, this site needs only two tools:

templates/log.html โ†—
.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).

Design tokens: custom properties

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.

Motion

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:

templates/base.html โ†—
.spin { display: inline-block; animation: spin 1.1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
๐Ÿ˜ From your world

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.

๐Ÿ”ฌ Try 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.

๐Ÿ