Restructure the shared SiteHeader into a three-zone information architecture — brand far-left, Board·Graph optically centered, Docs·GitHub·CTA·theme far-right — plus an accessible mobile disclosure and a stacking-context fix

0046-site-header-three-zone-information-architecture

decision read as Explain confidence verified status active 2026-06-17 owner forward-deployed-engineer
Reversibility
two-way door

DEC-0046 — SiteHeader three-zone information architecture (+ accessible mobile disclosure + stacking-context fix)

Decided by the user (2026-06-17) and built + verified this turn by the Principal Forward Deployed Engineer (with Principal UX Engineer / Principal SvelteKit Engineer surface ownership), in packages/app/src/lib/components/SiteHeader.svelte. Recorded automatically in the same turn (the standing rule). confidence: verified — built, type-clean, and the three-zone layout, the mobile disclosure (open/close, Escape, focus-restore, aria), and the see-through-panel fix were all confirmed in a real browser across desktop/wide/mobile in both themes.

Context

The shared SiteHeader — the single source-of-truth top bar composed by every chrome-light surface (landing via LandingPage.svelte, board + graph via the (app) group layout) — was a two-part bar: brand on the left, and one right-aligned <nav> holding every link (Board, Graph, Docs, GitHub) plus the CTA and the theme toggle. Everything competed for the same right cluster, so the live in-site surfaces (Board, Graph) read as equal-weight with the context-leaving links (Docs, GitHub), and the bar had no clear center of attention. The mobile treatment was a weak flex-wrap — the bar's contents just wrapped to a second line — which is neither a real navigation pattern nor accessible.

The user asked (2026-06-17): "the header needs polished. larger logo. Keep board and Graph in the middle and docs, github and theme switcher can be far right and logo far left. The mobile nav can use some work to. Especially on the landing page" — plus a follow-up: "make the width the content can span in the header wider." The "especially on the landing page" qualifier pointed at a concrete latent bug surfaced below.

