/*
 * base.css — Knowbility AIR 2027 Registration System
 *
 * Element defaults and the shared components: page chrome (§6), buttons (§7),
 * the focus indicator (§8), and forms (§9). Consumes tokens.css; no color,
 * size, or spacing literal appears below unless DESIGN.md states it inline.
 * The filled buttons' label is --color-on-fill (white in light, near-black in
 * dark, §2.6); the only literals left are the badge preview's fixed panes.
 *
 * Mobile-first. Breakpoints are em-based per DESIGN.md §5, so they respond to
 * browser zoom as well as viewport width — which is what carries reflow at
 * 400% (SC 1.4.10), not just narrow phones. Custom properties cannot be used
 * in a media query, so the values are repeated literally:
 *
 *   sm  40em / 640px   paired fields side by side
 *   md  48em / 768px   form grid goes label-left
 *   lg  64em / 1024px  full admin tables
 *
 *   One more width, used only by the admin header: below 37.5em / 600px it
 *   collapses behind a menu button. That width is not in this file. It is
 *   the `media` of the admin layout's link to admin-menu.css.
 */

/* ---------------------------------------------------------------- document */

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

/*
 * No root font size: 100% of the user's browser preference is the point of
 * the rem scale (§4.2).
 */
html {
    -webkit-text-size-adjust: 100%;
}

/* ------------------------------------------------------------------ fonts */

/*
 * DESIGN.md §4.1: self-hosted Atkinson Hyperlegible, SIL Open Font License
 * (public/fonts/OFL.txt). swap shows text in the fallback at once and never
 * blocks first paint; the URLs must match the layouts' preload exactly.
 */
@font-face {
    font-family: "Atkinson Hyperlegible Next";
    src: url("/fonts/AtkinsonHyperlegibleNext-Regular.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Atkinson Hyperlegible Next";
    src: url("/fonts/AtkinsonHyperlegibleNext-SemiBold.woff2") format("woff2");
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Atkinson Hyperlegible Next";
    src: url("/fonts/AtkinsonHyperlegibleNext-Bold.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Atkinson Hyperlegible Next";
    src: url("/fonts/AtkinsonHyperlegibleNext-Italic.woff2") format("woff2");
    font-weight: 400;
    font-style: italic;
    font-display: swap;
}

@font-face {
    font-family: "Atkinson Hyperlegible Mono";
    src: url("/fonts/AtkinsonHyperlegibleMono-Regular.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Atkinson Hyperlegible Mono";
    src: url("/fonts/AtkinsonHyperlegibleMono-Bold.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/*
 * The honeypot's wrapper relies on the `hidden` attribute to leave the
 * accessibility tree and the tab order together (PRD §Phase 2), and a later
 * display rule on any of its classes would silently defeat that. Pinning it
 * here means unhiding a honeypot has to be deliberate.
 */
[hidden] {
    display: none !important;
}

body {
    margin: 0;
    background-color: var(--color-surface);
    color: var(--color-text);
    font-family: var(--font-sans);
    font-size: var(--text-body);
    font-weight: var(--weight-regular);
    line-height: var(--leading-body);
}

/*
 * Sprint 29 spec §5: the admin dashboard and judging portal keep 16px body
 * text for dense tables (both layouts render body.admin); they share the
 * 1.6 line height above.
 */
body.admin {
    font-size: var(--text-base);
}

/*
 * Readable line length on public and registrant pages. Forms, tables and
 * record views keep the layout DESIGN.md §9 gives them.
 */
body:not(.admin) .page-main :is(p, li):not(form *, table *, dl *) {
    max-width: 65ch;
}

/*
 * §4.3 / SC 1.4.12: text spacing overrides must not clip or overlap, so no
 * fixed heights on text containers and no overflow:hidden on labels, buttons,
 * or badges anywhere in this file.
 */

/* ------------------------------------------------------------- typography */

h1,
h2,
h3,
h4,
h5,
h6 {
    margin-block: 0 var(--space-4);
    font-weight: var(--weight-bold);
}

/* §4.3: h1/h3/h5 render in brand purple, h2/h4/h6 in body text color. */
h1,
h3,
h5 {
    color: var(--color-brand);
}

h2,
h4,
h6 {
    color: var(--color-text);
}

/*
 * §4.2: admin caps at --text-3xl; the 2.5–4.5rem display scale is public-only.
 * The admin ceiling is the default so a new screen cannot drift into display
 * type by omission.
 */
h1 {
    font-size: var(--text-3xl);
    line-height: var(--leading-3xl);
}

h2 {
    font-size: var(--text-2xl);
    line-height: var(--leading-2xl);
}

h3 {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
}

h4,
h5,
h6 {
    font-size: var(--text-base);
    line-height: var(--leading-base);
}

.page--public h1 {
    font-size: var(--text-display);
    line-height: var(--leading-display);
}

p {
    margin-block: 0 var(--space-4);
    /* Long words and URLs must not force a horizontal scrollbar at 320px. */
    overflow-wrap: break-word;
}

.lead {
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
}

/* Public body text is --text-body (18px) since Sprint 29, so a public lead
   steps up to 20px to stay above it (Jeff, 9 Oct 2026). */
body:not(.admin) .lead {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
}

.text-meta {
    font-size: var(--text-xs);
    line-height: var(--leading-xs);
    color: var(--color-text-secondary);
}

code,
kbd,
samp,
pre,
.data-field {
    font-family: var(--font-mono);
}

a {
    color: var(--color-action);
    text-decoration: underline;
}

a:hover {
    color: var(--color-action-dark);
}

img,
svg {
    max-inline-size: 100%;
    block-size: auto;
}

hr {
    border: 0;
    border-block-start: 1px solid var(--color-rule);
    margin-block: var(--space-8);
}

/* ------------------------------------------------------------------ layout */

/*
 * §5 comfortable density: the public flow is one column at 48rem. Admin is
 * the compact mode and opts out with .l-container--wide.
 *
 * The modifier was defined and documented when the shell was built and then
 * used nowhere, so every admin screen ran at the public flow's reading
 * measure -- 736px of content on a 1280px window, with a seven-column
 * payments table scrolling inside it while 512px sat empty either side
 * (found 29 Aug 2026). Applying it is what §5 asked for all along.
 *
 * It caps at 64rem rather than removing the cap. §5 said "full width", which
 * `max-inline-size: none` takes literally: the same table stretched across
 * an ultrawide is as hard to read across as one that scrolls, and the header,
 * scope row and footer share this class, so an uncapped shell would spread
 * the nav to the screen edges. Jeff's call, 29 Aug 2026.
 */
.l-container {
    inline-size: 100%;
    max-inline-size: var(--measure-comfortable);
    margin-inline: auto;
    padding-inline: var(--space-4);
}

.l-container--wide {
    max-inline-size: var(--measure-compact);
}

.page-main {
    padding-block: var(--space-8) var(--space-12);
}

/* --------------------------------------------------------------- skip link */

/*
 * §6.2: present on every page and the first focusable element on it. Parked
 * above the viewport rather than display:none — hidden content is not
 * focusable, and a skip link that cannot be focused is not a skip link.
 * Nothing here is transitioned, so it also behaves under reduced motion.
 */
.skip-link {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    z-index: 100;
    transform: translateY(-100%);
    padding: var(--space-3) var(--space-4);
    background-color: var(--color-surface);
    color: var(--color-action-dark);
    font-weight: var(--weight-bold);
    text-decoration: underline;
    border-radius: 0 0 var(--radius) 0;
}

.skip-link:focus {
    transform: translateY(0);
}

/* ------------------------------------------------------------ page chrome */

/*
 * §6.1 header: wordmark upper left, horizontal nav, one high-emphasis slot
 * sitting apart from the nav. Not sticky this phase — §10.1 makes the admin
 * header sticky in Phase 6, and that is the point at which focus targets need
 * scroll-margin-top so the ring is never obscured (§8.1 / SC 2.4.11).
 */
.site-header {
    /* The drawing's height; the admin menu's offset is worked out from it. */
    --logo-size: 2.5rem;
    border-block-end: 1px solid var(--color-rule);
    padding-block: var(--space-4);
}

.site-header__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    /* The same gap as the form grid (§9.1), per Jeff, 26 Aug 2026. */
    gap: var(--space-4);
}

/*
 * The logo link (templates/partials/knowbility-logo.php). The link is the
 * full target height (§8.2) and the drawing sits inside it a little smaller.
 * The path takes the link's colour, so the brand token's dark value and the
 * forced-colours link colour both reach it.
 */
.site-header__wordmark {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target-min);
    color: var(--color-brand);
}

.site-header__wordmark svg {
    display: block;
    block-size: var(--logo-size);
    inline-size: auto;
    fill: currentColor;
}

.site-header__wordmark:hover {
    color: var(--color-brand-dark);
}

.site-nav ul {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* §8.2: nav items are not prose links, so they take the full target. */
.site-nav a {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target-min);
    padding-inline: var(--space-1);
}

/*
 * §10.1: the active section carries a 3px underline paired with
 * aria-current="page", so the indicator is not color alone and survives
 * High Contrast Mode.
 */
.site-nav a[aria-current="page"] {
    border-block-end: 3px solid var(--color-action);
    text-decoration: none;
}

/*
 * §10.1 admin chrome. The header is sticky here and only here, which is why
 * focus targets get scroll-margin-top: SC 2.4.11 is about the ring never
 * being parked underneath it.
 */
.site-header--admin {
    position: sticky;
    inset-block-start: 0;
    z-index: 50;
    background-color: var(--color-surface);
}

/*
 * The menu lines up with the word in the logo, not with the logo's middle
 * (Jeff, 10 Oct 2026). The word sits in the lower half of the drawing, under
 * the tree, so the row is laid out from the top and the menu is pushed down
 * until its text shares the word's baseline; the offset is a fraction of the
 * drawing's height and follows it. One more --space-4 parts the menu from
 * the logo. (The identity, theme and Sign out controls used to centre in
 * this row; since 11 Oct 2026 they are in the account strip above it.)
 *
 * Only from 48em, where the menu sits beside the logo. Narrower, it wraps
 * to a row of its own, and there an indent and an offset would only
 * misalign it with the logo above.
 */
@media (min-width: 48em) {
    .site-header--admin .site-header__inner {
        align-items: flex-start;
    }

    .site-header--admin .site-nav--admin {
        /* 0.27: measured in Chromium, the two baselines within a tenth of a pixel. */
        margin-block-start: calc(var(--logo-size) * 0.27);
        margin-inline-start: var(--space-4);
    }
}

body.admin :is(a, button, input, select, textarea, summary, [tabindex]) {
    scroll-margin-block-start: var(--admin-header-allowance);
}

/*
 * AccessU and Events, the two placeholders PRD §Phase 6 keeps for the future
 * platform consolidation. Plain text at 6.0:1 — never a disabled control,
 * which would promise something that does not exist, and never a dead link.
 */
.site-nav__inert {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target-min);
    padding-inline: var(--space-1);
    color: var(--color-text-secondary);
}

/*
 * The AIR and Core dropdowns are native <details>/<summary>: keyboard
 * operable, announced as a disclosure, and open to a visitor with no
 * JavaScript. Only the marker and the panel need styling.
 */
/*
 * Home and the group summaries are one row of tabs (Jeff's spec, 26 Aug).
 * For the Home <a>, `.site-nav a` (0,1,1) out-ranks this rule (0,1,0), so
 * these four shared layout declarations are only live on <summary> here --
 * change them in `.site-nav a` too if the two ever need to diverge.
 */
.site-nav__tab {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target-min);
    padding-inline: var(--space-1);
    color: var(--color-action);
    font-weight: var(--weight-bold);
    text-decoration: none;
    cursor: pointer;
}

.site-nav__tab:hover {
    color: var(--color-action-dark);
    text-decoration: underline;
}

/*
 * The panel. Padding is --space-1 and items sit on their target with no
 * extra block padding, so the AIR tree fits on a short screen; whatever
 * still does not fit scrolls. --admin-header-allowance is the sticky
 * header's allowance, the same token body.admin uses for scroll-margin.
 *
 * Out of flow at EVERY width. Until 27 Aug 2026 it was only positioned past
 * 48em and sat in flow below that, where `.site-nav ul` is a wrapping flex
 * row and the panel is inside one of its items: opening AIR made that item
 * ~350px tall, so AccessU was stranded at the end of the first line and
 * Events and Core wrapped underneath the open panel. The tab row came apart
 * around it. Taking the panel out of flow leaves the row alone, open or
 * shut.
 *
 * The containing block differs by width, and `inset-inline: 0` is written
 * to suit the narrow one:
 *
 *   narrow -- `.site-nav--admin` below, so the panel spans the nav and
 *             drops beneath the whole wrapped tab row. Anchoring to the tab
 *             here would let a tab sitting late in the row throw the panel
 *             off the right edge of a 375px screen.
 *   >=48em -- `.site-nav__group` takes over as the nearer positioned
 *             ancestor, so the panel drops under its own tab instead.
 */
.site-nav--admin {
    position: relative;
}

.site-nav__group > ul {
    position: absolute;
    z-index: 60;
    inset-block-start: 100%;
    inset-inline: 0;
    display: block;
    max-block-size: calc(100dvh - var(--admin-header-allowance));
    padding: var(--space-1);
    overflow-y: auto;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius);
    background-color: var(--color-surface);
}

