feat(header): replace white header with brand-navy header per design spec #163
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The global sticky header currently uses
bg-surface(white in light mode, dark-gray in dark mode), which is not aligned with the brand identity. The design spec defines a brand-navy header with a purple accent strip.Spec:
docs/specs/header-nav-redesign-spec.html(open in browser)Problems with current implementation
bg-surface(#ffffff light / #1a1a1a dark) instead of brand-navy#012851brand-purple(#B4B9FF) top strip (total header height should be 68px = 4px strip + 64px bar)bg-nav-active(rgba(180,185,255,0.15)) on white = ~1.08:1 contrast, fails WCAG#1a1a1ainstead of staying brand-navy (which is a brand constant, not a semantic surface)Required changes
frontend/src/routes/+layout.sveltebg-surface→bg-brand-navyon the<header><div class="h-1 bg-brand-purple"></div>accent strip above the nav barborder-b border-line-2(not needed on dark bg)frontend/src/routes/AppNav.sveltetext-ink→text-whitemd:breakpoint — show it at all sizes)text-ink-2→text-white/55, hover:text-white/85bg-nav-active text-inkpill →text-white+border-b-2 border-brand-mintunderlinetext-ink-2→text-white/70frontend/src/routes/UserMenu.sveltebg-primary— but in the navy contextprimary= navy, so change to explicitbg-brand-mint text-brand-navyfor contrasttext-ink-3→text-white/60frontend/src/routes/+layout.svelte(LanguageSwitcher wrapper)border-r border-lineseparator (not visible on dark bg)frontend/src/routes/login/+page.svelte(if applicable)frontend/src/routes/layout.css--c-nav-activevariable is no longer needed for desktop (underline replaces pill) — can be kept for mobile drawer active stateAcceptance criteria
#012851in both light and dark mode#B4B9FFaccent strip visible at top of headerrgba(255,255,255,0.55), hoverrgba(255,255,255,0.85)#012851(does not switch to#1a1a1a)👨💻 Felix Brandt — Senior Fullstack Developer
Questions & Observations
TDD challenge: This is a purely visual/CSS change — there's no meaningful unit test to write first. The real test layer here is Playwright visual regression +
axe-playwright. Before starting, I'd want to align with Sara on what Playwright snapshots we're adding at which breakpoints.Dead CSS variable: The issue says
--c-nav-active"can be kept for mobile drawer active state." That's a conditional keep, not a clear decision. If we keep it, it needs a comment explaining why. If it's only used in one place, the variable adds indirection without benefit — I'd prefer to inline the value in the mobile drawer directly and delete the variable.Component size check: The issue touches
+layout.svelte,AppNav.svelte,UserMenu.svelte, andlogin/+page.svelte. Before writing any code, I'd check whether these components are still within the 40-line template / 60-line total thresholds after the changes. The login page header addition especially might push it over — if so, extract aBrandHeader.svelteorAuthHeader.sveltecomponent.Dark mode override strategy:
bg-brand-navyin Tailwind CSS 4 — does the dark mode configuration have adark:variant that could override this? The issue says the header must stay navy in dark mode, which means we probably need to explicitly suppress anydark:bg-*rules on the header element. Worth checking whether the currentbg-surfaceuses a CSS custom property that dark mode remaps, or adark:Tailwind variant.Opacity-based text colors:
text-white/55andtext-white/85are Tailwind opacity modifiers — they producergba()values, not solid colors. The computed contrast depends on what's rendered underneath. Since the header background is always#012851, this should be fine, but I'd want a comment or test confirming the contrast passes rather than leaving it as an implicit assumption.Suggestions
AppNav.svelteandUserMenu.sveltecurrent line counts before writing code — split first if needed.BrandHeader.svelteor similar) rather than inline markup inlogin/+page.svelte.--c-nav-activeentirely and inline the mobile drawer active color directly — avoid one-off CSS variables.🏗️ Markus Keller — Application Architect
Questions & Observations
Brand constant vs. semantic token: The issue correctly identifies that brand-navy is a brand constant, not a semantic surface color. This is the right distinction. However, I'd like to understand how
bg-brand-navyis defined inlayout.css— is it a CSS custom property (--color-brand-navy: #012851) referenced via a Tailwind utility, or a hardcoded hex in a@layer utilitiesblock? The former survives theme changes correctly; the latter creates invisible coupling if the brand color ever needs to update.Dark mode suppression: If the current dark mode implementation in
layout.cssremaps--c-surfaceto#1a1a1avia a[data-theme="dark"]selector or@media (prefers-color-scheme: dark), then replacingbg-surfacewithbg-brand-navyon the<header>element should be sufficient — the dark mode rule won't target an explicit brand class. But if there's adark:bg-surfaceTailwind variant involved, explicitdark:bg-brand-navyis needed. The spec should clarify which pattern is in use so implementers don't have to guess.CSS token hygiene:
--c-nav-activeis being retired for desktop but may survive for mobile. A half-retired token is worse than a fully retired one — it lives in the codebase as an unexplained artifact. Decision needed: either clean it up entirely (inline the mobile value) or rename it to something that makes its sole remaining use obvious (--c-nav-drawer-active).Login page header scope: The issue lists adding a slim brand header to
login/+page.svelteas "if applicable." That conditional scope creates ambiguity. If this is in-scope, it should be in the acceptance criteria. If it's out-of-scope, it should be a separate issue. Half-done states in a PR are harder to review and test.No backend or API surface changes — this is a pure frontend CSS/layout change. No layer boundary concerns. No cross-domain coupling risks.
Suggestions
--c-nav-drawer-activeif it survives, so its remaining purpose is self-documenting.🧪 Sara Holt — QA Engineer
Questions & Observations
No unit tests applicable here — CSS/styling changes don't produce meaningful Vitest unit test coverage. The test burden for this issue sits entirely at the E2E layer (Playwright) and visual regression layer. That's fine, but it should be explicit: this PR will not increase branch coverage metrics, and no one should flag that as a quality concern.
WCAG contrast acceptance criterion is under-specified: The issue says "WCAG AA contrast met on all nav text (≥ 4.5:1)" but doesn't name which specific color combinations need to be verified. I'd want a checklist: inactive nav on navy, hover nav on navy, active nav on navy, hamburger icon on navy, language switcher text on navy, avatar initials on mint. Each one needs a confirmed ratio before the PR is merged, not just a visual eyeball.
Visual regression baseline: If we don't have existing Playwright snapshots for the header at 320px, 768px, and 1440px in both light and dark mode, this PR should establish them — so future changes are caught automatically. Do we have baseline snapshots today, or will this be a net-new addition?
Dark mode test: The acceptance criterion says "Dark mode: header stays
#012851." This needs a Playwright test: set dark mode preference viapage.emulateMedia({ colorScheme: 'dark' }), navigate to any authenticated page, assert the computed background color of the<header>element equalsrgb(1, 40, 81).Mobile smoke test: Viewport 375px, verify logo text is visible, hamburger icon is visible, no overflow.
Login page: If the language switcher is moved into a header element, there should be an E2E test that confirms it's reachable and functional on the login page (not just present).
Suggested Test Cases
Suggestions
🔐 Nora "NullX" Steiner — Security Engineer
Questions & Observations
Low security surface — this is a CSS/layout change with no new data flows, authentication changes, or backend involvement. No injection vectors, no auth boundary changes, no API surface changes. I'll note the few things worth checking:
Login page header — no auth data leakage: If a new header component is added to the login page (
+page.svelte), make sure it receives no user-specific props (session data, username, roles). The login page is pre-auth; any server load function for it must not expose authenticated state into the header template. Low probability risk, but worth a quick check during review.Inline styles: The accent strip is specified as
<div class="h-1 bg-brand-purple"></div>— using a Tailwind class rather thanstyle="background: #B4B9FF". Good. Inline style attributes with hardcoded values can conflict with strictstyle-srcCSP policies. Stick to Tailwind classes throughout this implementation; avoid anystyle=attribute additions.No
@CrossOrigin, no new endpoints, no Spring Security config changes — nothing here touches the security perimeter.Avatar change (
bg-brand-mint text-brand-navy): The avatar renders user initials. No change to how initials are derived or escaped — this remains a display-only change. No XSS concern introduced.Summary
No security concerns blocking this issue. One advisory: if the login page gets a header, double-check the server load function for
login/+page.server.tsduring PR review to confirm no authenticated data leaks into the pre-auth layout.🎨 Leonie Voss — UI/UX Designer
This issue is directly in my domain — the spec is detailed and well-considered. A few things I want to ensure we get right:
Questions & Observations
text-white/55contrast — verify the blended value, not just the spec claim:rgba(255,255,255,0.55)on#012851produces an effective computed color of approximatelyrgb(141, 158, 176)— contrast ratio against#012851is roughly 5.4:1, which passes WCAG AA. However, Tailwind's opacity modifier blends at render time in the browser. If any browser or OS accessibility setting increases the background luminance (e.g. Windows High Contrast mode, forced colors), this blending assumption breaks. Recommendation: verify the actual computed contrast in Chrome DevTools' accessibility panel on the final implementation, not just by calculation.Focus rings on dark background: The spec covers color, active states, and hover states — but I don't see a mention of focus ring styling on nav links when the header is dark. The default browser focus ring (blue outline) will be visible on dark navy but may clash. Define the focus ring explicitly:
focus-visible:outline-2 focus-visible:outline-brand-mintor similar. This is a WCAG 2.2 criterion 2.4.11 (Focus Appearance) concern.border-b-2 border-brand-mintactive state — I like this. Cleaner than the pill, and mint-on-navy has excellent contrast (light teal on dark navy). Just confirm the border appears correctly on the active link's bottom edge and not on the container — active state borders placed on a flex child can sometimes sit at the wrong position depending onalign-itemson the parent nav. Worth a quick visual check at 1440px.Mobile logo visibility: The spec says "show logo at all sizes." Currently hidden at
md:breakpoint. Ensure the logo text color change (text-ink→text-white) applies at all breakpoints, not just desktop — don't accidentally keep the old class on the mobile breakpoint if the logo is conditionally rendered.4px accent strip height: At 320px viewport, a 4px strip is fine — purely decorative. Confirmed not interactive, so touch target minimum does not apply.
Login page language switcher: Moving from floating top-right to a proper header slot is the right UX call. The floating position was visually disconnected and inaccessible on small viewports. The slim header (logo + lang switcher only) is the correct pattern for auth pages.
Avatar:
bg-brand-mint text-brand-navy:#A6DAD8background with#002850text — contrast is approximately 8.5:1, well above AAA. Correct choice. Confirm the avatar button also gets a visible focus ring:focus-visible:ring-2 focus-visible:ring-whitewould work on the dark header.Acceptance Criteria Gap
The current acceptance criteria don't include:
Suggestions
brand-purple(#B4B9FF) is already defined as a Tailwind CSS utility inlayout.css, or needs to be added — the accent strip depends on it.⚙️ Tobias Wendt — DevOps & Platform Engineer
Questions & Observations
No infrastructure changes — this is a frontend-only CSS/Tailwind change. No Docker Compose changes, no environment variables, no backend config involved. Clean from an ops perspective.
A few CI and build points worth checking:
brand-purpleutility availability: The accent strip usesbg-brand-purple. If this Tailwind utility doesn't exist yet inlayout.css, the Tailwind JIT compiler will silently skip it and the strip won't render. Thenpm run buildstep will succeed without error — there's no compile-time check for missing custom utilities. Whoever implements this should verify the class exists inlayout.cssbefore committing, or the CInpm run checkwon't catch it.Playwright CI configuration for dark mode: If the existing Playwright setup doesn't run tests with
colorScheme: 'dark', this PR is a good trigger to add aprojectsentry inplaywright.config.tsfor dark mode. Running both in CI costs ~zero extra setup but catches regressions like "header flips to #1a1a1a in dark mode again."Visual regression artifacts: If this PR introduces Playwright snapshot baselines, make sure the
.playwright/snapshots/directory (or wherever snapshots land) is committed to the repo — or alternatively stored as CI artifacts with a defined retention window. Uncommitted baselines will cause every CI run to regenerate them and never catch actual regressions.Build size: Tailwind 4 JIT purges unused classes. Adding
bg-brand-navy,bg-brand-purple,text-white/55,text-white/85,border-brand-mintwill add a few bytes to the CSS bundle. Not worth measuring unless the bundle is already close to a size budget — which it isn't for a family archive app.No new secrets, no env vars, no Docker changes needed. Deployment is unaffected.
Suggestions
projectsblock for'chromium-dark'(withcolorScheme: 'dark') if not present — this PR is the right time since dark mode behavior is an explicit acceptance criterion.bg-brand-purpleresolves to#B4B9FFby checkinglayout.css— don't trust that it exists.🎨 Leonie Voss — UI/UX Designer — Discussion Summary
Worked through five open items from my earlier review. All resolved.
1. Header background & accent strip tokens
Use
bg-primaryfor the header andbg-accentfor the 4px strip. Both follow the theme and stay on-brand in light and dark mode. The spec's references tobg-brand-navyandbg-brand-purpleshould be updated accordingly. The acceptance criterion "stays#012851in both modes" should read "stays brand-appropriate in both modes."2. Focus rings
Use
focus:outline-none focus-visible:ring-2 focus-visible:ring-accenton all interactive header elements (nav links, hamburger, avatar button, language switcher). Consistent with the existing pattern on the bell icon.3. Login page header
In scope for this PR. A slim header (logo + language switcher) replaces the floating language switcher on auth pages.
4. Mobile logo visibility
No specific breakpoint required — implementer tests at 320px and 375px and uses the smallest breakpoint where the full wordmark fits without overflow. No clipping, no truncation.
5. Active state border alignment
Verify during implementation that
border-b-2 border-accenton active nav links sits correctly at the bottom of the header bar. If the flex container usesitems-center, the active link may needself-stretch flex items-endor the nav container may needitems-stretch.Overall the spec is solid. The main thing to carry into implementation: use the semantic
primary/accenttokens throughout rather than hard-pinned brand hex values, and test the logo at 320px before settling on a breakpoint.✅ Implementation complete — branch
feat/issue-163-brand-navy-headerWhat was implemented
All 11 planned tasks done. 9 commits:
fd2a7a8refactor(layout): remove--c-nav-activeCSS token3dfaf69feat(LanguageSwitcher): addinvertedprop for dark-header contextadc1f34feat(layout): applybg-brand-navyheader with 4pxbg-accentstrip14b1cc7feat(AppNav): brand-navy header styles — logo, nav links, hamburger, mobile drawere65ddc6feat(UserMenu): brand-mint avatar, white guest icon, focus rings55e681cfeat(AuthHeader): new slimbg-brand-navyheader component for auth pages550a970feat(login): replace floating lang switcher withAuthHeader36bf591feat(forgot-password): addAuthHeaderfor consistent auth page brandingc905f13test(header): Playwright tests for brand-navy headerAcceptance criteria status
#012851in both light and dark mode (bg-brand-navy— static token, never remapped)bg-accentstrip visible at top of headerhidden md:flex)border-b-2 border-accentbottom underline (no pill)text-white/55, hovertext-white/85bg-brand-mint text-brand-navy(mint circle, navy initials — 8.5:1 contrast)text-white/70invertedprop on desktop; unchanged in mobile drawer#012851(static token, immune to theme remapping)AuthHeader, not floating top-rightfocus-visible:ring-2 focus-visible:ring-accenton all interactive header elementsDesign decisions
bg-brand-navy(static palette token) rather thanbg-primary(which flips to mint in dark mode), per the discussion.--c-nav-activeentirely; mobile drawer active state usesbg-accent-bginstead.AuthHeaderis also applied toforgot-password/+page.sveltefor consistent auth page branding.AuthHeaderreplaces the floating switcher only.