Skip to content

feat(ui): design refresh — token system, Cooklang palette, Primer structure - #456

Open
dubadub wants to merge 42 commits into
mainfrom
design/web-ui-refresh
Open

feat(ui): design refresh — token system, Cooklang palette, Primer structure#456
dubadub wants to merge 42 commits into
mainfrom
design/web-ui-refresh

Conversation

@dubadub

@dubadub dubadub commented Aug 17, 2026

Copy link
Copy Markdown
Member

Rebuilds the web UI on a token-based design system, adopts the Cooklang design system palette, and adopts GitHub Primer's structural conventions: flat surfaces, hairline borders, almost no elevation.

The original goal was making the recipe page usable on a tablet propped in a kitchen. That drove the density work; the design-system and palette work followed from it.

Measured, at 820px

before after
app bar height 80–100px 48px
steps visible without scrolling ~2.5 ~6
recipe index card height ~190px 59px

Foundation

A seventeen-token contract in input.css--bg, --surface, --text, --accent, and so on — registered with Tailwind via @theme inline so the generated utilities reference the custom properties and flip under .dark with no override rules. components.css holds the component vocabulary; every colour in the UI resolves through a token. No raw hex outside @media print.

This replaced a ~330-line block of .dark .* overrides in base.html that had to be hand-maintained in parallel with every component.

Deleted: custom-styles.css (146 lines, silently overrode output.css with duplicate class definitions — including a .btn-primary that produced a gradient at rest and a flat fill on hover) and styles.css (444 lines, referenced by nothing).

Colour

Uses the Cooklang design system palette. #16161D text, #715329 tag text, #E15A29 controls and icons, #F5DACF UI One are used exactly as specified.

Two values are darkened, hue preserved, because both are used as body text and both fail AA on a light ground as given: Text/Secondary #817B6D (3.7:1) and Text/Warning #FF3B30 (3.0:1).

The design system supplies Light only; Dark is derived in the same warm family, using the system's own #16161D as the canvas.

Two things the palette forced that are worth having independently:

  • Recipe links were blue and cookware was green. The design system contains neither. Links moved to the dark end of the DS orange; cookware dropped colour entirely for a dotted underline. Brown-vs-green was the exact confusable pair for red-green colour deficiency, so the ingredient/cookware distinction had been invisible to roughly 8% of men. Both cues are now non-hue.
  • cooking-mode.css was never migrated and was still hardcoded slate-navy with orange/amber gradients, including a heading whose colour came from a gradient clipped to text — invisible wherever the clip is unsupported. Now on tokens.

Typography

An audit across five pages found nine font sizes and six weights, with no scale behind either — six of the sizes sat inside a 2.5px band, below the threshold at which a reader can tell two sizes apart. Replaced with seven steps, each mapped to one job and each carrying its own line-height:

step size line-height role
display 24px 1.25 page title
title 18px 1.35 section headings
read 16px 1.6 recipe step text
body 14px 1.5 list rows, card titles
ui 13px 1.4 buttons, inputs, metadata
meta 12px 1.4 captions, counts, tags
label 11px 1.3 uppercase section labels

title is new: section headings had been set at 15, 16, 17, 19 and 20px in different templates — five sizes doing one job.

Line-height had been set in exactly two places in the entire component layer, so most text ran on browser or Tailwind defaults. Weights 550 and 650 are gone: they do render on macOS's variable SF Pro, but 600→650 is a ~1% change in advance width and snaps to a neighbour wherever the system font is not variable.

Tailwind's text-xs/sm/base/lg/2xl are aliased onto the scale rather than left beside it — their sizes already coincided but their line-heights did not, so the same visual size carried two different leadings depending on which name an author reached for. Aliasing fixes ~80 call sites without markup churn.

Measure: step text ran to 84 characters per line on a wide viewport. Capped at 68ch, which measures 74 and tracks font size rather than freezing a pixel width.