.site-nav__group > ul > li {
    min-inline-size: 13rem;
}

.site-nav__group > ul a {
    padding-block: 0;
    line-height: 1.3;
}

/*
 * Denser dropdown rows (Jeff's call, 27 Aug 2026).
 *
 * The rows already sit flush -- the 8px row gap went when the panel stopped
 * being a grid -- so the only spacing left to take is the target box itself.
 * 44px is SC 2.5.5 Target Size (Enhanced), which is AAA, and the rule this
 * project builds to. The AA floor is SC 2.5.8 at 24px, so 36px still clears
 * the conformance target with 12px to spare; what it gives up is the AAA
 * margin.
 *
 * Gated on `pointer: fine`, because that margin is worth most to a finger
 * and nothing to a mouse. A touch device keeps the full 44px at every width
 * -- including a tablet past 48em, which is why this asks about the pointer
 * rather than about the viewport. Raise --nav-row-fine to --target-min to
 * put the AAA size back everywhere.
 */
@media (min-width: 48em) and (pointer: fine) {
    .site-nav__group > ul {
        --nav-row-fine: 2.25rem;   /* 36px */
    }

    .site-nav__group > ul :is(a, .site-nav__heading, .site-nav__toggle) {
        min-block-size: var(--nav-row-fine);
    }

    /* The toggle is sized, not min-sized, and stays square. */
    .site-nav__group > ul .site-nav__toggle {
        inline-size: var(--nav-row-fine);
        block-size: var(--nav-row-fine);
    }
}

.site-nav__heading {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target-min);
    padding-inline: var(--space-1);
    font-weight: var(--weight-bold);
}

/* Named on the `ul` (not `.site-nav__sub` alone) so this out-ranks
   `.site-nav ul` (0,1,1) on specificity -- otherwise the sub-list's own
   `display: block` loses and grandchildren render in a flex row. */
.site-nav ul.site-nav__sub {
    display: block;
    margin: 0;
    padding-inline-start: var(--space-4);
    list-style: none;
}

/*
 * The level-3 disclosure control. Sits beside its label -- the label is a
 * link or a heading and keeps that job -- so this button owns nothing but
 * the open/closed state. Icon-only at the 44px target (§8.2); the name
 * comes from the .visually-hidden span inside it, and the arrow is
 * decorative. `hidden` in the markup until admin-nav.js reveals it.
 */
.site-nav__toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--target-min);
    block-size: var(--target-min);
    padding: 0;
    border: none;
    background: transparent;
    color: var(--color-action);
    cursor: pointer;
}

/*
 * A CSS triangle rather than a glyph: "\25B8" renders as an emoji on some
 * Android builds and as tofu on a few Linux font stacks, and neither can be
 * recoloured to keep its 3:1 non-text contrast (SC 1.4.11). currentColor
 * inherits the button's colour, so the ratio holds in both themes and in
 * High Contrast Mode.
 */
.site-nav__arrow {
    inline-size: 0;
    block-size: 0;
    border-block: 0.3em solid transparent;
    border-inline-start: 0.4em solid currentColor;
}

/* Rotates to point down when the list below it is open. A transform, so
   nothing reflows; guarded because a rotation is motion to a vestibular
   user even at this size. */
@media (prefers-reduced-motion: no-preference) {
    .site-nav__toggle .site-nav__arrow {
        transition: rotate 150ms ease;
    }
}

.site-nav__toggle[aria-expanded="true"] .site-nav__arrow {
    rotate: 90deg;
}

/* The label and its toggle share a row. */
.site-nav__group > ul > li > :is(a, .site-nav__heading) {
    vertical-align: middle;
}

@media (min-width: 48em) {
    /* One vertical column (Jeff's call, 27 Aug 2026).
     *
     * This was tried and reverted once the same day, for two reasons that
     * have both since been dealt with, so the reasoning is worth keeping:
     *
     * 1. Height. Fifteen AIR rows stacked ran 550px, past the fold on a
     *    short screen. AdminNav now folds the eight participant lists under
     *    a "Registrations" heading, so the column is eight rows, not
     *    fifteen.
     * 2. Reach. Anchored to the sticky header, a ~234px column sat far to
     *    the left of whichever tab opened it, and the trip across left the
     *    <li> and started the close timer. `.site-nav__group` is the
     *    containing block now, so the panel drops flush under its own tab.
     *
     * `display: block` rather than a one-column grid on purpose:
     * `.site-nav ul` sets `gap: var(--space-2) var(--space-4)`, and that 8px
     * row gap is only live while the panel is flex or grid. Block makes it
     * inert, which is the whole of "less spacing between items" -- the rows
     * close up while each keeps its target height. Nothing shrinks here;
     * only the gaps go.
     */
    /* Nearer than `.site-nav--admin`, so from here the panel resolves
       against its own tab rather than against the whole nav. */
    .site-nav__group {
        position: relative;
    }

    .site-nav__group > ul {
        /* Flush under the tab that opened it, so the pointer drops straight
           down onto it without leaving the <li> that owns both. The narrow
           rule stretches the panel with `inset-inline: 0`; here it is only
           pinned on the start side and takes its own width. */
        inset-inline-end: auto;
        inline-size: max-content;

        /* Against the viewport, not the tab: 100% would resolve against the
           <details>, which is only as wide as its summary, and would squash
           the panel to the width of the word "AIR". */
        max-inline-size: calc(100vw - 2 * var(--space-4));
    }

    /* Containing block for the flyout. */
    .site-nav__group > ul > li {
        position: relative;
    }

    /*
     * EVERYTHING BELOW IS SCOPED TO `.site-nav--enhanced`, the class
     * admin-nav.js adds once it has collapsed the level-3 lists.
     *
     * That scoping is load-bearing, not tidiness. The lists ship visible so
     * the nav works without JavaScript; if the flyout positioning applied
     * unconditionally, a browser with script off would paint every one of
     * them absolutely positioned at once, on top of each other -- strictly
     * worse than the in-flow indented list they replaced. Unscoped, the
     * no-JS fallback the markup carefully preserves would be undone here.
     */

    /* The scroll cap this panel carries existed because every level-3 list
       was stacked open, which made the AIR panel taller than a short
       viewport. Collapsed, and with the participant lists grouped under
       "Registrations", AIR is eight rows and ~300px -- so the cap has
       stopped earning its keep, and it has to go regardless: `overflow-y:
       auto` establishes a clipping box that would cut the flyouts off at
       the panel edge. Without script the lists are still stacked, so the
       cap is still right and stays.

       The trade this makes: past 48em the panel itself no longer scrolls,
       so a window shorter than roughly 360px would put its last rows out
       of reach. Flyouts are pulled up to fit (fitOnScreen in admin-nav.js);
       the panel is not. Worth revisiting if AIR grows well past eight. */
    .site-nav--enhanced .site-nav__group > ul {
        max-block-size: none;
        overflow: visible;
    }

    /*
     * Level 3 opens to the side rather than pushing the column down, so
     * the panel keeps its height no matter which parent is open. Its own
     * scroll cap, because one list (Lookup management, 7 rows) can still
     * out-run a short viewport where the panel itself cannot.
     */
    .site-nav--enhanced ul.site-nav__sub {
        position: absolute;
        z-index: 61;
        inset-block-start: 0;

        /* 100% is the row's own edge, which sits inside the panel's 4px
           padding and 1px border -- flush against that would drop the
           flyout's border on top of the panel's. Clearing both puts the two
           edge to edge. The pointer crosses those 5px on its way out to the
           flyout, which briefly leaves the <li> and schedules the close;
           CLOSE_DELAY_MS in admin-nav.js is what absorbs the trip. */
        inset-inline-start: calc(100% + var(--space-1) + 1px);
        min-inline-size: 13rem;
        max-block-size: calc(100dvh - var(--admin-header-allowance));
        padding: var(--space-1);
        overflow-y: auto;
        border: 1px solid var(--color-rule);
        border-radius: var(--radius);
        background-color: var(--color-surface);
    }

    /*
     * No inward flip for the flyouts. There was an `li:nth-child(3n)` rule
     * here while the panel was a three-column grid, where `3n` picked out
     * the third column, whose flyout would otherwise have opened past the
     * right edge of the viewport.
     *
     * It has no meaning in one column -- `3n` is then every third ROW, and
     * it sent Judging and Lookup management off the left edge of the screen
     * when this layout was first tried. A single ~234px column leaves the
     * rest of the page to its right, so nothing needs to flip. Bring the
     * rule back only alongside a multi-column panel.
     */
}

/*
 * §10.1: the active section carries a 3px underline paired with
 * aria-current="page" — an indicator that is not colour alone, so it survives
 * High Contrast Mode. Same rule as the public nav's link; a <summary> needs
 * it stated separately because it is not an <a>.
 */
