Files
school_compare/nextjs-app/components/Navigation.module.css
T
Tudor b11ee3c8de
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m6s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 12s
PR Checks / Build Frontend (no push) (pull_request) Successful in 49s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m7s
feat(brand): use the supplied logo artwork instead of a reconstruction
The mark shipped so far was my SVG approximation, and it was wrong: the real
mark is a teardrop pin with a white window and a path flowing out of its base,
carrying three leaves — not a circle with a separate tail.

Both colourways are extracted from the supplied sheet, which has a genuinely
transparent background, so these are the artwork rather than a trace:

  public/brand/mark.png          teal pin, white window and path, green leaves
  public/brand/mark-on-dark.png  white pin with the counter knocked through

Two colourways are needed, not one. Rendered against every real ground, the
teal pin holds up on Warm White, white cards, Sand and both dark-theme grounds
— but it vanishes on the teal footer band, where only the white window
survives. The header therefore serves the teal artwork and swaps to the
on-dark artwork for dark-theme viewers through a <picture> source, needing no
JavaScript; the footer forces on-dark, because its band is teal in both
themes.

Everything downstream now derives from those two files: the favicon
(app/icon.png, replacing icon.svg), the touch icon (app/apple-icon.png,
replacing the generated apple-icon.tsx), the three PWA rasters, and the share
card, which reads public/brand/mark.png off disk so it can never drift from
the header. The logo sizing CSS keyed off a square box, which would have
squashed a 153:189 artwork — height now drives and width follows.

The wordmark stays live text in Manrope. The sheet's wordmark is a raster with
visible edge fringing, and the written style guide specifies Manrope; live
text also stays selectable, scales cleanly and recolours with the theme.

KNOWN LIMITATION: the largest instance on the sheet is 153×189. That is ample
for the header at 38px, the favicon and the share card, but short of the 512px
PWA icon, which is upscaled and slightly soft. A vector would fix it and is a
single swap — every consumer goes through components/Logo.tsx or public/brand.

Verified: tsc clean, 159/159 tests, build green, /icon.png and /apple-icon.png
emit as static routes, and the header, footer, share card and all five icons
were rendered and inspected in both themes.
2026-08-08 22:22:58 +01:00

