max / makenotwork
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 `✓` 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 `✓` 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, '"') + '">' + 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); |