Heading semantics, which a visual audit does not surface: the recipes index went h1 → h3, skipping a level, and the shopping list page had no h1 at all — its title was an h2. Both fixed; every page now has exactly one h1 and no skipped levels.

Step text moved 15px → 16px. At 820×1037 the same five steps stay above the fold, but the page grows 69px — the cost of a readable size at arm's length, which is the stated use case.

Structure

Primer's conventions: borders define surfaces, elevation is almost absent.

The elevation scale is two values. --shadow-card is a 4%-alpha single pixel in light and none in dark. --shadow-overlay is for things that genuinely float — dropdowns, dialogs. Nothing in the document flow uses the second one.

Canvas #fcfcfb, white surfaces, #e4e0da hairlines, 6px radii. Controls signal state with fill, not depth.

Behaviour changes

  • Recipes index sorting was removed mid-PR and then restored, rebuilt on the token system with several latent defects fixed: name sort read the card's h3 text (so any markup change broke it silently), localeCompare had no options (so "Recipe 10" sorted before "Recipe 9"), and the choice was lost on every navigation. It now reads data-name, uses Intl.Collator with numeric collation, and persists in sessionStorage with a guarded read.
  • Recipe scaling now reuses the existing adjustScale() rather than a duplicate implementation, and preserves scroll position via sessionStorage. Previously a scale tap jumped you back to the top of the recipe.
  • Scaling gained −/+ buttons alongside the number input.
  • Coarse-pointer targets are 44px, and checkboxes 24px per WCAG 2.2 SC 2.5.8.

Bugs found and fixed along the way

  1. Cook mode was illegible in light theme. Inline entity badges lost their backgrounds and resolved the site theme's tokens against cook mode's always-dark card: 2.5:1. Fixed by adding .cooking-overlay to the dark token selector — 6.6:1.
  2. Cook mode rendered empty step cards. cooking-mode.js scraped .md\:col-span-2 ol and .leading-8, classes the layout work had deleted.
  3. The print path was broken. Print CSS targeted .bg-white, .grid, .md\:col-span-* — none of which still matched. A dark-theme recipe printed near-black on near-black and never collapsed to one column. Fixed by resetting tokens under @media print.
  4. CodeMirror lost the cascade in dark mode — a black caret on a near-black editor. Not a specificity problem: @layer components loses to CodeMirror's unlayered runtime-injected styles regardless of specificity. The rules had to move out of the layer.
  5. A custom-property resolution bug. --shadow-* were declared on :root in terms of var(--neu-*). A custom property containing var() resolves at its declaring element, not its user, so the composite froze with light values and inherited down. .dark got away with it by sharing <html> with :root; .cooking-overlay is a descendant and did not. (Introduced and fixed within this PR.)
  6. A theme-toggle bug. Transitioning a property whose value is a theme token races Chrome's custom-property invalidation when .dark flips — the element keeps the outgoing theme's colour until an unrelated recalc corrects it. Reproduced as recipe cards staying white on a dark page; intermittent rather than tied to the background shorthand. All token-colour transitions removed. (Introduced and fixed within this PR.)
  7. A vacuous test. recipe-display.spec.ts guarded its assertions behind if (metadataPills.count() > 0), and the count had become 0 — it could no longer fail. Retargeted with the guard removed.
  8. A Rust test pinned CSS classes. menu_api_test.rs matched <span class="text-sm text-gray-500">; the regex is now class-agnostic.

Items 1–4 and 7 all passed CI throughout. They were found by driving a browser and measuring computed styles, not by the suite.

The index sorter is the same story from the other direction: it had no tests at all, so it was removed and later restored without a single test changing colour. tests/e2e/recipes-sort.spec.ts now covers it, and was checked for vacuousness by mutation — neutering the comparator fails the ordering tests, disabling persistence fails the reload test.