.site-nav .is-current {
    border-block-end: 3px solid var(--color-action);
    text-decoration: none;
}

/* The slot §6.1 gives the CTA on public pages and the admin identity on admin
   pages. Pushed apart from the nav rather than appended to it. */
.site-header__aside {
    margin-inline-start: auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4);
}

/*
 * The account strip above the admin and judging header (§6.1, §10.1; Jeff,
 * 11 Oct 2026): name and role, the theme button, Sign out, at the right
 * edge at every width. It is not sticky and not inside the header, so it
 * scrolls away and the stuck part of a phone screen is one line. The rule
 * under it is the one the header draws under itself.
 */
.site-account {
    border-block-end: 1px solid var(--color-rule);
    padding-block: var(--space-2);
    background-color: var(--color-surface);
}

.site-account__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-4);
}

/*
 * A long name and role wrap inside the strip. The zero basis is what keeps
 * the two controls on the name's row: the name takes what is left of the
 * line and wraps inside it, where an `auto` basis sent Sign out to a row
 * of its own at 375px. `anywhere` breaks one long word before it could
 * push the row wider than a 320px screen. Nothing is cut off.
 */
.site-account__identity {
    flex: 1 1 0;
    min-inline-size: 0;
    overflow-wrap: anywhere;
    text-align: end;
}

/*
 * The small-screen menu button is drawn only by admin-menu.css, which the
 * admin layout loads for widths below 37.5em. Here it is off: once the
 * script has taken the `hidden` attribute away, this rule is what keeps
 * the button out of the wider header.
 */
.site-menu-button {
    display: none;
}

/*
 * §2.6 / §6.1: the light-dark control at the right of the public header. A
 * toggle button -- aria-pressed says whether dark is on -- with a moon
 * while dark is off and a sun while it is on, both decorative; the
 * visually-hidden text carries the name. Hidden until theme-toggle.js
 * reveals it, since without script there is nothing it could do; the OS
 * preference still applies. 44px target (§8.2), icon-only, no motion.
 */
.theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--target-min);
    block-size: var(--target-min);
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background-color: var(--color-surface);
    color: var(--color-text);
    cursor: pointer;
}

.theme-toggle:hover {
    background-color: var(--color-surface-subtle);
}

.theme-toggle__icon {
    inline-size: var(--space-6);
    block-size: var(--space-6);
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.theme-toggle[aria-pressed="true"] .theme-toggle__icon--moon,
.theme-toggle[aria-pressed="false"] .theme-toggle__icon--sun {
    display: none;
}

/* §6.3 */
.site-footer {
    border-block-start: 1px solid var(--color-rule);
    margin-block-start: var(--space-12);
    padding-block: var(--space-8);
}

.site-footer a {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target-min);
}

/*
 * §6.3 (26 Aug 2026): the public footer is the legal line alone, and that
 * line is the link back to knowbility.org. No top margin any more -- the
 * columns it used to sit under are gone.
 */
.site-footer__legal {
    margin: 0;
    font-size: var(--text-xs);
    line-height: var(--leading-xs);
    color: var(--color-text-secondary);
}

/*
 * §6.3 (30 Aug 2026): the admin footer repeats the header's theme toggle and
 * Sign out, right-aligned, so an admin at the bottom of a long list can act
 * without scrolling back up (Jeff).
 *
 * Only the admin footer renders `__inner` and `__aside`, so the public and
 * judging footers keep the single centred legal line they had -- the classes
 * are the scope, rather than a `--admin` qualifier on every rule.
 *
 * `flex-wrap` and the `auto` margin rather than `space-between`, and the
 * difference shows at 320px: `space-between` would strand the legal line and
 * the controls at opposite edges of a row too narrow for both. The margin
 * lets them wrap onto their own line instead, where it keeps them right --
 * measured at 321px, aside at 136..290 against a content box ending at 290.
 * Right rather than left when wrapped is deliberate and matches
 * `.site-account__inner`, so the controls sit under the same edge at every
 * width, which is the point of repeating them.
 */
.site-footer__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4);
}

.site-footer__aside {
    margin-inline-start: auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4);
}

/* ------------------------------------------------- public: sponsor band */

/*
 * Spec 2026-09-05 §3. Tier sizes are classes because the CSP allows no
 * inline style. Height steps down by tier; width follows the aspect ratio
 * under a per-tier cap. Tiers 2-5 were raised 0.5rem on 6 Sep 2026 (Jeff);
 * the 3.25rem floor stays above --target-min, so the smallest linked logo
 * is still a 44px target.
 */
.sponsor-band {
    border-block-start: 1px solid var(--color-rule);
    margin-block-start: var(--space-12);
    padding-block: var(--space-8);
}

.sponsor-band h2 {
    text-align: center;
    margin-block: 0 var(--space-6);
}

.sponsor-tier {
    margin-block-end: var(--space-8);
}

.sponsor-tier h3 {
    text-align: center;
    font-size: var(--text-lg);
    margin-block: 0 var(--space-4);
}

/*
 * Rows of at most three, every row centred (Jeff, 5 Sep 2026). Each logo
 * sits in a cell exactly one third of the row minus the gaps, so a fourth
 * logo always wraps, and `justify-content: center` centres a last row of
 * one or two. The image is centred inside its cell and scales down by
 * aspect ratio under the tier's height cap -- `max-block-size`, not
 * `block-size`, so a cell narrower than the logo shrinks it rather than
 * letting it overflow. Under 40rem a cell is the whole row: three tier-1
 * logos side by side at 320px would be about 80px each.
 */
.sponsor-tier__logos {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: var(--space-8);
}

.sponsor-tier__logo {
    flex: 0 0 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

@media (min-width: 40rem) {
    .sponsor-tier__logo {
        flex-basis: calc((100% - 2 * var(--space-8)) / 3);
    }
}

.sponsor-tier__logo a {
    display: inline-flex;
    max-inline-size: 100%;
}

.sponsor-tier__logo img {
    display: block;
    inline-size: auto;
    block-size: auto;
    max-inline-size: 100%;
    object-fit: contain;
    background: #ffffff;
    padding: var(--space-3);
    border-radius: var(--radius);
}

/*
 * A sponsor with no logo yet shows as its name, on its own centred line(s)
 * under the tier's logos, "Acme Co · Bright Labs · Cedar Inc". It stays a
 * real list so a screen reader announces the count. The dots are generated
 * content with an empty alternative (`/ ""`), so they are never read; the
 * first declaration is the fallback for a browser without that syntax, where
 * the dot is read aloud rather than lost. A dot trails its name (not leads
 * the next one) so a wrapped line never starts with a dot. A linked name
 * keeps the global underline and focus ring, and is at least a 44px target
 * (`--target-min`) with the item padding and gap between neighbours; names wrap inside
 * their item, so nothing overflows at 320px.
 */
.sponsor-tier__names {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-4) 0 0;
    padding: 0;
    list-style: none;
    color: var(--color-text);
    text-align: center;
}

.sponsor-tier__names li {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target-min);
    max-inline-size: 100%;
    padding-inline: var(--space-2);
    overflow-wrap: anywhere;
}

.sponsor-tier__names li:not(:last-child)::after {
    content: "\00b7";
    content: "\00b7" / "";
    margin-inline-start: var(--space-2);
}

.sponsor-tier__names a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-block-size: var(--target-min);
    min-inline-size: var(--target-min);
}

.sponsor-tier--1 img { max-block-size: 7rem; }
.sponsor-tier--2 img { max-block-size: 6rem; }
.sponsor-tier--3 img { max-block-size: 4.75rem; }
.sponsor-tier--4 img { max-block-size: 4rem; }
.sponsor-tier--5 img { max-block-size: 3.25rem; }

/* ------------------------------------------------------- home: event list */

.event-list {
    display: grid;
    gap: var(--space-4);
    margin: 0 0 var(--space-8);
    padding: 0;
    list-style: none;
}

.event-list__item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2) var(--space-4);
}

.event-list__link {
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
    font-weight: var(--weight-bold);
}

.event-list__name {
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
    font-weight: var(--weight-bold);
}

.event-list__window {
    color: var(--color-text-secondary);
}

/* ----------------------------------------------------------------- buttons */

/*
 * §7. All variants: 0.25rem radius, --font-sans, 16px minimum label, and the
 * §8.2 target size. There are no disabled buttons in this system — §7 requires
 * the control stay focusable and the reason be carried as text instead.
 */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-block-size: var(--target-min);
    min-inline-size: var(--target-min);
    padding: var(--space-2) var(--space-6);
    border: 1px solid transparent;
    border-radius: var(--radius);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-base);
    font-weight: var(--weight-bold);
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--motion-duration) var(--motion-easing);
}

.btn--primary {
    background-color: var(--color-action);
    color: var(--color-on-fill);
}

.btn--primary:hover {
    background-color: var(--color-action-dark);
    color: var(--color-on-fill);
}

/* §7: the one "Register" call to action, squared off at the top like the
   parent site's Donate button. One per page. */
.btn--cta {
    background-color: var(--color-brand);
    color: var(--color-on-fill);
    border-radius: 0 0 var(--radius) var(--radius);
}

.btn--cta:hover {
    background-color: var(--color-brand-dark);
    color: var(--color-on-fill);
}

.btn--destructive {
    background-color: var(--color-danger);
    color: var(--color-on-fill);
}

.btn--destructive:hover {
    background-color: var(--color-danger-dark);
    color: var(--color-on-fill);
}

/*
 * §7 gives tertiary an #f2f2f3 fill, which is 1.05:1 against a white page —
 * on its own that is not a perceivable control boundary (SC 1.4.11 wants
 * 3:1). The --color-border rule at 6.0:1 is what identifies the button; the
 * fill only tints it.
 */
.btn--tertiary {
    background-color: var(--color-surface-subtle);
    border-color: var(--color-border);
    color: var(--color-text);
}

.btn--tertiary:hover {
    background-color: var(--color-surface-sunken);
}

.btn--link {
    background-color: transparent;
    padding-inline: var(--space-2);
    color: var(--color-action);
    text-decoration: none;
}

.btn--link:hover {
    color: var(--color-action-dark);
    text-decoration: underline;
}

/* ------------------------------------------------------------------- focus */

/*
 * §8.1, the dual-ring indicator, reused verbatim from knowbility.org and
 * applied to every interactive element.
 *
 * Both rings are load-bearing; this is not decoration and must not be
 * simplified to "just the blue outline". A focus indicator has to clear 3:1
 * against whatever sits behind it (SC 1.4.11), and the blue ring alone is
 * invisible against exactly the buttons users press most — 1.0:1 on the
 * primary blue fill, 1.0:1 on destructive red, 1.7:1 on the purple CTA. The
 * white inner ring carries the indicator there (5.3:1 / 5.2:1 / 9.0:1); the
 * blue outer ring carries it on a white page. Dropping either introduces
 * failures on specific components.
 *
 * The outline is the fallback for contexts that lose box-shadows, forced-
 * colors mode above all, where it is the half that survives.
 */
