/* ===== Inter =====
   Self-hosted, not loaded from Google. A webfont from a CDN means every
   parent's browser making an outbound request carrying their IP, on every
   page load, from a private site about children. One 47KB file, served from
   our own domain, cached after the first page.

   The variable version, so 400 through 700 come out of a single request
   rather than four. SIL Open Font Licence, which is what makes self-hosting
   it legal.

   font-display: swap so text is readable in the system font immediately and
   reflows when Inter arrives, rather than a blank page while it loads. */
@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url("/assets/fonts/inter-latin.woff2") format("woff2");
    /* Latin basic only: everything this site displays is inside this range,
       and the full set is several times the size. */
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
        U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ===== Design tokens =====
   The fixed parameter set. Rules: docs/design-parameters.md.
   A raw hex, a raw px space or a raw radius anywhere below this block is a bug.

   Light and dark are declared together with light-dark(), so each value exists
   exactly once and the two themes cannot drift apart. Every such line is
   preceded by a plain fallback for browsers without light-dark() support. */
:root {
    /* Light by default, dark when the OS says so. The toggle overrides both,
       via [data-theme] at the bottom of this block. */
    color-scheme: light dark;

    /* ----- Brand. Fixed in both themes: the header, footer and primary button
       stay a dark brand surface at night, so the site keeps its identity. ----- */
    --color-navy: #0b1f3a;
    --color-navy-light: #14305c;
    --color-navy-dark: #071426;

    /* Navy is used two ways, and only one of them survives a dark background.
       As large chrome — header, nav, footer — it stays navy in both themes and
       reads fine at that scale. As an accent ON the page it does not: navy on
       the dark page background is near-invisible, which lost the primary
       button, the timeline dots and every accent border. So the accent roles
       get their own tokens that lift in dark.

       Surface = something with white text on it, so it must stay dark enough. */
    --color-accent-surface: #0b1f3a;
    --color-accent-surface: light-dark(#0b1f3a, #1d4ed8);
    --color-accent-surface-hover: #14305c;
    --color-accent-surface-hover: light-dark(#14305c, #2563eb);
    /* Marks — dots, rules, bullets — carry no text, so they go brighter still.
       --color-link is already exactly this colour in both themes. */

    /* ----- On-dark. Text and lines sitting on a dark surface — the navy
       header and button, the violet grant chip, the red danger button. Fixed
       in both themes, because the surface underneath is fixed. ----- */
    --color-on-dark: #ffffff;
    --color-on-dark-muted: #cbd5e1;
    --color-on-dark-hover: rgba(255, 255, 255, 0.06);
    --color-on-dark-active: rgba(255, 255, 255, 0.12);
    --color-on-dark-line: rgba(255, 255, 255, 0.35);
    --color-on-dark-line-strong: rgba(255, 255, 255, 0.6);
    --color-on-dark-line-soft: rgba(255, 255, 255, 0.25);

    /* ----- Neutrals. These flip. ----- */
    --color-text: #1a1a1a;
    --color-text: light-dark(#1a1a1a, #e6e9ed);
    --color-text-muted: #5b6472;
    --color-text-muted: light-dark(#5b6472, #9aa5b1);
    --color-bg: #fafafa;
    --color-bg: light-dark(#fafafa, #0f151c);
    --color-surface: #ffffff;
    --color-surface: light-dark(#ffffff, #18202a);
    --color-border: #dde1e6;
    --color-border: light-dark(#dde1e6, #2b3642);

    /* Separate from --color-navy: that stays a dark brand surface in both
       themes, while these have to flip to stay readable. */
    --color-heading: #0b1f3a;
    --color-heading: light-dark(#0b1f3a, #cfe0f5);
    --color-link: #0b1f3a;
    --color-link: light-dark(#0b1f3a, #8ab4f8);
    /* The glow behind a focused control. Same hue as the link, low alpha. */
    --color-focus-ring: rgba(11, 31, 58, 0.16);
    --color-focus-ring: light-dark(rgba(11, 31, 58, 0.16), rgba(138, 180, 248, 0.3));
    /* The fill behind anything currently chosen. Lighter than the surface in
       both themes -- picked should read as lit up, not as pressed in. */
    --color-selected: #eaf0fa;
    --color-selected: light-dark(#eaf0fa, #1e3253);

    /* ----- Chat. Laid out the way WhatsApp is, because that is what every
       parent already knows how to read -- but in the club's own colours, so
       it is the club's app and not a copy. The wallpaper sits a step back
       from the page; your bubbles are a soft club blue, theirs the plain
       surface; read ticks go blue, as they do everywhere else. ----- */
    --color-chat-bg: #e9edf2;
    --color-chat-bg: light-dark(#e9edf2, #0b1117);
    --color-bubble-out: #d6e4fa;
    --color-bubble-out: light-dark(#d6e4fa, #1b365e);
    --color-bubble-in: #ffffff;
    --color-bubble-in: light-dark(#ffffff, #1c252f);
    --color-tick-read: #1d74d8;
    --color-tick-read: light-dark(#1d74d8, #53bdeb);
    /* The unread count: a status, so it stays one bright colour in both. */
    --color-unread: #1d74d8;

    /* ----- Status ----- */
    --color-error-text: #b91c1c;
    --color-error-text: light-dark(#b91c1c, #fca5a5);
    --color-error-bg: #fee2e2;
    --color-error-bg: light-dark(#fee2e2, #3f1d1d);
    --color-error-border: #fca5a5;
    --color-error-border: light-dark(#fca5a5, #7f1d1d);
    /* Deliberately NOT --color-error-text: that flips pale in dark, which
       put white text on pale pink on the one button that deletes things. */
    --color-danger-surface: #b91c1c;
    --color-danger-surface-hover: #991b1b;
    --color-success-text: #166534;
    --color-success-text: light-dark(#166534, #86efac);
    --color-success-bg: #dcfce7;
    --color-success-bg: light-dark(#dcfce7, #14321f);
    --color-success-border: #86efac;
    --color-success-border: light-dark(#86efac, #166534);

    /* ----- Warning. The third state, between success and error.
       "Expires next month" is not a fault and not fine, and forcing it into
       red or green loses the only distinction that makes a matrix worth
       reading -- what needs booking versus what needs chasing. ----- */
    --color-warning-text: #92400e;
    --color-warning-text: light-dark(#92400e, #fcd34d);
    --color-warning-bg: #fef3c7;
    --color-warning-bg: light-dark(#fef3c7, #3a2c0a);
    --color-warning-border: #fcd34d;
    --color-warning-border: light-dark(#fcd34d, #92400e);

    /* ----- Role. Reinforcement only — the role is always spelled out in
       words as well, so colour never carries meaning on its own. ----- */
    --color-role-volunteer: #4ade80;
    --color-role-manager: #60a5fa;
    --color-role-developer: #fbbf24;
    --color-role-grant: #7c3aed;

    /* ----- Doncaster Knights brand -----
       Navy and red, sampled off the Knight of the Match artwork rather than
       guessed, so the site and the thing in a child's hand match. Fixed in
       both themes: brand colour is not a preference.

       The cream and the brown in that artwork are deliberately NOT here --
       they are the wood the award is printed on, not club colours. ----- */
    --brand-navy: #001b36;
    --brand-red: #d72a27;

    /* ----- Squad kit. A blue squad is blue in both themes. ----- */
    --color-squad-blue: #2563eb;
    --color-squad-red: #dc2626;
    /* The third option, for a Sunday with only enough players for one side.
       Not a fourth team -- it is the whole age group out as Doncaster
       Knights -- so it wears the club colour rather than a kit colour.
       --color-accent-surface, not raw navy: navy on a page background is
       against the contract, and this token lifts in dark. */
    --color-squad-knights: var(--color-accent-surface);

    /* ----- Type -----
       Inter, self-hosted, with the system stack behind it as a fallback. Two
       roles, one family: --font-display exists so headings, scores and figures
       can be changed in one place, and points at the same face -- they are
       told apart by weight, size and tracking.

       A condensed display stack was tried here and taken out again: it reads
       as squashed, and on Windows it resolved to Arial Narrow, a utility face
       rather than a premium one. */
    --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
    /* NOT condensed. A condensed stack was tried and reads as squashed --
       Arial Narrow, which is what it resolves to on Windows, is a utility face
       rather than a premium one. Headings are distinguished by weight and
       tracking instead, which is what the system faces are actually good at.
       A real premium typeface has to be self-hosted; until then this is the
       honest option rather than a narrow substitute. */
    --font-display: var(--font-sans);

    --text-xs: 0.6875rem;
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.5rem;
    --text-2xl: 2rem;
    --text-3xl: 2.5rem;
    /* h1 only, under 640px. The one size that changes by breakpoint. */
    --text-h1-phone: 1.625rem;

    /* ----- Space. 4px base. Nothing off this scale. ----- */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-8: 3rem;

    /* ----- Radius. The bigger the surface, the bigger the radius. ----- */
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 14px;
    --radius-pill: 999px;
    --radius-circle: 50%;

    /* ----- Elevation. Borders do the structural work in dark, shadows in
       light — so the tint gets heavier when the surface contrast drops. ----- */
    --shadow-tint: rgba(11, 31, 58, 0.08);
    --shadow-tint: light-dark(rgba(11, 31, 58, 0.08), rgba(0, 0, 0, 0.4));
    --shadow-tint-strong: rgba(11, 31, 58, 0.1);
    --shadow-tint-strong: light-dark(rgba(11, 31, 58, 0.1), rgba(0, 0, 0, 0.5));
    --shadow-sm:
        0 1px 2px var(--shadow-tint),
        0 1px 3px -1px var(--shadow-tint);
    --shadow-md:
        0 4px 6px -1px var(--shadow-tint-strong),
        0 2px 4px -2px var(--shadow-tint);
    --shadow-lift:
        0 10px 15px -3px var(--shadow-tint-strong),
        0 4px 6px -4px var(--shadow-tint);

    /* ----- Motion. State is 150ms, movement is 250ms, nothing is longer. ----- */
    --motion-state: 150ms ease-out;
    --motion-move: 250ms ease-out;

    /* ----- Layout ----- */
    --max-width: 1000px;
    --max-width-narrow: 420px;

    /* A rail item never fills the screen: the next one has to peek in from
       the right, because that peek is the only affordance saying "flick me". */
    --rail-item: min(78vw, 300px);
}

/* The toggle wins over the OS. Both themes resolve from the one token block
   above — there is no second copy of the palette to keep in sync. */
:root[data-theme="light"] {
    color-scheme: only light;
}

:root[data-theme="dark"] {
    color-scheme: only dark;
}

/* ===== Base ===== */
* {
    box-sizing: border-box;
}

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: 1.5;
    color: var(--color-text);
    background: var(--color-bg);
    /* Sharper stems on Mac and iOS, where the default is noticeably heavy. */
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* Type gets tighter as it gets bigger. Headings left at default tracking are
   the clearest tell that nobody set the type.

   Headings carry the condensed display face; body text stays on the system
   sans, which is the more readable of the two at paragraph sizes. */
h1, h2, h3 {
    font-family: var(--font-display);
    line-height: 1.15;
    color: var(--color-heading);
    /* Stops a two-word orphan on the second line of a wrapped heading. */
    text-wrap: balance;
}

/* Space belongs to the heading that owns it: a lot above, little below, so a
   heading sits with its content rather than floating between two blocks.
   Without this they used browser defaults, which are off the spacing scale. */
h1, h2, h3 {
    margin: 0 0 var(--space-3);
}

h2, h3 {
    margin-top: var(--space-6);
}

/* First thing in a card or a section owns no space above it. */
.card > :first-child,
main > :first-child {
    margin-top: 0;
}

h1 {
    font-size: var(--text-2xl);
    font-weight: 700;
    letter-spacing: -0.022em;
}

h2 {
    font-size: var(--text-xl);
    font-weight: 700;
    letter-spacing: -0.016em;
}

h3 {
    font-size: var(--text-lg);
    letter-spacing: -0.01em;
}

p {
    margin: 0 0 var(--space-4);
}

a {
    color: var(--color-link);
}

/* Visible on keyboard focus, never removed without a replacement. Offset so
   it reads as a ring around the control rather than a border on it. */
:focus-visible {
    outline: 2px solid var(--color-link);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* Non-negotiable: someone who has asked the OS to stop animations gets none. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

.container {
    max-width: var(--max-width);
    margin: 0 auto;
    padding: 0 var(--space-5);
}

main {
    min-height: 60vh;
    padding: var(--space-6) var(--space-5) var(--space-8);
}

/* ===== Header =====
   Two tiers: identity and account on top, site navigation below. Keeping
   "who am I" separate from "where am I going" stops the row becoming a
   jumble once the name, role and log out are all competing for space. */
.site-header {
    background: var(--color-navy);
    color: var(--color-on-dark);
    /* Coloured when signed in with elevated access — see .role-* below. */
    border-top: 5px solid transparent;
}

.header-top .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
    padding-top: var(--space-4);
    padding-bottom: var(--space-4);
}

.logo {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    color: var(--color-on-dark);
    font-weight: bold;
    text-decoration: none;
    font-size: var(--text-lg);
    letter-spacing: -0.01em;
    font-family: var(--font-display);
    letter-spacing: 0.01em;
}

/* ----- Account cluster -----
   Four groups in a fixed order, left to right: who you are, what you are
   previewing as, how it looks, leaving. Identity first because it never
   changes; log out last because it is the one with consequences.

   On a developer account this row carries a name, a role, any number of
   grants, three preview links, a theme toggle and a log out. As eight loose
   items it read as random, so the groups are separated and only the gaps
   between groups are wide. */
.account {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    font-size: var(--text-sm);
}

/* 1. Who you are. */
.account-identity {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* 3 and 4. Preference, then exit. */
.account-controls {
    display: inline-flex;
    align-items: center;
    gap: var(--space-4);
}

/* A rule between "View as Parent | Volunteer | Manager" and the session
   controls beside them. Both sets are drawn as pills by the same rule, so
   without a divider Diagnostics read as a fourth role you could preview.

   Only when it actually follows the picker: a volunteer has no picker, and a
   border hanging off the left of the first thing in the row would be a line to
   nowhere. */
.view-as-picker + .account-controls {
    padding-left: var(--space-4);
    border-left: 1px solid var(--color-on-dark-line);
}

/* Reads as one control rather than three loose links. */
.view-as-label {
    color: var(--color-on-dark-muted);
    opacity: 0.8;
}

.account a.account-name {
    color: var(--color-on-dark-muted);
    text-decoration: none;
}

.account a.account-name:hover {
    color: var(--color-on-dark);
    text-decoration: underline;
}

.account a {
    color: var(--color-on-dark);
    text-decoration: none;
}

.account a:hover {
    text-decoration: underline;
}

/* Colour is reinforcement only — the role is always spelled out, so this
   still reads correctly without colour vision. */
.role-chip {
    display: inline-block;
    border-radius: var(--radius-pill);
    padding: 1px var(--space-2);
    color: var(--color-navy-dark);
    background: var(--color-on-dark-muted);
}

.role-chip--volunteer {
    background: var(--color-role-volunteer);
}

.role-chip--manager {
    background: var(--color-role-manager);
}

.role-chip--developer {
    background: var(--color-role-developer);
}

.role-chip--grant {
    background: var(--color-role-grant);
    color: var(--color-on-dark);
}

body.role-volunteer .site-header {
    border-top-color: var(--color-role-volunteer);
}

body.role-manager .site-header {
    border-top-color: var(--color-role-manager);
}

body.role-developer .site-header {
    border-top-color: var(--color-role-developer);
}

/* ----- Navigation tier ----- */
.header-nav {
    background: var(--color-navy-dark);
    position: sticky;
    top: 0;
    z-index: 20;
}

/* It used to scroll sideways, because thirteen flat links wrapped to three
   rows and ate 140px of a 375px phone before any content appeared. Grouped
   there are seven, which fit on a laptop and wrap to two rows on a phone.
   
   The scroll had to go regardless: a dropdown panel inside a horizontally
   scrolling box is clipped by it, so the menus opened invisibly. An overflow
   that hides the thing it contains is worse than a second row. */
.header-nav .container {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
}

.header-nav > .container > a,
.nav-group > summary {
    color: var(--color-on-dark-muted);
    text-decoration: none;
    font-size: var(--text-sm);
    padding: var(--space-3) var(--space-3);
    border-bottom: 3px solid transparent;
    white-space: nowrap;
}

.header-nav > .container > a:hover,
.nav-group > summary:hover,
.nav-group[open] > summary {
    color: var(--color-on-dark);
    background: var(--color-on-dark-hover);
}

.header-nav > .container > a.is-active,
.nav-group.is-active > summary {
    color: var(--color-on-dark);
    font-weight: bold;
    border-bottom-color: var(--color-on-dark);
}

/* ===== Footer ===== */

/* The footer's parts on one line, wrapping to several on a narrow screen
   without the separators stranding themselves at the start of a line. */
.footer-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    margin: 0;
}

.site-footer {
    margin-top: var(--space-6);
    background: var(--color-navy);
    color: var(--color-on-dark-muted);
    text-align: center;
    padding: var(--space-4) 0;
    font-size: var(--text-sm);
}

/* A bare link on chrome takes the chrome's own colour.
   --color-link is tuned for a light page background; on the navy header and
   footer, and on the amber viewing-as bar, it reads as a stray blue at a
   different weight to everything around it. .logo and .account-name were each
   fixed individually, which left "Log in", "Log out" and the footer's terms
   link still blue -- so this is by selector, like the button promotion below,
   rather than one more per-element patch.

   Underlined rather than recoloured: on chrome the colour is already spoken
   for, so the underline is what says it is a link. */
.site-header a:not([class]),
.site-footer a,
.viewing-as a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
    opacity: 0.85;
    transition: opacity var(--motion-state);
}

.site-header a:not([class]):hover,
.site-footer a:hover,
.viewing-as a:hover {
    opacity: 1;
}

/* ===== Buttons ===== */
/* A bare <a> sitting in an action slot -- a page head, a form's actions, a
   row's actions -- is promoted to a button by selector. Site-wide, rather than
   per page: every one of these was a plain underlined link next to a real
   button, and fixing them one file at a time leaves the next page to make the
   same mistake. Anything with its own class (.back-link, .directions, .tag) is
   left alone. */
.btn,
.page-head a:not([class]),
.form-actions a:not([class]),
.row-actions a:not([class]) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    /* The charter's 44px floor, enforced here so every button inherits it
       rather than each page remembering. */
    min-height: 44px;
    background: var(--color-accent-surface);
    color: var(--color-on-dark);
    text-decoration: none;
    padding: var(--space-2) var(--space-5);
    /* Transparent rather than none: .btn-secondary has a real 1px border, and
       without this the two variants are 2px different in every dimension --
       enough that a primary and a secondary side by side never matched. */
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    font-size: var(--text-base);
    font-weight: 600;
    font-family: inherit;
    line-height: 1.2;
    cursor: pointer;
    text-align: center;
    transition: background var(--motion-state), box-shadow var(--motion-state),
                transform var(--motion-state);
}

.btn:hover {
    background: var(--color-accent-surface-hover);
    box-shadow: var(--shadow-md);
}

/* Presses should feel like presses. 1px is enough to read as physical. */
.btn:active {
    transform: translateY(1px);
    box-shadow: var(--shadow-sm);
}

/* A quieter button for the second choice in a pair, so two primary buttons
   never sit side by side competing. */
.btn-secondary,
.page-head a:not([class]),
.form-actions a:not([class]),
.row-actions a:not([class]) {
    background: var(--color-surface);
    color: var(--color-link);
    border: 1px solid var(--color-border);
}

.btn-secondary:hover,
.page-head a:not([class]):hover,
.form-actions a:not([class]):hover,
.row-actions a:not([class]):hover {
    background: var(--color-bg);
    border-color: var(--color-link);
}

.btn-block {
    display: block;
    width: 100%;
}

/* ===== Card ===== */
.card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: var(--space-5);
    margin-bottom: var(--space-4);
}

/* Anything you can tap lifts slightly under the cursor, so a tappable card is
   told apart from a static one without needing a chevron to say so. */
.card-link,
.fixture {
    transition: box-shadow var(--motion-state), border-color var(--motion-state);
}

.card-link:hover,
.fixture:hover {
    box-shadow: var(--shadow-md);
    border-color: var(--color-link);
}

.card:last-child {
    margin-bottom: 0;
}

.card-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* ===== Alerts ===== */
/* The base carries a visible surface of its own. It used to be a transparent
   border and no background, so an .alert with no modifier rendered as indented
   text and nothing else -- a component that disappears when you use it without
   a variant is a trap, and the terms page fell in it. A neutral alert is a
   real thing to want: a note that is neither an error nor a success. */
.alert {
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
    margin-bottom: var(--space-4);
    font-size: var(--text-sm);
}

.alert-error {
    color: var(--color-error-text);
    background: var(--color-error-bg);
    border-color: var(--color-error-border);
}

.alert-success {
    color: var(--color-success-text);
    background: var(--color-success-bg);
    border-color: var(--color-success-border);
}

/* ===== Forms ===== */
.form-group {
    margin-bottom: var(--space-4);
}

.form-group label {
    display: block;
    margin-bottom: var(--space-2);
    font-weight: 600;
    font-size: var(--text-sm);
    color: var(--color-text);
}

/* One control height across inputs, selects and textareas. Fields of three
   different heights in one form is the thing that reads as unfinished. */
.form-group input,
.form-group select,
.form-group textarea {
    width: 100%;
    min-height: 44px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    /* 16px minimum, or iOS zooms the viewport when the field takes focus. */
    font-size: var(--text-base);
    font-family: inherit;
    line-height: 1.4;
    background: var(--color-surface);
    color: var(--color-text);
    transition: border-color var(--motion-state), box-shadow var(--motion-state);
}

.form-group select {
    appearance: none;
    /* Room for the chevron drawn below. */
    padding-right: var(--space-6);
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: right var(--space-4) center, right var(--space-3) center;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

.form-group textarea {
    min-height: 96px;
    padding-top: var(--space-3);
    resize: vertical;
}

/* A ring rather than a hard outline: it reads as the field lighting up, and
   it follows the field's own radius instead of boxing it. */
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
    outline: none;
    border-color: var(--color-link);
    box-shadow: 0 0 0 3px var(--color-focus-ring);
}

.form-group input::placeholder,
.form-group textarea::placeholder {
    color: var(--color-text-muted);
    opacity: 1;
}

/* A hint under a label, so pages stop inventing their own. */
.form-hint {
    margin-top: var(--space-1);
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

.form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: center;
    margin-top: var(--space-5);
}

/* ===== Horizontal rails =====
   The house pattern for a set of peers — matches, players, sessions, dates.
   Four stacked cards is four screens of scrolling on a phone; one rail is a
   flick. Rules and when NOT to use one: docs/design-parameters.md §6.

   Shared mechanics with the header nav and .subtabs, which are the precedent
   for this rather than exceptions to it. */
.rail {
    display: flex;
    gap: var(--space-3);
    overflow-x: auto;
    /* Mandatory, so a flick always lands on an item and never half-way
       between two. */
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Bleeds to the screen edge and carries its own gutter, so the first item
       lines up with the page text while the rail itself runs edge to edge.
       Stopping short of the edge reads as clipped, not as deliberate. */
    margin: 0 calc(var(--space-5) * -1);
    padding: var(--space-1) var(--space-5) var(--space-4);
    list-style: none;
}

.rail::-webkit-scrollbar {
    display: none;
}

.rail-item {
    /* Never a full screen wide: the next item has to peek in from the right,
       because that peek is the only thing saying "there is more". */
    flex: 0 0 var(--rail-item);
    scroll-snap-align: start;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: var(--space-4);
}

/* A rail of plain links or chips rather than cards — dates, filters, squads. */
.rail--chips {
    gap: var(--space-2);
    padding-bottom: var(--space-3);
}

.rail--chips .rail-item {
    flex: 0 0 auto;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-pill);
    box-shadow: none;
    /* 44px minimum tap target, set here so every chip rail gets it. */
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
}

@media (max-width: 640px) {
    .rail {
        margin: 0 calc(var(--space-4) * -1);
        padding-left: var(--space-4);
        padding-right: var(--space-4);
    }
}

/* ===== Page-specific layout ===== */
.landing {
    max-width: 640px;
    margin: 0 auto;
    text-align: center;
    padding: var(--space-6) 0;
}

.landing-intro {
    font-size: var(--text-lg);
    color: var(--color-text-muted);
}

.auth-card {
    max-width: var(--max-width-narrow);
    margin: 0 auto;
}

.text-muted {
    color: var(--color-text-muted);
}

/* ===== Phones =====
   Tighter gutters and padding: 24px each side of a 375px screen is a lot of
   nothing when someone is reading a fixture list one-handed. */
@media (max-width: 640px) {
    /* The preview picker is the least-used group here, so it takes its own
       row rather than squeezing the name or pushing log out off the edge. */
    .view-as-picker {
        order: 1;
        flex-basis: 100%;
        justify-content: flex-start;
    }

    .container {
        padding: 0 var(--space-4);
    }

    main {
        padding: var(--space-5) var(--space-4) var(--space-6);
    }

    .card {
        padding: var(--space-4);
    }

    h1 {
        font-size: var(--text-h1-phone);
    }

    h2 {
        font-size: var(--text-lg);
    }

    /* Buttons go full width so they are easy to hit with a thumb. */
    .form-actions .btn {
        display: block;
        width: 100%;
    }

    .page-head {
        gap: var(--space-3);
    }
}

/* ===== Back link =====
   Every page that goes somewhere had its own bare underlined <a> — "Back to
   fixtures", "Back to squad", "Return home" — sitting loose above or below
   the content at a different place each time. One component, one position:
   above the page title, with the arrow doing the work so the text does not
   need underlining to read as a link. */
.back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    margin-bottom: var(--space-2);
    color: var(--color-text-muted);
    text-decoration: none;
    font-size: var(--text-sm);
    font-weight: 600;
    transition: color var(--motion-state);
}

.back-link::before {
    content: '←';
    font-size: var(--text-lg);
    line-height: 1;
}

.back-link:hover {
    color: var(--color-link);
}

/* ===== Page head ===== */
.page-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-3);
    /* A hairline under every page title, so each page opens the same way
       instead of the heading floating at a different distance each time. */
    border-bottom: 1px solid var(--color-border);
}

.page-head h1 {
    margin: 0;
}

/* The standing subtitle under a page title. */
.page-intro {
    margin-top: calc(var(--space-5) * -1 + var(--space-2));
    margin-bottom: var(--space-5);
    color: var(--color-text-muted);
    max-width: 60ch;
}

/* ===== A long-form document =====
   Terms, privacy, anything that is read rather than used. The whole column is
   held to a readable measure instead of only the lead paragraph, which is what
   made the terms page look ragged: a 60ch intro above cards running the full
   width of a laptop. */
.doc {
    max-width: 72ch;
}

.doc .page-intro {
    /* Already inside the measure, so the paragraph's own limit would narrow it
       twice. */
    max-width: none;
}

.doc h2 {
    margin-top: var(--space-6);
}

.doc .card > :first-child {
    margin-top: 0;
}

.doc .card > :last-child {
    margin-bottom: 0;
}

/* ===== Tags ===== */
.tag {
    display: inline-block;
    background: var(--color-accent-surface);
    color: var(--color-on-dark);
    border-radius: var(--radius-sm);
    padding: 0 var(--space-2);
    font-size: var(--text-sm);
}

.tag-warn {
    background: var(--color-error-text);
}

/* Same squad colours as the team-picker's .squad-option--blue/--red, reused
   rather than invented so a squad reads the same colour everywhere it appears. */
.tag-blue {
    background: var(--color-squad-blue);
}

.tag-red {
    background: var(--color-squad-red);
}

.tag-knights {
    background: var(--color-squad-knights);
}

/* ===== Selectable controls =====
   Squad options, try-scorer picker, opposition strength. Their border is
   heavier than a card's on purpose: it is what carries the selected state, so
   it has to read before anything is selected. One recipe, so the three do not
   drift apart again. */
.picker-option,
.strength-btn,
.player-tile {
    border: 2px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    font-family: inherit;
    transition: border-color var(--motion-state), background var(--motion-state);
}

/* ===== Chips =====
   One shape under five names. These were five separate rules repeating the
   same border, pill radius and background with small drifts in size, weight
   and padding — which is why a row of them never quite lined up.

   Two sizes only. Standard carries content; status is smaller and uppercase
   because it names a state the row is in rather than telling you something. */
.pill,
.subtab-count,
.kit-item,
.opposition-chip,
.birthday-flag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    background: var(--color-bg);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
    vertical-align: middle;
}

/* Status markers. */
.pill,
.subtab-count {
    padding: var(--space-1) var(--space-2);
}

/* Sits inline after a heading, so it needs to be held off the word. */
.pill {
    margin-left: var(--space-2);
}

/* A number, not a word: no casing, and wide enough that 1 and 11 are the
   same width so the tabs beside them don't shift. */


/* Standard chips: one height. These are labels you read, not targets you
   tap, so they are shorter than the 44px row chips -- three sizes in total and
   all three are stated: status (small), standard (32px), tappable (44px). */
.kit-item,
.opposition-chip,
.birthday-flag,
.weather-chip,
.report-person {
    min-height: 32px;
    color: var(--color-text);
}

.opposition-chip {
    /* Asymmetric: the remove button lives on the right. */
    padding-right: var(--space-2);
    padding-left: var(--space-4);
}

/* The one nudge rather than a fact, so it keeps its amber edge. */
.birthday-flag {
    background: var(--color-surface);
    border-color: var(--color-role-developer);
}

/* ===== Micro-labels =====
   The small uppercase text that names a thing rather than saying it: SUN/SEP
   on a date block, a role chip, a section label, a status pill.

   These were seven separate treatments with four different letter-spacings
   and three different sizes. At this size tracking is not decoration — set
   too tight, uppercase text closes up and stops being readable — so it is one
   value everywhere.

   Deliberately NOT in this group: .pathway-who (a person's name) and
   .strength-label (a button's own label). Uppercasing either would be wrong. */
.role-chip,
.pill,
.subtab-count,
.fixture-dow,
.fixture-mon,
.fixture-year,
.kit-label {
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* Holds a club name now, not the word "US". Names are not shouted, and a long
   one set in 11px caps wraps badly on a phone. */
.scoreline-label {
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1.25;
    color: var(--color-text-muted);
}

/* A number, not a word. Wide enough that 1 and 11 are the same width, so the
   tabs beside them do not shift as counts change. */
.subtab-count {
    min-width: 1.6em;
    text-transform: none;
    letter-spacing: 0;
}

/* ===== Fixture / session rows =====
   Date as a calendar chip on the left so a season's worth of rows can be
   scanned down the left edge on a phone. */
.fixture {
    /* Anchor for the stretched link below. */
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--space-4);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: var(--space-4);
    margin-bottom: var(--space-3);
}

.fixture-date {
    display: flex;
    flex-direction: column;
    align-items: center;
    flex-shrink: 0;
    min-width: 52px;
    padding: var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    line-height: 1.15;
}

.fixture-dow,
.fixture-mon,
.fixture-year {
    color: var(--color-text-muted);
}

.fixture-day {
    font-size: var(--text-xl);
    font-weight: bold;
    color: var(--color-heading);
    font-family: var(--font-display);
}

.fixture-body {
    flex: 1;
    min-width: 0;
}

.fixture-club {
    font-size: var(--text-lg);
    font-weight: bold;
    color: var(--color-heading);
    text-decoration: none;
}

.fixture-club:hover {
    text-decoration: underline;
}

/* The whole row is the tap target. The club name's link is stretched over the
   card, which retired a ~40px chevron column that spent a lot of a 375px
   screen on one glyph. Anything that must stay separately tappable lifts
   itself back above it -- see .fixture-tags.

   Only when it IS a link. On the training page .fixture-club is a span, so
   this was laying an invisible sheet over the whole row that swallowed every
   click and went nowhere -- which is why the plan upload appeared to do
   nothing. An overlay whose only job is to carry a link should not exist
   where there is no link. */
a.fixture-club::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
}

/* One wrapping row of chips instead of four stacked blocks. Directions, the
   birthday nudge, the match count and the volunteer's edit link were each
   taking a line of their own, which was most of the row's height.

   They are one shape now. Before this they were four: an outlined pill, a
   boxed button, loose grey text and an underlined link, all in the same row —
   which is what made the row look assembled rather than designed. */
.fixture-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

/* The same lift for everything else interactive inside a row. Stated once,
   here, rather than remembered by each thing that gets added to a row later --
   forgetting it is silent: the control renders, looks right, and does nothing
   when you press it. */
.fixture .inline-edit,
.fixture .plan-list,
.fixture .row-actions {
    position: relative;
    z-index: 1;
}

/* The one chip shape. Everything in the row is this, and the variants below
   change only colour — never height, radius or padding. */
.fixture-tags > * {
    /* Above the row's stretched link, so each stays individually tappable. */
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    margin: 0;
    padding: 0 var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    background: var(--color-surface);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    text-decoration: none;
    transition: border-color var(--motion-state), color var(--motion-state);
}

/* Directions is the one that gets tapped in a car park, so it is the only
   one that carries the accent colour. The height and radius are repeated here
   because the base .directions rule matches with equal specificity and wins on
   source order otherwise. */
.fixture-tags .directions {
    min-height: 44px;
    padding: 0 var(--space-4);
    border-radius: var(--radius-pill);
    /* The base .directions rule sets bold; matched to its neighbours here so
       the row is one weight as well as one shape. */
    font-weight: 600;
    color: var(--color-link);
    border-color: var(--color-link);
}

.fixture-tags .directions:hover,
.fixture-tags .row-actions:hover {
    border-color: var(--color-link);
    color: var(--color-link);
}

/* The birthday keeps its amber edge — it is the one thing in the row that is
   a nudge rather than a fact — but now at the same size as everything else. */
.fixture-tags .birthday-flag {
    border-color: var(--color-role-developer);
    color: var(--color-text);
}

.fixture-tags .row-actions a {
    color: inherit;
    text-decoration: none;
}

/* The way into the write-up on a closed fixture. Carries the accent because
   on a finished fixture it is the one thing most people are after. */
.report-chip {
    color: var(--color-link);
    border-color: var(--color-link);
}

/* Metadata, not a control: reads as a label rather than something to press.
   Same shape as its neighbours -- the shared rule above already gave it that. */
.weather-chip {
    background: var(--color-bg);
    cursor: default;
}

/* The advice sentence that sometimes comes with the forecast. It is prose, so
   it sits under the chip row rather than in it -- inside, it was being given
   the 44px pill treatment and reading as a button you couldn't press. */
.weather-advice {
    margin: var(--space-3) 0 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    /* Balances against the row's left edge rather than the date column. */
    max-width: 60ch;
}

.fixture-edit {
    flex-shrink: 0;
    font-size: var(--text-sm);
    white-space: nowrap;
}

.pill-warn {
    background: var(--color-error-bg);
    border-color: var(--color-error-border);
    color: var(--color-error-text);
}

/* The counterpart to .pill-warn. The word in the pill always says the same
   thing as the colour, so this is reinforcement and never the message. */
.pill-ok {
    background: var(--color-success-bg);
    border-color: var(--color-success-border);
    color: var(--color-success-text);
}

.is-cancelled .fixture-club {
    text-decoration: line-through;
}

/* The sentence a coach reads on the way to training. */
.concussion-summary {
    font-size: var(--text-lg);
    line-height: 1.4;
    margin-bottom: var(--space-3);
}

.gates {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}

.gates .pill {
    margin-left: 0;
}

/* ===== Timeline ===== */
.timeline {
    list-style: none;
    margin: 0;
    padding: 0;
}

.timeline li {
    position: relative;
    padding-left: var(--space-6);
    padding-bottom: var(--space-5);
}

.timeline li::before {
    content: '';
    position: absolute;
    left: 7px;
    top: 1.35rem;
    bottom: 0;
    width: 2px;
    background: var(--color-border);
}

.timeline li:last-child {
    padding-bottom: 0;
}

.timeline li:last-child::before {
    display: none;
}

.timeline-dot {
    position: absolute;
    left: 0;
    top: 0.3rem;
    width: 16px;
    height: 16px;
    border-radius: var(--radius-circle);
    border: 2px solid var(--color-border);
    background: var(--color-surface);
}

.timeline .is-done .timeline-dot {
    background: var(--color-link);
    border-color: var(--color-link);
}

.timeline .is-current .timeline-dot {
    background: var(--color-role-developer);
    border-color: var(--color-role-developer);
}

.timeline .is-due .timeline-dot {
    background: var(--color-role-volunteer);
    border-color: var(--color-role-volunteer);
}

.action-banner {
    font-size: var(--text-lg);
}

.restart-form {
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border);
}

/* Bulk contact entry: one guardian per row, wrapping to stacked on a phone. */
.guardian-grid {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: var(--space-3);
    align-items: end;
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--color-border);
}

.guardian-grid:last-child {
    border-bottom: none;
    margin-bottom: 0;
    padding-bottom: 0;
}

.guardian-grid-name {
    grid-column: 1 / -1;
}

.guardian-grid .form-group {
    margin-bottom: 0;
}

@media (max-width: 640px) {
    .guardian-grid {
        grid-template-columns: 1fr;
    }
}

.page-head span {
    display: flex;
    align-items: center;
    gap: var(--space-4);
}

/* ===== Directions ===== */
.directions-wrap {
    margin: var(--space-3) 0;
}

/* Sized as a proper touch target: this gets tapped in a car park, one-handed,
   probably in the rain. */
.directions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    /* A postcode split across two lines is unreadable at a glance. */
    white-space: nowrap;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-link);
    text-decoration: none;
    font-size: var(--text-sm);
    font-weight: bold;
}

.directions::before {
    content: '➜';
    transform: rotate(-45deg);
}

.directions:hover {
    border-color: var(--color-link);
}

/* ===== Sub-tabs ===== */
.subtabs {
    position: relative;
    z-index: 1;
    display: flex;
    gap: var(--space-1);
    margin-bottom: var(--space-5);
    border-bottom: 1px solid var(--color-border);
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

.subtabs::-webkit-scrollbar {
    display: none;
}

.subtabs a {
    white-space: nowrap;
}

.subtabs a {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    color: var(--color-text-muted);
    text-decoration: none;
    font-size: var(--text-sm);
    border-bottom: 3px solid transparent;
    margin-bottom: -1px;
}

.subtabs a:hover {
    color: var(--color-text);
}

.subtabs a.is-active {
    color: var(--color-heading);
    font-weight: bold;
    border-bottom-color: var(--color-heading);
}

.subtab-count--warn {
    background: var(--color-error-bg);
    border-color: var(--color-error-border);
    color: var(--color-error-text);
}

/* A second tier of tabs inside a tab. Pills rather than a second underlined
   bar, because two identical tab bars stacked read as one broken one -- the
   page-level bar stays the primary, this is a filter within it. */
.subtabs--pills {
    border-bottom: none;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

.subtabs--pills a {
    min-height: 40px;
    padding: 0 var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    border-bottom: 1px solid var(--color-border);
    margin-bottom: 0;
}

.subtabs--pills a.is-active {
    border-color: var(--color-link);
    background: var(--color-selected);
    color: var(--color-heading);
}

/* ===== Collapsible rows =====
   Closed, this is a row you tap -- it does not need a card's padding wrapped
   around a single line of text. The summary's own 44px keeps the tap target,
   and the full padding comes back when it opens. */
details.card {
    padding: 0 var(--space-4);
}

details.card[open] {
    padding-bottom: var(--space-4);
}

details.card > summary {
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    /* The default triangle is lost once summary becomes a flex container, so
       the chevron below replaces it — without one, nothing says "tap me". */
    list-style: none;
    min-height: 44px;
}

details.card > summary::-webkit-details-marker {
    display: none;
}

details.card > summary::after {
    content: '⌄';
    margin-left: auto;
    font-size: var(--text-lg);
    line-height: 1;
    color: var(--color-text-muted);
    transform: translateY(-0.2em);
    transition: transform 0.15s ease;
}

details.card[open] > summary::after {
    transform: rotate(180deg) translateY(0.1em);
}

details.card[open] > summary {
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--color-border);
}

.guardian-summary-name {
    font-weight: bold;
}

.matrix-row {
    display: grid;
    grid-template-columns: 2fr auto 2fr auto;
    gap: var(--space-3);
    align-items: center;
    padding: var(--space-2) 0;
    border-top: 1px solid var(--color-border);
}

@media (max-width: 640px) {
    .matrix-row {
        grid-template-columns: 1fr auto;
    }
}

.guardian-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border);
}

.guardian-actions form {
    margin: 0;
}

/* A link meant to be copied out and pasted into WhatsApp. */
.copy-link {
    margin-top: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    font-size: var(--text-sm);
    word-break: break-all;
}

/* ===== List row actions ===== */
.row-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-3);
    font-size: var(--text-sm);
}

/* Equal halves, not content-sized. With `auto` the width followed the label,
   so "Record Doncaster Blue" and "Record it" came out different widths and no
   two rows on a page lined up. `1 1 0` plus min-width:0 makes every pair the
   same regardless of how long the team name is. */
.row-actions .btn {
    flex: 1 1 0;
    min-width: 0;
}

/* Unless there is only one of them. Equalising a pair is the point; stretching
   a lone action across the whole card is just a button that has lost its
   shape, and it reads as a banner rather than something to press. */
.row-actions .btn:only-child {
    flex: 0 0 auto;
}

/* Side by side on a phone, a long label like "Record Doncaster Blue" wraps to
   three lines in a half-width button. Full width each, stacked, which is what
   the contract says a primary action does on a phone.

   Stated here rather than in the Phones block: a media query adds no
   specificity, so from up there it lost to the rule directly above. */
@media (max-width: 640px) {
    .row-actions .btn {
        flex-basis: 100%;
    }

    /* Still not the lone one: full width on a phone is right for a pair that
       would otherwise be two cramped half-buttons, and wrong for a single
       action that already fits. */
    .row-actions .btn:only-child {
        flex-basis: auto;
    }
}

/* Inside a fixture row .row-actions is itself a chip, so its link must not be
   promoted to a button -- that would put a button inside a chip. */
.fixture-tags .row-actions a:not([class]) {
    all: unset;
    cursor: pointer;
    color: inherit;
}

/* Inside a fixture row .row-actions is a chip, not an action row. Stated here
   because the rule above is later in the file than .fixture-tags > * and would
   otherwise win on source order and put a margin above the chip. */
.fixture-tags .row-actions {
    display: inline-flex;
    gap: var(--space-2);
    margin-top: 0;
    font-size: var(--text-sm);
}

/* An inline destructive action sitting among plain-text row actions -- the
   "Delete" on a game row. A full .btn-danger on its own line makes deleting a
   single row look like the most important thing on the page, so this matches
   the weight of the links beside it and carries its meaning in the colour and
   the word together, never the colour alone.

   The form wrapper is neutralised the same way .opposition-chip form does it:
   a form is block-level by default and would otherwise break the row. */
.row-actions form,
.action-delete form {
    display: inline;
    margin: 0;
}

.action-delete {
    padding: 0;
    border: none;
    background: none;
    font-family: inherit;
    font-size: inherit;
    font-weight: 600;
    color: var(--color-danger-surface);
    text-decoration: underline;
    cursor: pointer;
    transition: color var(--motion-state);
}

.action-delete:hover {
    color: var(--color-danger-surface-hover);
}

.is-cancelled strong,
.is-cancelled .text-muted {
    text-decoration: line-through;
}

/* ===== Forms: extras ===== */
.form-group small {
    display: block;
    margin-top: var(--space-1);
    font-size: var(--text-sm);
}

.form-check label {
    font-weight: normal;
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
}

.consent-block .form-check {
    margin-bottom: var(--space-4);
}

/* Not a choice — reads as a statement, so it must not look like a tick box. */
.consent-notice {
    padding: var(--space-3);
    border-left: 3px solid var(--color-link);
    background: var(--color-bg);
    font-size: var(--text-sm);
    line-height: 1.5;
}

.consent-block .form-check label {
    line-height: 1.4;
}

/* A checkbox, its label and an optional hint. The hint lines up with the
   label text rather than the box, and the whole label is a 44px target --
   a 13px native checkbox is not something to aim at one-handed. */
.form-check > label {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 44px;
    font-weight: 600;
    cursor: pointer;
}

.form-check input[type="checkbox"],
.form-check input[type="radio"] {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
    margin: 0;
    /* Paints the native control in the brand colour, so it matches the rest
       of the form without rebuilding the checkbox from scratch. */
    accent-color: var(--color-link);
    cursor: pointer;
}

.form-check > small {
    display: block;
    margin-top: var(--space-1);
    padding-left: calc(22px + var(--space-3));
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

fieldset {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: var(--space-4);
    margin: 0 0 var(--space-4);
}

legend {
    font-size: var(--text-sm);
    font-weight: bold;
    padding: 0 var(--space-2);
}

/* ===== Destructive actions ===== */
.danger-zone {
    border-color: var(--color-error-border);
}

.btn-danger {
    background: var(--color-danger-surface);
}

.btn-danger:hover {
    background: var(--color-danger-surface-hover);
}

/* ===== Theme toggle ===== */
/* One shape for every control in the header bar. The theme toggle, the
   view-as links and log out sit side by side, so they get the same height,
   radius, border and type -- previously a pill, a 4px box and a bare link. */
.theme-toggle,
.view-as-picker a,
.account-controls a {
    display: inline-flex;
    align-items: center;
    min-height: 28px;
    padding: 0 var(--space-3);
    border: 1px solid var(--color-on-dark-line);
    border-radius: var(--radius-pill);
    background: none;
    color: var(--color-on-dark-muted);
    font-family: inherit;
    font-size: var(--text-sm);
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition: border-color var(--motion-state), color var(--motion-state);
}

.theme-toggle:hover,
.view-as-picker a:hover,
.account-controls a:hover {
    border-color: var(--color-on-dark);
    color: var(--color-on-dark);
    text-decoration: none;
}



/* ===== What to bring ===== */
.kit {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-3) 0;
}

.kit-label {
    color: var(--color-text-muted);
}

/* Ticked for this one event, so it stands out from the three constants. */
.kit-item--extra {
    border-color: var(--color-link);
    background: var(--color-surface);
    font-weight: bold;
}

/* A short form sitting inside an expanded row, not a page of its own. */
.inline-form {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border);
}

/* Name over email, so two accounts held by the same person are told apart at a
   glance rather than after a click. */
.person-summary {
    display: flex;
    flex-direction: column;
    line-height: 1.3;
    margin-right: auto;
}

.person-email {
    font-size: var(--text-sm);
    font-weight: normal;
    color: var(--color-text-muted);
}

/* ===== View as =====
   Deliberately loud. Forgetting you are previewing and concluding the site is
   broken is the obvious failure mode. */
.viewing-as {
    background: var(--color-role-developer);
    color: var(--color-navy-dark);
    font-size: var(--text-sm);
    font-weight: bold;
    padding: var(--space-3) 0;
}

.viewing-as .container {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

.viewing-as a {
    color: var(--color-navy-dark);
}

.view-as-picker {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
}





.view-as-picker a.is-active {
    background: var(--color-role-developer);
    border-color: var(--color-role-developer);
    color: var(--color-navy-dark);
}

.injury-entry {
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--color-border);
}

.injury-entry:last-child {
    padding-bottom: 0;
    margin-bottom: 0;
    border-bottom: none;
}

/* A radio group is a fieldset with a legend, not a label pointing at one
   input. The browser's default fieldset box is reset so it lines up with the
   other .form-groups instead of drawing a border around every choice row. */
fieldset.form-group {
    border: none;
    padding: 0;
    margin: 0 0 var(--space-4);
    min-width: 0;
}

fieldset.form-group > legend {
    padding: 0;
    margin-bottom: var(--space-2);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--color-text);
}

/* ===== Games =====
   A closed-off game moves to the "Closed games" sub-tab. It is still there
   and can be reopened -- it just stops competing for attention with the games
   still to play. Same sub-tab pattern as Fixtures/Results on /fixtures. */
.game-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2) var(--space-3);
    margin-bottom: var(--space-2);
}

.game-score {
    font-size: var(--text-xl);
    font-weight: bold;
    color: var(--color-heading);
    letter-spacing: -0.015em;
}

/* Done with: the score still reads, the rest steps back. */
.game-card.is-done .game-head,
.game-card.is-done .text-muted {
    color: var(--color-text-muted);
}

/* ===== Photos =====
   Action shots and team photos attached to a fixture. */
.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: var(--space-4);
    margin: 0;
    padding: 0;
    list-style: none;
}

.photo-tile {
    display: flex;
    flex-direction: column;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}

/* The one that leads the report. */
.photo-tile.is-hero {
    border-color: var(--color-link);
    box-shadow: var(--shadow-md);
}

/* 4:5, because almost every photo here comes off a phone held upright.
   A fixed ratio is still needed -- a grid of mixed shapes comes out ragged --
   but 3:2 and 4:3 both took a third off the top and bottom of a portrait shot,
   which is where the faces are. 4:5 costs a landscape photo a little at the
   sides and a portrait one almost nothing. Same ratio as the report's tiles,
   so a photo looks the same on the site as it does in the PDF. */
.photo-tile img {
    display: block;
    width: 100%;
    /* Fixed ratio so a grid of portrait and landscape shots still lines up. */
    aspect-ratio: 4 / 5;
    object-fit: cover;
    background: var(--color-bg);
}

.photo-tile-body {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-3);
}

/* One award inside a team's card. A team can have more than one, so they need
   separating from each other -- without a rule between them two awards read as
   one award with two names. The first has no line above it: a separator before
   the first item is a border pretending to be a divider. */
.knight-award + .knight-award {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border);
}

