Files
dashboards/.agents/phases/complete/18_nav_hamburger/02_dropdown_css.md
T
ducoterra 10f86772ee phase: 18_nav_hamburger
**Phase 18 — collapse the nav bar into a hamburger: complete, all green.**

Verified (criteria):
- ≤640px: the tab links leave the bar, the ☰ is visible, and it opens a dropdown
  under the sticky masthead (`top: 100%`, `z-index: 41`, viewport-wide) with the
  five stacked 40px links; above 640px the one-row desktop masthead is unchanged
  and the button hidden — `data/logs/nav-task04-verify.cjs` 24/24 and
  `data/logs/nav-final-pass.cjs` 45/45; screenshots
  `data/logs/nav-hamburger-{closed,open}-360.png`, `data/logs/nav-masthead-desktop-1280.png` ✓
- The four close paths: the button again, a tab click (navigates and closes),
  Esc (focus back on the opener), and `matchMedia` resize back above the
  breakpoint (drops a stale `.is-open`; `aria-expanded` tracks state on every
  path through the single `setNavMenu()` funnel) ✓
- `uv run pytest` → 1454 passed, 15 skipped; `bash .agents/validate.sh` → exit 0
  (coverage 98.94% ≥ 94, ruff clean, pyright 0 errors, `node --check` + hygiene
  clean); served-page pins: button on all seven tabbed pages and nowhere on
  /login, CSS and binding compared on the 640px literal ✓

Real-device finding (mobile Chrome): the emulated 360px pass was blind to the
system top-edge zone — Android Chrome is edge-to-edge by default, so the sticky
bar started under the status bar and the first-row ☰ sat in a region the device
does not deliver taps to (unreachable, not unbound; the same symptom brain of
reese fixed in production). The ≤640px block now pads the bar by
`env(safe-area-inset-top, 0px)` (the block's `padding-block` value restated on
the top side), verified on the operator's phone; `env()` resolves to 0 on every
non-notch surface, so all other widths render byte-identical. The served-byte
pin's rule loop carries the line; CDP's `safeAreaInsets` override does not feed
`env()`, which is the honest limit of the emulated proof.

Docs: AGENTS.md *UI conventions* masthead entry extended with the three pins,
the drift-comparison, the safe-area rule, and the real-device verification
caveat; README's `web/` bullet names the ≤640px collapse with the desktop
masthead unchanged above the breakpoint.

Deviations: pinned the shipped token shape (`#nav-toggle { display:
var(--nav-toggle-display) }` + the `:root` token) instead of the task file's
literal `.nav-toggle { display: none }` — that literal loses to
`button.ghost { display: inline-flex }` (task 01 measured it); the test
docstring says so, so nobody "restores" the broken shape. `uv pip list
--outdated` still advisory (fastapi 0.153.0 → 0.156.2); this phase moved no
dependency. NOTES.md (emptied) / TODO.md (operator items added) are
harness-side changes — left in the working tree, as in phase 17.

Next pending phase: `.agents/phases/todo/19_ups_events_expandable/`.

LLM-Generated: true
2026-10-20 16:47:10 -04:00

3.8 KiB

Task 02 — The ≤640px dropdown CSS

Phase: 18_nav_hamburger · Source: TODO.md:3 — "The nav bar needs to collapse into a hamburger like brain of reese does" Story: n/a

Objective

The CSS that makes the hamburger real at ≤640px: the toggle button visible, the inline tab nav hidden, the .is-open nav rendered as a dropdown under the sticky masthead, and the masthead wrapping to two rows so the masthead-right cluster does not overflow a phone. Above 640px nothing changes.

Work

  1. src/dashboards/web/static/dash.css — the base .nav-toggle { display: none; } rule already landed with task 01's markup (the button is invisible on every width until this task's block). Extend the existing @media (max-width: 640px) block at line 286 (or add a sibling block — the file's convention is one block per concern; a /* mobile nav */-commented block beside the masthead section is the cleaner home):
    • .masthead { height: auto; min-height: var(--header-h); flex-wrap: wrap; } — the single-row fixed-height masthead cannot hold brand + toggle + the whole masthead-right cluster at 360px; two rows, first row brand + toggle, second row the cluster.
    • .nav-toggle { display: inline-flex; align-items: center; } (it is a button.ghost, so the outlined style is inherited — no second palette, the file's rule).
    • nav.tabs { display: none; } and nav.tabs.is-open { display: flex; flex-direction: column; gap: 0; position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-raised); border-bottom: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0, 0, 0, .35); padding: .4rem; z-index: 41; } — the dropdown. The masthead is position: sticky, which is a non-static containing block for its absolute descendants, so top: 100% pins it directly under the (wrapped, two-row) masthead. z-index: 41 is one above the masthead's 40 so the dropdown sits on the page content.
    • nav.tabs.is-open a { padding: .6rem .7rem; border-radius: var(--radius-sm); } — touch-sized rows; the existing :hover/[aria-current="page"] rules carry over unchanged.
    • Optional at most: a 120ms opacity/translateY transition on nav.tabs.is-open, disabled inside the existing @media (prefers-reduced-motion: reduce) block at line 938 (add the dropdown selector to that block's list of motion-killers if used). If in doubt, ship without the transition.
  2. ASSUMPTION: the dropdown is a single full-width column under the masthead (brain's shape, adapted: brain hides the nav in a #app-nav container; here the nav element itself is the container and gets id="nav-tabs" + .is-open). No separate menu panel element.
  3. ASSUMPTION: no body-level marker class (brain's nav-menu-open exists for its sticky bottom chat cluster, which the dashboards do not have — the dropdown competes with nothing; do not copy a side effect nobody needs).

Testing & Quality

  • Integration: the served-stylesheet pins land in task 04 (base rule present, ≤640px block contains the toggle-visible + tabs-hidden + .is-open rules); the untouched node --check gate passes (CSS is not checked, but confirm no syntax error by loading the page in the browser — task 04's verification)
  • Coverage: >90% on modified code (CSS only; the gate's pytest/ruff/pyright must stay green)

Completion Criteria

  • at a 360px-wide browser window: the tab links are gone from the row, the ☰ button is visible, and toggling .is-open (by hand in devtools) shows the full-width dropdown under the masthead with the five links stacked and the aria-current tab highlighted
  • at 700px and 1200px the masthead is byte-identical in layout to before (one row, inline tabs, no button)
  • the dropdown does not trap or cover the masthead-right cluster, and the page content scrolls under it
  • bash .agents/validate.sh green