:focus-visible {
    box-shadow:
        0 0 0 0.0825rem var(--color-focus-inner),
        0 0 0.0625rem 0.225rem var(--color-focus);
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

/* §8.1: browser default rings are not used anywhere. */
:focus:not(:focus-visible) {
    outline: none;
}

/* -------------------------------------------------------------- feedback */

/* §8.3: never color alone — the glyph and the text carry the state. */
.feedback-error {
    color: var(--color-danger);
}

.feedback-success {
    color: var(--color-success);
}

/* ------------------------------------------------------------------- forms */

/*
 * §9.1: two-column, label left, input right at md and up; single column with
 * the label above the input below that. The label track is content-sized with
 * a max and never a fixed px width — Atkinson Hyperlegible and its system
 * fallbacks set wider than a narrow face (§4.1), and a fixed column would
 * truncate labels at larger text sizes.
 *
 * The whole form is ONE grid. Each `.form-row` is `display: contents` at md,
 * so its label and input are cells of the form's grid and every label shares
 * the same content-sized track — a row-level grid would size its own track
 * and the inputs would come out ragged. Row and column gaps are equal at md;
 * below it the gap between fields is taller than the gap inside one, so the
 * label-above-input pairs read as sets.
 */
.form-grid {
    display: grid;
    gap: var(--space-6);
}

.form-row {
    display: grid;
    gap: var(--space-2);
}

@media (min-width: 48em) {
    .form-grid {
        grid-template-columns: fit-content(16rem) minmax(0, 1fr);
        gap: var(--space-4);
        align-items: start;
        /*
         * §9.1 sizes the two columns but never the form, because until
         * 29 Aug 2026 the container did it: at 48rem the input column came
         * out around 500px whatever the screen. Admin's move to the compact
         * 64rem measure took that with it and put a four-digit "Edition
         * year" in an 875px box. A form keeps the comfortable measure even
         * inside a wide shell -- §5's two densities are about the content,
         * and a form is reading-width content wherever it sits. Changes
         * nothing in the public flow, whose container is this width already.
         */
        max-inline-size: var(--measure-comfortable);
    }

    .form-row {
        display: contents;
    }

    /*
     * The <form> element is the grid, so everything in it that is not a
     * field — section headings, lead paragraphs, the required-fields note,
     * the error summary, the actions, a role's permission matrix — spans
     * both columns. Declared before
     * the tip and error rules below so those win for the paragraphs they
     * name.
     */
    .form-grid > :is(h2, h3, p, .form-note, .form-error-summary, .form-actions, .data-table__scroll) {
        grid-column: 1 / -1;
    }

    /* Tips and errors that follow a choice group sit under the value column. */
    .form-grid > .form-tip,
    .form-grid > .form-error {
        grid-column: 2;
        margin-block-start: 0;
    }
}

/* A section heading inside the grid separates from the fields above it. */
.form-grid > :is(h2, h3) {
    margin-block: var(--space-4) 0;
}

/*
 * One column at every width: each label sits above its answer, and radio
 * and checkbox options start at the same left edge as the text boxes (Jeff,
 * 23 Sep 2026, for the Web Planning questionnaire). Its long questions
 * read as a column; in the two-column grid, right-aligned labels beside
 * boxes alternated with full-width option groups, so the answers shifted
 * left and right. A deliberate exception to §9.1 for this form only: it is
 * the base, below-md layout of `.form-grid`, kept at every width, so the
 * md rules that dissolve rows and right-align labels are undone here.
 */
.form-stack {
    display: grid;
    gap: var(--space-6);
    max-inline-size: var(--measure-comfortable);
}

.form-stack > :is(h2, h3) {
    margin-block: var(--space-4) 0;
}

.form-stack .choice-list,
.form-stack .form-fieldset {
    margin-block-end: 0;
}

/* A field after a box's options (the inventory's details box) starts clear
   of the last option rather than touching it. */
.form-stack .form-fieldset > .form-row {
    margin-block-start: var(--space-4);
}

@media (min-width: 48em) {
    .form-stack .form-row {
        display: grid;
    }

    .form-stack .form-label {
        text-align: start;
        padding-block-start: 0;
    }
}

/* §9.3: required is the default; every form opens with this note. */
.form-note {
    margin-block-end: var(--space-6);
}

.form-label {
    font-weight: var(--weight-bold);
    /* §9.1: labels wrap rather than truncate. A clipped label is an
       unlabeled field — no overflow:hidden, no ellipsis, no nowrap. */
}

@media (min-width: 48em) {
    /*
     * §9.1: labels are right-aligned against the input column. The top
     * padding drops the label's first line level with the input's first
     * line — half of what the 44px minimum height adds over one line box.
     */
    .form-label {
        text-align: end;
        padding-block-start: calc((var(--target-min) - var(--leading-base) * var(--text-base)) / 2);
    }
}

.form-label__optional {
    font-weight: var(--weight-regular);
    color: var(--color-text-secondary);
}

/* §9.5 */
.form-input,
.form-select,
.form-textarea {
    inline-size: 100%;
    min-block-size: var(--target-min);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background-color: var(--color-surface);
    color: var(--color-text);
    font-family: var(--font-sans);
    /* 16px floor: below it, iOS Safari zooms on focus and the layout jumps
       under the user mid-form (§4.2). */
    font-size: var(--text-base);
    line-height: var(--leading-base);
}

/* §9.5: focus applies the dual ring and does not remove the border. */

/* §9.4: the failing input gains a 2px danger border in addition to the error
   text and its glyph — the border is never the only signal. */
.form-input--invalid,
.form-select--invalid,
.form-textarea--invalid {
    border-width: 2px;
    border-color: var(--color-danger);
}

/* §9.4: below each input, error first, then tip. */
.form-tip {
    margin-block-start: var(--space-2);
    font-size: var(--text-xs);
    line-height: var(--leading-xs);
    color: var(--color-text-secondary);
}

.form-error {
    margin-block-start: var(--space-2);
    color: var(--color-danger);
    font-weight: var(--weight-bold);
}

/* §9.4: the page-level summary uses the same treatment at --text-lg, with its
   heading in --color-text (which is already the h2 default). */
.form-error-summary {
    margin-block-end: var(--space-6);
    padding: var(--space-4);
    border: 2px solid var(--color-danger);
    border-radius: var(--radius);
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
    color: var(--color-danger);
}

.form-error-summary a {
    color: var(--color-danger);
}

/*
 * User testing (Jeff, 26 Sep 2026): the 32px gap above the action row
 * read as too much space between the last field and its buttons, so
 * the top margin comes down to the field-to-field rhythm (--space-4).
 * Nothing set space below the row before this -- the actions sat flush
 * against whatever came next -- so a matching --space-8 goes below
 * instead.
 */
.form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
    margin-block-start: var(--space-4);
    margin-block-end: var(--space-8);
}

/*
 * The registrations list's search: label, box, Search, and Clear search on
 * one line (Jeff, 21 Sep 2026). Two forms sit in it -- Clear posts on its
 * own -- so the bar and the search form are both flex rows with the same
 * gap, and the pair reads as one row of controls. The same inline shape as
 * `.event-scope` in the admin header.
 *
 * The box is capped rather than left to fill the measure: a name or an
 * address is short, and a full-width box is what made the area tall. It
 * still shrinks, and everything wraps, so at 320px the label, the box and
 * the buttons stack without a horizontal scroll. `--space-2` between
 * controls is the 8px that adjacent targets need (§8.2).
 */
.search-bar,
.search-bar__form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

/*
 * A definite size, not a flex-basis: `.form-input` is `inline-size: 100%`,
 * which counts for nothing when the form works out its own content width,
 * so with only a basis the form came out box-less and narrow and every
 * control wrapped onto a line of its own (measured, 21 Sep 2026).
 */
.search-bar__input {
    flex: 0 1 auto;
    inline-size: 24rem;
    max-inline-size: 100%;
}

/*
 * A flex item will not go narrower than its content unless told it may, and
 * the box's 24rem is content: at 375px the form held the box at 384px and the
 * page scrolled sideways (measured, 21 Sep 2026). With this the form follows
 * the bar, and the box's `max-inline-size: 100%` has something to be 100% of.
 */
.search-bar__form {
    min-inline-size: 0;
    max-inline-size: 100%;
}

/* `.form-label` pads its top to meet a grid row's box; here the row centres. */
.search-bar .form-label {
    padding-block-start: 0;
}

/* The tip belongs to the bar above it; the filters below are another form. */
.search-bar__tip {
    margin-block: var(--space-2) var(--space-8);
}

/*
 * A searchable lookup list's top row (Countries, Jeff, 2 Oct 2026): the
 * Add button on the left, the search on the right. It wraps, so at 320px
 * the search drops under Add instead of scrolling sideways; `--space-4`
 * keeps the two well past the 8px adjacent targets need (§8.2).
 */
.list-toolbar {
    display: flex;
    flex-wrap: wrap;
    /* Top, not centre: the search side carries its tip underneath, and
       Add belongs level with the box, not halfway down the tip. */
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    margin-block-end: var(--space-6);
}

.list-toolbar > p {
    margin-block: 0;
}

/* The search and its tip as one unit, so the tip stays under the box. */
.list-toolbar__search {
    min-inline-size: 0;
    max-inline-size: 100%;
}

.list-toolbar .search-bar__tip {
    margin-block-end: 0;
}

/* Short: a country's name or code is a word or two, not an address. */
.search-bar__input--short {
    inline-size: 14rem;
}

/*
 * The registrations list's filter form collapses behind a native
 * <details>/<summary> (Jeff, 26 Sep 2026, user testing): four dropdowns
 * pushed the row count below the fold on smaller screens, and most
 * visits change none of them. <details> needs no script for any of
 * this -- open, closed, keyboard operation, and the expanded/collapsed
 * state announced to a screen reader all come from the browser, the
 * same as the AIR and Core nav dropdowns above (`.site-nav__tab`).
 */
.filter-panel {
    margin-block-end: var(--space-4);
}

/*
 * Styled as a small button rather than the browser's plain disclosure
 * text. `display: inline-flex` drops the native marker triangle the
 * same way `.site-nav__tab` does; `::after` below draws a replacement
 * that rotates with `[open]`, the same border-trick triangle
 * `.site-nav__arrow` uses, but as generated content rather than a real
 * element -- there is nothing here for a screen reader to reach or an
 * `aria-hidden` to withhold, so the native `<summary>` announces only
 * what it always did (Jeff, 26 Sep 2026). The global `:focus-visible`
 * rule already rings the summary, so nothing here repeats that.
 */
.filter-panel__summary {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-block-size: var(--target-min);
    min-inline-size: var(--target-min);
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background-color: var(--color-surface-subtle);
    color: var(--color-text);
    font-weight: var(--weight-bold);
    cursor: pointer;
}

.filter-panel__summary:hover {
    background-color: var(--color-surface-sunken);
}

.filter-panel__summary::after {
    content: '';
    inline-size: 0;
    block-size: 0;
    border-block: 0.3em solid transparent;
    border-inline-start: 0.4em solid currentColor;
}