Verification

  • E2E: 148 passed / 0 failed / 5 skipped
  • Type re-audit: six sizes and four weights in use across seven pages, nothing off-scale (serial; shopping-list*.spec.ts races on the shared seed/.shopping-list fixture under parallel workers — pre-existing).
  • cargo test, cargo fmt, cargo clippy clean.
  • 840 text-contrast probes across 8 pages × 2 themes, zero below AA.
  • Theme toggle verified over 8 synchronous flips read with no settle time.

Known gaps

  • WCAG 1.4.11. Hairline borders on controls sit near 1.9:1, below the 3:1 wanted for interactive component boundaries. This is inherent to Primer — GitHub's own button borders are ~1.4:1. Text is at AA throughout and focus rings are solid 2px.
  • --accent #E15A29 is 3.69:1 as a graphical colour, which passes, but it cannot carry white text (3.7:1), so filled accent buttons use the DS near-black as ink.

Follow-ups, deliberately not in this PR

  • Add recipe-method to the seven locales; replace the hardcoded Method literal and two stepper aria-labels.
  • Decouple --ok from its dual "in stock" / "cookware" roles.
  • Format pantry quantities (250%g250 g).
  • Remove now-dead print rules.
  • Cook mode keeps its own raw-px type sizes. It is read from across a kitchen rather than at desk distance, so retuning it needs a real tablet to validate rather than a guess.

42 commits · 29 files · see diff

dubadub added 30 commits August 15, 2026 20:33
text-orange-600 is 3.60:1 on white, below AA. The old .recipe-card
gradient bar caused axe to report this as incomplete rather than a
violation; removing it in Task 4 unmasked the pre-existing failure.
Badges lost their own background in Task 4, so they resolved the site
theme's tokens. Cook mode's card is always dark navy, so in light mode
ingredient/cookware text fell to ~2.5:1. Adding .cooking-overlay to the
dark token block restores 6.6:1 and 6.4:1.
- brand link had no accessible name below sm (span is display:none there)
- search-selected was ~1.1:1 in dark mode; accent bar now carries the signal
- guard app bar overflow across the 768-1280 band (zero slack at 768)
- record search-result markup migration as a Task 13 prerequisite
Dropped during the Task 6 rewrite. No test covers this span, so nothing
failed, but long preparation notes could overflow .step-refs without it.
The 20px checkbox failed the 24x24 minimum the finding itself cited, and
.stepper's border-box meant its buttons resolved to 42px, not 44.
Cards drop from ~190px hero tiles to ~59px rows. The client-side sorter
is removed at the user's request: it reparented every grid child, which
constrained the markup (title had to stay an h3) and blocked the grouped
layout the design calls for.

Locale keys sort-by/sort-name/sort-modified/sort-created/
sort-direction-toggle are now unused but left in place, in case sorting
returns server-side.
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review: feat(ui) design refresh with semantic tokens

Went through the diff file by file (design tokens/components.css, the JS that got repointed at new class names, and every migrated template). Overall this is a careful, well-reasoned refactor — the in-code comments explaining why (cascade-layer specificity for CodeMirror, the hidden-vs-inline ordering trick, the .cooking-overlay token scoping, the shopping-list race condition notes) are genuinely useful and make the diff much easier to trust than a typical mass-restyle.

Strengths

  • Design tokens are applied consistently almost everywhere. Raw text-gray-900 dark:text-gray-100-style utility pairs are gone in favor of text-text/text-muted/etc., which is exactly the point of the change and should make future dark-mode bugs much rarer.
  • XSS hygiene is solid in the touched JS. shopping_list.html and search.js consistently run user/recipe data through escHtml/escapeHtml before interpolating into innerHTML, including in spots that were rewritten from scratch (renderRefs, category/pantry rendering).
  • Deliberate preservation of E2E-load-bearing class names (.nav-pill, .recipe-card, .ingredient-badge, .metadata-pill) is called out and honored — good discipline for a change this wide.
  • Real bugs fixed along the way are legitimate and independently verifiable from the diff: cooking-mode.js was scraping .md\:col-span-2 ol / .leading-8, both deleted by the recipe template migration, and is correctly repointed to .step-list / .step-body. The pantry's applyFilter() no longer reaches for the recipes-index's .recipe-card/.grid.
  • tests/e2e/tablet.spec.ts is a good regression net for the actual motivating use case (tablet in the kitchen), and the recipe-display.spec.ts fix (if (metadataPills.count() > 0) silently skipping all assertions) is a genuine test-coverage improvement, not just a refactor.