Options considered

  1. Keep the two-part bar; just enlarge the logo and tidy the right cluster. Lowest effort; rejected — it does not satisfy the explicit placement ask (Board/Graph centered, Docs/GitHub/theme far right, brand far left) and leaves the live surfaces visually indistinguishable from the leave-the-context links.
  2. Center the nav by floating it next to the brand (flex with margin: auto). Rejected — that centers the nav against the remaining space beside the brand, not against the header, so the center group drifts as the brand or right cluster grow; it only looks centered at one width.
  3. Fork a per-surface header so the landing can differ from board/graph. Rejected outright — it violates the standing atomic / fully-open-by-default rule (Site chrome unified into shared SiteHeader + SiteFooter + ThemeToggle components, and the docs/KB surface made fully open — DEC-0022's DOCS_ENABLED landing-only gate is removed so /knowledge ships publicly alongside the landing/board/graph) and DEC-0045's single-mount guarantee; per-surface variation is already expressible as props (cta, badge, wordmark).
  4. Three-zone 1fr auto 1fr grid information architecture (CHOSEN). A 3-column grid: brand (justify-self: start) · center nav (grid-column: 2) · right cluster (justify-self: end). The two 1fr rails are equal, so the auto center track sits in the header's true middle regardless of how wide the brand or right cluster grow — true optical centering, not float-next-to-brand. Board/Graph occupy the center; Docs↗/GitHub↗ + CTA + theme retire to the right cluster. Remains one shared component (props carry per-surface variation), so the IA lands everywhere at once.

Decision

Restructure SiteHeader.svelte into the three-zone grid (option 4), and in the same edit make four supporting changes the polish pass required:

  1. Three-zone IA (1fr auto 1fr). LEFT zone .site-header__brand = the wordmark (+ optional read-only badge). CENTER zone .site-header__center (<nav aria-label="Primary">) = centerLinks (Board, Graph) — the live in-site surfaces — optically centered. RIGHT zone .site-header__right = .site-header__right-nav (<nav aria-label="Resources">) holding rightLinks (Docs, GitHub, both target="_blank" + the external-↗ affordance) and the landing cta, then the ThemeToggle, then the mobile hamburger. The links remain ONE source set (centerLinks + rightLinks), grouped not duplicated; the mobile panel renders the same two lists in reading order.
  2. Larger wordmark. Height 1.5rem → 2rem for a more confident brand presence, balanced against the sm-sized nav.
  3. Accessible mobile disclosure (≤ --ds-bp-sm / 640px). Replace flex-wrap with a real <button class="site-menu-toggle"> carrying aria-label (Open/Close menu), aria-expanded, aria-controls="site-menu-panel", aria-haspopup, opening a raised .site-menu-panel (<nav aria-label="Menu">) that holds every link. Svelte 5 runes (menuOpen = $state(false) + tick()): focus moves to the first focusable item on open and returns to the button on close; Escape closes; outside-pointerdown closes (without stealing focus back); link activation closes. The panel is hidden/display:none above the breakpoint so it never traps focus on desktop.
  4. Wider chrome span. Header max-width --ds-container-lg (1024px) → --ds-container-xl (1200px), so the chrome spans wider than the centered body column and the brand / right cluster sit nearer the viewport edges.
  5. Stacking-context fix. .site-header gets z-index: var(--ds-z-sticky) so it forms its own stacking context above page content — fixing a real see-through-panel bug (see Rationale).

Every value is an existing --ds-* token (spacing, color, radius, motion, shadow, z-index, weight, text size) — no theme hex/px bypass (Establish the design system and the UX-engineering function / Recalibrate the Dossier brand identity — demote color, promote type + restraint + craft — then build the showcase landing). It remains ONE shared atomic component: all three consumers compose it unchanged; the .js-gated, reduced-motion-suppressed entrance choreography was re-authored per zone (the three zones are siblings now, not one nav) with explicit --stagger indices so the left→right wave still reads brand → Board → Graph → Docs → GitHub → cta → theme.

Rationale

  • Placement encodes information hierarchy. Board/Graph are the live in-site surfaces, so they hold the center of attention; Docs + GitHub leave the context (new tab, external-↗), so they retire to the right cluster beside the theme control. The IA now matches what each link does — directly the user's placement ask.
  • 1fr auto 1fr centers against the header, not the leftover space. Equal rails balance, so the center track is in the header's true middle at every width — the failure mode of option 2 (float-next-to-brand drift) is avoided by construction.
  • The z-index fix resolves a REAL bug, not a hypothetical. During live QA on the landing page the open mobile menu panel rendered see-through: the hero <h1> carries a transform for its entrance animation, which forms its OWN stacking context, and with the header at z-index: auto the absolutely-positioned panel (opaque white at its own --ds-z-dropdown) was not in a context that out-ranked that transformed hero, so the H1 painted through it. This is exactly the "especially on the landing page" symptom — the landing is the only surface with a transformed hero directly under the header. Lifting the whole header to the nav-bar --ds-z-sticky tier makes it form a stacking context that clears all page content, and the panel's own --ds-z-dropdown resolves inside it. Verified via elementFromPoint over the panel (returns the panel's own link after the fix, the hero's text before).
  • Accessibility is a disclosure, not a layout trick. A labeled <button> with full aria + focus management + Escape + outside-click + visible focus ring is the correct, keyboard- and screen-reader-navigable pattern; flex-wrap was neither announced nor operable as a menu.
  • One atomic component, not a fork. Per-surface needs (CTA, badge, wordmark) are already props; restructuring the one component lands the IA on landing + board + graph simultaneously and keeps the single-source chrome true by construction — and only the internal markup moved, so DEC-0045's single-mount/no-replay-on-nav guarantee is untouched (the header still lives in the (app) group layout).

Consequences

  • The header now reads as three legible zones on every chrome-light surface at once; the live surfaces are visually privileged over the leave-the-context links.
  • Mobile gains a real accessible menu (the disclosure), and the previously see-through landing menu panel is opaque and occluding in both themes.
  • .site-header now forms a stacking context (z-index: --ds-z-sticky). Any future surface that wants content to paint above the header must opt into a higher tier deliberately — a minor, expected constraint of a sticky-bar architecture.
  • The entrance choreography is now authored per-zone with explicit --stagger indices; adding/reordering a nav item means updating the corresponding --stagger (a small maintenance surface, documented inline).
  • The hamburger glyph's bar geometry is hard-coded (height: 2px, the ±6px offsets) rather than tokenized — flagged as out-of-scope follow-up, filed as Abstract the hard-coded hamburger-bar / icon-stroke geometry into a --ds-* token family (border-width / icon-stroke).

Reversibility

Two-way door: the change is the internal markup + scoped styles of one component. Reverting to the two-part bar is a local edit; nothing else depends on the three-zone structure, and no data/route/contract changed. The durable commitments are the principle — the header's IA encodes information hierarchy (live surfaces centered, leave-the-context links + controls clustered right), the menu is a real accessible disclosure, and the bar owns its own stacking context — not these exact pixel values, which are tunable tokens.

Review

confidence: verified — built and measured. pnpm check (svelte-kit sync + svelte-check) is 0 errors / 0 warnings across 467 files after the change; pnpm build compiles + bundles and prerenders all three surfaces with the new markup (the build's final EPERM: symlink step is the pre-existing, environment-specific win32 Vercel-adapter packaging failure — NOT from this change). Live Playwright QA at desktop (1280), wide desktop (1440, full 1200px header span), and mobile (390) in both light and dark themes confirmed: the three-zone layout + optical centering; the mobile disclosure open/close + Escape + focus-restore + aria assertions; and the see-through-panel bug fixed (panel opaque + occluding in both themes, via elementFromPoint). The verification gate is therefore met; no open promotion gate. See Persistent read-only chrome via an (app) route-group layout — stop the header re-animating on intra-app nav, Site chrome unified into shared SiteHeader + SiteFooter + ThemeToggle components, and the docs/KB surface made fully open — DEC-0022's DOCS_ENABLED landing-only gate is removed so /knowledge ships publicly alongside the landing/board/graph, Migrate chrome-light app surfaces to SvelteKit; docs stay on Astro/Starlight (two apps, one origin), Establish the design system and the UX-engineering function, and Abstract the hard-coded hamburger-bar / icon-stroke geometry into a --ds-* token family (border-width / icon-stroke).