/* ==========================================================================
   Public site — design system and presentation

   Loaded ONLY by the public pages, after app.css. app.css supplies the base
   design tokens (:root), the brand mark and the shared button styles; this file
   owns the public presentation so the admin panel and the public site cannot
   leak styles into one another.

   Every selector is scoped to .public-page, which is on <body>. That does two
   things: it keeps this stylesheet from touching the admin UI, and it gives
   these rules the specificity they need to supersede the earlier public rules
   that still live in app.css.

   MOBILE FIRST
   ------------
   Everything here is written for a 320px screen first and widened from there,
   with breakpoints at 576 / 768 / 992 / 1200px. Layout is flex and grid with
   auto-fitting tracks, so most components reflow without a media query at all.
   Nothing here is a shrunk-down desktop layout.

   COLOUR
   ------
   The brand ramp (--sky-50 … --sky-900) and the semantic tokens (--surface,
   --surface-muted, --border, --text, --text-muted, --on-primary, --ring) come
   from app.css, and an administrator's colour choices are injected on
   .public-page by site-theme.php. THIS FILE MUST NOT HARD-CODE A BRAND COLOUR:
   anything brand-tinted is composed from those tokens with color-mix(), written
   after a plain fallback so a browser without color-mix() still paints. That is
   what lets a school re-theme the whole site without an edit here.

   Sky blue throughout. No purple, violet, indigo or magenta anywhere.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Design tokens
   -------------------------------------------------------------------------- */

.public-page {
    /* Rhythm and measure */
    --gutter: clamp(1.1rem, 4.2vw, 2.25rem);
    --container: 1180px;
    --measure: 68ch;
    --section-y: clamp(3rem, 6.5vw, 5.25rem);

    /* Spacing scale */
    --space-3xs: 0.25rem;
    --space-2xs: 0.5rem;
    --space-xs: 0.75rem;
    --space-sm: 1rem;
    --space-md: 1.4rem;
    --space-lg: 2rem;
    --space-xl: 2.75rem;
    --space-2xl: 3.5rem;

    /* Radii */
    --radius-xs: 0.375rem;
    --radius-sm: 0.625rem;
    --radius-md: 0.875rem;
    --radius-lg: 1.25rem;
    --radius-xl: 1.75rem;
    --radius-pill: 999px;

    /* The minimum comfortable touch target, used on every control a finger hits. */
    --tap: 2.75rem;

    /* Fluid type scale */
    --fs-eyebrow: 0.78rem;
    --fs-xs: 0.84rem;
    --fs-sm: 0.92rem;
    --fs-body: clamp(1rem, 0.97rem + 0.15vw, 1.06rem);
    --fs-lead: clamp(1.06rem, 1rem + 0.35vw, 1.22rem);
    --fs-h4: clamp(1.02rem, 0.98rem + 0.22vw, 1.12rem);
    --fs-h3: clamp(1.1rem, 1.02rem + 0.45vw, 1.32rem);
    --fs-h2: clamp(1.5rem, 1.15rem + 1.5vw, 2.15rem);
    --fs-h1: clamp(2rem, 1.1rem + 3.4vw, 3.4rem);

    /* Shadows. The plain rgba is the fallback; the color-mix() line after it
       re-tints the shadow to whatever brand colour the school chose, so a
       palette change moves the shadows with it. */
    --shadow-xs: 0 1px 2px rgba(12, 74, 110, 0.07);
    --shadow-sm: 0 2px 10px rgba(12, 74, 110, 0.08);
    --shadow-sm: 0 2px 10px color-mix(in srgb, var(--sky-900) 10%, transparent);
    --shadow-md: 0 12px 30px rgba(12, 74, 110, 0.13);
    --shadow-md: 0 12px 30px color-mix(in srgb, var(--sky-900) 15%, transparent);
    --shadow-lg: 0 26px 60px rgba(12, 74, 110, 0.18);
    --shadow-lg: 0 26px 60px color-mix(in srgb, var(--sky-900) 20%, transparent);

    /* Brand-tinted washes, used by the gallery mount and the card hairlines. */
    --tint-soft: rgba(14, 165, 233, 0.07);
    --tint-soft: color-mix(in srgb, var(--sky-500) 8%, transparent);
}

/* --------------------------------------------------------------------------
   2. Page shell and base typography
   -------------------------------------------------------------------------- */

.public-page {
    min-height: 100vh;
    margin: 0;
    background: var(--surface);
    color: var(--text);
    font-size: var(--fs-body);
    line-height: 1.7;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Long words typed by an administrator must never widen the page. */
    overflow-wrap: break-word;
}

/* Smooth in-page jumps, with room for the sticky header. Scoped with :has so
   the admin panel keeps the browser default; where :has is unsupported the
   browser simply jumps, which is a perfectly good fallback. */
html:has(body.public-page) {
    scroll-behavior: smooth;
    scroll-padding-top: 5.5rem;
}

.public-page h1,
.public-page h2,
.public-page h3,
.public-page h4 {
    margin: 0;
    color: var(--sky-900);
    font-weight: 780;
    line-height: 1.18;
    letter-spacing: -0.02em;
    text-wrap: balance;
}

.public-page p {
    margin: 0;
    text-wrap: pretty;
}

.public-page a {
    color: var(--sky-700);
    text-underline-offset: 3px;
}

.public-page .public-container {
    width: 100%;
    max-width: var(--container);
    margin: 0 auto;
    padding: 0 var(--gutter);
}

/* Anchors jumped to must clear the sticky header even without scroll-padding. */
.public-page [id] {
    scroll-margin-top: 5.5rem;
}

.public-page .skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 60;
    padding: 0.75rem 1.15rem;
    background: var(--sky-700);
    color: var(--on-primary);
    border-radius: 0 0 var(--radius-sm) 0;
    font-weight: 650;
    text-decoration: none;
}

.public-page .skip-link:focus {
    left: 0;
}

/* --------------------------------------------------------------------------
   3. Header and navigation

   Mobile first: the nav folds into a <details> disclosure. From 992px the
   toggle is hidden and the same list is laid out inline. The desktop rule uses
   author !important to beat the browser's own "a closed <details> hides its
   content" rule, which is what makes the desktop menu work with JavaScript off.
   -------------------------------------------------------------------------- */

.public-page .site-header {
    position: sticky;
    top: 0;
    z-index: 40;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    transition: box-shadow 0.2s ease;
}

/* A translucent bar with a blur reads better over a full-bleed hero. The solid
   background above is the fallback for a browser without backdrop-filter. */
@supports (backdrop-filter: blur(1px)) {
    .public-page .site-header {
        background: color-mix(in srgb, var(--surface) 88%, transparent);
        backdrop-filter: blur(14px) saturate(1.4);
        -webkit-backdrop-filter: blur(14px) saturate(1.4);
        border-bottom-color: transparent;
    }
}

.public-page .site-header.is-scrolled {
    border-bottom-color: var(--border);
    box-shadow: var(--shadow-md);
}

.public-page .site-header-inner {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-sm);
    padding-top: 0.7rem;
    padding-bottom: 0.7rem;
}

.public-page .site-brand {
    display: inline-flex;
    align-items: center;
    gap: 0.7rem;
    min-width: 0;
    text-decoration: none;
}

.public-page .site-brand-text {
    font-weight: 800;
    font-size: clamp(1rem, 0.9rem + 0.4vw, 1.18rem);
    letter-spacing: -0.02em;
    color: var(--sky-900);
    min-width: 0;
    overflow-wrap: break-word;
}