/* Rotates to point down when the panel below it is open. A transform, so
   nothing reflows; guarded because a rotation is motion to a vestibular
   user even at this size (same guard as `.site-nav__arrow`). */
@media (prefers-reduced-motion: no-preference) {
    .filter-panel__summary::after {
        transition: rotate 150ms ease;
    }
}

.filter-panel[open] .filter-panel__summary::after {
    rotate: 90deg;
}

.filter-panel[open] .filter-panel__summary {
    margin-block-end: var(--space-4);
}

/*
 * §10.1: the current item carries a 3px underline paired with
 * aria-current="page", so the indicator is not colour alone and survives High
 * Contrast Mode. The same device as `.site-nav a[aria-current="page"]`, stated
 * again for the link groups this class wraps — the team list's category
 * toggle is one, and it was setting the ARIA attribute with nothing visible
 * behind it (Task 10 live walk, W4).
 *
 * Deliberately keyed on the attribute rather than on a class of its own: any
 * `.form-actions` nav that marks its current link this way should look the
 * same, and the pagination navs next door never set it.
 */
.form-actions a[aria-current="page"] {
    border-block-end: 3px solid var(--color-action);
    text-decoration: none;
}

/*
 * §9.2: radio and checkbox groups are the one exception to the grid. The
 * control sits left with its text immediately adjacent, never pushed to the
 * far side of the row, and the whole pair is the target.
 *
 * The wrapper is a div, so it is not what the pointer hits; the label is. It
 * therefore stretches to the row's full height, carries the 44 x 44 minimum
 * itself (§8.2, WCAG 2.5.5 AAA), and owns the gap to the control as padding
 * so that gap is clickable too. The native control stays 24 x 24 -- the
 * label beside it is the equivalent target the SC asks for.
 *
 * A closed option is a `p.choice` (sponsorship, type), and a paragraph's
 * default 1em bottom margin gave it 16px more space than the div rows around
 * it below the md breakpoint, where it does not dissolve into the grid
 * (Jeff, 9 Sep 2026). The list gap is the only spacing between options.
 */
.choice {
    display: flex;
    align-items: center;
    min-block-size: var(--target-min);
    margin: 0;
}

.choice input {
    inline-size: var(--space-6);
    block-size: var(--space-6);
    margin: 0;
    flex: none;
}

.choice label {
    display: flex;
    align-items: center;
    align-self: stretch;
    flex: 1 1 auto;
    min-block-size: var(--target-min);
    min-inline-size: var(--target-min);
    padding-inline-start: var(--space-3);
    /* Paints above a following `.form-tip` that is pulled up into this box. */
    position: relative;
}

/*
 * A taken sponsorship level has no control, so its name would sit where
 * the radios are while its benefits below (`.choice-description`) indent
 * to the label text -- the benefits then read as detached from their
 * heading (Jeff, 4 Sep 2026). The control's width plus the label's gap
 * puts the name where every other level's name is, directly above its
 * own benefits. The md grid rule zeroes this again, because there the
 * label column does the placing.
 */
.choice > .type-closed__reason {
    padding-inline-start: calc(var(--space-6) + var(--space-3));
}

fieldset {
    margin: 0;
    padding: 0;
    border: 0;
    min-inline-size: 0;
}

/*
 * §9.2 at md: a choice inside the form grid is a "merged" row. The wrapper
 * and its list dissolve (`display: contents`), the control lands in the label
 * column flush against its right edge — where the text labels end — and the
 * option text takes the value column.
 *
 * A fieldset used to dissolve the same way, so its options kept the shared
 * label track. Since 1 Sep 2026 (Jeff) every fieldset draws a border, and
 * `display: contents` generates no box to draw it on — so at md a fieldset
 * is instead its OWN grid with the same two tracks, spanning both of the
 * form's columns. Rows and choices dissolve into it exactly as they did
 * into the form, and the legend spans it as a full-width row. The one
 * visible trade: each fieldset sizes its own label track, so tracks may
 * differ between boxes — acceptable, because the border now separates
 * them. (`subgrid` still cannot share the form's tracks: a fieldset lays
 * its children out in an anonymous box that is not itself a grid item, so
 * the subgrid never inherits them — measured 26 Aug 2026.)
 */
.form-grid .choice-list,
.form-grid .form-fieldset {
    /* The grid's gap is the spacing now; a trailing margin would stack on it. */
    margin-block-end: 0;
}

@media (min-width: 48em) {
    /*
     * Only choices that are grid items merge: direct children of the form
     * or of a (dissolved) fieldset, with or without a `.choice-list` between.
     * A choice inside a row's value cell keeps its flex layout there, and
     * the `--inline` and `--stacked` lists are excluded by name: `:is()`
     * gives this selector (0,3,0), so without the `:not()` it out-ranks
     * their (0,2,0) rules below and dissolves them into one merged row per
     * option — which is how the skills page actually rendered until
     * 31 Aug 2026.
     */
    :is(.form-grid, .form-grid .form-fieldset) > :is(.choice-list, .choice):not(.choice-list--inline, .choice-list--stacked),
    :is(.form-grid, .form-grid .form-fieldset) > .choice-list:not(.choice-list--inline, .choice-list--stacked) > .choice {
        display: contents;
    }

    :is(.form-grid, .form-grid .form-fieldset) > .choice > input,
    :is(.form-grid, .form-grid .form-fieldset) > .choice-list:not(.choice-list--inline, .choice-list--stacked) > .choice > input {
        justify-self: end;
        align-self: center;
    }

    /* The `:not()` matches the dissolving rule above: an inline or stacked
       list's labels keep the base --space-3 gap to their control — this
       rule's specificity beats anything that could restore it below. */
    :is(.form-grid, .form-grid .form-fieldset) > .choice > label,
    :is(.form-grid, .form-grid .form-fieldset) > .choice-list:not(.choice-list--inline, .choice-list--stacked) > .choice > label {
        padding-inline-start: 0;
    }

    /*
     * A taken sponsorship level is a dissolved `.choice` holding one span
     * and no control, so left to auto-placement the span fills the radio
     * column and every radio and label after it shifts one cell: the next
     * level's radio lands in the label column, flush right (found by Jeff,
     * 4 Sep 2026). Pinning the text to the label column leaves the radio
     * cell empty, which is the truth of it, and the label's height and
     * centring keep the row rhythm even with the open levels around it.
     */
    :is(.form-grid, .form-grid .form-fieldset) > .choice-list:not(.choice-list--inline, .choice-list--stacked) > .choice > .type-closed__reason {
        grid-column: 2;
        display: flex;
        align-items: center;
        min-block-size: var(--target-min);
        padding-inline-start: 0;
    }

    /* A tip or error written between the options of a dissolved list. The
       `:not()` matters here too: a stacked list is its own single-column
       grid, and `grid-column: 2` inside it would mint a phantom second
       column and push an inline error off to the right of its checkbox. */
    :is(.form-grid, .form-grid .form-fieldset) > .choice-list:not(.choice-list--inline, .choice-list--stacked) > :is(.form-tip, .form-error) {
        grid-column: 2;
        margin-block: 0;
    }

    /* The option-then-tip pull-up survives the dissolve: the fieldset's
       row-gap plays the list gap's part here, so the same negative margin
       closes the same distance. Written after the reset above with one
       more class so it wins. */
    :is(.form-grid, .form-grid .form-fieldset) > .choice-list:not(.choice-list--inline, .choice-list--stacked) > .choice + .form-tip {
        margin-block-start: calc(-1 * (var(--space-2) + var(--space-2)));
    }

    .form-grid .form-fieldset {
        grid-column: 1 / -1;
        display: grid;
        grid-template-columns: fit-content(16rem) minmax(0, 1fr);
        gap: var(--space-4);
        align-items: start;
    }

    /*
     * A fieldset that is purely an option group tightens its rhythm
     * (Jeff, 1 Sep 2026): the form's --space-4 row gap between 44px
     * option rows read as dead height. The row height itself keeps the
     * --target-min floor from `.choice`, which §8.2 / WCAG 2.5.5 does
     * not allow to shrink -- the gap is the only lever.
     */
    .form-grid .form-fieldset:has(> .choice-list, > .choice) {
        row-gap: var(--space-2);
    }

    .form-grid .form-fieldset legend,
    .form-grid .form-fieldset > :is(h2, h3, .form-actions) {
        grid-column: 1 / -1;
    }

    .form-grid .form-fieldset legend {
        margin-block-end: 0;
    }

    .form-grid .form-fieldset > .form-tip,
    .form-grid .form-fieldset > .form-error,
    .form-grid .choice-description {
        grid-column: 2;
        margin-block: 0;
        padding-inline-start: 0;
    }

    /*
     * The proficiency scales are the one exception to merged rows: five
     * short options per skill area stay on one line, full width under the
     * area's legend rather than in the value column — the label track would
     * otherwise indent every scale by up to 16rem of empty space.
     */
    .form-grid .choice-list--inline {
        display: flex;
        grid-column: 1 / -1;
    }

    .form-grid .choice-list--inline .choice {
        display: flex;
    }

    /*
     * Any form holding an `--inline` list — today that is the skills form
     * alone, and a future adopter inherits this compression with the class:
     * one legend-plus-scale pair per area makes the uniform --space-4 row
     * gap read as dead height, so the gap tightens and the legend takes the
     * difference back as top margin — 8px inside an area, 16px between
     * areas, so the grouping survives the compression. Row height itself is
     * untouched: each scale row keeps the --target-min (44px) floor from
     * `.choice`, which WCAG 2.5.5 does not allow to shrink.
     *
     * The error paragraphs follow the scales to the left edge; leaving them
     * in the value column would indent them against rows that no longer are.
     */
    .form-grid:has(.choice-list--inline) {
        row-gap: var(--space-2);
    }

    .form-grid:has(.choice-list--inline) .form-fieldset legend {
        margin-block-start: var(--space-2);
    }

    .form-grid:has(.choice-list--inline) > .form-error,
    .form-grid:has(.choice-list--inline) .form-fieldset > .form-error {
        grid-column: 1 / -1;
    }

    /*
     * A `--stacked` list (the volunteer page's date and skill checklists)
     * opts out of merged rows the same way `--inline` does, but keeps the
     * base one-column stack: full width, controls on the left edge, the
     * list's own --space-2 gap between rows. Its labels keep the base
     * --space-3 gap via the `:not()` on the padding rule above.
     */
    .form-grid .choice-list--stacked {
        grid-column: 1 / -1;
    }

    /*
     * A tip that introduces a stacked list rather than annotating a value
     * cell spans the row like the list it describes. Declared after the
     * column-2 tip rules above so it wins the equal-specificity tie for
     * the fieldset case.
     */
    .form-grid > .form-tip--wide,
    .form-grid .form-fieldset > .form-tip--wide {
        grid-column: 1 / -1;
    }
}

legend {
    padding: 0;
    font-weight: var(--weight-bold);
}

/* ------------------------------------------- public registration flow */

