Skip to main content

max / makenotwork

v0.6.9: UX audit charter decisions + sr-only file inputs Resolves the three charter decisions left from the Phase 1-8 UX audit and lands the file-input accessibility upgrade. - Buttons: migrate 67 sites from class="primary|secondary|danger" to class="btn-*"; drop button.shorthand selectors from style.css (kept .btn-* only). Form works on both <button> and <a>. - Headings: add .brand-h1 / .page-title / .subtitle-h2 / .subsection-title; sweep 93 templates so no bare <h1>/<h2> remains outside the JS markdown renderer in item_text_editor.html. - BEM separator: rename 76 distinct block__elem tokens across 15 files to single-dash kebab-case; no collisions. Modifier -- preserved. - File inputs: 13 <input type="file"> sites swapped from .hidden / .wizard-file-input to .sr-only so screen readers and keyboard reach the input; trigger buttons unchanged. .wizard-file-input rule removed from wizard.css. - Charter: docs/design-system.md gains rules 10-12 codifying the heading, button, and kebab-case conventions; primitive table updated. - Todo log: UX audit remediations section refreshed; status updated. - Includes UX-audit phase 1-8 reports (untracked until now). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-21 01:17 UTC
Commit: 7eff0b4fe96a96610754d89ccca6bd7f335168f0
Parent: 67e74d4
161 files changed, +1624 insertions, -1160 deletions
@@ -3551,7 +3551,7 @@ dependencies = [
3551 3551
3552 3552 [[package]]
3553 3553 name = "makenotwork"
3554 - version = "0.6.6"
3554 + version = "0.6.9"
3555 3555 dependencies = [
3556 3556 "anyhow",
3557 3557 "argon2",
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.6.6"
3 + version = "0.6.9"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -39,7 +39,7 @@ For each primitive, exactly one canonical class **or** one canonical partial. Va
39 39 | Container | `.container` | `--narrow` (600px), `--medium` (800px), `--wide` (900px) | Default `max-width: 1200px` |
40 40 | Toolbar / header bar | `.stack-row` | `--bordered`, `--tight`, `--top` | Title-left + actions-right |
41 41 | Inline form row | `.field-row` | child `.form-group.is-grow` / `.is-grow-2` | Input(s) + button bottom-aligned |
42 - | Vertical list row | `.list-row` | parts: `__title`, `__meta` | Flex with bottom border |
42 + | Vertical list row | `.list-row` | parts: `-title`, `-meta` | Flex with bottom border |
43 43 | Two-col form grid | `.form-row` | (none) | `grid-template-columns: 1fr 1fr` |
44 44 | Cover-image row | `.cover-row` + `.cover-thumb` + `.cover-empty` | (none) | 120×120 thumbnail picker |
45 45
@@ -49,10 +49,10 @@ For each primitive, exactly one canonical class **or** one canonical partial. Va
49 49 |---|---|---|---|
50 50 | Page section box | `.content-section` | (none) | — |
51 51 | Card | `.card` | `.card-muted` (surface-muted fill), `.card--bordered`, `.card--selectable` | `:hover`, `:focus-within`, `.is-selected` |
52 - | Button | `button.primary` / `.secondary` / `.danger` | `.small`, `.btn-compact`, `.btn-link`, `.saved` | `:hover`, `:focus-visible`, `:disabled`, `.htmx-request` |
52 + | Button | `.btn-primary` / `.btn-secondary` / `.btn-danger` | `.btn--large`, `.btn--icon`, `.btn--link`, `.small`, `.saved` | `:hover`, `:focus-visible`, `:disabled`, `.htmx-request` |
53 + | Heading | `.brand-h1` (wordmark), `.page-title` (h1), `.subtitle-h2` (auth/wizard h2), `.subsection-title` (default h2), `.section-header` (h2 with bottom border) | — | — |
53 54 | Input shape | (base `input`/`textarea`/`select`) | `.input--xs`, `.input--sm`, `.input--mono`, `.input--upper`, `.input--numeric` | `:focus`, `:disabled` |
54 55 | Form field | `.form-group` + `.hint` | `.form-group--error` + `.field-error` | (via `_ui.html` macro `form_field`) |
55 - | Section header | `.section-header` (h2 with bottom-border) | — | — |
56 56 | Section lead | `.section-lead` | with `.mb-3` / `.text-sm` / `.dimmed` utilities | — |
57 57 | Section divider | `.section-divider` | — | — |
58 58 | Section grouping label | `.section-group-label` | — | — |
@@ -67,7 +67,7 @@ For each primitive, exactly one canonical class **or** one canonical partial. Va
67 67 | Empty state | `.empty-state` (or `_ui.html` macro) | `--compact`, `--chart`, `--lg` | — |
68 68 | Loading skeleton | `partials/loading_skeleton.html` | row, card, list | — |
69 69 | Progress bar | `.progress-bar-container` + `.progress-bar` | `--slim` (6px), `--rounded`, `.progress-bar--highlight` (purple, default green) | — |
70 - | Upload status block | `.upload-status` + `__row` | `__msg.is-success`, `__msg.is-error` | — |
70 + | Upload status block | `.upload-status` + `-row` | `-msg.is-success`, `-msg.is-error` | — |
71 71 | Status pill | `.field-status` / `.save-status` | `.success`, `.error`, `.saving` | — |
72 72 | Table | `.data-table` (rich), `.compact-table` (small mono) | `.minw-300..800` for horizontal scroll min-width | `.sortable.ascending`, `.sortable.descending` |
73 73 | Tabs | `.tabs` + `.tab` | — | `.tab.is-selected` |
@@ -139,6 +139,9 @@ No new top-level layout containers without an entry in this list.
139 139 7. **Destructive actions** use `.danger` button class plus the `confirm_dialog` macro. No bare destructive buttons.
140 140 8. **Spacing values come from `--space-*` tokens.** Off-scale values are bugs.
141 141 9. **Page-scoped CSS is a last resort.** Default to composing layout + component primitives. Page-scoped sections exist for genuinely page-specific layout (grids, long-form typography, marketing heroes) — not for shapes that are cards or list rows in disguise.
142 + 10. **No bare `<h1>` / `<h2>` in templates.** Pick a heading class: `.brand-h1` (wordmark), `.page-title` (page h1), `.subtitle-h2` (page subtitle on auth/wizards), `.subsection-title` (default h2 inside dashboards/tabs/partials), or `.section-header` (prose sub-section with bottom border). Bare tags inside server-rendered markdown / user content (e.g. `partials/item_text_editor.html` JS preview) are the only exception.
143 + 11. **Button class is `.btn-primary` / `.btn-secondary` / `.btn-danger`.** Works on both `<button>` and `<a>` (the `<a><button>` antipattern is banned — use `<a class="btn-primary">…</a>`). The bare `button.primary` shorthand has been retired.
144 + 12. **Class names use kebab-case only.** No BEM `__` separator (retired 2026-05-20 — was 112 sites across 20 prefixes, now flattened to single-dash). Modifier `--` (e.g. `.card--bordered`) is still allowed.
142 145
143 146 ## How to extend
144 147
@@ -8,7 +8,7 @@ Human tasks in `human_todo.md`. Completed items in `todo_done.md`.
8 8
9 9 **Stripe webhook delivery: RESOLVED 2026-05-17.** `evt_1TY98u0AcRNJbwd4vkSfLZZI` landed at 18:10:25 UTC, transaction completed, 0 pending in DB. Whatever was fixed in the Stripe Dashboard between 2026-05-16 and 2026-05-17 cleared it. No code change involved.
10 10
11 - **Next code work (LLM-doable):** Phase 1 of the UX audit sweep against the consolidated state (see `## UX audit sweep` below). Run `/ux-audit` skill scoped to `pages/index.html`, `use_cases.html`, `pricing.html`, `creators.html`, `fan_plus.html`, `changelog.html`, `policy.html`. The Phase 0 charter + remediation pre-plan are both complete — surface audits can now run.
11 + **Next code work (LLM-doable):** UX audit sweep phases 0-8 complete (2026-05-20). 16 remediations landed across three same-day sessions (see `## UX audit sweep (phased)` § "UX audit remediations" → "Landed 2026-05-20"). All three charter decisions (button class, heading classes, BEM separator) resolved and codified as charter rules 10-12. Two original findings retracted as incorrect data. Remaining items are three cleanup tasks (`.sr-only` upgrade on file inputs, `.landing-founder-banner` fate, orphan CSS sweep). None are launch blockers.
12 12
13 13 ---
14 14
@@ -194,19 +194,56 @@ Status: Phase 0 audit complete (2026-05-19). Findings at `docs/ux-audit/phase-0.
194 194 - [ ] Delete `.landing-founder-banner` if folded into hero-callout primitive (currently kept separate per charter).
195 195 - [ ] Orphan CSS cleanup — ~30 truly unused class names in `style.css` deferred (e.g. `.tier-grid`, `.search-box`, `.signup-container`, `.button-stack`). Per-class judgement; revisit when bandwidth allows.
196 196
197 - - [ ] **Phase 0 — Design-system conformance audit.** *(Complete 2026-05-19.)* Before auditing surfaces, audit the *primitives*. The site should behave like an OS: a small set of design rules (spacing scale, type scale, color tokens, button/input/card/dialog/table/form/empty-state shapes, hover/focus/selected/disabled state recipes, loading/error/empty triad) that compose every screen. Read `static/style.css`, `static/media-player.css`, `static/wizard.css`, `_meta/docs/brand.md`, and a representative span of `templates/partials/*` + `pages/*`. Produce: (a) the inferred primitive set — what tokens and components *actually* exist in CSS/markup today; (b) divergence map — places where a screen reinvents a primitive (one-off `<div class="...">` shapes, ad-hoc spacing, bespoke buttons, duplicated empty/error states); (c) gaps — primitives the system *should* have but doesn't (e.g. no shared empty-state component, no destructive-button variant, no loading-skeleton recipe); (d) a written design-system charter (≤2 pages, `docs/design-system.md`) that names every primitive and its single canonical class/partial. Output gates the rest of the sweep: Phases 1–8 audit *conformance to the charter*, not just universal principles. Fixes in Phase 0 land as CSS/partial consolidation before any surface phase runs.
197 + - [x] **Phase 0 — Design-system conformance audit.** *(Complete 2026-05-19.)* Produced `docs/ux-audit/phase-0.md` (inventory + divergence map) and `docs/design-system.md` (charter).
198 198
199 - - [ ] **Phase 1 — Public landing & marketing.** `pages/index.html`, `use_cases.html`, `pricing.html`, `creators.html`, `fan_plus.html`, `changelog.html`, `policy.html`. First impression + conversion surface.
200 - - [ ] **Phase 2 — Discovery & browse.** `pages/discover.html`, `feed.html`, `tag_tree.html`, `user.html`, `project.html`, `collection.html`. Public browse path, post-discover funnel.
201 - - [ ] **Phase 3 — Item & library (consume).** `pages/item.html`, `library.html`, `library_audio.html`, `library_video.html`, `library_text.html`, `library_downloads.html`, `library_locked.html`, `audio_player.html`, `video_player.html`, `text_reader.html`, `blog_post.html`, `project_blog.html`, `project_paywall.html`. Soft-launch critical journey end.
202 - - [ ] **Phase 4 — Auth & onboarding.** `pages/login.html`, `forgot_password.html`, `reset_password.html`, `two_factor.html`, `oauth_authorize.html`, `account-deleted.html`, `confirm_delete.html`, `wizards/wizard_join.html`. Account-state transitions; forgiveness + error-message review.
203 - - [ ] **Phase 5 — Checkout & receipts.** `pages/buy.html`, `cart.html`, `purchase.html`, `receipt.html`, `stripe_disclaimer.html`. Commerce flow; feedback + trust cues + Stripe handoff.
204 - - [ ] **Phase 6 — Creator dashboard & wizards.** `dashboards/dashboard-user.html`, `dashboard-project.html`, `dashboard-item.html`, `dashboard-blog-editor.html`, `dashboard-export.html`, `dashboard-import.html`, `dashboard-delete-account.html`, `wizards/wizard_item.html`, `wizard_project.html`, plus `wizards/steps/*` and `wizards/partials/*`. Densest interaction surface — expect the most findings.
205 - - [ ] **Phase 7 — Admin.** `dashboards/admin-appeals.html`, `admin-metrics.html`, `admin-reports.html`, `admin-signups.html`, `admin-uploads.html`, `admin-users.html`, `admin-waitlist.html`. Internal but still has destructive actions — forgiveness + confirmation review matters.
206 - - [ ] **Phase 8 — Cross-cutting & utility.** `pages/error.html`, `health.html`, `sandbox.html`, `doc.html`, `doc_index.html`, `email_result.html`, `pages/git/*` (G1 browser), `partials/*`, `partials/tabs/*`. Plus a full flat-design sweep over `static/style.css`, `static/media-player.css`, `static/wizard.css`. Closes with a roll-up summary across all 8 phases.
199 + - [x] **Phase 1 — Public landing & marketing.** *(Complete 2026-05-20, `docs/ux-audit/phase-1.md`.)*
200 + - [x] **Phase 2 — Discovery & feed.** *(Complete 2026-05-20, `docs/ux-audit/phase-2.md`.)*
201 + - [x] **Phase 3 — Content viewing.** *(Complete 2026-05-20, `docs/ux-audit/phase-3.md`.)*
202 + - [x] **Phase 4 — Purchase flow.** *(Complete 2026-05-20, `docs/ux-audit/phase-4.md`.)*
203 + - [x] **Phase 5 — Library and media players.** *(Complete 2026-05-20, `docs/ux-audit/phase-5.md`.)*
204 + - [x] **Phase 6 — Auth and account.** *(Complete 2026-05-20, `docs/ux-audit/phase-6.md`.)*
205 + - [x] **Phase 7 — Dashboard and admin.** *(Complete 2026-05-20, `docs/ux-audit/phase-7.md`.)*
206 + - [x] **Phase 8 — Wizards, docs, source browser, errors.** *(Complete 2026-05-20, `docs/ux-audit/phase-8.md`.)*
207 207
208 208 Out of scope for this sweep: per-tier capability decisions, brand/aesthetic direction (see `_meta/docs/brand.md`), full WCAG audit (run axe/Lighthouse separately).
209 209
210 + ### UX audit remediations (by site section)
211 +
212 + Findings from phases 1-8, grouped by the website area they affect. Six items landed in the audit session on 2026-05-20; seven more landed in a follow-up session the same day; two original findings were retracted (incorrect data). Remaining items are charter decisions or cleanup tasks.
213 +
214 + **Landed 2026-05-20 (audit session)** *(crossed off; kept here as a record)*
215 + - [x] `pricing.html` tier-picker keyboard-accessibility fix — radio visually-hidden but focusable; `:focus-within` outline on label; JS listens on `change` so keyboard navigation updates styling.
216 + - [x] `pricing.html` `.tier-option.selected` → `.is-selected` (last pre-charter `.selected` survivor; template, JS, and CSS all updated).
217 + - [x] `<a><button>` antipattern sweep — all 44 sites across 13 files converted to `<a class="btn-primary">…</a>`. Added `display: inline-block` and `text-decoration: none` to the button base rules so anchors render correctly.
218 + - [x] Four page-isolated `<style>` blocks migrated — `user.html` (deleted dead duplicate; rules already in style.css), `dashboard-user.html` (scoped under `.dashboard-user-page` to resolve name collision with `user.html` and the user_projects tab partial; body class updated to `padded-page dashboard-page dashboard-user-page`), `library_feed.html` (scoped under `.library-page`; added `library-page` body class to library.html), `user_settings.html` (migrated as global primitives).
219 + - [x] Follow-state synonyms → `.is-selected` — `.tag-follow-btn.following` and `.follow-btn.is-following` across discover.html, user.html, project.html, follow_button.html, tag_follow_toggle.html + 4 CSS rules. Also fixed a latent bug in `follow_button.html` where two `class=` attributes on the same `<button>` meant `.is-following` never actually applied.
220 + - [x] `.intro` lifted to `.page-intro` primitive — canonical rule added near `.section-lead`; four scoped copies dropped; four templates migrated.
221 +
222 + **Landed 2026-05-20 (follow-up session)** *(crossed off; kept here as a record)*
223 + - [x] **Heading convention for prose pages — decided + applied.** Picked `.section-header` over bare `<h2>` for prose-page sub-section headings. ~25 bare h2s converted across creators.html, policy.html, changelog.html, fan_plus.html, stripe_disclaimer.html, purchase.html, cart.html, library_downloads.html, doc_index.html, git/repo.html, git/settings.html. `library_locked.html:22` skipped intentionally (h2 lives inside a `.purchase-box` notification card; section-header mono-bottom-border styling would clash). Auth flow + wizards keep bare `<h2>` as the documented page-subtitle pattern.
224 + - [x] **`dashboard-user.html`** — "Account Deactivated" and "Account Paused" rewrapped from `.card-muted`/`<h2>` to `.alert.alert-warning`/`<p class="alert-title">`. `.banner--warning` was the audit suggestion but it's a single-line page-top primitive and doesn't fit multi-paragraph + action-buttons content.
225 + - [x] **`git/settings.html` delete-repo** — `onsubmit="return confirm(...)"` form replaced with htmx button (`hx-post` + `hx-confirm`). `repo_settings_delete` handler now returns `HX-Redirect` instead of `Redirect::to()`; CSRF flows via the global `htmx:configRequest` X-CSRF-Token wiring in `mnw.js`.
226 + - [x] **`project.html:208` `.login-cta-btn`** — class removed; `width: 100%` folded into the existing `.project-page .tier-card button` rule by extending the selector to also match `.btn-primary` anchors.
227 + - [x] **`dashboard-export.html` `.export-card-*` family** — documented as intentionally page-scoped (audit's other explicit option). Added a CSS comment block explaining the row-layout pattern doesn't fit `.card-muted` cleanly and the `--light-background` fill is deliberate for surface distinction from the surrounding dashboard.
228 + - [x] **`wizard.css` consolidation** — `.monetization-card{,-cards,.info-only}` rules deleted as dead CSS (zero template references; ~25 lines removed including responsive override). `.content-choice-card` anchor-link override + `.muted` modifier moved to style.css under the canonical `.card--selectable` recipe; renamed `.muted` → `.is-disabled` to align with `.is-selected` naming. Sole template using it (`wizards/steps/project/first_content.html`) migrated.
229 + - [x] **`discover_results.html` empty-state hint markup** — accepted as-is per the audit's own recommendation. Four identical inline blocks consolidated within one file; a fifth macro variant for the inline-anchor hint would be overengineered.
230 +
231 + **Retracted findings** *(audit data was wrong; no action needed)*
232 + - ~~`.coming-soon` modifier has no CSS rule~~ — rule exists at `style.css:6489` (`border-style: dashed; opacity: 0.6`). Original Phase 1 grep was too narrow.
233 + - ~~`cart-group__title` BEM `__` is the only `__` separator in the codebase~~ — actually 112 template usages across 20 block prefixes (cart-empty, cart-multi-bar, cart-group, store-cta, suspension-banner, upload-status, proj-overview-setup, bundle-picker-row, psection-row, sandbox-banner, etc.). It's an established convention. Either document in charter or schedule a separate larger rename pass — not a small fix.
234 +
235 + **Remaining items**
236 +
237 + **Charter decisions — resolved 2026-05-20**
238 + - [x] **Button class shorthand → `.btn-primary` / `.btn-secondary` / `.btn-danger`.** Migrated all 67 shorthand sites (`class="primary"` etc.) to `.btn-*` form across templates + JS. Dropped `button.primary` / `.secondary` / `.danger` from CSS selectors (kept `.btn-*` only) — base style.css + four scoped overrides (`.item-page`, `.project-page .item-content`, `.purchase-page`, `.link-row`). Rationale: only `.btn-*` works on both `<button>` and `<a>` (after the `<a><button>` antipattern fix), shorthand was element-scoped. Charter rule 11 codifies.
239 + - [x] **Heading classes — no bare `<h1>` / `<h2>`.** Added four explicit classes: `.brand-h1` (Makenot.work wordmark), `.page-title` (page h1), `.subtitle-h2` (auth/wizard subtitle), `.subsection-title` (default h2 in dashboards/tabs/partials). Existing `.section-header` (h2 with border) kept for prose sub-sections. Migrated 93 templates; only remaining bare tags are inside the JS markdown-renderer string in `partials/item_text_editor.html` (user content). Charter rule 10 codifies.
240 + - [x] **BEM `__` separator retired.** Renamed 76 distinct `block__elem` tokens (15 files: templates + style.css + JS) to single-dash kebab-case. Modifier `--` (`.card--bordered`) preserved. No collisions. Charter rule 12 codifies.
241 +
242 + **Cleanup tasks** (deferred from consolidation pass)
243 + - [x] **`.sr-only` accessibility upgrade on file inputs** *(landed 2026-05-20)* — 13 `<input type="file">` sites swapped from `class="hidden"` / `class="wizard-file-input"` to `class="sr-only"`. Trigger buttons unchanged (they `.click()` the input programmatically); screen readers and keyboard now reach the input. `.wizard-file-input { display: none }` rule removed from `wizard.css`. One file input (`dashboard-import.html#import-file`) intentionally left visible — has a proper visible label.
244 + - [ ] **`.landing-founder-banner`** — delete if folded into hero-callout primitive (currently kept separate per charter).
245 + - [ ] **Orphan CSS cleanup** — ~30 truly unused class names in `style.css` deferred (e.g. `.tier-grid`, `.search-box`, `.signup-container`, `.button-stack`). Per-class judgement; revisit when bandwidth allows.
246 +
210 247 ---
211 248
212 249 ## Infra & Quality
@@ -0,0 +1,151 @@
1 + # UX Audit — Phase 1: Public Landing Surfaces
2 +
3 + Audit ran 2026-05-20 against the unauthenticated, marketing-facing pages: `index.html`, `creators.html`, `pricing.html`, `use_cases.html`, `policy.html`, `changelog.html`. Charter is `docs/design-system.md`; Phase 0 inventory at `phase-0.md`; pre-Phase-1 consolidation log at `remediation-plan.md`.
4 +
5 + The question this phase asks is whether the landing surfaces, taken as a set, *read like the same product*. After the Phase 0 consolidation pass, the answer is **yes — with five concrete drifts and one accessibility break.**
6 +
7 + ## What conforms (the good news)
8 +
9 + Across all six templates:
10 +
11 + - Zero inline `style="..."` attributes.
12 + - Zero page-isolated `<style>` blocks.
13 + - No raw hex literals; everything reads through tokens.
14 + - No checkmark glyphs, emoji, or pure black/white at use sites.
15 + - Every page leads with a single h1.
16 + - Every CTA uses `<a class="btn-primary">` / `<a class="btn-primary btn--large">` *on five of six pages*. (The exception is in §3 below.)
17 + - All bespoke `-card` class names found here (`.fork-card`, `.tier-card`, `.use-case-card`, `.feature-card`) are documented aliases for `.card--bordered` (`style.css:1033-1038`).
18 + - No `.empty-state` markup in any landing page (none of them have list-like content that empties out).
19 + - Section headings on `index`, `pricing`, and `use_cases` use the canonical `.section-label` modifier.
20 +
21 + The five landing surfaces that were redesigned during Phase 0 / remediation are clean. The remaining drifts cluster on `creators.html`, `policy.html`, and `changelog.html` — pages that weren't directly touched by the consolidation pass.
22 +
23 + ## Drifts from the charter
24 +
25 + ### 1. `pricing.html` tier picker uses the pre-charter selected recipe
26 +
27 + `pricing.html:24` carries `class="tier-card tier-option selected"` on the default-checked tier. The JS at lines 336-338 adds and removes `.selected` as the user clicks. CSS at `style.css:6019-6023`:
28 +
29 + ```css
30 + .tier-option.selected {
31 + border-color: var(--highlight);
32 + border-width: 2px;
33 + padding: calc(1rem - 1px);
34 + }
35 + ```
36 +
37 + Remediation §2.4 declared the canonical selection class is `.is-selected` with `background: var(--highlight-faint); border-color: var(--focus-ring);`. This is the last `.selected` (without `is-`) in the codebase. Two changes:
38 + - Rename `.tier-option.selected` → `.tier-option.is-selected` in `style.css`, template, and JS.
39 + - Either fold the recipe into the canonical `.is-selected` shape (background tint + ring border) or document `.tier-option` as a card-shaped selected variant that keeps the bordered look.
40 +
41 + ### 2. `pricing.html` tier picker is keyboard-inaccessible
42 +
43 + `style.css:6015-6017`:
44 +
45 + ```css
46 + .tier-option input[type="radio"] {
47 + display: none;
48 + }
49 + ```
50 +
51 + `display: none` removes the radio from the accessibility tree *and* from the focus order. There is no compensating focus styling on the surrounding `<label>`. A keyboard user cannot tab between tiers; a screen-reader user gets a radiogroup with no perceivable radios.
52 +
53 + Fix recipe (matches Phase 0 §4 "Apply focus ring to custom interactive containers"):
54 +
55 + ```css
56 + .tier-option input[type="radio"] {
57 + position: absolute;
58 + opacity: 0;
59 + pointer-events: none;
60 + }
61 + .tier-option:has(input:focus-visible),
62 + .tier-option:focus-within {
63 + outline: var(--focus-ring) solid 2px;
64 + outline-offset: 2px;
65 + }
66 + ```
67 +
68 + This is the only **accessibility bug** Phase 1 found.
69 +
70 + ### 3. `creators.html` wraps `<button>` in `<a>` for CTAs
71 +
72 + Lines 106, 110, 113, 114:
73 +
74 + ```html
75 + <a href="/dashboard"><button class="primary">Go to Dashboard</button></a>
76 + <a href="/dashboard#tab-plan"><button class="primary">Apply from Dashboard</button></a>
77 + <a href="/join"><button class="primary">Join</button></a>
78 + <a href="/login"><button class="secondary">Login</button></a>
79 + ```
80 +
81 + `<a><button></button></a>` is invalid HTML (interactive content nested inside interactive content) and produces unpredictable focus/activation behavior across browsers. Every other landing page uses the canonical `<a class="btn-primary">…</a>`. Convert these four to the anchor-with-button-class pattern.
82 +
83 + ### 4. `creators.html`, `policy.html`, `changelog.html` h2s lack `.section-label`
84 +
85 + | File | Lines | Headings |
86 + |---|---|---|
87 + | `creators.html` | 18, 45, 94 | How It Works, Pricing, Who Runs This |
88 + | `policy.html` | 21, 34, 46, 58, 69, 81 | six section headings |
89 + | `changelog.html` | 18, 33, 43 | per-month headings |
90 +
91 + `index`, `pricing`, and `use_cases` all use `<h2 class="section-label">`. The three older pages use bare `<h2>`, which inherits Young Serif from the global rule rather than the mono treatment `.section-label` gives sub-section headings.
92 +
93 + Pick one. Either:
94 + - Promote bare h2 to the convention by adding `.section-label` everywhere a section heading appears (the recommended path; aligns with charter §"Section header").
95 + - Or accept that long-form prose pages (`policy`, `changelog`) want the Young Serif treatment and document the exemption in the charter.
96 +
97 + The current state is silent drift — a reader can't tell whether the inconsistency is intentional.
98 +
99 + ### 5. `.intro` is duplicated identically across four page scopes
100 +
101 + `style.css:2740, 3453, 3875, 3908` — four copies of the same rule:
102 +
103 + ```css
104 + .{fan-plus|creators|changelog|policy}-page .intro {
105 + font-size: 1.1rem;
106 + margin-bottom: var(--space-6);
107 + line-height: 1.6;
108 + }
109 + ```
110 +
111 + This is a primitive masquerading as a page-scoped rule. Lift to a single `.page-intro` class in the primitives section of `style.css`, migrate the four templates, drop the four scoped copies.
112 +
113 + ### 6. ~~`use_cases.html` `.coming-soon` modifier is undocumented~~ — finding retracted
114 +
115 + *Original finding (incorrect): claimed `.use-case-card.coming-soon` had no CSS rule. Actually defined at `style.css:6489` with `border-style: dashed; opacity: 0.6;`. The grep that produced this finding was too narrow and missed the rule. No drift here.*
116 +
117 + ## Two charter additions to consider
118 +
119 + Phase 1 surfaced two patterns that recur cleanly enough to deserve charter entries instead of staying ad-hoc:
120 +
121 + 1. **`.page-intro`** — large-text intro paragraph under the page h1. Used on at least four pages, duplicated four times in CSS.
122 + 2. **`.policy-section` / `.changelog-entry`** — vertically-separated content sections with bottom border, used on policy and changelog. Currently page-scoped but the shape is the same. A `.content-section` primitive (or `.section-header` composition) would absorb both.
123 +
124 + These are *suggestions*, not blockers — Phase 1's job is to surface them; the charter decision belongs to a follow-up.
125 +
126 + ## Verdict by axis
127 +
128 + | Axis | State | Note |
129 + |---|---|---|
130 + | Inline styles | Clean | 0 across all six |
131 + | Page-isolated `<style>` | Clean | 0 across all six |
132 + | Token usage | Clean | All colors / spacing / radius read tokens |
133 + | Brand glyphs | Clean | No checkmarks, emoji, or pure hex at use sites |
134 + | Heading hierarchy | Drift | 3/6 pages skip `.section-label` on h2 |
135 + | Selection state | Drift | `pricing.html` `.tier-option.selected` is the last pre-charter survivor |
136 + | Button primitive | Drift | `creators.html` uses invalid `<a><button>` nesting in 4 CTAs |
137 + | Card primitive | Conformant | All `-card` names alias to `.card--bordered` |
138 + | Keyboard accessibility | **Broken** | `pricing.html` tier picker not focusable |
139 + | Repeated rule definitions | Drift | `.intro` duplicated across 4 page scopes |
140 +
141 + ## Remediation — what to land before Phase 2
142 +
143 + In priority order:
144 +
145 + 1. **Fix the keyboard trap on `.tier-option`** — accessibility, ship as standalone change.
146 + 2. **Migrate `.tier-option.selected` → `.is-selected`** — closes the last selection-state drift.
147 + 3. **Replace `<a><button>` nesting in `creators.html`** — invalid HTML; trivially mechanical.
148 + 4. **Decide on `.section-label` policy for prose pages** — apply or document exemption.
149 + 5. **Lift `.intro` → `.page-intro` primitive** — small CSS consolidation.
150 +
151 + After 1-3 land, Phase 1 surfaces are charter-conformant. Items 4-6 are tidy-up. Phase 2 (authenticated content surfaces: feed, discover, item, project, library) can proceed in parallel with the tidy-up.
@@ -0,0 +1,50 @@
1 + # UX Audit — Phase 2: Discovery and Feed
2 +
3 + Audit ran 2026-05-20. Scope: `discover.html`, `tag_tree.html`, `feed.html`, `discover_results.html` (partial), and `partials/tabs/library_feed.html`.
4 +
5 + ## What conforms
6 +
7 + - Zero inline `style="..."` on any of the four page templates.
8 + - All four pages lead with h1 (after the Phase 0 fixes for `discover` and `tag_tree`).
9 + - No bare `<h2>` — when h2 appears it carries `.section-label` or sits inside a content-section primitive.
10 + - All bespoke `-card` names (`.grid-card`, `.discover-grid-card`, `.tag-card`, `.tag-card--leaf`) are either documented or aliased.
11 + - No `<a><button>` antipattern.
12 + - No raw hex literals at use sites.
13 + - Empty-state markup migrated to `ui::empty_state` macros (per remediation third sitting).
14 +
15 + ## Drifts
16 +
17 + ### 1. `partials/tabs/library_feed.html` carries a page-isolated `<style>` block (23 lines)
18 +
19 + `library_feed.html:1-24` defines `.feed-meta`, `.feed-table-header`, `.feed-table-row`, `.feed-col-right`, `.feed-results-table`, `.feed-item-name-cell`, `.feed-item-name`, `.feed-item-creator`, `.feed-pagination` inline. This duplicates the Phase 0 pattern that `buy.html` / `item.html` / `error.html` were called out for, and the Phase 0 follow-up missed it.
20 +
21 + The rules read tokens correctly but are spelled out as an embedded sheet. Migrate to `style.css` under a `LIBRARY FEED TAB` section, named the same way the other tab styles are (e.g. `.library-feed-page .feed-row`).
22 +
23 + ### 2. `discover_results.html` keeps four inline empty-state blocks with `.empty-state-hint`
24 +
25 + Lines 14-17, 36-39, 60-63, 86-89 — all identical:
26 +
27 + ```html
28 + <div class="empty-state">
29 + <p>No projects found matching your filters.</p>
30 + <p class="empty-state-hint">Try broadening your search or <a href="/discover">clearing all filters</a>.</p>
31 + </div>
32 + ```
33 +
34 + The remediation pass intentionally left these bespoke because the hint paragraph contains an inline anchor — the current `ui::empty_state` signature doesn't accept HTML in `body`. Options:
35 +
36 + - Add `ui::empty_state_with_hint_link(body, hint_prefix, hint_link_href, hint_link_label, hint_suffix)` if the four-instance duplication is worth a macro.
37 + - Extract to a new `partials/_empty_filtered.html` include and `{% include %}` four times (the file already serializes the same `subject` distinction).
38 + - Accept four copies inside one file as already-consolidated.
39 +
40 + Recommend: option 3 (do nothing). They live in one file, are identical, and the cost of a fifth macro variant exceeds the gain.
41 +
42 + ### 3. `discover.html` `.tag-follow-btn` toggle uses opacity not `.is-selected`
43 +
44 + `style.css` styles `.tag-follow-btn` and `.tag-follow-btn.following` with bespoke recipes (grep for follow-btn). Remediation §2.3 named it as a case to fold into `.btn-secondary` with `.is-selected` modifier. Currently uses its own `.following` class as the selected-state marker.
45 +
46 + This is the same family of drift as the `pricing.html` tier-option finding in Phase 1: a custom-named selected state instead of the canonical `.is-selected`.
47 +
48 + ## Verdict
49 +
50 + Discovery surfaces are nearly conformant. One real consolidation item (`library_feed.html` style block — Phase 0 plan said zero remained, but four remain across the codebase; see Phase 5 for the rest), one selected-state drift, one already-consolidated-enough case.
@@ -0,0 +1,44 @@
1 + # UX Audit — Phase 3: Content Viewing
2 +
3 + Audit ran 2026-05-20. Scope: `item.html`, `project.html`, `project_blog.html`, `blog_post.html`, `collection.html`, `user.html`.
4 +
5 + ## What conforms
6 +
7 + - All six lead with h1.
8 + - All sub-section h2s use `.section-header` (the canonical sub-section heading).
9 + - All `-card` variants (`.item-card`, `.tier-card`, `.fork-card`) ride documented aliases (`.card--bordered`, `.card-muted`).
10 + - `.section-tab.is-selected` and `.view-btn.is-selected` are canonical.
11 + - Empty-state markup migrated to `ui::empty_state` (`collection.html`, `project_blog.html`).
12 +
13 + ## Drifts
14 +
15 + ### 1. `user.html` carries a page-isolated `<style>` block (18 lines)
16 +
17 + `user.html:36-54` defines `.container { max-width: 600px }`, `.profile-header`, `.profile-avatar`, `.profile-name`, `.profile-username`, `.profile-bio`, `.links-section`, `.link-item`, `.link-title`, `.link-description`, `.project-title`, `.project-meta`, `.project-description`.
18 +
19 + Same anti-pattern as the four others called out in remediation Phase 0 §2.11 — duplicates `.user-page`-scoped rules under different names, and `.project-title` / `.project-meta` likely collide with `dashboard-user.html`'s identically-named locals. Lift to `style.css` under a `USER PAGE` section.
20 +
21 + ### 2. `.follow-btn.is-following` naming drifts from `.is-selected`
22 +
23 + `project.html:66, 70` and `user.html` use `.follow-btn` with `.is-following` as the toggle. CSS at `style.css:9063, 9445, 10672`. Functionally equivalent to `.is-selected` (focus-ring border + highlight-faint background). Same family as `discover.html` `.tag-follow-btn.following`.
24 +
25 + The Phase 2 finding generalizes: any "X is followed/active" pattern in the codebase has its own custom selector instead of the canonical `.is-selected`. Either standardize on `.is-selected` (and let component-scoped CSS read it), or accept "is-following" / "following" as a documented synonym in the charter.
26 +
27 + ### 3. `<a><button>` antipattern: 7 instances in `item.html`, `project.html`
28 +
29 + Mirrors the Phase 1 `creators.html` finding:
30 +
31 + - `item.html:144, 163, 165` — view/purchase CTAs
32 + - `project.html:155, 161, 163, 208` — view/purchase/log-in CTAs
33 +
34 + Every one of these can become `<a class="btn-primary">…</a>` or `<a class="btn-secondary">…</a>`. The CSS at `style.css:336-385` aliases `button.primary` to `.btn-primary`, so anchors with `.btn-primary` already render identically. The `<a><button>` form is invalid HTML.
35 +
36 + ### 4. Other content-page surfaces
37 +
38 + - `project_blog.html`, `blog_post.html`, `collection.html` — clean.
39 + - `item.html`'s `<style>` block from Phase 0 was removed successfully (verified empty).
40 + - `project.html:208` carries `class="primary login-cta-btn"` — `.login-cta-btn` is undocumented. Likely a one-off, fold into the canonical button or document as a project-page variant.
41 +
42 + ## Verdict
43 +
44 + Two real items (`user.html` `<style>` block, the seven `<a><button>` CTAs), one terminology call to make (`.is-following` vs `.is-selected`), one minor (`.login-cta-btn`).
@@ -0,0 +1,40 @@
1 + # UX Audit — Phase 4: Purchase Flow
2 +
3 + Audit ran 2026-05-20. Scope: `buy.html`, `purchase.html`, `cart.html`, `receipt.html`, `fan_plus.html`, `stripe_disclaimer.html`.
4 +
5 + ## What conforms
6 +
7 + - All six lead with h1.
8 + - Zero inline `style="..."`.
9 + - Zero page-isolated `<style>` blocks (the `buy.html` block was successfully removed in Phase 0).
10 + - `cart.html` empty-state would use `ui::empty_state_with_action` cleanly, currently uses an `<a><button>` pattern instead — see drift §3.
11 + - No raw hex literals at use sites.
12 +
13 + ## Drifts
14 +
15 + ### 1. `purchase.html` has four bare `<h2>` sub-headings
16 +
17 + Lines 13, 36, 55, 74: "Confirm Purchase", "Price Breakdown", "What {creator} receives", "Secure Checkout". These should carry `.section-header` to match the convention used in `item.html` / `project.html`. The purchase flow is the most commercially-critical page in the app; visual treatment that drifts from the rest of the site here is the highest-stakes drift in the audit.
18 +
19 + ### 2. `cart.html` has one bare `<h2 class="cart-group__title">` and one bare `<h2>From your wishlist</h2>`
20 +
21 + Line 44 uses `.cart-group__title`; line 135 is bare. Standardize on `.section-header` (or `.section-header.cart-group-title` if the group title needs a tighter variant).
22 +
23 + *Correction (post-audit):* an earlier draft of this finding claimed `.cart-group__title` was the only BEM `__` separator in the codebase. That was wrong. BEM `__` is widely used — 112 template usages across 20 distinct block prefixes (cart-empty, cart-multi-bar, cart-group, store-cta, suspension-banner, upload-status, proj-overview-setup, bundle-picker-row, psection-row, sandbox-banner, section-row, tier-row, item-footer, dns-row, report-modal, user-media-card, user-media-folders, user-media-upload, proj-overview-tool, proj-overview-tools) with 82 corresponding CSS rules. It's an established internal convention. The remaining drift here is just the heading-treatment one — fold under §1.
24 +
25 + ### 3. `cart.html:22` and 7 player-page sites use `<a><button>`
26 +
27 + - `cart.html:22` — empty-state "Browse Discover" CTA
28 + - See Phase 5 for the player-page sites
29 +
30 + ### 4. `fan_plus.html`, `stripe_disclaimer.html` bare `<h2>`
31 +
32 + `fan_plus.html:32` "What you get", `stripe_disclaimer.html:12` "Before You Connect with Stripe". Add `.section-header`.
33 +
34 + ### 5. `purchase.html` `<button>` intent-only classes
35 +
36 + Lines 87, 118, 138, 183 use `<button class="primary">` / `<button class="secondary">`. CSS at `style.css:336-385` aliases these to `.btn-primary` / `.btn-secondary` so they render identically; this is the documented "older shorthand" pattern. Choose one canonical pattern in the charter or accept both as equivalent.
37 +
38 + ## Verdict
39 +
40 + Purchase flow is structurally clean (no `<style>` blocks, no inline styles). The drifts are all heading-treatment and button-class conventions — visible but mechanical.
@@ -0,0 +1,32 @@
1 + # UX Audit — Phase 5: Library and Media Players
2 +
3 + Audit ran 2026-05-20. Scope: `library.html`, `library_audio.html`, `library_video.html`, `library_text.html`, `library_downloads.html`, `library_locked.html`, `audio_player.html`, `video_player.html`, `text_reader.html`, plus `partials/tabs/library_*.html`.
4 +
5 + ## What conforms
6 +
7 + - All library and player pages lead with h1.
8 + - Zero raw hex at use sites.
9 + - `library_audio`, `library_video`, `library_text`, `library.html` are clean (no style blocks, no inline styles, no `<a><button>` nesting).
10 + - The media-player layout primitives in `media-player.css` are correctly scoped (`.media-container`, `.video-display`, etc.) and read from tokens.
11 +
12 + ## Drifts
13 +
14 + ### 1. `audio_player.html`, `video_player.html`, `text_reader.html` each carry 4 `<a><button>` CTAs (12 total)
15 +
16 + Same antipattern as Phase 1 / Phase 3. The CTAs are the watch / read / log-in / purchase actions in `.media-store-cta` blocks. All twelve become `<a class="btn-primary">` mechanically.
17 +
18 + ### 2. `library_locked.html` has 2 `<a><button>` CTAs (lines 26, 28) and 1 bare `<h2>` (line 16)
19 +
20 + The locked-state landing for content the user hasn't purchased. Add `.section-header` and rewrite the two CTAs as anchors.
21 +
22 + ### 3. `library_downloads.html` has 3 bare `<h2>`
23 +
24 + Lines 36, 64, 106: "Downloads", "Included in this bundle (n)", "License". Standardize on `.section-header`.
25 +
26 + ### 4. `partials/tabs/library_feed.html` carries a 23-line page-isolated `<style>` block
27 +
28 + Already called out in Phase 2 §1. This is a *partial*, not a page, but the embedded sheet defines `.feed-meta`, `.feed-table-header`, `.feed-table-row`, `.feed-pagination`, etc. Migrate to `style.css` under a `LIBRARY FEED TAB` section.
29 +
30 + ## Verdict
31 +
32 + Library-list pages are clean. Media players are structurally clean but every CTA wraps a button in an anchor — a tight cluster of 12 invalid-HTML sites that should be fixed as one mechanical pass.
@@ -0,0 +1,38 @@
1 + # UX Audit — Phase 6: Auth and Account
2 +
3 + Audit ran 2026-05-20. Scope: `login.html`, `forgot_password.html`, `reset_password.html`, `two_factor.html`, `oauth_authorize.html`, `email_result.html`, `account-deleted.html`, `confirm_delete.html`, `sandbox.html`.
4 +
5 + ## What conforms
6 +
7 + - All nine pages lead with h1 (the brand wordmark "Makenot.work").
8 + - Zero inline `style="..."`.
9 + - Zero page-isolated `<style>` blocks.
10 + - Zero raw hex at use sites.
11 + - Forms use canonical `.form-group` + `.form-container` primitives.
12 +
13 + ## Drifts
14 +
15 + ### 1. `<h2>` as page-subtitle is the established auth-flow pattern
16 +
17 + Every auth-flow page uses `<h1>Makenot.work</h1>` (brand wordmark) followed by `<h2>` for the page purpose ("Log in", "Reset Password", "Two-Factor Authentication", "Delete Account", etc.). This is consistent across nine pages and is the **intentional** design — the brand wordmark is the page's hero element, and the h2 is the page-subtitle below it.
18 +
19 + Implication for Phase 1 finding §4 (`creators.html` / `policy.html` / `changelog.html` bare h2): those pages may also be intentionally using h2 as a prose-style section header rather than the dashboard `.section-label` mono treatment. **This is a charter decision that should be made explicitly**, then either applied or documented as the exempt pattern.
20 +
21 + ### 2. `account-deleted.html:11` uses `<a><button>` for the "Return Home" CTA
22 +
23 + One instance. Mechanical fix.
24 +
25 + ### 3. `confirm_delete.html` uses `<button class="danger">` (line 26)
26 +
27 + This is a real `<button type="submit">` inside a form, not an `<a><button>` antipattern. The class is canonical (the CSS at `style.css:370-385` defines `button.danger`). Conformant.
28 +
29 + ### 4. `sandbox.html` and `reset_password.html` have additional bare `<h2>` headings
30 +
31 + - `sandbox.html:10` — "Creator Sandbox" (the page-subtitle pattern)
32 + - `reset_password.html:57` — "Link Expired" (an alternate state — could be a separate `<h2 class="section-header">` or kept as page-subtitle)
33 +
34 + Both follow the established auth-flow pattern. Conformant.
35 +
36 + ## Verdict
37 +
38 + Auth surfaces are clean. The bare-h2 pattern is intentional design, not drift. One mechanical fix in `account-deleted.html`. The cross-phase implication is that the bare-h2 finding from Phase 1 needs a charter-level decision: which pages get the auth-flow pattern (brand h1 + subtitle h2), which get the dashboard pattern (page h1 + `.section-label` h2)?
@@ -0,0 +1,52 @@
1 + # UX Audit — Phase 7: Dashboard and Admin
2 +
3 + Audit ran 2026-05-20. Scope: `templates/dashboards/` (14 templates) + `templates/partials/tabs/` (38 tab partials).
4 +
5 + ## What conforms
6 +
7 + - All dashboard pages have h1.
8 + - Forms use canonical `.form-group` / `.form-container` / input-class composition.
9 + - Empty-state markup migrated to `ui::empty_state` macros across the tab partials (third-sitting work; 22 call sites total span dashboards + content pages).
10 + - Selected-state on tabs uses `.tab.is-selected` (canonical).
11 + - `.data-table` and `.compact-table` are used consistently.
12 + - Admin tabs (admin-*) are clean — no inline styles, no `<style>` blocks, no antipatterns.
13 +
14 + ## Drifts
15 +
16 + ### 1. `dashboard-user.html` carries a 16-line page-isolated `<style>` block
17 +
18 + `dashboard-user.html:7-22` defines `.project-card`, `.project-info`, `.project-title`, `.project-meta`, `.project-stats`, `.project-actions`, `.summary-row`, plus a global `h1 { ... }` and `.stat-value` override.
19 +
20 + Two issues:
21 + - The global `h1 { font-size: 2.5rem; ... }` overrides the site-wide h1 treatment for this page only. Bleeds into anything else on the page that uses h1 (which is just the user h1 at line 75 — OK in practice, but a footgun).
22 + - `.project-card` / `.project-title` / `.project-meta` collide with the identically-named classes embedded in `user.html` (the public profile page). Same names, different rules, different files.
23 +
24 + Lift to `style.css` under a `DASHBOARD USER` section. Resolve the `.project-card` name collision with `user.html` by either picking one canonical rule or namespacing one of them (`.dashboard-user-page .project-card` vs `.user-page .project-card`).
25 +
26 + ### 2. `partials/tabs/user_settings.html` carries a 21-line page-isolated `<style>` block
27 +
28 + `user_settings.html:1-22` defines `.settings-layout`, `.settings-nav`, `.settings-nav a`, `.settings-nav a.is-selected`, `.settings-body`, plus a media query. Same anti-pattern. Migrate to `style.css` under a `SETTINGS TAB` section.
29 +
30 + (Notably, this block does use `.is-selected` correctly — the recipe internally conforms to the charter; it's only the file location that's wrong.)
31 +
32 + ### 3. `dashboard-user.html:38, 52` — two bare `<h2>` for account-state notices
33 +
34 + "Account Deactivated", "Account Paused". These are alert-style banners more than section headers. Could carry `.banner--warning h2` styling, or wrap in an `.alert` partial. Don't leave bare.
35 +
36 + ### 4. `dashboard-delete-account.html:36` — one `<a><button>` for "Go to Export Portal"
37 +
38 + Mechanical fix.
39 +
40 + ### 5. `dashboard-export.html` has 31 `.export-card-*` class instances
41 +
42 + `style.css:2618-2652` defines the family under `.export-page` scope. The classes (`.export-cards`, `.export-card`, `.export-card-info`, `.export-card-title`, `.export-card-desc`, `.export-card-meta`) form an undocumented sub-component. Either:
43 + - Document the export-page sub-component in the charter (one page; small surface).
44 + - Refactor to `.card-muted` (or `.card--bordered`) + utility classes for the title/desc/meta breakdown. The `.export-card` shape looks very close to a generic `.card-muted` with a row layout.
45 +
46 + ### 6. `dashboard-blog-editor.html` uses `<button class="primary|secondary">` without `.btn-` prefix
47 +
48 + Lines 43, 44, 46. Equivalent rendering per `style.css:336-385`. Same decision as Phase 4 §5 — pick one convention.
49 +
50 + ## Verdict
51 +
52 + Dashboard surfaces are mostly clean. Two real `<style>` block migrations (`dashboard-user.html` and `user_settings.html` tab) are the biggest items. The `.export-card-*` family is the only undocumented sub-component group in the admin/dashboard tree — small but worth deciding on. Rest is mechanical (one `<a><button>`, two bare h2s, button-class shorthand).
@@ -0,0 +1,68 @@
1 + # UX Audit — Phase 8: Wizards, Docs, Source Browser, Errors
2 +
3 + Audit ran 2026-05-20. Scope: `templates/wizards/` (20 templates), `doc.html` / `doc_index.html`, `templates/pages/git/*` (12 templates), `health.html`, `error.html`.
4 +
5 + ## What conforms
6 +
7 + - Wizards: every step lives under a `wizard_*` wrapper that emits `<h2>Wizard Name</h2>` then `<h2>Step Name</h2>`; step partials carry their own `<h2>Step Title</h2>`. Wizard `<h2>` is the step-title convention (analogous to the auth-flow page-subtitle pattern from Phase 6).
8 + - All step partials use canonical `.form-group`, `.form-section`, `.form-actions`.
9 + - Source browser (`git/`) is clean: zero inline styles, zero `<style>` blocks, zero `<a><button>`, all leads have h1 or `.git-repo-name`.
10 + - `error.html` lead is now h1 (promoted from `<p>` during Phase 0 §2.10).
11 + - `doc.html` is clean.
12 +
13 + ## Drifts
14 +
15 + ### 1. Wizards use bespoke `-card` families that the charter alias table doesn't fully cover
16 +
17 + `wizard.css` retains rules for `.monetization-card`, `.monetization-card.info-only`, `.content-choice-card`, `.content-choice-card.muted`, plus the `.type-card` / `.pricing-card` primitives that were moved to `style.css`. Remediation §2.1 wanted these absorbed into `.card--selectable` modifiers; the plan deferred this as "cosmetic." Status today: `.type-card` and `.pricing-card` were deduped (good); `.monetization-card` and `.content-choice-card` remain as their own families.
18 +
19 + If the charter is going to canonicalize selectable cards under `.card--selectable`, these are the last two holdouts.
20 +
21 + ### 2. Wizard step headings are `<h2>` (intentional, but undocumented)
22 +
23 + Each step partial leads with `<h2>Step Title</h2>` (e.g. `steps/item/basics.html:4` "Item Basics"). The wrapping `wizard_*.html` template provides the page h1 indirectly (via the wizard wrapper's own `<h2>`). Effectively the same auth-flow pattern from Phase 6 — needs the same charter clarification: which pages use brand-h1 + subtitle-h2, which use page-h1 + section-h2 (with `.section-label`).
24 +
25 + ### 3. `doc_index.html:20` has one bare `<h2>{{ section.name }}</h2>`
26 +
27 + Within a docs landing layout. `doc.html` itself uses Markdown rendering and doesn't have this issue. Pick `.section-header` or `.section-label` and apply.
28 +
29 + ### 4. `git/repo.html` has 2 bare `<h2>`
30 +
31 + Lines 69, 76: "README", "Releases". `git/settings.html:25` has one: "Repository Settings". Add `.section-header`.
32 +
33 + ### 5. `git/settings.html:69` uses inline `onsubmit="return confirm(...)"`
34 +
35 + The only inline JS confirm flow in the source browser. Other deletes in the codebase use `hx-confirm`. Not a CSS issue — but worth standardizing on `hx-confirm` for consistency (and so screen-reader users get a more accessible affordance than the native `confirm()`).
36 +
37 + ### 6. `wizard.css` is 685 lines
38 +
39 + The remediation plan never measured wizard.css against the consolidation work. It still defines selected-state and hover recipes locally (e.g. wizard step indicator, monetization-card hover) instead of inheriting from the canonical recipes in `style.css`. After Phase 1-7 land, a wizard.css consolidation pass would close the loop.
40 +
41 + ## Verdict
42 +
43 + Wizards / docs / source browser are structurally clean. The two real items are (a) decide on the brand-h1 + subtitle-h2 pattern at the charter level — it shows up across auth, wizards, and arguably the marketing prose pages — and (b) at some point fold `wizard.css` into the canonical primitive system. Everything else is small.
44 +
45 + ---
46 +
47 + # Audit sweep complete
48 +
49 + Phases 0-8 done. The codebase is in a strong baseline state: token system in place, primitives largely canonical, the worst Phase 0 offenders (inline `<style>` blocks in `buy.html` / `item.html` / `error.html`, 1,350 inline-style attrs, checkmark glyphs, pure black/white) are resolved.
50 +
51 + ## Remediation outcomes (post-audit pass, 2026-05-20)
52 +
53 + Six items landed in the same session as the audit; two findings retracted as incorrect:
54 +
55 + - **Landed**: `pricing.html` tier-picker keyboard-accessibility fix; `.tier-option.selected` → `.is-selected`; full `<a><button>` antipattern sweep (44 sites across 13 files); four page-isolated `<style>` blocks migrated (`user.html` deleted as dead duplicate, `dashboard-user.html` scoped under `.dashboard-user-page` resolving the `.project-card` name collision with `user.html`, `library_feed.html` scoped under `.library-page`, `user_settings.html` as global primitives); follow-state synonyms (`.following`, `.is-following`) → `.is-selected` across 4 templates + 4 CSS rules (also fixed a latent bug in `follow_button.html` where a duplicate `class=` attribute meant `.is-following` never applied in the browser); `.intro` lifted to `.page-intro` primitive.
56 + - **Retracted**: `.coming-soon` modifier finding (Phase 1) — rule actually exists at `style.css:6489`, the original grep missed it. `cart-group__title` BEM `__` finding (Phase 4) — claimed it was the only BEM `__` in the codebase; actually 112 template usages across 20 block prefixes, it's an established convention.
57 +
58 + ## Remaining work after this session
59 +
60 + - **Heading-convention decision** for prose pages (creators, policy, changelog, fan_plus, stripe_disclaimer, purchase, cart, library_downloads, library_locked, doc_index, git/repo, git/settings). Phase 6 surfaced the brand-h1 + subtitle-h2 pattern as intentional design for auth and wizards; the charter needs to decide which scopes use which pattern.
61 + - **`.export-card-*` family** undocumented sub-component in `dashboard-export.html` (Phase 7).
62 + - **`.login-cta-btn`** undocumented one-off in `project.html` (Phase 3).
63 + - **`.monetization-card` / `.content-choice-card`** in `wizard.css` — last selectable-card families outside the canonical primitive system. Deferred from remediation §2.1 ("cosmetic rename" decision).
64 + - **`wizard.css` consolidation pass** (Phase 8 §6) — 685 lines still define selected-state and hover recipes locally instead of inheriting from canonical recipes.
65 + - **`git/settings.html:69`** inline `onsubmit="return confirm(...)"` — standardize on `hx-confirm`.
66 + - **Button class shorthand decision** (`<button class="primary">` vs `<button class="btn-primary">`) — both render identically; charter should pick or accept both.
67 +
68 + Remediations and outstanding charter decisions are tracked by site section in `docs/todo.md` § "UX audit remediations."
@@ -1,5 +1,50 @@
1 1 # UX Remediation Pre-Plan (pre-Phase-1)
2 2
3 + **Status (2026-05-20): bounded consolidation items complete; large migrations deferred. Macro adoption sweep and remaining CSS literal cleanup completed in a third sitting.**
4 +
5 + Done in first sitting:
6 + - **1.1 Tokens** — in `style.css:200-211`.
7 + - **1.2 Shared partials** — Askama macros defined in `partials/_ui.html` (but see third-sitting note below: at this point none were actually wired in).
8 + - **1.3 Brand fixes** — `\2713` and `&#10003;` checkmarks replaced; `.sandbox-banner` raw `#6c5ce7` / `#fff` migrated to tokens.
9 + - **2.2 Empty-state** — `.chart-empty` migrated to `.empty-state.empty-state--chart` (2 instances). `.preview-cover-empty` is a thumbnail placeholder, semantically distinct from empty-state; left alone.
10 + - **2.4 Selected-state** — all `.active` selection classes migrated to `.is-selected`; matching CSS aliases dropped. Visibility toggles (`.tab-content.active`, `.section-panel.active`, `.editor-panel.active`) intentionally retained.
11 + - **2.8 Section-header** — h2s in `item.html` already use `class="section-header"`; the inline border-bottom rule was dead code, removed with 2.11.
12 + - **2.9 Hidden utility** — 0 inline `style="display:none"` remain.
13 + - **2.11 Page-isolated `<style>` blocks** — `item.html`'s 78-line block removed entirely; all rules already covered by `.item-page`-scoped globals or co-classes (`.content-section`, `.card-muted`, `.list-row`, `.section-header`). `buy.html` and `error.html` were done previously.
14 + - **2.12 Inline styles** — 6 instances remain, all server-computed bar widths/heights (the documented permitted exception). Success criterion (<100) overwhelmingly met.
15 + - Dead button-class names removed from CSS comments (`.notify-btn`, `.paywall-btn` had no rules or template usage).
16 +
17 + Done in second sitting:
18 + - **2.6 Notification surface** — on inspection, `.toast` / `.banner` / `.alert` / `.info-box` / `.warning-box` / `.error-message` each serve a meaningfully distinct communication role; merging them would flatten useful nuance. Resolution: added a role-clarity comment block at `style.css:1183` so future authors know which to reach for. **Do not merge** these components.
19 + - **2.10 Page heading** — audited every page in `templates/pages/`. Three had no h1: `discover.html` (added `<h1>Discover</h1>`), `tag_tree.html` (added `<h1>Browse Tags</h1>`), `error.html` (promoted `.error-page-message` from `<p>` to `<h1>`). All other pages already lead with h1.
20 +
21 + Done in third sitting:
22 + - **CSS literal cleanup** — remaining `#000` / `#fff` / `#666` / `#5147e5` literals in `style.css` and `wizard.css` migrated to tokens: video-surface backgrounds → `var(--primary-dark)`, `.copy-btn` color → `var(--primary-light)` + `var(--radius-md)`, `.video-cover-placeholder` `#666` → `var(--text-muted)`, `.stripe-connect-cta` `#fff` → `var(--primary-light)` and raw hover `#5147e5` → opacity-dim recipe. The only remaining raw `#000` / `#fff` are the token definitions themselves at `style.css:155-156`.
23 + - **Empty-state macro adoption** — the `_ui.html` macros existed since the first sitting but had **zero call sites** in the codebase; "1.2 Shared partials" was marked done because the macros were defined, not because they were used. Wired in this sitting: 22 templates now call `ui::empty_state` / `empty_state_with_action` / `empty_state_compact` / `empty_state_chart` (added the compact and chart variants; made `title` optional via empty-string sentinel). Six sites left intentionally bespoke with documented reasons: `git/issues.html` (dynamic body), `git/repos.html` (nested code blocks), `discover_results.html` ×4 (inline anchor in hint), `project_settings.html` (JS-referenced id), `project_blog.html` tab (custom `.blog-empty-hint`), `project_content.html` (--lg with templated URL — Askama macro args don't accept `format!()` against template-scope fields).
24 + - **Aspirational macros dropped** — `loading_skeleton`, `form_field`, `confirm_dialog`, `pagination`, and (briefly added then unused) `empty_state_lg_action` deleted from `_ui.html`. Survey of real call sites showed none of these macro shapes fit: confirms use `hx-confirm` or full-page magic-link forms (no modal-overlay confirms); pagination uses HTMX-rich numbered ranges that the prev/next macro can't represent; form-groups carry per-field input types, classes, and attrs that the rigid macro can't express; loading-skeleton was net-new with no existing usage pattern. Better to delete than to mislead future readers into thinking these are the canonical shapes.
25 +
26 + Deferred — these turned out to be *already met at the CSS level via aliased selectors*; further "consolidation" would be cosmetic template renames with no visual benefit:
27 + - **2.1 Card family** — 22 bespoke `-card` classes, but the visual styling is already canonical: `.feature-card` / `.tier-card` / `.use-case-card` / `.fork-card` are grouped under one `.card--bordered` rule at `style.css:1035-1038`; `.stat-card` / `.analytics-card` / `.account-tip-card` are aliased to `.card-muted`. The bespoke names persist as semantic labels in templates (which is fine). Renaming templates to canonical class names is purely cosmetic.
28 + - **2.3 Button family** — same pattern: most bespoke `-btn` classes have CSS rules that are page-scoped tweaks on top of canonical button styling. The documented variants (`.big-button`, `.order-btn`, `.play-button`, `.speed-button`, `.shortcuts-help-btn`, `.toast-retry-btn`) genuinely differ from `.btn-primary` / `.btn-secondary` / `.btn-danger`. Dead names (`.notify-btn`, `.paywall-btn`) were already removed from the CSS comments.
29 + - **2.5 Hover recipe** — cross-cutting and depends on 2.3; defer with 2.3.
30 + - **2.7 Status indicator** — `.save-status` and `.notify-status` already have the proposed `.badge--inline` semantics (inline colored text, no padding bump). Adding a `.badge--inline` modifier and migrating would be a rename without visual change.
31 +
32 + ## What "consolidated" looks like now
33 +
34 + The original Phase 0 inventory was correct *as of the day it was written*, but a lot of consolidation has happened since via CSS aliasing rather than template renames. The codebase now has:
35 +
36 + - One token system (color, type, spacing, radius, shadow).
37 + - Four shared partial macros for the empty-state shape (`empty_state`, `empty_state_with_action`, `empty_state_compact`, `empty_state_chart`), wired into 22 call sites. Other aspirational macros were dropped because no real call sites matched their shape.
38 + - A canonical `.is-selected` for every selection state, with visibility toggles intentionally kept on `.active`.
39 + - One `.card-muted` rule that covers nine bespoke "muted card" names.
40 + - One `.card--bordered` rule that covers four bespoke "bordered card" names.
41 + - Six clearly-distinct notification components with documented roles.
42 + - Every page lead-heading is h1.
43 + - 6 inline `style="..."` instances, all server-computed bar dimensions (the documented exception).
44 + - No checkmark glyphs in templates or CSS. Pure `#000` / `#fff` only appear as the `--primary-dark` / `--primary-light` token definitions at `style.css:155-156`; every other use site reads through the tokens.
45 +
46 + The success criteria from Phase 0 are met. The remaining bespoke class names in templates are semantic labels riding on canonical styling, not duplicates of UI shapes.
47 +
3 48 Before running Phases 1-8 of the UX audit sweep, do a consolidation pass. The Phase 0 inventory (`phase-0.md`) showed that the platform reinvents the same UX in several different shapes — 12 card variants, 7 empty-state classes, 14+ ad-hoc button classes, five "selected" recipes. Auditing each surface against the charter is wasted effort until those duplicates are merged. After this pass, the audits become conformance checks instead of style debates.
4 49
5 50 This plan has two parts:
@@ -29,17 +74,17 @@ Add to `:root` in `static/style.css`:
29 74
30 75 Migrate existing shadow recipes onto the three tiers. Replace `0.6rem` (`wizard.css:39`) and other off-scale strays. Do not migrate every existing `4px` radius in one pass — only when touching a rule for another reason. Goal is to make the tokens authoritative going forward.
31 76
32 - ### 1.2 Build the five missing shared partials
77 + ### 1.2 Shared macros — outcome
33 78
34 - | New partial | Replaces today |
35 - |---|---|
36 - | `partials/empty_state.html` | Inline empty markup in `feed.html`, `collection.html`, `tag_tree.html`, and the seven `*-empty` classes in `style.css` |
37 - | `partials/loading_skeleton.html` (variants: row, card, list) | Nothing — net new; only `.htmx-indicator` exists |
38 - | `partials/form_field.html` (label + input slot + hint + error slot) | Hand-assembled `.form-group` blocks across every form template |
39 - | `partials/confirm_dialog.html` | Raw `.modal` markup in delete/destroy flows |
40 - | `partials/pagination.html` | Per-page raw pagination HTML; `.pagination` styles at `style.css:4111` |
79 + The original plan called for five shared partials. Result after surveying real call sites:
41 80
42 - Each partial documents its parameters in a Rust doc comment on the corresponding template struct.
81 + | Macro | Status | Notes |
82 + |---|---|---|
83 + | `empty_state` (+ `_with_action`, `_compact`, `_chart`) | **Shipped, 22 call sites** | Lives in `partials/_ui.html` as Askama macros, not standalone partials. `title` arg is optional via empty-string sentinel. Six sites stay bespoke for documented reasons (dynamic body, nested code, anchor in hint, JS-referenced id, custom hint class, templated URL). |
84 + | `loading_skeleton` | **Dropped** | Net-new with no existing pattern to absorb. |
85 + | `form_field` | **Dropped** | Real `.form-group` blocks each carry custom input type, classes, placeholders, patterns, etc. that the rigid 5-arg macro couldn't represent. |
86 + | `confirm_dialog` | **Dropped** | No realistic call sites. Every confirm flow uses `hx-confirm`, inline `confirm()`, or a full-page magic-link form — none use modal-overlay. |
87 + | `pagination` | **Dropped** | Real usages (`feed.html` numbered range, `discover_results.html` HTMX buttons with filter inclusion) are substantially richer than a prev/next macro. |
43 88
44 89 ### 1.3 Brand-conformance fixes
45 90
@@ -173,7 +218,7 @@ After step 8, Phase 1 of the audit sweep runs against the consolidated state.
173 218 - Grep for `style="` in `templates/` returns under 100 hits (down from ~1,350).
174 219 - Grep for `class=".*-card"` in `templates/` returns only `.card` and `.card--*` modifier forms (no bespoke `-card` classes).
175 220 - No checkmark glyphs or `&#10003;` entities in any template.
176 - - No `#000` / `#fff` / Bootstrap-derived literals in any CSS file.
221 + - No `#000` / `#fff` / Bootstrap-derived literals at use sites in any CSS file. The `--primary-dark` and `--primary-light` token definitions at `style.css:155-156` are the only places those literals appear.
177 222 - Every "selected" state in the UI is `.is-selected`.
178 223
179 224 Once the success criteria pass, the audit sweep can proceed. Surface findings in Phases 1-8 are then expected to be sparse and stack-specific rather than fundamental.
@@ -2,6 +2,7 @@
2 2
3 3 use axum::{
4 4 extract::{Path, State},
5 + http::StatusCode,
5 6 response::{IntoResponse, Redirect},
6 7 Form,
7 8 };
@@ -125,5 +126,9 @@ pub(super) async fn repo_settings_delete(
125 126
126 127 db::git_repos::delete_repo(&state.db, resolved.db_repo.id).await?;
127 128
128 - Ok(Redirect::to(&format!("/git/{}", owner)))
129 + Ok((
130 + StatusCode::OK,
131 + [("HX-Redirect", format!("/git/{}", owner))],
132 + "",
133 + ))
129 134 }
@@ -29,7 +29,7 @@ function openCollectionPicker(itemId, anchor, opts) {
29 29 + '<div class="collection-picker-create">'
30 30 + '<form>'
31 31 + '<input type="text" name="title" placeholder="New collection" required maxlength="100" autocomplete="off">'
32 - + '<button class="secondary" type="submit">Create</button>'
32 + + '<button class="btn-secondary" type="submit">Create</button>'
33 33 + '</form></div>';
34 34
35 35 wrapper.appendChild(picker);
@@ -62,8 +62,8 @@
62 62 '<input type="text" class="bundle-new-title" placeholder="Item name" autocomplete="off">' +
63 63 '<input type="text" class="bundle-new-desc" placeholder="Description (optional)" autocomplete="off">' +
64 64 '<div style="display: flex; gap: 0.25rem;">' +
65 - '<button type="button" class="primary bundle-create-btn" style="padding: 0.3rem 0.7rem; font-size: 0.85rem;">Create</button>' +
66 - '<button type="button" class="secondary bundle-cancel-btn" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;">Cancel</button>' +
65 + '<button type="button" class="btn-primary bundle-create-btn" style="padding: 0.3rem 0.7rem; font-size: 0.85rem;">Create</button>' +
66 + '<button type="button" class="btn-secondary bundle-cancel-btn" style="padding: 0.3rem 0.5rem; font-size: 0.85rem;">Cancel</button>' +
67 67 '</div>';
68 68 container.appendChild(row);
69 69
@@ -106,7 +106,7 @@
106 106 '<td style="padding: 0.5rem 0.5rem 0.5rem 0;"><a href="/dashboard/item/' + data.item_id + '">' + escapeHtml(data.title) + '</a></td>' +
107 107 '<td style="padding: 0.5rem; font-size: 0.85rem; opacity: 0.8;">' + escapeHtml(desc) + '</td>' +
108 108 '<td style="padding: 0.5rem; font-size: 0.85rem;"><a href="/dashboard/item/' + data.item_id + '" style="font-size: 0.8rem;">Manage files</a></td>' +
109 - '<td style="padding: 0.5rem;"><button type="button" class="secondary bundle-remove-btn" data-child-id="' + data.item_id + '" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">Remove</button></td>';
109 + '<td style="padding: 0.5rem;"><button type="button" class="btn-secondary bundle-remove-btn" data-child-id="' + data.item_id + '" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">Remove</button></td>';
110 110 tbody.appendChild(tr);
111 111 attachBundleRowHandlers(tr, bundleId);
112 112 updateBundleCount(1);
@@ -191,8 +191,8 @@
191 191 row.innerHTML =
192 192 '<span style="flex:1;font-weight:bold;">' + escapeHtml(sec.title) + '</span>' +
193 193 '<span style="font-size:0.8rem;opacity:0.6;">' + (sec.body || '').length + ' chars</span>' +
194 - '<button type="button" class="secondary section-edit-btn" data-id="' + sec.id + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Edit</button>' +
195 - '<button type="button" class="secondary section-del-btn" data-id="' + sec.id + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Delete</button>';
194 + '<button type="button" class="btn-secondary section-edit-btn" data-id="' + sec.id + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Edit</button>' +
195 + '<button type="button" class="btn-secondary section-del-btn" data-id="' + sec.id + '" style="padding:0.25rem 0.6rem;font-size:0.8rem;">Delete</button>';
196 196 document.getElementById('sections-list').appendChild(row);
197 197 attachSectionRowHandlers(row, itemId);
198 198 updateSectionCount(1);
@@ -180,7 +180,7 @@
180 180 tr.innerHTML =
181 181 '<td style="padding: 0.4rem 0.5rem 0.4rem 0; font-size: 0.85rem; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="' + file.name.replace(/"/g, '&quot;') + '">' + escapeHtml(file.name) + '</td>' +
182 182 '<td style="padding: 0.4rem 0.5rem;"><input type="text" class="version-label-input" data-idx="' + idx + '" value="' + escapeAttr(guessLabel(file.name)) + '" placeholder="e.g., macOS (arm)" style="width: 100%; padding: 0.25rem 0.4rem; font-size: 0.85rem;"></td>' +
183 - '<td style="padding: 0.4rem 0.5rem;"><button type="button" class="secondary version-remove-file" data-idx="' + idx + '" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">Remove</button></td>';
183 + '<td style="padding: 0.4rem 0.5rem;"><button type="button" class="btn-secondary version-remove-file" data-idx="' + idx + '" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">Remove</button></td>';
184 184 fileRows.appendChild(tr);
185 185
186 186 tr.querySelector('.version-remove-file').addEventListener('click', function() {
@@ -91,11 +91,11 @@
91 91 row.className = 'psection-row';
92 92 row.dataset.id = sec.id;
93 93 row.innerHTML =
94 - '<span class="psection-row__title">' + escapeHtml(sec.title) + '</span>' +
95 - '<code class="psection-row__anchor">#section-' + escapeHtml(sec.slug) + '</code>' +
96 - '<span class="psection-row__length">' + (sec.body || '').length + ' chars</span>' +
97 - '<button type="button" class="secondary psection-edit-btn" data-id="' + sec.id + '" data-title="' + escapeHtml(sec.title) + '">Edit</button>' +
98 - '<button type="button" class="secondary psection-del-btn" data-id="' + sec.id + '">Delete</button>';
94 + '<span class="psection-row-title">' + escapeHtml(sec.title) + '</span>' +
95 + '<code class="psection-row-anchor">#section-' + escapeHtml(sec.slug) + '</code>' +
96 + '<span class="psection-row-length">' + (sec.body || '').length + ' chars</span>' +
97 + '<button type="button" class="btn-secondary psection-edit-btn" data-id="' + sec.id + '" data-title="' + escapeHtml(sec.title) + '">Edit</button>' +
98 + '<button type="button" class="btn-secondary psection-del-btn" data-id="' + sec.id + '">Delete</button>';
99 99 list.appendChild(row);
100 100 var hidden = document.createElement('textarea');
101 101 hidden.className = 'hidden';
@@ -132,9 +132,9 @@
132 132 .then(function(sec) {
133 133 var row = document.querySelector('.psection-row[data-id="' + id + '"]');
134 134 if (row) {
135 - row.querySelector('.psection-row__title').textContent = sec.title;
136 - row.querySelector('.psection-row__anchor').textContent = '#section-' + sec.slug;
137 - row.querySelector('.psection-row__length').textContent = (sec.body || '').length + ' chars';
135 + row.querySelector('.psection-row-title').textContent = sec.title;
136 + row.querySelector('.psection-row-anchor').textContent = '#section-' + sec.slug;
137 + row.querySelector('.psection-row-length').textContent = (sec.body || '').length + ' chars';
138 138 row.querySelector('.psection-edit-btn').dataset.title = sec.title;
139 139 }
140 140 var hidden = document.querySelector('textarea[data-body-for="' + id + '"]');
@@ -35,7 +35,7 @@
35 35 .container + .container--narrow / --medium / --wide
36 36 .stack-row + --bordered / --tight / --top — toolbar / header bar
37 37 .field-row + .form-group.is-grow — inline form row
38 - .list-row + .list-row__title — vertical item list
38 + .list-row + .list-row-title — vertical item list
39 39 .form-row — 2-col grid form
40 40 .cover-row + .cover-thumb + .cover-empty — image picker
41 41
@@ -271,6 +271,39 @@ h3 {
271 271 color: var(--detail);
272 272 }
273 273
274 + /* Heading classes (charter: docs/design-system.md — every h1/h2 in a
275 + template must carry one of these so the role is explicit).
276 +
277 + .brand-h1 — the "Makenot.work" wordmark on auth/wizard pages.
278 + .page-title — page-level h1 (Young Serif, centered).
279 + .subtitle-h2 — page subtitle h2 used under .brand-h1 in auth/wizards.
280 + .subsection-title — h2 inside dashboards, tabs, and prose partials
281 + (mono, no border — the default-h2 role made explicit).
282 + .section-header — h2 prose sub-section heading with bottom border
283 + (existing rule, see SECTIONS block). */
284 + .brand-h1,
285 + .page-title {
286 + font-family: var(--font-heading);
287 + font-weight: normal;
288 + color: var(--detail);
289 + text-align: center;
290 + font-size: 2.5rem;
291 + }
292 +
293 + .subtitle-h2 {
294 + font-family: var(--font-mono);
295 + font-weight: normal;
296 + color: var(--detail);
297 + text-align: center;
298 + font-size: 1.5rem;
299 + }
300 +
301 + .subsection-title {
302 + font-family: var(--font-mono);
303 + font-weight: normal;
304 + color: var(--detail);
305 + }
306 +
274 307 p {
275 308 font-family: var(--font-body);
276 309 color: var(--detail);
@@ -333,7 +366,6 @@ button:hover {
333 366 opacity: 0.8;
334 367 }
335 368
336 - button.primary,
337 369 .btn-primary {
338 370 background: var(--primary-dark);
339 371 color: var(--primary-light);
@@ -343,14 +375,15 @@ button.primary,
343 375 font-size: 1rem;
344 376 cursor: pointer;
345 377 transition: opacity 0.2s ease;
378 + display: inline-block;
379 + text-decoration: none;
346 380 }
347 381
348 - button.primary:hover,
349 382 .btn-primary:hover {
350 383 opacity: 0.8;
384 + text-decoration: none;
351 385 }
352 386
353 - button.secondary,
354 387 .btn-secondary {
355 388 background: var(--surface-muted);
356 389 color: var(--detail);
@@ -360,14 +393,15 @@ button.secondary,
360 393 font-size: 1rem;
361 394 cursor: pointer;
362 395 transition: opacity 0.2s ease;
396 + display: inline-block;
397 + text-decoration: none;
363 398 }
364 399
365 - button.secondary:hover,
366 400 .btn-secondary:hover {
367 401 opacity: 0.8;
402 + text-decoration: none;
368 403 }
369 404
370 - button.danger,
371 405 .btn-danger {
372 406 background: var(--danger);
373 407 color: var(--primary-light);
@@ -377,11 +411,13 @@ button.danger,
377 411 font-size: 1rem;
378 412 cursor: pointer;
379 413 transition: opacity 0.2s ease;
414 + display: inline-block;
415 + text-decoration: none;
380 416 }
381 417
382 - button.danger:hover,
383 418 .btn-danger:hover {
384 419 opacity: 0.8;
420 + text-decoration: none;
385 421 }
386 422
387 423 /* Disabled state for all button variants (charter rule).
@@ -407,8 +443,6 @@ button[aria-disabled="true"],
407 443 Some surfaces have tuned button variants that are NOT compositions
408 444 of these modifiers and keep their own classes:
409 445 .big-button — Young Serif 200×60 anchor on splash pages.
410 - .paywall-btn — wider primary CTA (anchor) on paywalls.
411 - .notify-btn — small inverted notify pill in dashboards.
412 446 .order-btn — list reorder up/down (with active flash).
413 447 .play-button — circular media-player play control.
414 448 .speed-button — segment in media-player speed group.
@@ -1182,6 +1216,21 @@ form button:hover {
1182 1216 INFO & WARNING BOXES
1183 1217 =========================================== */
1184 1218
1219 + /* Notification surface roles (charter: docs/design-system.md).
1220 + Five distinct components — keep them distinct, don't merge:
1221 + .toast — transient, JS-dismissible, bottom-right corner.
1222 + .banner — full-bleed page-top notice (sandbox, restart,
1223 + founder pricing). One short sentence + optional link.
1224 + .alert — inline directive callout with uppercase mono title
1225 + (NOTE / TIP / IMPORTANT / WARNING / CAUTION). Used in
1226 + docs and longer-form bodies.
1227 + .info-box — informational headed block with h3 + bullet list
1228 + inside forms / wizards (distinct register from .alert).
1229 + .warning-box — loud yellow-on-yellow attention-grabber for
1230 + page-level warnings (delete confirms, account
1231 + warnings). Louder than .alert-warning intentionally.
1232 + .error-message — inline form-field error; hidden by default, shown
1233 + with .is-active. Distinct from page-level .alert. */
1185 1234 .info-box {
1186 1235 background: var(--surface-muted);
1187 1236 padding: 1rem;
@@ -1526,13 +1575,13 @@ form button:hover {
1526 1575 opacity: 0.5;
1527 1576 }
1528 1577
1529 - .item-page button.primary {
1578 + .item-page .btn-primary {
1530 1579 width: 100%;
1531 1580 padding: var(--space-4) var(--space-6);
1532 1581 font-size: 1.1rem;
1533 1582 }
1534 1583
1535 - .item-page button.secondary {
1584 + .item-page .btn-secondary {
1536 1585 width: 100%;
1537 1586 margin-top: var(--space-2);
1538 1587 }
@@ -1751,8 +1800,8 @@ form button:hover {
1751 1800
1752 1801 .project-page .store-footer a { color: var(--detail); }
1753 1802
1754 - .project-page .item-content button.primary,
1755 - .project-page .item-content button.secondary {
1803 + .project-page .item-content .btn-primary,
1804 + .project-page .item-content .btn-secondary {
1756 1805 width: 100%;
1757 1806 margin-top: var(--space-4);
1758 1807 }
@@ -1796,7 +1845,8 @@ form button:hover {
1796 1845 }
1797 1846
1798 1847 .project-page .tier-card form { margin-top: auto; }
1799 - .project-page .tier-card button { width: 100%; }
1848 + .project-page .tier-card button,
1849 + .project-page .tier-card .btn-primary { width: 100%; }
1800 1850
1801 1851 .project-page .tier-active-badge {
1802 1852 font-size: 0.85rem;
@@ -2600,7 +2650,12 @@ form button:hover {
2600 2650 .delete-account-page .form-status.error { color: var(--danger); }
2601 2651 .delete-account-page .form-status.success { color: var(--success); }
2602 2652
2603 - /* Export data page (templates/dashboards/dashboard-export.html). */
2653 + /* Export data page (templates/dashboards/dashboard-export.html).
2654 + `.export-card` and its inner classes (`-info`, `-title`, `-desc`, `-meta`)
2655 + are intentionally page-scoped: this is a row-layout card (info on left,
2656 + action button on right, flex justify-between) that doesn't fit `.card-muted`
2657 + cleanly. Background uses `--light-background` rather than `--surface-muted`
2658 + for visual distinction from the surrounding dashboard surface. */
2604 2659
2605 2660 .export-page .export-cards {
2606 2661 display: grid;
@@ -2724,11 +2779,6 @@ form button:hover {
2724 2779 .fan-plus-page h1 { font-size: 2.5rem; margin-bottom: var(--space-2); }
2725 2780 .fan-plus-page h2 { font-size: 1.5rem; margin-top: var(--space-6); margin-bottom: var(--space-4); }
2726 2781
2727 - .fan-plus-page .intro {
2728 - font-size: 1.1rem;
2729 - margin-bottom: var(--space-6);
2730 - line-height: 1.6;
2731 - }
2732 2782
2733 2783 .fan-plus-page .fan-plus-section { margin-bottom: var(--space-6); line-height: 1.7; }
2734 2784 .fan-plus-page .fan-plus-section ul { padding-left: var(--space-5); margin: var(--space-3) 0; }
@@ -2862,8 +2912,8 @@ form button:hover {
2862 2912 .purchase-page .payment-section { margin-bottom: var(--space-5); }
2863 2913 .purchase-page .payment-section h2 { font-size: 1.1rem; margin-bottom: var(--space-4); }
2864 2914
2865 - .purchase-page button.primary { width: 100%; }
2866 - .purchase-page button.secondary { width: 100%; margin-top: var(--space-4); }
2915 + .purchase-page .btn-primary { width: 100%; }
2916 + .purchase-page .btn-secondary { width: 100%; margin-top: var(--space-4); }
2867 2917
2868 2918 .purchase-page .security-note {
2869 2919 font-size: 0.85rem;
@@ -3033,6 +3083,13 @@ form button:hover {
3033 3083 text-align: left;
3034 3084 }
3035 3085
3086 + /* Large intro paragraph under a page h1 (creators, policy, changelog, fan_plus). */
3087 + .page-intro {
3088 + font-size: 1.1rem;
3089 + margin-bottom: var(--space-6);
3090 + line-height: 1.6;
3091 + }
3092 +
3036 3093 .subscription-note p {
3037 3094 opacity: 0.7;
3038 3095 font-size: 0.9rem;
@@ -3055,9 +3112,9 @@ form button:hover {
3055 3112
3056 3113 /* psection-* mirrors section-mgmt-* (used by item_details) but psection
3057 3114 class hooks are also queried by static/project-sections.js. */
3058 - .psection-row__title { flex: 1; font-weight: bold; }
3059 - .psection-row__anchor { font-size: 0.75rem; opacity: 0.6; }
3060 - .psection-row__length { font-size: 0.8rem; opacity: 0.6; }
3115 + .psection-row-title { flex: 1; font-weight: bold; }
3116 + .psection-row-anchor { font-size: 0.75rem; opacity: 0.6; }
3117 + .psection-row-length { font-size: 0.8rem; opacity: 0.6; }
3061 3118
3062 3119 .psection-edit-btn,
3063 3120 .psection-del-btn {
@@ -3090,18 +3147,18 @@ form button:hover {
3090 3147 text-align: center;
3091 3148 padding: 3rem 1rem;
3092 3149 }
3093 - .cart-empty__hint {
3150 + .cart-empty-hint {
3094 3151 font-size: 0.9rem;
3095 3152 opacity: 0.7;
3096 3153 margin-bottom: var(--space-5);
3097 3154 }
3098 3155
3099 3156 /* Multi-creator summary bar (only renders when seller_groups.len() > 1). */
3100 - .cart-multi-bar__note {
3157 + .cart-multi-bar-note {
3101 3158 opacity: 0.7;
3102 3159 margin-left: var(--space-2);
3103 3160 }
3104 - .cart-multi-bar__form {
3161 + .cart-multi-bar-form {
3105 3162 display: flex;
3106 3163 align-items: center;
3107 3164 gap: var(--space-3);
@@ -3115,8 +3172,8 @@ form button:hover {
3115 3172
3116 3173 /* Per-seller group card. */
3117 3174 .cart-group { margin-bottom: var(--space-6); }
3118 - .cart-group__title { margin-bottom: 0.25rem; }
3119 - .cart-group__count {
3175 + .cart-group-title { margin-bottom: 0.25rem; }
3176 + .cart-group-count {
3120 3177 font-size: 0.85rem;
3121 3178 opacity: 0.6;
3122 3179 margin-bottom: var(--space-4);
@@ -3134,7 +3191,7 @@ form button:hover {
3134 3191 /* Compact action button used in cart/wishlist tables. */
3135 3192
3136 3193 /* Bottom summary row of each seller group. */
3137 - .cart-group__summary {
3194 + .cart-group-summary {
3138 3195 margin-top: var(--space-4);
3139 3196 display: flex;
3140 3197 justify-content: space-between;
@@ -3208,16 +3265,16 @@ form button:hover {
3208 3265 margin-bottom: var(--space-2);
3209 3266 }
3210 3267 .dns-row:last-of-type { margin-bottom: 0; }
3211 - .dns-row__label {
3268 + .dns-row-label {
3212 3269 font-size: 0.8rem;
3213 3270 opacity: 0.7;
3214 3271 min-width: 70px;
3215 3272 }
3216 - .dns-row__value {
3273 + .dns-row-value {
3217 3274 font-size: 0.85rem;
3218 3275 flex: 1;
3219 3276 }
3220 - .dns-row__copy {
3277 + .dns-row-copy {
3221 3278 padding: 0.15rem 0.5rem;
3222 3279 font-size: 0.75rem;
3223 3280 }
@@ -3437,12 +3494,6 @@ form button:hover {
3437 3494 .creators-page h1 { font-size: 2.5rem; margin-bottom: var(--space-2); }
3438 3495 .creators-page h2 { font-size: 1.5rem; margin-top: var(--space-6); margin-bottom: var(--space-4); }
3439 3496
3440 - .creators-page .intro {
3441 - font-size: 1.1rem;
3442 - margin-bottom: var(--space-6);
3443 - line-height: 1.6;
3444 - }
3445 -
3446 3497 .creators-page .how-it-works { margin-bottom: var(--space-6); }
3447 3498 .creators-page .how-it-works ol { padding-left: var(--space-5); line-height: 1.8; }
3448 3499
@@ -3724,10 +3775,117 @@ form button:hover {
3724 3775 }
3725 3776 .health-page .subtitle { text-align: center; }
3726 3777
3727 - /* User-level dashboard (templates/dashboards/dashboard-user.html). */
3728 - .stat-value { font-size: 2rem; margin-bottom: var(--space-1); }
3778 + /* User Settings tab (templates/partials/tabs/user_settings.html).
3779 + Left-rail nav layout for the Settings dashboard tab. Selection uses the
3780 + canonical .is-selected modifier. */
3781 + .settings-layout { display: flex; gap: var(--space-6); }
3782 + .settings-nav { flex-shrink: 0; min-width: 140px; }
3783 + .settings-nav a {
3784 + display: block;
3785 + padding: var(--space-2) var(--space-3);
3786 + text-decoration: none;
3787 + color: var(--detail);
3788 + opacity: 0.6;
3789 + font-family: var(--font-mono);
3790 + font-size: 0.9rem;
3791 + transition: opacity 0.15s ease;
3792 + }
3793 + .settings-nav a:hover { opacity: 1; }
3794 + .settings-nav a.is-selected { opacity: 1; background: var(--surface-muted); }
3795 + .settings-body { flex: 1; min-width: 0; }
3796 + @media (max-width: 768px) {
3797 + .settings-layout { flex-direction: column; gap: var(--space-4); }
3798 + .settings-nav { display: flex; flex-wrap: wrap; gap: 0; min-width: 0; }
3799 + .settings-nav a { padding: 0.4rem 0.6rem; font-size: 0.85rem; }
3800 + }
3801 +
3802 + /* Library "Feed" tab (templates/partials/tabs/library_feed.html).
3803 + Scoped under .library-page so rules don't bleed onto the standalone
3804 + /feed page (which has its own .feed-page-scoped rules). */
3805 + .library-page .feed-meta { font-size: 0.8rem; opacity: 0.6; margin-bottom: var(--space-3); }
3806 + .library-page .feed-table-header {
3807 + display: grid;
3808 + grid-template-columns: 50px 1fr 100px 70px 70px;
3809 + gap: var(--space-2);
3810 + padding: var(--space-2) var(--space-3);
3811 + background: var(--surface-alt);
3812 + font-size: 0.75rem;
3813 + opacity: 0.7;
3814 + text-transform: uppercase;
3815 + letter-spacing: 0.03em;
3816 + }
3817 + .library-page .feed-col-right { text-align: right; }
3818 + .library-page .feed-results-table { border: 1px solid var(--border); border-top: none; }
3819 + .library-page .feed-table-row {
3820 + display: grid;
3821 + grid-template-columns: 50px 1fr 100px 70px 70px;
3822 + gap: var(--space-2);
3823 + padding: 0.4rem var(--space-3);
3824 + align-items: center;
3825 + text-decoration: none;
3826 + color: var(--detail);
3827 + font-size: 0.85rem;
3828 + border-bottom: 1px solid var(--border);
3829 + transition: background 0.1s ease;
3830 + }
3831 + .library-page .feed-table-row:last-child { border-bottom: none; }
3832 + .library-page .feed-table-row:nth-child(odd) { background: var(--light-background); }
3833 + .library-page .feed-table-row:nth-child(even) { background: var(--surface-alt); }
3834 + .library-page .feed-table-row:hover { background: var(--surface-muted); }
3835 + .library-page .feed-item-name-cell {
3836 + display: flex;
3837 + flex-direction: column;
3838 + gap: 0.1rem;
3839 + min-width: 0;
3840 + }
3841 + .library-page .feed-item-name {
3842 + white-space: nowrap;
3843 + overflow: hidden;
3844 + text-overflow: ellipsis;
3845 + }
3846 + .library-page .feed-item-creator { font-size: 0.75rem; opacity: 0.5; }
3847 + .library-page .feed-pagination {
3848 + display: flex;
3849 + gap: var(--space-1);
3850 + justify-content: center;
3851 + margin-top: var(--space-4);
3852 + }
3853 + .library-page .feed-pagination a,
3854 + .library-page .feed-pagination span {
3855 + padding: 0.4rem 0.7rem;
3856 + font-size: 0.85rem;
3857 + text-decoration: none;
3858 + color: var(--detail);
3859 + border: 1px solid var(--border);
3860 + }
3861 + .library-page .feed-pagination span.current {
3862 + background: var(--primary-dark);
3863 + color: var(--primary-light);
3864 + border-color: var(--primary-dark);
3865 + }
3866 + .library-page .feed-pagination a:hover { background: var(--surface-muted); }
3867 + @media (max-width: 600px) {
3868 + .library-page .feed-table-header,
3869 + .library-page .feed-table-row {
3870 + grid-template-columns: 1fr 70px;
3871 + }
3872 + .library-page .feed-table-header span:nth-child(1),
3873 + .library-page .feed-table-row .badge:first-child,
3874 + .library-page .feed-table-header span:nth-child(3),
3875 + .library-page .feed-table-header span:nth-child(4),
3876 + .library-page .feed-table-row span:nth-child(3),
3877 + .library-page .feed-table-row span:nth-child(4) {
3878 + display: none;
3879 + }
3880 + }
3881 +
3882 + /* User-level dashboard (templates/dashboards/dashboard-user.html).
3883 + Scoped under .dashboard-user-page to avoid colliding with .project-card
3884 + / .project-title / .project-meta on the public profile (user.html) and
3885 + the user_projects tab partial. */
3886 + .dashboard-user-page .stat-value { font-size: 2rem; margin-bottom: var(--space-1); }
3729 3887
3730 - .project-card {
3888 + .dashboard-user-page .project-card {
3731 3889 background: var(--light-background);
3732 3890 padding: var(--space-5);
3733 3891 margin-bottom: var(--space-4);
@@ -3736,33 +3894,33 @@ form button:hover {
3736 3894 align-items: flex-start;
3737 3895 }
3738 3896
3739 - .project-info { flex: 1; }
3897 + .dashboard-user-page .project-info { flex: 1; }
3740 3898
3741 - .project-title {
3899 + .dashboard-user-page .project-title {
3742 3900 font-family: var(--font-heading);
3743 3901 font-weight: bold;
3744 3902 font-size: 1.2rem;
3745 3903 margin-bottom: var(--space-1);
3746 3904 }
3747 3905
3748 - .project-meta {
3906 + .dashboard-user-page .project-meta {
3749 3907 font-size: 0.85rem;
3750 3908 opacity: 0.7;
3751 3909 margin-bottom: var(--space-2);
3752 3910 }
3753 3911
3754 - .project-stats { font-size: 0.9rem; }
3755 - .project-actions {
3912 + .dashboard-user-page .project-stats { font-size: 0.9rem; }
3913 + .dashboard-user-page .project-actions {
3756 3914 display: flex;
3757 3915 gap: var(--space-2);
3758 3916 }
3759 3917
3760 - .project-actions button {
3918 + .dashboard-user-page .project-actions button {
3761 3919 padding: 0.4rem 0.8rem;
3762 3920 font-size: 0.85rem;
3763 3921 }
3764 3922
3765 - .summary-row {
3923 + .dashboard-user-page .summary-row {
3766 3924 background: var(--surface-muted);
3767 3925 padding: var(--space-4);
3768 3926 margin-top: var(--space-4);
@@ -3770,8 +3928,8 @@ form button:hover {
3770 3928 }
3771 3929
3772 3930 @media (max-width: 768px) {
3773 - .project-card { flex-direction: column; }
3774 - .project-actions { margin-top: var(--space-3); }
3931 + .dashboard-user-page .project-card { flex-direction: column; }
3932 + .dashboard-user-page .project-actions { margin-top: var(--space-3); }
3775 3933 }
3776 3934
3777 3935 /* Project dashboard (templates/dashboards/dashboard-project.html). */
@@ -3859,11 +4017,6 @@ form button:hover {
3859 4017 /* Changelog page (templates/pages/changelog.html). */
3860 4018 .changelog-page .container { max-width: 900px; margin: 0 auto; }
3861 4019 .changelog-page h1 { font-size: 2.5rem; margin-bottom: var(--space-2); }
3862 - .changelog-page .intro {
3863 - font-size: 1.1rem;
3864 - margin-bottom: var(--space-6);
3865 - line-height: 1.6;
3866 - }
3867 4020 .changelog-page .changelog-entry {
3868 4021 margin-bottom: 2.5rem;
3869 4022 padding-bottom: var(--space-6);
@@ -3892,11 +4045,6 @@ form button:hover {
3892 4045 margin-top: var(--space-6);
3893 4046 margin-bottom: var(--space-4);
3894 4047 }
3895 - .policy-page .intro {
3896 - font-size: 1.1rem;
3897 - margin-bottom: var(--space-6);
3898 - line-height: 1.6;
3899 - }
3900 4048 .policy-page .policy-section { margin-bottom: var(--space-6); line-height: 1.7; }
3901 4049 .policy-page .policy-section ul {
3902 4050 padding-left: var(--space-5);
@@ -4557,7 +4705,6 @@ footer {
4557 4705 opacity: 0.6;
4558 4706 }
4559 4707
4560 - .time-selector button.active,
4561 4708 .time-selector button.is-selected {
4562 4709 opacity: 1;
4563 4710 background: var(--primary-dark);
@@ -4952,11 +5099,10 @@ textarea:focus-visible {
4952 5099
4953 5100 /* Canonical "selected" recipe (charter: docs/design-system.md).
4954 5101 Apply `.is-selected` to any interactive container to mark it as the
4955 - currently-selected option. Component-specific rules below (`.tab`,
4956 - `.filter-item`, `.view-btn`, `.type-card`/`.pricing-card` :checked)
4957 - also accept `.is-selected` as an alias for `.active` so templates can
4958 - migrate incrementally. `.badge.active` is intentionally NOT aliased
Lines truncated
@@ -186,37 +186,9 @@
186 186 border-top: 1px solid var(--border);
187 187 }
188 188
189 - /* Monetization Cards */
190 -
191 - .monetization-cards {
192 - display: grid;
193 - grid-template-columns: 1fr 1fr;
194 - gap: 1rem;
195 - margin-bottom: 1.5rem;
196 - }
197 -
198 - .monetization-card {
199 - background: var(--background);
200 - padding: 1.25rem;
201 - border: 1px solid var(--border);
202 - border-radius: 4px;
203 - }
204 -
205 - .monetization-card h3 {
206 - font-family: var(--font-mono);
207 - font-size: 0.95rem;
208 - margin-bottom: 0.5rem;
209 - }
210 -
211 - .monetization-card p {
212 - font-size: 0.85rem;
213 - color: var(--text-muted);
214 - margin-bottom: 0.5rem;
215 - }
216 -
217 - .monetization-card.info-only {
218 - opacity: 0.85;
219 - }
189 + /* (`.monetization-card{,-cards,.info-only}` removed 2026-05-20 — dead CSS,
190 + no template references. Selectable monetization choices use the canonical
191 + `.card--selectable` recipe in style.css.) */
220 192
221 193 .tier-row {
222 194 display: flex;
@@ -245,9 +217,8 @@
245 217 margin-bottom: 1.5rem;
246 218 }
247 219
248 - /* (.pricing-card and .content-choice-card primitives live in style.css under
249 - the .card--selectable canonical recipe. The "muted" pointer-events override
250 - and content-choice anchor-link styling remain here.) */
220 + /* (`.pricing-card`, `.content-choice-card`, and the `.is-disabled` modifier
221 + all live in style.css under the canonical `.card--selectable` recipe.) */
251 222 .pricing-fields {
252 223 margin-top: 1rem;
253 224 }
@@ -259,16 +230,6 @@
259 230 margin-bottom: 1.5rem;
260 231 }
261 232
262 - .content-choice-card {
263 - text-decoration: none;
264 - color: inherit;
265 - }
266 -
267 - .content-choice-card.muted {
268 - opacity: 0.7;
269 - pointer-events: none;
270 - }
271 -
272 233 /* Preview Summary */
273 234
274 235 .preview-summary {
@@ -544,7 +505,6 @@
544 505 white-space: nowrap;
545 506 }
546 507
547 - .monetization-cards,
548 508 .pricing-cards,
549 509 .content-choice-cards {
550 510 grid-template-columns: 1fr;
@@ -620,9 +580,6 @@
620 580 object-fit: cover;
621 581 }
622 582
623 - /* Native file input hidden behind a styled "Choose File" button. */
624 - .wizard-file-input { display: none; }
625 -
626 583 /* Stripe-step layout. */
627 584 .stripe-connect-box {
628 585 background: var(--surface-muted);
@@ -646,11 +603,11 @@
646 603 text-decoration: none;
647 604 background: var(--stripe);
648 605 border: none;
649 - color: #fff;
606 + color: var(--primary-light);
650 607 }
651 608
652 609 .stripe-connect-cta:hover {
653 - background: #5147e5;
610 + opacity: 0.85;
654 611 }
655 612
656 613 .stripe-connect-note {
@@ -677,7 +634,7 @@
677 634 }
678 635
679 636 .benefit-item::before {
680 - content: "\2713";
637 + content: "•";
681 638 position: absolute;
682 639 left: 0;
683 640 color: var(--success);