/*
 * The uploaded logo, in place of the letter tile.
 *
 * The one image on the site whose own proportions win. Rather than being pushed
 * into a fixed box it declares the height it wants and is bounded in both
 * directions: never taller than the header's rhythm, never wider than a share of
 * the bar. A wide logo therefore shrinks instead of pushing the navigation off
 * the screen, and a tall one cannot stretch the header.
 */
.public-page .site-brand-logo {
    display: block;
    flex: 0 0 auto;
    width: auto;
    height: 2.5rem;
    max-width: min(9rem, 40vw);
    object-fit: contain;
    object-position: left center;
}

@media (min-width: 768px) {
    .public-page .site-brand-logo {
        height: 3rem;
        max-width: 11rem;
    }
}

/* --- the menu button --- */

.public-page .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    min-height: var(--tap);
    min-width: var(--tap);
    padding: 0.5rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--sky-800);
    font-weight: 650;
    font-size: var(--fs-sm);
    cursor: pointer;
    list-style: none;
    transition: background-color 0.18s ease, border-color 0.18s ease;
}

.public-page .nav-toggle::-webkit-details-marker {
    display: none;
}

.public-page .nav-toggle:hover {
    background: var(--sky-50);
    border-color: var(--sky-200);
}

.public-page .nav-toggle:focus-visible {
    outline: 3px solid var(--ring);
    outline-offset: 2px;
}

.public-page .nav-toggle-bars,
.public-page .nav-toggle-bars::before,
.public-page .nav-toggle-bars::after {
    display: block;
    width: 1.1rem;
    height: 2px;
    border-radius: 2px;
    background: var(--sky-700);
    transition: transform 0.22s ease, background-color 0.22s ease;
}

.public-page .nav-toggle-bars {
    position: relative;
}

/* The word beside the bars. Kept on one line so it cannot wrap and make the
   button taller than the rest of the header at 320px. */
.public-page .nav-toggle-text {
    white-space: nowrap;
}

.public-page .nav-toggle-bars::before,
.public-page .nav-toggle-bars::after {
    content: "";
    position: absolute;
    left: 0;
}

.public-page .nav-toggle-bars::before { top: -6px; }
.public-page .nav-toggle-bars::after { top: 6px; }

/* Open: the bars become a close mark, so the control says what it will do next
   rather than only what is currently on screen. */
.public-page .site-nav-disclosure[open] > .nav-toggle .nav-toggle-bars {
    background: transparent;
}

.public-page .site-nav-disclosure[open] > .nav-toggle .nav-toggle-bars::before {
    transform: translateY(6px) rotate(45deg);
}

.public-page .site-nav-disclosure[open] > .nav-toggle .nav-toggle-bars::after {
    transform: translateY(-6px) rotate(-45deg);
}

/* --- the drawer --- */

.public-page .site-nav {
    display: none;
    list-style: none;
}

.public-page .site-nav-disclosure[open] > .site-nav {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    z-index: 5;
    max-height: calc(100vh - 4.5rem);
    max-height: calc(100dvh - 4.5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-sm) var(--gutter) var(--space-md);
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
    animation: nav-drop 0.18s ease-out;
}

/* Animating in works even though the element goes from display:none - an
   animation starts when the box is first generated, which a transition does not.
   There is no matching exit animation, because the content is display:none again
   the moment the <details> closes; the backdrop covers that. */
@keyframes nav-drop {
    from { opacity: 0; transform: translateY(-0.5rem); }
    to { opacity: 1; transform: none; }
}

.public-page .site-nav-list {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.public-page .site-nav-link,
.public-page .site-nav-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem;
    min-height: var(--tap);
    padding: 0.6rem 0.75rem;
    border-radius: var(--radius-sm);
    color: var(--sky-900);
    font-weight: 650;
    font-size: 1rem;
    text-decoration: none;
    cursor: pointer;
    list-style: none;
}

.public-page .site-nav-summary::-webkit-details-marker {
    display: none;
}

.public-page .site-nav-link:hover,
.public-page .site-nav-link:focus-visible,
.public-page .site-nav-summary:hover,
.public-page .site-nav-summary:focus-visible {
    background: var(--sky-50);
    color: var(--sky-800);
    text-decoration: none;
}

.public-page .site-nav-link:focus-visible,
.public-page .site-nav-summary:focus-visible {
    outline: 3px solid var(--ring);
    outline-offset: -3px;
}

.public-page .site-nav-link.is-active {
    color: var(--sky-700);
}

.public-page .site-nav-sub {
    margin: 0.1rem 0 0.4rem;
    padding: 0 0 0 0.9rem;
    list-style: none;
    border-left: 2px solid var(--sky-100);
}

.public-page .site-nav-sublink {
    display: flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 0.96rem;
    text-decoration: none;
    overflow-wrap: anywhere;
}

.public-page .site-nav-sublink:hover,
.public-page .site-nav-sublink:focus-visible,
.public-page .site-nav-sublink.is-active {
    background: var(--sky-50);
    color: var(--sky-800);
    text-decoration: none;
}

.public-page .nav-caret {
    flex: 0 0 auto;
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid currentColor;
    transition: transform 0.18s ease;
}

.public-page .nav-more[open] > .site-nav-summary .nav-caret {
    transform: rotate(180deg);
}

.public-page .site-nav-cta {
    margin-top: 0.6rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--border);
}

.public-page .site-nav-cta .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: var(--tap);
}

/* --- the backdrop --- */

.public-page .site-nav-backdrop {
    display: none;
}

/* Revealed only while the menu is open. :has() is a progressive enhancement
   here: without it the menu still opens and every link still works, there is
   simply no dim behind it. Never on desktop, where the nav is inline and nothing
   is modal. Clicking it closes the menu, because it sits outside the <details>
   and public.js already closes on any click outside. */
body.public-page:has(.site-nav-disclosure[open]) .site-nav-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 4;
    background: rgba(8, 47, 73, 0.44);
    animation: backdrop-in 0.18s ease-out;
}

@keyframes backdrop-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* --- desktop --- */

@media (min-width: 992px) {
    .public-page .site-header-inner {
        flex-wrap: nowrap;
        padding-top: 0.85rem;
        padding-bottom: 0.85rem;
    }

    .public-page .site-nav-disclosure > .nav-toggle {
        display: none;
    }

    /* Author !important is the one reliable way to beat the user-agent rule that
       hides a closed <details>' content - and this is exactly what makes the
       desktop navigation work with JavaScript switched off. */
    .public-page .site-nav {
        display: block !important;
        position: static;
        padding: 0;
        background: none;
        border: 0;
        box-shadow: none;
        max-height: none;
        overflow: visible;
        animation: none;
    }

    .public-page .site-nav-list {
        flex-direction: row;
        align-items: center;
        gap: 0.1rem;
    }

    .public-page .site-nav-link,
    .public-page .site-nav-summary {
        min-height: auto;
        padding: 0.55rem 0.8rem;
        font-size: 0.97rem;
    }

    /* The active page gets a real indicator rather than only a colour shift, so
       the state does not rest on colour alone. */
    .public-page .site-nav-link.is-active {
        position: relative;
        background: var(--sky-50);
    }

    .public-page .site-nav-link.is-active::after {
        content: "";
        position: absolute;
        left: 0.8rem;
        right: 0.8rem;
        bottom: 0.25rem;
        height: 2px;
        border-radius: 2px;
        background: var(--sky-600);
    }

    .public-page .site-nav-cta {
        margin-top: 0;
        margin-left: 0.5rem;
        padding-top: 0;
        border-top: 0;
    }

    .public-page .site-nav-cta .btn {
        width: auto;
    }

    .public-page .nav-more {
        position: relative;
    }

    .public-page .nav-more > .site-nav-sub {
        position: absolute;
        top: calc(100% + 0.5rem);
        left: 0;
        z-index: 6;
        min-width: 15rem;
        max-width: min(22rem, 90vw);
        margin: 0;
        padding: 0.4rem;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        box-shadow: var(--shadow-lg);
    }

    .public-page .site-nav-sublink {
        min-height: auto;
        padding: 0.55rem 0.75rem;
    }

    body.public-page:has(.site-nav-disclosure[open]) .site-nav-backdrop {
        display: none;
    }
}

