:root {
    /* Single source of truth for gender-based name/initials colours. Used by the
       Members tab (name cells + photo-fallback initials) and the live session
       Attendee Stats names so the two never drift. CSS custom properties inherit
       through the cascade, so Blazor scoped component CSS (PegboardApp.styles.css)
       can read these despite the scoping attribute. */
    --gender-male: #2e6fa9;
    --gender-female: #a85283;

    /* SPEC-32 I4-S1 (#670), R-I2-44: the gender NAME INK — a second role, not a drift, and
       deliberately NOT a consolidation of the pair above.

       TWO ROLES, TWO PAIRS. The pair above is a FILL: avatar discs, chips, tiles, where the colour
       sits behind white text and its own contrast against the page is not what matters. This pair is
       a TEXT INK, for a small name in a dense row of numbers. Measured against white, this pair gives
       9.0:1 (male) and 9.8:1 (female); the fill pair gives 5.3:1 and 5.0:1. Both pass WCAG AA, and
       only one of them is comfortable to read at 0.85rem beside five figures.

       SO DO NOT FOLD THE TWO PAIRS INTO ONE. A future reader who "tidies" this by pointing the name
       ink at --gender-male / --gender-female will HALVE the contrast of every inked name in the
       product to fix an inconsistency that is not one. The genuine inconsistency — the Members tab
       inks a name with the FILL pair while /reports/partnerships inks one with this pair, and nobody
       has asked the Owner which is right — is filed as #680 and is not settled here.

       WHY THEY ARE TOKENS AND NOT A COMPONENT RULE. Three surfaces now render the same ink from the
       same fact: /reports/partnerships (.nm.m / .nm.f), the profile's counterpart tables on all four
       hosts (profile.css), and the in-session board. A Blazor component-scoped stylesheet is
       unreachable from any other component, so a second host referencing those class names by name
       gets a correct-looking class list and NO colour, silently — the I2-S1b lesson, and the reason
       R-I2-44 exists. The values live here once; every surface reads them.

       --gender-name-default is the report's third value, for a name whose gender is not stated. It is
       NOT applied by the profile, which leaves such a name in the table's own inherited ink and gives
       it no class at all (AC-I4-S1-4): a visible "unknown" state would publish a hole in the club's
       record on every row. It is a token so the report's own .nm.u stops being a literal. */
    --gender-name-male: #1f4b76;
    --gender-name-female: #6f2950;
    --gender-name-default: #33455c;
}

html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
    width: 100%;
    min-height: 100vh;
    margin: 0;
}