Issues

  1. Incomplete token migration: API docs method badges still use raw Tailwind palette classes.
    src/web/templates.rs (EndpointDoc::method_classes, ~line 743) still returns hardcoded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200-style strings for the GET/POST/PUT/DELETE badges in templates/api_docs.html. Every other color in this PR was intentionally routed through the new --info/--ok/--accent/--danger tokens (per the comment in input.css: "Do not introduce raw hex values or Tailwind palette utilities in templates"), so this is the one remaining spot still hand-picking a light/dark pair per method. Minor, but worth a follow-up so the api-docs page doesn't silently diverge from the rest of the dark-mode contrast work that was otherwise verified for AA compliance.

  2. Scroll-preserving rescale wasn't ported to the menu page.
    templates/recipe.html now calls goToScale(this.value) on the scale <input>'s onchange, which stashes scrollY in sessionStorage before navigating so a rescale doesn't dump you back at the top (called out explicitly in the PR description as a fix). templates/menu.html has the identical stepper UI (/+ buttons calling the same adjustScale()), but its <input> still wires onchange="window.location.href = ... + '?scale=${this.value}' + " directly — the old behavior. Since adjustScale() dispatches a change event on the same input, using the stepper on a menu page still jumps back to the top. Looks like a straightforward miss rather than an intentional difference; would be a one-line fix to swap in goToScale() there too.

  3. docs/superpowers/ adds ~2,700 lines of planning/spec/mockup docs to the tree.
    docs/superpowers/plans/…-web-ui-refresh.md (2114 lines), the design spec (224 lines), and a standalone mockup HTML (395 lines) make up more than half the diff's line count. This is clearly high-quality planning material, but it's not referenced by CLAUDE.md as a documented convention, and it's the kind of implementation-process artifact that often ages poorly once merged (task checklists, "measured baseline" tables, environment-specific notes like the macOS Playwright workaround). Worth a quick check with the team on whether this should land in the repo permanently vs. staying attached to the PR/issue.

Nits

  • recipes.html still emits data-modified/data-created attributes on every recipe card even though the client-side sorter that read them was deleted in this same PR. Harmless, but dead weight in the rendered HTML unless it's intentionally there for a future server-side sort.
  • Pre-existing (not introduced here) but adjacent to code this PR touches: the inline recipe-search dropdown built in templates/base.html interpolates ${recipe.name} into innerHTML without escaping, whereas the sibling implementation in static/js/search.js does escape via escapeHtml. Given recipe titles are local/user-authored content it's low severity, but since this PR already touches that exact block (just for class names) it might be worth folding in the same escaping for consistency.
  • static/css/input.css and a couple of other touched files end without a trailing newline (pre-existing, not this PR's doing, but flagged since they're right next to new content).

Test coverage

E2E additions look appropriately targeted (tablet.spec.ts, the corrected metadata assertions, the loosened method-badge regex in menu_api_test.rs). No Rust logic changed outside of test files, so cargo test/clippy/fmt claims in the description are plausible as stated. I didn't run the Playwright suite myself as part of this review.

No security concerns beyond item 2 above (which is a UX regression, not a vulnerability) — the escaping discipline in the rewritten JS is good.

Repoints the token contract at the Cooklang design system colours and
rebuilds the component layer around soft dual-shadow elevation.

Palette
- Text/Primary #16161D, Text/Tags #715329, Controls+Icons/Stroke #E15A29
  and Background/UI One #F5DACF are used exactly as specified.