289 lines
6.6 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Header and mobile tab bar, on the schoolcompare brand.
*
* The header is a white card band closed by a single hairline rule — no
* shadow. The guideline's chrome is quiet; the colour on this surface is the
* wordmark and the one active nav item, and nothing else.
*/
.header {
position: sticky;
top: 0;
z-index: 1000;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
}
.container {
max-width: 1400px;
margin: 0 auto;
/* Add the safe-area inset to horizontal padding so the header content
clears the notch in landscape on iPhones. */
padding-inline: max(1.5rem, env(safe-area-inset-left)) max(1.5rem, env(safe-area-inset-right));
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
/* 64px desktop / 56px mobile is load-bearing: the school detail page's
sticky section nav docks against it with a hardcoded `top` in
SchoolDetailShell.module.css and schoolSections.module.css, and the
scroll-spy observers use it as a rootMargin. Change it in all four
places or not at all. */
height: 64px;
}
.logo {
display: flex;
align-items: center;
gap: 0.625rem;
/* Padded hit area so the logo link is ≥44×44 on touch */
margin: -0.375rem -0.5rem;
padding: 0.375rem 0.5rem;
text-decoration: none;
color: var(--text-primary);
transition: color var(--transition);
-webkit-tap-highlight-color: transparent;
}
.logo:hover {
color: var(--brand-stronger);
}
/*
* The mark colours itself — pin in var(--brand), leaf knocked out in
* var(--bg-card), which is this header's own ground. It takes no colour from
* currentColor, so there is deliberately no `color` here.
*/
.logoIcon {
display: inline-flex;
flex: 0 0 auto;
/* The artwork is taller than it is wide (153:189), so height drives the box
and width follows. Constraining both would squash the pin. */
height: 38px;
}
.logoIcon img {
display: block;
height: 100%;
width: auto;
}
/*
* "schoolcompare", one word, lower case. Manrope 700 pulled tight — the
* negative tracking is what makes the two halves read as a single word rather
* than as two.
*/
.logoText {
font-family: var(--font-display);
font-size: var(--step-2);
font-weight: 700;
line-height: 1;
letter-spacing: -0.035em;
}
.logoTextAccent {
color: var(--brand);
}
.logo:hover .logoTextAccent {
color: var(--brand-stronger);
}
.nav {
display: flex;
align-items: center;
gap: 0.125rem;
}
.navLink {
position: relative;
display: flex;
align-items: center;
gap: 0.4375rem;
padding: 0.5rem 0.875rem;
font-family: var(--font-ui);
font-size: var(--step--1);
font-weight: 500;
color: var(--text-secondary);
text-decoration: none;
border-radius: var(--radius-md);
transition: color var(--transition), background-color var(--transition);
}
.navLink:hover {
color: var(--text-primary);
background: var(--bg-secondary);
}
/* The current page is marked twice — brand hue and a heavier weight — so the
state never rests on colour alone. */
.navLink.active {
color: var(--brand);
background: var(--brand-bg);
font-weight: 600;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.125rem;
height: 1.125rem;
padding: 0 0.375rem;
font-family: var(--font-ui);
font-size: var(--step--2);
font-weight: 700;
line-height: 1;
color: var(--brand-on);
background: var(--brand);
border-radius: 9999px;
animation: badgePop 0.3s ease-out;
}
@keyframes badgePop {
0% {
transform: scale(0.6);
opacity: 0;
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* ─── Bottom tab bar (mobile only) ──────────────────────────────── */
.bottomBar {
display: none;
}
.tab {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
flex: 1;
min-height: 56px;
padding: 0.375rem 0.25rem;
font-family: var(--font-ui);
color: var(--text-secondary);
text-decoration: none;
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.01em;
transition: color var(--transition);
-webkit-tap-highlight-color: transparent;
}
.tabActive {
color: var(--brand);
font-weight: 600;
}
.tabIconWrap {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
/* The active tab's icon sits in a brand-tinted stadium — the same rounded
geometry as the rest of the system, and a second cue beyond hue. Drawn as a
pseudo-element so it can bleed wider than the 24px icon box without moving
the badge's anchor. */
.tabIconWrap::before {
content: '';
position: absolute;
inset: -2px -9px;
border-radius: var(--radius-xl);
background: transparent;
transition: background-color var(--transition);
}
.tabActive .tabIconWrap::before {
background: var(--brand-bg);
}
.tab:active .tabIconWrap::before {
background: var(--brand-bg);
}
.tabIcon {
position: relative;
width: 22px;
height: 22px;
}
.tabLabel {
line-height: 1;
}
.tabBadge {
position: absolute;
top: -7px;
right: -7px;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
font-family: var(--font-ui);
font-size: 0.6875rem;
font-weight: 700;
line-height: 1;
color: var(--brand-on);
background: var(--brand);
border: 2px solid var(--bg-card);
border-radius: 9999px;
animation: badgePop 0.3s ease-out;
}
@media (max-width: 640px) {
.container {
padding: 0 1rem;
height: 56px;
}
/* Hide the top text nav; the bottom bar takes over */
.nav {
display: none;
}
.logoIcon {
width: 34px;
height: 34px;
}
.logoText {
font-size: var(--step-1);
}
.bottomBar {
display: flex;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
background: var(--bg-card);
border-top: 1px solid var(--border);
box-shadow: 0 -2px 12px rgba(var(--shadow-rgb), 0.06);
/* Respect iPhone home-indicator (bottom) and notch (left/right in
landscape) insets so the tab content never sits under system UI. */
padding-bottom: env(safe-area-inset-bottom, 0);
padding-inline: env(safe-area-inset-left, 0) env(safe-area-inset-right, 0);
/* Compensate for iOS Chrome's auto-hiding URL bar — Navigation.tsx
writes the offset based on the Visual Viewport API. translate3d
(instead of translateY) forces hardware compositing so the bar
doesn't lag/flicker during the toolbar animation. */
transform: translate3d(0, var(--mobile-bar-offset, 0px), 0);
}
}