/* The presentation shot on a Knight of the Match card.
   Same fixed ratio as the tiles, so a portrait photo and a landscape one make
   cards the same height. */
.knight-photo {
    display: block;
    width: 100%;
    max-width: 22rem;
    aspect-ratio: 4 / 5;
    object-fit: cover;
    border-radius: var(--radius-md);
    background: var(--color-bg);
}

/* Choosing that shot: the same .choice control, but the thing being chosen is
   a picture, so it needs to be looked at rather than read. */
.choices-photo .choice {
    flex: 0 1 11rem;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: var(--space-2);
    padding: var(--space-2);
    text-align: left;
}

.choices-photo .choice > span {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.choices-photo .choice img {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 5;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--color-bg);
}

.photo-caption {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

/* ===== Playing time =====
   Minutes per child against the RFU guidance. */
.playing-mins {
    font-weight: bold;
    color: var(--color-heading);
}

/* Over the day's limit. The number is always shown as well, so this is
   reinforcement rather than the message. */
.report-person--over {
    border-color: var(--color-error-border);
    background: var(--color-error-bg);
    color: var(--color-error-text);
}

.report-person--over .playing-mins {
    color: var(--color-error-text);
}

/* ===== On duty =====
   Who was there in a coaching or first-aid capacity, and who refereed. */
.on-duty-row {
    padding-top: var(--space-3);
    margin-top: var(--space-3);
    border-top: 1px solid var(--color-border);
}

.on-duty-row:first-of-type {
    padding-top: 0;
    margin-top: 0;
    border-top: none;
}

.on-duty-row h4 {
    margin: 0 0 var(--space-2);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.on-duty-row p {
    margin: 0;
}

/* ===== Knight of the Match =====
   One per team, per fixture. Enforced by the unique key on
   (fixture_id, squad, award), not by the form. */
.knight-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
}

.knight-name {
    font-size: var(--text-lg);
    color: var(--color-heading);
}

.knight-reason {
    margin: var(--space-2) 0 0;
    color: var(--color-text-muted);
    max-width: 60ch;
}

/* ===== Choice cards =====
   A small, fixed set of options as tappable cards instead of a dropdown. A
   native select on a phone opens an OS wheel that hides the rest of the form,
   shows one option at a time and takes three gestures to answer; a row of
   cards shows every option at once and takes one tap.

   TWO tests, and a control has to pass both.

   1. Is the set small and fixed? Up to about six — Home/Away, a role, a
      relationship, which of the Core Four. Keep the select for lists that come
      from the database and grow without limit: players, opposition teams,
      matches, injury types. Twenty cards is not a choice, it is a wall.
      Judge the real list, not the table it came from -- and again when it
      grows. The course list was briefly cards, at three courses, and went
      back to a select the same day at sixteen.

   2. Is it asked ONCE on the page? A control repeated per row fails even when
      the set is tiny — relationship on each of thirty-four guardians, or
      access level on each person, would be two hundred cards on one screen.
      Those stay dropdowns, and that is not an inconsistency: the same question
      asked once and asked thirty-four times is not the same control.

   The radio still does all the work and is still in the form; it is only not
   drawn. Keyboard, tab order, form submission and screen readers behave
   exactly as a radio group, which is what this is. */
.choices {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.choice {
    /* Share the row evenly, but never squeeze below a readable width. */
    flex: 1 1 auto;
    min-width: 6rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: 48px;
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-base);
    line-height: 1.3;
    text-align: center;
    cursor: pointer;
    transition: border-color var(--motion-state), background var(--motion-state);
}

/* Hidden, not removed: it is still the control being operated. */
.choice input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.choice:hover {
    border-color: var(--color-link);
}

.choice:has(input:checked) {
    border-color: var(--color-link);
    background: var(--color-selected);
    font-weight: 600;
}

/* The input is invisible, so the ring has to be drawn on the label or a
   keyboard user cannot see where they are. */
.choice:has(input:focus-visible) {
    outline: 2px solid var(--color-link);
    outline-offset: 2px;
}

.choice:has(input:disabled) {
    opacity: 0.5;
    cursor: not-allowed;
}

/* For options that need a line of explanation under the label. */
.choices--stacked {
    flex-direction: column;
}

.choices--stacked .choice {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    text-align: left;
    gap: var(--space-1);
}

.choice-desc {
    font-weight: 400;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

/* ===== Squad selection ===== */
.squad-counts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-5);
}

