From 308fa4eb26705cc4a029f699eecd115f5698c727 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Mon, 18 May 2026 00:33:47 +0200 Subject: [PATCH] docs(stylesheet): redesign from first principles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Throw away the historical naming. New vocabulary chosen for clarity and agentic-dev predictability: parts use hyphenated child classes (.card-header), variant modifiers chain on the parent (.button.primary), state stays on ARIA attributes. Variants compose via Tier-3 component-scoped tokens (--button-bg etc.) — .button.danger.outline is a real outlined-danger button with no combination rule. Adds toast, spinner, heading, app-header as first-class components. Renames panel→card, modal→dialog, badge→tag; collapses state-* into tag variants via ui.lifecycle_tag. Adds an explicit template-rewrite phase in the migration plan, since every template's class attributes change. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-05-17-stylesheet-redesign.md | 1741 +++++++++++------ .../2026-05-17-stylesheet-redesign-design.md | 283 ++- 2 files changed, 1332 insertions(+), 692 deletions(-) diff --git a/docs/superpowers/plans/2026-05-17-stylesheet-redesign.md b/docs/superpowers/plans/2026-05-17-stylesheet-redesign.md index 924ace5..2ec73e8 100644 --- a/docs/superpowers/plans/2026-05-17-stylesheet-redesign.md +++ b/docs/superpowers/plans/2026-05-17-stylesheet-redesign.md @@ -2,11 +2,11 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Replace `l4d2web/static/css/*` (~1,436 LOC, 196 component classes) with a tiered design system: two-tier tokens, `@layer`-ordered cascade, budgeted component classes, five high-leverage macros, an in-app style guide, and the "system is closed" workflow rule enforced via `AGENTS.md`. +**Goal:** Replace the current ~1.4k LOC stylesheet (`l4d2web/static/css/*` + 196 component classes) with a from-scratch design system. New naming, new vocabulary, new structure — no preservation of historical names. Every template's class attributes get rewritten. -**Architecture:** Pure custom CSS (no framework base). Single entry stylesheet `main.css` declares `@layer reset, tokens, elements, layout, components, widgets, utilities;` and `@import`s the rest. Tokens split into `primitives.css` (raw palette) + `semantic.css` (role aliases + dark-mode branch). Component classes are CSS-only by default; macros under `templates/ui/` exist only for five composites where markup correctness is load-bearing (`field`, `modal`, `tabs`, `confirm_form`, `badge_state`). A new public route `/styleguide` is the canonical reference contributors and agents read before producing new UI. +**Architecture:** Pure custom CSS, single entry `main.css`, `@layer reset, tokens, elements, layout, components, widgets, utilities;`. Three-tier tokens (primitives → semantic → component-scoped). Naming convention: **parts use hyphenated child classes** (`.card-header`), **variant modifiers chain on the parent** (`.button.primary`). State on ARIA attributes (`[disabled]`, `[aria-busy]`, `[aria-selected]`, `[aria-invalid]`). Five macros under `templates/ui/` for high-leverage composites. Public `/styleguide` is the canonical reference. -**Tech Stack:** Plain CSS using modern features (`@layer`, `color-mix()`, custom properties). Jinja macros for the five composites. Flask blueprint for `/styleguide`. No Sass, no PostCSS, no bundler. +**Tech Stack:** Plain CSS (`@layer`, `color-mix()`, custom properties). Jinja macros. Flask blueprint. No Sass, no PostCSS, no bundler. **Spec:** `docs/superpowers/specs/2026-05-17-stylesheet-redesign-design.md` @@ -18,61 +18,136 @@ |---|---|---| | Create | `l4d2web/static/css/main.css` | Entry. Declares `@layer` order + `@import`s | | Create | `l4d2web/static/css/reset.css` | Modern reset (~30 LOC) | -| Create | `l4d2web/static/css/tokens/primitives.css` | Raw palette: grays, blue, red/green/amber, spacing, type scale, radii, shadows, motion, fonts | -| Create | `l4d2web/static/css/tokens/semantic.css` | Aliases: `--color-*`, `--space-*`, plus `[data-theme="dark"]` branch | -| Create | `l4d2web/static/css/elements.css` | Bare HTML defaults: `body`, `h1-h6`, `a`, `code`, `kbd`, `pre`, `hr`, form elements, tables | -| Rewrite | `l4d2web/static/css/layout.css` | `.container`, `.stack`, `.stack-h`, section spacing | -| Create | `l4d2web/static/css/components/button.css` | `.btn` + variants/sizes + states | -| Create | `l4d2web/static/css/components/field.css` | `.field` + label / hint / error | -| Create | `l4d2web/static/css/components/table.css` | `.table` | -| Create | `l4d2web/static/css/components/panel.css` | `.panel` + heading / body / footer | -| Create | `l4d2web/static/css/components/modal.css` | `.modal` (styles ``) + header / body / footer / close / `.modal-wide` | -| Create | `l4d2web/static/css/components/tabs.css` | `.tabs` + `.tab[aria-selected]` + `.tab-panel` | -| Create | `l4d2web/static/css/components/badge.css` | `.badge` + semantic + `.state-*` | -| Create | `l4d2web/static/css/components/nav.css` | `.site-header`, `.primary-nav`, `.account-nav`, `.brand` | -| Create | `l4d2web/static/css/components/dropdown.css` | `` helper + `.dropdown` | +| Create | `l4d2web/static/css/widgets/file-tree.css` | `.file-tree`, `.file-tree-item`, `.file-tree-toggle`, `.file-tree-children` | +| Create | `l4d2web/static/css/widgets/overlay-list.css` | `.overlay-list`, `.overlay-list-item`, `.overlay-list-handle`, `.overlay-list-meta` | +| Create | `l4d2web/static/css/widgets/console.css` | `.console`, `.console-line`, `.console-line.cmd`, `.console-line.out`, `.console-input` | +| Create | `l4d2web/static/css/widgets/editor.css` | `.editor` (CodeMirror wrapper) + relocated `--cm-*` tokens | +| Create | `l4d2web/static/css/widgets/logs.css` | Log-viewer styling, retargeted to new semantic tokens | +| Create | `l4d2web/static/css/widgets/server-status.css` | `.server-status`, `.server-status-state`, `.server-status-actions`, `.server-status-meta` | +| Create | `l4d2web/static/css/widgets/player-list.css` | `.player-list`, `.player-card`, `.player-card-avatar`, `.player-card-name`, `.player-card-meta` | +| Create | `l4d2web/static/css/utilities.css` | `.muted`, `.mono`, `.truncate`, `.visually-hidden` | | Create | `l4d2web/templates/ui/_field.html` | `ui.field`, `ui.checkbox`, `ui.select` | -| Create | `l4d2web/templates/ui/_modal.html` | `ui.modal` | +| Create | `l4d2web/templates/ui/_dialog.html` | `ui.dialog` | | Create | `l4d2web/templates/ui/_tabs.html` | `ui.tabs`, `ui.tab_panel` | | Create | `l4d2web/templates/ui/_confirm_form.html` | `ui.confirm_form` | -| Create | `l4d2web/templates/ui/_badge.html` | `ui.badge_state` | -| Modify | `l4d2web/templates/base.html` | Swap 5 `` tags for 1; add inline theme-init script | -| Create | `l4d2web/templates/styleguide.html` | Style guide page with every primitive + do/don't blocks | +| Create | `l4d2web/templates/ui/_tag.html` | `ui.tag`, `ui.lifecycle_tag` | +| Modify | `l4d2web/templates/base.html` | Swap 5 `` tags for 1; rewrite header markup; add inline theme-init script | +| Create | `l4d2web/templates/styleguide.html` | Style guide page + ✅/❌ blocks | | Create | `l4d2web/routes/styleguide_routes.py` | Public route `/styleguide` | -| Modify | `l4d2web/l4d2web/app.py` | Register styleguide blueprint; remove spike registration (commit 7) | -| Modify | `AGENTS.md` | Add "UI work" section codifying the workflow rule | -| Delete | `l4d2web/static/css/components.css` | (commit 7) | -| Delete | `l4d2web/static/css/tokens.css` | (commit 7) | -| Delete | `l4d2web/static/css/logs.css` (root) | (commit 7) — moved to `widgets/` | -| Delete | `l4d2web/static/css/console-autocomplete.css` (root) | (commit 7) — moved to `widgets/` | -| Delete | `l4d2web/static/css/editor.css` (root) | (commit 7) — moved to `widgets/` | -| Delete | `l4d2web/static/css/spike/` | (commit 7) — scaffolding | -| Delete | `l4d2web/templates/spike.html` | (commit 7) | -| Delete | `l4d2web/routes/spike_routes.py` | (commit 7) | -| Delete | `l4d2web/static/vendor/css/` | (commit 7) — only used by spike | +| Modify | `l4d2web/l4d2web/app.py` | Register styleguide blueprint; remove spike registration (Task 11) | +| Modify | `AGENTS.md` | Add "UI work" section codifying naming + workflow | +| Rewrite | All ~25 templates in `l4d2web/templates/` | Class attributes use new vocabulary; macros where applicable | +| Delete | `l4d2web/static/css/components.css` | (Task 11) | +| Delete | `l4d2web/static/css/tokens.css` | (Task 11) | +| Delete | `l4d2web/static/css/logs.css` (root) | (Task 11) — replaced by `widgets/logs.css` | +| Delete | `l4d2web/static/css/console-autocomplete.css` (root) | (Task 11) — folded into `widgets/console.css` | +| Delete | `l4d2web/static/css/editor.css` (root) | (Task 11) — replaced by `widgets/editor.css` | +| Delete | `l4d2web/static/css/spike/` | (Task 11) — scaffolding | +| Delete | `l4d2web/templates/spike.html` | (Task 11) | +| Delete | `l4d2web/routes/spike_routes.py` | (Task 11) | +| Delete | `l4d2web/static/vendor/css/` | (Task 11) — only used by spike | -## Reference: spike artifacts +## Token migration (used in Task 6 when relocating widget CSS) -The pre-validated CSS lives in **`l4d2web/static/css/spike/custom.css`**. It is organized exactly by the `@layer` blocks the production stylesheet uses. Each task below extracts a layer (or part of a layer) into its production file. +The old `tokens.css` and the new `tokens/semantic.css` overlap on color names but differ on spacing, radii, and a few colors. Apply this table when copying any rule out of the old `components.css` or root-level widget files: -Read the spike file as needed: +| Old | New | Notes | +|---|---|---| +| `--color-bg`, `--color-text`, `--color-muted`, `--color-border`, `--color-link`, `--color-primary`, `--color-danger`, `--color-warning`, `--color-success`, `--color-focus`, `--color-surface` | (same) | Unchanged. | +| `--color-surface-muted` | `--color-surface-2` | | +| `--color-border-muted` | `--color-border-soft` | | +| `--color-button-primary` | `--color-primary` | | +| `--color-button-danger` | `--color-danger` | | +| `--color-log-bg` | `--color-surface-2` | | +| `--color-log-text` | `--color-text` | | +| `--space-xs` | `--space-1` | 0.25rem | +| `--space-s` | `--space-2` | 0.5rem | +| `--space-m` | `--space-3` | 0.75rem | +| `--space-l` | `--space-4` | 1rem | +| `--space-xl` | `--space-5` | 1.5rem | +| `--space-2xl` | `--space-6` | 2rem | +| `--radius-base`, `--radius-s` | `--radius-1` | 0.25rem | +| `--radius-m` | `--radius-2` | 0.5rem | +| `--line` | `1px solid var(--color-border)` | Inline-expanded — `--line` is not in the new system | +| `--line-soft` | `1px solid var(--color-border-soft)` | Inline-expanded | +| `--font-mono` | (same) | Unchanged. | -```bash -cat l4d2web/l4d2web/static/css/spike/custom.css | sed -n '/^@layer reset/,/^}/p' -``` +CodeMirror tokens (`--cm-*`, `--syntax-*`, `--editor-rows`) move with the editor widget — see Task 6, Step 4b. -Or open it in an editor and copy the `@layer X { … }` blocks one at a time. +## Class-name migration (used in Task 10 for templates) -The spike file's `@layer` declarations and `@layer X { … }` wrappers stay in the file when copied into the production files; the `@layer` wrapper around each block is what gives the production cascade its ordering. +The new vocabulary is a full rename. The mapping is dense; agents executing Task 10 should treat this as a find-replace table. Old names on the left, new on the right; modifier-style chains are space-separated multi-class additions. -The spike template `l4d2web/templates/spike.html` is the reference for the style-guide page's markup vocabulary (every widget appears there at least once). +| Old class | New class | Notes | +|---|---|---| +| `.btn` | `.button` | All variants follow | +| `.btn-primary` | `.button primary` | Chained modifier — two classes | +| `.btn-secondary` | `.button` | Default (no modifier) is the secondary look | +| `.btn-danger` | `.button danger` | | +| `.btn-outline` | `.button outline` | | +| `.btn-link` | `.button link` | (also subsumes `.link-button` below) | +| `.btn-sm` | `.button small` | | +| `.button-secondary` (old short name) | `.button` | | +| `.danger-outline` | `.button danger outline` | Composes from existing axes | +| `.link-button` | `.button link` | | +| `.button-row` | `.row` | Generic horizontal flex (in layout) | +| `.panel`, `.card` | `.card` | One vocabulary | +| `.panel-heading`, `.card-heading` | `.card-header` | Match HTML semantics | +| `.panel-body` | `.card-body` | | +| `.panel-footer` | `.card-footer` | | +| `.modal` | `.dialog` | Matches `` element | +| `.modal-wide` | `.dialog wide` | Chained modifier | +| `.modal-header` | `.dialog-header` | | +| `.modal-body` | `.dialog-body` | | +| `.modal-footer` | `.dialog-footer` | | +| `.modal-close` | `.dialog-close` | | +| `.badge` | `.tag` | | +| `.badge-success` | `.tag success` | Chained | +| `.badge-warning` | `.tag warning` | | +| `.badge-danger` | `.tag danger` | | +| `.badge-muted` | `.tag muted` | | +| `.state-running` | `.tag success` | Use `ui.lifecycle_tag(state)` instead of hand-picking | +| `.state-stopped` | `.tag muted` | | +| `.state-unknown` | `.tag muted` | | +| `.state-transient` | `.tag warning` | | +| `.state-drift` | `.tag danger` | | +| `.site-header` | `.app-header` | | +| `.site-header-inner` | `.app-header-inner` | | +| `.primary-nav` | `.nav` | | +| `.account-nav` | `.account` | | +| `.brand` | (same) | | +| `.page-heading` | `.heading` | | +| `.page-footer-actions`, `.form-actions-inline`, `.button-row` | `.row` (or `.cluster` for wrap-friendly) | | +| `.sr-only` | `.visually-hidden` | | +| `.overlay-picker` | `.overlay-list` | | +| `.overlay-picker-list` | `.overlay-list` direct child `
    ` styled by selector | | +| `.overlay-picker-row` | `.overlay-list-item` | | +| `.overlay-picker-handle` | `.overlay-list-handle` | | +| `.overlay-picker-name`, `.overlay-picker-expose` | `.overlay-list-meta` (consolidated) | | +| `.overlay-picker-remove`, `.overlay-picker-add`, `.overlay-picker-empty` | `.overlay-list-remove` / `-add` / `-empty` | | +| `.file-tree-row` | `.file-tree-item` | | +| `.file-tree-row-file` | `.file-tree-item file` | Chained modifier | +| `.field-input` (rare) | drop the class; style via `.field > input` selector | | + +`★ Insight ─────────────────────────────────────` +Search-and-replace gotcha: the chained-modifier form means a single attribute changes from `class="btn btn-primary"` to `class="button primary"`. Both are two-class lists, but the *order matters* for the inheritance chain (component class first, modifiers after). Templates need attention to ordering when the rename is mechanical. +`─────────────────────────────────────────────────` --- @@ -86,7 +161,7 @@ The spike template `l4d2web/templates/spike.html` is the reference for the style Nothing else changes; `base.html` still loads the old CSS. The new files exist but are not yet referenced. -- [ ] **Step 1: Create `static/css/tokens/` directory** +- [ ] **Step 1: Create the tokens/ directory** ```bash mkdir -p l4d2web/l4d2web/static/css/tokens @@ -97,9 +172,9 @@ mkdir -p l4d2web/l4d2web/static/css/tokens `l4d2web/l4d2web/static/css/main.css`: ```css -/* Entry stylesheet. Declares the @layer order, then @imports each layer's - file(s). Cascade specificity is determined by the @layer order — never - by selector specificity or file order. */ +/* Entry stylesheet. Declares @layer order, then @imports each layer's + file(s). Cascade specificity is determined by layer order, not by + selector specificity or file order. */ @layer reset, tokens, elements, layout, components, widgets, utilities; @@ -112,19 +187,23 @@ mkdir -p l4d2web/l4d2web/static/css/tokens @import url("./components/button.css") layer(components); @import url("./components/field.css") layer(components); @import url("./components/table.css") layer(components); -@import url("./components/panel.css") layer(components); -@import url("./components/modal.css") layer(components); +@import url("./components/card.css") layer(components); +@import url("./components/dialog.css") layer(components); @import url("./components/tabs.css") layer(components); -@import url("./components/badge.css") layer(components); -@import url("./components/nav.css") layer(components); +@import url("./components/tag.css") layer(components); +@import url("./components/toast.css") layer(components); +@import url("./components/spinner.css") layer(components); +@import url("./components/app-header.css") layer(components); +@import url("./components/heading.css") layer(components); @import url("./components/dropdown.css") layer(components); @import url("./widgets/file-tree.css") layer(widgets); -@import url("./widgets/overlay-picker.css") layer(widgets); -@import url("./widgets/console-autocomplete.css") layer(widgets); +@import url("./widgets/overlay-list.css") layer(widgets); +@import url("./widgets/console.css") layer(widgets); @import url("./widgets/editor.css") layer(widgets); @import url("./widgets/logs.css") layer(widgets); -@import url("./widgets/live-state.css") layer(widgets); +@import url("./widgets/server-status.css") layer(widgets); +@import url("./widgets/player-list.css") layer(widgets); @import url("./utilities.css") layer(utilities); ``` @@ -134,10 +213,6 @@ mkdir -p l4d2web/l4d2web/static/css/tokens `l4d2web/l4d2web/static/css/reset.css`: ```css -/* Modern reset. Mirrors the Andy-Bell / Josh-Comeau style. Intentional - choices: keep button looking like a button (no all:unset), preserve - focus rings via :focus-visible later in elements.css. */ - *, *::before, *::after { box-sizing: border-box; } body, h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin: 0; } ul[role="list"], ol[role="list"] { list-style: none; padding: 0; } @@ -152,57 +227,45 @@ h1, h2, h3, h4 { text-wrap: balance; } - [ ] **Step 4: Write `tokens/primitives.css`** -Extract from `static/css/spike/custom.css` — the `:root { … }` block inside `@layer tokens` that contains the primitive palette (lines beginning `--gray-50:`, `--blue-200:`, `--red-200:`, etc., through the font stacks). The content is: - `l4d2web/l4d2web/static/css/tokens/primitives.css`: ```css -/* Tier 1 — primitives. Raw palette. No semantic meaning. Consumed only - by tokens/semantic.css, never by component CSS directly. */ +/* Tier 1 — primitives. Raw palette. Consumed only by tokens/semantic.css, + never by component CSS directly. */ :root { - /* grayscale */ --gray-50: #fafafa; --gray-100: #f4f4f5; --gray-200: #e4e4e7; --gray-300: #d4d4d8; --gray-400: #a1a1aa; --gray-500: #71717a; --gray-600: #52525b; --gray-700: #3f3f46; --gray-800: #27272a; --gray-900: #18181b; --gray-950: #09090b; - /* brand blue */ --blue-200: #bfdbfe; --blue-300: #93c5fd; --blue-500: #3b82f6; --blue-600: #2563eb; --blue-700: #1d4ed8; --blue-800: #1e40af; - /* state colors */ --red-200: #fecaca; --red-400: #fca5a5; --red-700: #b42318; --green-300: #86efac; --green-700: #067647; --amber-300: #fcd34d; --amber-700: #a15c07; - /* spacing */ --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; --space-4: 1rem; --space-5: 1.5rem; --space-6: 2rem; --space-7: 3rem; - /* radii */ --radius-1: 0.25rem; --radius-2: 0.5rem; --radius-3: 0.75rem; --radius-full: 9999px; - /* type scale */ --text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem; --text-lg: 1.125rem; --text-xl: 1.25rem; --text-2xl: 1.5rem; - /* line heights */ --leading-tight: 1.2; --leading-normal: 1.5; - /* shadows */ --shadow-sm: 0 1px 2px rgba(0,0,0,0.08); --shadow-md: 0 4px 12px rgba(0,0,0,0.10); --shadow-lg: 0 16px 32px rgba(0,0,0,0.18); - /* motion */ --duration-fast: 120ms; --ease-out: cubic-bezier(0.22, 1, 0.36, 1); - /* fonts */ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } @@ -213,10 +276,8 @@ Extract from `static/css/spike/custom.css` — the `:root { … }` block inside `l4d2web/l4d2web/static/css/tokens/semantic.css`: ```css -/* Tier 2 — semantic tokens. Aliases for use cases. Components consume - ONLY these (var(--color-*), var(--space-*), etc.) — never primitives - directly. Dark mode lives here: [data-theme="dark"] redefines the - semantic aliases to point at different primitives. */ +/* Tier 2 — semantic tokens. Components reference only these. Dark mode + re-points the aliases in the [data-theme="dark"] block. */ :root, :root[data-theme="light"] { @@ -234,6 +295,8 @@ Extract from `static/css/spike/custom.css` — the `:root { … }` block inside --color-primary-hover: var(--blue-800); --color-on-primary: #ffffff; + --color-info: var(--blue-500); + --color-danger: var(--red-700); --color-on-danger: #ffffff; --color-warning: var(--amber-700); @@ -260,6 +323,8 @@ Extract from `static/css/spike/custom.css` — the `:root { … }` block inside --color-primary-hover: var(--blue-200); --color-on-primary: var(--gray-950); + --color-info: var(--blue-300); + --color-danger: var(--red-400); --color-on-danger: var(--gray-950); --color-warning: var(--amber-300); @@ -270,7 +335,7 @@ Extract from `static/css/spike/custom.css` — the `:root { … }` block inside } ``` -- [ ] **Step 6: Verify the files exist and are syntactically valid CSS** +- [ ] **Step 6: Verify the files exist** ```bash test -s l4d2web/l4d2web/static/css/main.css && \ @@ -280,9 +345,7 @@ test -s l4d2web/l4d2web/static/css/tokens/semantic.css && \ echo OK ``` -Expected: `OK` - -(There is no CSS linter in this project. Syntactic validity is verified visually in Task 2 when `main.css` is loaded by the browser and DevTools would report parse errors.) +Expected: `OK`. - [ ] **Step 7: Commit** @@ -290,7 +353,7 @@ Expected: `OK` git add l4d2web/l4d2web/static/css/main.css \ l4d2web/l4d2web/static/css/reset.css \ l4d2web/l4d2web/static/css/tokens/ -git commit -m "feat(stylesheet): foundation — main.css, reset, two-tier tokens" +git commit -m "feat(stylesheet): foundation — main.css, reset, three-tier tokens" ``` --- @@ -300,18 +363,15 @@ git commit -m "feat(stylesheet): foundation — main.css, reset, two-tier tokens **Files:** - Create: `l4d2web/static/css/elements.css` - Rewrite: `l4d2web/static/css/layout.css` -- Modify: `l4d2web/templates/base.html` — collapse 5 `` tags into 1, add inline theme-init script +- Modify: `l4d2web/templates/base.html` — collapse 5 `` tags into 1, rewrite header markup to new vocabulary (the `.app-header` block), add inline theme-init script -After this commit the site loads the new system. Pages will look bare in places where component classes aren't yet styled. That is intentional and visible. +After this commit the site loads the new system. Existing pages still use OLD class names in their attributes, so most things look bare until Task 10 rewrites templates. The `base.html` header is rewritten in this task so the top of every page renders correctly from here on. - [ ] **Step 1: Write `elements.css`** `l4d2web/l4d2web/static/css/elements.css`: ```css -/* Bare HTML defaults. Consumes semantic tokens only. Anything that styles - a tag selector lives here; class selectors live in components/. */ - html, body { font-family: var(--font-sans); font-size: var(--text-base); @@ -368,11 +428,13 @@ th { background: var(--color-surface-2); font-weight: 600; font-size: var(--text Overwrite `l4d2web/l4d2web/static/css/layout.css` with: ```css -/* Composition objects. Don't paint, only arrange. */ - .container { max-width: 72rem; margin-inline: auto; padding-inline: var(--space-4); } -.stack > * + * { margin-top: var(--space-3); } -.stack-h { display: flex; gap: var(--space-3); align-items: center; } + +.stack { display: flex; flex-direction: column; gap: var(--space-3); } +.stack > * + * { margin-top: 0; } /* flex gap supersedes ad-hoc margins */ + +.row { display: flex; gap: var(--space-2); align-items: center; } +.cluster { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; } main > section { margin-block: var(--space-6); } main > section > h2:first-child { @@ -384,11 +446,11 @@ main > section > h2:first-child { - [ ] **Step 3: Update `base.html`** -In `l4d2web/l4d2web/templates/base.html`, replace the five separate `` tags with a single `` for `main.css`, and add an inline theme-init script that reads `prefers-color-scheme` before paint (uses the existing `g.csp_nonce` for CSP compliance). +Replace the entire `` and `` of `l4d2web/l4d2web/templates/base.html` to use the new vocabulary. Read the current file first to preserve the script-include block and the modal-container `` at the bottom. Edit `l4d2web/l4d2web/templates/base.html`: -Old `` block (the five stylesheet links): +Old `` block (the five stylesheet links + meta tags): ```html @@ -398,12 +460,11 @@ Old `` block (the five stylesheet links): ``` -New `` block: +New `` block — replace the above with: ```html