Files
school_compare/nextjs-app/components/school/SchoolDetailShell.module.css
TudorandClaude Opus 5 0571d1c0ff
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m17s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m18s
fix(web): stop the sheet-open rule stealing .sectionNav's layout
Review catch, and a bad one: the previous commit anchored its insertion on
`padding: 0.5rem 0.75rem;` and closed .sectionNav there. Everything that
followed in the rule — margin-bottom, box-shadow, display: flex, align-items,
gap — was orphaned into .sectionNavSheetOpen, which is only applied while the
mobile jump sheet is open.

So the sticky nav lost its flex layout, spacing and shadow in the closed
state, which is virtually every page view on every school detail page. A
site-wide regression introduced by a fix for one mobile menu.

Redone by anchoring on the complete rule, closing brace included, so nothing
can be orphaned. .sectionNav is now byte-identical to main and the diff is
purely additive; .sectionNavSheetOpen carries the z-index and nothing else.

The staging experiment that validated this fix set nav.style.zIndex = '1100'
with every other declaration intact, so it was always testing this version
rather than the broken one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:43:58 +01:00

783 lines
16 KiB
CSS

/* Styles for SchoolDetailShell — the interactive chrome of a detail page.
Derived from the classes the shell's JSX references; the section styles
live in components/school/schoolSections.module.css. Classes used by both
appear in both files, which is correct: CSS Modules hash them per-file. */
.container {
width: 100%;
min-width: 0;
max-width: 100%;
}
/* Standalone back link, sits above the header card on the page background. */
.topBack {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin: 0 0 0.75rem;
padding: 0.25rem 0;
font-size: 1.0625rem;
font-weight: 600;
color: var(--brand-strong);
background: none;
border: none;
cursor: pointer;
line-height: 1.2;
transition: color 0.15s ease;
}
.topBack:hover {
color: var(--brand-strong);
text-decoration: underline;
text-underline-offset: 2px;
}
/* Header Section */
.header {
position: relative;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
/* Padding lives on .headerContent so the map band can bleed to the edges. */
padding: 0;
margin-bottom: 0;
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.headerContent {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1.5rem;
padding: 1.25rem 1.5rem;
}
/* With a map band above, slide the title up under the fade so map and title
read as one object; the Compare button floats glassy over the map. */
.headerHasMap .headerContent {
padding-top: 0;
margin-top: -0.5rem;
}
/* The title (not the whole content row) rises above the map fade. Keeping
.headerContent unpositioned matters: .actions must anchor to .header so it
floats over the map band, not over the title. */
.headerHasMap .titleSection {
position: relative;
z-index: 3;
}
.headerHasMap .actions {
position: absolute;
top: 14px;
right: 14px;
z-index: 6;
margin: 0;
/* Beat the mobile `.actions { width: 100% }` rule — a floating button
must never stretch across the title. */
width: auto;
}
.headerHasMap .actions .btnAdd {
background: var(--bg-card);
color: var(--brand-strong);
border-color: transparent;
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
box-shadow: 0 2px 10px rgba(var(--shadow-rgb), 0.16);
}
.headerHasMap .actions .btnAdd:hover {
background: #fff;
}
/* Full label by default; phones over the map get an icon-only button
(same compact treatment as the section-nav compare icon). */
.btnCompareGlyph {
display: none;
}
/* Inline "View on map ↗" trigger next to the address. */
.mapLink {
border: none;
background: none;
padding: 0;
font: inherit;
font-weight: 600;
color: var(--brand-strong);
cursor: pointer;
white-space: nowrap;
}
.mapLink:hover {
color: var(--brand-strong);
text-decoration: underline;
text-underline-offset: 2px;
}
.titleSection {
flex: 1;
}
.schoolName {
font-size: clamp(2rem, 5vw, 3.25rem);
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
line-height: 1.1;
letter-spacing: -0.01em;
font-family: var(--font-display);
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.metaItem {
font-size: 0.8125rem;
color: var(--text-secondary);
padding: 0.125rem 0.5rem;
background: var(--bg-secondary);
border-radius: 3px;
}
.address {
font-size: 0.875rem;
color: var(--text-muted);
margin: 0 0 0.75rem;
}
/* Expanded header details (headteacher, website, trust, pupils) */
.headerDetails {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.25rem;
margin-top: 0.5rem;
}
.headerDetail {
font-size: 0.8125rem;
color: var(--text-secondary);
}
.headerDetail strong {
color: var(--text-primary);
font-weight: 600;
}
.headerDetail a {
color: var(--brand);
text-decoration: none;
}
.headerDetail a:hover {
text-decoration: underline;
}
/* "Show all details" reveal — only rendered on mobile/tablet, where the
header details block is collapsed below the fold. Hidden on desktop. */
.detailsToggle {
display: none;
align-items: center;
gap: 0.25rem;
margin-top: 0.5rem;
padding: 0;
background: none;
border: none;
font-size: 0.8125rem;
font-weight: 600;
color: var(--brand);
cursor: pointer;
}
.actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
align-self: center;
}
.btnAdd,
.btnRemove {
padding: 0.75rem 1.25rem;
font-size: 0.9375rem;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.08));
}
.btnAdd {
background: var(--brand-strong);
color: var(--brand-on);
}
.btnAdd:hover {
background: var(--brand-stronger);
transform: translateY(-1px);
}
.btnRemove {
background: var(--brand);
color: var(--brand-on);
}
.btnRemove:hover {
opacity: 0.9;
}
/* ── Sticky Section Navigation ──────────────────────── */
/* Docks directly under the global header; Back and "All" stay pinned while
only the section links scroll. */
.sectionNav {
position: sticky;
top: 64px; /* global header height on desktop */
z-index: 10;
background: var(--bg-card);
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 10px 10px;
padding: 0.5rem 0.75rem;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(var(--shadow-rgb), 0.04);
display: flex;
align-items: center;
gap: 0.5rem;
}
/* `position: sticky` with a z-index makes .sectionNav a stacking context, so
the jump sheet's own z-index only orders it INSIDE that context. Against the
fixed bottom tab bar (Navigation.module.css, z-index 1000) what counts is
.sectionNav's 10 — which is why the sheet's last item was painted over, and
untappable, once the list grew long enough to reach the bar.
Lifted only while the sheet is open, and only to 1100: above the bar, below
the comparison toast (2000), the fullscreen map (5000) and the info popover
(9999). This rule carries the z-index and nothing else; every other
declaration belongs to .sectionNav in both states. */
.sectionNavSheetOpen {
z-index: 1100;
}
.sectionNavBack {
flex: none;
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0.625rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--brand-strong);
background: none;
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
}
.sectionNavBack:hover {
background: var(--bg-secondary);
border-color: var(--brand);
}
/* The scrolling middle: section links only. */
.sectionNavLinks {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 0.375rem;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
scroll-snap-type: x proximity;
scroll-padding-inline: 0.5rem;
}
.sectionNavLinks::-webkit-scrollbar {
display: none;
}
.sectionNavLink {
display: inline-flex;
align-items: center;
padding: 0.3rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
color: var(--text-secondary);
text-decoration: none;
border-radius: 4px;
transition: all 0.15s ease;
white-space: nowrap;
scroll-snap-align: start;
}
.sectionNavLink:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.sectionNavLinkActive {
background: var(--brand-strong);
color: var(--brand-on);
font-weight: 600;
}
.sectionNavLinkActive:hover {
background: var(--brand-strong);
color: var(--brand-on);
}
/* ── Mobile: the scrolling links collapse into one "section" menu button ──
(hidden on desktop, where the links fit). */
.sectionNavMenu {
display: none; /* shown only ≤640px */
flex: 1;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-height: 38px;
padding: 0.34rem 0.7rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
font-family: var(--font-ui);
color: var(--text-primary);
}
.sectionNavMenuCur {
display: flex;
align-items: center;
gap: 0.45rem;
min-width: 0;
}
.sectionNavMenuEyebrow {
flex: none;
font-size: 0.64rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-muted);
}
.sectionNavMenuNow {
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sectionNavMenuChev {
flex: none;
color: var(--text-muted);
font-size: 0.7rem;
}
/* Compact icon version of the Compare CTA, used on mobile. */
.sectionNavCompareIcon {
display: none; /* shown only ≤640px */
position: relative;
flex: none;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 9px;
border: 1px solid var(--brand-strong);
background: var(--brand-strong);
color: var(--brand-on);
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}
.sectionNavCompareIcon svg {
width: 19px;
height: 19px;
}
.sectionNavCompareBadge {
position: absolute;
top: -5px;
right: -5px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--bg-card);
color: var(--brand-strong);
border: 1.5px solid var(--brand);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 800;
line-height: 1;
}
.sectionNavCompareIconIn {
background: var(--bg-card);
border-color: var(--brand);
color: var(--brand);
}
/* Compare CTA carried into the bar once the hero's button scrolls away. */
.sectionNavCompare {
flex: none;
display: inline-flex;
align-items: center;
padding: 0.34rem 0.7rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--brand-on);
background: var(--brand-strong);
border: 1px solid var(--brand-strong);
border-radius: 999px;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
}
.sectionNavCompare:hover {
background: var(--brand-stronger);
border-color: var(--brand-stronger);
}
.sectionNavCompareIn {
background: var(--bg-card);
color: var(--brand);
border-color: var(--brand);
}
.sectionNavCompareIn:hover {
background: var(--bg-secondary);
border-color: var(--brand);
}
/* "All ▾" jump menu (desktop). */
.sectionNavAll {
flex: none;
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.34rem 0.65rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-primary);
background: var(--bg-secondary);
border: none;
border-radius: 999px;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s ease;
}
.sectionNavAll:hover {
background: var(--border);
}
.sectionsBackdrop {
position: fixed;
inset: 0;
z-index: 1500;
background: rgba(var(--shadow-rgb), 0.28);
}
.sectionsPanel {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 1600;
width: 230px;
max-height: min(70vh, 460px);
overflow-y: auto;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 18px 44px rgba(var(--shadow-rgb), 0.2);
padding: 0.35rem;
}
.sectionsPanelHead {
font-family: var(--font-display);
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
padding: 0.4rem 0.6rem 0.5rem;
}
.sectionsItem {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.55rem 0.6rem;
border-radius: 8px;
font-size: 0.85rem;
color: var(--text-secondary);
text-decoration: none;
transition: background 0.12s ease;
}
.sectionsItem:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.sectionsItemActive {
background: var(--brand-bg);
color: var(--brand-strong);
font-weight: 600;
}
.sectionsTick {
color: var(--brand-strong);
}
/* GIAS "Open, but proposed to close" notice strip */
.closingStrip {
background: var(--status-below-bg);
border-left: 4px solid var(--status-below-bg);
border-radius: 0 6px 6px 0;
padding: 0.55rem 0.9rem;
margin: 0.5rem 0;
font-size: 0.88rem;
color: var(--status-below);
max-width: 68ch;
}
.closingStrip strong {
color: var(--status-below);
}
@media (max-width: 640px) {
.headerHasMap .actions .btnCompareLabel {
display: none;
}
.headerHasMap .actions .btnCompareGlyph {
display: inline;
}
.headerHasMap .actions .btnAdd,
.headerHasMap .actions .btnRemove {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
width: 40px;
height: 40px;
padding: 0;
border-radius: 999px;
font-size: 1.375rem;
line-height: 1;
}
}
@media (max-width: 640px) {
.sectionNav {
top: 56px; /* global header is shorter on mobile */
padding: 0.4rem 0.6rem;
gap: 0.375rem;
}
}
@media (max-width: 640px) {
.sectionNavLink,
.sectionNavBack {
min-height: 36px;
padding: 0.5rem 0.75rem;
font-size: 0.8125rem;
}
}
@media (max-width: 640px) {
.sectionNavCompare {
min-height: 36px;
}
}
@media (max-width: 640px) {
.sectionNavAll {
min-height: 36px;
}
}
@media (max-width: 768px) {
.headerContent {
flex-direction: column;
gap: 1rem;
}
.actions {
width: 100%;
}
.btnAdd,
.btnRemove {
flex: 1;
}
.schoolName {
font-size: 1.25rem;
word-break: break-word;
}
/* Pills wrap horizontally instead of stacking — short tokens like
"Manchester" / "Voluntary aided" fit 2 per row instead of 3 full
rows of empty horizontal space. */
.meta {
flex-direction: row;
flex-wrap: wrap;
gap: 0.375rem;
}
/* Secondary header info (headteacher, website, pupil count, trust,
contact, area) isn't needed above the fold on phones/tablets, so it's
collapsed by default and revealed on demand via the "Show all details"
link — reclaiming the vertical space so the metrics surface sooner. */
.detailsToggle {
display: inline-flex;
}
.headerDetails {
display: none;
}
.headerDetailsOpen {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
}
@media (max-width: 640px) {
.sectionNavLinks {
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
}
/* When scrolled to the end, drop the fade so the last item isn't dimmed. */
.sectionNavLinks.atEnd {
-webkit-mask-image: none;
mask-image: none;
}
.sectionsPanel {
position: fixed;
top: auto;
left: 0;
right: 0;
bottom: 0;
width: auto;
max-height: 74vh;
border-radius: 16px 16px 0 0;
padding: 0.5rem 0.6rem calc(0.8rem + env(safe-area-inset-bottom, 0));
box-shadow: 0 -10px 40px rgba(var(--shadow-rgb), 0.25);
}
.sectionsItem {
padding: 0.7rem 0.6rem;
font-size: 0.9rem;
}
}
/* Swap which controls show at the mobile breakpoint. Declared last so these
display rules win over the base (equal-specificity) declarations above. */
@media (max-width: 640px) {
.sectionNavLinks,
.sectionNavCompare,
.sectionNavAll,
.sectionNavBackLabel {
display: none;
}
.sectionNavMenu {
display: flex;
}
.sectionNavCompareIcon {
display: inline-flex;
}
}