.squad-pick {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-2);
}

.squad-pick-options {
    display: flex;
    gap: var(--space-2);
}

/* Big enough to hit with a thumb while holding a clipboard. */
/* Same recipe as .picker-option and .strength-btn: this is the same kind of
   control and was the only one of the three at 1px and 40px. */
.squad-option {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: 0 var(--space-4);
    border: 2px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    font-size: var(--text-base);
    cursor: pointer;
    transition: border-color var(--motion-state), background var(--motion-state);
}

.squad-option:has(input:checked) {
    border-color: var(--color-link);
    background: var(--color-selected);
    font-weight: bold;
}

.squad-option--blue:has(input:checked) {
    border-color: var(--color-squad-blue);
    box-shadow: inset 0 0 0 1px var(--color-squad-blue);
}

.squad-option--red:has(input:checked) {
    border-color: var(--color-squad-red);
    box-shadow: inset 0 0 0 1px var(--color-squad-red);
}

.squad-option--knights:has(input:checked) {
    border-color: var(--color-squad-knights);
    box-shadow: inset 0 0 0 1px var(--color-squad-knights);
}

.squad-option input:disabled + span {
    color: var(--color-text-muted);
}

.inline-edit {
    margin-top: var(--space-3);
    font-size: var(--text-sm);
}