- Text/Secondary #817B6D (3.7:1) and Text/Warning #FF3B30 (3.0:1) are
  darkened; both fail AA as body text on a light ground, and both are used
  as body text today. Hue is preserved.
- The DS supplies Light only. Dark is derived in the same warm family.

Elevation
- --neu-light / --neu-dark drive four composites (raised, raised-sm, inset,
  inset-sm). Cards, buttons and nav rise; fields, chips and the active nav
  item sink. Pressable surfaces invert to inset on :active.
- Print flattens every composite to none — soft shadows print as smudge.

Palette coherence
- Recipe links were blue and cookware was green; the DS contains neither.
  Links move to the dark end of the DS orange, cookware drops colour
  entirely for a dotted underline. Both now carry a non-hue cue, which also
  fixes brown-vs-green being the confusable pair for red-green deficiency.
- cooking-mode.css was never migrated in the previous refresh and was still
  hardcoded slate-navy with orange/amber gradients. It is now on tokens.

Fixes a latent custom-property bug: --shadow-* were declared on :root in
terms of var(--neu-*), and a custom property containing var() resolves at
its declaring element, not its user. .dark got away with it by sharing
<html> with :root; .cooking-overlay is a descendant and did not, so cook
mode drew a 95%-opacity white halo whenever the site was in light theme.
The composites are now redeclared in the dark block.

Verified: 140 E2E passed / 0 failed / 5 skipped; cargo test, fmt, clippy
clean; 846 text-contrast probes across 8 pages x 2 themes, zero failures.
Replaces the soft dual-shadow elevation with GitHub Primer's structural
convention, keeping the Cooklang design system colours from the previous
commit. Borders define surfaces; elevation is almost absent.

- Canvas lightens to #fcfcfb with white surfaces and a #e4e0da hairline on
  every box. Radii 16/10px -> 6px.
- The elevation scale is now two values: --shadow-card, a 4%-alpha single
  pixel in light and `none` in dark, and --shadow-overlay for things that
  genuinely float (dropdowns, dialogs). Nothing in flow uses the latter.
- Controls signal state with fill, not depth. Chips are flat and bordered.
- Dark canvas is the design system's own #16161D.
- cooking-mode.css follows the same structure.

Contrast improves across the board on the lighter canvas: --accent as a
graphical colour goes from 2.95:1 to 3.69:1, clearing the 3:1 it previously
missed, and every text token gains roughly a point.

Fixes a theme-toggle bug introduced while building this: transitioning a
property whose value is a theme token races Chrome's custom-property
invalidation when .dark flips, leaving the element on the outgoing theme's
colour until an unrelated recalc corrects it. It reproduced as recipe cards
staying white on a dark page, and it was intermittent rather than tied to
the background shorthand specifically. All such transitions are removed;
hover feedback is instant, which suits a flat design anyway. Verified with
8 synchronous toggles read with no settle time, zero mismatches.

Verified: 140 E2E passed / 0 failed / 5 skipped; cargo test, fmt, clippy
clean; 840 text-contrast probes across 8 pages x 2 themes, zero failures.
@dubadub dubadub changed the title feat(ui): design refresh with semantic tokens feat(ui): design refresh — token system, Cooklang palette, Primer structure Aug 17, 2026
Brings back the sort controls removed in 12a5cc7, restyled onto the token
system and with the defects the original carried fixed.

Kept client-side: recipes.html is rendered by the static site builder as
well as the server, and a ?sort= query has nothing to answer it in a static
export.

Fixed while restoring:
- Name sort read el.querySelector('h3').textContent, so any change to the
  card markup silently broke sorting. It now reads a data-name attribute.
  components.css carried a comment warning future editors about exactly this
  coupling; that warning is no longer needed.
- Sorting used localeCompare with no options, so "Recipe 10" sorted before
  "Recipe 9". Now Intl.Collator with numeric and sensitivity 'base'.