a, .btn-link {
    color: #006bb7;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

/* #268: Bootstrap's default .btn-outline-secondary is grey ink + grey border on
   a transparent fill, which reads as a disabled control in our UI - reported on
   End session / Kiosk (full screen) but the washed-out look is shared by every
   outline-secondary button, so fix it at the source. Give it a tangible surface
   and darker ink so it's obviously an active button; genuinely-disabled buttons
   still fade out via Bootstrap's .65 :disabled opacity, restoring the contrast
   between enabled and disabled. */
.btn-outline-secondary {
    color: #2d3540;
    border-color: #97a0aa;
    background-color: #eef1f4;
}

.btn-outline-secondary:hover,
.btn-outline-secondary:focus-visible,
.btn-outline-secondary:active {
    color: #fff;
    background-color: #5a636d;
    border-color: #545d66;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

@media (min-width: 641px) {
    /* In fullscreen the sidebar is hidden by default but stays in the DOM so
       the hamburger overlay (BL-008) can slide it in on demand. The .page--kiosk
       overlay rules in MainLayout.razor.css already handle the open/closed state. */
    body.clubSession-dashboard-fullscreen .content {
        padding-left: 0.35rem !important;
        padding-right: 0.35rem !important;
    }
}

/* BL-008 / #276 / #298: the top bar (hamburger + any status banner) flows
   normally in fullscreen, the same as in every other mode. It used to float as
   an absolute 0-height layer to reclaim a row, but since #298 the status banner
   shares that bar - floating it overlaid the dashboard's own kiosk header
   (hamburger over the session title, banner over the controls; screenshot
   2026-06-09). A normal-flow row sits cleanly above the dashboard header with
   nothing overlapping and the banner still clickable; the profile menu is
   reachable via the hamburger-opened sidebar footer. So: no special fullscreen
   positioning for the top bar. */
body.clubSession-dashboard-fullscreen .clubSession-dashboard-shell {
    min-height: 100vh;
}

.legal-page {
    max-width: 48rem;
    margin: 0 auto;
    padding: 1.5rem 1rem 3rem;
    color: #2a3a4d;
    line-height: 1.55;
}

.legal-page h1 {
    color: #1f3f5e;
    margin: 0 0 0.35rem;
}

.legal-page h2 {
    color: #1f3f5e;
    margin: 1.4rem 0 0.4rem;
    font-size: 1.15rem;
}

.legal-page p,
.legal-page ul {
    margin: 0 0 0.75rem;
}

.legal-page ul {
    padding-left: 1.25rem;
}

.legal-page .legal-meta {
    background: #f6f9fc;
    border: 1px solid #c4d1de;
    border-radius: 0.5rem;
    padding: 0.5rem 0.7rem;
    font-size: 0.9rem;
    color: #4d6177;
}

/* Shared auth-page layout — used by /login, /signup, /forgot-password, /reset-password, /confirm-email.
   Previously scoped to Login.razor.css, which meant the other auth pages rendered unstyled
   (inputs and buttons collapsed against each other). */
/* Shared form markers used across auth + admin forms. */
.required-marker {
    color: #b34040;
    margin-left: 0.1rem;
}

.required-legend {
    margin: 0.4rem 0 0;
    color: #6c7a8c;
    font-size: 0.8rem;
}

.field-hint {
    color: #6c7a8c;
    font-weight: 400;
    font-size: 0.8rem;
    margin-left: 0.25rem;
}

.login-shell {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Top padding 3rem → 1rem so the brand block sits closer to the top
       and the auth card stays in the visible viewport on tablet portrait. */
    padding: 1rem 1.5rem 2rem;
    background:
        radial-gradient(circle at 28% 18%, rgba(140, 190, 240, 0.22) 0%, transparent 55%),
        radial-gradient(circle at 78% 88%, rgba(93, 176, 97, 0.10) 0%, transparent 55%),
        linear-gradient(180deg, #294f76 0%, #1f3d5b 100%);
    margin: -1rem -1.5rem -1rem;
}

/* MainLayout's .content padding goes asymmetric at >=1280px (2rem left,
   1.5rem right). Match that on the left so the gradient bleeds flush. */
@media (min-width: 1280px) {
    .login-shell {
        margin-left: -2rem;
    }
}

.login-brand {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.2rem;
    margin-bottom: 0.75rem;
}

.login-brand-mark {
    font-size: 2.4rem;
    font-weight: 800;
    color: #ffffff;
    letter-spacing: 0.6px;
    line-height: 1;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.28);
}

/* BL-057 (2026-04-26 v6): split assets — figures + wordmark sized
   independently. Mike: "people in the logo are too large vs the text".
   Figures shrunk relative to wordmark; on tablet portrait the form now
   stays in view. Both PNGs are transparent so they sit natively on the
   navy gradient. */
.login-brand-logo {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
}

.login-brand-logo .login-brand-figures {
    display: block;
    height: 3.5rem;
    width: auto;
}

.login-brand-logo .login-brand-wordmark {
    display: block;
    height: 3.5rem;
    width: auto;
    max-width: 100%;
}

.login-brand-tagline {
    color: #cfe1f3;
    font-size: 0.95rem;
    font-weight: 500;
    letter-spacing: 0.2px;
}

.login-card {
    width: min(30rem, 92vw);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 0.85rem;
    background: #ffffff;
    padding: 1.5rem 1.5rem 1.3rem;
    box-shadow: 0 14px 32px rgba(8, 24, 50, 0.34);
}

.login-card h1 {
    margin: 0 0 0.35rem;
    color: #1f3f5e;
}

.login-card .form-control {
    margin-bottom: 0.5rem;
}

.login-help {
    color: #5e748a;
    margin: 0 0 0.8rem;
}

.login-actions {
    margin-top: 1.0rem;
}

.login-signup-link {
    margin-top: 0.75rem;
    color: #4d6177;
    font-size: 0.92rem;
}

.login-signup-link a,
.login-forgot-link a {
    color: #0f4f8b;
    font-weight: 600;
}

.login-forgot-link {
    margin-top: 0.35rem;
    color: #4d6177;
    font-size: 0.9rem;
}

.login-demo-help {
    margin: 0;
    color: #5f7083;
    font-size: 0.85rem;
}

/* #683 (portal S9a): the "email me a code" passwordless block below the password form. */
.login-emailcode {
    margin-top: 0.25rem;
}

.login-emailcode .login-help {
    margin-bottom: 0.5rem;
}

/* #683: off-screen honeypot on the login page (Login.razor reuses the name but the
   Signup scoped style doesn't reach here — provide a global visually-hidden style). */
.login-emailcode .signup-website-field {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* Top-level honeypot hider: the .signup-website-field field must be off-screen ANYWHERE
   the class is used — /forgot-password (ForgotPassword.razor) renders it with no
   .login-emailcode wrapper, so the scoped rule above never reached it and the "Club
   website" honeypot showed (a visible field that real users can fill, rejecting genuine
   requests). This unscoped rule covers every host of the class. */
.signup-website-field {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.login-error {
    margin-top: 0.75rem;
    border: 1px solid #d88f8f;
    background: #f7e3e3;
    color: #8a2d2d;
    border-radius: 0.5rem;
    padding: 0.35rem 0.5rem;
    font-size: 0.88rem;
}

.login-notice {
    margin-top: 0.75rem;
    border: 1px solid #9ec3e6;
    background: #eaf3fc;
    color: #1c4b78;
    border-radius: 0.5rem;
    padding: 0.35rem 0.5rem;
    font-size: 0.88rem;
}

.login-readonly-field {
    color: #4d6177;
    background: #eef3f8;
}

.legal-page a {
    color: #0f4f8b;
    font-weight: 600;
}

/* MCP track 3 — kiosk PIN UI (settings card + unlock modal + sidebar indicator) */

.kiosk-pin-card {
    border-color: #d7e0ec;
}

.kiosk-pin-status {
    color: #4e6f8e;
    margin-bottom: 0.5rem;
}

.kiosk-pin-form .form-label {
    font-size: 0.85rem;
    font-weight: 600;
    color: #24384f;
    margin-bottom: 0.2rem;
}

.kiosk-pin-form input.form-control {
    max-width: 16rem;
    letter-spacing: 0.4em;
    font-family: monospace;
    font-size: 1.05rem;
}

.kiosk-unlock-modal {
    position: fixed;
    inset: 0;
    z-index: 1090;
    display: flex;
    align-items: center;
    justify-content: center;
}

.kiosk-unlock-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(13, 44, 85, 0.45);
}

.kiosk-unlock-dialog {
    position: relative;
    background: #ffffff;
    border-radius: 0.5rem;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
    padding: 1.25rem 1.4rem;
    max-width: 22rem;
    width: 92%;
}

.kiosk-unlock-dialog h3 {
    font-size: 1.05rem;
    margin-bottom: 0.4rem;
    color: #0d2c55;
}

/* Effective Permissions (Kiosk Axis 2, epic #325) — S2b-i β: the exit-PIN keypad (Screen B).
   Ported verbatim from the v2 archive. Masked-dot display + big courtside touch keypad. */
.kiosk-exit-pin-dialog {
    text-align: center;
    max-width: 20rem;
    position: relative; /* anchor the × close (UX-3) */
}

/* UX-3: the visible × close, top-right, so a mis-tap has an obvious escape (→ Cancel, stays locked). */
.kiosk-exit-pin-close {
    position: absolute;
    top: 0.6rem;
    right: 0.7rem;
}

/* UX-10: reserve the wrong-PIN error slot so the keypad never jumps when the error appears/clears. */
.kiosk-exit-pin-error-slot {
    min-height: 2rem;
    margin-top: 0.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
}

.kiosk-pin-dots {
    display: flex;
    justify-content: center;
    gap: 0.6rem;
    margin: 0.9rem 0 0.4rem;
    min-height: 1rem;
}

.kiosk-pin-dot {
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 999px;
    border: 2px solid #0d2c55;
    background: transparent;
}

.kiosk-pin-dot.is-filled {
    background: #0d2c55;
}

.kiosk-keypad {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.5rem;
    margin-top: 0.8rem;
}

.kiosk-keypad-key {
    min-height: 3.75rem; /* ~64px courtside touch floor */
    font-size: 1.4rem;
    font-weight: 600;
    color: #0d2c55;
    background: #f1f5f9;
    border: 1px solid #cbd5e1;
    border-radius: 0.5rem;
}

.kiosk-keypad-key:hover:not(:disabled) {
    background: #e2e8f0;
}

.kiosk-keypad-key:disabled {
    opacity: 0.5;
}

.kiosk-keypad-confirm {
    color: #ffffff;
    background: #0d6efd;
    border-color: #0d6efd;
    font-size: 1.15rem; /* "OK" text (UX-2) reads better a touch smaller than the 1.4rem digits */
}

.kiosk-keypad-confirm:hover:not(:disabled) {
    background: #0b5ed7;
}

/* UX-3: Cancel bumped to keypad-key weight (min-height inherited from .kiosk-keypad-key) but
   full-width + smaller type so it reads as the calm secondary, not a 4th keypad column. */
.kiosk-exit-pin-cancel.kiosk-keypad-key {
    width: 100%;
    font-size: 1rem;
    background: transparent;
}

.kiosk-keypad-del {
    font-size: 1.2rem;
}

/* ADR 0007 (#985 E4): CoordinatorApproval approve-to-start modal. Reuses the kiosk keypad
   classes above for the PIN entry; these style the violations list + centring. */
.prepared-approve-lead {
    margin: 0 0 0.4rem;
    font-size: 0.9rem;
}

.prepared-approve-violations {
    margin: 0 0 0.75rem;
    padding-left: 1.1rem;
    font-weight: 600;
    color: #8a3213;
}

.prepared-approve-content .kiosk-keypad,
.prepared-approve-content .kiosk-pin-dots {
    margin-left: auto;
    margin-right: auto;
}

.prepared-approve-error-slot {
    min-height: 1.75rem;
    margin: 0.4rem 0;
}

/* Shared password / PIN length-hint styles. Used by /signup, /reset-password,
   and the kiosk PIN setup card so the live feedback pattern is consistent. */
.signup-password-hint {
    font-size: 0.82rem;
    margin-bottom: 0;
}

.signup-password-hint-neutral {
    color: #6c757d;
}

.signup-password-hint-short {
    color: #8a5a00;
}

.signup-password-hint-ok {
    color: #1e7e34;
}

/* Tile-grid bulk session check-in (#194) */
.bulk-check-in-hint {
    color: #4a5870;
    font-size: 0.88rem;
    margin: 0 0 0.6rem 0;
}
/* Single-tap A-Z letter filter for the bulk-add modal (#200) */
.bulk-check-in-letters-special {
    display: flex;
    gap: 0.3rem;
    margin: 0 0 0.4rem 0;
}
/* 26 letters across 13 columns -> exactly two full rows. */
.bulk-check-in-letters {
    display: grid;
    grid-template-columns: repeat(13, 1fr);
    gap: 0.3rem;
    margin: 0 0 0.7rem 0;
}
.bulk-check-in-letter {
    /* #909 touch-target polish: >= ~30px tappable box (1.9rem at a 16px root), content
       centred. inline-flex so the min-height applies inside the grid cells too. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    min-width: 1.9rem;
    min-height: 1.9rem;
    padding: 0.5rem 0.35rem;
    border: 1px solid #c5cfde;
    border-radius: 6px;
    background: #fff;
    color: #1f3f5e;
    font-size: 1rem;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    transition: background 80ms ease, border-color 80ms ease, color 80ms ease;
}
.bulk-check-in-letters-special .bulk-check-in-letter {
    padding-left: 0.9rem;
    padding-right: 0.9rem;
}
.bulk-check-in-letter:hover:not(:disabled) {
    border-color: #2962ff;
    background: #f4f8ff;
}
.bulk-check-in-letter.active {
    background: #2962ff;
    border-color: #2962ff;
    color: #fff;
}
.bulk-check-in-letter:disabled {
    opacity: 0.3;
    cursor: default;
}
.bulk-check-in-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
    grid-auto-rows: min-content;
    gap: 0.5rem;
    /* FIXED height (not min/max) so the pane is exactly the same size whether the
       filter shows 1 tile or 50 - it never shrinks/shifts when you pick a letter
       (#200 feedback). Tiles top-align; the list scrolls inside when long. */
    height: 50vh;
    overflow-y: auto;
    align-content: start;
}
.bulk-check-in-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.35rem;
    padding: 0.5rem 0.3rem;
    border: 1px solid #c5cfde;
    border-radius: 8px;
    background: #fff;
    cursor: pointer;
    position: relative;
    text-align: center;
    transition: border-color 80ms ease, background 80ms ease, box-shadow 80ms ease;
}
/* Tone each tile by gender, matching the waiting-queue chips (blue = men, pink =
   women). The gender tone is KEPT through hover AND selection — interaction is shown
   with a shadow / ring (+ the check glyph), NOT by replacing the gender background,
   which would wash every tile blue regardless of gender. */
.bulk-check-in-tile.gender-male {
    background: #dfeeff;
    border-color: #8fb6e0;
}
.bulk-check-in-tile.gender-female {
    background: #ffe4f3;
    border-color: #e3a4c7;
}
.bulk-check-in-tile:hover {
    box-shadow: 0 2px 6px rgba(13, 44, 85, 0.18);
}
.bulk-check-in-tile.selected {
    box-shadow: inset 0 0 0 2px #2962ff;
}
.bulk-check-in-tile:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}
.bulk-check-in-tile-photo {
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    object-fit: cover;
    background: #e6ebf3;
}
.bulk-check-in-tile-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 1rem;
    color: #4a5870;
}
.bulk-check-in-tile-name {
    font-size: 0.78rem;
    line-height: 1.1;
    color: #28344a;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    word-break: break-word;
}
.bulk-check-in-tile-check {
    position: absolute;
    top: 0.2rem;
    right: 0.3rem;
    width: 1.1rem;
    height: 1.1rem;
    border-radius: 50%;
    background: #2962ff;
    color: #fff;
    font-size: 0.72rem;
    line-height: 1.1rem;
    text-align: center;
}