.inline-edit > summary {
    cursor: pointer;
    color: var(--color-link);
}

/* ===== Match day =====
   Built for a cold hand on a phone: big targets, no page reloads, and a save
   state you can see without reading. */
.save-state {
    position: sticky;
    /* Below the sticky nav, not underneath it. */
    top: 52px;
    z-index: 15;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-md);
    background: var(--color-success-bg);
    color: var(--color-success-text);
    border: 1px solid var(--color-success-border);
    font-size: var(--text-sm);
    font-weight: bold;
    margin-bottom: var(--space-4);
}

.save-state.is-bad {
    background: var(--color-error-bg);
    color: var(--color-error-text);
    border-color: var(--color-error-border);
}

.scoreline {
    display: flex;
    gap: var(--space-4);
    text-align: center;
}

.scoreline-side {
    /* Equal halves, so the two sides are symmetric however long the club
       names are. min-width:0 lets a long name wrap instead of forcing the
       card wider than the screen. */
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
}

/* Pushed to the bottom of the column so both sides' controls sit on the same
   line even when one club name wraps to two and the other doesn't. */
.scoreline-action {
    display: flex;
    gap: var(--space-2);
    margin-top: auto;
    padding-top: var(--space-3);
}


.scoreline-value {
    font-size: var(--text-3xl);
    font-weight: bold;
    line-height: 1;
    color: var(--color-heading);
    font-family: var(--font-display);
}