/*
 * Added for the Phase 4 flow. Every value is a token; no color is introduced
 * here, and nothing below overrides a rule declared earlier in this file.
 */

/* §9.4: the summary lists one link per error, each carrying its own message. */
.form-error-summary__list {
    margin-block: 0;
    padding-inline-start: var(--space-6);
}

.form-error-summary__list li + li {
    margin-block-start: var(--space-2);
}

/*
 * "Step N of M". The sentence is the indicator every user gets; the numbered
 * list repeats it as a glance-able strip whose current item is marked by
 * weight and a 3px underline as well as aria-current, so color is never what
 * distinguishes it (DESIGN.md §10.1 uses the same device for the active nav
 * section).
 */
.step-progress {
    /* The tick's height and the current step's underline. The alignment
       rules below are sums of these, so they live in one place. */
    --step-progress-check: 1.25rem;
    --step-progress-underline: 3px;
    margin-block-end: var(--space-6);
}

.step-progress__sentence {
    margin-block-end: var(--space-2);
    font-weight: var(--weight-bold);
}

/* Each step is as wide as its label, and the strip wraps to another row
   when they do not all fit (Jeff, 9 Oct 2026). The equal 4.5rem columns
   this replaces held "Deployment" and "Maintenance" only when the browser
   hyphenated them; Chrome on a Mac did not, and the words ran over their
   neighbours. No `flex-grow`: a step left alone on the last row would
   stretch across it, and so would the current step's underline. Wrapping
   happens before any shrinking, so a label only breaks inside a word when
   it is wider than the whole strip (320px, 400% zoom): `min-inline-size:
   0` lets the step shrink to the strip and `overflow-wrap: anywhere` lets
   the word break, rather than scroll the page sideways (SC 1.4.10). */
.step-progress__list {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.step-progress__step {
    display: flex;
    flex: 0 1 auto;
    align-items: center;
    justify-content: center;
    min-inline-size: 0;
    max-inline-size: 100%;
    min-block-size: var(--target-min);
    color: var(--color-text-secondary);
    text-align: center;
    overflow-wrap: anywhere;
}

/* Column, so a started step's tick stacks above its label and takes none of
   the column's width (Jeff, 30 Sep 2026). The hidden "Step N:" and status
   spans are absolutely positioned, so they stay out of this flow. */
.step-progress__step .btn--link {
    flex-direction: column;
    gap: 0;
    inline-size: 100%;
    block-size: 100%;
    padding-inline: var(--space-1);
}

.step-progress__check {
    inline-size: var(--step-progress-check);
    block-size: var(--step-progress-check);
    flex-shrink: 0;
}

/* Steps without a tick -- the current one and any not yet reached --
   reserve the tick's height above their label, so every label in a row
   sits on the same line (Jeff, 30 Sep 2026). Padding the top by the tick's
   height moves the centred label down by half of it, exactly where a
   started step's label sits below its tick. The current step adds its 3px
   underline, which would otherwise lift its label by half that; a
   transparent border on every step would not do, because forced colours
   paint transparent borders. */
.step-progress__step:not(:has(a)) {
    padding-block-start: var(--step-progress-check);
}

/* Two classes, to outrank the rule above for a current step with no link. */
.step-progress__step.step-progress__step--current {
    padding-block-start: calc(var(--step-progress-check) + var(--step-progress-underline));
}

.step-progress__step--current {
    color: var(--color-text);
    font-weight: var(--weight-bold);
    border-block-end: var(--step-progress-underline) solid var(--color-action);
}

/* §9.2: option groups stack, control left, text immediately beside it. */
.choice-list {
    display: grid;
    gap: var(--space-2);
    margin-block-end: var(--space-4);
}

/*
 * A tip that belongs to the option above it (the team page's three modes,
 * Jeff, 2 Sep 2026). A `.choice` row is --target-min tall with its label
 * centred, so the row's own slack below the label text plus the list gap
 * read as the tip floating midway between two options. Pulling the tip up
 * by the list gap and then by most of the row's slack (Jeff, 3 Sep 2026:
 * the list gap alone still left 12px) sets the tip about 4px under the
 * label text; the gap below the tip, before the next option, is untouched.
 * The row keeps its full height, and the tip's box now overlaps the bottom
 * of the label's -- `.choice label` is positioned so it still paints and
 * hit-tests above the tip there, and the 44px target stays whole.
 */
.choice-list > .choice + .form-tip {
    margin-block-start: calc(-1 * (var(--space-2) + var(--space-2)));
}

/*
 * A sponsorship level's benefits, sitting under its radio rather than inside
 * its label (Sprint 9 Task 11). Indented to the label's text so the
 * description reads as belonging to the option above it, and given the
 * secondary text colour so it does not compete with the level's name. The
 * indent is the control's own width plus the label's leading gap, which is
 * what `.choice label` uses.
 */
.choice-description {
    margin-block-end: var(--space-2);
    padding-inline-start: calc(var(--space-6) + var(--space-3));
    color: var(--color-text-secondary);
}

/*
 * Pulled up under its option exactly as the tip above is (Jeff, 3 Sep
 * 2026), and for the same reason. One rule serves both widths: at md the
 * grid's `.form-grid .choice-description` reset is two classes, and this
 * is three, so it wins there without a second copy.
 */
.choice-list > .choice + .choice-description {
    margin-block-start: calc(-1 * (var(--space-2) + var(--space-2)));
}

.choice-description > :first-child {
    margin-block-start: 0;
}

.choice-description > :last-child {
    margin-block-end: 0;
}

/*
 * The proficiency scales only. Five short options per skill area read better
 * on one line where there is room, and fall back to the stack below 40em so
 * the page stays clean at 320px.
 */
@media (min-width: 40em) {
    .choice-list--inline {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-2) var(--space-6);
    }
}

/*
 * Boxed since 1 Sep 2026 (Jeff): every fieldset, public flow and admin
 * alike, draws its group. 1px `--color-rule`, not `--color-border` -- the
 * boundary is decorative grouping (the legend carries the semantics), and
 * §3 reserves the stronger grey for the borders that identify a control.
 * Both tokens re-resolve in the dark theme, so no second declaration here.
 */
.form-fieldset {
    margin-block-end: var(--space-6);
    padding: var(--space-4);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius);
}

.form-fieldset legend {
    /*
     * `float` takes the legend out of "rendered legend" mode (the HTML
     * rendering spec keys on a non-none computed float), so it sits inside
     * the padded box instead of notched into the border line; at full
     * inline size, everything after it starts below. Inside the md form
     * grid the float is moot -- the legend is a grid item spanning both
     * columns via the rule above.
     */
    float: left;
    inline-size: 100%;
    margin-block-end: var(--space-2);
}

/*
 * What comes after the floated legend must start below it. "Everything
 * after it starts below" holds for ordinary blocks, but a grid or flex
 * container establishes its own formatting context and is placed *beside*
 * a float instead -- and beside a 100%-wide float the only room left is
 * zero, so under 48em every `.choice-list` or `.form-row` that follows a
 * legend collapsed to a 0-wide column at the fieldset's right edge and the
 * page scrolled sideways (found 2 Sep 2026 on /register/team at 375px;
 * WCAG 1.4.10). `clear` is what a block-level box, grid containers
 * included, uses to step past a float. Inside the md form grid the legend
 * is a grid item, not a float, and `clear` is inert there.
 */
.form-fieldset > legend + * {
    clear: both;
}

/*
 * A closed registration type or a full sponsorship level: text carrying its
 * reason, in its place among the open options — never a disabled radio, which
 * fails contrast by definition and cannot be focused, so a keyboard user never
 * learns why it is dead (DESIGN.md §7). The `.type-closed` section and its
 * list, which held Step 4's closed types below the Continue button, went on
 * 6 Sep 2026 when that page took the sponsorship page's in-place layout; the
 * `.choice > .type-closed__reason` rules above are what place it now.
 *
 * The name keeps body text and only the reason is secondary (Jeff, 9 Sep
 * 2026): a wholly grey line, indented to the label column with no control
 * beside it, read as a tip belonging to the radio above it.
 */
.type-closed__reason {
    color: var(--color-text);
}

.type-closed__note {
    color: var(--color-text-secondary);
}

.part-list__status {
    display: block;
}

/*
 * §9.7: a single record on screen — the review page, the admin detail pages,
 * the judge's submission view. The same shape as the form grid it mirrors:
 * one grid per list, label column content-sized, labels right-aligned at md;
 * label above value below it. Each dt/dd pair is wrapped in a div that
 * dissolves at md so every label shares one track.
 */
.summary-section {
    margin-block-end: var(--space-8);
}

.detail-list,
.summary-list {
    display: grid;
    gap: var(--space-6);
    margin: 0 0 var(--space-4);
}

.detail-list > div,
.summary-list > div {
    display: grid;
    gap: var(--space-1);
}

.detail-list dt,
.summary-list dt {
    font-weight: var(--weight-bold);
}

.detail-list dd,
.summary-list dd {
    margin: 0;
    /* A long unbroken answer must not force a horizontal scrollbar at 320px.
       `min-inline-size: 0` lets the value cell shrink below its content; a
       grid item's default of `auto` would grow the track instead. */
    min-inline-size: 0;
    overflow-wrap: break-word;
}

/* A preformatted value (the email-template preview) scrolls inside its cell
   rather than widening the page — the reflow defect admin-broadcast.php's
   header comment records. */
.detail-list dd > pre,
.summary-list dd > pre {
    max-inline-size: 100%;
    overflow-x: auto;
}

/* The rendered email (spec 2026-10-05 §3a). A fixed block size with the
   content scrolling inside the frame, so it reflows at 320px without a
   script measuring it. */
.email-preview-frame {
    display: block;
    inline-size: 100%;
    block-size: 32rem;
    border: 1px solid var(--color-border);
    /* A literal, not a token: a mail program shows an unstyled email on
       white in both themes, and the frame's document has no dark styles. */
    background: #fff;
}

.email-preview-source {
    margin-block-start: var(--form-gap, 1rem);
}

.email-preview-source > summary {
    cursor: pointer;
    min-block-size: 44px;
    padding-block: 0.625rem;
}

@media (min-width: 48em) {
    .detail-list,
    .summary-list {
        grid-template-columns: fit-content(16rem) minmax(0, 1fr);
        gap: var(--space-4);
        align-items: start;
    }

    .detail-list > div,
    .summary-list > div {
        display: contents;
    }

    .detail-list dt,
    .summary-list dt {
        text-align: end;
    }
}

/*
 * The Web Planning answers, on the admin view and the client's own review
 * page: each question and its answer on one line, the questions left-aligned
 * in one fixed-width column so every answer starts at the same edge (Jeff,
 * 23 Sep 2026). A deliberate exception to §9.1's right-aligned record
 * labels, which `.detail-list` keeps. Below md the answer sits under its
 * question, with more air between pairs than inside one.
 */
.answer-list {
    margin: 0 0 var(--space-4);
}

.answer-list dt {
    font-weight: var(--weight-bold);
}

.answer-list dd {
    margin: var(--space-1) 0 var(--space-4);
    min-inline-size: 0;
    overflow-wrap: break-word;
}