/* CSV import self-service panel on Members (#95) */
.member-import-panel {
    border: 1px solid #c5cfde;
    border-radius: 8px;
    background: #f8fafc;
    padding: 1rem 1.2rem;
    margin: 0.5rem 0 1rem 0;
}
.member-import-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}
.member-import-input-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0.5rem 0;
    flex-wrap: wrap;
}
.member-import-file-btn {
    position: relative;
    overflow: hidden;
    cursor: pointer;
    margin: 0;
}
.member-import-file-input {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
    width: 100%;
    height: 100%;
}
.member-import-file-name {
    font-size: 0.85rem;
    color: #4a5870;
    word-break: break-all;
}
.member-import-textarea {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.85rem;
}
.member-import-summary {
    font-size: 0.9rem;
}
.member-import-preview {
    max-height: 22rem;
    overflow-y: auto;
}

/* #393: the floating "Feedback" button (.quick-feedback-fab, position:fixed bottom-right,
   z-index 1900) sat on top of the score-entry popover and its Save control, obscuring score
   entry during a live session. Hide it whenever an overlay that the operator is actively
   working in is open - the score-entry popover (which renders .score-combo-backdrop) or any
   Bootstrap-style modal (.modal.show). The Feedback button reappears once the overlay closes. */
body:has(.score-combo-backdrop) .quick-feedback-fab,
body:has(.modal.show) .quick-feedback-fab {
    display: none !important;
}