/* --------------------------------------------------------------------------
   4. Hero — the single hero, used when no hero slides are published
   -------------------------------------------------------------------------- */

.public-page .hero {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    padding: clamp(3.5rem, 10vw, 7rem) 0 clamp(3.5rem, 9vw, 6rem);
    background: linear-gradient(152deg, var(--sky-700) 0%, var(--sky-800) 48%, var(--sky-900) 100%);
    color: var(--on-primary);
}

/* Two soft light pools. Pure CSS — no background image to download. */
.public-page .hero-glow {
    position: absolute;
    inset: -20% -10% auto -10%;
    height: 150%;
    z-index: -1;
    background:
        radial-gradient(38rem 26rem at 88% 12%, rgba(255, 255, 255, 0.2), transparent 62%),
        radial-gradient(30rem 22rem at 8% 88%, rgba(125, 211, 252, 0.26), transparent 66%);
    pointer-events: none;
}

@media (min-width: 992px) {
    .public-page .hero {
        padding: clamp(5rem, 10vw, 8.5rem) 0 clamp(4.5rem, 9vw, 7rem);
    }
}

.public-page .hero-eyebrow {
    margin: 0 0 1rem;
    font-size: var(--fs-eyebrow);
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--sky-200);
}

.public-page .hero-title {
    margin: 0;
    max-width: 20ch;
    font-size: var(--fs-h1);
    font-weight: 800;
    line-height: 1.05;
    letter-spacing: -0.028em;
    color: var(--on-primary);
    text-wrap: balance;
}

.public-page .hero-intro {
    margin: 1.35rem 0 0;
    max-width: 58ch;
    font-size: var(--fs-lead);
    line-height: 1.68;
    color: rgba(255, 255, 255, 0.92);
    text-wrap: pretty;
}

/* The hero's words are the administrator's too, so they get the same guard. */
.public-page .hero-eyebrow,
.public-page .hero-title,
.public-page .hero-intro {
    overflow-wrap: break-word;
}

.public-page .hero-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 2.1rem;
}

.public-page .hero-btn,
.public-page .public-cta-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap);
    padding: 0.85rem 1.6rem;
    border-radius: var(--radius-sm);
    font-weight: 700;
    font-size: 1rem;
    text-align: center;

    /* A button is a flex item, so `min-width: auto` would let a long unbroken
       label refuse to shrink and widen the row past the screen. break-word on
       its own does not affect intrinsic sizing, which is why the min-width is
       needed here and not on the block-level text above. */
    min-width: 0;
    overflow-wrap: break-word;
}

/* On a dark brand band the buttons invert: a white fill for the primary action,
   an outlined secondary. Both are measured against the band, not against white. */
.public-page .hero-actions .btn-sky,
.public-page .public-cta-actions .btn-sky {
    background-color: #ffffff;
    border-color: #ffffff;
    color: var(--sky-800);
}

.public-page .hero-actions .btn-sky:hover,
.public-page .hero-actions .btn-sky:focus,
.public-page .public-cta-actions .btn-sky:hover,
.public-page .public-cta-actions .btn-sky:focus {
    background-color: var(--sky-50);
    border-color: var(--sky-50);
    color: var(--sky-900);
}

.public-page .hero-actions .btn-outline-sky,
.public-page .public-cta-actions .btn-outline-sky {
    border-color: rgba(255, 255, 255, 0.7);
    color: var(--on-primary);
}

.public-page .hero-actions .btn-outline-sky:hover,
.public-page .hero-actions .btn-outline-sky:focus,
.public-page .public-cta-actions .btn-outline-sky:hover,
.public-page .public-cta-actions .btn-outline-sky:focus {
    background-color: rgba(255, 255, 255, 0.16);
    border-color: #ffffff;
    color: var(--on-primary);
}

.public-page .hero-scroll {
    display: inline-flex;
    align-items: center;
    gap: 0.7rem;
    min-height: var(--tap);
    margin-top: 2.1rem;
    color: rgba(255, 255, 255, 0.88);
    font-size: var(--fs-sm);
    font-weight: 650;
    text-decoration: none;
}

.public-page .hero-scroll:hover,
.public-page .hero-scroll:focus-visible {
    color: var(--on-primary);
    text-decoration: none;
}

.public-page .hero-scroll-icon {
    width: 2.25rem;
    height: 2.25rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid rgba(255, 255, 255, 0.5);
    border-radius: 50%;
    animation: hero-nudge 2.4s ease-in-out infinite;
}

.public-page .hero-scroll-icon::after {
    content: "";
    width: 0.5rem;
    height: 0.5rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
}

@keyframes hero-nudge {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(4px); }
}

/* --------------------------------------------------------------------------
   5. Sections — the shared shell and the section introduction
   -------------------------------------------------------------------------- */

.public-page .public-section {
    position: relative;
    padding: var(--section-y) 0;
    background: var(--surface);
}

.public-page .public-section--alt {
    background: var(--surface-muted);
}

.public-page .section-head {
    max-width: 48rem;
    margin-bottom: var(--space-xl);
}

.public-page .section-eyebrow {
    margin: 0 0 0.6rem;
    font-size: var(--fs-eyebrow);
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--sky-700);
    overflow-wrap: break-word;
}

/*
 * The section heading, with the page's one recurring accent: a short rule
 * beneath it. The rule is a pseudo-element rather than a border so it can be a
 * gradient and so it never changes the element's box.
 */
.public-page .section-title {
    position: relative;
    margin: 0 0 1.15rem;
    max-width: 34ch;
    padding-bottom: 0.85rem;
    font-size: var(--fs-h2);
    font-weight: 800;
    line-height: 1.14;
    letter-spacing: -0.022em;
    color: var(--sky-900);
    text-wrap: balance;

    /* The heading is typed by an administrator, so it can be one unbroken word. */
    overflow-wrap: break-word;
}

.public-page .section-title::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 3rem;
    height: 3px;
    border-radius: 3px;
    background: linear-gradient(90deg, var(--sky-400), var(--sky-600));
}

.public-page .section-lead {
    margin: 0;
    max-width: 58ch;
    font-size: var(--fs-lead);
    line-height: 1.68;
    color: var(--text-muted);
    text-wrap: pretty;
}

/* A section head with no lead: the heading's own padding is the only space
   needed below it, so the extra margin would just be a gap. */
.public-page .section-head > .section-title:last-child {
    margin-bottom: 0;
}

/* --------------------------------------------------------------------------
   6. Section content written by the administrator

   Only the tags HtmlSanitizer allows can appear here: p, br, strong, b, em, i,
   u, s, ul, ol, li, h2, h3, h4, blockquote, code, pre and links.
   -------------------------------------------------------------------------- */

.public-page .public-section-body {
    max-width: var(--measure);
    font-size: var(--fs-body);
    line-height: 1.78;
    color: var(--text);

    /*
     * This content is typed by hand, so it can contain anything: a pasted web
     * address, an unbroken reference number, a long code token, a string of
     * digits. A single token that cannot wrap sets a min-content width wider
     * than this column, which pushes the section past the edge of the viewport
     * and puts a horizontal scrollbar across the whole page.
     *
     * `break-word` splits a word only when it cannot fit on a line of its own,
     * so ordinary prose and the editor's manual line breaks are untouched -
     * nothing is clipped, truncated or hidden, the text simply wraps.
     *
     * overflow-wrap is inherited, so this one declaration also covers every
     * element the editor can produce. <pre> is deliberately unaffected - it keeps
     * white-space: pre and its own overflow-x, so a code block still scrolls
     * inside its own box instead of being broken up.
     */
    overflow-wrap: break-word;
}

.public-page .public-section-body > *:first-child { margin-top: 0; }
.public-page .public-section-body > *:last-child { margin-bottom: 0; }

/* The opening paragraph is promoted to a lead, which is what gives a long
   section a clear entry point without any extra field to fill in. */
.public-page .public-section-body > p:first-child {
    font-size: var(--fs-lead);
    line-height: 1.68;
    color: var(--text-muted);
}

.public-page .public-section-body p { margin: 0 0 1.15rem; }

.public-page .public-section-body h2,
.public-page .public-section-body h3,
.public-page .public-section-body h4 {
    margin: 2rem 0 0.65rem;
    color: var(--sky-900);
    line-height: 1.25;
}

.public-page .public-section-body h2,
.public-page .public-section-body h3 { font-size: var(--fs-h3); }
.public-page .public-section-body h4 { font-size: var(--fs-h4); }

/* A subsection heading carries a short accent bar, so a long section reads as
   genuinely divided rather than as one wall of text. */
.public-page .public-section--topic .public-section-body > :is(h2, h3, h4) {
    position: relative;
    padding-left: 0.9rem;
}

.public-page .public-section--topic .public-section-body > :is(h2, h3, h4)::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.15em;
    bottom: 0.15em;
    width: 3px;
    border-radius: 3px;
    background: var(--sky-400);
}

.public-page .public-section-body ul,
.public-page .public-section-body ol {
    margin: 0 0 1.15rem;
    padding-left: 1.35rem;
}

.public-page .public-section-body li { margin-bottom: 0.45rem; }

.public-page .public-section-body a {
    color: var(--sky-700);
    font-weight: 550;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
}

.public-page .public-section-body a:hover { color: var(--sky-900); }

.public-page .public-section-body code {
    padding: 0.12rem 0.4rem;
    background: var(--surface-muted);
    border: 1px solid var(--border);
    border-radius: var(--radius-xs);
    font-size: 0.92em;
}

.public-page .public-section-body pre {
    overflow-x: auto;
    padding: 1rem 1.15rem;
    background: var(--surface-muted);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.public-page .public-section-body pre code {
    padding: 0;
    border: 0;
    background: none;
}

/*
 * The sanitizer unwraps <img>, so a section cannot currently contain one. This
 * is a guard rather than an active rule: if that ever changes, a photograph
 * wider than the column must still not be able to widen the page.
 */
.public-page .public-section-body img {
    max-width: 100%;
    height: auto;
}

/* --- a list becomes a grid of cards ---------------------------------------
 *
 * The single biggest visual win available without a schema change. A school that
 * writes its programmes, facilities or subjects as a bullet list gets a proper
 * card grid, because a <ul> is already a list of independent items - it only
 * needs to be laid out as one. Nothing is moved or rewritten; the stylesheet
 * only changes how the existing list is displayed.
 *
 * auto-fit decides the column count from the space available: one per row on a
 * phone, two on a tablet, three on a laptop. The track floor is `min(100%, 16rem)`
 * rather than a bare length, so a card can never be forced wider than the screen
 * it is on - which is the usual cause of horizontal overflow in a grid.
 */

.public-page .public-section--list .public-section-body {
    /* Cards use the full width; only the prose is held to a reading measure. */
    max-width: none;
}

.public-page .public-section--list .public-section-body > p {
    max-width: var(--measure);
}

.public-page .public-section--list .public-section-body > ul,
.public-page .public-section--list .public-section-body > ol {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
    gap: var(--space-md);
    margin: var(--space-lg) 0 0;
    padding: 0;
    list-style: none;
}

.public-page .public-section--list .public-section-body > ul > li,
.public-page .public-section--list .public-section-body > ol > li {
    position: relative;
    margin: 0;
    padding: var(--space-md);
    padding-left: calc(var(--space-md) + 0.35rem);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-xs);
    line-height: 1.65;
    color: var(--text-muted);
    overflow-wrap: break-word;
    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}

/* The card's accent. A pseudo-element, so it cannot be mistaken for content. */
.public-page .public-section--list .public-section-body > ul > li::before,
.public-page .public-section--list .public-section-body > ol > li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    border-radius: var(--radius-md) 0 0 var(--radius-md);
    background: linear-gradient(180deg, var(--sky-400), var(--sky-600));
}

/* A leading bold phrase is the item's title - the natural way to write
   "**Junior Secondary** - description" in the editor. */
.public-page .public-section--list .public-section-body > ul > li > strong:first-child,
.public-page .public-section--list .public-section-body > ol > li > strong:first-child {
    display: block;
    margin-bottom: 0.3rem;
    font-size: var(--fs-h3);
    font-weight: 750;
    line-height: 1.25;
    letter-spacing: -0.012em;
    color: var(--sky-900);
}

/* Hover only where a pointer can actually hover, so a touch device does not
   leave a card stuck in its lifted state after a tap. */
@media (hover: hover) and (pointer: fine) {
    .public-page .public-section--list .public-section-body > ul > li:hover,
    .public-page .public-section--list .public-section-body > ol > li:hover {
        transform: translateY(-3px);
        border-color: var(--sky-200);
        box-shadow: var(--shadow-md);
    }
}

/* --- a quotation becomes a pull quote ------------------------------------- */

.public-page .public-section--quote .public-section-body > blockquote {
    position: relative;
    margin: var(--space-lg) 0;
    padding: var(--space-lg) var(--space-md) var(--space-md) calc(var(--space-md) + 1rem);
    background: var(--sky-50);
    border: 1px solid var(--border);
    border-left: 4px solid var(--sky-500);
    border-radius: var(--radius-md);
    font-size: var(--fs-lead);
    line-height: 1.6;
    font-weight: 500;
    color: var(--sky-900);
}

.public-page .public-section--quote .public-section-body > blockquote::before {
    content: "\201C";
    position: absolute;
    left: 0.5rem;
    top: 0.1rem;
    font-size: 3rem;
    line-height: 1;
    color: var(--sky-300);
    pointer-events: none;
}

.public-page .public-section--quote .public-section-body > blockquote p {
    margin: 0;
}

.public-page .public-section--quote .public-section-body > blockquote p + p {
    margin-top: 0.75rem;
}

/* Honest empty state — shown only when the school has published nothing. */
.public-page .empty-card {
    max-width: 44rem;
    padding: clamp(1.6rem, 4vw, 2.6rem);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
}

.public-page .empty-card .section-title {
    font-size: var(--fs-h3);
}

.public-page .empty-text {
    margin: 0;
    color: var(--text-muted);
    line-height: 1.7;
}

/* --------------------------------------------------------------------------
   7. Card grids — the components the reserved sections render into

   Every one of these sections is empty until the next stage fills it, so nothing
   below is on screen today. It is built now so that connecting the content later
   is a data change and not a markup change.
   -------------------------------------------------------------------------- */

.public-page .card-grid,
.public-page .quote-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
    gap: var(--space-md);
    margin: 0;
    padding: 0;
    list-style: none;
}

.public-page .card {
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
    .public-page .card:hover {
        transform: translateY(-4px);
        border-color: var(--sky-200);
        box-shadow: var(--shadow-lg);
    }
}

.public-page .card-body {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    flex: 1 1 auto;
    min-width: 0;
    padding: var(--space-md);
}

.public-page .card-media {
    flex: 0 0 auto;
    background: var(--surface-muted);
}

.public-page .card-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
}

.public-page .card-title {
    margin: 0;
    font-size: var(--fs-h3);
    font-weight: 750;
    line-height: 1.25;
    letter-spacing: -0.012em;
    color: var(--sky-900);
    overflow-wrap: break-word;
}

.public-page .card-text {
    margin: 0;
    color: var(--text-muted);
    line-height: 1.65;
    overflow-wrap: break-word;
}

.public-page .card-meta {
    margin: 0;
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--sky-700);
    overflow-wrap: break-word;
}

.public-page .card-link {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    min-height: var(--tap);
    margin-top: auto;
    padding-top: 0.4rem;
    color: var(--sky-700);
    font-weight: 650;
    text-decoration: none;
}

.public-page .card-link:hover,
.public-page .card-link:focus-visible {
    color: var(--sky-900);
    text-decoration: underline;
}

.public-page .card-link-arrow {
    transition: transform 0.18s ease;
}

.public-page .card-link:hover .card-link-arrow {
    transform: translateX(3px);
}

/* --- news article (detail page) --- */

.public-page .news-article-main {
    padding: clamp(2rem, 1.4rem + 2.5vw, 3.5rem) 0 4rem;
}

.public-page .news-article {
    max-width: var(--measure);
    margin: 0 auto;
}

.public-page .news-article-head {
    margin-bottom: var(--space-md);
}

.public-page .news-article-date {
    margin: 0 0 0.55rem;
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--sky-700);
    overflow-wrap: break-word;
}

.public-page .news-article-title {
    margin: 0;
    font-size: var(--fs-h1);
    line-height: 1.14;
    overflow-wrap: break-word;
}

.public-page .news-article-figure {
    margin: 0 0 var(--space-lg);
}

.public-page .news-article-figure img {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

.public-page .news-article-back {
    margin: 0 0 var(--space-md);
}

.public-page .news-article-back--foot {
    margin: var(--space-lg) 0 0;
}

.public-page .news-article-back a {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: var(--tap);
    font-weight: 650;
    text-decoration: none;
}

.public-page .news-article-back a:hover,
.public-page .news-article-back a:focus-visible {
    text-decoration: underline;
}

.public-page .news-article-link {
    margin: var(--space-md) 0 0;
}

/* --- testimonials --- */

.public-page .quote {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    height: 100%;
    margin: 0;
    padding: var(--space-lg) var(--space-md);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}

.public-page .quote-text {
    position: relative;
    flex: 1 1 auto;
    margin: 0;
    color: var(--text);
    font-size: var(--fs-lead);
    line-height: 1.6;
}

.public-page .quote-text p { margin: 0; }

.public-page .quote-attribution {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding-top: var(--space-sm);
    border-top: 1px solid var(--border);
}

.public-page .quote-author {
    font-style: normal;
    font-weight: 700;
    color: var(--sky-900);
}

.public-page .quote-role {
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

/* --- contact --- */

.public-page .contact-layout {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
    gap: var(--space-lg);
}

.public-page .contact-list {
    display: grid;
    gap: var(--space-md);
    margin: 0;
}

.public-page .contact-item {
    padding-left: var(--space-sm);
    border-left: 3px solid var(--sky-200);
}

.public-page .contact-label {
    margin: 0 0 0.2rem;
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.public-page .contact-value {
    margin: 0;
    font-size: var(--fs-lead);
    font-weight: 600;
    color: var(--sky-900);
    overflow-wrap: break-word;
}

.public-page .contact-value a {
    color: inherit;
    text-decoration: none;
}

.public-page .contact-value a:hover {
    text-decoration: underline;
}

/* Social accounts live in the footer, where every page has them. These rules are
   shared with the footer's own list so the two cannot drift. */
.public-page .site-footer-social-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.public-page .site-footer-social-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0.4rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--sky-700);
    font-weight: 600;
    font-size: var(--fs-sm);
    text-decoration: none;
    transition: background-color 0.18s ease, border-color 0.18s ease;
}

/* --------------------------------------------------------------------------
   8. Gallery — the featured carousel

   Still the same carousel: one slide per view, the same controls, the same
   swipe, the same autoplay. Only the presentation changed.
   -------------------------------------------------------------------------- */

.public-page .gallery {
    position: relative;
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    background: var(--surface);
    overflow: hidden;
    box-shadow: var(--shadow-lg);
}

.public-page .gallery-viewport {
    overflow: hidden;
    touch-action: pan-y;
    cursor: grab;
}

.public-page .gallery-viewport:focus-visible {
    outline: 3px solid var(--ring);
    outline-offset: -3px;
}

.public-page .gallery-viewport:active { cursor: grabbing; }

.public-page .gallery-track {
    display: flex;
    margin: 0;
    padding: 0;
    list-style: none;
    will-change: transform;
}

.public-page .gallery-slide {
    flex: 0 0 100%;
    min-width: 100%;
    margin: 0;
}

/* The frame every gallery slide is shown in.

   Column, not row. The caption belongs under the photograph; as a row the two
   competed for the same width, so a slide with a caption put the caption beside
   the image and squeezed the image into whatever was left.

   The height is definite rather than a minimum. When the frame is only as tall
   as the photograph on screen, the gallery changes size as it rotates and drags
   the whole page with it; a reserved box means every slide occupies the same
   space and the carousel is steady whatever is uploaded. The photograph itself
   is contained, so nothing is cropped to fill the box.

   On a phone the height is proportional to the width, so the frame can never be
   taller than the screen; from 768px it takes a share of the viewport instead. */
.public-page .gallery-figure {
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: clamp(190px, 58vw, 300px);
    /* Nothing may spill out of the reserved box, whatever a caption does. */
    overflow: hidden;
    /* A soft brand wash, so the letterbox either side of a contained photograph
       reads as a mount rather than as empty space. */
    background:
        radial-gradient(120% 100% at 50% 0%, var(--tint-soft), transparent 70%),
        var(--surface-muted);
}

@media (min-width: 768px) {
    .public-page .gallery-figure {
        height: clamp(320px, 46vh, 620px);
    }
}

/* The photograph takes whatever is left once the caption has its space.
   min-height: 0 is what allows it to shrink below its intrinsic height rather
   than pushing the frame taller and breaking the fixed shape; the fitting itself
   comes from .img-contain, which shows the whole photograph and never stretches
   it. */
.public-page .gallery-image {
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
    min-height: 0;
    user-select: none;
    -webkit-user-drag: none;
}

.public-page .gallery-caption {
    /* Allowed to shrink, and to scroll if an unusually long caption would
       otherwise spill out of the reserved height. Nothing is cut off: a caption
       that fits - which is every caption this schema can hold, since the column
       is capped at 255 characters - simply renders, and one that somehow does not
       can be scrolled rather than swallowed. */
    flex: 0 1 auto;
    width: 100%;
    min-height: 0;
    overflow-y: auto;
    padding: 0.9rem 1.15rem 1rem;
    margin: 0;
    font-size: var(--fs-sm);
    line-height: 1.6;
    color: var(--text-muted);
    text-align: center;
    border-top: 1px solid var(--border);
    background: var(--surface);

    /* Captions and titles are typed by an administrator, like everything else. */
    overflow-wrap: break-word;
}

.public-page .gallery-caption strong {
    display: block;
    margin-bottom: 0.15rem;
    color: var(--sky-900);
    font-size: 1rem;
}

.public-page .gallery-control {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: var(--tap);
    height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.94);
    background: color-mix(in srgb, var(--surface) 94%, transparent);
    color: var(--sky-800);
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    box-shadow: var(--shadow-md);
    transition: background-color 0.18s ease, transform 0.18s ease;
}