@media (min-width: 48em) {
    .answer-list {
        display: grid;
        grid-template-columns: 18rem minmax(0, 1fr);
        gap: var(--space-4) var(--space-6);
        align-items: start;
    }

    .answer-list dd {
        margin: 0;
    }
}

/* A suggested team's name with its Pair icon beside it (Jeff, 23 Sep 2026). */
.suggestion-list__team {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

/*
 * Content that is knowingly missing, marked as missing. The word
 * "Placeholder:" in the markup is what carries that — this rule only makes it
 * look like a block rather than body copy.
 *
 * Deliberately a border and nothing else. A --color-surface-subtle fill would
 * read better in light theme and disappear in dark, where that token keeps its
 * light value (tokens.css §2.6: the dark set is incomplete pending Jeff's
 * scope call). --color-border clears 3:1 on both surfaces, so this block is
 * one of the few here that is already correct in both.
 */
.placeholder-note {
    margin-block-end: var(--space-6);
    padding-inline-start: var(--space-4);
    border-inline-start: 4px solid var(--color-border);
}

.placeholder-note > :last-child {
    margin-block-end: 0;
}

/* Confirmation page: one bulleted line per invitee (Jeff, 3 Sep 2026):
   the name, a dash, and the mailto. */
.invitation-list {
    margin: 0 0 var(--space-6);
    padding-inline-start: var(--space-6);
    list-style: disc;
}

.invitation-list li {
    padding-block: var(--space-1);
    overflow-wrap: break-word;
}

/* The per-client "no suggestion" lines under Suggested pairings
   (user-testing round spec, 26 Sep 2026 -- PR 2): indented so a
   scanning eye lands on the team names in `.suggestion-list` rather
   than these explanatory lines. */
.suggestion-empty {
    margin-inline-start: var(--space-6);
}

/* -------------------------------------------------------------- utilities */

/*
 * Visually hidden but still in the accessibility tree — used for the "Error:"
 * that pairs with §9.4's decorative ⚠ glyph, among others. Never applied to
 * something a sighted keyboard user needs to see.
 */
.visually-hidden {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ------------------------------------------------- admin data table (§3) */

/*
 * The scroller, not the document, is what moves sideways at 320px (§7). It is
 * focusable and labelled because a region that scrolls must be reachable by
 * keyboard alone — a mouse wheel is not an accessibility feature.
 */
.data-table__scroll {
    /* Positioned so the absolutely-positioned .visually-hidden spans in the
       row-action links resolve their containing block here and are clipped by
       the scroller; static let them widen the document instead (Task 12c
       walk, defect D1 — 552px of page scroll at a 320px viewport). */
    position: relative;
    overflow-x: auto;
    max-inline-size: 100%;
}

.data-table {
    inline-size: 100%;
    border-collapse: collapse;
    margin-block: var(--space-4);
}

/*
 * Every table's caption is visible (Jeff, 9 Sep 2026): it is the table's
 * title for everyone, not a screen-reader aside. Six captions used to carry
 * .visually-hidden and the rest fell back to the browser default, which
 * centres the text and butts it against the header row. Start-aligned to
 * sit over the first column like a heading, weighted so it reads as one,
 * and given a little air before the headers.
 */
.data-table caption {
    text-align: start;
    font-weight: 600;
    /* The same inline padding as a cell, so the caption's first letter
       lines up with the first column's text rather than the table edge. */
    padding-inline: var(--space-4);
    padding-block-end: var(--space-2);
}

/*
 * A table directly under its heading keeps only the heading's own margin
 * (Jeff, 9 Sep 2026): the table's top margin stacked on it put 32px
 * between the heading and the caption, and the caption is the heading's
 * next line, not a new block.
 */
:is(h1, h2, h3) + .data-table__scroll > .data-table,
:is(h1, h2, h3) + .data-table {
    margin-block-start: 0;
}

.data-table th,
.data-table td {
    padding: var(--space-3) var(--space-4);
    text-align: start;
    vertical-align: top;
    border-block-end: 1px solid var(--color-border);
}

/*
 * A row's first cell is a `.btn--link` for the §8.2 target, which centres
 * its text in a 44px box; a top-aligned neighbour puts its text 10px higher
 * (measured 4 Sep 2026). Meeting on the middle lines every cell up with the
 * link, and pulling the link's own inline padding back puts the name under
 * its header instead of 8px to the right of it.
 *
 * Row headers too (10 Sep 2026): a list's first cell is a `<th scope="row">`
 * so the row's name is announced with every cell, and it sits beside the
 * same 44px icon links, so it meets them on the same middle line.
 */
.data-table td,
.data-table tbody th {
    vertical-align: middle;
}

.data-table td > .btn--link {
    margin-inline-start: calc(-1 * var(--space-2));
}

.data-table thead th {
    white-space: nowrap;
    background-color: var(--color-surface-subtle);
}

/*
 * The exception to the line above, for a header naming two things at once --
 * "Organization / Contact" is 154px on one line and 121px on two, and that
 * 33px is the difference between the payments table fitting its container at
 * 1280 and scrolling (measured 29 Aug 2026). Opt-in per header: a nowrap
 * default is right for the short ones, which would look ragged wrapped.
 */
.data-table thead th.data-table__th--wrap {
    white-space: normal;
}

/*
 * Sortable headers (spec 2026-09-26 §3, PR 6). The header's words are the
 * link, and the link carries the §8.2 44px floor, so the cell gives up its
 * block padding instead of stacking 24px on top of it. Underlined at rest:
 * in a header row colour would otherwise be the only thing telling a link
 * from a plain header (SC 1.4.1). --color-action on the subtle header
 * fill is 4.7:1 (bin/check-contrast.php, "link on subtle"). Focus is the
 * global dual ring; the table's top margin gives it room in the scroller.
 */
.data-table thead th:has(> .data-table__sort) {
    padding-block: 0;
}

.data-table__sort {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-block-size: var(--target-min);
    min-inline-size: var(--target-min);
    color: var(--color-action);
    text-decoration: underline;
    text-underline-offset: 0.2em;
}

.data-table__sort:hover {
    color: var(--color-action-dark);
}

.data-table__arrow {
    font-size: 0.875em;
}

/* Money reads down the column, so the digits line up rather than the words. */
.data-table__amount {
    text-align: end;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/*
 * Two values in one cell, stacked (Jeff, 29 Aug 2026): a date over its time,
 * an organization over its contact. The column is then as wide as the longer
 * line instead of the two side by side, which is what took the payments table
 * off the screen. Block spans, so each is its own line without a <br>.
 */
.data-table__stacked > span {
    display: block;
    /*
     * Each line is one value and breaks as a unit. Without this the date
     * split as "2026-08-" / "27" across two lines -- three lines for one
     * timestamp, and a date cut at the hyphen reads as a different date.
     * The cell may stack; the values inside it may not.
     */
    white-space: nowrap;
}

/*
 * §3: the admin-editable HEX colours the dot and the border only. Text stays
 * --color-text on a neutral chip, so no colour an admin picks can break text
 * contrast, and the label carries the meaning by itself.
 */
.status-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--status-color, var(--color-border));
    border-radius: var(--radius);
    background-color: var(--color-surface-subtle);
    color: var(--color-text);
    white-space: nowrap;
}

.status-badge__dot {
    inline-size: 0.5rem;
    block-size: 0.5rem;
    border-radius: 50%;
    background-color: var(--status-color, var(--color-border));
    flex-shrink: 0;
}

/*
 * The formatting toolbar above the two textareas that hold admin-authored
 * HTML (Jeff, 29 Aug 2026). Built by rich-text.js, so it never exists
 * without the script that makes it work.
 *
 * A wrapping row of ordinary buttons rather than a bar that scrolls: six
 * labels at 44px tall reflow onto a second line on a narrow screen, and a
 * toolbar that scrolls sideways is one whose last button can be missed.
 */
.rich-text-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-block-end: var(--space-2);
}

/*
 * Text labels, not icons. The row-action icons elsewhere earn their tooltips
 * by being one per row in a table; here there is room for the words, and
 * "Bulleted list" needs no learning.
 */
.rich-text-toolbar__button {
    font-size: var(--text-sm);
}

/*
 * The Heading level select (Jeff, 7 Sep 2026): a real <select> beside the
 * Heading button, sized to its five two-character options rather than
 * stretched to the form's full width like `.form-select` alone would be.
 * Height matches the buttons so the row stays one line.
 */
.rich-text-toolbar__select {
    inline-size: auto;
    min-block-size: var(--target-min);
    font-size: var(--text-sm);
}

/*
 * A status allowed to wrap instead of forcing its column to the width of the
 * longest label (Jeff, 29 Aug 2026). Opt-in: on a narrow table a wrapping
 * badge looks broken, so only the payments screen asks for it.
 *
 * **After `.status-badge`, not before, and that is load-bearing.** Both
 * selectors are one class, so the cascade settles this on source order alone:
 * written above, `white-space: normal` lost to `.status-badge`'s `nowrap` and
 * the label spilled out of its own border instead of wrapping -- 178px of
 * text in a 126px box. Neither axe nor the template tests could see it; the
 * computed style in a browser is what caught it.
 *
 * No cap goes with it, and the cap this rule carried for an afternoon is
 * worth recording. While admin ran at the public flow's 48rem the status
 * column had to be forced narrow -- 18ch -- or the table overflowed. Once
 * the shell took the compact measure it was meant to have (64rem), the same
 * cap was the only thing putting "Invoice sent, payment pending" on three
 * lines in a column with room for one. Permission to wrap is enough on its
 * own: with `table-layout: auto` the column takes the width it can get and
 * wraps when it cannot, which is the responsive answer a fixed cap replaces
 * with a guess about one viewport.
 */
.status-badge--wrap {
    white-space: normal;
}

/*
 * --color-surface-subtle has no dark counterpart: §2.6 defines four dark
 * tokens and this is not one of them, and inventing a fifth would settle Open
 * Item 7 by the back door. But --color-text *does* flip, so both components
 * above would paint near-white text on a near-white chip — 1:1, the label
 * gone entirely, which is worse than the incomplete dark rendering the rest
 * of the sheet accepts.
 *
 * Falling back to --color-surface costs the subtle tint under dark and keeps
 * every label legible. The badge still reads as a badge: §3 puts its outline
 * and its dot on the stored HEX, and both are unaffected. Remove this block
 * when §2.6 gains a dark value for the subtle surface.
 */
@media (prefers-color-scheme: dark) {
    .data-table thead th,
    .status-badge {
        background-color: var(--color-surface);
    }
}

/*
 * Admin manual chapters (package 5). The chapter partials are class-free
 * because PHPWord converts the same markup, so their tables are styled from
 * the wrapper. Same tokens as .data-table; headers wrap, since a chapter
 * table is prose in columns and has to reflow at 320px.
 */
.help-chapter table {
    inline-size: 100%;
    border-collapse: collapse;
    margin-block: var(--space-4);
    table-layout: fixed;
    overflow-wrap: anywhere;
}