- The choice was lost on every navigation. Now persisted in sessionStorage,
  with the read guarded — a corrupt value falls back to the defaults rather
  than throwing and leaving the page unsorted.
- A persisted field that does not exist on the current page (the "created"
  option is dropped where not every recipe has a date) is now validated
  against the live options instead of being applied blindly.
- Directories kept source order; they now sort among themselves.
- The direction button had a title but no accessible name; it has an
  aria-label now.
- Controls were styled with raw gray-/purple- palette utilities that did not
  flip with the theme. New .select component, tokens throughout.

Adds tests/e2e/recipes-sort.spec.ts — 8 tests, the feature previously had
none, which is how it was removed and restored without any test changing
colour. Verified non-vacuous by mutation: neutering the comparator fails the
reverse-order and modified-date tests, and disabling persistence fails the
survives-a-reload test.

Verified: 148 E2E passed / 0 failed / 5 skipped; cargo test, fmt, clippy
clean; no horizontal overflow at 768/800/820/1024/1280px.
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review: feat(ui): design refresh — token system, Cooklang palette, Primer structure

Reviewed the full diff (gh pr diff 456, 28 files) against CLAUDE.md's conventions (token-driven CSS, Askama templates, Playwright/cargo test gates). Overall this is a well-executed, unusually well-documented refactor — the write-up below focuses on what needs attention before merge, but the design-system foundation itself is solid.

Strengths

  • The 17-token contract in static/css/input.css (--bg, --surface, --text, --accent, …) wired through Tailwind's @theme inline is a real improvement over the ~330-line hand-maintained .dark .* override block it replaces — one source of truth per color, and it was verified there's no raw hex outside token definitions and the @media print block.
  • Deleting custom-styles.css and styles.css (dead/silently-conflicting stylesheets, one of them shipping a .btn-primary that visibly disagreed with itself on hover) is good cleanup, not just churn.
  • adjustScale() reuse between keyboard shortcuts and the new −/+ stepper buttons is clean — no duplicated scaling logic.
  • The cooking-mode selector fix, the CodeMirror @layer components cascade fix, the print-stylesheet reset, and the class-agnostic tests/menu_api_test.rs regex all check out against the diff — these aren't just claimed in the description, they're actually there.
  • New test coverage (tests/e2e/recipes-sort.spec.ts, tests/e2e/tablet.spec.ts) is substantive rather than smoke-test-shaped, and the previously-vacuous assertion in recipe-display.spec.ts (guarded behind if (metadataPills.count() > 0), which had silently become uncheckable) is genuinely fixed.
  • The sort feature's sessionStorage read is guarded with a corrupt-JSON fallback, and uses Intl.Collator with numeric collation instead of bare localeCompare — correctly fixes the "Recipe 10 sorts before Recipe 9" class of bug.

Findings

Moderate — theme-toggle transition race not fully eliminated.
The PR description says all token-colour CSS transitions were removed to fix a Chrome custom-property-invalidation race (recipe cards briefly staying white after .dark toggles). components.css does strip transition from .btn:hover / .recipe-card:hover with a comment explaining why. But templates/base.html's mobile #more-dropdown nav links and the search-result links (static/js/search.js / base.html's inline handler) still pair transition-colors with token-backed hover colors (hover:bg-sunk hover:text-text) — the exact pattern removed elsewhere. Worth confirming whether these were intentionally left in (maybe they don't respond to the initial .dark flip since they're not visible at toggle time) or missed in the sweep.

Moderate — scale stepper buttons behave inconsistently between recipe.html and menu.html.
templates/menu.html picked up the same new −/+ scale buttons as templates/recipe.html, but only recipe.html wires them to goToScale() with the sessionStorage scroll-position preservation described in the PR. On the menu page, clicking the stepper still does a bare window.location.href navigation, dropping the user back to the top — the exact "scale tap jumped you back to the top" bug the PR set out to fix, still present on one of the two pages that got the new control.