@media (min-width: 768px) {
    .public-page .gallery-control {
        width: 3rem;
        height: 3rem;
    }
}

.public-page .gallery-control:hover {
    background: #ffffff;
    transform: translateY(-50%) scale(1.06);
}

.public-page .gallery-control:focus-visible {
    outline: 3px solid var(--ring);
    outline-offset: 2px;
}

.public-page .gallery-control--prev { left: clamp(0.5rem, 2vw, 1.1rem); }
.public-page .gallery-control--next { right: clamp(0.5rem, 2vw, 1.1rem); }

.public-page .gallery-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    flex-wrap: wrap;
    padding: 0.5rem 1.15rem;
    border-top: 1px solid var(--border);
    background: var(--surface);
}

.public-page .gallery-dots,
.public-page .hero-dots {
    display: flex;
    align-items: center;
    gap: 0;
    flex-wrap: wrap;
}

/*
 * The indicator is a pill, not a dot: the current slide's marker stretches.
 *
 * The visible mark is the content box, kept small by padding and drawn with
 * background-clip. That lets the control itself be --tap tall - a real touch
 * target - while the mark stays delicate. Only the width changes on the active
 * pill, so the row never jumps height as it moves.
 */
.public-page .gallery-dot {
    width: 1.75rem;
    height: var(--tap);
    padding: calc((var(--tap) - 0.5rem) / 2) 0.5rem;
    border: 0;
    border-radius: var(--radius-pill);
    background-color: var(--sky-200);
    background-clip: content-box;
    cursor: pointer;
    transition: width 0.22s ease, background-color 0.22s ease;
}

.public-page .gallery-dot:hover { background-color: var(--sky-300); }
.public-page .gallery-dot.is-current {
    width: 2.75rem;
    background-color: var(--sky-600);
}

.public-page .gallery-dot:focus-visible {
    outline: 3px solid var(--ring);
    outline-offset: -6px;
}

/* --------------------------------------------------------------------------
   9. Closing call to action
   -------------------------------------------------------------------------- */

.public-page .public-cta {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    padding: var(--section-y) 0;
    background: linear-gradient(142deg, var(--sky-700) 0%, var(--sky-800) 55%, var(--sky-900) 100%);
    color: var(--on-primary);
}

/* The same soft light pool as the hero, so the two brand bands rhyme. */
.public-page .public-cta::before {
    content: "";
    position: absolute;
    inset: -30% -10% auto -10%;
    height: 170%;
    z-index: -1;
    background: radial-gradient(34rem 22rem at 82% 10%, rgba(255, 255, 255, 0.18), transparent 64%);
    pointer-events: none;
}

.public-page .public-cta-inner {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-lg);
}

@media (min-width: 768px) {
    .public-page .public-cta-inner {
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-xl);
    }
}

.public-page .public-cta-text {
    max-width: 46ch;
    min-width: 0;
}

.public-page .public-cta-eyebrow {
    margin: 0 0 0.6rem;
    font-size: var(--fs-eyebrow);
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--sky-200);
    overflow-wrap: break-word;
}

.public-page .public-cta-title {
    margin: 0;
    font-size: clamp(1.5rem, 1.1rem + 1.8vw, 2.15rem);
    font-weight: 800;
    line-height: 1.16;
    letter-spacing: -0.02em;
    color: var(--on-primary);
    text-wrap: balance;
    overflow-wrap: break-word;
}

.public-page .public-cta-body {
    margin: 0.9rem 0 0;
    font-size: var(--fs-lead);
    line-height: 1.65;
    color: rgba(255, 255, 255, 0.9);
    text-wrap: pretty;
    overflow-wrap: break-word;
}

.public-page .public-cta-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    flex: 0 0 auto;
    width: 100%;
}

@media (min-width: 768px) {
    .public-page .public-cta-actions {
        width: auto;
        justify-content: flex-end;
    }
}

/* --------------------------------------------------------------------------
   10. Footer
   -------------------------------------------------------------------------- */

.public-page .site-footer {
    padding: clamp(2.5rem, 5vw, 3.75rem) 0 1.5rem;
    background: linear-gradient(180deg, var(--sky-900) 0%, #082f49 100%);
    color: rgba(255, 255, 255, 0.84);
}

.public-page .site-footer-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-lg);
    padding-bottom: var(--space-lg);
}

@media (min-width: 768px) {
    .public-page .site-footer-grid[data-columns="2"] {
        grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
    }

    .public-page .site-footer-grid[data-columns="3"] {
        grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr) minmax(0, 1fr);
    }
}

.public-page .site-footer-brand {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    min-width: 0;
}

/* A grid child holds text, so it needs the same min-width guard as a flex one:
   without it a long school name or a long email address sets the column's
   min-content width and pushes the grid wider than the screen. */
.public-page .site-footer-column {
    min-width: 0;
}

/* The shared branding block is light-on-dark here, so its colours are overridden
   in this one place rather than in the partial. */
.public-page .site-footer .site-brand-text {
    color: #ffffff;
}

.public-page .site-footer .brand-mark {
    background: rgba(255, 255, 255, 0.14);
    color: #ffffff;
}

.public-page .site-footer-tagline {
    margin: 0;
    max-width: 42ch;
    font-size: var(--fs-sm);
    line-height: 1.65;
    color: rgba(255, 255, 255, 0.78);
    overflow-wrap: break-word;
}

.public-page .site-footer-heading {
    margin: 0 0 0.9rem;
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.62);
}

