# Step 2 — Tailwind v4 design system, theme toggle, reader pages
Worktree: `/home/thallada/workspace/the-daily-epub-v2b` (branch `v2-frontend`).
Read `00-shared.md` first, especially the design language — it is the spec.
You own the design system and the **reader-facing** templates. Do not restyle
`src/web/templates/dashboard/*` (step 3 does that, on top of your system); they
may look plain until then, which is expected. Do not implement the sidebar
table of contents (step 4).
## 1. Tailwind toolchain (no Node at build or run time)
- `package.json` at the repo root with `devDependencies`
`tailwindcss` and `@tailwindcss/cli` pinned to `4.3.3`, scripts
`css` (`tailwindcss -i src/web/tailwind.css -o src/web/static/app.css --minify`),
`css:watch`, and `css:check` (build to a temp file and `diff` against the
committed `app.css`, non-zero on drift). `package-lock.json` committed;
`node_modules/` and `dev/` added to `.gitignore`.
- The compiled `src/web/static/app.css` is **committed** and embedded with
`include_str!` exactly as today, so `cargo build` and the server never need
Node. Document the workflow (edit templates/`tailwind.css` → `npm run css` →
commit both) in `README.md` and in `docs/runbooks/web-dashboard-rollout.md`.
- Fonts: `src/web/static/fonts/Newsreader.woff2`, `Newsreader-italic.woff2` and
`OFL.txt` are already in your worktree. Serve them from
`web::static_asset` (`include_bytes!`, `font/woff2`, same ETag/304 logic,
long cache) — extend the route so `/static/fonts/Newsreader.woff2` or
`/static/Newsreader.woff2` works (pick one, keep `safe` matching: a fixed
match arm per file, no path joins). `@font-face` in `tailwind.css` with
`font-weight: 200 800; font-stretch: normal; font-display: swap;` and the
`unicode-range` for latin from Google's CSS
(`U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD`).
Add a `` for the
regular face in the layout.
## 2. Theme toggle (item 2)
- `src/web/static/theme.js` (new, served like `app.js`, `include_str!`): a
handful of lines run **synchronously in `
`** before paint: read
`localStorage.theme` (`"light"`/`"dark"`, anything else = system), set
`document.documentElement.dataset.theme` accordingly (remove the attribute for
system). Wrap storage access in try/catch. No inline script (CSP).
- The toggle button in the layout's "ears" row: `