/* #822: unsupported / out-of-date browser warning. The banner is always server-rendered
   (in App.razor, static SSR) so it exists even when the interactive runtime never starts,
   but is hidden until <html> gets the .browser-unsupported class from the feature-detect
   preflight or the liveness fallback. Pinned to the TOP so an old-device user sees it at
   once (unlike #blazor-error-ui which sits at the bottom for a post-start error), and above
   every overlay so it is never obscured. Mirrors the lightyellow warning convention of
   #blazor-error-ui. */
#unsupported-browser-ui {
    display: none;
}

html.browser-unsupported #unsupported-browser-ui {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    box-sizing: border-box;
    padding: 0.7rem 1.25rem;
    background: lightyellow;
    color: #2d3540;
    color-scheme: light only;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
    font-size: 0.95rem;
    line-height: 1.4;
    /* Above every overlay (modal backdrops ~1040, score popover 1200, feedback FAB 1900,
       and #blazor-error-ui uses the max) so the warning and its Reload link are always visible. */
    z-index: 2147483646;
}

#unsupported-browser-ui .unsupported-browser-text {
    flex: 1 1 16rem;
    min-width: 0;
}

#unsupported-browser-ui .unsupported-browser-reload {
    flex: 0 0 auto;
    font-weight: 600;
    color: #1861ac;
    text-decoration: underline;
}

/* ── Effective Permissions (Kiosk Axis 2, epic #325) — Locked-visible affordance ──
   Owner feedback (2026-07-15): the earlier amber/primary-looking treatment (amber border + text +
   wash + an "Organiser only" caption underneath) read as too loud. A Locked-visible control should
   now read QUIETLY as "not available in player mode" — a plain, muted, disabled-LOOKING default
   button (grey/secondary), NOT amber and NOT primary. It stays TAPPABLE (opens the organiser-only
   explainer that points to "Enter Admin Mode") — it only LOOKS disabled; we deliberately do NOT set
   the native `disabled` attribute, which would kill the tap-for-explainer. Discoverability of "how
   to use these" is now carried by the always-visible "Enter Admin Mode" toolbar button, so the
   per-control affordance can be quiet. A small, muted lock glyph (see .kiosk-locked-control .lock-icon
   below) signals "locked, unlockable in Admin Mode" rather than looking inertly broken.
   Higher specificity than base .btn / .btn-outline-secondary so it wins without !important. */
.kiosk-locked-control.btn-outline-secondary,
.kiosk-locked-control.btn {
    border-color: #ced4da;
    color: #6c757d;
    background-color: #f8f9fa;
    /* Reads as a resting disabled control, but is still a live tap target for the explainer. */
    cursor: pointer;
    opacity: 1;
}

.kiosk-locked-control.btn-outline-secondary:hover,
.kiosk-locked-control.btn-outline-secondary:focus,
.kiosk-locked-control.btn:hover,
.kiosk-locked-control.btn:focus {
    /* Barely-there hover so it stays quiet but confirms it IS tappable (for the "why"). */
    border-color: #adb5bd;
    color: #6c757d;
    background-color: #eef0f2;
}

/* The lock glyph on a Locked-visible control: SMALL + MUTED (unobtrusive). LockIcon renders a
   16px amber SVG (its intrinsic amber is correct elsewhere — e.g. the game-prep slot locks — so we
   do NOT touch the component); here we scope it down to ~12px and desaturate it to grey so it reads
   as a subtle "locked" hint, not an amber flag. */
.kiosk-locked-control .lock-icon {
    width: 12px;
    height: 12px;
    filter: grayscale(1) opacity(0.65);
    vertical-align: -1px;
}