Nit — planning artifacts committed to docs/.
docs/superpowers/plans/2026-08-15-web-ui-refresh.md and the paired spec/mockup files (~2,700 lines combined) read as internal AI-assisted planning narration (task-numbered, e.g. "Task 4", "Task 13") rather than reference documentation. Worth a call on whether these belong in the shipped tree or should stay out-of-band.

Nit — missing trailing newline.
static/css/input.css (and the deleted custom-styles.css) don't end in a newline; minor, but worth a pass for tracked text files.

Not flagged, but worth confirming before merge

Per CLAUDE.md, please confirm cargo fmt, cargo clippy, and cargo test are clean, and that the Playwright suite result quoted in the description (148 passed / 0 failed / 5 skipped) is reproducible on CI rather than only locally — the shopping-list race noted as "pre-existing" is fine to leave, just flagging it's environment-dependent (serial execution requirement) rather than fixed here.

No security issues found — XSS hygiene (escHtml usage, Askama auto-escaping) looked consistent everywhere it was touched, and no unvalidated sessionStorage/user input reached innerHTML or a URL sink.


🤖 Automated review generated with Claude Code

Audit of five pages found nine distinct font sizes and six weights, with
no scale behind either.

Sizes were 11.5, 12, 12.5, 13, 13.5, 14, 15, 16 and 24px — six of them
inside a 2.5px band. Half-pixel steps sit below the threshold at which a
reader can tell two sizes apart, so they could not express hierarchy; they
only made each new declaration a guess. Replaced with seven steps, each at
least 1px from its neighbours and each mapped to one job:

  24 display  page title
  18 title    section headings
  16 read     recipe step text
  14 body     list rows, card titles, default
  13 ui       buttons, inputs, metadata line
  12 meta     captions, counts, tags, notes
  11 label    uppercase section labels

18px is new: section headings had been set at 15, 16, 17, 19 and 20px in
different templates, five sizes doing one job.

Every step carries its own line-height, falling as size rises — long-form
text needs the leading to carry the eye back to the next line, headings do
not. Line-height had been set in exactly two places in the whole component
layer, so most text was on browser or Tailwind defaults.

Weights 550 and 650 are gone. They do render on macOS, where the system
font resolves to variable SF Pro, but 600 to 650 is a ~1% change in advance
width and snaps to a neighbouring weight wherever the system font is not
variable — hierarchy that is invisible on one platform and absent on
another. Now 400/500/600/700.

Tailwind's own text-xs/sm/base/lg/2xl are aliased onto the scale rather
than left alongside it. Their sizes already coincided but their
line-heights did not, so the same visual size carried two different
leadings depending on which name an author reached for. Aliasing fixes ~80
call sites without touching markup, and keeps a future `text-sm` on-scale.

Measure: step text ran to 84 characters per line on a wide viewport, past
the point where the eye reliably finds the next line. Capped at 68ch, which
measures 74 characters and tracks font size rather than freezing a width.

Heading semantics, which a visual audit does not surface:
- The recipes index went h1 -> h3, skipping a level. Card titles are h2 now.
  The comment requiring an <h3> was stale from the sorter restore, and the
  E2E suite now selects .recipe-card-title rather than the tag name.
- The shopping list page had no h1; its title was an h2. Promoted.
All pages now have exactly one h1 and no skipped levels.

Step text moves 15px -> 16px. At 820x1037 the same five steps stay above
the fold, but the page grows 69px; that is the cost of a readable size at
arm's length, which is the stated use case.

Cook mode keeps its own raw-px sizes deliberately. It is read from across a
kitchen rather than at desk distance, and retuning it needs a real tablet
to validate, not a guess. Noted as a follow-up.

Verified: 148 E2E passed / 0 failed / 5 skipped; cargo test, fmt, clippy
clean; 802 text-contrast probes across 8 pages x 2 themes, zero failures;
re-audit finds six sizes and four weights in use, nothing off-scale.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant