/* Shared foundations: tokens, resets, and the elements every page uses.
 *
 * The editorial ledger language, from the Claude Design handoff. Its rules:
 *
 *   Type      Instrument Serif for anything that carries a number or a name;
 *             DM Mono for everything else. Labels are 10-11px, uppercase,
 *             letter-spaced 0.14-0.2em. There is no sans-serif anywhere.
 *   Colour    moss green on paper. Flat fills, no gradients except a team's
 *             own card tint, no shadows except a lifted card.
 *   Shape     nothing is rounded. Every corner is square and every division
 *             is a 1px rule rather than a box.
 *   Weight    a heavy rule (ink at 85%) opens a section; hairline rules
 *             (--color-border) separate the rows inside it; a closing heavy
 *             rule ends a list.
 *
 * Light mode only, so there is one palette and no theme switching anywhere.
 */

:root {
    color-scheme: light;

    --color-bg: #f0f8ea;
    --color-surface: #f0f8ea;
    --color-surface-muted: #e4efda;
    --color-border: #cfe0be;
    --color-text: #232b1c;
    --color-text-muted: #5b6650;
    /* --color-accent, --color-success, --color-warning, --gold and --silver
       are all darkened a few percent from the handoff's values — enough to
       clear WCAG AA (4.5:1 for text, 3:1 for large text and graphical UI)
       against this palette's paper background, not enough to read as a
       different colour. --color-danger and --bronze already cleared their
       thresholds unchanged. */
    --color-accent: #5a763a;
    --color-accent-strong: #33421f;
    --color-accent-soft: #eaf2de;
    --color-success: #427933;
    --color-success-soft: #e7f3de;
    --color-danger: #b5462f;
    --color-danger-soft: #fbe9e4;
    --color-warning: #8a6319;
    --color-warning-soft: #f6ecd4;

    --color-ink-dark: #1a1f14;
    --color-ink-dark-2: #33421f;
    --color-lime: #c3dc92;
    --color-lime-strong: #d8ecab;

    --gold: #b88525;
    --silver: #838ea0;
    --bronze: #b87333;

    --radius-sm: 0px;
    --radius-md: 0px;
    --radius-lg: 0px;
    --shadow-sm: none;
    --shadow-md: 0 12px 32px rgba(20, 25, 20, 0.16);

    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-7: 3rem;

    --font-serif: "Instrument Serif", Georgia, serif;
    --font-body: "DM Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
    --font-mono: "DM Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    --sidebar-width: 300px;
}

*, *::before, *::after { box-sizing: border-box; }

/*
 * The hidden attribute is only a browser default of `display: none`, so any
 * rule of ours that sets display beats it and the element stays on screen.
 * That is how the sign in panel survived signing in, and how the "Registration
 * received" panel showed before anyone had registered.
 *
 * Both pages hide and show whole views this way, so make the attribute win
 * outright rather than remembering to check every new display rule against it.
 */
[hidden] { display: none !important; }

body {
    margin: 0;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    font-family: var(--font-body);
    font-size: 15px;
    line-height: 1.5;
    color: var(--color-text);
    background: var(--color-bg);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
    margin: 0;
    font-family: var(--font-serif);
    font-weight: 400;
    line-height: 0.92;
    letter-spacing: -0.02em;
}
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }

a { color: var(--color-accent); }
a:hover { color: var(--color-accent-strong); }

:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.skip-link {
    position: absolute;
    left: var(--space-3);
    top: -3rem;
    z-index: 30;
    padding: var(--space-2) var(--space-4);
    background: var(--color-ink-dark);
    color: var(--color-bg);
    font-size: 0.7rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    transition: top 120ms ease-in;
}
.skip-link:focus { top: var(--space-3); }

/* --- Layout ------------------------------------------------------------- */

.container {
    width: 100%;
    max-width: 1100px;
    margin: 0 auto;
    padding: 0 var(--space-4);
}

main.container {
    flex: 1;
    padding-top: var(--space-6);
    padding-bottom: var(--space-7);
    min-width: 0;
}

/*
 * Declared here, above the breakpoints, and not after them: a media query adds
 * no specificity, so an unconditional block placed later in the file silently
 * beats every override inside an earlier @media. That is what used to happen —
 * the desktop footer kept this centred text and this rule instead of the
 * left-aligned, ruleless one the sidebar layout wants, and its border ran 72px
 * wider than the content rules above it on each side.
 */
.site-footer {
    border-top: 1px solid var(--color-border);
    margin-top: var(--space-7);
    padding: var(--space-5) 0;
    color: var(--color-text-muted);
    font-size: 0.69rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    text-align: center;
}

/* --- The sidebar -------------------------------------------------------- */

.site-header {
    position: relative;
    background: var(--color-ink-dark);
    color: var(--color-bg);
    border-bottom: 1px solid rgba(240, 248, 234, 0.14);
}
.site-header__inner {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: center;
    justify-content: space-between;
    min-height: 60px;
    padding: 0 var(--space-4);
}
.site-header__brand {
    display: inline-flex;
    align-items: center;
    font-family: var(--font-body);
    font-size: 0.69rem;
    font-weight: 500;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--color-lime);
    text-decoration: none;
}
.site-header__brand:hover { color: var(--color-lime-strong); }
/* The trophy glyph was the brand mark for the old sans-serif header; the
   ledger's brand is a plain uppercase wordmark, so the emoji is dropped
   rather than restyled. It was already aria-hidden. */
.site-header__mark { display: none; }

/* --- The sidebar gauge -------------------------------------------------- */

/*
 * A big serif count under the wordmark, from the handoff. It is laid out for
 * more than one figure even though the leaderboard currently shows a single
 * one: "laps run" is the obvious second, and it is not shown because
 * score_events has no public read policy and both totals caches hold points
 * rather than laps, so an anonymous visitor cannot be told how many laps have
 * been run without a schema change.
 *
 * Only shown once the header is a sidebar. Below 781px it is a horizontal bar
 * with the nav toggle in it, and the same number is in the page lede anyway.
 */
.gauge { display: none; }
.gauge__figure { min-width: 0; }
.gauge__value {
    font-family: var(--font-serif);
    font-size: 2.875rem;
    line-height: 0.9;
    color: var(--color-lime);
    font-variant-numeric: tabular-nums;
}
.gauge__label {
    font-size: 0.63rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: rgba(240, 248, 234, 0.55);
    margin-top: 8px;
}

.site-header__controls {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.site-nav { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.site-nav__link {
    padding: var(--space-2) var(--space-3);
    color: rgba(240, 248, 234, 0.55);
    text-decoration: none;
    font-size: 0.72rem;
    font-weight: 400;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}
.site-nav__link:hover { color: var(--color-lime); }
.site-nav__link--current { color: var(--color-lime); }

/* Hidden above the breakpoint, where the nav is always shown inline. */
.nav-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    padding: 0;
    border: 1px solid rgba(240, 248, 234, 0.25);
    background: transparent;
    color: var(--color-bg);
    cursor: pointer;
    transition: background-color 120ms ease;
}
.nav-toggle:hover { background: rgba(240, 248, 234, 0.1); }

.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
    display: block;
    width: 1.1rem;
    height: 2px;
    background: currentColor;
}
.nav-toggle__bars { position: relative; }
.nav-toggle__bars::before,
.nav-toggle__bars::after {
    content: '';
    position: absolute;
    left: 0;
}
.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after { top: 6px; }

/*
 * Below the breakpoint the nav becomes a full-width dropdown, positioned
 * against .site-header (not its immediate parent) so it drops below the
 * whole header bar regardless of where it sits in .site-header__controls.
 * The toggle button takes over showing/hiding it; see js/utils/nav-toggle.js.
 *
 * Scoped to .site-header__controls .site-nav, i.e. pages that actually carry
 * a .nav-toggle button. 404.html deliberately loads no JavaScript and has no
 * button to reopen a collapsed nav, so its bare .site-nav is left alone here
 * and just wraps onto more than one line at this width instead.
 */
@media (max-width: 780px) {
    .nav-toggle { display: inline-flex; }

    .site-header__controls .site-nav {
        display: none;
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        flex-direction: column;
        gap: 0;
        margin: 0;
        padding: var(--space-2) var(--space-4) var(--space-4);
        background: var(--color-ink-dark);
        border-bottom: 1px solid rgba(240, 248, 234, 0.14);
        z-index: 20;
    }
    .site-header__controls .site-nav--open { display: flex; }

    .site-header__controls .site-nav__link {
        padding: var(--space-3) 0;
        border-top: 1px solid rgba(240, 248, 234, 0.12);
        font-size: 0.78rem;
    }
}

/*
 * Above the breakpoint the header becomes a full-height sticky sidebar: brand
 * at the top, nav pinned to the bottom, in the same moss-dark panel the rest
 * of the design uses for its inverted blocks. Pure layout — every element
 * keeps its id and class, so js/utils/nav-toggle.js needs no changes.
 */
@media (min-width: 781px) {
    body {
        display: grid;
        grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
        grid-template-rows: 1fr auto;
        grid-template-areas:
            "sidebar main"
            "sidebar footer";
        min-height: 100vh;
    }

    /*
     * The dark column runs the whole height of the document, not just the
     * first screen — so the panel is stretched (a grid item's default) and it
     * is the inner block that sticks. Making the header itself 100vh and
     * sticky, which is the obvious way round, leaves paper showing below the
     * fold on any page taller than the viewport.
     */
    .site-header {
        grid-area: sidebar;
        border-bottom: 0;
        padding: 40px 32px;
    }

    .site-header__inner {
        position: sticky;
        top: 40px;
        flex-direction: column;
        align-items: stretch;
        justify-content: space-between;
        min-height: calc(100vh - 80px);
        max-height: calc(100vh - 80px);
        overflow-y: auto;
        gap: 48px;
        padding: 0;
    }

    .gauge {
        display: flex;
        gap: 34px;
        margin-top: 44px;
    }

    .site-nav {
        flex-direction: column;
        gap: 2px;
        flex: none;
    }
    .site-nav__link {
        padding: 9px 0;
        border-top: 1px solid rgba(240, 248, 234, 0.12);
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-3);
    }
    /* The arrow marks where you are, exactly as the handoff's nav does. */
    .site-nav__link--current::after { content: "\2192"; }

    .nav-toggle { display: none !important; }

    /*
     * Centred in the column rather than pinned to its left edge, which left
     * every page's spare width as one dead band down the right-hand side. The
     * footer is given the same measured box, not just the same padding: it is
     * a separate grid item, so without a matching max-width and auto margins
     * its text would keep starting at the column's left edge while the main
     * content moved inward, and the two would visibly fall out of line.
     */
    main.container {
        grid-area: main;
        max-width: 1400px;
        margin: 0 auto;
        padding: 64px 72px 24px;
    }

    .site-footer {
        grid-area: footer;
        width: 100%;
        max-width: 1400px;
        margin: 0 auto;
        padding: 0 72px 64px;
        text-align: left;
        border-top: 0;
    }
    .site-footer .container { padding: 0; margin: 0; max-width: none; }
}

@media (min-width: 781px) and (max-width: 1100px) {
    body { grid-template-columns: 240px minmax(0, 1fr); }
    main.container { padding: 48px 40px 24px; }
    .site-footer { padding: 0 40px 48px; }
}

/* --- The eyebrow -------------------------------------------------------- */

/*
 * Label — hairline — meta. It opens every screen in the handoff and is the
 * single most recognisable piece of the language, so it is a real component
 * rather than something each page reinvents.
 */
.eyebrow {
    display: flex;
    align-items: baseline;
    gap: 14px;
    margin-bottom: 10px;
}
.eyebrow__label,
.eyebrow__meta {
    font-size: 0.63rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    white-space: nowrap;
}
.eyebrow__rule {
    flex: 1;
    height: 1px;
    background: var(--color-border);
}
.eyebrow__meta { display: inline-flex; align-items: center; gap: 8px; }
.eyebrow__meta--live { color: var(--color-success); }
.eyebrow__meta--flag { color: var(--color-danger); }

/*
 * On a phone the meta can be longer than the screen — "Hurlstone Agricultural
 * High School" is 275px on its own — so it is allowed to wrap and the rule
 * shrinks to whatever is left rather than the row running off the side.
 */
@media (max-width: 640px) {
    .eyebrow { flex-wrap: wrap; }
    .eyebrow__label,
    .eyebrow__meta { white-space: normal; min-width: 0; }
    .eyebrow__rule { min-width: 24px; }
}

/* --- Hero --------------------------------------------------------------- */

.page-hero {
    font-size: clamp(3.25rem, 9vw, 8.25rem);
    letter-spacing: -0.045em;
    line-height: 0.82;
}
.page-hero--small { font-size: clamp(2.75rem, 7vw, 6rem); }

.page-lede {
    font-family: var(--font-serif);
    font-style: italic;
    font-size: clamp(1.15rem, 2vw, 1.5rem);
    line-height: 1.35;
    color: var(--color-text-muted);
    margin-top: 22px;
    max-width: 40ch;
}

/* The heavy rule that closes a hero and opens the working part of a page. */
.rule {
    height: 1px;
    background: var(--color-text);
    opacity: 0.85;
    margin: 64px 0 0;
    border: 0;
}
.rule--tight { margin-top: 34px; }

/* A hairline rule that closes a ruled list, matching the handoff's habit of
   bracketing a list between a heavy top and a heavy bottom. */
.rule-close { border-top: 1px solid var(--color-text); }

.section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding: 18px 0 28px;
}
/* Also set on a <legend>, which the h1/h2/h3 rule above does not reach. */
.section-head__title {
    font-family: var(--font-serif);
    font-weight: 400;
    line-height: 0.92;
    letter-spacing: -0.02em;
    font-size: clamp(1.6rem, 3vw, 2.125rem);
}
.section-head--plain { padding: 0 0 20px; }

/*
 * Retained because several pages and components still build a .page-heading;
 * here it is simply the hero stack rather than the old flex-with-underline.
 */
.page-heading { margin-bottom: var(--space-5); }
.page-heading__title {
    font-size: clamp(3.25rem, 9vw, 8.25rem);
    letter-spacing: -0.045em;
    line-height: 0.82;
}
.page-heading__meta {
    margin-top: 22px;
    color: var(--color-text-muted);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

/* --- The inverted panel ------------------------------------------------- */

/* Moss-dark on paper. Used for the register aside, the About tier block and
   anywhere the page needs to change its voice. */
.panel-dark {
    background: var(--color-ink-dark);
    color: var(--color-bg);
    padding: 48px 52px;
}
.panel-dark--compact { padding: 32px 28px; }
.panel-dark h2, .panel-dark h3 { color: var(--color-bg); }
.panel-dark p { color: rgba(240, 248, 234, 0.62); }
.panel-dark__label {
    display: block;
    color: var(--color-lime);
    font-size: 0.63rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    margin-bottom: 6px;
}

/* --- The lap-tier strip ------------------------------------------------- */

/* The rising numbers, one column per tier. The column count follows the items
   rather than being fixed: the tiers are admin-editable and were collapsed from
   four to three in 0042, and a hard `repeat(4, ...)` left a dead fourth column,
   which inside a dark panel reads as a grey box beside full kit. On paper it is
   a bare grid bracketed by hairlines; inside a dark panel the 1px grid gap
   becomes the rules. */
.tier-strip {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    border-top: 1px solid var(--color-border);
    border-bottom: 1px solid var(--color-border);
}
.tier-strip__item { padding: 24px 20px 24px 0; }
.tier-strip__points {
    font-family: var(--font-serif);
    font-size: 2.75rem;
    line-height: 1;
    color: var(--color-accent);
}
.tier-strip__name {
    font-size: 0.69rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    margin-top: 8px;
}

.panel-dark .tier-strip {
    gap: 1px;
    background: rgba(240, 248, 234, 0.15);
    border: 0;
}
.panel-dark .tier-strip__item {
    background: var(--color-ink-dark);
    padding: 24px 20px;
}
/* The leftmost column sits against the panel's own padding, so its extra 20px
   would push the first tier out of line with the title and the notes above it.
   Every other column needs it to clear the 1px gap rules. */
.panel-dark .tier-strip__item:first-child { padding-left: 0; }
.panel-dark .tier-strip__points { color: var(--color-lime); }
.panel-dark .tier-strip__name { color: rgba(240, 248, 234, 0.55); }

@media (max-width: 640px) {
    .tier-strip {
        grid-auto-flow: row;
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    /* Two columns now, so items 1 and 3 are the ones on the panel's edge. */
    .panel-dark .tier-strip__item:nth-child(odd) { padding-left: 0; }
}

/* --- Numbered steps ----------------------------------------------------- */

/* A ruled list where each row is numbered in the rule colour: the About page's
   six steps and the donations page's six. It lives here rather than in either
   page's stylesheet because two pages draw it — the same reason .team-tinted
   does. The reveal below is opt-in and belongs with it: a page turns it on by
   adding js-motion to <html>, which js/utils/scroll-reveal.js's callers only do
   when JavaScript is running and the reader has not asked for less motion. */

.steps {
    display: flex;
    flex-direction: column;
    list-style: none;
    margin: 0;
    padding: 0;
}

.step {
    display: grid;
    grid-template-columns: 90px minmax(0, 1fr);
    gap: 36px;
    padding: 34px 0;
    border-top: 1px solid var(--color-border);
}

/* Set in the rule colour: the numbers order the page without competing with
   the headings for attention. */
.step__number {
    font-family: var(--font-serif);
    font-size: 2.875rem;
    line-height: 1;
    color: var(--color-border);
    font-variant-numeric: tabular-nums;
}

.step__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    /* A line of prose stops being easy to read somewhere around here, however
       much room the column has. */
    max-width: 56ch;
}

.step__title { font-size: clamp(1.5rem, 3vw, 2rem); }

.step__body p {
    font-size: 0.875rem;
    line-height: 1.75;
    color: var(--color-text-muted);
}

.step__body strong { color: var(--color-text); font-weight: 400; }

.step__body a {
    text-decoration: underline;
    text-underline-offset: 3px;
    /* A step may carry a bare URL — the donations page spells one out rather
       than hiding it behind "here" — and a monospace URL is one long word
       that will push a phone sideways if it is not allowed to break. */
    overflow-wrap: anywhere;
}

/* --- Callouts ----------------------------------------------------------- */

/* A one-line notice above a page's main content. It lives here rather than in
   home.css because two pages draw it now: the leaderboard's dismissible
   invitations, and the notice on /register telling somebody who followed a link
   from an archive which event the form actually registers for. */
.callout {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    padding: 11px 0;
    border-top: 1px solid var(--color-border);
    color: var(--color-text-muted);
    font-size: 0.78rem;
}
.callout:last-child { border-bottom: 1px solid var(--color-border); }
.callout a { text-decoration: underline; text-underline-offset: 3px; }

.callout__close {
    flex-shrink: 0;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--color-text-muted);
    font-family: var(--font-mono);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}
.callout__close:hover { color: var(--color-text); }

/* A finished event announces itself above its own final leaderboard. It sits
   outside .callouts and cannot be dismissed: the other two are invitations a
   returning visitor has already read, this one is the reason the numbers on
   the page are not moving. */
.callout--archive {
    border-bottom: 1px solid var(--color-border);
    color: var(--color-text);
}
.callout--archive strong { font-weight: 400; }

/* A step's link when it stands on its own line rather than inside a sentence.
   A block's body is plain text, so a link that used to be woven into a
   paragraph is now its own element under it; this keeps it the size of the
   prose it follows instead of jumping to the body's base size, and stops it
   stretching the full width of the column. */
.step__link {
    align-self: start;
    font-size: 0.875rem;
}

@media (max-width: 640px) {
    .step {
        grid-template-columns: minmax(0, 1fr);
        gap: var(--space-3);
        padding: 26px 0;
    }
    .step__number { font-size: 2rem; }
}

/* --- The reveal, when asked --------------------------------------------- */

/*
 * Scoped to html.js-motion, which a page's module only adds when JavaScript is
 * running and the reader has not asked for less motion — so nothing here can
 * leave the page stuck part-way through a reveal.
 */
html.js-motion .step {
    opacity: 0;
    transform: translate3d(0, 28px, 0);
    transition: opacity 420ms ease, transform 420ms cubic-bezier(.2, .7, .3, 1);
}

html.js-motion .step.is-visible {
    opacity: 1;
    transform: none;
}

@media (prefers-reduced-motion: reduce) {
    html.js-motion .step {
        opacity: 1;
        transform: none;
        transition: none;
    }
}

/* --- Status messages ---------------------------------------------------- */

.status {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding: 40px var(--space-4);
    border-top: 1px solid var(--color-border);
    color: var(--color-text-muted);
    text-align: center;
    font-family: var(--font-serif);
    font-style: italic;
    font-size: 1.375rem;
}
.status--error {
    border-top: 2px solid var(--color-danger);
    color: var(--color-danger);
    font-family: var(--font-body);
    font-style: normal;
    font-size: 0.85rem;
}

/* --- Buttons ------------------------------------------------------------ */

.button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: 10px 18px;
    border: 1px solid var(--color-border);
    border-radius: 0;
    appearance: none;
    font: inherit;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 0.63rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    line-height: 1.2;
    cursor: pointer;
    background: transparent;
    color: var(--color-text);
    text-decoration: none;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease, opacity 120ms ease;
}
.button:hover { background: var(--color-surface-muted); }
.button:disabled { opacity: 0.55; cursor: progress; }

.button--primary { background: var(--color-lime); color: var(--color-ink-dark); border-color: var(--color-lime); }
.button--primary:hover { background: var(--color-lime-strong); border-color: var(--color-lime-strong); }

.button--secondary { background: transparent; border-color: var(--color-border); color: var(--color-text); }
.button--secondary:hover { background: var(--color-surface-muted); }

.button--danger { background: transparent; color: var(--color-danger); border-color: var(--color-danger); }
.button--danger:hover { background: var(--color-danger); color: var(--color-bg); }

.button--ghost { background: transparent; color: var(--color-text-muted); border-color: var(--color-border); padding: 8px 14px; }
.button--ghost:hover { background: var(--color-surface-muted); color: var(--color-text); }

.button--block { width: 100%; }

/* The handoff's full-width call to action: lime, left-aligned, with an arrow
   the markup does not have to carry. */
.button--cta {
    width: 100%;
    justify-content: flex-start;
    padding: 17px 24px;
    font-size: 0.75rem;
    letter-spacing: 0.16em;
}
.button--cta::after { content: "\00a0\2192"; }

.button--chip {
    padding: 4px 9px;
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    font-variant-numeric: tabular-nums;
}
.button--chip-add:hover { background: var(--color-success-soft); color: var(--color-success); }
.button--chip-remove:hover { background: var(--color-danger-soft); color: var(--color-danger); }

/* A row of stacked text buttons under a form, divided by hairlines. */
.link-stack {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: 26px;
}
.link-stack .button {
    justify-content: flex-start;
    border: 0;
    border-top: 1px solid var(--color-border);
    padding: 11px 0;
    font-size: 0.69rem;
    letter-spacing: 0.1em;
    color: var(--color-text-muted);
}
.link-stack .button:hover { background: transparent; color: var(--color-text); }

/* --- Forms -------------------------------------------------------------- */

.form { display: flex; flex-direction: column; gap: var(--space-5); }

.field { display: flex; flex-direction: column; gap: 10px; }

.field__label {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-2);
    font-weight: 400;
    font-size: 0.63rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}
.field__count { font-size: 0.63rem; letter-spacing: 0.18em; }
.field__hint { color: var(--color-text-muted); font-size: 0.75rem; line-height: 1.7; }
.field__error { color: var(--color-danger); font-size: 0.75rem; min-height: 1.2em; }

.input {
    width: 100%;
    padding: 8px 0 12px;
    font: inherit;
    font-family: var(--font-mono);
    font-size: 1rem;
    color: var(--color-text);
    background: transparent;
    border: 0;
    border-bottom: 1px solid var(--color-border);
    border-radius: 0;
}
.input:focus { border-color: var(--color-text); outline: none; }
.input--textarea { resize: vertical; min-height: 6rem; }
.input--select {
    appearance: none;
    cursor: pointer;
    /* Room for the caret, drawn as a background chevron so a bare <select>
       still reads as one without a wrapper element. */
    padding-right: 22px;
    background-image:
        linear-gradient(45deg, transparent 50%, currentColor 50%),
        linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: right 7px top 50%, right 2px top 50%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

/* The one big serif field a screen is built around — a team's name. */
.input--display {
    font-family: var(--font-serif);
    font-size: clamp(1.75rem, 4vw, 2.375rem);
    border-bottom-color: var(--color-text);
    padding: 8px 0 14px;
}

.field--invalid .input { border-color: var(--color-danger); }

.form__error {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-danger);
    color: var(--color-danger);
    font-size: 0.8rem;
}
.form__success {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-success);
    color: var(--color-success);
    font-size: 0.8rem;
}

/* --- Bib numbers -------------------------------------------------------- */

/*
 * Drawn here rather than in a page stylesheet because three places draw them:
 * the registration card, the counting row and the team hub. A chip is a ruled
 * numeral in the ledger's serif, square like everything else here — a runner's
 * bib is a number on a rectangle of paper, and this is the same object.
 */
.bibs {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
}

.bibs__list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    list-style: none;
    margin: 0;
    padding: 0;
}

.bib-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 4px 3px 10px;
    border: 1px solid var(--color-text);
    line-height: 1;
}
/* No remove button to make room for. */
.bib-chip--plain { padding-right: 10px; }

.bib-chip__number {
    font-family: var(--font-serif);
    font-size: 1.25rem;
    font-variant-numeric: tabular-nums;
}

.bib-chip__remove {
    border: 0;
    background: none;
    padding: 0 4px;
    font-family: var(--font-mono);
    font-size: 0.9rem;
    line-height: 1;
    color: var(--color-text-muted);
    cursor: pointer;
}
.bib-chip__remove:hover { color: var(--color-danger); }
.bib-chip__remove:disabled { opacity: 0.4; cursor: default; }

.bibs__empty {
    margin: 0;
    font-size: 0.69rem;
    color: var(--color-text-muted);
}

.bibs__form {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}
.bibs__input {
    width: 6.5rem;
    font-family: var(--font-serif);
    font-size: 1.0625rem;
    padding: 3px 0 6px;
}
.bibs__message {
    flex-basis: 100%;
    margin: 0;
    font-size: 0.69rem;
    color: var(--color-text-muted);
}
.bibs__message:empty { display: none; }
.bibs__message--error { color: var(--color-danger); }

/* --- Cards and credentials ---------------------------------------------- */

/* A bordered block. The design uses these sparingly — most things are ruled
   rows rather than boxes — so this stays plain. */
.card {
    background: transparent;
    border: 1px solid var(--color-border);
    padding: 28px;
}

/* A team's hub link and password. Shown once at registration and once more
   if a House Captain issues a new one; never readable anywhere else. */
.credentials {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    border: 1px solid var(--color-text);
}

.credentials__title { margin: 0; font-family: var(--font-serif); font-size: 1.5rem; }

.credentials__intro,
.credentials__warning,
.credentials__emailed {
    margin: 0;
    font-size: 0.78rem;
    line-height: 1.7;
    color: var(--color-text-muted);
}

/* Empty until a registration succeeds, so it must not leave a gap before then. */
.credentials__emailed:empty { display: none; }

.credentials__item {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--space-2) var(--space-3);
}

.credentials__label {
    grid-column: 1 / -1;
    font-size: 0.63rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.credentials__value {
    font-family: var(--font-mono);
    font-size: 0.9rem;
    padding: var(--space-2) var(--space-3);
    background: var(--color-surface-muted);
    /* A hub link is long and a password must never be silently truncated. */
    overflow-wrap: anywhere;
    user-select: all;
}

.credentials__copy { white-space: nowrap; }

/* In an admin card, where it sits inside a message area rather than a panel. */
.credentials--inline { margin-top: var(--space-2); font-size: 0.85rem; }

@media (max-width: 480px) {
    .credentials__item { grid-template-columns: 1fr; }
    .credentials__copy { width: 100%; }
}

/* --- Panel headings ----------------------------------------------------- */

.panel-heading {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: var(--space-4);
}
.panel-heading__title { font-size: clamp(1.5rem, 3vw, 2.125rem); }
.panel-heading__note {
    flex-basis: 100%;
    color: var(--color-text-muted);
    font-size: 0.75rem;
    line-height: 1.8;
    max-width: 62ch;
}

/* --- Roster editor ------------------------------------------------------ */

/*
 * Shared by the registration form and the team hub. Each person is a ruled
 * row numbered 01, 02, 03 — the handoff's runner list — with the name as a
 * serif field rather than a boxed input.
 */

.roster { display: flex; flex-direction: column; }

.roster__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
}

.roster__row {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 20px;
    padding: 16px 0;
    border-top: 1px solid var(--color-border);
}

/* 01, 02, 03 — set in the rule colour so the names stay the loudest thing. */
.roster__index {
    font-family: var(--font-serif);
    font-size: 1.625rem;
    line-height: 1;
    color: var(--color-border);
    font-variant-numeric: tabular-nums;
}

.roster__field { min-width: 0; }
.roster__field--grade { width: 7rem; }

/* Capped rather than filling the column: at full width the two halves of a
   name drift so far apart they stop reading as one person's name. */
.roster__field--name {
    display: flex;
    gap: var(--space-3);
    min-width: 0;
    max-width: 28rem;
}
.roster__field--name .input {
    flex: 1;
    min-width: 0;
    font-family: var(--font-serif);
    font-size: 1.5rem;
    padding: 4px 0 6px;
    border-bottom-color: transparent;
}
.roster__field--name .input:focus { border-bottom-color: var(--color-text); }
.roster__field--name .input::placeholder { color: var(--color-border); }

.roster__field--grade .input {
    font-size: 0.69rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    border-bottom-color: transparent;
    color: var(--color-text-muted);
}
.roster__field--grade .input:focus { border-bottom-color: var(--color-text); }

.roster__row--invalid .input { border-bottom-color: var(--color-danger); }

.roster__error {
    grid-column: 1 / -1;
    margin: 0;
    font-size: 0.75rem;
    color: var(--color-danger);
}
.roster__error:empty { display: none; }

.roster__row--withdrawn {
    display: block;
    padding: 16px 0;
}

.roster__withdrawn {
    margin: 0;
    font-size: 0.78rem;
    color: var(--color-text-muted);
}

.roster__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-text);
}

.roster__count {
    margin: 0;
    font-size: 0.63rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}

@media (max-width: 640px) {
    .roster__row {
        grid-template-columns: 30px minmax(0, 1fr) auto;
        gap: 6px 14px;
    }
    .roster__field--name { grid-column: 2 / -1; flex-direction: column; gap: var(--space-2); }
    .roster__field--grade { grid-column: 2; width: auto; }
    .roster__index { font-size: 1.25rem; }
    .roster__add { width: 100%; }
    .roster__footer { flex-direction: column-reverse; align-items: stretch; gap: var(--space-3); }
    .roster__count { text-align: center; }
}

/* --- Badges and tables -------------------------------------------------- */

.badge {
    display: inline-flex;
    align-items: center;
    padding: 4px 10px;
    border: 1px solid currentColor;
    font-size: 0.63rem;
    font-weight: 400;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    white-space: nowrap;
}
.badge--pending { color: var(--color-warning); }
.badge--approved { color: var(--color-success); }
.badge--rejected { color: var(--color-danger); }
.badge--archived { color: var(--color-text-muted); }

.table-scroll {
    overflow-x: auto;
    min-width: 0;
}

table { border-collapse: collapse; width: 100%; font-size: 0.78rem; }
th, td { padding: 15px var(--space-3) 15px 0; text-align: left; vertical-align: baseline; }
thead th {
    padding-top: 0;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--color-text);
    font-size: 0.56rem;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: var(--color-text-muted);
    white-space: nowrap;
    font-weight: 400;
}
tbody td { border-bottom: 1px solid var(--color-border); }

/* ---------------------------------------------------------------------------
 * A team's colours
 *
 * Every team has a hue and a pattern — chosen in its hub, or derived from its
 * name by js/utils/team-identity.js when it has chosen nothing. Two pages draw
 * them: the leaderboard's cards and rows, and the picker in the team hub, so
 * the drawings live here rather than in either page's stylesheet.
 *
 * Two classes and one attribute:
 *   .team-tinted   carries --team-hue and paints the tinted background
 *   .team-pattern  an absolutely positioned layer inside it
 *   [data-pattern] on an ancestor of .team-pattern, says which drawing
 * ------------------------------------------------------------------------ */

/*
 * The ink and the band are both derived from the team's hue and both sit on a
 * background derived from the same hue, so their contrast has to hold for
 * every hue the app can assign — not just the one that happens to be on
 * screen. HSL lightness is not perceptual: hsl(60 55% 30%) is far brighter
 * than hsl(240 55% 30%), and at the values these started from the yellow,
 * green and cyan teams failed AA outright (ink as low as 4.06:1, band as low
 * as 1.81:1 against the darker end of the card gradient).
 *
 * 28% and 36% are the lightest each can be while the worst of the twelve hues
 * in js/utils/team-identity.js still clears its threshold — 4.5:1 for the ink,
 * which sets the small roster line, and 3:1 for the band, which is only ever
 * used at large sizes or as a graphical edge.
 */
.team-tinted {
    --tint-top: hsl(var(--team-hue, 100) 62% 97%);
    --tint-bottom: hsl(var(--team-hue, 100) 48% 88%);
    --team-ink: hsl(var(--team-hue, 100) 55% 28%);
    --team-band: hsl(var(--team-hue, 100) 50% 36%);

    background: linear-gradient(160deg, var(--tint-top), var(--tint-bottom));
}

/*
 * The pattern. One layer, behind everything, at an alpha low enough that a
 * team name never has to compete with it. Which pattern a team gets is in
 * js/utils/team-identity.js; the five are drawn here.
 */
.team-pattern {
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 0.32;
}

[data-pattern="stripes"] .team-pattern {
    background-image: repeating-linear-gradient(
        135deg,
        var(--team-ink) 0 1px,
        transparent 1px 11px
    );
}

[data-pattern="dots"] .team-pattern {
    background-image: radial-gradient(var(--team-ink) 1px, transparent 1.4px);
    background-size: 14px 14px;
}

/*
 * Actual chevrons, not a crosshatch: four quarter-tile diagonals, the top pair
 * shifted half a tile across, which is what turns two sets of diagonals into
 * rows of Vs. Solid fills rather than line work, so this one is drawn much
 * fainter than the others.
 */
[data-pattern="chevrons"] .team-pattern {
    opacity: 0.16;
    background-size: 20px 20px;
    background-position: -10px 0, -10px 0, 0 0, 0 0;
    background-image:
        linear-gradient(135deg, var(--team-ink) 25%, transparent 25%),
        linear-gradient(225deg, var(--team-ink) 25%, transparent 25%),
        linear-gradient(315deg, var(--team-ink) 25%, transparent 25%),
        linear-gradient(45deg, var(--team-ink) 25%, transparent 25%);
}

[data-pattern="grid"] .team-pattern {
    background-image:
        repeating-linear-gradient(0deg, var(--team-ink) 0 1px, transparent 1px 16px),
        repeating-linear-gradient(90deg, var(--team-ink) 0 1px, transparent 1px 16px);
}