/* Layout wrapper for a Locked-visible control. The "Organiser only" caption that used to stack
   underneath has been REMOVED (owner feedback 2026-07-15) — the wrapper now just holds the single
   control; kept so the toolbar/panel gap rules below keep working without touching every call site. */
.kiosk-locked-control-wrap {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.1rem;
}

/* Restore the inter-control gap the wrapper otherwise loses. The normal .end-session-trigger
   carries margin-left: 0.25rem on the button itself, but under kiosk that button is nested inside
   .kiosk-locked-control-wrap, so in the toolbar the gap must sit on the wrapper (the flex item). */
.clubSession-kiosk-actions .kiosk-locked-control-wrap {
    margin-left: 0.25rem;
}

/* Persistent KIOSK badge (● KIOSK) — shown while the kiosk HAT is active (S2a). Copies the v2
   player-board pill shape in a distinct board accent. Solid #0b7285 (NOT a gradient — a gradient's
   lighter stop drops the label below AA); white on #0b7285 = 5.59:1, clears WCAG AA for the small
   label. Distinct class from the Axis-1 body.kiosk-mode fullscreen mechanism so the two axes never
   collide. */
.player-board-pill {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    background: #0b7285;
    color: #ffffff;
    border: 1px solid rgba(255, 255, 255, 0.4);
    border-radius: 999px;
    padding: 0.22rem 0.62rem;
    font-size: 0.74rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.07em;
    flex: 0 0 auto;
    white-space: nowrap;
    box-shadow: 0 1px 6px rgba(13, 16, 20, 0.3);
}

/* S2b-i β + UX rework: the pill becomes the EXIT affordance — a real <button> (mirrors the Axis-1
   super-user "Administer" pill). Reset native button chrome, keep the pill shape, guarantee a ≥40px
   touch target + pointer cursor + a visible focus ring. UX-1/UX-4: it must READ as tappable, so it
   also carries a "tap to exit" sub-label + a chevron — no longer an inert status badge. */
.player-board-pill-btn {
    cursor: pointer;
    min-height: 2.5rem; /* ≥40px courtside/touch target */
    font-family: inherit;
    padding: 0.22rem 0.7rem; /* a touch more room for the sub-label + chevron */
}

.player-board-pill-btn:hover {
    background: #095c6b;
}

.player-board-pill-btn:focus-visible {
    outline: 2px solid #ffffff;
    outline-offset: 2px;
}

/* The lock glyph (now the shared <LockIcon>, UX-13) sizing inside the pill. */
.player-board-lock {
    display: inline-flex;
    align-items: center;
    line-height: 1;
}

.player-board-lock svg {
    width: 0.85rem;
    height: 0.85rem;
}

.player-board-pill-label {
    font-weight: 700;
}

/* the affordance sub-label — smaller + lighter than the KIOSK label so the pill reads as a control
   without shouting. UX-7: bumped to 0.72rem for courtside legibility. */
.player-board-pill-sub {
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    opacity: 0.9;
    text-transform: lowercase;
    margin-left: 0.1rem;
}

/* #898: the one-tap "Tap for full screen" re-enter affordance, shown on the LOCKED board (kiosk or
   Admin Mode) only while windowed + fullscreen is supported. Courtside-legible + a big touch target,
   but NOT alarming — the calm kiosk teal (matches .player-board-pill), never an amber warning colour.
   Reuses the kiosk accent language; no new brand. */
.clubSession-reenter-fullscreen {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    margin-top: 0.3rem;
    min-height: 2.75rem; /* ≥44px courtside/touch target */
    padding: 0.4rem 0.95rem;
    background: #0b7285; /* same calm teal as the KIOSK pill */
    color: #ffffff;
    border: 1px solid rgba(255, 255, 255, 0.4);
    border-radius: 999px;
    font-family: inherit;
    font-size: 0.98rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 1px 6px rgba(13, 16, 20, 0.3);
}

.clubSession-reenter-fullscreen:hover {
    background: #095c6b;
}

.clubSession-reenter-fullscreen:focus-visible {
    outline: 2px solid #ffffff;
    outline-offset: 2px;
}

.clubSession-reenter-fullscreen-glyph {
    font-size: 1.1rem;
    line-height: 1;
}

/* S2b-i rework: the ADMIN MODE pill — a DISTINCT (amber/elevated) colour from the calm teal KIOSK pill,
   so "you are elevated, changes are live" reads at a glance. Mirrors the Axis-1 super-user write-pill's
   elevated-amber language. Not a button (the banner carries the controls). */
.player-board-pill-admin {
    background: #9a6a00;
    border-color: rgba(255, 255, 255, 0.5);
}

/* The Admin-Mode banner under the session title: the live-changes line + Exit Admin Mode (primary), with
   "Stop kiosk mode" demoted to its own row (MUST-3). Amber-tinted so it is clearly a different state from
   Locked kiosk. */
.clubSession-admin-mode-banner {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin-top: 0.25rem;
    padding: 0.35rem 0.6rem;
    border: 1px solid #9a6a00;
    border-radius: 0.4rem;
    background: #fdf6e3;
}

/* Row 1: the live-changes text + the OBVIOUS first target (Exit Admin Mode). */
.clubSession-admin-mode-primary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/* Row 2 (MUST-3): "Stop kiosk mode" separated by a top divider + demoted to a link, so a hurried tap
   can't end the shared-device session by mistake. */
.clubSession-admin-mode-stop-row {
    border-top: 1px dashed #cbb27a;
    padding-top: 0.3rem;
}

.admin-mode-stop-kiosk-btn {
    color: #8a5a00;
    font-size: 0.78rem;
    text-decoration: underline;
    padding: 0 0.1rem;
}

.clubSession-admin-mode-text {
    font-size: 0.82rem;
    color: #6b4a00;
    font-weight: 600;
}

