From cf9fa387574ab59a0a44bc221b910fb68477bc64 Mon Sep 17 00:00:00 2001 From: Alexey Dubovskoy Date: Fri, 4 Sep 2026 22:40:44 +0100 Subject: [PATCH 01/25] docs: add web UI token foundation design spec Supersedes the PR #456 spec: same CSS foundation and palette, but the existing page layouts and density are kept. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu --- .../specs/2026-09-04-web-ui-tokens-design.md | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md diff --git a/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md b/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md new file mode 100644 index 00000000..10f05c5a --- /dev/null +++ b/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md @@ -0,0 +1,276 @@ +# Web UI: Token Foundation With the Existing Layout + +**Date:** 2026-09-04 +**Status:** Approved, ready for planning +**Supersedes:** PR #456 (`design/web-ui-refresh`). This spec reuses that PR's CSS foundation and visual identity and discards its density work. At the end, this branch is force-pushed over `design/web-ui-refresh` so PR #456 keeps its number and discussion; its description is rewritten to match this spec. + +## Goal + +Rebuild the web UI's styling on Tailwind v4's CSS-first model with a semantic token layer, adopt the Cooklang design-system palette and the flat, hairline-bordered surface language from PR #456, and make the pages visually consistent with one another. Keep every page's structure, spacing, and dimensions as they are on `main` today. + +## Non-goals + +- No compaction. The 48px app bar, the 60px index rows, the sticky ingredient rail, the unboxed hairline step list, the single-line pantry rows, and the compact shopping list from PR #456 are not adopted. +- No backend, routing, template-data, or API changes. +- No change to cook mode's layout. Only its stylesheet moves onto tokens. +- No new pages, no new features beyond the behaviour fixes listed in section 5. + +## Starting point + +`main` is already on Tailwind v4 (`tailwindcss` and `@tailwindcss/cli` 4.3.3). Its `input.css` still drives Tailwind through `@config "../../tailwind.config.js"`, defines its components with `@apply` and raw hex gradients, and relies on a ~450-line block of `.dark .*` overrides in `templates/base.html` plus two extra stylesheets (`custom-styles.css`, which shadows `output.css`, and `styles.css`, which nothing references). + +Work happens on a new branch from `main` (`design/web-ui-tokens`). Nothing is cherry-picked from PR #456; its final CSS files are used as the reference and copied where the spec says so. + +## 1. Foundation + +### 1.1 `static/css/input.css` + +Structure, in order: + +1. `@import "tailwindcss";` +2. `@custom-variant dark (&:where(.dark, .dark *));` — replaces `darkMode: 'class'`. +3. `@source "../../templates";` and `@source "../../static/js";` — replaces the content globs. No Rust source contains class names, so `src/**/*.rs` is not scanned. +4. Token declarations: `:root` (light), `.dark, .cooking-overlay` (dark), each with `color-scheme`. +5. `@theme inline` registering every colour token as a Tailwind colour. +6. `@theme` type scale. +7. `body { font-size: var(--text-body); line-height: 1.5; }` +8. `@import "./components.css";` +9. `@media print` token reset and layout flattening. +10. CodeMirror rules, outside any layer. + +`@config` is removed and `tailwind.config.js` is deleted. The gradient keyframes it defined are unused once gradients are gone. + +### 1.2 Tokens + +Seventeen semantic colour tokens plus two radii, two shadows. Values are the PR's final values, verbatim. + +| Token | Light | Dark | +|---|---|---| +| `--bg` | `#fcfcfb` | `#16161d` | +| `--surface` | `#ffffff` | `#1c1c24` | +| `--surface-sunk` | `#f5f3f0` | `#23232c` | +| `--border` | `#e4e0da` | `#30303b` | +| `--border-strong` | `#c3bcb1` | `#43434f` | +| `--text` | `#16161d` | `#efeae6` | +| `--text-muted` | `#5f5a51` | `#ada69b` | +| `--text-faint` | `#6a645b` | `#948d83` | +| `--accent` | `#e15a29` | `#e15a29` | +| `--accent-text` | `#715329` | `#f08050` | +| `--accent-soft` | `#f5dacf` | `#3a2820` | +| `--accent-ink` | `#16161d` | `#16161d` | +| `--ok` | `#3d6849` | `#6fb283` | +| `--ok-soft` | `#e2e8df` | `#1e2a22` | +| `--danger` | `#c4261c` | `#ff6b60` | +| `--danger-soft` | `#f7dfdc` | `#2e1b1a` | +| `--danger-ink` | `#ffffff` | `#16161d` | +| `--info` | `#8a3d14` | `#e59a6d` | +| `--disabled` | `#d3cdcb` | `#4a4a55` | +| `--inactive` | `#8a8075` | `#8f8880` | +| `--radius-control` | `6px` | `6px` | +| `--radius-card` | `6px` | `6px` | +| `--shadow-card` | `0 1px 0 rgba(27,31,36,.04)` | `none` | +| `--shadow-overlay` | `0 8px 24px rgba(27,31,36,.12)` | `0 8px 24px rgba(0,0,0,.5)` | + +The print block resets all of these to the PR's print values (white surfaces, dark text, no shadows) under `:root, .dark, .cooking-overlay`. + +Tailwind names, registered under `@theme inline`: `bg`, `surface`, `sunk`, `line`, `line-strong`, `text`, `muted`, `faint`, `accent`, `accent-text`, `accent-soft`, `accent-ink`, `ok`, `ok-soft`, `danger`, `danger-soft`, `danger-ink`, `info`, `disabled`, `inactive`. Border tokens are named `line` so the utility reads `border-line`. + +No raw hex appears outside the token declarations and `@media print`. No Tailwind palette utility (`gray-*`, `orange-*`, `purple-*`, …) appears in any template, script, or stylesheet. No `dark:` variant appears in any template; every colour flips through its token. + +### 1.3 Type scale + +The PR's seven steps, with the page-title step raised to main's size. + +| Step | Size | Line-height | Role | +|---|---|---|---| +| `display` | **30px** | 1.2 | page title (`h1`) | +| `title` | 18px | 1.35 | section headings | +| `read` | 16px | 1.6 | recipe step text, notes | +| `body` | 14px | 1.5 | list rows, card titles, default | +| `ui` | 13px | 1.4 | controls, metadata | +| `meta` | 12px | 1.4 | captions, counts, tags | +| `label` | 11px | 1.3 | uppercase section labels | + +`display` and `title` carry the PR's negative letter-spacing. Tailwind's `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-2xl` are aliased onto `meta`, `body`, `read`, `title`, `display` as in the PR; `text-3xl` is also aliased onto `display`. `text-4xl` is not used anywhere after this change. Four weights only: 400, 500, 600, 700. + +Exception: recipe step text keeps main's `leading-8` (2.0 line-height) on 16px text. This is the one place the reading step's line-height is overridden, and it is a deliberate density choice. + +### 1.4 `static/css/components.css` + +Copied from the PR, then adjusted. Everything lives in `@layer components` and resolves through tokens. + +**Kept as in the PR:** `:focus-visible` ring, `.card`, `.card-head` (including `.plain` and `.count`), `.btn-danger` colours, `.select` colours, `.row` family (used by the shopping list sidebar and JS-rendered lists), `.metaline` (available but unused), `.section-label`, `.item-status-dot` states, `.ingredient-badge`, `.cookware-badge`, `.timer-badge`, `.tag`, `.metadata-pill`, `.recipe-note`, `.step-refs`, `.image-step`, `.recipe-image-placeholder`, `.search-input` colours, `.nav-pill` colours, `a.text-info` underline, the coarse-pointer 44px target block, and the "no transitions on token colours" rule. + +**Retuned to main's dimensions:** + +| Class | Change | +|---|---| +| `.btn`, `.btn-primary`, `.btn-danger` | height 40px, padding `0 16px`, `text-body`, svg 20px. Matches main's `px-4 py-2` buttons. | +| `.select` | height 40px to sit level with `.btn`. | +| `.stepper` | height 40px; buttons 32px wide; input 56px wide. | +| `.nav-pill` | main's `px-5 py-2`, `border-radius: 9999px`, `text-body`. Flat fill states: hover `--surface-sunk`; active `--accent-soft` background, `--accent-text` colour, weight 600. | +| `.icon-btn` | 36px square (main's `p-2` + 20px icon), radius `--radius-control`, svg 20px. | +| `.search-input` | height 44px, padding `0 16px 0 40px`, `text-body`; keeps PR's border and focus rule. | +| `.step-number` | 32px circle (main), `--accent-soft` fill, `--accent-text` glyph, `text-body` weight 700, no border. | +| `.recipe-card` | block card: `display:flex; flex-direction:column; overflow:hidden`, `--surface`, hairline, `--radius-card`, `--shadow-card`; hover `border-color: var(--border-strong)`. No `::before` gradient stripe, no scale transform. Content padding is `p-6` in the template as today. | +| `.recipe-card-icon` | 64px circle, `--surface-sunk` fill, hairline, emoji at 24px. | +| `.recipe-card-title` | `h2` styled as main's `text-lg font-bold`, i.e. `text-title` 700, `--text`. | +| `.recipe-card-sub` | `text-meta`, `--text-faint`. | + +**Removed:** `.appbar`, `.appbar-brand`, `.appbar-nav`, `--appbar-h`, `.recipe-layout`, `.recipe-rail`, `.step-list` / `.step-list > li` hairline rules, `.step-body` max-width, `.ingredient-list` padding, `.pantry-item` single-line rules, `.pantry-dates`, `.pantry-date` separators, `.pantry-actions` opacity rules. + +**Added:** + +| Class | Definition | +|---|---| +| `.nav-card` | main's nav container: `--surface`, hairline, `--radius-card`, `--shadow-card`, `margin-bottom: 2rem`. Not sticky. | +| `.step-list` | `list-style:none; margin:0; padding:0;` only. Step boxes are `li.step-box`. | +| `.step-box` | main's per-step box: `--surface-sunk` fill, hairline, `--radius-card`, `padding: 1rem`. | +| `.step-body` | `font-size: var(--text-read); line-height: 2;` | +| `.ingredient-list` | `list-style:none; margin:0; padding:0;` | +| `.ingredient-row` | main's tinted row: `display:flex; justify-content:space-between; align-items:center; padding: .5rem .75rem; border-radius: var(--radius-control); background: var(--surface-sunk);` Quantity uses `.row-value`. Note uses `.row-note`. | +| `.pantry-item` | main's block: `--surface-sunk` fill, `--radius-control`, `padding: 1rem`, `border: 1px solid transparent`; hover `border-color: var(--border)`. `.out-of-stock` variant: `--danger-soft` fill, `--danger` border; its `.quantity-display` is `--danger`. | +| `.pantry-actions` | opacity 0, revealed on `.pantry-item:hover`, `:focus-within`; always visible on coarse pointers. (Same behaviour as main's `group-hover:opacity-100`.) | + +### 1.5 Other stylesheets + +- `static/css/custom-styles.css` and `static/css/styles.css` are deleted; the `` to `custom-styles.css` in `base.html` is removed. +- `static/css/cooking-mode.css` is replaced with the PR's tokenised version verbatim (66 added / 58 removed lines against main). Its layout is unchanged. +- `static/css/output.css` stays gitignored and is never committed. + +### 1.6 `templates/base.html` `` block in ``) + +Every page is now on tokens, so nothing depends on the `.dark .*` overrides. The print block is rewritten to target the new markup only. + +- [ ] **Step 1: Replace the entire `` in `` with: + +```html + +``` + +- [ ] **Step 2: Confirm the override block is gone and no palette utility survives anywhere** + +Run: +```bash +grep -c '\.dark ' templates/base.html +grep -rnE '(^|[ "'"'"'])(hover:|focus:|group-hover:)?(bg|text|border|from|to|via|ring|placeholder|decoration)-(gray|orange|purple|pink|blue|indigo|green|emerald|red|yellow|lime|amber|cyan|slate|white)\b' templates static/js/*.js static/css/input.css static/css/components.css static/css/cooking-mode.css src/web/templates.rs +grep -rn 'gradient' templates static/js/*.js static/css/input.css static/css/components.css static/css/cooking-mode.css +grep -rn 'dark:' templates static/js/*.js | grep -v 'dark:block\|dark:hidden' +``` +Expected: `0` from the first command, nothing from the other three except comment lines in `components.css` that mention the word "gradient" (those are fine). Fix any class or property occurrence that prints before continuing. + +- [ ] **Step 3: Build everything and run the full suites** + +```bash +cargo fmt && cargo clippy 2>&1 | tail -3 && cargo test 2>&1 | grep -E 'test result|FAILED' +npm run build-css && npm run build-js +npm test -- --project=chromium --workers=1 +``` +Expected: clippy clean, every cargo test result line `ok`, Playwright reports 0 failed. The accessibility spec asserts one `h1` per page and AA contrast; the shopping list, pantry and error pages now have an `h1`, which is what it wants. + +- [ ] **Step 4: Print check** + +In dark mode, open `/recipe/Neapolitan%20Pizza`, `Cmd+P`, preview: dark text on white, one column, no nav. Cancel. + +- [ ] **Step 5: Commit** + +```bash +git add templates/base.html +git commit -q -F - <<'EOF' +refactor(ui): delete the dark-mode override block + +Every page resolves its colours through tokens now, so the ~450 lines +of `.dark .*` utility overrides are dead. The print block is rewritten +against the current markup. + +Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu +EOF +``` + +--- + +### Task 13: Visual pass, PR description, force-push + +**Files:** none new. This task verifies, documents and ships. + +- [ ] **Step 1: Side-by-side against main** + +Start a second server on main for comparison: + +```bash +git worktree add /tmp/cookcli-main origin/main +cd /tmp/cookcli-main && npm install --silent && npm run build-css && npm run build-js && cargo build +``` + +then, in a second terminal, `/tmp/cookcli-main/target/debug/cook server /tmp/cookcli-main/seed --port 9081`. + +For each of `/`, `/recipe/Neapolitan%20Pizza`, `/shopping-list` (with a recipe added), `/pantry`, `/preferences`, `/edit/Neapolitan%20Pizza`, `/api-docs`, and a menu page, compare `:9080` against `:9081` at 1440, 1024 and 820px in light and dark. What must match: nav height, card positions and heights, grid columns, button heights, step box padding, pantry block size. What must differ: colours, gradients gone, hairline borders, 6px radii, one accent. + +When done, stop that server and run `git worktree remove /tmp/cookcli-main`. + +- [ ] **Step 2: Cook mode in light theme** + +On `:9080` in light theme open a recipe, press Cook. Entity badges on the step card are readable; the header pill for the active section is accent-filled. + +- [ ] **Step 3: Static build smoke test** + +```bash +cargo run -q -- build ./seed --output /tmp/cook-static 2>&1 | tail -2 +ls /tmp/cook-static/static/css/ +open /tmp/cook-static/index.html +``` +Expected: `output.css` and `cooking-mode.css` present, no `custom-styles.css`; the index renders on tokens from `file://`, and search works (main's script-tag loader was kept). + +- [ ] **Step 4: Write the PR description** + +Save to `/tmp/pr-body.md`: + +```markdown +Rebuilds the web UI's styling on a Tailwind v4 CSS-first token layer and adopts the Cooklang design-system palette with flat, hairline-bordered surfaces. Every page keeps the layout, spacing and dimensions it has on `main`. + +This replaces the earlier version of this PR, which bundled the same foundation with a density pass (48px app bar, 60px index rows, sticky ingredient rail, compact lists). The density work is dropped; the foundation, palette, type scale and bug fixes are kept. + +## Foundation + +- `input.css` is fully CSS-first: `@custom-variant dark` and `@source` replace `tailwind.config.js`, which is deleted. +- Twenty semantic tokens (`--bg`, `--surface`, `--text`, `--accent`, …) registered with `@theme inline`, so `bg-surface` / `text-muted` / `border-line` are real utilities that flip under `.dark` with no override rules. +- `components.css` holds the component vocabulary. Every colour resolves through a token; no raw hex outside `@media print`. +- Deleted: `custom-styles.css` (shadowed `output.css`), `styles.css` (unreferenced), and the ~450-line `.dark .*` override block in `base.html`. + +## Look + +Cooklang DS palette, no gradients, one accent, 6px radii, hairline borders, two-value elevation scale. Inline entities are weight + tint (ingredients) and a dotted underline (cookware), so the distinction no longer rests on a red/green hue pair. + +## Type + +Seven-step scale with per-step line-heights; Tailwind's size names are aliased onto it. Page titles stay at 30px. Step text keeps its 2.0 leading. + +## Fixes carried over + +- Index sorter reads `data-name`, collates numerically, persists in `sessionStorage`; now covered by `recipes-sort.spec.ts`. +- Scale changes preserve scroll position; −/+ stepper shares `adjustScale` with the keyboard shortcuts. +- Every page has exactly one `h1` and no skipped heading level. +- Cook mode legible in light theme; its step capture no longer scrapes layout utilities. +- Print path works from dark theme. +- CodeMirror dark caret/gutters (rules moved out of `@layer`). +- `menu_api_test.rs` no longer pins CSS classes; `recipe-display.spec.ts` lost its vacuous guard. + +## Verification + +- `cargo fmt`, `cargo clippy`, `cargo test` clean. +- Playwright: full suite green in Chromium. +- Each page compared against `main` at 1440/1024/820px, light and dark. + +Spec: `docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md`. + +https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu +``` + +- [ ] **Step 5: Force-push over the PR branch and update the description** + +The user approved replacing PR #456's branch. Its old commits stay reachable from the PR's history. + +```bash +git push --force-with-lease=design/web-ui-refresh origin design/web-ui-tokens:design/web-ui-refresh +gh pr edit 456 --title "feat(ui): token foundation and Cooklang palette, existing layout kept" --body-file /tmp/pr-body.md +gh pr view 456 --json url,title -q '.url + " " + .title' +``` + +If `--force-with-lease` is refused because the remote moved, fetch, look at what changed on `design/web-ui-refresh`, and ask the user before retrying. + +- [ ] **Step 6: Report** + +Tell the user: the PR URL, the number of commits on the branch, which E2E projects were run, and anything skipped. + +--- + +## Self-review against the spec + +| Spec section | Task | +|---|---| +| 1.1 `input.css` structure, `@custom-variant`, `@source`, no `@config` | 1 | +| 1.2 tokens (light, dark, print) | 1 | +| 1.3 type scale, 30px display, `text-3xl` alias, 2.0 step leading | 1 (`.step-body` in components) | +| 1.4 component retuning and additions | 1 | +| 1.5 stylesheet deletions, cook-mode replacement | 1 | +| 1.6 `base.html` style block | 2 (search rules), 12 (dark block, print) | +| 3.1 base | 2 | +| 3.2 recipes | 3 | +| 3.3 recipe | 5 | +| 3.4 shopping list | 6 | +| 3.5 pantry | 7 | +| 3.6 menu | 8 | +| 3.7 preferences | 9 | +| 3.8 edit, new | 10 | +| 3.9 api docs, error, `method_classes` | 11 | +| 3.10 scripts | 4 | +| 4 heading semantics | 3, 5, 6, 7, 11 | +| 5 behaviour fixes | 3 (sorter), 5 (scale), 1 (cook-mode tokens, print, CodeMirror, no transitions), 4 (cook-mode JS), 8 (menu test) | +| 6 tests | 3, 5, 8, 9 | +| 7 verification | 12, 13 | +| 8 sequencing | task order | diff --git a/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md b/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md index 10f05c5a..f4d8eddd 100644 --- a/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md +++ b/docs/superpowers/specs/2026-09-04-web-ui-tokens-design.md @@ -29,7 +29,7 @@ Structure, in order: 1. `@import "tailwindcss";` 2. `@custom-variant dark (&:where(.dark, .dark *));` — replaces `darkMode: 'class'`. -3. `@source "../../templates";` and `@source "../../static/js";` — replaces the content globs. No Rust source contains class names, so `src/**/*.rs` is not scanned. +3. `@source "../../templates";`, `@source "../../static/js";` and `@source "../../src/web/templates.rs";` — replaces the content globs. The one Rust file that emits class names is `src/web/templates.rs` (`method_classes()` for the API docs method badge), so it is scanned explicitly; the rest of `src/` is not. 4. Token declarations: `:root` (light), `.dark, .cooking-overlay` (dark), each with `color-scheme`. 5. `@theme inline` registering every colour token as a Tailwind colour. 6. `@theme` type scale. @@ -129,7 +129,7 @@ Copied from the PR, then adjusted. Everything lives in `@layer components` and r | `.step-body` | `font-size: var(--text-read); line-height: 2;` | | `.ingredient-list` | `list-style:none; margin:0; padding:0;` | | `.ingredient-row` | main's tinted row: `display:flex; justify-content:space-between; align-items:center; padding: .5rem .75rem; border-radius: var(--radius-control); background: var(--surface-sunk);` Quantity uses `.row-value`. Note uses `.row-note`. | -| `.pantry-item` | main's block: `--surface-sunk` fill, `--radius-control`, `padding: 1rem`, `border: 1px solid transparent`; hover `border-color: var(--border)`. `.out-of-stock` variant: `--danger-soft` fill, `--danger` border; its `.quantity-display` is `--danger`. | +| `.pantry-item` | main's block: `--surface-sunk` fill, `--radius-control`, `padding: 1rem`, `border: 1px solid transparent`; hover `border-color: var(--border)`. `.out-of-stock` variant: `--danger-soft` fill, `--danger` border; its `.quantity-display` is `--danger`. `.low-stock` variant: `--accent-soft` fill, `--accent` border; its `.quantity-display` is `--accent-text`. The JS toggles only these state classes; no utility classes are added at runtime. | | `.pantry-actions` | opacity 0, revealed on `.pantry-item:hover`, `:focus-within`; always visible on coarse pointers. (Same behaviour as main's `group-hover:opacity-100`.) | ### 1.5 Other stylesheets @@ -219,7 +219,7 @@ Eleven gradients become `.card p-6` sections with `h2.text-title`. Language and ### 3.9 `api_docs.html`, `error.html` -Cards and tokens; all `dark:` utilities removed; code blocks `bg-sunk`. +Cards and tokens; all `dark:` utilities removed; code blocks `bg-sunk`. The HTTP method badge classes come from `method_classes()` in `src/web/templates.rs`; they become `bg-sunk text-text` (GET), `bg-ok-soft text-ok` (POST), `bg-accent-soft text-accent-text` (PUT), `bg-danger-soft text-danger` (DELETE), `bg-sunk text-muted` (other). This is a string change only, no behaviour change. ### 3.10 Scripts From 2901588222886af60851fff0e1575bc3d3999893 Mon Sep 17 00:00:00 2001 From: Alexey Dubovskoy Date: Fri, 4 Sep 2026 23:16:52 +0100 Subject: [PATCH 03/25] feat(ui): add token layer and component vocabulary Tailwind moves fully CSS-first: @custom-variant dark and @source lines replace tailwind.config.js. custom-styles.css (which shadowed output.css) and the unreferenced styles.css are deleted. Component dimensions match what main rendered; only the colour system changes. Claude-Session: https://claude.ai/code/session_013urND2B6Y3Z7WQuDpE8ZDu --- static/css/components.css | 553 +++++++++++++++++++++++++++++++++++ static/css/cooking-mode.css | 124 ++++---- static/css/custom-styles.css | 146 --------- static/css/input.css | 476 ++++++++++++++++-------------- static/css/styles.css | 445 ---------------------------- tailwind.config.js | 37 --- templates/base.html | 1 - 7 files changed, 881 insertions(+), 901 deletions(-) create mode 100644 static/css/components.css delete mode 100644 static/css/custom-styles.css delete mode 100644 static/css/styles.css delete mode 100644 tailwind.config.js diff --git a/static/css/components.css b/static/css/components.css new file mode 100644 index 00000000..751eae69 --- /dev/null +++ b/static/css/components.css @@ -0,0 +1,553 @@ +/* ============================================================ + COMPONENT LAYER + Imported from input.css after the token registration. Everything here + resolves through the tokens; never introduce raw hex values or Tailwind + palette utilities. Dimensions match what main rendered before the token + migration — this file changes how things look, not how big they are. + Colour transitions are deliberately absent: transitioning a token-valued + property races Chrome's custom-property invalidation when .dark flips, + and elements keep the outgoing theme's colour until the next recalc. + ============================================================ */ +@layer components { + /* ---------- Focus ---------- */ + :focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: var(--radius-control); + } + + /* ---------- Surfaces ---------- */ + .card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + } + + .card-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-bottom: 1px solid var(--border); + } + + .card-head h2, + .card-head h3 { + margin: 0; + font-size: var(--text-label); + font-weight: 600; + letter-spacing: .06em; + text-transform: uppercase; + color: var(--text-muted); + } + + .card-head.plain h2, + .card-head.plain h3 { + font-size: var(--text-body); + font-weight: 600; + letter-spacing: 0; + text-transform: none; + color: var(--text); + } + + .card-head .count { + font-size: var(--text-meta); + color: var(--text-faint); + font-variant-numeric: tabular-nums; + } + + /* The nav container: main's rounded card, not sticky. */ + .nav-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + margin-bottom: 2rem; + } + + /* ---------- Controls ---------- */ + /* 40px: main's px-4 py-2 buttons at 16px text. */ + .btn { + display: inline-flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 16px; + border-radius: var(--radius-control); + font-size: var(--text-body); + font-weight: 500; + white-space: nowrap; + cursor: pointer; + text-decoration: none; + background: var(--surface); + color: var(--text); + border: 1px solid var(--border-strong); + } + + .btn:hover { + background: var(--surface-sunk); + border-color: var(--border-strong); + } + + .btn:active { background: var(--surface-sunk); filter: brightness(.97); } + + .btn svg { width: 20px; height: 20px; flex: 0 0 auto; } + + /* One accent-filled button per view. Everything else is neutral. */ + .btn-primary { + background: var(--accent); + border-color: var(--accent); + color: var(--accent-ink); + } + + .btn-primary:hover { + background: var(--accent); + filter: brightness(1.06); + } + + /* The commit half of a destructive confirmation. */ + .btn-danger { + background: var(--danger); + border-color: var(--danger); + color: var(--danger-ink); + } + + .btn-danger:hover { + background: var(--danger); + filter: brightness(1.06); + } + + .icon-btn { + width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: var(--radius-control); + background: transparent; + color: var(--text-muted); + cursor: pointer; + text-decoration: none; + } + + .icon-btn:hover { + background: var(--surface-sunk); + color: var(--text); + } + + .icon-btn svg { width: 20px; height: 20px; } + + .select { + height: 40px; + padding: 0 10px; + border-radius: var(--radius-control); + border: 1px solid var(--border-strong); + background: var(--surface); + color: var(--text); + font-size: var(--text-body); + font-weight: 500; + cursor: pointer; + } + + .select:hover { background: var(--surface-sunk); } + + .stepper { + display: inline-flex; + align-items: center; + height: 40px; + overflow: hidden; + background: var(--surface); + border: 1px solid var(--border-strong); + border-radius: var(--radius-control); + } + + .stepper button { + width: 32px; + height: 100%; + border: 0; + background: transparent; + color: var(--text-muted); + cursor: pointer; + font-size: var(--text-read); + } + + .stepper button:hover { + background: var(--surface-sunk); + color: var(--text); + } + + .stepper button:active { background: var(--surface-sunk); filter: brightness(.97); } + + .stepper input { + width: 56px; + height: 100%; + border: 0; + background: transparent; + color: var(--text); + text-align: center; + font-size: var(--text-body); + font-weight: 600; + font-variant-numeric: tabular-nums; + -moz-appearance: textfield; + appearance: textfield; + } + + .stepper input::-webkit-outer-spin-button, + .stepper input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .stepper input:focus { outline: none; } + + .search-input { + width: 100%; + height: 44px; + padding: 0 16px 0 40px; + border-radius: var(--radius-control); + border: 1px solid var(--border-strong); + background: var(--surface); + color: var(--text); + font-size: var(--text-body); + } + + .search-input::placeholder { color: var(--text-faint); } + + .search-input:focus { + outline: none; + border-color: var(--accent); + } + + /* ---------- Navigation ---------- */ + .nav-pill { + padding: .5rem 1.25rem; + border-radius: 9999px; + font-size: var(--text-body); + font-weight: 500; + color: var(--text-muted); + text-decoration: none; + white-space: nowrap; + } + + .nav-pill:hover { + background: var(--surface-sunk); + color: var(--text); + } + + .nav-pill.active { + background: var(--accent-soft); + color: var(--accent-text); + font-weight: 600; + } + + /* ---------- Lists ---------- */ + .row { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 14px; + font-size: var(--text-body); + } + + .row + .row { border-top: 1px solid var(--border); } + + .row .row-name { flex: 1; min-width: 0; } + + .row-value { + white-space: nowrap; + font-weight: 600; + color: var(--accent-text); + font-variant-numeric: tabular-nums; + } + + .row-note { + color: var(--text-faint); + font-size: var(--text-meta); + } + + /* Recipe ingredient rows: main's tinted row, on the sunk surface. */ + .ingredient-list { list-style: none; margin: 0; padding: 0; } + + .ingredient-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: .5rem .75rem; + border-radius: var(--radius-control); + background: var(--surface-sunk); + } + + .section-label { + font-size: var(--text-label); + font-weight: 600; + letter-spacing: .06em; + text-transform: uppercase; + color: var(--text-faint); + margin: 18px 0 8px; + padding: 0 14px; + } + + /* ---------- Text ---------- */ + .metaline { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 0; + font-size: var(--text-ui); + color: var(--text-muted); + } + + .metaline > *:not(:last-child)::after { + content: "·"; + margin: 0 8px; + color: var(--text-faint); + } + + .metaline b { font-weight: 600; color: var(--text); } + + /* Recipe-reference links. --info is a warm orange, so the underline is + what makes it read as a link. */ + a.text-info { text-decoration: underline; text-underline-offset: 2px; } + + /* ---------- Status ---------- */ + .item-status-dot { + flex: 0 0 auto; + width: 8px; + height: 8px; + border-radius: 50%; + } + + .item-status-dot.in-stock { background: var(--ok); } + + .item-status-dot.low-stock { background: var(--accent); } + + .item-status-dot.out-of-stock { background: var(--danger); } + + /* ---------- Inline recipe entities ---------- */ + /* Weight + tint, not bordered gradient pills, so they stop competing with + the prose they sit inside. */ + .ingredient-badge { + color: var(--accent-text); + font-weight: 600; + white-space: nowrap; + } + + /* Distinguished from an ingredient by texture, not hue, so the split is + visible to red-green deficient readers. */ + .cookware-badge { + color: var(--text); + font-weight: 600; + white-space: nowrap; + text-decoration: underline dotted var(--inactive); + text-underline-offset: 3px; + } + + .timer-badge { + padding: 1px 6px; + border-radius: 5px; + background: var(--surface-sunk); + border: 1px solid var(--border); + color: var(--text); + font-weight: 600; + font-variant-numeric: tabular-nums; + white-space: nowrap; + } + + /* main's 32px circle, on a flat accent tint. */ + .step-number { + flex: 0 0 32px; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent-soft); + color: var(--accent-text); + font-size: var(--text-body); + font-weight: 700; + font-variant-numeric: tabular-nums; + } + + .tag { + display: inline-block; + padding: 1px 8px; + border-radius: 9999px; + font-size: var(--text-meta); + font-weight: 500; + background: var(--surface-sunk); + border: 1px solid var(--border); + color: var(--text-muted); + } + + /* Neutral bordered pill. The metadata-* key classes stay in the markup as + hooks but carry no colour of their own any more. */ + .metadata-pill { + display: inline-flex; + align-items: center; + padding: .25rem .75rem; + border-radius: 9999px; + font-size: var(--text-body); + font-weight: 500; + white-space: nowrap; + background: var(--surface); + border: 1px solid var(--border); + color: var(--text-muted); + } + + .metadata-pill svg { + width: 1rem; + height: 1rem; + margin-right: .5rem; + } + + /* ---------- Recipes index ---------- */ + /* main's tall card: block layout, p-6 content from the template, no + gradient stripe, no scale transform. */ + .recipe-card { + display: flex; + flex-direction: column; + overflow: hidden; + text-decoration: none; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + } + + .recipe-card:hover { border-color: var(--border-strong); } + + .recipe-card-icon { + width: 64px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--surface-sunk); + border: 1px solid var(--border); + font-size: 24px; + margin-bottom: 1rem; + } + + /* An

, not an

: the index has an

and nothing between. */ + .recipe-card-title { + display: block; + margin: 0 0 .5rem; + font-size: var(--text-title); + font-weight: 700; + line-height: 1.35; + color: var(--text); + } + + .recipe-card-sub { + display: block; + font-size: var(--text-meta); + color: var(--text-faint); + } + + /* ---------- Recipe page ---------- */ + .step-list { list-style: none; margin: 0; padding: 0; } + + /* main's per-step box. */ + .step-box { + background: var(--surface-sunk); + border: 1px solid var(--border); + border-radius: var(--radius-card); + padding: 1rem; + } + + /* Step prose keeps main's leading-8 (2.0) on 16px text — the one place + the reading step's line-height is overridden, by decision. */ + .step-body { + font-size: var(--text-read); + line-height: 2; + color: var(--text); + } + + .step-refs { + margin-top: .5rem; + padding-left: 1rem; + border-left: 2px solid var(--accent); + font-size: var(--text-body); + color: var(--text-muted); + } + + .recipe-note { + display: flex; + gap: 8px; + padding: 12px 14px; + border-radius: var(--radius-card); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + background: var(--surface-sunk); + color: var(--text-muted); + font-style: italic; + } + + .image-step { + @apply w-full max-h-80 object-contain rounded-xl; + } + + .recipe-image-placeholder { + background: var(--surface-sunk); + border: 1px solid var(--border); + color: var(--accent-text); + @apply w-30 h-30 rounded-full flex items-center justify-center text-5xl; + } + + /* ---------- Pantry ---------- */ + /* main's four-line stat block. The transparent border keeps the box from + shifting when a state border appears. */ + .pantry-item { + background: var(--surface-sunk); + border: 1px solid transparent; + border-radius: var(--radius-control); + padding: 1rem; + } + + .pantry-item:hover { border-color: var(--border); } + + .pantry-item.out-of-stock { + background: var(--danger-soft); + border-color: var(--danger); + } + + .pantry-item.out-of-stock .quantity-display, + .pantry-item.out-of-stock .out-of-stock-icon { + color: var(--danger); + font-weight: 600; + } + + .pantry-item.low-stock { + background: var(--accent-soft); + border-color: var(--accent); + } + + .pantry-item.low-stock .quantity-display, + .pantry-item.low-stock .out-of-stock-icon { + color: var(--accent-text); + font-weight: 600; + } + + /* Row actions appear on hover or focus, as main's group-hover did. */ + .pantry-actions { opacity: 0; } + + .pantry-item:hover .pantry-actions, + .pantry-item:focus-within .pantry-actions { opacity: 1; } + + /* ---------- Coarse pointers ---------- */ + /* Touch targets per WCAG 2.2 SC 2.5.8. Only active on touch devices. */ + @media (pointer: coarse) { + .btn { height: 44px; } + .select { height: 44px; } + .stepper { height: 46px; } + .stepper button { width: 44px; } + .icon-btn { width: 44px; height: 44px; } + .ref-checkbox, + #list-content input[type="checkbox"] { width: 24px; height: 24px; } + .pantry-actions { opacity: 1; } + } +} diff --git a/static/css/cooking-mode.css b/static/css/cooking-mode.css index 848030b5..cc24740c 100644 --- a/static/css/cooking-mode.css +++ b/static/css/cooking-mode.css @@ -1,10 +1,16 @@ -/* Cooking Mode Overlay */ +/* Cooking Mode Overlay + Colours here resolve through the design tokens, not raw hex. .cooking-overlay + is listed alongside .dark in input.css, so the tokens below always resolve to + their dark values — cook mode is deliberately dark whatever the site theme is, + because it is read at arm's length in a bright kitchen. + Structure follows the rest of the app: flat surfaces, hairline borders, no + elevation. */ .cooking-overlay { position: fixed; inset: 0; z-index: 9999; - background: #1a1a2e; - color: #e0e0e0; + background: var(--bg); + color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; flex-direction: column; @@ -19,7 +25,8 @@ align-items: center; justify-content: space-between; padding: 12px 20px; - background: rgba(0, 0, 0, 0.3); + background: var(--surface); + border-bottom: 1px solid var(--border); flex-shrink: 0; gap: 12px; z-index: 2; @@ -28,7 +35,7 @@ .cooking-header-title { font-size: 16px; font-weight: 600; - color: #fff; + color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -56,40 +63,39 @@ font-weight: 500; white-space: nowrap; cursor: pointer; - background: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.7); - border: 1px solid rgba(255, 255, 255, 0.15); - transition: all 0.2s; + background: var(--surface); + color: var(--text-muted); + border: 1px solid var(--border); } .cooking-section-pill:hover { - background: rgba(255, 255, 255, 0.15); + color: var(--text); + background: var(--surface-sunk); } .cooking-section-pill.active { - background: linear-gradient(135deg, #f97316, #eab308); - color: #fff; - border-color: transparent; + background: var(--accent); + color: var(--accent-ink); + border-color: var(--accent); } .cooking-close-btn { width: 40px; height: 40px; border-radius: 50%; - background: rgba(255, 255, 255, 0.1); - border: none; - color: #fff; + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; - transition: background 0.2s; } .cooking-close-btn:hover { - background: rgba(255, 255, 255, 0.2); + background: var(--surface-sunk); } /* Carousel container */ @@ -121,11 +127,11 @@ overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; - border-radius: 20px; + border-radius: 6px; padding: 32px; transition: transform 0.3s ease-out, opacity 0.3s ease-out; scrollbar-width: thin; - scrollbar-color: rgba(255,255,255,0.2) transparent; + scrollbar-color: var(--border-strong) transparent; } .cooking-card::-webkit-scrollbar { @@ -133,7 +139,7 @@ } .cooking-card::-webkit-scrollbar-thumb { - background: rgba(255,255,255,0.2); + background: var(--border-strong); border-radius: 2px; } @@ -171,19 +177,19 @@ /* Section header card */ .cooking-card-section { - background: linear-gradient(135deg, #1e293b, #0f172a); - border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--surface); + border: 1px solid var(--border); text-align: center; } +/* Was a gradient clipped to the text, which forced -webkit-text-fill-color: + transparent — that renders as an invisible heading anywhere the clip is + unsupported. A solid token has no such failure mode. */ .cooking-card-section h2 { font-size: 32px; font-weight: 700; margin-bottom: 24px; - background: linear-gradient(135deg, #f97316, #eab308); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + color: var(--accent-text); } .cooking-mise-grid { @@ -198,8 +204,9 @@ justify-content: space-between; align-items: baseline; padding: 8px 12px; - background: rgba(255, 255, 255, 0.05); - border-radius: 10px; + background: var(--surface-sunk); + border: 1px solid var(--border); + border-radius: 6px; gap: 8px; cursor: pointer; transition: opacity 0.2s; @@ -212,26 +219,26 @@ .cooking-mise-name { font-size: 18px; - color: #e0e0e0; + color: var(--text); } .cooking-mise-note { font-size: 14px; - color: rgba(255, 255, 255, 0.5); + color: var(--text-faint); font-style: italic; } .cooking-mise-qty { font-size: 16px; font-weight: 600; - color: #f97316; + color: var(--accent-text); white-space: nowrap; } /* Step card */ .cooking-card-step { - background: linear-gradient(135deg, #1e293b, #162032); - border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--surface); + border: 1px solid var(--border); } .cooking-step-number { @@ -241,8 +248,8 @@ width: 28px; height: 28px; border-radius: 50%; - background: linear-gradient(135deg, #f97316, #eab308); - color: #fff; + background: var(--accent); + color: var(--accent-ink); font-weight: 700; font-size: 13px; margin-right: 8px; @@ -261,18 +268,15 @@ .cooking-step-text { font-size: 24px; line-height: 1.6; - color: #f0f0f0; -} - -/* Override badge styles within cooking mode for readability */ -.cooking-step-text .ingredient-badge { - font-size: 22px; - padding: 2px 10px; + color: var(--text); } +/* Scale entity markup up for arm's-length reading. Only .timer-badge has a + background of its own, so it is the only one that needs horizontal padding; + padding on the other two just pushed the surrounding words apart. */ +.cooking-step-text .ingredient-badge, .cooking-step-text .cookware-badge { font-size: 22px; - padding: 2px 10px; } .cooking-step-text .timer-badge { @@ -283,9 +287,9 @@ .cooking-step-ingredients { margin-top: 20px; padding-top: 16px; - border-top: 1px solid rgba(255, 255, 255, 0.1); + border-top: 1px solid var(--border); font-size: 15px; - color: rgba(255, 255, 255, 0.5); + color: var(--text-faint); } .cooking-step-ingredients span { @@ -294,8 +298,8 @@ /* Done card */ .cooking-card-done { - background: linear-gradient(135deg, #1e293b, #0f172a); - border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--surface); + border: 1px solid var(--border); text-align: center; } @@ -306,36 +310,36 @@ .cooking-card-done p { font-size: 20px; - color: rgba(255, 255, 255, 0.6); + color: var(--text-muted); } .cooking-done-close-btn { margin-top: 28px; padding: 12px 40px; - border-radius: 12px; + border-radius: 6px; border: none; - background: linear-gradient(135deg, #f97316, #eab308); - color: #fff; + background: var(--accent); + color: var(--accent-ink); font-size: 18px; font-weight: 600; cursor: pointer; - transition: opacity 0.2s; + transition: filter 0.2s; } .cooking-done-close-btn:hover { - opacity: 0.85; + filter: brightness(1.06); } /* Progress bar */ .cooking-progress { height: 3px; - background: rgba(255, 255, 255, 0.1); + background: var(--surface-sunk); flex-shrink: 0; } .cooking-progress-bar { height: 100%; - background: linear-gradient(90deg, #f97316, #eab308); + background: var(--accent); transition: width 0.3s ease-out; } @@ -364,7 +368,7 @@ width: calc(100% - 24px); padding: 20px 16px; max-height: 75vh; - border-radius: 16px; + border-radius: 6px; } .cooking-card-section h2 { @@ -378,7 +382,11 @@ } .cooking-step-text .ingredient-badge, - .cooking-step-text .cookware-badge, + .cooking-step-text .cookware-badge { + font-size: 16px; + white-space: normal; + } + .cooking-step-text .timer-badge { font-size: 16px; padding: 1px 8px; diff --git a/static/css/custom-styles.css b/static/css/custom-styles.css deleted file mode 100644 index 99f03afb..00000000 --- a/static/css/custom-styles.css +++ /dev/null @@ -1,146 +0,0 @@ -/* Custom styles to enhance Tailwind */ -:root { - --primary-orange: #ff6b35; - --primary-green: #4ade80; - --light-orange: #fed7aa; - --light-blue: #dbeafe; - --light-green: #dcfce7; - --light-yellow: #fef3c7; -} - -/* Recipe cards with colorful borders */ -.recipe-card { - position: relative; - overflow: hidden; -} - -.recipe-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, #ff6b35, #fbbf24, #4ade80); -} - -/* Ingredient badges */ -.ingredient-badge { - background: linear-gradient(135deg, #fef3c7, #fed7aa); - border: 1px solid #fbbf24; - white-space: nowrap; -} - -.cookware-badge { - background: linear-gradient(135deg, #dcfce7, #bbf7d0); - border: 1px solid #4ade80; - white-space: nowrap; -} - -.timer-badge { - background: linear-gradient(135deg, #fee2e2, #fecaca); - border: 1px solid #f87171; - white-space: nowrap; -} - -/* Enhanced buttons */ -.btn-primary { - background: linear-gradient(135deg, #ff6b35, #f97316); - box-shadow: 0 4px 14px 0 rgba(255, 107, 53, 0.3); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px 0 rgba(255, 107, 53, 0.4); -} - -/* Search input with colorful focus */ -.search-input:focus { - border-color: #ff6b35; - box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1); -} - -/* Recipe image placeholder with gradient */ -.recipe-image-placeholder { - background: linear-gradient(135deg, #ff6b35, #fbbf24); - width: 120px; - height: 120px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 3rem; -} - -/* Step numbers with colorful circles */ -.step-number { - width: 32px; - height: 32px; - background: linear-gradient(135deg, #ff6b35, #f97316); - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - flex-shrink: 0; -} - -/* Tags styling */ -.tag { - display: inline-block; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.75rem; - font-weight: 500; - margin-right: 0.5rem; - background: linear-gradient(135deg, #fbbf24, #fb923c); - color: white; - box-shadow: 0 2px 4px rgba(251, 146, 60, 0.2); -} - -/* Navigation pills */ -.nav-pill { - padding: 0.5rem 1.25rem; - border-radius: 9999px; - transition: all 0.3s; - font-weight: 500; - color: #6b7280; - position: relative; -} - -.nav-pill:hover { - background: linear-gradient(135deg, #fef3c7, #fed7aa); - color: #ea580c; - transform: translateY(-2px); - box-shadow: 0 4px 6px rgba(234, 88, 12, 0.1); -} - -.nav-pill.active { - background: linear-gradient(135deg, #ff6b35, #f97316); - color: white; - font-weight: 700; - box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3); -} - -/* Recipe metadata pills */ -.metadata-pill { - display: inline-flex; - align-items: center; - padding: 0.375rem 0.875rem; - border-radius: 9999px; - background: linear-gradient(135deg, #f9fafb, #f3f4f6); - border: 1px solid #e5e7eb; - font-size: 0.875rem; - color: #374151; - font-weight: 500; - line-height: 1.25rem; - white-space: nowrap; -} - -.metadata-pill svg { - width: 1rem; - height: 1rem; - margin-right: 0.5rem; -} \ No newline at end of file diff --git a/static/css/input.css b/static/css/input.css index cace11d6..52e740b2 100644 --- a/static/css/input.css +++ b/static/css/input.css @@ -1,217 +1,265 @@ @import "tailwindcss"; -/* Tailwind v4 is CSS-first and no longer auto-discovers tailwind.config.js. - @config keeps the existing JS config (custom colours, gradient keyframes, - darkMode: 'class' and the content globs) authoritative. */ -@config "../../tailwind.config.js"; - -/* Custom component classes */ -@layer components { - /* Recipe cards with colorful borders */ - .recipe-card { - @apply relative overflow-hidden; - } - - .recipe-card::before { - content: ''; - @apply absolute top-0 left-0 right-0 h-1; - background: linear-gradient(90deg, #ff6b35, #fbbf24, #4ade80); - } - - /* Ingredient badges */ - .ingredient-badge { - @apply inline-block align-middle px-2 py-0.5 rounded-full text-sm font-medium whitespace-nowrap; - background: linear-gradient(135deg, #fef3c7, #fed7aa); - @apply text-orange-700 border border-orange-400; - } - - .cookware-badge { - @apply inline-block align-middle px-2 py-0.5 rounded-full text-sm font-medium whitespace-nowrap; - background: linear-gradient(135deg, #dcfce7, #bbf7d0); - @apply text-green-900 border border-green-400; - } - - .timer-badge { - @apply inline-block align-middle px-2 py-0.5 rounded-full text-sm font-medium whitespace-nowrap; - background: linear-gradient(135deg, #fee2e2, #fecaca); - @apply text-red-900 border border-red-400; - } - - /* Enhanced buttons */ - .btn-primary { - background: linear-gradient(135deg, #ff6b35, #f97316); - @apply shadow-lg hover:-translate-y-0.5 transition-all; - } - - .btn-primary:hover { - box-shadow: 0 6px 20px 0 rgba(255, 107, 53, 0.4); - } - - /* Step numbers with colorful circles */ - .step-number { - @apply w-8 h-8 flex items-center justify-center rounded-full text-white font-bold shrink-0; - background: linear-gradient(135deg, #ff6b35, #f97316); - } - - /* Tags styling */ - .tag { - @apply inline-block px-3 py-1 rounded-full text-xs font-medium mr-2; - background: linear-gradient(135deg, #fbbf24, #fb923c); - @apply text-white shadow-xs; - } - - /* Navigation pills */ - .nav-pill { - @apply px-5 py-2 rounded-full transition-all font-medium text-gray-500 relative; - } - - .nav-pill:hover { - background: linear-gradient(135deg, #fef3c7, #fed7aa); - @apply text-orange-700 -translate-y-0.5 shadow-md; - } - - .nav-pill.active { - background: linear-gradient(135deg, #ff6b35, #f97316); - @apply text-white font-bold shadow-lg; - } - - /* Recipe metadata pills - simple outline style */ - .metadata-pill { - @apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium whitespace-nowrap border bg-white; - } - - .metadata-servings { - @apply text-gray-600 border-gray-300; - } - - .metadata-time { - @apply text-blue-600 border-blue-300; - } - - .metadata-difficulty { - @apply text-yellow-600 border-yellow-300; - } - - .metadata-course { - @apply text-orange-600 border-orange-300; - } - - .metadata-prep { - @apply text-indigo-600 border-indigo-300; - } - - .metadata-cook { - @apply text-red-600 border-red-300; - } - - .metadata-cuisine { - @apply text-green-600 border-green-300; - } - - .metadata-diet { - @apply text-lime-600 border-lime-300; - } - - .metadata-author { - @apply text-pink-600 border-pink-300; - } - - .metadata-custom { - @apply text-purple-600 border-purple-300; - } - - /* Search input with colorful focus */ - .search-input:focus { - @apply border-primary-orange; - box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1); - } - - /* Step images in recipes */ - .image-step { - @apply w-full max-h-80 object-contain rounded-xl; - } - - /* Recipe image placeholder with gradient */ - .recipe-image-placeholder { - background: linear-gradient(135deg, #ff6b35, #fbbf24); - @apply w-30 h-30 rounded-full flex items-center justify-center text-white text-5xl; - } - - /* Pantry item styles with improved dark mode support */ +/* Tailwind v4 is CSS-first. The two things tailwind.config.js used to own — + class-based dark mode and the list of files to scan for class names — are + declared here instead. */ +@custom-variant dark (&:where(.dark, .dark *)); + +@source "../../templates"; +@source "../../static/js"; +/* The one Rust file that emits class names: method_classes() for the API + docs method badge. */ +@source "../../src/web/templates.rs"; + +/* ============================================================ + DESIGN TOKENS + Seventeen semantic colour names plus radii and shadows, defined once per + theme. Every colour in the UI resolves through one of these. Do not + introduce raw hex values or Tailwind palette utilities in templates. + --accent-ink is the foreground for text sitting ON a filled --accent + background: the design system's near-black, which clears AA on the + accent fill where white would not. + ============================================================ */ +:root { + color-scheme: light; + + --bg: #fcfcfb; + --surface: #ffffff; + --surface-sunk: #f5f3f0; + --border: #e4e0da; + --border-strong: #c3bcb1; + --text: #16161d; /* DS Text/Primary */ + --text-muted: #5f5a51; /* DS Text/Secondary, darkened for AA */ + --text-faint: #6a645b; + --accent: #e15a29; /* DS Controls/Primary, Icons/Primary */ + --accent-text: #715329; /* DS Text/Tags */ + --accent-soft: #f5dacf; /* DS Background/UI One */ + --accent-ink: #16161d; + --ok: #3d6849; + --ok-soft: #e2e8df; + --danger: #c4261c; /* DS Text/Warning, darkened for AA */ + --danger-soft: #f7dfdc; + --danger-ink: #ffffff; + /* Links. The design system is entirely warm, so this is the dark end of + the DS orange rather than a blue; every link also carries an underline + so the affordance never rests on hue alone. */ + --info: #8a3d14; + --disabled: #d3cdcb; /* DS Controls/Disabled */ + --inactive: #8a8075; /* DS Controls/Inactive, darkened for AA */ + + --radius-control: 6px; + --radius-card: 6px; + + /* Two elevations. In-flow surfaces get --shadow-card, which is barely a + shadow at all — the border does the work. --shadow-overlay is only for + things floating above the page: dropdowns, dialogs, search results. */ + --shadow-card: 0 1px 0 rgba(27, 31, 36, .04); + --shadow-overlay: 0 8px 24px rgba(27, 31, 36, .12); +} + +/* .cooking-overlay is listed here deliberately: cook mode is always dark + regardless of the site theme, so its inline entity badges must resolve the + dark token values or they render at ~2.5:1 on the dark card. */ +.dark, +.cooking-overlay { + color-scheme: dark; + + --bg: #16161d; + --surface: #1c1c24; + --surface-sunk: #23232c; + --border: #30303b; + --border-strong: #43434f; + --text: #efeae6; + --text-muted: #ada69b; + --text-faint: #948d83; + --accent: #e15a29; + --accent-text: #f08050; + --accent-soft: #3a2820; + --accent-ink: #16161d; + --ok: #6fb283; + --ok-soft: #1e2a22; + --danger: #ff6b60; + --danger-soft: #2e1b1a; + --danger-ink: #16161d; + --info: #e59a6d; + --disabled: #4a4a55; + --inactive: #8f8880; + + --shadow-card: none; + --shadow-overlay: 0 8px 24px rgba(0, 0, 0, .5); +} + +/* Register tokens with Tailwind so they generate real utilities + (bg-surface, text-muted, border-line, …). `inline` makes the generated + utilities reference the custom property, so they flip automatically under + .dark with no override rules. Border tokens are named `line` so the + utility reads `border-line` rather than `border-border`. */ +@theme inline { + --color-bg: var(--bg); + --color-surface: var(--surface); + --color-sunk: var(--surface-sunk); + --color-line: var(--border); + --color-line-strong: var(--border-strong); + --color-text: var(--text); + --color-muted: var(--text-muted); + --color-faint: var(--text-faint); + --color-accent: var(--accent); + --color-accent-text: var(--accent-text); + --color-accent-soft: var(--accent-soft); + --color-accent-ink: var(--accent-ink); + --color-ok: var(--ok); + --color-ok-soft: var(--ok-soft); + --color-danger: var(--danger); + --color-danger-soft: var(--danger-soft); + --color-danger-ink: var(--danger-ink); + --color-info: var(--info); + --color-disabled: var(--disabled); + --color-inactive: var(--inactive); +} + +/* ============================================================ + TYPE SCALE + Seven steps, each with a role and a line-height chosen for that role. + `display` is 30px — the size main's page titles have always been — the + rest are the PR #456 scale. Tailwind's own names are aliased onto the + steps so a stray `text-sm` cannot drift off the scale. + ============================================================ */ +@theme { + --text-display: 30px; + --text-display--line-height: 1.2; + --text-display--letter-spacing: -.01em; + + --text-title: 18px; + --text-title--line-height: 1.35; + --text-title--letter-spacing: -.005em; + + --text-read: 16px; + --text-read--line-height: 1.6; + + --text-body: 14px; + --text-body--line-height: 1.5; + + --text-ui: 13px; + --text-ui--line-height: 1.4; + + --text-meta: 12px; + --text-meta--line-height: 1.4; + + --text-label: 11px; + --text-label--line-height: 1.3; + --text-label--letter-spacing: .06em; + + --text-xs: var(--text-meta); + --text-xs--line-height: 1.4; + --text-sm: var(--text-body); + --text-sm--line-height: 1.5; + --text-base: var(--text-read); + --text-base--line-height: 1.6; + --text-lg: var(--text-title); + --text-lg--line-height: 1.35; + --text-2xl: var(--text-display); + --text-2xl--line-height: 1.2; + --text-3xl: var(--text-display); + --text-3xl--line-height: 1.2; +} + +body { + font-size: var(--text-body); + line-height: 1.5; +} + +/* Component vocabulary. Kept in its own file so this one stays the small, + stable theme contract. */ +@import "./components.css"; + +/* Print: force the light token values regardless of the active theme. + Layout flattening lives in base.html's print block. */ +@media print { + :root, + .dark, + .cooking-overlay { + --bg: #ffffff; + --surface: #ffffff; + --surface-sunk: #f5f4f2; + --border: #dddad5; + --border-strong: #bbb6ae; + --text: #111111; + --text-muted: #444444; + --text-faint: #666666; + --accent: #a8380c; + --accent-text: #a8380c; + --accent-soft: #f7ece5; + --accent-ink: #ffffff; + --ok: #23624a; + --ok-soft: #eaf3ee; + --danger: #96201a; + --danger-soft: #f8eae9; + --danger-ink: #ffffff; + --info: #1f4f86; + --disabled: #cccccc; + --inactive: #767676; + + --shadow-card: none; + --shadow-overlay: none; + } + + .card, + .btn, + .stepper, + .nav-card, + .recipe-card, + .step-box, + .ingredient-row, .pantry-item { - @apply bg-gray-50 dark:bg-gray-800; - } - - .pantry-item:hover { - @apply bg-gray-100 dark:bg-gray-700; - } - - /* Out-of-stock pantry items with better dark mode contrast */ - .pantry-item.out-of-stock { - @apply bg-red-50 border border-red-200 dark:bg-red-950/30 dark:border-red-900/50; - } - - .pantry-item.out-of-stock:hover { - @apply bg-red-100 dark:bg-red-950/40; - } - - /* Pantry text colors for dark mode */ - .pantry-item .text-gray-900 { - @apply dark:text-gray-100; - } - - .pantry-item .text-gray-600 { - @apply dark:text-gray-400; - } - - .pantry-item .text-gray-500 { - @apply dark:text-gray-400; - } - - /* Out-of-stock text in dark mode */ - .pantry-item.out-of-stock .quantity-display { - @apply text-red-600 dark:text-red-400; - } - - /* Status dots with dark mode support */ - .item-status-dot.in-stock { - @apply bg-green-500 dark:bg-green-400; - } - - .item-status-dot.out-of-stock { - @apply bg-red-500 dark:bg-red-400; - } - - /* CodeMirror editor dark mode support */ - .dark .cm-editor { - @apply bg-gray-900; - } - - .dark .cm-gutters { - @apply bg-gray-800 border-gray-700; - } - - .dark .cm-lineNumbers .cm-gutterElement { - @apply text-gray-400; - } - - .dark .cm-activeLineGutter { - @apply bg-gray-700; - } - - .dark .cm-activeLine { - @apply bg-gray-800/50; - } - - .dark .cm-content { - caret-color: white; - } - - .dark .cm-cursor { - border-left-color: white; - } - - .dark .cm-selectionBackground { - @apply bg-blue-800/50!; - } -} \ No newline at end of file + border: 1px solid var(--border) !important; + box-shadow: none !important; + } + + .card, + .step-box { + break-inside: avoid; + } +} + +/* ============================================================ + CODEMIRROR + Token-driven in BOTH themes. These rules live OUTSIDE @layer components on + purpose: CodeMirror injects its base theme as an unlayered - +
-