.their-buttons {
    display: flex;
    gap: var(--space-2);
    justify-content: center;
}

.their-buttons .btn {
    padding: var(--space-2) var(--space-3);
    min-width: 52px;
}

.player-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: var(--space-2);
    margin-bottom: var(--space-5);
}

.player-tile {
    display: flex;
    align-items: stretch;
    overflow: hidden;
}

.player-tile.is-in {
    border-color: var(--color-link);
    background: var(--color-selected);
}

/* A tick, so "who is in" is readable at a glance across twenty tiles rather
   than by comparing border colours two at a time. */
.player-tile.is-in::after {
    content: '✓';
    display: flex;
    align-items: center;
    padding: 0 var(--space-3);
    font-size: var(--text-lg);
    font-weight: bold;
    color: var(--color-link);
}

.player-tile-name,
.picker-option {
    flex: 1;
    min-height: 56px;
    padding: var(--space-2);
    font-family: inherit;
    font-size: var(--text-base);
    font-weight: bold;
    color: var(--color-text);
    cursor: pointer;
    text-align: left;
    line-height: 1.2;
}

.player-tile.is-in .player-tile-name {
    color: var(--color-heading);
}

.player-tile-surname {
    display: block;
    font-weight: normal;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

.try-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

/* Full-screen so a mis-tap hits a name rather than the page behind it.
   [hidden] needs its own rule: an author `display` declaration of equal
   specificity beats the UA stylesheet's `[hidden] { display: none }`, so
   without this the try-scorer picker showed on page load, not just on tap. */
.picker[hidden] {
    display: none;
}

.picker {
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgba(0, 0, 0, 0.6);
    display: flex;
    align-items: flex-end;
}

.picker-inner {
    width: 100%;
    max-height: 85vh;
    overflow-y: auto;
    padding: var(--space-4);
    background: var(--color-bg);
    border-top-left-radius: var(--radius-md);
    border-top-right-radius: var(--radius-md);
}

.picker-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-4);
}



.try-edit {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-height: 48px;
    padding: var(--space-2) 0;
    border: none;
    background: none;
    font-family: inherit;
    font-size: var(--text-base);
    color: var(--color-text);
    text-align: left;
    cursor: pointer;
}

.try-edit-hint {
    font-size: var(--text-sm);
    color: var(--color-link);
}