.public-page .site-footer-list {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.public-page .site-footer-list a,
.public-page .site-footer-value {
    color: rgba(255, 255, 255, 0.84);
    font-size: var(--fs-sm);
    text-decoration: none;
    overflow-wrap: break-word;
}

.public-page .site-footer-label {
    display: block;
    font-size: var(--fs-xs);
    color: rgba(255, 255, 255, 0.6);
}

.public-page .site-footer-list a:hover { color: #ffffff; text-decoration: underline; }

.public-page .site-footer-list a:focus-visible {
    outline: 3px solid rgba(255, 255, 255, 0.6);
    outline-offset: 3px;
    border-radius: var(--radius-xs);
}

.public-page .site-footer-social {
    padding-bottom: var(--space-lg);
}

.public-page .site-footer-social-link {
    background: transparent;
    border-color: rgba(255, 255, 255, 0.28);
    color: #ffffff;
}

.public-page .site-footer-social-link:hover,
.public-page .site-footer-social-link:focus-visible {
    background: rgba(255, 255, 255, 0.14);
    border-color: rgba(255, 255, 255, 0.6);
    color: #ffffff;
}

.public-page .site-footer-bottom {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    padding-top: var(--space-md);
    border-top: 1px solid rgba(255, 255, 255, 0.16);
}

@media (min-width: 768px) {
    .public-page .site-footer-bottom {
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
    }
}

.public-page .site-footer-copyright {
    margin: 0;
    font-size: var(--fs-sm);
    color: rgba(255, 255, 255, 0.78);
    overflow-wrap: break-word;
}

.public-page .site-footer-legal {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1.35rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.public-page .site-footer-legal a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    color: rgba(255, 255, 255, 0.86);
    font-size: var(--fs-sm);
    font-weight: 600;
    text-decoration: none;
}

.public-page .site-footer-legal a:hover { color: #ffffff; text-decoration: underline; }

.public-page .site-footer-legal a:focus-visible {
    outline: 3px solid rgba(255, 255, 255, 0.6);
    outline-offset: 3px;
    border-radius: var(--radius-xs);
}

/* ==========================================================================
   Stage 17 - public Result Checker

   Scoped to .public-page, like the rest of this stylesheet, so nothing here can
   reach the admin. The public page is served with data-bs-theme="light" always,
   so this block needs no dark variant - and it uses only the sky scale plus
   Bootstrap's own alert tones, so there is no purple to be found.

   The layout is one column by default and widens at 48rem; the result table
   scrolls sideways rather than squashing its nine columns into a phone.
   ========================================================================== */

.checker-main {
    padding: 2.5rem 0 4rem;
}

.checker-intro {
    max-width: 46rem;
    margin: 0 auto 2rem;
    text-align: center;
}

.checker-eyebrow {
    margin: 0 0 0.5rem;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--sky-700);
}

.checker-title {
    margin: 0 0 0.75rem;
    font-size: clamp(1.75rem, 1.2rem + 2.4vw, 2.5rem);
    font-weight: 800;
    letter-spacing: -0.02em;
    color: var(--text);
}

.checker-lede {
    margin: 0;
    font-size: 1.02rem;
    line-height: 1.6;
    color: var(--text-muted);
}

/* --- the form card ------------------------------------------------------- */

.checker-card {
    max-width: 52rem;
    margin: 0 auto;
    padding: 1.75rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 1rem;
    box-shadow: var(--shadow-card);
}

.checker-card-title {
    margin: 0 0 1.25rem;
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--text);
}

.checker-field + .checker-field,
.checker-grid + .checker-field {
    margin-top: 1.1rem;
}

.checker-label {
    display: block;
    margin-bottom: 0.4rem;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--text);
}

.checker-input {
    display: block;
    width: 100%;
    padding: 0.7rem 0.85rem;
    font-size: 1rem;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0.6rem;
    transition: border-color 150ms ease, box-shadow 150ms ease;
}

.checker-input:hover {
    border-color: var(--sky-300);
}

.checker-input:focus {
    outline: none;
    border-color: var(--sky-500);
    box-shadow: 0 0 0 3px var(--ring);
}

.checker-input--pin {
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

.checker-hint {
    margin: 0.45rem 0 0;
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--text-muted);
}

.checker-grid {
    display: grid;
    gap: 1.1rem;
    margin-top: 1.1rem;
}

@media (min-width: 48rem) {
    .checker-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

.checker-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 1.5rem;
}

.checker-note {
    max-width: 52rem;
    margin: 1.25rem auto 0;
    font-size: 0.88rem;
    line-height: 1.6;
    text-align: center;
    color: var(--text-muted);
}

/* --- outcome states ------------------------------------------------------ */

.checker-state {
    max-width: 52rem;
    margin: 0 auto 1.5rem;
    padding: 1.25rem 1.4rem;
    border: 1px solid var(--border);
    border-left-width: 5px;
    border-radius: 0.75rem;
    background: var(--surface-muted);
}

.checker-state-title {
    margin: 0 0 0.5rem;
    font-size: 1.1rem;
    font-weight: 700;
    color: var(--text);
}

.checker-state-body {
    margin: 0;
    line-height: 1.6;
    color: var(--text);
}

.checker-state-hint {
    margin: 0.6rem 0 0;
    font-size: 0.88rem;
    line-height: 1.55;
    color: var(--text-muted);
}

.checker-state--danger {
    border-left-color: #dc3545;
    background: #f8d7da;
    border-color: #f5c2c7;
}

.checker-state--danger .checker-state-title,
.checker-state--danger .checker-state-body {
    color: #842029;
}

.checker-state--danger .checker-state-hint {
    color: #842029;
    opacity: 0.85;
}

.checker-state--warning {
    border-left-color: #ffc107;
    background: #fff3cd;
    border-color: #ffecb5;
}

.checker-state--warning .checker-state-title,
.checker-state--warning .checker-state-body {
    color: #664d03;
}

.checker-state--warning .checker-state-hint {
    color: #664d03;
    opacity: 0.85;
}

/* --- the published result ------------------------------------------------ */

.result-card {
    max-width: 60rem;
    margin: 0 auto 1.5rem;
    padding: 1.75rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: 5px solid var(--sky-500);
    border-radius: 1rem;
    box-shadow: var(--shadow-card);
}

.result-card-head {
    padding-bottom: 1.25rem;
    margin-bottom: 1.25rem;
    border-bottom: 1px solid var(--border);
}

.result-name {
    margin: 0 0 1rem;
    font-size: clamp(1.35rem, 1.1rem + 1.2vw, 1.85rem);
    font-weight: 800;
    letter-spacing: -0.01em;
    color: var(--text);
}

.result-meta {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: 0.9rem 1.25rem;
    margin: 0;
}

.result-meta dt {
    margin: 0 0 0.15rem;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.result-meta dd {
    margin: 0;
    font-weight: 600;
    color: var(--text);
}

.result-table-wrap {
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: 0.75rem;
}

.result-table {
    width: 100%;
    min-width: 46rem;
    border-collapse: collapse;
    font-size: 0.92rem;
}

.result-table thead th {
    padding: 0.7rem 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: left;
    white-space: nowrap;
    color: var(--sky-900);
    background: var(--sky-50);
    border-bottom: 1px solid var(--border);
}

.result-table tbody th,
.result-table tbody td {
    padding: 0.65rem 0.75rem;
    text-align: left;
    color: var(--text);
    border-bottom: 1px solid var(--border);
}

.result-table tbody tr:last-child th,
.result-table tbody tr:last-child td {
    border-bottom: 0;
}

.result-table tbody tr:nth-child(even) th,
.result-table tbody tr:nth-child(even) td {
    background: var(--surface-muted);
}

.result-subject {
    font-weight: 600;
    white-space: nowrap;
}

.result-num {
    text-align: right !important;
    font-variant-numeric: tabular-nums;
}

.result-total {
    font-weight: 700;
}

.result-grade {
    text-align: center !important;
}

.result-grade-chip {
    display: inline-block;
    min-width: 2rem;
    padding: 0.15rem 0.5rem;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--sky-900);
    background: var(--sky-100);
    border-radius: 999px;
}

.result-summary {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
    gap: 0.9rem;
    margin-top: 1.25rem;
}

.result-stat {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    padding: 0.9rem 1rem;
    background: var(--surface-muted);
    border: 1px solid var(--border);
    border-radius: 0.75rem;
}