/*
 * Camo, in the spirit of the Australian Army's AMCU.
 *
 * Unlike the other four this is filled shapes rather than line work, so it is
 * drawn from the team's hue shifted a little either way rather than from one
 * ink. What stops it reading as polka dots is that the blotches come in
 * clusters: three or four ellipses of the same tone, overlapping, merge into
 * one irregular mass with no circle left in it. The tile is wider than a card
 * so the repeat never appears twice on the same card, and the three body
 * tones are painted light over mid over dark, which is the order that reads
 * as layered cloth rather than as a pile of shapes. A fourth, near-black tone
 * is scattered as small flecks on top — the fine speckle a MultiCam-style
 * print has that a smooth three-tone blotch pattern does not — and the
 * transitions between tones are cut harder (82%/84% rather than 74%/76%) so
 * edges read as printed rather than airbrushed. A blotch that crosses the
 * edge of the tile is emitted a second time one tile over, so it runs across
 * the seam instead of being cut off — a straight edge is the one thing
 * camouflage cannot have. Four gradient tones plus four fleck layers is still
 * thirty layers total, the same budget as before.
 */
[data-pattern="camo"] {
    --camo-a: hsl(var(--team-hue, 100) 22% 58%);
    --camo-b: hsl(calc(var(--team-hue, 100) + 30) 28% 32%);
    --camo-c: hsl(calc(var(--team-hue, 100) - 18) 20% 82%);
    --camo-d: hsl(calc(var(--team-hue, 100) - 8) 18% 14%);
}

/*
 * 0.5, which is what the handoff draws camo at — and, unlike the other four,
 * it is filled shapes rather than line work, so at the 0.85 it used to carry
 * the darkest blotches swallowed the team name sitting over them.
 */
[data-pattern="camo"] .team-pattern {
    opacity: 0.5;
    background-size: 260px 210px;
    background-image:
        radial-gradient(ellipse 58px 25px at 87.7% 28.2%, var(--camo-c) 0 82%, transparent 84%),
        radial-gradient(ellipse 58px 25px at -12.3% 28.2%, var(--camo-c) 0 82%, transparent 84%),
        radial-gradient(ellipse 41px 30px at 82.7% 36.6%, var(--camo-c) 0 82%, transparent 84%),
        radial-gradient(ellipse 53px 31px at 26.6% 57.0%, var(--camo-c) 0 82%, transparent 84%),
        radial-gradient(ellipse 52px 31px at 40.0% 50.6%, var(--camo-c) 0 82%, transparent 84%),
        radial-gradient(ellipse 55px 35px at 57.3% 83.2%, var(--camo-c) 0 82%, transparent 84%),
        radial-gradient(ellipse 39px 32px at 55.6% 87.0%, var(--camo-c) 0 82%, transparent 84%),
        radial-gradient(ellipse 39px 32px at 55.6% -13.0%, var(--camo-c) 0 82%, transparent 84%),
        radial-gradient(ellipse 51px 25px at 13.2% 17.7%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 51px 25px at 113.2% 17.7%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 39px 30px at 16.6% 21.6%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 39px 25px at 58.6% 58.8%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 40px 27px at 64.8% 65.9%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 50px 29px at 94.0% 18.1%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 50px 29px at -6.0% 18.1%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 55px 27px at 99.6% 1.8%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 55px 27px at -0.4% 1.8%, var(--camo-a) 0 82%, transparent 84%),
        radial-gradient(ellipse 44px 34px at 38.3% 9.1%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 44px 34px at 38.3% 109.1%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 51px 28px at 38.9% 17.5%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 39px 26px at 10.8% 68.1%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 39px 26px at 110.8% 68.1%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 44px 31px at 12.9% 74.7%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 44px 31px at 112.9% 74.7%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 54px 32px at 79.3% 84.4%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 49px 35px at 75.9% 89.3%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(ellipse 49px 35px at 75.9% -10.7%, var(--camo-b) 0 82%, transparent 84%),
        radial-gradient(circle 9px at 30.0% 30.0%, var(--camo-d) 0 88%, transparent 90%),
        radial-gradient(circle 7px at 70.0% 45.0%, var(--camo-d) 0 88%, transparent 90%),
        radial-gradient(circle 8px at 47.0% 72.0%, var(--camo-d) 0 88%, transparent 90%),
        radial-gradient(circle 6px at 20.0% 82.0%, var(--camo-d) 0 88%, transparent 90%);
}

/* ---------------------------------------------------------------------------
 * Motion
 *
 * The handoff's own motion, minus the scroll-linked parts: a rise on render
 * and a pulsing live dot. Applied directly rather than on an
 * IntersectionObserver, because the leaderboard re-renders in full on every
 * realtime score change and a scroll-triggered reveal would either re-fire on
 * each update or never fire again after the first. All of it collapses under
 * prefers-reduced-motion.
 * ------------------------------------------------------------------------ */
@keyframes om-rise {
    from { opacity: 0; transform: translateY(22px); }
    to { opacity: 1; transform: none; }
}
@keyframes om-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.3; }
}
@keyframes om-bar-grow {
    from { transform: scaleX(0); }
    to { transform: scaleX(1); }
}

.om-rise { animation: om-rise 620ms cubic-bezier(.16, 1, .3, 1) both; }
.om-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: currentColor;
    animation: om-pulse 2s ease-in-out infinite;
}

.page-hero, .page-heading__title { animation: om-rise 620ms cubic-bezier(.16, 1, .3, 1) both; }
.page-lede { animation: om-rise 620ms cubic-bezier(.16, 1, .3, 1) 90ms both; }

@media (prefers-reduced-motion: reduce) {
    .om-rise, .page-hero, .page-heading__title, .page-lede { animation: none; }
    .om-dot { animation: none; }
}