.form-actions--split {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

.form-actions--split .btn {
    flex: 1 1 auto;
}

/* ===== Opposition teams ===== */
.opposition-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.opposition-chip form {
    margin: 0;
}

.opposition-chip button {
    width: 28px;
    height: 28px;
    border: none;
    border-radius: var(--radius-circle);
    background: none;
    color: var(--color-text-muted);
    font-size: var(--text-lg);
    line-height: 1;
    cursor: pointer;
}

.opposition-chip button:hover {
    background: var(--color-error-bg);
    color: var(--color-error-text);
}

/* ===== Opposition strength ===== */
.strength-row {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

/* The radio is hidden but still focusable and still in the form -- it used to
   carry the `hidden` attribute, which took it out of the tab order so a
   strength could not be set from a keyboard at all. */
.strength-btn input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.strength-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    min-height: 64px;
    padding: var(--space-2) var(--space-1);
    color: var(--color-text-muted);
    cursor: pointer;
}

.strength-btn:has(input:focus-visible) {
    outline: 2px solid var(--color-link);
    outline-offset: 2px;
}

/* .is-on is rendered by PHP from the saved value, so on its own the buttons
   did not respond until the form was saved. :has() makes the selection show
   the moment it is tapped. */
.strength-btn:has(input:checked),
.strength-btn.is-on {
    background: var(--color-selected);
    border-color: var(--color-heading);
    background: var(--color-bg);
    color: var(--color-heading);
}

.strength-num {
    font-size: var(--text-lg);
    font-weight: bold;
}

.strength-label {
    font-size: var(--text-xs);
    text-align: center;
    line-height: 1.1;
}

/* Birthdays — a nudge, not an alert, so it reads as warm rather than urgent. */
.birthday-card {
    border-left: 4px solid var(--color-role-developer);
}

.birthday-card strong {
    display: block;
    margin-bottom: var(--space-2);
}

/* Glanceable on a list: you should spot the cake without reading the row. */

/* ===== Rail heading =====
   The row above a rail: what it is, and the one word that says it moves
   sideways. Separate from .page-head because a page has one of those and can
   have several rails. */
.rail-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.rail-head h2 {
    margin-bottom: 0;
}

.rail-hint {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    white-space: nowrap;
}

/* ===== Pathway =====
   The four stations of an injury, across rather than down. Same rail mechanics
   as .rail (docs/design-parameters.md §6) but fixed-width stations rather than
   cards, because the set is always four and the point is the shape, not the
   content. It reads identically on every record, so a parent who has seen one
   knows where they are on the next without reading it. */
.pathway {
    display: flex;
    list-style: none;
    margin: 0 calc(var(--space-5) * -1) var(--space-5);
    padding: 0 var(--space-5);
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

.pathway::-webkit-scrollbar {
    display: none;
}

.pathway li {
    position: relative;
    flex: 1 0 8.5rem;
    /* Leaves room for the dot, which is absolutely positioned at the top so it
       can sit on the rail line rather than in the text flow. */
    padding-top: var(--space-8);
    text-align: center;
}

/* The line, drawn behind the dots. Each station owns its own segment, so
   marking a station done colours its run of the line too. */
.pathway li::before {
    content: '';
    position: absolute;
    /* Half the dot's height, so the line meets the dots through their centres. */
    top: 1.15rem;
    left: 0;
    right: 0;
    height: 2px;
    background: var(--color-border);
}

.pathway li:first-child::before {
    left: 50%;
}

.pathway li:last-child::before {
    right: 50%;
}

.pathway .is-done::before {
    background: var(--color-success-border);
}

.pathway-dot {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1;
    display: grid;
    place-items: center;
    width: 2.3rem;
    height: 2.3rem;
    border-radius: var(--radius-circle);
    background: var(--color-surface);
    border: 2px solid var(--color-border);
    color: var(--color-text-muted);
    font-weight: bold;
    font-size: var(--text-sm);
}

.pathway .is-done .pathway-dot {
    background: var(--color-success-bg);
    border-color: var(--color-success-border);
    color: var(--color-success-text);
}

/* Carries white text, so it uses the accent surface rather than brand navy,
   which would vanish against the dark page background. */
.pathway .is-current .pathway-dot {
    background: var(--color-accent-surface);
    border-color: var(--color-accent-surface);
    color: var(--color-on-dark);
}

.pathway-title {
    display: block;
    font-size: var(--text-sm);
    font-weight: bold;
    color: var(--color-text-muted);
    padding: 0 var(--space-1);
}

.pathway .is-done .pathway-title,
.pathway .is-current .pathway-title {
    color: var(--color-text);
}

.pathway-who {
    display: block;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    padding: 0 var(--space-1);
}

/* ===== Next action =====
   One card, at the top of a record, naming the single thing it is waiting for
   and whose job it is. Everything else on the page is reference; this is the
   bit that moves. The left border is an accent mark carrying no text, so it
   uses --color-link rather than the accent surface. */
.nextup {
    border-left: 4px solid var(--color-link);
}

.nextup h2 {
    font-size: var(--text-lg);
    margin-bottom: var(--space-2);
}

.nextup .whose {
    display: block;
    font-size: var(--text-sm);
    font-weight: normal;
    color: var(--color-text-muted);
    margin-bottom: var(--space-3);
}

/* ===== Fit-to-return declaration =====
   The statements a parent agrees to before a player comes back. Set wider and
   looser than a normal list because people actually have to read it. */
.declaration {
    list-style: none;
    margin: 0 0 var(--space-4);
    padding: 0;
}

.declaration li {
    position: relative;
    padding-left: var(--space-5);
    margin-bottom: var(--space-3);
    line-height: 1.5;
}

.declaration li::before {
    content: '';
    position: absolute;
    left: 0;
    /* Optical: aligns the bullet with the middle of the first line rather than
       its top. */
    top: 0.45em;
    width: var(--space-2);
    height: var(--space-2);
    border-radius: var(--radius-circle);
    background: var(--color-link);
}

/* A declaration form's bullets are replaced by its checkboxes. */
.declaration li:has(input)::before {
    display: none;
}

.declaration li:has(input) {
    padding-left: 0;
}

.declared-by {
    border-left: 4px solid var(--color-success-border);
}

/* The signature line. Deliberately not styled as a throwaway text input: the
   parent is putting their name to something. */
.sign-name {
    font-size: var(--text-lg);
}

/* ===== Match report =====
   The write-up parents read and the thing that gets printed for Spond. It is
   a document, not a screen: one column, a real measure, no controls in it.

   There is no crest file in the project, so the branding is typographic --
   a monogram, a navy rule, and the team name set as a masthead. */
.report-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

.report-actions-buttons {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

.report {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: var(--space-6) var(--space-5) var(--space-5);
    max-width: 46rem;
    margin: 0 auto;
}

/* ----- Masthead ----- */
/* A navy band rather than black text on white with a thin rule. The masthead
   is the first thing a parent sees and the thing that says whose report this
   is, so it carries the brand instead of describing it. */
.report-head {
    /* Bleeds to the card's edges, hence the negative margins matching the
       card's own padding. */
    margin: calc(var(--space-6) * -1) calc(var(--space-5) * -1) var(--space-5);
    padding: var(--space-5);
    background: var(--brand-navy);
    border-bottom: 4px solid var(--brand-red);
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    color: #ffffff;
}

.report-head h1,
.report-head .report-team {
    color: #ffffff;
}

/* The opposition name is the title of the document, so it gets to be big.
   No uppercase: without a condensed face, caps at this size just eat the line
   and stop being readable. */
.report-head h1 {
    font-size: 2.25rem;
    line-height: 1.05;
    letter-spacing: -0.025em;
}

.report-head .report-meta {
    color: rgba(255, 255, 255, 0.78);
}

.report-head .report-kicker {
    color: var(--brand-red);
}

/* On navy the monogram needs the opposite treatment: light disc, navy text. */
.report-head .report-brand-text {
    display: flex;
    flex-direction: column;
    line-height: 1.2;
}

/* The club name set as a wordmark. There is no crest file in the project, so
   the masthead is typographic rather than carrying an invented monogram. */
.report-team {
    font-family: var(--font-display);
    font-size: var(--text-base);
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-heading);
}

.report-kicker {
    margin-top: var(--space-1);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.report-head h1 {
    margin: 0 0 var(--space-2);
}

.report-meta {
    margin: 0;
    color: var(--color-text-muted);
}

/* ----- The day at a glance -----
   Four short facts before any detail, so a parent skimming on a phone gets
   the shape of the afternoon without scrolling. */
.report-summary {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
    gap: var(--space-3);
    margin: 0 0 var(--space-5);
    padding: var(--space-4);
    background: var(--color-bg);
    border-radius: var(--radius-md);
}

.report-stat--wide {
    grid-column: 1 / -1;
}

/* A sentence, not a figure, so it does not get the big bold treatment the
   other stats have. Stated at dd.report-stat-note because plain
   .report-stat-note loses to .report-stat dd on specificity. */
.report-stat dd.report-stat-note {
    font-family: var(--font-sans);
    font-size: var(--text-sm);
    font-weight: normal;
    line-height: 1.4;
    letter-spacing: normal;
    color: var(--color-text-muted);
}

.report-stat {
    display: flex;
    flex-direction: column;
}

/* Pushed to the bottom of the cell so the figures line up across the row even
   when one label wraps to two lines and its neighbours do not. */
.report-stat dd {
    margin-top: auto;
}

.report-stat dt {
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.report-stat dd {
    margin: var(--space-1) 0 0;
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.01em;
    color: var(--color-heading);
}

/* ----- The coaches' note ----- */
.report-comment {
    margin-bottom: var(--space-5);
    padding-left: var(--space-4);
    border-left: 3px solid var(--brand-red);
}

.report-comment h2 {
    margin-top: 0;
}

.report-comment p:last-child {
    margin-bottom: 0;
}

/* Who wrote it. Parents want to know whose view it is, and with several
   coaches on the age group "From the coaches" on its own is anonymous. */
.report-byline {
    margin: var(--space-3) 0 0;
    font-weight: 600;
    color: var(--color-heading);
}

/* ----- One squad's afternoon -----
   Blue's day and Red's day are two different days. Mixing them made "2 won"
   meaningless, so each side gets its own record and its own games. */
.report-squad-section {
    margin-top: var(--space-6);
}

.report-squad-head {
    padding-bottom: var(--space-3);
    border-bottom: 2px solid var(--color-border);
    margin-bottom: var(--space-2);
}

.report-squad-head h2 {
    margin: 0 0 var(--space-2);
}

.report-squad-record {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

/* ----- One game, as a card -----
   Flat rows separated by hairlines read as a table of results. A card with the
   squad's own colour across the top reads as a match. */
.report-game {
    margin-top: var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    overflow: hidden;
}

/* The squad colour class (.tag-blue / .tag-red) supplies the background, so a
   game is the colour of the side that played it. */
.report-game-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background: var(--color-accent-surface);
    color: #ffffff;
}

/* The squad colour has to beat .report-game-head's own background, and
   .tag-blue sits far earlier in the file, so it loses on source order at equal
   specificity. Stated at two classes here so a game is actually the colour of
   the side that played it. */
.report-game-head.tag-blue {
    background: var(--color-squad-blue);
}

.report-game-head.tag-knights {
    background: var(--color-squad-knights);
}

.report-game-head.tag-red {
    background: var(--color-squad-red);
}

.report-game-team {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 700;
    letter-spacing: 0.01em;
    min-width: 0;
}

.report-game-team--them {
    margin-left: auto;
    text-align: right;
}

.report-game-v {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    opacity: 0.75;
}

.report-game-score {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-4);
    padding: var(--space-4);
    background: var(--color-bg);
    border-bottom: 1px solid var(--color-border);
}

.report-score {
    font-size: var(--text-3xl);
    font-weight: bold;
    line-height: 1;
    letter-spacing: -0.03em;
    color: var(--color-heading);
    white-space: nowrap;
    font-family: var(--font-display);
}

.report-noscore {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

/* Won / Drew / Lost. Colour plus the word, never colour alone. */
.report-badge {
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-pill);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
}

.report-badge--won {
    background: var(--color-success-bg);
    color: var(--color-success-text);
    box-shadow: inset 0 0 0 1px var(--color-success-border);
}

.report-badge--lost {
    background: var(--color-error-bg);
    color: var(--color-error-text);
    box-shadow: inset 0 0 0 1px var(--color-error-border);
}

.report-badge--drew {
    background: var(--color-surface);
    color: var(--color-text-muted);
    box-shadow: inset 0 0 0 1px var(--color-border);
}

.report-game-body {
    padding: var(--space-4);
}

/* ----- People as chips -----
   A run of names separated by middots is a paragraph to read. Chips are a
   thing to scan, which is what a parent does: they are looking for one name. */
.report-people {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.report-person {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    background: var(--color-surface);
    font-size: var(--text-sm);
}

/* A try scorer earns a bit more weight than a name on the register. */
.report-person--scorer {
    font-weight: bold;
    color: var(--color-heading);
    border-color: var(--color-heading);
}

.report-person--standout {
    border-color: var(--color-role-developer);
    font-weight: 600;
}

/* The count against a name -- two tries, three assists. */
.report-multi {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.35em;
    height: 1.35em;
    border-radius: var(--radius-pill);
    background: var(--brand-red);
    color: #ffffff;
    font-size: var(--text-xs);
    font-weight: 600;
}

.report-block {
    margin-bottom: var(--space-4);
}

.report-block h3 {
    margin: 0 0 var(--space-2);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.report-block p {
    margin: 0;
}

.report-scorers {
    margin: 0;
    font-weight: bold;
    color: var(--color-heading);
}

.report-assists {
    margin: var(--space-1) 0 0;
    color: var(--color-text-muted);
}

.report-unrecorded {
    margin: var(--space-3) 0 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

/* Everyone who turned out. The reason this page exists for a parent: a child
   who played but did not score should still be named. */
.report-squad {
    color: var(--color-text-muted);
}

.report-notes {
    margin: 0;
    color: var(--color-text-muted);
}

/* ----- Awards and extras ----- */
.report-awards,
.report-birthdays,
.report-officials {
    margin-top: var(--space-6);
    padding-top: var(--space-5);
    border-top: 3px solid var(--brand-red);
}

.report-officials,
.report-birthdays {
    border-top-width: 1px;
    border-top-color: var(--color-border);
    margin-top: var(--space-5);
    padding-top: var(--space-4);
}

.report-officials h2,
.report-officials .report-block:last-child {
    margin-top: 0;
}

.report-awards h2,
.report-birthdays h2 {
    margin-top: 0;
}

.report-award-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--space-4);
}

/* The medal alongside the section heading rather than on every card: it marks
   the section once, and leaves each card free for the photograph of the award
   being handed over. */
.report-awards-head {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-bottom: var(--space-4);
}

.report-awards-head h2 {
    margin: 0;
}

.report-awards-intro {
    margin: calc(var(--space-3) * -1) 0 var(--space-4);
    color: var(--color-text-muted);
}

.report-awards-head .report-awards-intro {
    margin: var(--space-1) 0 0;
}

/* Fixed ratio, so a portrait shot and a landscape one make cards of the same
   height. */
.report-award-photo {
    width: 11rem;
    aspect-ratio: 4 / 5;
    object-fit: cover;
    flex-shrink: 0;
    border-radius: var(--radius-md);
    background: var(--color-bg);
}

/* The medal beside the name. A player looks for this before they read a word
   of the report, so it is given real size rather than an icon's worth. */
.report-award {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4);
    background: var(--color-bg);
    border-radius: var(--radius-md);
    border-left: 3px solid var(--brand-red);
}

.report-medal {
    width: 92px;
    height: 92px;
    flex-shrink: 0;
    /* The artwork is on white; on a tinted panel a circular mask keeps it from
       sitting in a white square. */
    border-radius: var(--radius-circle);
    background: #ffffff;
}

.report-award-body {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
    min-width: 0;
}

/* Which of the Core Four it was given for -- the same four named on the back
   of the medal. */
.report-cornerstone {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3) var(--space-1) var(--space-1);
    border-radius: var(--radius-pill);
    background: var(--brand-navy);
    color: #ffffff;
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* The same disc that is printed on the back of the medal. */
.report-cornerstone img {
    width: 22px;
    height: 22px;
    display: block;
}

.report-award-name {
    font-size: var(--text-lg);
    font-weight: bold;
    color: var(--color-heading);
}

.report-award-reason {
    color: var(--color-text-muted);
}

.report-empty {
    color: var(--color-text-muted);
}

.report-foot {
    margin-top: var(--space-6);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border);
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    text-align: center;
}

@media (max-width: 640px) {
    /* Stacked on a phone: three columns at 375px leaves each club name in a
       two-inch gutter. */
    .report-scoreline {
        grid-template-columns: 1fr;
        gap: var(--space-2);
        text-align: left;
    }

    .report-side--them {
        text-align: left;
    }
}

/* ===== Print =====
   This is printed to PDF and posted to Spond far more often than it is put on
   paper, so it keeps its colour. An earlier version stripped the panels and
   backgrounds to save ink, which is why the PDF looked bare -- ink is not the
   constraint when the output is a file.

   Last block in the file on purpose: these are overrides, and at equal
   specificity the later rule wins, so none of it needs !important. */
@media print {
    /* Print always uses the light palette. A dark-mode report is not what gets
       posted to Spond, and on paper it is unreadable.

       All three selectors, not just :root: the toggle sets
       :root[data-theme="dark"], which is a class+element higher in specificity
       than a bare :root and therefore beat this rule even inside @media print.
       Printing while the site was in dark mode produced a PDF with near-black
       panels and grey-on-grey headings. */
    :root,
    :root[data-theme="dark"],
    :root[data-theme="light"] {
        color-scheme: only light;
    }

    /* Backgrounds are dropped by default in most print settings, which would
       lose the masthead band, the squad colours and every badge -- the things
       that make it read as a document rather than a list. */
    *,
    *::before,
    *::after {
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
    }

    .site-header,
    .header-nav,
    .site-footer,
    .viewing-as,
    .report-actions,
    .alert,
    .back-link {
        display: none;
    }

    body {
        background: #ffffff;
    }

    main {
        padding: 0;
        min-height: 0;
    }

    .container {
        max-width: none;
        padding: 0;
    }

    /* The card chrome goes; the masthead band inside it stays. */
    .report {
        max-width: none;
        margin: 0;
        padding: 0;
        border: none;
        border-radius: 0;
        box-shadow: none;
        background: none;
    }

    .report-head {
        margin: 0 0 var(--space-5);
        border-radius: 0;
    }

    /* Only the small, self-contained things avoid a break. Putting
       break-inside:avoid on the squad sections is what produced four pages
       with a half-empty first one. */
    .report-game,
    .report-award,
    .report-block,
    .report-summary {
        break-inside: avoid;
    }

    .report-squad-section {
        break-inside: auto;
    }

    /* A heading should not be the last thing on a page. */
    .report-squad-head,
    .report-awards h2,
    .report-block h3 {
        break-after: avoid;
    }

    /* Paper takes smaller type than a screen. */
    html {
        font-size: 10.5pt;
    }

    .report-squad-section {
        margin-top: var(--space-4);
    }

    .report-awards,
    .report-birthdays {
        margin-top: var(--space-4);
        padding-top: var(--space-3);
    }

    .report-summary {
        padding: var(--space-3);
        margin-bottom: var(--space-4);
    }

    .report-comment {
        margin-bottom: var(--space-4);
    }

    .report-game-score {
        padding: var(--space-2);
    }

    .report-game-body {
        padding: var(--space-3);
    }

    /* Tighter inside a card so two or three fit on a page. With cards kept
       whole by break-inside:avoid, card height is what decides how much white
       space is left at the foot of each page. */
    .report-block {
        margin-bottom: var(--space-2);
    }

    .report-block:last-child {
        margin-bottom: 0;
    }

    .report-people {
        gap: var(--space-1);
    }

    .report-person {
        padding: 0 var(--space-2);
        font-size: 9pt;
    }

    .report-score {
        font-size: var(--text-xl);
    }

    .report-game {
        margin-top: var(--space-3);
    }

    .report-game-head {
        padding: var(--space-2) var(--space-3);
    }

    /* Squad names print as solid labels rather than rounded UI pills. */
    .tag {
        border-radius: 2px;
    }

    a {
        color: inherit;
        text-decoration: none;
    }

    /* A real page margin, not zero.
       Zeroing it does hide Chrome's print header and footer -- they are drawn
       inside the margin, so with no margin there is nowhere to draw them -- but
       that is a Chrome behaviour, it cannot be verified in Firefox from here,
       and if Firefox still draws its header the result is a header printed on
       top of the masthead. A browser-only trick that might be worse in the
       browser actually being used is not a fix.

       The real answer to "it looks like a web page printout" is to render the
       PDF on the server and serve it as a download, which is browser-
       independent by construction. */
    @page {
        margin: 12mm;
    }
}

/* ===== Training matrix =====
   People down, courses across, one cell per pairing. A grid rather than a card
   per person because the question it answers is a comparison -- who is missing
   what -- and a comparison you have to scroll between is not one you can make.

   With sixteen courses the table is wider than any phone, so it scrolls
   sideways inside its own box with the name column pinned. That is the whole
   trick: the row label has to stay visible or a cell three columns in means
   nothing. */
.matrix-scroll {
    overflow-x: auto;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
}

.matrix {
    /* separate, not collapse: a collapsed border disappears under a sticky
       cell, leaving the pinned column with no edge as the table scrolls. */
    border-collapse: separate;
    border-spacing: 0;
    /* max-content so columns keep their width instead of being squeezed to
       fit, which is what makes the sideways scroll work. */
    width: max-content;
    min-width: 100%;
    font-size: var(--text-sm);
}

.matrix th,
.matrix td {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--color-border);
    text-align: left;
    vertical-align: middle;
}

.matrix thead th {
    min-width: 9rem;
    max-width: 11rem;
    background: var(--color-bg);
    border-bottom: 2px solid var(--color-border);
    font-size: var(--text-xs);
    line-height: 1.3;
    vertical-align: bottom;
}

/* The pinned name column. Its own background, or the cells scrolling
   underneath show through. */
.matrix .matrix-person {
    position: sticky;
    left: 0;
    z-index: 2;
    min-width: 11rem;
    background: var(--color-surface);
    border-right: 1px solid var(--color-border);
}

.matrix thead .matrix-person {
    z-index: 3;
    background: var(--color-bg);
}

.matrix tbody tr:last-child th,
.matrix tbody tr:last-child td {
    border-bottom: 0;
}

.matrix-roles {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin-top: var(--space-1);
}

/* Which roles a course is compulsory for, under its name in the header. */
.matrix-mandatory {
    display: block;
    margin-top: var(--space-1);
    font-weight: normal;
    color: var(--color-text-muted);
}

/* The cell itself. The date is the message; the colour repeats it, and the
   state is spelled out in the title so colour is never carrying it alone. */
.matrix-state {
    display: block;
    padding: var(--space-1) var(--space-2);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    font-variant-numeric: tabular-nums;
    text-align: center;
    white-space: nowrap;
}

.matrix-state--valid {
    background: var(--color-success-bg);
    border-color: var(--color-success-border);
    color: var(--color-success-text);
}

.matrix-state--expiring {
    background: var(--color-warning-bg);
    border-color: var(--color-warning-border);
    color: var(--color-warning-text);
}

.matrix-state--expired,
.matrix-state--missing {
    background: var(--color-error-bg);
    border-color: var(--color-error-border);
    color: var(--color-error-text);
}

.matrix-state--pending {
    background: var(--color-selected);
    border-color: var(--color-link);
    color: var(--color-text);
}

/* Not required for this person's roles and not held. Nothing is wrong, so it
   does not get a colour -- only the ones that mean something do. */
.matrix-state--none {
    color: var(--color-text-muted);
    text-align: center;
}

/* A qualification recorded with no certificate uploaded. It is a caveat on the
   date above it, not a state of its own. */
.matrix-nofile {
    display: block;
    margin-top: var(--space-1);
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    text-align: center;
}

/* ===== A recorded qualification =====
   Name and state on one line, then the facts as labelled pairs. The old card
   was a bold line and a run-on grey sentence, which meant the expiry date --
   the only part anyone is looking for -- was buried mid-sentence. */
.qual-card-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
}

.facts {
    display: grid;
    /* Pairs that wrap, rather than a fixed count: four facts sit on one row on
       a laptop and two on a phone, with no breakpoint needed. */
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: var(--space-3);
    margin: var(--space-3) 0 0;
}