.result-stat-label {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.result-stat-value {
    font-size: 1.3rem;
    font-weight: 800;
    color: var(--sky-800);
}

.result-remarks {
    display: grid;
    gap: 0.9rem;
    margin-top: 1.25rem;
}

.result-remark p {
    margin: 0.3rem 0 0;
    line-height: 1.6;
    color: var(--text);
}

.result-footnote {
    margin: 1.25rem 0 0;
    font-size: 0.85rem;
    color: var(--text-muted);
}

@media (max-width: 40rem) {
    .checker-card,
    .result-card {
        padding: 1.25rem;
        border-radius: 0.85rem;
    }
}

/* --------------------------------------------------------------------------
   Stage 14 - Hero slider (administrator-managed slides)

   Used only when at least one enabled hero slide exists. The single hero above
   is untouched, and inherits none of this.
   -------------------------------------------------------------------------- */

/* The section stops padding: each slide owns its own height. The .hero gradient
   stays as the backdrop, so the band looks right before the images arrive. */
.public-page .hero--slider {
    padding: 0;
}

.public-page .hero-slider {
    position: relative;
}

.public-page .hero-slider-viewport {
    overflow: hidden;
    touch-action: pan-y;
    cursor: grab;
}

.public-page .hero-slider-viewport:active {
    cursor: grabbing;
}

.public-page .hero-slider-viewport:focus-visible {
    outline: 3px solid var(--on-primary);
    outline-offset: -3px;
}

.public-page .hero-slider-track {
    display: flex;
    margin: 0;
    padding: 0;
    list-style: none;
    will-change: transform;
}

/* The image is absolutely positioned, so it never contributes height: the band
   is sized by its text plus a sensible minimum. That is what stops a very wide
   photograph from stretching the hero down the page. */
.public-page .hero-slide {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: center;
    flex: 0 0 100%;
    min-width: 100%;
    min-height: clamp(20rem, 62vh, 36rem);
    overflow: hidden;
}

@media (min-width: 992px) {
    .public-page .hero-slide {
        min-height: clamp(24rem, 64vh, 42rem);
    }
}

.public-page .hero-slide-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
}

/* So the text stays readable over any photograph, light or dark. The vertical
   scrim carries the headline; the horizontal one keeps the left-hand text clear
   even when the photograph itself is pale. */
.public-page .hero-slide-scrim {
    position: absolute;
    inset: 0;
    background:
        linear-gradient(180deg, rgba(8, 47, 73, 0.52) 0%, rgba(8, 47, 73, 0.82) 100%),
        linear-gradient(90deg, rgba(8, 47, 73, 0.5) 0%, rgba(8, 47, 73, 0.1) 70%);
}

.public-page .hero-slide-inner {
    position: relative;
    z-index: 1;
    padding-top: clamp(3.5rem, 10vw, 7rem);
    /* Extra room at the foot for the dots. */
    padding-bottom: clamp(4.5rem, 10vw, 7rem);
    color: var(--on-primary);
}

.public-page .hero-slide-title {
    max-width: 20ch;
    margin: 0;
    font-size: clamp(1.75rem, 0.95rem + 3.2vw, 3.15rem);
    font-weight: 800;
    line-height: 1.07;
    letter-spacing: -0.026em;
    color: var(--on-primary);
    text-wrap: balance;
    overflow-wrap: break-word;
}

.public-page .hero-slide-body {
    max-width: 56ch;
    margin-top: 1.1rem;
    font-size: var(--fs-lead);
    line-height: 1.68;
    color: rgba(255, 255, 255, 0.92);
    overflow-wrap: break-word;
}

/* The write-up is sanitized HTML, so it can be a heading or a list. Flatten
   their spacing here rather than letting an administrator's formatting push the
   hero out of shape. */
.public-page .hero-slide-body :is(p, ul, ol, blockquote) {
    margin: 0 0 0.7rem;
}

.public-page .hero-slide-body :is(p, ul, ol, blockquote):last-child {
    margin-bottom: 0;
}

.public-page .hero-slide-body :is(h2, h3, h4) {
    margin: 0 0 0.4rem;
    font-size: 1.15rem;
    color: inherit;
}

.public-page .hero-slide-body a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 3px;
}

/* Controls ---------------------------------------------------------------- */

.public-page .hero-control {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    padding: 0;
    border: 1px solid rgba(255, 255, 255, 0.55);
    border-radius: 50%;
    background: rgba(8, 47, 73, 0.5);
    color: var(--on-primary);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

@media (min-width: 768px) {
    .public-page .hero-control {
        width: 3rem;
        height: 3rem;
    }
}

.public-page .hero-control:hover {
    background: rgba(8, 47, 73, 0.78);
    border-color: var(--on-primary);
}

.public-page .hero-control:focus-visible {
    outline: 3px solid var(--on-primary);
    outline-offset: 2px;
}

.public-page .hero-control--prev {
    left: clamp(0.5rem, 2vw, 1.25rem);
}

.public-page .hero-control--next {
    right: clamp(0.5rem, 2vw, 1.25rem);
}

.public-page .hero-slider-footer {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    flex-wrap: wrap;
    padding: 0.5rem clamp(1rem, 4vw, 2rem);
    background: linear-gradient(0deg, rgba(8, 47, 73, 0.7) 0%, rgba(8, 47, 73, 0) 100%);
}

/* The same pill indicator as the gallery, so the two carousels read as one
   system rather than as two components that happen to share a page. */
.public-page .hero-dot {
    width: 1.75rem;
    height: var(--tap);
    padding: calc((var(--tap) - 0.5rem) / 2) 0.5rem;
    border: 0;
    border-radius: var(--radius-pill);
    background-color: rgba(255, 255, 255, 0.5);
    background-clip: content-box;
    cursor: pointer;
    transition: width 0.22s ease, background-color 0.22s ease;
}

.public-page .hero-dot:hover {
    background-color: rgba(255, 255, 255, 0.8);
}

.public-page .hero-dot.is-current {
    width: 2.75rem;
    background-color: var(--on-primary);
}

.public-page .hero-dot:focus-visible {
    outline: 3px solid var(--on-primary);
    outline-offset: -6px;
}

/* --------------------------------------------------------------------------
   11. Motion

   Reveal-on-scroll is opt-in through a class that assets/js/public-init.js puts
   on <html> before the first paint. Without that class none of this applies, so
   with JavaScript switched off every element is simply visible from the start -
   which is the only safe direction to fail.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
    .js-reveal .public-page [data-reveal] {
        opacity: 0;
        transform: translateY(14px);
    }

    .js-reveal .public-page [data-reveal].is-revealed {
        opacity: 1;
        transform: none;
        transition:
            opacity 0.55s cubic-bezier(0.22, 0.68, 0.32, 1),
            transform 0.55s cubic-bezier(0.22, 0.68, 0.32, 1);
    }
}

/* --------------------------------------------------------------------------
   12. Reduced motion

   Only this stylesheet's own decorative motion is disabled here. The carousel
   deliberately keeps its own handling - carousel.js starts paused when reduced
   motion is requested - so its transforms are left alone, because a blanket
   transition:none would stop the transitionend event it relies on to advance.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
    html:has(body.public-page) {
        scroll-behavior: auto;
    }

    .public-page .hero-scroll-icon {
        animation: none;
    }

    .public-page .site-nav-disclosure[open] > .site-nav,
    body.public-page:has(.site-nav-disclosure[open]) .site-nav-backdrop {
        animation: none;
    }

    .public-page .site-header,
    .public-page .nav-toggle-bars,
    .public-page .nav-caret,
    .public-page .card,
    .public-page .card-link-arrow,
    .public-page .gallery-control,
    .public-page .gallery-dot,
    .public-page .hero-control,
    .public-page .hero-dot {
        transition: none;
    }
}
