# Task 3 — site shell: nav state, ears, runs feeds table, FOUC, faster navigation Worktree: `/home/thallada/workspace/the-daily-epub-ui3` (branch `ui-shell`). Dev port 3603. Files you own: `src/web/templates/layout.html`, `src/web/mod.rs` (static assets, headers), `src/web/public.rs`, `src/web/issue.rs` (page/nav plumbing only, not the `Toc`), `src/web/templates/dashboard/run.html`, the "dashboard shell" and `@layer base` parts of `src/web/tailwind.css`, and `src/web/static/theme.js`. Read the shared brief first. ## 1. "Latest" is highlighted on archived issues (operator issue 5) Every issue page (`public::show_issue`, `issue::render_full`, `issue::article`, `world`, `behind`) passes `"latest"` as `active_nav`, so viewing `/issues/2026-09-01` from the archive highlights *Latest* in the site nav. Highlight *Archive* instead whenever the issue is not the newest one. Cleanest: `issue::load` (which has the db) records `is_latest` on `IssueView` via `db.latest_issue_date()`, and each renderer chooses `"latest"`/`"archive"` from it; the public branch in `show_issue` needs the same. Add a router test that fetches an older issue and asserts `aria-current="page"` is on the Archive link and not on Latest (and the reverse for the newest issue). ## 2. "Morning edition" ears on non-issue pages (operator issue 10) `layout.html`'s default `{% block ears %}` prints "Morning edition" — it shows on settings and every other dashboard/account page where it is meaningless. Make the default empty and let issue pages keep overriding it with their dateline. On dashboard pages show a short "Dashboard" label there instead (only when `page.is_admin()` and the active nav is a dashboard section) so the row does not look orphaned; anything else stays empty. Check the row keeps its height and the theme toggle stays right-aligned when the left cell is empty. `issue_public.html`'s empty-state hero may keep its own "Morning edition" eyebrow. ## 3. Runs page "Feeds (top 20)" overflow (operator issue 6) `dashboard/run.html`: the Feeds card wraps its table in `.scroll-x`, whose `> table { min-width: max-content }` lets long feed titles push the *entries* column out of the card until the reader scrolls sideways. That table must fit the card: `table-fixed w-full`, the feed cell `truncate` (nowrap + ellipsis) with the full name in a `title` attribute, entries column a fixed narrow width, no horizontal scroll container (or a `.scroll-x` variant that allows shrinking — your call, but don't break the other tables that rely on `max-content`). Check the Timings and Provider usage cards next to it still look right, and the dashboard overview if it has a similar box. ## 4. Flash of unstyled content (operator issue 8) The operator sees a brief flash of unstyled content when navigating between pages, "or maybe just sometimes". Reproduce before fixing: with the dev server, use Playwright to capture the first frames of a navigation with the cache disabled and a throttled network (there are older experiments `flash.mjs` / `flash2.mjs` in the screenshot directory named in the shared brief you may crib from) and note what actually flashes: the fallback serif before Newsreader loads (`font-display: swap`), a theme flip, the TOC panel, restored `details` state, or something else. Then fix the causes you confirm. Expected fixes: - Static assets are already versioned by content hash (`?v={{ page.asset_version }}`), so serve `/static/*` with `Cache-Control: public, max-age=31536000, immutable` (update `static_assets_use_content_hash_etags`); keep the ETag. - Fonts: preload both Newsreader files, and change `font-display` from `swap` to `block` (no visible fallback; with the preload and immutable cache the block period is only ever paid once) — or `optional` if you can show it behaves better here. Explain the choice in the handoff. - Anything initialised by app.js at the end of `` that changes layout (e.g. `details[id]` open state restored from localStorage) should either be decided pre-paint in `theme.js` or made not to shift layout. ## 5. Faster, SPA-like navigation without a client router (operator issue 9) The operator wants link clicks to feel like an instant swap rather than a full reload, but wants to decide on HTMX (boost mode) in a separate effort. **Do not** write a fetch-and-swap router and do not add HTMX. Deliver the standards-based progressive enhancements that get most of the way there: - **Cross-document view transitions**: `@view-transition { navigation: auto; }` in `tailwind.css` with a short (~120–150 ms) cross-fade of the page and the header kept stable (`view-transition-name` on the site header so the masthead and nav do not fade; the reader's sticky TOC bar may also stay put). Respect reduced motion (`@media (prefers-reduced-motion: reduce)` → no animation). - **Speculation rules** for prerender/prefetch on hover: inline `