/* MUST-4: at <=15s idle-remaining, the banner shifts to a prominent amber warning. */
.clubSession-admin-mode-banner.is-warning {
    background: #fff3cd;
    border-color: #c9860a;
    box-shadow: 0 0 0 2px rgba(201, 134, 10, 0.25);
}

.clubSession-admin-mode-banner.is-warning .clubSession-admin-mode-text {
    color: #7a4f00;
    font-weight: 700;
}

/* MUST-5: the brief "Returned to player mode" flash on the Admin Mode -> Locked transition. */
.clubSession-returned-flash {
    display: inline-block;
    margin-top: 0.2rem;
    padding: 0.2rem 0.55rem;
    border-radius: 0.35rem;
    background: #0b7285;
    color: #ffffff;
    font-size: 0.8rem;
    font-weight: 600;
    animation: returned-flash-fade 3s ease-in forwards;
}

@keyframes returned-flash-fade {
    0% { opacity: 0; }
    10% { opacity: 1; }
    80% { opacity: 1; }
    100% { opacity: 0; }
}

.player-board-dot {
    width: 0.46rem;
    height: 0.46rem;
    border-radius: 50%;
    background: #ffffff;
    animation: player-board-dot-pulse 1.3s ease-in-out infinite;
}

@keyframes player-board-dot-pulse {
    0% { transform: scale(1); opacity: 1; }
    50% { transform: scale(1.25); opacity: 0.72; }
    100% { transform: scale(1); opacity: 1; }
}

/* ── Club-membership chips (#443, SPEC-20 §8, R-C12.25) ───────────────────────────────
   ONE chip rendering shared by /users and /admin/signups. Lifted VERBATIM out of
   SystemUsers.razor.css (where Blazor CSS isolation scoped them to that one component, so
   a second page reusing the markup rendered unstyled plain text with no build error and no
   failing test). Global + neutrally named, the "two surfaces must not diverge" requirement
   is true by construction. No declaration was altered in the move.
   Per-surface tweaks belong in that surface's scoped .razor.css (which wins on specificity
   AND load order) - never by editing or forking these three rules. */
.club-chip-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
}

.club-chip {
    background: #eef2f7;
    border: 1px solid #d7e0ec;
    border-radius: 0.35rem;
    padding: 0.1rem 0.45rem;
    font-size: 0.82rem;
    white-space: nowrap;
}

.club-chip-role {
    color: #4e6f8e;
    font-size: 0.78rem;
    font-style: normal;
    margin-left: 0.25rem;
}

/* ── SPEC-32 I2-S1a (#314): rating-movement colour. SPLIT OWNERSHIP, promoted to global. ──
   One class pair with TWO emitters that now sit in different components: the session recap's
   aggregate "Rating change" column (SessionRecap.razor, caller-owned) and the per-game delta spans
   inside the shared MatchList. Blazor CSS isolation scopes a rule to ONE component, so a pair used by
   two can be neither relocated (breaks the caller) nor duplicated (two sources of truth for one
   colour) — it has to be promoted.

   Their own definition already documented the dual use before this slice ("used by the aggregate
   Rating-change column AND the drill-in per-game delta"), so the scoping to SessionRecap was
   incidental rather than intended. They are semantic colour carrying a WCAG contrast note, which
   wants exactly one home.

   Values unchanged. #1f7a54 was darkened from #2f9e6f to meet WCAG AA (>=4.5:1) as body text on
   white — it gives 5.29:1. Do not lighten it. */
.rating-delta-up {
    color: #1f7a54;
    font-weight: 600;
}

.rating-delta-down {
    color: #c0392b;
    font-weight: 600;
}

/* ── SPEC-32 I4-S2 (#670), R-I2-44.1: THE `vs expected` CELL AND THE CONFIDENCE METER, PROMOTED. ──

   These rules were in PartnershipReport.razor.css. They are here now, values unchanged, because the
   profile's counterpart tables render the same figure with the same confidence beside it and a Blazor
   component-scoped stylesheet is UNREACHABLE from any other component — a second host referencing
   `.vsexp` or `.conf` by name would get a correct-looking class list and no styling at all, silently.
   That is R-I2-44's whole subject and it is the same move I2-S1a made two blocks up for
   `.rating-delta-up`, for the same mechanical reason.

   *** A FINDING CAME OUT OF THE MOVE, AND IT IS NOT THIS SLICE'S TO HIDE. ***
   `.conf`'s rules NEVER APPLIED ON /reports/partnerships. Blazor's CSS isolation stamps its scope
   attribute on elements the .razor file DECLARES AS MARKUP; the report builds its confidence meter
   from a RenderTreeBuilder fragment in its @code block, and builder-emitted elements carry no scope
   attribute — so `.conf[b-6s1tjoc5dq]` matched nothing and the four bars have been rendering as empty
   zero-width <i> elements since the meter shipped. Only the word was visible. Promoting the rules
   makes them apply, so the bars appear on that page for the first time. Recorded in spec § 2.3 with
   the browser evidence and raised for the Owner rather than presented as a refactor: R-I2-44.2 says a
   visible change on the report means a value was altered in passing, and NO VALUE IS ALTERED HERE —
   the change is a dead definition becoming reachable, which is precisely what R-I2-44 asks for.
   `.vsexp`'s rules DID apply: that cell is markup in the .razor file and carries the scope attribute.

   THE COLOURS ARE TOKENS AND THE GEOMETRY IS NOT. The two hosts must not drift on the ink — that is
   R-I2-47.4's one-banding-expression rule arriving in the stylesheet — so over / under / even are
   named once here. The bar heights and gaps are a single definition rather than a token set because
   there is exactly one meter shape in the product and a token per bar height would be ceremony. */
:root {
    --vsexp-over: #136c3e;
    --vsexp-under: #b3261e;
    --vsexp-even: #52617a;
    --vsexp-inputs: #6c7a8c;
    --confidence-bar: #cdd7e4;
    --confidence-bar-on: #3f7ac0;
    --confidence-label: #6c7a8c;
}

.vsexp {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-end;
    line-height: 1.15;
}