.facts dt {
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.facts dd {
    margin: var(--space-1) 0 0;
    font-size: var(--text-sm);
    color: var(--color-text);
}

/* A course already covered is still listed, greyed, so you can see it is there
   rather than wonder why it is missing. */
.form-group select option:disabled {
    color: var(--color-text-muted);
}

/* ===== Fundraising ledger =====
   The balance first and largest: it is the question the page is opened to
   answer. In and out sit beside it, because a balance with no working shown
   invites the question it was meant to settle. */
.ledger-totals {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
    gap: var(--space-4);
    margin: 0;
}

.ledger-totals dt {
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.ledger-totals dd {
    margin: var(--space-1) 0 0;
    font-size: var(--text-lg);
    font-weight: 600;
    /* Lining figures of one width, so columns of money line up. */
    font-variant-numeric: tabular-nums;
}

.ledger-totals .ledger-balance {
    font-size: var(--text-2xl);
}

.ledger-entry-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
}

.ledger-amount {
    font-size: var(--text-lg);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Money out is not an error, so it is not the error colour -- it is simply the
   other direction, and the sign in front of it is what carries the meaning. */
.ledger-amount.is-in { color: var(--color-success-text); }
.ledger-amount.is-out { color: var(--color-text-muted); }

/* ===== Grouped nav =====
   A <details> in the nav bar. Flat, thirteen links ran off the end of a laptop
   screen with nothing to say they were there -- the sideways scroll that works
   on a phone just hides things on a desktop. */
.nav-group {
    position: relative;
    flex-shrink: 0;
}

/* Everything the summary shares with a plain nav link is set once, with the
   links, above. Only what is genuinely its own lives here: it is a disclosure
   control with an arrow, where the others are links.

   inline-flex, not flex, so it keeps the same line box as the links beside it.
   A block-level flex summary was a different height and sat on a different
   baseline, which is what made the bar look misaligned. */
.nav-group > summary {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
    list-style: none;
}

/* Safari draws its own triangle through ::-webkit-details-marker, which
   list-style alone does not remove. */
.nav-group > summary::-webkit-details-marker {
    display: none;
}

/* The disclosure arrow, drawn rather than inherited, so it matches the type. */
.nav-group > summary::after {
    content: "";
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 4px solid currentColor;
    opacity: 0.7;
    transition: transform var(--motion-state);
}

.nav-group[open] > summary::after {
    transform: rotate(180deg);
}

.nav-group-menu {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 30;
    min-width: 12rem;
    display: flex;
    flex-direction: column;
    padding: var(--space-2);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
}

/* The panel sits on the page surface, not on the navy, so its links take the
   page's link colours rather than the chrome's. */
.nav-group-menu a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-height: 44px;
    padding: 0 var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--color-text);
    font-size: var(--text-sm);
    text-decoration: none;
    white-space: nowrap;
}

.nav-group-menu a:hover {
    background: var(--color-bg);
}

.nav-group-menu a.is-active {
    background: var(--color-selected);
    font-weight: 600;
}

/* On a phone the bar scrolls sideways, and an absolutely positioned panel
   inside a scrolling box is clipped by it. Below the small breakpoint the menu
   drops into the flow instead and the bar grows to fit. */
@media (max-width: 640px) {
    .nav-group {
        position: static;
    }

    .nav-group-menu {
        position: static;
        width: 100%;
        margin-bottom: var(--space-2);
    }
}

/* ===== Signed-in home =====
   Next match and next training side by side on a laptop, stacked on a phone.
   Two cards, because they answer two questions that get asked together. */
.home-next {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
    gap: var(--space-4);
    margin-bottom: var(--space-4);
}

.home-card-label {
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.home-headline {
    margin: var(--space-1) 0 var(--space-3);
    font-size: var(--text-xl);
}

/* Inside the alert, so it inherits the alert's own colour rather than the
   page's link colour, which would fight the panel it sits on. */
.home-jobs {
    margin: var(--space-2) 0 0;
    padding-left: var(--space-4);
}

.home-jobs li {
    margin-bottom: var(--space-1);
}

/* The helmet in the masthead, set against the wordmark's cap height. */
.logo img {
    height: 38px;
    width: auto;
    flex-shrink: 0;
}

/* ===== Signed-in home: the hero =====
   The next match is the reason most people open the site, so it is not one
   card among several. A page where every block has the same weight has no
   headline, which is what made this one read as a form. */
.home-greeting {
    margin: 0 0 var(--space-4);
    font-size: var(--text-lg);
    color: var(--color-text-muted);
}

.home-hero {
    position: relative;
    overflow: hidden;
    margin-bottom: var(--space-4);
    padding: var(--space-5);
    border-radius: var(--radius-lg);
    background: var(--color-navy);
    color: var(--color-on-dark);
    box-shadow: var(--shadow-md);
}

/* The crest, large and quiet, behind the text. Decorative, so it is a
   background rather than an <img> -- nothing is announced twice and nothing
   reflows if it fails to load. */
.home-hero::after {
    content: "";
    position: absolute;
    top: 50%;
    right: var(--space-5);
    width: 150px;
    height: 154px;
    transform: translateY(-50%);
    background: url("/assets/img/crest.webp") no-repeat center / contain;
    pointer-events: none;
}

/* Above the crest, and short of it, so long text never runs under the artwork. */
.home-hero-body {
    position: relative;
    z-index: 1;
    /* Short of the crest, so nothing ever runs underneath it. */
    max-width: min(100%, calc(100% - 190px));
}

.home-hero .home-card-label {
    color: var(--color-on-dark-muted);
}

.home-hero-title {
    margin: var(--space-1) 0 var(--space-4);
    font-size: var(--text-2xl);
    line-height: 1.1;
}

.home-hero-quiet {
    color: var(--color-on-dark-muted);
}

/* The facts inside the hero sit on navy, so they take the on-dark colours
   rather than the page's. */
.home-hero .facts dt {
    color: var(--color-on-dark-muted);
}

.home-hero .facts dd,
.home-hero p {
    color: var(--color-on-dark);
}

/* A button on a dark panel: the page's accent fill would be navy on navy. */
.btn-on-dark {
    background: var(--color-on-dark);
    border-color: var(--color-on-dark);
    color: var(--color-navy);
}

.btn-on-dark:hover {
    background: var(--color-on-dark-muted);
    border-color: var(--color-on-dark-muted);
    color: var(--color-navy);
}

/* A secondary action that stays a link. The site promotes a bare <a> in a
   .row-actions to a button, which is right when it is the only action and
   wrong when it is the lesser of two -- two identical buttons side by side
   make you read both to find out which one you wanted. */
.row-actions a.link-plain {
    display: inline;
    min-height: 0;
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-link);
    font-weight: normal;
    text-decoration: underline;
    text-underline-offset: 3px;
}

@media (max-width: 640px) {
    /* No room beside the text on a phone, and behind it it would just be
       clutter. */
    .home-hero::after {
        display: none;
    }

    .home-hero-body {
        max-width: 100%;
    }
}

/* The crest on the two pages somebody sees before they are inside the site.
   Both are centred pages with nothing else on them, and a wall of text with no
   mark on it is the thing that looked unfinished. */
.landing-crest,
.auth-crest {
    display: block;
    margin: 0 auto var(--space-4);
    height: auto;
}

.landing-crest {
    width: min(220px, 55vw);
}

.auth-crest {
    width: min(140px, 40vw);
}

/* ===== Training plans =====
   The documents attached to a session. A row per file rather than a card: a
   plan is one line -- what it is, what kind of file, who put it there -- and a
   card round each would outweigh the session it belongs to. */
.plan-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

.plan-item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2) var(--space-3);
    font-size: var(--text-sm);
}

.plan-item a {
    font-weight: 600;
}

/* ===== Month calendar =====
   Monday to Sunday across, weeks down. A table, because that is what a month
   is -- days by weeks -- and the alternative was inventing a grid component
   for one page.

   NOTE FOR THE DESIGN SESSION: this block was written by the Club/structure
   session on 2026-10-06, against the contract, because the page shipped
   unstyled and Scott had to look at it. The need is written up under "Design
   requests" in STATUS.md. Take it over, or replace it -- it is built from
   tokens and nothing else, so it should be safe to move wholesale.

   The hard part is the small screen. Seven columns at 420px is fifty pixels a
   column, which is not a calendar, so below 640px the grid stops being a grid
   and each week becomes a stack of the days that have something on them. A
   month of empty Tuesdays is not information worth a row. */
.month {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    margin-bottom: var(--space-4);
}

.month th {
    padding: var(--space-2);
    text-align: left;
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    border-bottom: 1px solid var(--color-border);
}

.month td {
    /* Tall enough for a date and three entries, so a busy Sunday does not
       stretch its whole week and leave the month lurching down the page. */
    height: 5.5rem;
    padding: var(--space-2);
    vertical-align: top;
    border: 1px solid var(--color-border);
    font-size: var(--text-sm);
    line-height: 1.35;
}

/* The date itself: a target, and the only thing in the cell that is one. */
.month td > a,
.month td > span {
    display: inline-block;
    margin-bottom: var(--space-1);
    font-weight: 700;
}

/* Days belonging to the month either side. Shown so every week is a full row,
   dimmed so the month still reads as the month. */
.month-outside {
    background: var(--color-bg);
}

.month-outside > a,
.month-outside > span {
    color: var(--color-text-muted);
    font-weight: 600;
}

/* Today. A left edge rather than a fill: a filled cell competes with the
   entries inside it, which are the point. */
.month-today {
    box-shadow: inset 3px 0 0 var(--color-link);
}

.month td div {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    margin-top: var(--space-1);
    font-size: var(--text-xs);
}

/* The pills in a cell are labels at a glance, not the 32px chips they are
   elsewhere -- seven columns cannot carry those. */
.month td .pill {
    margin-left: 0;
    padding: 0 var(--space-1);
    min-width: 1.25rem;
}

@media (max-width: 640px) {
    /* Not a grid any more. Each week becomes the days that have something on
       them, one under the other -- a month of empty Tuesdays is not worth a
       row on a phone. */
    .month,
    .month tbody,
    .month tr,
    .month td {
        display: block;
        width: auto;
        height: auto;
    }

    .month thead {
        display: none;
    }

    .month tr {
        margin-bottom: var(--space-3);
    }

    .month td {
        border: none;
        border-bottom: 1px solid var(--color-border);
        padding: var(--space-2) 0;
    }

    /* A day with nothing on it has nothing to say at this size. */
    .month td:not(:has(div)) {
        display: none;
    }

    .month-outside {
        background: none;
    }
}

/* ===== Week calendar =====
   The same idea as the month and a different job. Seven columns again, but
   one week instead of five, so a column is wide enough to carry ten age
   groups one under the other -- which is what the month grid cannot do once
   a club is bigger than a couple of teams.

   Written by the Club/structure session, 2026-10-06, with the .month block
   above and for the same reason. See STATUS.md. */
.week {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    margin-bottom: var(--space-4);
}

.week th {
    padding: var(--space-2);
    text-align: left;
    border-bottom: 2px solid var(--color-border);
    font-size: var(--text-sm);
}

.week th a {
    display: block;
    font-weight: 700;
    text-decoration: none;
}

/* The date under the day, not beside it: "Mon" and "12 Oct" side by side in
   a column this narrow wraps into two lines anyway, and wrapping chosen is
   tidier than wrapping that happens. */
.week th a span {
    display: block;
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--color-text-muted);
}

.week td {
    /* Deep enough that a quiet week still reads as a week rather than as a
       row of headings with nothing under them. */
    height: 9rem;
    padding: var(--space-2);
    vertical-align: top;
    border: 1px solid var(--color-border);
    border-top: none;
}

.week-today {
    box-shadow: inset 3px 0 0 var(--color-link);
}

.week-item {
    padding: var(--space-2) 0;
    font-size: var(--text-xs);
    line-height: 1.35;
    border-bottom: 1px solid var(--color-border);
}

.week-item:last-child {
    border-bottom: none;
}

.week-item .pill {
    margin-left: var(--space-1);
    padding: 0 var(--space-1);
    min-width: 1.25rem;
}

@media (max-width: 640px) {
    /* Seven columns on a phone is forty pixels a column. The week becomes a
       day at a time, headings included, which is the same information in the
       order somebody reads it anyway. */
    .week,
    .week thead,
    .week tbody,
    .week tr,
    .week th,
    .week td {
        display: block;
        width: auto;
        height: auto;
    }

    .week thead {
        display: none;
    }

    .week td {
        border: none;
        border-bottom: 1px solid var(--color-border);
        padding: var(--space-3) 0;
    }

    /* Without the headings the day has to be named on the cell itself. */
    .week td::before {
        content: attr(data-day);
        display: block;
        margin-bottom: var(--space-1);
        font-weight: 700;
    }

    /* A day with nothing on it says so in one line and takes no more room. */
    .week td:not(:has(.week-item)) {
        color: var(--color-text-muted);
    }
}

/* ===== One day, by the clock =====
   Opening a day went straight into three headings and a wall of lines, with
   the duties -- the longest section and usually the least urgent -- taking
   most of the page. This is the morning in the order it will happen, which
   is the one arrangement none of those lists offered.

   Club/structure session, 2026-10-06, with .month and .week above. */
.day {
    width: 100%;
    border-collapse: collapse;
    margin-top: var(--space-3);
}

.day td {
    padding: var(--space-3) var(--space-2);
    vertical-align: top;
    border-top: 1px solid var(--color-border);
    font-size: var(--text-sm);
}

.day tr:first-child td {
    border-top: none;
}

/* The time column: fixed, so the entries line up down the page and the eye
   can run the clock without reading the rest. */