.help-chapter caption {
    text-align: start;
    font-weight: 600;
    padding-inline: var(--space-4);
    padding-block-end: var(--space-2);
}

.help-chapter th,
.help-chapter td {
    padding: var(--space-3) var(--space-4);
    text-align: start;
    vertical-align: top;
    border-block-end: 1px solid var(--color-border);
}

.help-chapter thead th {
    background-color: var(--color-surface-subtle);
}

/*
 * §8.2: the Chapters nav, the "Help contents" link atop a chapter, and
 * the chapter list on the help index are navigation, not prose -- the
 * §8.2 exception for inline links within prose does not reach them, so
 * each gets the full 44px target. The chapter partials
 * (templates/help/*.php) are untouched: their links stay the exception.
 */
.help-chapter nav a,
.help-chapter__contents a,
.help-chapter-list a {
    display: inline-flex;
    align-items: center;
    min-block-size: var(--target-min);
}

/* ---------------------------------------------------- idle-expiry warning */

/*
 * The admin idle warning (PRD §Phase 6), and since 6 Oct 2026 the pin
 * removal confirm on the build screen: every admin `<dialog>` carries
 * `.admin-dialog`. The dialog is native, so its behaviour needs no CSS at
 * all — this only puts it on the same surface as the rest of the admin
 * chrome. The UA default paints it on the system `canvas` color, which is
 * neither of this palette's surfaces and carries none of DESIGN.md's
 * verified contrast figures, so both colors are stated.
 */
.admin-dialog {
    max-width: var(--measure-comfortable);
    padding: var(--space-6);
    border: 2px solid var(--color-border);
    border-radius: var(--radius);
    background-color: var(--color-surface);
    color: var(--color-text);
}

.admin-dialog > :first-child {
    margin-block-start: 0;
}

/*
 * `.form-actions` now carries `margin-block-end: var(--space-8)` (user
 * testing, 26 Sep 2026) so the last form on a page does not sit flush
 * against whatever follows it. Inside this dialog there is nothing after
 * the button row for that space to separate -- only the dialog's own
 * padding -- and a margin cannot collapse through a `<dialog>` boundary,
 * so it would otherwise show up as a bare gap under the buttons.
 */
.admin-dialog > :last-child {
    margin-block-end: 0;
}

/*
 * The scrim is derived from --color-text rather than written as a literal,
 * because --color-text inverts with the palette: in dark mode a hardcoded
 * #121313 scrim would be the same color as the surface behind it and the
 * dimming would read as nothing at all. color-mix keeps it "60% of whatever
 * the current text color is", which is dark over the light palette and light
 * over the dark one — separation either way, and the screen behind stays
 * legible enough that an admin can see what they were in the middle of.
 */
.admin-dialog::backdrop {
    background-color: color-mix(in srgb, var(--color-text) 60%, transparent);
}

/* --------------------------------------------- badge colour preview (admin) */

/*
 * Two fixed panes on the payment-status form (DESIGN.md §3): the same badge
 * as it will render on the light surface and on the dark one, side by side,
 * with the measured ratio stated as text under each.
 *
 * These panes pin their own surface tokens rather than following the viewer's
 * theme, and that is the point -- the admin is choosing a colour for BOTH
 * themes, so seeing only their own would hide half the decision. This is the
 * one place in the sheet where a `data-theme` attribute means anything; it is
 * scoped to `.badge-preview__pane`, and it is deliberately not a theme
 * toggle. DESIGN.md Open Item 3 (is dark mode in 2027 scope at all?) stays
 * open, and §2.6's missing dark tokens stay missing.
 *
 * `--color-surface-subtle` has no dark counterpart, so the dark pane gives
 * the chip the plain dark surface -- the same fallback the media query above
 * makes for the real badge, for the same reason.
 */
.badge-preview {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
}

.badge-preview__pane {
    flex: 1 1 14rem;
    padding: var(--space-4);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius);
}

.badge-preview__pane[data-theme="light"] {
    --color-surface: #ffffff;
    --color-surface-subtle: #f2f2f3;
    --color-text: #121313;
    background-color: #ffffff;
    color: #121313;
}

.badge-preview__pane[data-theme="dark"] {
    --color-surface: #121313;
    --color-surface-subtle: #121313;
    --color-text: #f2f2f3;
    background-color: #121313;
    color: #f2f2f3;
}

/*
 * Higher specificity than the dark-preference block above, so the light pane
 * keeps its subtle chip even when the admin's own machine is in dark mode.
 */
.badge-preview__pane .status-badge {
    background-color: var(--color-surface-subtle);
    color: var(--color-text);
}

.badge-preview__caption {
    margin: 0 0 var(--space-2);
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
}

.badge-preview__ratio {
    margin: var(--space-2) 0 0;
    font-size: var(--text-xs);
}

/*
 * The sponsorship-level description preview (Sprint 9 Task 11). Admin-authored
 * HTML, sanitized server-side, rendered inside the admin form so the author
 * can see what a registrant will get. The border earns its place: without a
 * visible edge there is nothing to tell an admin where their content stops and
 * the form's own chrome starts, and the whole point of the panel is that the
 * boundary is legible. Borrowed treatment from .badge-preview__pane rather
 * than invented -- same tokens, same radius.
 */
.html-preview {
    padding: var(--space-4);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius);
}

.html-preview > :first-child {
    margin-block-start: 0;
}

.html-preview > :last-child {
    margin-block-end: 0;
}

/* ------------------------------------------------------------------ motion */

/*
 * Phase 16 spec §4: row actions as icons. A square target the size §8.2
 * asks for, with the tertiary button's own colours; the visible label is
 * the tooltip below, drawn from data-tooltip on hover and focus (SC 1.4.13:
 * hoverable, persistent, and dismissible -- row-icons.js sets
 * data-tooltip-dismissed on Escape). The complete accessible name is the
 * visually-hidden text inside the button, never the tooltip, so nothing
 * depends on the pseudo-element being read. The transition rides the
 * global reduced-motion rule at the end of this file.
 */
.btn--icon {
    position: relative;
    inline-size: var(--target-min);
    block-size: var(--target-min);
    padding: 0;
    display: inline-grid;
    place-items: center;
}

/* A cell of icon actions stays on one row: at 1280 px the events list's
   five would otherwise wrap inside the cell, which is the one layout the
   spec checks by name. */
td:has(> .btn--icon),
td:has(> form > .btn--icon) {
    white-space: nowrap;
}

/* A POST action in an icon cell (the lookup lists' Deactivate) rides in a
   form, which is block-level by default and would stack below the Edit
   link; inline-block lets the cell's nowrap above hold both on one row. */
.row-action {
    display: inline-block;
}

/* A cell of labelled row buttons (the registration detail screen's
   Reminders panel, spec 2026-10-01): each is its own single-button form,
   and the gap keeps the 8 px §8.2 asks for between adjacent targets
   whether they sit side by side or wrap. */
.table-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* Adjacent icon targets keep the 8 px §8.2 asks for between them. */
.btn--icon + .btn--icon,
.btn--icon + form > .btn--icon,
form + .btn--icon,
form + form > .btn--icon {
    margin-inline-start: var(--space-2);
}

.icon {
    inline-size: 1.25rem;
    block-size: 1.25rem;
}

.btn--icon[data-tooltip]::after {
    content: attr(data-tooltip);
    /* Fixed, not absolute: the lists sit in .data-table__scroll, whose
       overflow clip would cut an absolutely-positioned tooltip off at the
       scroller's edge. row-icons.js sets the two custom properties from
       the button's viewport rectangle through the CSSOM (no inline style,
       so no CSP exception); without script the tooltip still shows, at the
       pseudo-element's static position. */
    position: fixed;
    inset-block-start: var(--tooltip-y, auto);
    inset-inline-start: var(--tooltip-x, auto);
    translate: -50% 0;
    z-index: 2;
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background-color: var(--color-surface);
    color: var(--color-text);
    font-size: var(--text-base);
    font-weight: var(--weight-bold);
    line-height: var(--leading-base);
    white-space: nowrap;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition:
        opacity var(--motion-duration) var(--motion-easing),
        visibility var(--motion-duration) var(--motion-easing);
}

.btn--icon[data-tooltip]:not([data-tooltip-dismissed]):hover::after,
.btn--icon[data-tooltip]:not([data-tooltip-dismissed]):focus-visible::after {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
}

/*
 * §12: reduce removes transitions and animation entirely. Kept last so it
 * overrides anything declared above, and scoped to durations rather than the
 * properties themselves so a state change still happens — instantly.
 *
 * The transition duration is 0s, not the 0.01ms the common snippet uses. A
 * 0.01ms transition is still a transition: for one frame the computed value
 * is the OLD one, so any script that writes a style and reads it straight
 * back sees the value it just replaced. That is what dropped the admin
 * nav's pulled-up flyout under reduced motion (fitOnScreen cleared the
 * offset, measured the un-cleared box, and concluded the list fitted;
 * 22 Sep 2026). A zero-duration transition never starts, so the new value
 * applies at once and no transitionend is owed -- nothing here listens
 * for one.
 */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0s !important;
        scroll-behavior: auto !important;
    }
}

/*
 * Spec §2: the admin header's event-scope control, on its own centred row
 * under the nav (Jeff, 28 Aug 2026) so identity, theme, and Sign out keep
 * the top right. A real form -- label, select, submit -- laid out inline;
 * the select gets the shared .form-select treatment and its own width so a
 * long edition name does not stretch the row.
 */
.site-header__scope {
    display: flex;
    justify-content: center;
    margin-block-start: var(--space-4);
}

.event-scope {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.event-scope__select {
    inline-size: auto;
    min-inline-size: 12rem;
}

/* Print (spec 2026-09-21 §10): staff hand a client's questionnaire to a
   team. Chrome and navigation are hidden; the answers print as they read.
   CSS only -- there is no print button, the browser's own is enough. */
@media print {
    .skip-link,
    .site-account,
    .site-header,
    .site-footer,
    .no-print {
        display: none;
    }

    body {
        background: #fff;
        color: #000;
    }

    a[href] {
        color: #000;
        text-decoration: underline;
    }

    /* .data-table__scroll's overflow-x: auto computes overflow-y to auto
       too (CSS Overflow §3), which makes it a scroll container -- and a
       scroll container is not guaranteed to fragment across printed
       pages, so a long inventory could print cut off rather than
       continuing onto the next sheet. Declared visible for print, where
       pagination, not an on-screen scrollbar, is what matters. */
    .data-table__scroll {
        overflow: visible;
    }

    h2 {
        break-after: avoid;
    }

    tr,
    dt,
    dd {
        break-inside: avoid;
    }
}

/* Admin list thumbnail (spec 2026-09-05 §4): height-fixed, on the same
   white plate as the public band so a dark logo reads in the dark theme. */
.logo-thumb {
    display: block;
    block-size: 3rem;
    inline-size: auto;
    max-inline-size: 10rem;
    object-fit: contain;
    background: #ffffff;
    padding: var(--space-1);
    border-radius: var(--radius);
}
