# Phase 20 — Masthead & navigation polish **Source:** `TODO.md` L4, L5, L6, L10, L11 — "masthead row layout, un-sticky bar, hamburger animation, always-visible admin links" **Story:** n/a **Context:** `src/dashboards/web/static/dash.css` (`.masthead` at line 99 — `position: sticky; top: 0; z-index: 40; display: flex; height: var(--header-h)`; `.masthead-right` at line 148 — `margin-inline-start: auto`; `#nav-toggle` at line 190 — the `--nav-toggle-display` token reveal; the ≤640px block at lines 208–280 — masthead wrap, `masthead-right { justify-content: flex-end }`, the `nav.tabs` dropdown at `top: 100%` under the sticky bar, the `env(safe-area-inset-top, 0px)` padding line), `src/dashboards/web/static/app.js` (the `setNavMenu()` funnel at lines 695–722 — four close paths, `matchMedia('(max-width: 640px)')`), `src/dashboards/web/pages/*.html` (the masthead in every page: brand, `#nav-toggle`, `nav.tabs#nav-tabs`, `.masthead-right` holding `#nav-status` + clock + `⚷ Certs` + `⚙ Account` + theme toggle; `certs.html` lacks `⚷ Certs`, `settings.html` lacks `⚙ Account` — the phase-15 "no self-link" rule this phase inverts), `tests/test_api.py` (`test_every_page_links_to_both_admin_pages` at line 999, `test_the_disks_tab_is_in_the_masthead_of_every_page` at line 3913, `test_the_nav_toggle_is_in_the_masthead_of_every_tabbed_page` at line 3957) ## Objective The masthead becomes a three-row bar at **all widths** — row 1 the brand and the five dashboard tabs, row 2 the per-source status indicators left-justified in their own row, row 3 the `⚷ Certs` / `⚙ Account` / `☾ Dark` controls left-justified in their own row under the indicators — the bar stops being sticky (it scrolls away, maximising data screen space), the hamburger menu slides out and back in (180 ms, the `brain_of_reese` pattern, stilled under `prefers-reduced-motion`), and both admin links stay visible on every page including the two admin pages themselves. The served page markup is unchanged except the two added admin links; the tab structure (five links, one `aria-current`), the `#nav-toggle` token reveal, the safe-area padding line and the four close paths all stay exactly as phase 18 pinned them. ## Dependencies - `19_ups_events_expandable` (todo) — both phases touch `ups.html` (the masthead, the events card); landing 19 first keeps this phase's diff confined to the masthead. ## Tasks 1. `01_two_row_masthead.md` — the three-row left-justified masthead at all widths (CSS-only reflow; the mobile block re-cut for the new rows) 2. `02_unsticky_masthead.md` — the bar scrolls away: `sticky` → `relative`, the dropdown anchor kept 3. `03_hamburger_slide.md` — the 180 ms slide+fade on the ≤640px dropdown, stilled under `prefers-reduced-motion` 4. `04_admin_links_visible.md` — `⚷ Certs` on `/certs`, `⚙ Account` on `/settings`; the "no self-link" pin inverted to "all eight pages link to both" ## Testing & Quality - Unit/integration: `tests/test_api.py` — the served-page pins (task 04's rewritten admin-link test over all eight non-login pages, the unchanged tab-structure pins at lines 3913/3957, the CSS pins for the row layout and the un-sticky change), the unchanged `setNavMenu` behaviour - Coverage: **>90%** on new/modified code (the phase is markup + JS + CSS; the gate applies to the changed Python test files) - The phase is "Complete" only when the **entire** project test suite passes 100% (every test in the project, not just this phase's) ## Documentation & Consistency - The final pass reviews `README.md` and `AGENTS.md` in full against the project as it stands after this phase — fixing every inconsistency found, **including pre-existing ones not caused by this phase**. AGENTS.md's masthead paragraph (the phase-18 "specificity ladder" and the `top: 100%`-under-the-sticky-bar wording) and the phase-15 "neither admin page links to itself" sentence (gap 12's "now questioned" item — this phase is the decision) both get rewritten to the new state in the section's voice. ## Completion Criteria - [ ] at 1280 px and at 360 px, every tabbed page and both admin pages render the three-row masthead: brand + tabs, left-justified indicators row, left-justified controls row under it (screenshots in `data/logs/`, invariant 10) - [ ] scrolling any page at 1280 px and 360 px leaves the bar behind (no sticky offset), while the mobile dropdown still opens directly under the bar - [ ] at 360 px the hamburger menu slides out and back in (180 ms); with `prefers-reduced-motion` it snaps - [ ] `/certs` renders `⚷ Certs` and `/settings` renders `⚙ Account`, before the theme toggle and outside `.tabs`, exactly like the five dashboards - [ ] `uv run pytest` green, coverage not down, `ruff` + `pyright` clean - [ ] `bash .agents/validate.sh` green (including `node --check` of the modified `app.js`) - [ ] no behavior change in completed work: the tab structure, the token reveal, the safe-area padding line, the four close paths, the status dots and the theme toggle are all unchanged