.day-at {
    width: 4.5rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.day td:last-child {
    width: 35%;
    text-align: right;
}

@media (max-width: 640px) {
    /* The right-hand column drops under the entry rather than squeezing a
       pitch name into a third of a phone. */
    .day,
    .day tbody,
    .day tr,
    .day td {
        display: block;
        width: auto;
    }

    .day tr {
        padding: var(--space-3) 0;
        border-top: 1px solid var(--color-border);
    }

    .day tr:first-child {
        border-top: none;
    }

    .day td {
        border-top: none;
        padding: 0;
        text-align: left;
    }

    .day td:last-child {
        width: auto;
        text-align: left;
    }

    .day-at {
        width: auto;
    }
}

/* One entry on the week grid: who, what, when, where, on a line.
   Club/structure session, 2026-10-06, with the .week block above. */
.week-line {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-1);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--color-border);
    font-size: var(--text-xs);
    line-height: 1.3;
}

.week-line:last-child {
    border-bottom: none;
}

.week-line .pill {
    margin-left: 0;
    padding: 0 var(--space-1);
    min-width: 1.25rem;
}

/* The pills on a day heading -- whose duty it is, how many are away -- sit
   under the date rather than beside it, because a column this narrow cannot
   carry a date and two chips on one line. */
.week th .pill {
    display: inline-block;
    margin: var(--space-1) var(--space-1) 0 0;
}

/* ===== A slot picker =====
   Pitches down, half hours across, free or not. Moving a session meant
   typing a date, a time and a pitch and finding out afterwards whether the
   three of them worked together; this shows the answer instead.

   Built on .matrix, which already pins the first column and scrolls
   sideways -- the same trick the training matrix needs for the same reason.
   Only the column width and the two cell states are new.

   Club/structure session, 2026-10-06, with the .month and .week blocks. */
.matrix--slots th,
.matrix--slots td {
    padding: 0;
    text-align: center;
}

.matrix--slots thead th {
    min-width: 0;
    max-width: none;
    width: 4rem;
    padding: var(--space-2) 0;
    text-align: center;
}

.matrix--slots .matrix-person {
    min-width: 8rem;
    padding: var(--space-2) var(--space-3);
    text-align: left;
}

.matrix--slots td form {
    margin: 0;
}

/* Free: the whole cell is the target, so there is nothing to aim at. */
.slot-free button {
    display: block;
    width: 100%;
    padding: var(--space-3) var(--space-1);
    border: none;
    background: var(--color-success-bg);
    color: var(--color-success-text);
    font-family: inherit;
    font-size: var(--text-xs);
    font-weight: 600;
    cursor: pointer;
    transition: background var(--motion-state);
}

.slot-free button:hover,
.slot-free button:focus-visible {
    background: var(--color-success-border);
}

/* Taken: quiet. A wall of red for a normally busy evening would read as
   something being wrong, and nothing is -- most of a week is spoken for. */
.slot-taken {
    padding: var(--space-3) var(--space-1);
    color: var(--color-text-muted);
    background: var(--color-bg);
}

/* ===== Chat =====
   Messages, laid out the way WhatsApp is: a list of conversations with an
   avatar, the last thing said and an unread count; a thread on a wallpaper
   with your bubbles on the right and theirs on the left; ticks that turn
   blue when it has been read; a composer stuck to the bottom of the screen.
   Every parent already knows how to read that, which is most of the battle
   in getting them to use it instead.

   Colours are the chat tokens in the token block -- the club's blue rather
   than WhatsApp's green, so it is recognisably ours.

   Written by the Club comms session, 2026-10-09, on Scott's instruction
   ("sort the design out, take your lead from WhatsApp") rather than by the
   design session. Tokens only, the 640px breakpoint only. Review or take it
   over. */

/* ----- Avatars ----- */
.avatar {
    flex: none;
    width: 3rem;
    height: 3rem;
    border-radius: var(--radius-circle);
    display: grid;
    place-items: center;
    overflow: hidden;
    background: var(--color-selected);
    color: var(--color-link);
    font-size: var(--text-sm);
    font-weight: 700;
    letter-spacing: 0.02em;
}

.avatar img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    background: var(--color-navy);
}

.avatar svg {
    width: 55%;
    height: 55%;
}

.avatar--sm {
    width: 2.5rem;
    height: 2.5rem;
    font-size: var(--text-xs);
}

.avatar--lg {
    width: 6rem;
    height: 6rem;
    font-size: var(--text-xl);
}

/* ----- Conversation list ----- */
.chat-list {
    list-style: none;
    margin: 0 0 var(--space-4);
    padding: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    overflow: clip;
}

.chat-list li + li {
    border-top: 1px solid var(--color-border);
}

.chat-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 4.5rem;
    padding: var(--space-3) var(--space-4);
    color: var(--color-text);
    text-decoration: none;
    transition: background var(--motion-state);
}

/* A row can be a link (a conversation), a button (start one with this
   person) or a label (tick them into a group). Same row, whichever. */
button.chat-row,
label.chat-row {
    width: 100%;
    margin: 0;
    border: none;
    background: none;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

a.chat-row:hover,
button.chat-row:hover,
label.chat-row:hover {
    background: var(--color-bg);
}

.chat-row input[type="checkbox"] {
    flex: none;
    width: 1.25rem;
    height: 1.25rem;
    margin: 0;
    accent-color: var(--color-accent-surface);
}

.chat-list form {
    margin: 0;
}

.chat-row-main {
    flex: 1;
    min-width: 0;
}

.chat-row-line {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
}

.chat-row-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.chat-row-time {
    flex: none;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
}

.chat-row.is-unread .chat-row-time {
    color: var(--color-unread);
    font-weight: 600;
}

.chat-row-preview {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

.chat-row.is-unread .chat-row-preview {
    color: var(--color-text);
}

.chat-row-flags {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--color-text-muted);
}

.chat-row-flags svg {
    width: 1rem;
    height: 1rem;
}

.chat-unread {
    min-width: 1.375rem;
    height: 1.375rem;
    padding: 0 var(--space-1);
    border-radius: var(--radius-pill);
    display: inline-grid;
    place-items: center;
    background: var(--color-unread);
    color: var(--color-on-dark);
    font-size: var(--text-xs);
    font-weight: 700;
}

/* ----- The conversation ----- */
.chat {
    display: flex;
    flex-direction: column;
    background: var(--color-chat-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    overflow: clip;
}

.chat-head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
}

.chat-head-back {
    flex: none;
    width: 2.75rem;
    height: 2.75rem;
    display: grid;
    place-items: center;
    border-radius: var(--radius-circle);
    color: var(--color-link);
    text-decoration: none;
}

.chat-head-back svg {
    width: 1.5rem;
    height: 1.5rem;
}

.chat-head-title {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 2.75rem;
    color: var(--color-text);
    text-decoration: none;
}

.chat-head-text {
    min-width: 0;
    line-height: 1.25;
}

.chat-head-name {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.chat-head-sub {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
}

.chat-thread {
    flex: 1;
    min-height: 60vh;
    list-style: none;
    margin: 0;
    padding: var(--space-3) var(--space-3) var(--space-4);
    display: flex;
    flex-direction: column;
}

.chat-empty {
    align-self: center;
    margin: auto 0;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    box-shadow: var(--shadow-sm);
}

.chat-day {
    align-self: center;
    margin: var(--space-4) 0 var(--space-2);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    box-shadow: var(--shadow-sm);
}

/* One message. A column so the bubble and its action row line up on the
   sender's side. */
.msg {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    margin-top: var(--space-2);
}

.msg--out {
    align-items: flex-end;
}

/* Same sender, straight after: tucked up, no repeated name. */
.msg--cont {
    margin-top: var(--space-1);
}

.msg-bubble {
    position: relative;
    max-width: 65%;
    padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
    border-radius: var(--radius-lg);
    background: var(--color-bubble-in);
    color: var(--color-text);
    box-shadow: var(--shadow-sm);
    line-height: 1.4;
    overflow-wrap: anywhere;
}

.msg--out .msg-bubble {
    background: var(--color-bubble-out);
    cursor: pointer;
}

/* The corner that points at the sender, on the first of a run -- WhatsApp's
   tail, without drawing one. */
.msg--in:not(.msg--cont) .msg-bubble {
    border-top-left-radius: var(--radius-sm);
}

.msg--out:not(.msg--cont) .msg-bubble {
    border-top-right-radius: var(--radius-sm);
}

/* The time sits in the bubble's bottom-right corner, on the last line of the
   text if it fits there and on its own line if not. */
.msg-bubble::after {
    content: "";
    display: block;
    clear: both;
}

.msg-name {
    display: block;
    padding-top: var(--space-1);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--color-link);
}

.msg-text {
    padding-top: var(--space-1);
}

.msg-withdrawn {
    font-style: italic;
    color: var(--color-text-muted);
}

.msg-meta {
    float: right;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    margin: var(--space-2) 0 0 var(--space-3);
    font-size: var(--text-xs);
    line-height: 1;
    white-space: nowrap;
    color: var(--color-text-muted);
}

.msg-ticks {
    display: inline-flex;
    color: var(--color-text-muted);
}

.msg-ticks svg {
    width: 1.125rem;
    height: 0.75rem;
}

.msg-ticks.is-read {
    color: var(--color-tick-read);
}

.msg-photo {
    display: block;
    margin: var(--space-2) 0 0 calc(-1 * var(--space-2));
}

.msg-photo img {
    display: block;
    width: 100%;
    max-width: 18rem;
    border-radius: var(--radius-md);
}

.msg-file {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 2.75rem;
    margin-top: var(--space-2);
    padding: var(--space-2);
    border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--color-text) 6%, transparent);
    color: var(--color-text);
    text-decoration: none;
}

.msg-file-icon {
    flex: none;
    width: 2rem;
    height: 2.25rem;
    display: grid;
    place-items: center;
    border-radius: var(--radius-sm);
    background: var(--color-accent-surface);
    color: var(--color-on-dark);
    font-size: var(--text-xs);
    font-weight: 700;
}

.msg-file-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-sm);
}

.msg-file-size {
    display: block;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
}

/* Tap your own bubble: who has seen it, and the way to withdraw it. */
.msg-actions {
    display: none;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-1);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    box-shadow: var(--shadow-sm);
    font-size: var(--text-xs);
    color: var(--color-text-muted);
}

.msg.is-open .msg-actions {
    display: flex;
}

.msg-actions form {
    margin: 0;
}

/* ----- Composer ----- */
.chat-composer {
    position: sticky;
    bottom: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-2);
    margin: 0;
    padding: var(--space-2) var(--space-2) calc(var(--space-2) + env(safe-area-inset-bottom));
    background: var(--color-chat-bg);
}

.chat-attached,
.chat-error {
    flex-basis: 100%;
    margin: 0;
}

.chat-attached {
    padding: 0 var(--space-3);
    font-size: var(--text-xs);
    color: var(--color-text-muted);
}

.chat-attached[hidden],
.chat-error[hidden] {
    display: none;
}

.chat-icon-btn {
    position: relative;
    flex: none;
    width: 2.75rem;
    height: 2.75rem;
    display: grid;
    place-items: center;
    border: none;
    border-radius: var(--radius-circle);
    background: transparent;
    color: var(--color-text-muted);
    cursor: pointer;
    transition: background var(--motion-state);
}

.chat-icon-btn:hover,
.chat-icon-btn:focus-within {
    background: var(--color-surface);
}

.chat-icon-btn svg {
    width: 1.375rem;
    height: 1.375rem;
}

/* The real file input, kept for the browser and screen readers, not for
   the eye: the paperclip is what you tap. */
.chat-file-input {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
}

.chat-input {
    flex: 1;
    min-width: 0;
    min-height: 2.75rem;
    max-height: 8rem;
    margin: 0;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    background: var(--color-bubble-in);
    color: var(--color-text);
    font: inherit;
    line-height: 1.4;
    resize: none;
}

.chat-input:focus {
    outline: none;
    border-color: var(--color-link);
    box-shadow: 0 0 0 var(--space-1) var(--color-focus-ring);
}

.chat-send {
    flex: none;
    width: 2.75rem;
    height: 2.75rem;
    display: grid;
    place-items: center;
    border: none;
    border-radius: var(--radius-circle);
    background: var(--color-accent-surface);
    color: var(--color-on-dark);
    cursor: pointer;
    transition: background var(--motion-state);
}

.chat-send:hover {
    background: var(--color-accent-surface-hover);
}

.chat-send:disabled {
    opacity: 0.5;
    cursor: default;
}

.chat-send svg {
    width: 1.25rem;
    height: 1.25rem;
}

/* ----- Shortcut chips and the people search -----
   "Coaches", "First aiders", "Everyone in U11s": one tap adds the lot. A chip
   whose people are all ticked reads as selected -- the selection rule from
   the contract: selected fill, accent border, weight. */
.chat-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

.chat-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 2.75rem;
    padding: 0 var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    background: var(--color-surface);
    color: var(--color-text);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--motion-state), border-color var(--motion-state);
}

.chat-chip:hover {
    border-color: var(--color-link);
}

.chat-chip[aria-pressed="true"] {
    background: var(--color-selected);
    border-color: var(--color-link);
    font-weight: 700;
}

.chat-chip:disabled {
    opacity: 0.5;
    cursor: default;
}

.chat-chip-count {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--color-text-muted);
}

/* A row's second line can be who somebody is -- "Coach · Parent of Oscar" --
   which wraps rather than being cut off, because the end of it is the part
   that tells you which Katie this is. */
.chat-row-name + .chat-row-line .chat-row-preview {
    white-space: normal;
}

/* ----- Info: a conversation's people and settings ----- */
.chat-info-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-5);
    text-align: center;
}

.chat-info-head h1,
.chat-info-head p {
    margin: 0;
}

/* On a phone the thread is the screen, edge to edge, the way an app is. */
@media (max-width: 640px) {
    .chat {
        margin: calc(-1 * var(--space-5)) calc(-1 * var(--space-4)) calc(-1 * var(--space-6));
        border: none;
        border-radius: 0;
    }

    .msg-bubble {
        max-width: 85%;
    }

    .chat-list {
        margin-left: calc(-1 * var(--space-4));
        margin-right: calc(-1 * var(--space-4));
        border-left: none;
        border-right: none;
        border-radius: 0;
    }
}