.vsexp .delta {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    font-size: 0.875rem;
}

.vsexp .delta.over { color: var(--vsexp-over); }
.vsexp .delta.under { color: var(--vsexp-under); }
.vsexp .delta.even { color: var(--vsexp-even); }

/* I4-S3's sub-line. Promoted with its siblings rather than left behind: R-I2-44 names it among the
   four definitions caught by the same trap, and splitting one cell's rules across two stylesheets by
   slice boundary is how the next reader loses track of which file owns the cell. */
.vsexp .inputs {
    font-size: 0.66rem;
    color: var(--vsexp-inputs);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.conf {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
}

.conf .bars {
    display: inline-flex;
    gap: 2px;
    align-items: flex-end;
    height: 14px;
}

.conf .bars i {
    width: 4px;
    background: var(--confidence-bar);
    border-radius: 1px;
    display: inline-block;
}

.conf .bars i.on { background: var(--confidence-bar-on); }
.conf .bars i:nth-child(1) { height: 5px; }
.conf .bars i:nth-child(2) { height: 8px; }
.conf .bars i:nth-child(3) { height: 11px; }
.conf .bars i:nth-child(4) { height: 14px; }

.conf small {
    color: var(--confidence-label);
    font-size: 0.68rem;
}

/* #669: THE METER'S OWN NAME, for a host that renders it with no column header above it.
   HERE AND NOT IN profile.css FOR THE REASON THE WHOLE BLOCK ABOVE IS HERE: the element is emitted by
   ConfidenceMeter's RenderTreeBuilder fragment, so it carries no component scope attribute and a
   scoped stylesheet could never reach it — the finding recorded at the top of this block, applied
   rather than re-learned.
   It reads the SAME two values `.conf small` does rather than restating them, so the label and the
   level word cannot drift into two greys at two sizes. */
.conf .conf-label {
    color: var(--confidence-label);
    font-size: 0.68rem;
}

/* The abbreviation's expansion: present for a screen reader, absent from layout.
   `position: absolute` is what makes it cost NOTHING here — an out-of-flow child of a flex container
   is skipped by the flex layout entirely, so it takes no track and no `gap`. A `clip`/`width: 0`
   variant would still be a flex item and would still be given a gap, which on a cell measured at
   4.32px of slack is the difference between three lines and four. The off-screen shape is the one
   `.signup-website-field` above already uses; it is stated again rather than shared because that one
   is a honeypot and this is a label, and a later edit to either must not silently move the other. */
.conf .conf-label-full {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* ══ SPEC-20 §10 — the activity surface's colour tokens (R10.7) ═══════════════════════════════
   EVERY colour on /admin/activity/next resolves through a name declared HERE, and no literal
   colour value appears in any rule outside this block — including the chart JS, which reads these
   with getComputedStyle at draw time rather than carrying hex of its own. Today the four series
   colours are repeated literals across five interop calls in UserActivity.razor:821-829; this is
   where that stops.

   IT LIVES IN app.css, NOT IN A .razor.css, AND THAT IS NOT A PREFERENCE. Blazor scoped CSS
   rewrites selectors with the scope attribute, so a `:root` rule in a component stylesheet becomes
   `:root[b-xxxxx]` and matches nothing. Custom properties inherit through the cascade, so defining
   them here and READING them from scoped component CSS is the shape that works — the same
   conclusion ClubSessionDashboard.razor.css:1547 and PartnershipReport.razor.css:402 record
   reaching the hard way.

   THERE IS DELIBERATELY NO `prefers-color-scheme` BLOCK (§10.14 Q1 / R10.7). The approved mockup
   ships a full dark palette; copying it would make this the one admin page that inverts on a
   dark-OS machine while /admin/signups and /clubs beside it stay light — a page half-inheriting a
   theme nobody built. The surface renders identically whatever the OS reports, and an app-wide
   dark theme is its own question.

   THE CONTRAST FLOOR (R10.7, §10.15 condition 3) IS APPLIED HERE, and it was applied at I2 rather
   than I1 because I1 shipped without it. The argument for doing it before the remaining three
   panels is condition 3's own: I2 spreads these tokens across four panels and a summary strip, so
   a floor applied afterwards is a re-authoring rather than a correction.

   `--activity-faint` is RETIRED, not renamed. All three of its roles were text — `.stat-note`,
   `.as-of`, and `ticks.color` on both chart axes, and a tick label drawn on a canvas is text —
   so there was nothing left to hold to 1.4.11's 3:1. They all take `--activity-muted`, which is
   darkened from #6b7c90 (4.09:1, a narrow fail) to #5f7085 (4.85:1) against `--activity-card`,
   the darkest ground any of this text sits on. If a genuinely non-text role wants a lighter value
   later it gets its own token, named for that role, and is held to 3:1.

   THE FOUR SERIES TOKENS ARE NON-TEXT ROLES AND STAY AT THE MOCKUP'S VALUES. They colour bars,
   lines and swatches, where 1.4.11's 3:1 applies and all four clear it (session 3.60:1, recap
   3.81:1, club 5.00:1, player 6.39:1 on the card). Two of them do NOT clear 4.5:1, which is why
   the summary strip renders its figures in `--activity-ink` with a series-coloured swatch rather
   than in the series colour itself — see ActivitySummaryStrip.razor.css. */
:root {
    --activity-session: #b5761f;
    --activity-recap: #2f8f6b;
    --activity-club: #4c6f95;
    --activity-player: #6f4c95;

    --activity-ink: #2b4a73;

    /* THE HEADING INK — the mockup's `--ink-2`, restored by the §10.17 sweep (row 10). The build
       collapsed the mockup's two inks into one, so every panel title and every stat figure on the
       page sat one step lighter than the approved target with nothing recording the decision.

       Restoring it moves WITH R10.7's contrast floor rather than against it, and this is measured
       rather than assumed: against `--activity-card` (the darkest ground any of it sits on) the
       headings go from **8.62:1** on `--activity-ink` to **11.84:1** on this. It is the one token
       change in this feature that needed no argument about whether it was safe.

       TWO INKS, TWO ROLES, and do not fold them back: `--activity-ink` is BODY ink — table cells,
       the checkbox label, the panel's running prose. This one is the ink of the things you scan for:
       panel titles, stat figures, the summary numeral. The hierarchy between them is the whole point,
       which is exactly what one ink could not express. (Same warning, same reason, as the two gender
       pairs at the top of this file.) */
    --activity-ink-2: #1e3555;

    --activity-muted: #5f7085;
    --activity-card: #f7faff;
    --activity-card-2: #eef4fc;
    --activity-line: rgba(120, 145, 175, 0.30);
    --activity-line-2: rgba(120, 145, 175, 0.16);
    /* THE STRUCTURAL RULE — added at I3, and it is the fourth ("compound") row of §10.17.
       `--activity-line-2` at 0.16 alpha composites over `--activity-card` to about rgb(227,233,242)
       on rgb(247,250,255): **~1.17:1**, and that number was carrying the whole of "these are three
       separate stat tiles". The transcription from the mockup was faithful and the mockup was right
       — there `.panel` has no ground, so the `.stats` block sat on the plain white page and the
       region was framed before the dividers mattered. This build treats the panel as a full card
       (divergence #17, deliberate and accepted), so the outer border drawn in that token vanished
       into a background of the same colour and the hairline was left alone.

       SOLID, NOT A STRONGER ALPHA, because alpha cannot reach the floor: 0.30-0.40 over this card
       lands at roughly 1.3-1.5:1. This is the SAME HUE the three line tokens are alpha variants of —
       rgb(120,145,175) at full strength — so it adds no new colour to the surface, it removes the
       alpha that was destroying one. Measured **3.11:1 against --activity-card**, clearing 1.4.11's
       3:1 for a non-text structural boundary. It has no text role and owes no 4.5:1. */
    --activity-rule: #7891af;

    --activity-grid: rgba(120, 145, 175, 0.22);

    /* THE GO-LIVE MARK — added at I3 signoff, and it is §10.15 condition 3's remedy applied to the
       case that condition was written for: *"if a genuine non-text role wants a lighter value later
       it gets its own token, named for that role, and is held to 3:1."*

       I3 gave `--activity-grid` a SECOND role and took the first one's value. As a y-axis gridline
       that token is legitimately outside 1.4.11 — the tick labels carry the values and the line is
       scaffolding. As the go-live mark it is the SOLE CARRIER OF A FACT, which is squarely 1.4.11's
       "parts of graphics required to understand the content", and at rgba(120,145,175,0.22) it
       composites over `--activity-card` to **~1.24:1** — below the 1.17:1 that §10.17 row 28 REJECTED
       in this same increment, on this same ground, for carrying this same kind of fact. The
       arithmetic was three declarations above it the whole time, in `--activity-rule`'s own comment:
       alpha cannot reach the floor.

       Solid, **3.11:1 measured** against `--activity-card`. Same hue and same value as
       `--activity-rule` today and deliberately NOT that token, on the `--activity-primary` /
       `--activity-club` precedent: a structural divider between stat tiles and a mark denoting a
       club going live are two different facts, and the next person to retune either must not
       silently move the other. `--activity-grid` keeps the gridline role it was named for. */
    --activity-annotation: #7891af;
    --activity-hatch: rgba(120, 145, 175, 0.55);
    /* THE CHROME ACCENT — the mockup's `--primary`, and the §10.17 sweep's central finding made
       repairable. Five separate "small misses" in that register (rows 8, 11, 12, 13, and the accent
       rule in row 7) were ONE unrecorded decision surfacing five times: the mockup spends `--primary`
       on the segmented control's pressed fill, the info button's open state, the info body's accent
       rule, the checkbox and the focus ring, and the build gave it no counterpart at all. All five
       now read from here, so the decision exists in one place and a sixth chrome role has somewhere
       to go.

       IT IS THE SAME VALUE AS `--activity-club` AND IS DELIBERATELY NOT THAT TOKEN. #4c6f95 is
       already in this block as the club-signups SERIES colour. Pointing chrome at a series token
       would re-record the mistake the sweep just found, in the other direction: the next person to
       retune the club series would silently repaint every pressed control on the page, and the next
       person to restyle the chrome would silently move a data colour. They are equal today and they
       are not the same fact. (The two gender pairs at the top of this file are the same lesson.)

       MEASURED, not assumed. White on it is **5.23:1**, so the mockup's white-on-primary pressed
       fill clears 1.4.3's 4.5:1 for the 0.78rem segment labels without reopening R10.7. As a
       non-text accent — the info body's left rule, the focus ring — it is 4.73:1 against
       `--activity-card-2`, comfortably past 1.4.11's 3:1. */
    --activity-primary: #4c6f95;

    /* The ink that sits ON --activity-primary. A token rather than `#fff` in the stylesheet only
       because R10.7 admits no literal colour outside this block — white is still white, but the rule
       has no exception clause and does not need one. */
    --activity-on-primary: #ffffff;

    --activity-up: #2f7d5a;
    --activity-down: #b5504a;

    /* THE FLOATING-LAYER SEPARATOR, and the only shadow on this surface. The mockup gives one to
       `.now` (mockup line 137) and the build dropped it there deliberately — four stacked cards each
       casting a shadow is noise, and the surface reads better flat. This token exists for the ONE
       element that genuinely floats: the sticky control rail, which passes over the panels as the
       page scrolls. An opaque background alone says "solid"; it does not say "in front". Named for
       the role rather than for `.rail` so a second floating element can share it, and declared here
       rather than in the scoped stylesheet because it carries a literal colour (R10.7).

       Stronger than the mockup's `0 1px 2px rgba(43,74,115,0.06)`, which was sized for a card at
       rest on the page ground, not for one sliding over content. */
    --activity-shadow: 0 2px 6px rgba(43, 74, 115, 0.10);
}
