/* The public storefront.
 *
 * This is the only stylesheet a parent ever loads. The admin's style.css is a
 * separate world and the two must not be mixed: the stations are a working
 * tool on a bench, this is a shop.
 *
 * Everything is mobile first. The rules below assume one narrow column and
 * widen at the two breakpoints at the bottom of the file.
 *
 * Montserrat is served from this app. The landing pages the league already
 * runs self-host the same two files, so the store sits beside them without a
 * round trip to a font CDN and without handing a third party a log line for
 * every parent who opens their team's link.
 */

@font-face {
    font-family: "Montserrat";
    src: url("/static/fonts/Montserrat-Regular.woff2") format("woff2"),
         url("/static/fonts/Montserrat-Regular.ttf") format("truetype");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Montserrat";
    src: url("/static/fonts/Montserrat-SemiBold.woff2") format("woff2"),
         url("/static/fonts/Montserrat-SemiBold.ttf") format("truetype");
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}

/* Oswald, the headings and the bar: self-hosted rather than from Google
 * Fonts, the same variable latin file Google served (weights 500 to 700,
 * the only ones used). OFL, licence beside it in static/fonts. The base
 * template preloads exactly this URL. */
@font-face {
    font-family: "Oswald";
    src: url("/static/fonts/Oswald-Latin-Variable.woff2") format("woff2");
    font-weight: 500 700;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
        U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
        U+FEFF, U+FFFD;
}

/* The <picture> around a photograph that has WebP copies (photo_variants)
 * takes no box of its own, so the <img> inside is laid out exactly as it
 * was without one: every rule written for the bare img still applies. */
.photo-picture { display: contents; }

/* One palette, one place. A brand change is an edit to this block.
 *
 * --brand is the league's light blue. It is bright enough that white text on
 * it fails contrast, so it is used as a fill under dark ink, never as a
 * background for white type. --brand-deep is the darkened version used for
 * link text, focus rings and anything that has to be legible on paper.
 *
 * --accent is the league's yellow. On white it measures about 1.7:1, which
 * is nowhere near readable, so it appears only as a fill or a rule with
 * --accent-ink on top of it, never as text.
 */
:root {
    --brand: #4dbbff;
    --brand-deep: #0a6a9e;
    --brand-wash: #eaf6ff;
    --accent: #facc15;
    --accent-ink: #4a3600;
    --accent-wash: #fff8dd;

    --ink: #14181f;
    --ink-soft: #4c5663;
    --paper: #ffffff;
    --page: #f4f6f9;
    --rule: #e6eaef;
    --rule-strong: #c2cbd6;
    /* The page's stripes, after cfaorangecounty.com: wide vertical bands,
     * alternating --page and this, a shade darker, so the motif reads only
     * as a quiet texture behind the cards. */
    --stripe: #eceff4;

    --good: #10633c;
    --good-wash: #e9f6ef;

    /* The pair --good has always had a counterpart for: something the
     * shopper asked for that did not happen, and has to act on. Darker than
     * the league's red so the sentence on the wash clears contrast. */
    --bad: #b02420;
    --bad-deep: #7d1a17;
    --bad-wash: #fdf0ef;
    --bad-rule: #f0c2bd;

    /* The two states a jersey number can be in when somebody else has it.
     * Red for bought, yellow for a returning player's reservation, which is
     * what the owner asked for in those words. Each is an ink and the fill
     * it is read on, and each pair measures well past AA: 7.79:1 for the red
     * and 7.50:1 for the yellow. The colour is never the only signal -
     * every row says "Taken by" or "Reserved for" and the name in its own
     * text - but a parent who is reading the colour has to be able to read
     * the words on it as well. */
    --taken-ink: #8f1d1d;
    --taken-wash: #fdecec;
    --reserved-ink: #6a4a00;
    --reserved-wash: #fff6d9;

    /* The type scale. Montserrat 400 for prose, 600 for anything that
     * names something. Nothing on the storefront uses a size that is not
     * one of these. */
    --text-xs: 0.75rem;
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.5rem;

    --radius: 10px;
    --radius-sm: 6px;

    /* Depth is three layers, not one blur: a hairline contact shadow that
     * separates the card from the page, a short ambient one, and a long
     * soft one that does the actual lifting. A single large blur reads as
     * fog; these read as paper on a table. */
    --shadow: 0 1px 1px rgba(20, 24, 31, 0.05),
              0 2px 4px rgba(20, 24, 31, 0.04),
              0 8px 20px rgba(20, 24, 31, 0.05);
    --shadow-lift: 0 1px 2px rgba(20, 24, 31, 0.06),
                   0 6px 12px rgba(20, 24, 31, 0.06),
                   0 18px 38px rgba(20, 24, 31, 0.10);

    /* Overridden per team on <body>. The team's kit colour is a trim on this
     * page, not the page's colour scheme: the store must read as the league's
     * whichever team's link opened it. */
    --team-accent: #1f3a5f;

    /* The league's brand for the departments (owner's mockup, 2026-10-20):
     * the navy header and the hubs' footer, ESL red for the search button
     * and the sale tag, gold for the featured entry, the current entry's
     * rule and the hero's bar and button. Gold is a light on navy or a fill
     * under navy ink, never text on paper, where it measures about 1.6:1. */
    --navy: #13213f;
    --navy-raised: #22356a;
    --esl-red: #b3232e;
    --gold: #f5c518;
    --heading-font: "Oswald", "Arial Narrow", "Helvetica Neue", Arial, sans-serif;
}

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

body.storefront {
    margin: 0;
    font-family: "Montserrat", system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
    font-size: 16px;
    line-height: 1.5;
    color: var(--ink);
    /* The page is not flat white and the cards are. That one step of tone is
     * what makes a card look like an object rather than a border drawn on a
     * document, and it is the cheapest depth a shop can buy. The wash is
     * cooler at the top, under the masthead, and settles to the page tone by
     * the time the grid starts. */
    background-color: var(--page);
    /* The owner's "subtle stripes", like cfaorangecounty.com's: vertical
     * bands 38px wide (the CFA site's own width), --page and --stripe in
     * turn, under the cool wash at the top. The site's are navy on navy;
     * these are the store's light page colour, low in contrast. Cards,
     * panels and forms all paint --paper over them, so the bands are only
     * ever behind the page, never behind words. Gradients, not an image. */
    background-image:
        linear-gradient(180deg, #e9eef5 0, rgba(244, 246, 249, 0) 26rem),
        repeating-linear-gradient(90deg, var(--page) 0 38px, var(--stripe) 38px 76px);
    background-repeat: no-repeat, repeat;
    -webkit-text-size-adjust: 100%;
}

body.storefront h1,
body.storefront h2,
body.storefront h3,
body.storefront h4 {
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.25;
}

/* Plain prose links take the brand colour and the semibold weight, and no
 * underline at rest: a rule under every address and every "keep shopping"
 * is what makes a page read as a document rather than a shop. Weight and
 * colour carry it standing still; the underline comes back on hover and on
 * focus, offset clear of the descenders, so the affordance is never lost to
 * anyone who is looking for it. Anything that is a link only because it
 * navigates — the masthead, the cart pill, the checkout button — sets its
 * own treatment below, and each of those selectors is written under
 * `body.storefront` so it outranks this one rather than losing to it. */
body.storefront a {
    color: var(--brand-deep);
    font-weight: 600;
    text-decoration: none;
}

body.storefront a:hover,
body.storefront a:focus-visible {
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
}

body.storefront img { max-width: 100%; }

/* Keyboard focus is visible everywhere, on a ring dark enough to be seen on
 * both the paper cards and the brand fills. */
body.storefront :focus-visible {
    outline: 3px solid var(--brand-deep);
    outline-offset: 2px;
    border-radius: 4px;
}

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

body.storefront .skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 20;
    background: var(--paper);
    color: var(--brand-deep);
    padding: 0.6rem 1rem;
    border-radius: 0 0 var(--radius) 0;
    font-weight: 600;
}

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

/* ------------------------------------------------------------------ */
/* Header                                                              */
/* ------------------------------------------------------------------ */

/* The navy header, the same on every storefront page (owner's mockup,
 * 2026-10-20): the league's combined mark going home, the departments, the
 * search box, and the account links and cart pill.
 *
 * On a phone it is three rows: the mark with the cart pill opposite it, the
 * bar, then the search box. The pill keeps the top row to itself so it is
 * the easy target. From 60rem it is one row and sticky; on a phone it is
 * not, because three rows pinned to the top of a 700px screen is a fifth of
 * the screen gone, and the basket is also on every store page's own rail.
 *
 * Every text colour on navy below is written with the navy behind it (or
 * under a selector that names the navy ancestor), so the contrast sweep in
 * tests/test_contrast_sweep.py measures it against what it really sits on. */
.store-header {
    position: relative;
    z-index: 10;
    background: var(--navy);
    box-shadow: 0 2px 10px rgba(19, 33, 63, 0.25);
}

.store-header-inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem 1rem;
    max-width: 72rem;
    margin: 0 auto;
    padding: 0.5rem 1rem 0.75rem;
}

body.storefront .site-brand {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    flex: none;
    order: 1;
}

body.storefront .site-brand:hover { text-decoration: none; }

body.storefront .site-brand:focus-visible { outline-color: var(--gold); }

.site-logo {
    display: block;
    height: 40px;
    width: auto;
}

.store-actions {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin-left: auto;
    flex: 0 0 auto;
    order: 2;
}

/* The search box: a white pill with a round red button, as on the mockup.
 * The pill draws the focus ring for the field inside it, so the field
 * itself does not draw a second one inside the first. */
.site-search {
    order: 4;
    flex: 1 1 100%;
    display: flex;
    align-items: center;
    gap: 0.25rem;
    min-height: 44px;
    padding: 0.2rem 0.2rem 0.2rem 1rem;
    background: var(--paper);
    border-radius: 999px;
}

.site-search:focus-within {
    outline: 3px solid var(--gold);
    outline-offset: 2px;
}

body.storefront .site-search-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    padding: 0.35rem 0;
    background: var(--paper);
    color: var(--ink);
    font: inherit;
    font-size: var(--text-sm);
}

body.storefront .site-search-input:focus-visible { outline: none; }

.site-search-input::placeholder { color: var(--ink-soft); opacity: 1; }

body.storefront .site-search-button {
    flex: none;
    display: inline-grid;
    place-items: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--esl-red);
    color: #ffffff;
    cursor: pointer;
}

body.storefront .site-search-button:hover { background: #941c25; }

/* The store's own band under the navy header: the old masthead, kept so a
 * store page still looks like itself. The team's kit colour is the stripe
 * at its top, the one place a team's own colour paints something. */
.store-band {
    background: linear-gradient(180deg, #ffffff 0%, #f7fafd 100%);
    border-bottom: 1px solid var(--rule);
}

.store-band::before {
    content: "";
    display: block;
    height: 4px;
    background: var(--team-accent);
}

.store-band-inner {
    max-width: 72rem;
    margin: 0 auto;
    padding: 0.35rem 1rem;
}

body.storefront .store-brand {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-width: 0;
    min-height: 44px;
    text-decoration: none;
    color: inherit;
}

body.storefront .store-brand:hover { text-decoration: none; }

.store-logo {
    height: 32px;
    width: auto;
    flex: none;
}

.store-titles { min-width: 0; }

.store-band h1 {
    font-size: var(--text-base);
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.team-line {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--ink-soft);
    display: flex;
    align-items: center;
    gap: 0.35rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.team-swatch {
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 50%;
    background: var(--team-accent);
    border: 1px solid rgba(20, 24, 31, 0.2);
    flex: none;
}

/* The cart pill, "Cart · 2". The count is also read out in the link's
 * accessible name, so a screen reader hears "Cart, 2 items" rather than a
 * bare number. White on the navy header: the page's second button, and the
 * one a thumb reaches for from anywhere. */
body.storefront .cart-link {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 44px;
    background: var(--paper);
    color: var(--navy);
    text-decoration: none;
    font-family: var(--heading-font);
    font-weight: 600;
    font-size: var(--text-sm);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    border-radius: 999px;
    padding: 0.4rem 1rem;
    white-space: nowrap;
    transition: background 120ms ease;
}

body.storefront .cart-link:hover {
    background: var(--gold);
    text-decoration: none;
}

body.storefront .cart-link:focus-visible { outline-color: var(--gold); }

/* The badge and its spoken sentence share a wrapper, because that wrapper is
 * the element htmx swaps when the basket changes. It lays out as if it were
 * not there, so the pill looks exactly as it did before. */
.cart-count-slot {
    display: inline-flex;
    align-items: center;
}

/* The badge, when the basket has just changed.
 *
 * Two testers added a third item and could not tell the sidebar had taken
 * it: a digit changing from 2 to 3 in the corner of a page is not a visible
 * event. The badge swells once and settles. Nothing about it changes colour,
 * so there is no new pair for the contrast sweep to measure, and the whole
 * thing is off where motion is not wanted. */
@keyframes cart-count-bump {
    0% { transform: scale(1); }
    35% { transform: scale(1.45); }
    100% { transform: scale(1); }
}

/* The script re-adds the class to replay it. */
.cart-count-slot--bumped .cart-count {
    animation: cart-count-bump 420ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .cart-count-slot--bumped .cart-count { animation: none; }
}

/* In the pill the count is plain type after the dot, as the mockup has it. */
.cart-count {
    display: inline-block;
    min-width: 0.6rem;
    color: var(--navy);
    font-weight: 700;
}

body.storefront .cart-link .cart-count {
    background: var(--paper);
}

/* Both are 44px tall on a phone, because a thumb is 44px wide whatever the
 * text measures. The padding does it rather than a height, so the label
 * stays vertically centred at every text size. */
body.storefront .orders-link,
body.storefront .signout-form .link-button {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    font-size: var(--text-sm);
    color: #ffffff;
    background: var(--navy);
    text-decoration: none;
    padding: 0.4rem 0.55rem;
    border-radius: var(--radius-sm);
    white-space: nowrap;
}

body.storefront .orders-link:hover,
body.storefront .signout-form .link-button:hover {
    color: #ffffff;
    background: var(--navy-raised);
    text-decoration: none;
}

body.storefront .orders-link:focus-visible,
body.storefront .signout-form .link-button:focus-visible { outline-color: var(--gold); }

.signout-form { margin: 0; }

/* ------------------------------------------------------------------ */
/* The navigation bar                                                  */
/* ------------------------------------------------------------------ */

/* The departments - Uniforms, Fan Gear, Training Supplies - and, while a
 * store is featured, its gold entry. White uppercase Oswald on the navy
 * header. On a phone they get a row of their own and wrap rather than
 * scroll, so nothing is hidden off the edge and the page never moves
 * sideways; from 60rem they sit between the mark and the search box.
 *
 * The current entry is marked THREE ways and only one is colour: the weight
 * steps up, a 3px gold rule appears under the label, and the link carries
 * aria-current="page". Anyone who cannot see the gold still sees which one
 * they are on. */
.store-nav {
    order: 3;
    flex: 1 1 100%;
}

.store-nav-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    list-style: none;
    margin: 0;
    padding: 0;
    gap: 0 0.15rem;
}

.store-nav-item {
    flex: 0 1 auto;
    min-width: 0;
}

body.storefront .store-nav-link {
    display: flex;
    align-items: center;
    /* A thumb is 44px wide whatever the text measures. */
    min-height: 44px;
    padding: 0.45rem 0.55rem 0.3rem;
    font-family: var(--heading-font);
    font-size: var(--text-sm);
    font-weight: 500;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: #ffffff;
    background: var(--navy);
    text-decoration: none;
    white-space: nowrap;
    /* Transparent rather than absent, so nothing shifts by 3px when an
     * entry becomes the current one. */
    border-bottom: 3px solid transparent;
    transition: background 120ms ease;
}

body.storefront .store-nav-link:hover {
    color: #ffffff;
    background: var(--navy-raised);
    text-decoration: none;
}

body.storefront .store-nav-link:focus-visible {
    outline: 3px solid var(--gold);
    outline-offset: -3px;
    border-radius: var(--radius-sm);
}

body.storefront .store-nav-link.is-current {
    font-weight: 700;
    border-bottom-color: var(--gold);
}

/* The featured store, in gold: the one entry that is there only for a
 * while, so it is the one that should catch the eye. */
body.storefront .store-nav-link.is-featured {
    color: var(--gold);
    font-weight: 600;
}

/* The team finder and the sign-in card centre one card on an empty page, so
 * the bar has to be pinned to the top itself rather than joining the stack
 * in the middle of the screen. The page's own padding is set in the auth
 * block further down, where its shorthand cannot overwrite this one. */
body.auth-page .store-nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 10;
    padding: 0.25rem 0.5rem;
    background: var(--navy);
}

.store-main {
    max-width: 72rem;
    margin: 0 auto;
    padding-block: 1.25rem 2.5rem;
    padding-inline: 1rem;
}

.store-footer {
    border-top: 1px solid var(--rule);
    background: linear-gradient(180deg, #fbfcfe 0%, var(--paper) 100%);
}

.store-footer p {
    max-width: 72rem;
    margin: 0 auto;
    padding-block: 1.5rem 2rem;
    padding-inline: 1rem;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

/* A mailto sits inside a sentence, so it stays inline: making it 44px tall
 * broke "Questions? Write to ..." across two lines and left the full stop
 * stranded. The padding widens the hit box without taking it out of the
 * line, which is the trade the target-size rule makes for inline links. */
.store-footer a {
    display: inline;
    padding-block: 0.35rem;
}

/* Tucked under the questions line rather than given a band of its own. */
body.storefront .store-footer .staff-link {
    padding-top: 0;
    margin-top: -1rem;
}

/* ------------------------------------------------------------------ */
/* Buttons and controls                                                */
/* ------------------------------------------------------------------ */

/* The form reset, and the reason every button below it kept its colour by
 * luck. `body.storefront button` is one class and two elements; `.add-form
 * button` is one class and one element, so this rule outranked every single
 * class button rule in the sheet and `color: inherit` won all seven of them.
 * They looked right only because the body's inherited ink happens to be the
 * same --ink they each asked for. Anything that wanted a different colour -
 * the dismiss button's --accent-ink, the remove button's --ink-soft - was
 * being thrown away silently.
 *
 * :where() contributes nothing to specificity, so this reads as (0,0,1): a
 * floor that every rule below beats, which is what a reset is for. */
:where(body.storefront) button,
:where(body.storefront) input,
:where(body.storefront) select,
:where(body.storefront) textarea {
    font: inherit;
    color: inherit;
}

/* The primary button. A one-stop gradient and a contact shadow, so it sits
 * on the card rather than being printed on it, and it presses in by a pixel
 * when it is clicked. Dark ink on the league's light blue, never white:
 * white on this blue does not measure. */
.add-form button,
.kit-form button,
.cart-line-form button,
.checkout-form button,
.suggestion-form button,
.checkout-pay button,
.request-form button {
    background: linear-gradient(180deg, #62c3ff 0%, var(--brand) 100%);
    color: var(--ink);
    border: 0;
    border-radius: var(--radius);
    padding: 0.7rem 1.1rem;
    font-weight: 600;
    cursor: pointer;
    min-height: 44px;
    box-shadow: 0 1px 2px rgba(20, 24, 31, 0.14),
                0 1px 0 rgba(255, 255, 255, 0.45) inset;
    transition: background 120ms ease, box-shadow 120ms ease,
                transform 80ms ease;
}

.add-form button:hover,
.kit-form button:hover,
.checkout-form button:hover,
.suggestion-form button:hover,
.checkout-pay button:hover,
.request-form button:hover {
    background: linear-gradient(180deg, #4dbbff 0%, #23a7fb 100%);
    box-shadow: 0 3px 8px rgba(20, 24, 31, 0.18),
                0 1px 0 rgba(255, 255, 255, 0.45) inset;
}

.add-form button:active,
.kit-form button:active,
.checkout-form button:active,
.suggestion-form button:active,
.checkout-pay button:active,
.request-form button:active {
    transform: translateY(1px);
    box-shadow: 0 1px 1px rgba(20, 24, 31, 0.18);
}

/* Pay is the one button on the store that is dark rather than light blue:
 * it ends the checkout, and it should not look like the four "add another
 * thing" buttons above it. It takes its shape from the shared rule above and
 * only repaints itself here.
 *
 * The body.storefront qualifier is load-bearing. Without it this rule and the
 * shared one have the same specificity, so the only thing making white ink
 * win over the shared dark ink is that this block sits further down the file.
 * Move either block and the button goes dark-on-dark, which is exactly what
 * was reported. The qualifier is true of every page that has a Pay button and
 * it settles the cascade on specificity instead of on line numbers. */
body.storefront .checkout-pay button {
    width: 100%;
    background: linear-gradient(180deg, #2b3442 0%, var(--ink) 100%);
    color: #fff;
    font-size: var(--text-lg);
}

/* Qualified for the same reason: the shared :hover sets the light blue and
 * no colour at all, so it would otherwise put white ink on light blue. */
body.storefront .checkout-pay button:hover {
    background: linear-gradient(180deg, #3a4454 0%, #262f3d 100%);
}

/* Update sits inside the shared light-blue rule above and then repaints
 * itself white, exactly as Pay does. Qualified for the same reason: without
 * `body.storefront` the two rules tie and only the line number keeps the
 * white fill, so sorting the sheet would put --ink on the blue gradient and
 * this button would change under us. */
body.storefront .cart-line-form button {
    background: var(--paper);
    color: var(--ink);
    border: 1px solid var(--rule-strong);
    padding: 0.4rem 0.7rem;
    font-size: var(--text-sm);
    min-height: 38px;
}

body.storefront .cart-line-form button:hover { border-color: var(--ink); }

/* The change-request launchers on the family page. They are buttons that
 * read as links, and they keep the class name the markup already used. */
body.storefront .link-button {
    background: var(--paper);
    border: 1px solid var(--rule-strong);
    color: var(--brand-deep);
    border-radius: var(--radius-sm);
    padding: 0.4rem 0.7rem;
    font-size: var(--text-sm);
    cursor: pointer;
}

.link-button:hover { border-color: var(--brand-deep); }

body.storefront .signout-form .link-button {
    background: none;
    border: 0;
    color: var(--ink-soft);
    cursor: pointer;
}

body.storefront label {
    display: block;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink-soft);
    margin-bottom: 0.15rem;
}

body.storefront select,
body.storefront input[type="text"],
body.storefront input[type="email"],
body.storefront input[type="tel"],
body.storefront input[type="number"],
body.storefront textarea {
    width: 100%;
    background: var(--paper);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.65rem;
    min-height: 44px;
}

body.storefront select:hover,
body.storefront input:hover,
body.storefront textarea:hover { border-color: var(--ink-soft); }

body.storefront textarea { min-height: 5rem; resize: vertical; }

.field-hint {
    display: block;
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--ink-soft);
    margin-top: 0.2rem;
}

/* ------------------------------------------------------------------ */
/* Store page: the product grid and the cart rail                      */
/* ------------------------------------------------------------------ */

.store-layout {
    display: grid;
    gap: 1.35rem;
    align-items: start;
}

/* The shelf: kit package, sort and cards, stacked as one item of the layout
 * grid, so the wide-screen layout has exactly two columns to fill. */
.store-shelf {
    display: grid;
    gap: 1.35rem;
    align-content: start;
}

.store-shelf > * { min-width: 0; }

/* The shelf title. A shop names its department once, quietly, and gets out
 * of the way of the goods: one step up in size, a short rule under it, and
 * nothing else. */
.store-intro {
    margin: 0 0 0.25rem;
    padding-bottom: 0.9rem;
    border-bottom: 1px solid var(--rule);
}

.store-intro h2 {
    margin: 0;
    font-size: var(--text-xl);
    letter-spacing: -0.02em;
}

/* On the front door only: the sentence that points a parent whose errand is
 * a jersey at the two uniform destinations, because they have come to the
 * wrong shelf and a sentence is kinder than leaving them to read the bar. */
.store-intro-line {
    margin: 0.4rem 0 0;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

/* A shop with nothing in it yet. Not an error, so it is not styled as one. */
.store-empty {
    margin: 1.5rem 0 0;
    font-size: var(--text-base);
    color: var(--ink-soft);
}

/* Every one of these is a grid or flex child. Without min-width: 0 each
 * would take its content's intrinsic width as a floor, and one long product
 * name or one wide number grid would push the whole page off a phone. */
.store-layout > *,
.checkout-layout > *,
.product-card,
.product-body,
.add-form,
.add-form > *,
.field,
.cart-line-body,
.summary-line > * { min-width: 0; }

/* Two to a row on a phone.
 *
 * One card per row meant a 356px square and a whole form before the second
 * product existed, which is a catalogue page and not a shop: nobody scrolls
 * four screens to find out that socks are sold. Everything that is bought by
 * size alone goes two-up, the way a shop's shelf reads, and the one card
 * that carries the pickers takes the full width as the page's hero. */
.product-list {
    display: grid;
    gap: 0.9rem;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    margin: 0;
}

.product-card--custom { grid-column: 1 / -1; }

.product-card,
.cart-aside,
.notice-card,
.cart-page,
.checkout-card,
.checkout-summary,
.order-card,
.auth-card {
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.product-card {
    padding: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    transition: box-shadow 160ms ease, transform 160ms ease,
                border-color 160ms ease;
}

/* The shelf lift. A couple of pixels and a longer shadow, which is all a
 * card needs to answer a pointer; the transform is the cheap half, because
 * it is composited and never reflows the grid. */
.product-card:hover,
.product-card:focus-within {
    box-shadow: var(--shadow-lift);
    border-color: var(--rule-strong);
    transform: translateY(-2px);
}

/* A hover lift is decoration. Anybody who has asked for less movement gets
 * the shadow and none of the travel. */
@media (prefers-reduced-motion: reduce) {
    .product-card { transition: box-shadow 160ms ease; }
    .product-card:hover,
    .product-card:focus-within { transform: none; }
}

/* The photo well.
 *
 * A square, always. When a product has an image_url the photo fills it; when
 * it has none the placeholder fills the identical square, so the day the
 * photographs arrive every card keeps its exact size and the grid does not
 * reflow. Nothing about the card's height depends on which branch renders. */
.product-media {
    position: relative;
    aspect-ratio: 1 / 1;
    width: 100%;
    /* A flex item in a column card. Without this it is shrinkable, and on a
     * narrow phone it is the tallest thing in the card and therefore the
     * first thing a shrink takes from. */
    flex: none;
    background: var(--page);
    border-bottom: 1px solid var(--rule);
    overflow: hidden;
}

/* The square, without aspect-ratio.
 *
 * Every browser that has aspect-ratio uses the rule above; this is for the
 * ones that do not, where an empty div would otherwise be zero high and the
 * card would show no picture at all. The ratio box is a zero-height element
 * with 100% top padding, which resolves against the WIDTH of the parent and
 * so is square by construction. */
@supports not (aspect-ratio: 1 / 1) {
    .product-media::before {
        content: "";
        display: block;
        padding-top: 100%;
    }
}

/* The hero card is full width on a phone, so a square well would be 356px of
 * one colour before the shopper reaches the size dropdown. A 5:4 crop keeps
 * the picture generous and puts the controls on the same screen. */
.product-card--custom .product-media { aspect-ratio: 5 / 4; }

/* Both fillers are positioned against the well rather than sized at 100% of
 * it.
 *
 * This is the iPhone fix. `height: 100%` is a percentage, and a percentage
 * height has to resolve against a parent with a DEFINITE height. The well's
 * height comes from aspect-ratio, which older WebKit does not treat as
 * definite for that purpose: it hands back auto, the child computes to zero,
 * and the swatch disappears while the square around it still paints the page
 * grey. `inset: 0` asks no such question. It sizes to the containing block
 * directly, and the containing block is the well, because the well is
 * positioned. */
.product-photo,
.product-placeholder {
    position: absolute;
    inset: 0;
}

/* The placeholder is an ordinary box, so inset alone stretches it. An <img>
 * is a replaced element and does not, and that difference is deliberate
 * here: the photograph is sized from ITS OWN proportions and then clamped by
 * the well, rather than being told to be the well's shape.
 *
 * It used to be `width: 100%; height: 100%; object-fit: cover`, which fills
 * the well by scaling the picture up and cutting off whatever will not fit.
 * In a square well that costs a jersey its hem; in the tall, narrow well the
 * hero card used to grow it cost four fifths of the shirt, and what was left
 * was blown up 1.4x. That is the "distorted jersey" the owner saw.
 *
 * `margin: auto` against `inset: 0` centres a box of intrinsic size in its
 * containing block, and max-width and max-height shrink it to fit while the
 * auto dimensions keep the ratio exactly. The percentages are safe here in a
 * way they are not on a static child, because an absolutely positioned box
 * resolves them against its containing block, which is the well itself.
 * object-fit: contain is the belt to that brace: it is a no-op while the
 * box already matches the photograph, and it still refuses to squash the
 * garment if a browser ever resolves those clamps differently. */
.product-photo {
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    margin: auto;
    object-fit: contain;
    display: block;
}

/* The placeholder that stands in for a photograph.
 *
 * A square of the product's own colour with the product's name set on it.
 * It costs nothing, needs no image files, and works for any product added
 * later: the colour comes from products.color and everything below is a
 * lookup on that one value. A product with no colour of its own — a practice
 * tee, a backpack — has nothing to show, so it gets neutral grey.
 *
 * It occupies exactly the square a real photo will, so setting image_url
 * swaps a picture in with no layout shift at all. */
.product-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 12%;
    text-align: center;
    /* Colour underneath, shading on top: the square falls away towards the
     * hem the way a garment does under a light, instead of reading as a
     * paint chip.
     *
     * The shading only ever DARKENS. A white sheen would have looked better
     * still on navy, but it is the one direction that can take a light ink
     * below AA — the mid greens are the tight ones, and a 16% highlight took
     * white on Teal from 5.09 to 3.77. Darkening costs the dark inks far
     * less, and 8% is the most any of them can give up: it leaves Kelly, the
     * closest, at 4.56. The only light on the square is the hairline below,
     * which is one pixel and never sits behind a letter. */
    background-color: var(--swatch, #e6eaef);
    background-image: linear-gradient(
        160deg,
        rgba(20, 24, 31, 0) 38%,
        rgba(20, 24, 31, 0.08) 100%
    );
    color: var(--swatch-ink, var(--ink));
    /* White needs a rule or it vanishes against a white card. Every other
     * swatch sets this to transparent, so the line only appears where it
     * is needed. The second inset is the lit top edge. */
    box-shadow: inset 0 0 0 1px var(--swatch-edge, transparent),
                inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

.placeholder-name {
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: 0.01em;
    overflow-wrap: anywhere;
}

/* The two thumbnail wells reuse the swatch, and it is absolutely positioned
 * now, so each has to be the thing it positions against. Without this it
 * would escape to the nearest positioned ancestor and land somewhere else
 * on the page entirely. */
.cart-line-media,
.summary-line-media { position: relative; }

/* The league's colour names, mapped once. Each entry sets the swatch and
 * the ink that stays legible on it — dark ink on the pale colours, light
 * ink on the dark ones — so the name can always be read. Keep the colour
 * values in step with ACCENT_COLORS in storefront_routes.py; a colour that
 * is not listed falls through to the neutral grey above. */
.product-placeholder[data-color="Black"]      { --swatch: #222222; --swatch-ink: #ffffff; }
.product-placeholder[data-color="Columbia"]   { --swatch: #7bb8e0; --swatch-ink: #14181f; }
.product-placeholder[data-color="Gold"]       { --swatch: #d4a017; --swatch-ink: #14181f; }
.product-placeholder[data-color="Green"]      { --swatch: #1f7a3f; --swatch-ink: #ffffff; }
.product-placeholder[data-color="Grey"]       { --swatch: #8a8a8a; --swatch-ink: #14181f; }
.product-placeholder[data-color="Kelly"]      { --swatch: #2e9e4f; --swatch-ink: #14181f; }
.product-placeholder[data-color="Light Blue"] { --swatch: #7bc4f0; --swatch-ink: #14181f; }
.product-placeholder[data-color="Maroon"]     { --swatch: #7b1b2b; --swatch-ink: #ffffff; }
.product-placeholder[data-color="Navy"]       { --swatch: #1b2a5b; --swatch-ink: #ffffff; }
.product-placeholder[data-color="Orange"]     { --swatch: #e06b1f; --swatch-ink: #14181f; }
.product-placeholder[data-color="Pink"]       { --swatch: #e06b9b; --swatch-ink: #14181f; }
.product-placeholder[data-color="Purple"]     { --swatch: #5b2d8a; --swatch-ink: #ffffff; }
.product-placeholder[data-color="Red"]        { --swatch: #c0272d; --swatch-ink: #ffffff; }
.product-placeholder[data-color="Royal"]      { --swatch: #1d4ed8; --swatch-ink: #ffffff; }
.product-placeholder[data-color="Silver"]     { --swatch: #b8b8b8; --swatch-ink: #14181f; }
.product-placeholder[data-color="Teal"]       { --swatch: #1f7a7a; --swatch-ink: #ffffff; }
.product-placeholder[data-color="White"]      { --swatch: #ffffff; --swatch-ink: #14181f; --swatch-edge: #c2cbd6; }
.product-placeholder[data-color="Yellow"]     { --swatch: #e8c520; --swatch-ink: #14181f; }

.product-body {
    padding: 0.8rem 0.85rem 0.95rem;
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    flex: 1;
}

.product-card--custom .product-body {
    padding: 0.95rem 1.05rem 1.15rem;
    gap: 0.85rem;
}

.product-head { display: grid; gap: 0.15rem; }

.product-name {
    font-size: var(--text-base);
    letter-spacing: -0.01em;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem;
}

.product-color {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--ink-soft);
    background: var(--page);
    border: 1px solid var(--rule);
    border-radius: 999px;
    padding: 0.05rem 0.5rem;
}

.product-price {
    margin: 0.1rem 0 0;
    font-size: var(--text-lg);
    font-weight: 600;
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
}

/* One figure, and it changes under the shopper when they change size, so it
 * must not shift the card about as it goes from $28.00 to $30.00. Tabular
 * figures above and a fixed line box here keep it still. */
.product-price .price-amount {
    display: inline-block;
    line-height: 1.2;
}

/* A crisp divider between what the thing is and what you do about it. It is
 * one hairline, and it is the difference between a card and a list. */
.add-form {
    display: grid;
    gap: 0.8rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--rule);
}

.add-form .field-row {
    display: grid;
    gap: 0.7rem;
    grid-template-columns: 1fr;
}

.add-form button { margin-top: 0.15rem; }

/* "Added to your cart", under the button that was pressed.
 *
 * The cart panel says it too, and on a phone that panel is below every
 * product on the shelf, so a parent pressed Add, saw nothing move, and
 * pressed it again. This is the same sentence where they are looking.
 *
 * display: none until there is something to say, so an empty slot takes no
 * room and the button does not sit above a gap. It is BELOW the button, so
 * showing it never moves the button under the thumb that just pressed it.
 * The green is the wash every other success on the store uses. */
/* That the add worked, said at the button that was pressed.
 *
 * Solid green with white type, dressed exactly as the refusal below it is
 * dressed in solid red, because the two are the same job: the one sentence
 * answering the one press. It was a pale wash on a pale card until round
 * five, and three testers reported seeing no confirmation at all.
 *
 * --good measures 7.2:1 against white, so the type is read as easily as the
 * refusal's is. test_contrast_sweep checks the pair for itself. */
.add-feedback {
    display: none;
    margin: 0.5rem 0 0;
    padding: 0.55rem 0.7rem;
    border-radius: var(--radius-sm);
    background: var(--good);
    color: #ffffff;
    font-size: var(--text-sm);
    font-weight: 600;
}

.add-feedback--shown { display: block; }

/* A refusal, said at the button that was refused.
 *
 * Dressed exactly as the banner at the top of the goods is, because it is the
 * same sentence: solid red with white type, not a wash. The banner is pinned
 * and a parent at the bottom of a tall kit card is not looking at it - they
 * are looking here, at the button their thumb just left. Four testers read
 * the banner's arrival as nothing having happened. */
.add-refusal {
    display: none;
    margin: 0.5rem 0 0;
    padding: 0.55rem 0.7rem;
    border-radius: var(--radius-sm);
    background: var(--bad);
    color: #ffffff;
    font-size: var(--text-sm);
    font-weight: 600;
}

.add-refusal--shown { display: block; }

/* What a field has just refused to take, said under the field.
 *
 * Nothing is dropped in silence any more. maxlength ate a fifth digit, or the
 * tail of a pasted number, with no word said and no event a page could hear;
 * the script owns the cap now so that it can explain it. */
.field-note {
    display: none;
    margin-top: 0.2rem;
    color: var(--bad-deep);
    font-size: var(--text-xs);
    font-weight: 600;
}

.field-note--shown { display: block; }

/* "No name or number on this shirt" is a checkbox, so its label is a row and
 * not the stacked block every other label is. Qualified because it is a
 * <label>, and `body.storefront label` otherwise outranked it and painted
 * --ink-soft over the --ink this asks for. */
body.storefront .no-customization {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 44px;
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--ink);
    background: var(--page);
    border: 1px solid var(--rule);
    border-radius: var(--radius-sm);
    padding: 0.55rem 0.7rem;
    margin: 0;
    cursor: pointer;
    transition: border-color 120ms ease, background 120ms ease;
}

body.storefront .no-customization:hover {
    border-color: var(--rule-strong);
    background: var(--brand-wash);
}

.no-customization input {
    width: 1.2rem;
    height: 1.2rem;
    min-height: 0;
    margin: 0;
    flex: none;
    accent-color: var(--brand-deep);
}

/* ------------------------------------------------------------------ */
/* The number picker                                                   */
/* ------------------------------------------------------------------ */

.number-picker {
    border: 1px solid var(--rule);
    border-radius: var(--radius-sm);
    padding: 0.75rem;
    margin: 0;
    display: grid;
    gap: 0.6rem;
    background: var(--paper);
    min-width: 0;
}

.number-picker legend {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink-soft);
    padding: 0 0.3rem;
    display: flex;
    align-items: center;
    gap: 0.35rem;
}

/* The "?" beside the jersey number.
 *
 * It is a <button> now rather than a span with a title on it, because a title
 * is a hover tooltip and a phone has no hover. That makes it a button inside
 * .add-form, and `.add-form button` paints the big blue Add to cart: this
 * rule is qualified with body.storefront so it outranks that one rather than
 * depending on which is lower in the file. min-height goes back to 0 for the
 * same reason - the shared button rule sets 44px. */
body.storefront .help-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    min-height: 0;
    padding: 0;
    border-radius: 50%;
    background: var(--brand-wash);
    color: var(--brand-deep);
    border: 1px solid var(--brand);
    box-shadow: none;
    font-size: var(--text-xs);
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
}

/* The edge darkens and the wash does not. --brand-deep on --brand measures
 * 2.44:1, which is the trap the Pay button fell into; the ink stays on the
 * wash it is legible on. */
body.storefront .help-icon:hover {
    border-color: var(--brand-deep);
    box-shadow: none;
}

.picker-help {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

/* Why a non player shirt has no number box. Always on show, unlike the help
 * behind the "?", because it explains something that is missing: a box that
 * was there a moment ago and is not there now. */
.picker-note {
    margin: 0 0 0.6rem;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

/* What is already spoken for.
 *
 * Not a grid of every number: the owner wants three digit numbers possible,
 * and a thousand bubbles is not a card. This is the short list of what a
 * shopper may not type, which is bounded by the roster and not by the
 * season's range - twenty players, twenty rows.
 *
 * Each row is full width, which is also the fix for "some got split oddly".
 * The old cells were 3.4rem wide and wrapped `anywhere`, so a name broke in
 * the middle of itself. A row has the whole card to lay a name out on and
 * breaks between the words. */
.held-numbers { display: grid; gap: 0.35rem; }

.held-head {
    margin: 0;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--ink-soft);
}

.held-empty {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

.held-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.3rem;
    /* Twenty rows is a lot of card on a phone. Past about six the list
     * scrolls inside itself rather than burying the Add button. */
    max-height: 14rem;
    overflow-y: auto;
}

/* The number is a fixed column and the note takes the rest, so the numbers
 * line up down the left and every note starts in the same place. */
.held-item {
    display: grid;
    grid-template-columns: 2.4rem minmax(0, 1fr);
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: var(--radius-sm);
    background: var(--page);
    font-size: var(--text-sm);
}

.held-number {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

/* One string, and it breaks between its words. `break-word` only splits a
 * word when that word alone will not fit on a line, which is the difference
 * between a name that wraps and a name that is chopped in half. */
.held-note {
    min-width: 0;
    overflow-wrap: break-word;
    line-height: 1.35;
}

/* Red for bought, yellow for a returning player's hold, and the shopper's
 * own household in the brand's own blue: their child's number is not
 * somebody else's hold and must never read as one. Each colour has its word
 * beside it in the note, so none of them is carrying the meaning alone. */
.held-item[data-state="taken"] {
    background: var(--taken-wash);
    border-color: var(--taken-ink);
    color: var(--taken-ink);
}

.held-item[data-state="reserved"] {
    background: var(--reserved-wash);
    border-color: var(--reserved-ink);
    color: var(--reserved-ink);
}

.held-item[data-state="yours"] {
    background: var(--brand-wash);
    border-color: var(--brand-deep);
    color: var(--brand-deep);
}

/* ------------------------------------------------------------------ */
/* The cart                                                            */
/* ------------------------------------------------------------------ */

.cart-aside { padding: 1.1rem; }

.cart-aside h2 {
    margin: 0 0 0.85rem;
    padding-bottom: 0.65rem;
    border-bottom: 1px solid var(--rule);
    font-size: var(--text-lg);
    letter-spacing: -0.01em;
}

.cart-lines {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.9rem;
}

.cart-line {
    display: grid;
    grid-template-columns: 56px 1fr;
    gap: 0.75rem;
    padding-bottom: 0.9rem;
    border-bottom: 1px solid var(--rule);
}

.cart-lines .cart-line:last-child { border-bottom: 0; padding-bottom: 0; }

/* Only drawn when a basket holds two shops. A family buying for a rec child
   and a club child needs to see which lines are which. */
.cart-store-heading h3 {
    margin: 0;
    font-size: var(--text-sm);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-soft);
}

.cart-line-media {
    width: 56px;
    height: 56px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--rule);
    overflow: hidden;
    background: var(--page);
}

.cart-line-media .product-placeholder { border-radius: 0; padding: 0; }

.cart-line-body { display: grid; gap: 0.3rem; min-width: 0; }

.cart-line-name {
    font-weight: 600;
    font-size: var(--text-sm);
    line-height: 1.35;
}

.cart-line-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
}

/* A typed name is as long as a parent's name really is now, so the pill has
 * to be able to give way: it may take the whole width of the panel and break
 * inside a word rather than push the cart sideways on a phone. */
.cart-line-number,
.cart-line-name-text {
    font-size: var(--text-xs);
    color: var(--ink-soft);
    background: var(--page);
    border: 1px solid var(--rule);
    border-radius: 999px;
    padding: 0.05rem 0.5rem;
    max-width: 100%;
    min-width: 0;
    overflow-wrap: anywhere;
}

.cart-line-price { font-weight: 600; font-size: var(--text-sm); }

.cart-line-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
    margin-top: 0.15rem;
}

/* The box takes 1 at the least, and this is where that is said. It wraps
 * onto its own line under the control it describes rather than squeezing
 * the quantity field. */
.cart-qty-note {
    flex-basis: 100%;
    font-size: var(--text-xs);
    color: var(--ink-soft);
}

.cart-line-form label {
    margin: 0;
    font-size: var(--text-xs);
}

.cart-line-form input[type="number"] {
    width: 4.2rem;
    min-height: 38px;
    padding: 0.35rem 0.45rem;
}

.cart-subtotal {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
    font-weight: 600;
    font-size: var(--text-base);
    margin: 1rem 0 0;
    padding-top: 0.9rem;
    border-top: 2px solid var(--ink);
}

.cart-subtotal-note {
    margin: 0.3rem 0 0;
    font-size: var(--text-xs);
    color: var(--ink-soft);
}

body.storefront .checkout-link {
    display: block;
    text-align: center;
    background: linear-gradient(180deg, #2b3442 0%, var(--ink) 100%);
    color: #fff;
    text-decoration: none;
    font-weight: 600;
    border-radius: var(--radius);
    padding: 0.85rem 1rem;
    margin-top: 0.9rem;
    box-shadow: 0 2px 6px rgba(20, 24, 31, 0.2);
}

body.storefront .checkout-link:hover {
    background: linear-gradient(180deg, #3a4454 0%, #262f3d 100%);
    box-shadow: 0 4px 12px rgba(20, 24, 31, 0.26);
    text-decoration: none;
}

.cart-empty {
    margin: 0;
    color: var(--ink-soft);
    font-size: var(--text-sm);
}

.keep-shopping { margin: 1.1rem 0 0; font-size: var(--text-sm); }

/* Inline for the same reason as the footer's: these sit in sentences. */
.keep-shopping a,
.cart-subtotal-note a {
    display: inline;
    padding-block: 0.35rem;
}

.cart-page { padding: 1rem; }
.cart-page > h2 { margin: 0 0 0.75rem; font-size: var(--text-xl); }

/* The cart page nests the panel inside the page card, so the panel drops its
 * own frame there and the shopper sees one card, not two. */
.cart-page .cart-aside {
    border: 0;
    box-shadow: none;
    padding: 0;
    background: none;
    position: static;
    /* And it is not a scroller here. The rail on the store page is bounded
     * by the screen and contains its overscroll, which is right beside a
     * shelf of goods and wrong on a page that is nothing but the basket: a
     * tester found the wheel stopping dead near the bottom of the cart page
     * whenever the pointer sat over a line's controls, because the wheel
     * was scrolling the panel and `contain` would not let it chain on to
     * the page. On this page the panel is the page, so all three go back. */
    max-height: none;
    overflow-y: visible;
    overscroll-behavior: auto;
    display: block;
}

/* And the lines are not a scroller here either. Spelled with the aside in
 * front of it so it outranks the store page's rule wherever that rule sits
 * in the file, rather than depending on which of the two comes last. */
.cart-page .cart-aside .cart-lines {
    max-height: none;
    overflow-y: visible;
}

.cart-page .cart-aside > h2 { display: none; }

/* ------------------------------------------------------------------ */
/* Notices                                                             */
/* ------------------------------------------------------------------ */

/* Why something did not happen, where the shopper is actually looking.
 *
 * The owner, on a phone: "it doesnt show the warning of why i cant use a
 * reserved number it just doesnt add to cart." Every basket change answers
 * with the cart panel, and below 60rem that panel is the LAST thing in a
 * single-column page - so the sentence explaining a refusal was swapped in
 * under every product on the shelf and the parent met a button that did
 * nothing.
 *
 * So it sticks. The element sits at the top of the goods and pins itself
 * under the masthead, which means it is on screen whether the shopper is at
 * the top of the page filling in the kit form or three screens down. It is
 * empty on a whole page and emptied again by the next success. */
/* Hidden until there is something to say. A modifier rather than :empty,
 * because the template leaves whitespace inside the element and :empty
 * counts whitespace as content: the bar would never have hidden. */
.store-alert:not(.store-alert--shown) { display: none; }

.store-alert {
    position: sticky;
    /* Clear of the masthead, which is sticky at the top with its own accent
     * rule. Half a rem of daylight under it so the two read as two things. */
    top: 4.25rem;
    z-index: 9;
    margin: 0 0 1rem;
    border-radius: var(--radius-sm);
    /* Solid red, not a wash. This is the one thing on the page the shopper
     * has to act on, and it is read on a phone in daylight: a pale tint with
     * a red edge is a note, and they were tapping Add again instead of
     * reading it. */
    background: var(--bad);
    border: 1px solid var(--bad-deep);
    box-shadow: 0 6px 16px rgba(20, 24, 31, 0.18);
}

.store-alert-body {
    margin: 0;
    padding: 0.75rem 0.9rem;
    color: #ffffff;
    font-size: var(--text-sm);
    font-weight: 600;
}

.cart-message,
.request-message {
    background: var(--good-wash);
    border: 1px solid #bfe3d0;
    border-left: 4px solid var(--good);
    color: var(--ink);
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.75rem;
    margin: 0 0 0.85rem;
    font-size: var(--text-sm);
}

/* A refusal in the cart panel, at the checkout or on the family page is not
 * news, and it may not be dressed as news.
 *
 * Round two of the browser testing: four testers, on four different pages,
 * read a refused coupon and a refused change request as having worked,
 * because the box was the same green as the confirmation. The modifier
 * existed and the markup carried it. It lost the cascade. It was written
 * ABOVE the rule it overrides and ties with it on specificity, so the green
 * won on source order and nothing on any of those pages was ever red.
 *
 * So it sits below that rule now, and it repaints the border outright rather
 * than only its colours, because the rule above sets the whole shorthand and
 * border-color alone was being thrown away with the rest.
 *
 * --bad on --bad-wash measures 6.05:1, so the sentence is read as easily as
 * the green one was. The colour is never the only signal: the element is
 * role="alert" and the wording says what did not happen. */
.cart-message--error,
.request-message--error {
    background: var(--bad-wash);
    border: 1px solid var(--bad-rule);
    border-left: 4px solid var(--bad);
    color: var(--bad);
    font-weight: 600;
}

.team-notice,
.checkout-warning,
.request-locked {
    background: var(--accent-wash);
    border: 1px solid #ecd06a;
    border-left: 4px solid var(--accent);
    color: var(--accent-ink);
    border-radius: var(--radius-sm);
    padding: 0.75rem 0.85rem;
    margin: 0 0 0.9rem;
    font-size: var(--text-sm);
}

.team-notice p,
.checkout-warning p { margin: 0 0 0.35rem; }
.team-notice p:last-of-type,
.checkout-warning p:last-child { margin-bottom: 0; }

.notice-dismiss {
    margin-top: 0.6rem;
    background: var(--paper);
    border: 1px solid #ecd06a;
    color: var(--accent-ink);
    border-radius: var(--radius-sm);
    padding: 0.35rem 0.8rem;
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;
    min-height: 36px;
}

.checkout-suggestion {
    background: var(--brand-wash);
    border: 1px solid #b6def8;
    border-left: 4px solid var(--brand-deep);
    border-radius: var(--radius-sm);
    padding: 0.75rem 0.85rem;
    margin: 0 0 0.9rem;
    font-size: var(--text-sm);
}

.checkout-suggestion p { margin: 0 0 0.4rem; }
.suggested-address { font-weight: 600; }

/* The answer to "use this address", and deliberately not the warning in a
 * quieter voice. The warning is a question and this is the settled answer to
 * it, so it is the store's own settled tone rather than the carrier blue the
 * question is drawn in. A tester pressed the button twice because the page
 * looked identical after each press; looking different is the fix. */
.checkout-accepted {
    background: var(--good-wash);
    border: 1px solid #bfe3d0;
    border-left: 4px solid var(--good);
    color: var(--good);
    border-radius: var(--radius-sm);
    padding: 0.75rem 0.85rem;
    margin: 0 0 0.9rem;
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
}

.checkout-accepted p { margin: 0 0 0.35rem; }
.checkout-accepted p:last-child { margin-bottom: 0; }

/* The site's answer to an unticked agreement box, in place of the browser's
 * own bubble. Empty and out of the layout until there is something to say,
 * so nothing moves on the page until the shopper has earned it. */
.checkout-ack-refusal {
    display: none;
    margin: 0.5rem 0 0;
    padding: 0.5rem 0.65rem;
    border-radius: var(--radius-sm);
    background: var(--bad-wash);
    border-left: 4px solid var(--bad);
    color: var(--bad-deep);
    font-size: var(--text-sm);
    font-weight: 600;
    overflow-wrap: anywhere;
}

.checkout-ack-refusal--shown { display: block; }

/* The carrier's own words, and the address it hands back, are the only text
 * on the checkout the league does not write. Shippo sends references, codes
 * and occasionally a URL, and a run with no space in it does not break at a
 * space: measured in a browser at 390px, one such message took the document
 * to 445px and the whole page was cut off at the right edge, mid-word, with
 * no way to scroll the heading back into view on a phone. Every other page
 * in the flow reflowed correctly because every other page is the league's
 * own prose. The message boxes are given the same leave to break inside a
 * word that the cart's typed-name pill already has. */
.checkout-suggestion,
.checkout-suggestion p,
.suggested-address,
.checkout-warning,
.cart-message {
    overflow-wrap: anywhere;
}
.suggestion-form { margin: 0.5rem 0 0.6rem; }
.suggestion-form button { padding: 0.5rem 0.9rem; font-size: var(--text-sm); min-height: 40px; }

.notice-card { padding: 1.5rem; max-width: 30rem; margin: 1rem auto; text-align: center; }
.notice-card p { margin: 0 0 0.75rem; }
.notice-card p:last-child { margin-bottom: 0; }

/* ------------------------------------------------------------------ */
/* Checkout                                                            */
/* ------------------------------------------------------------------ */

.checkout-layout {
    display: grid;
    gap: 1.25rem;
    align-items: start;
}

.checkout-card { padding: 1.25rem 1rem; }
.checkout-card > h2 { margin: 0 0 1rem; font-size: var(--text-xl); }

.checkout-form { display: grid; gap: 0.85rem; }
.checkout-form button { margin-top: 0.3rem; justify-self: start; }

.checkout-form .field-pair {
    display: grid;
    gap: 0.85rem;
    grid-template-columns: 1fr;
}

.checkout-summary { padding: 1rem; }
.checkout-summary h2 { margin: 0 0 0.75rem; font-size: var(--text-lg); }

.summary-lines {
    list-style: none;
    margin: 0 0 0.9rem;
    padding: 0;
    display: grid;
    gap: 0.6rem;
}

.summary-line {
    display: grid;
    grid-template-columns: 44px 1fr auto;
    gap: 0.6rem;
    align-items: center;
    font-size: var(--text-sm);
}

.summary-line-media {
    width: 44px;
    height: 44px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--rule);
    overflow: hidden;
    background: var(--page);
}

.summary-line-media .product-placeholder { padding: 0; }
.summary-line-name { min-width: 0; }
.summary-line-qty { display: block; color: var(--ink-soft); font-size: var(--text-xs); }
.summary-line-price { font-weight: 600; white-space: nowrap; }

.checkout-totals {
    width: 100%;
    border-collapse: collapse;
    margin-top: 0.5rem;
}

.checkout-totals td {
    padding: 0.4rem 0;
    vertical-align: top;
    font-size: var(--text-sm);
}

.checkout-totals td:last-child {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.checkout-totals .totals-note {
    display: block;
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--ink-soft);
    line-height: 1.35;
}

.checkout-totals .checkout-total-row td {
    border-top: 2px solid var(--ink);
    padding-top: 0.6rem;
    font-size: var(--text-lg);
    font-weight: 600;
}

.checkout-pay { margin-top: 1rem; }

/* The two boxes a family ticks before paying. Set as a grid rather than a
   plain inline label so the second and third lines of the returns wording
   line up under the first and not under the box, which is what makes a long
   sentence beside a checkbox readable at all. */
.checkout-agreements {
    display: grid;
    gap: 0.6rem;
    margin-bottom: 0.9rem;
}

.checkout-agreement {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.5rem;
    align-items: start;
    font-size: var(--text-xs);
    line-height: 1.45;
    color: var(--ink-soft);
    cursor: pointer;
}

.checkout-agreement input {
    /* Nudged down onto the first line's baseline: a checkbox sitting on the
       cap height of a two-line label reads as belonging to nothing. */
    margin-top: 0.15rem;
}

.checkout-reassurance {
    margin: 0.75rem 0 0;
    font-size: var(--text-xs);
    color: var(--ink-soft);
}

.checkout-lines {
    width: 100%;
    border-collapse: collapse;
    margin-top: 1rem;
}

.checkout-lines td {
    padding: 0.45rem 0;
    border-bottom: 1px solid var(--rule);
    font-size: var(--text-sm);
}

.checkout-lines td:last-child {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* The thank-you page. */
.success-mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    background: var(--good-wash);
    color: var(--good);
    font-size: var(--text-xl);
    font-weight: 600;
    margin-bottom: 0.75rem;
}

.order-number {
    font-weight: 600;
    font-size: var(--text-lg);
}

.back-to-store { margin-top: 1.25rem; font-size: var(--text-sm); }

/* ------------------------------------------------------------------ */
/* The family page                                                     */
/* ------------------------------------------------------------------ */

.me { display: grid; gap: 1rem; max-width: 46rem; margin: 0 auto; }
.me > h2 { margin: 0; font-size: var(--text-xl); }
.me-intro, .me-empty { color: var(--ink-soft); margin: 0; font-size: var(--text-sm); }

.me-empty {
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    padding: 1.5rem;
    text-align: center;
}

.order-card { padding: 1rem; display: grid; gap: 0.85rem; }
.order-card-head h3 { margin: 0; font-size: var(--text-lg); }

.order-card-meta {
    margin: 0.2rem 0 0;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

.order-items {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.5rem;
}

.order-item {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: baseline;
    padding: 0.55rem 0.65rem;
    background: var(--page);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
}

.order-item-name { font-weight: 600; }
.order-item-size,
.order-item-number { color: var(--ink-soft); font-size: var(--text-sm); }

.order-item-status {
    margin-left: auto;
    font-size: var(--text-xs);
    font-weight: 600;
    background: var(--brand-wash);
    color: var(--brand-deep);
    padding: 0.1rem 0.55rem;
    border-radius: 999px;
    white-space: nowrap;
}

.order-tracking {
    margin: 0;
    font-size: var(--text-sm);
    background: var(--good-wash);
    border-radius: var(--radius-sm);
    padding: 0.55rem 0.65rem;
}

.tracking-number { font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; }

.request-launcher {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
    border-top: 1px solid var(--rule);
    padding-top: 0.85rem;
}

.request-launcher-label {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink-soft);
    width: 100%;
}

.request-form {
    display: grid;
    gap: 0.75rem;
    border-top: 1px solid var(--rule);
    padding-top: 0.85rem;
}

.request-form button { justify-self: start; }
.request-form textarea { width: 100%; }

.request-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(4rem, 1fr));
    gap: 0.35rem;
}

.request-grid-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    padding: 0.4rem 0.3rem;
    font-size: var(--text-sm);
    font-weight: 600;
    min-height: 44px;
    margin: 0;
    text-align: center;
}

.request-grid-cell.free { cursor: pointer; }
.request-grid-cell.free:hover { border-color: var(--brand-deep); }
.request-grid-cell.free input { width: auto; min-height: 0; margin: 0 0 0.15rem; accent-color: var(--brand-deep); }
/* A number somebody else has. Dimmer than a free one on purpose, but it
 * still has to be read: #7c8695 on --page measured 3.40:1, so the grey goes
 * one step darker to 5.21:1 and the cell stays visibly quieter than the
 * live ones beside it. */
.request-grid-cell.taken { background: var(--page); border-color: var(--rule); color: #5f6875; }
.request-grid-cell.yours { background: var(--brand-wash); border-color: var(--brand-deep); color: var(--brand-deep); }

.order-request-history h4 {
    margin: 0 0 0.4rem;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

.order-request-history ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.35rem;
    font-size: var(--text-sm);
}

.request-history-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0.45rem 0.6rem;
    background: var(--page);
    border-radius: var(--radius-sm);
}

.request-kind { font-weight: 600; }
.request-status { color: var(--ink-soft); }
.request-note, .request-decision { color: var(--ink-soft); }

/* Backordered: a note about timing, never a reason a size cannot be bought. */
.cart-line-backordered {
    background: var(--accent-wash);
    color: var(--accent-ink);
    border: 1px solid #ecd06a;
    border-radius: 999px;
    padding: 0.05rem 0.5rem;
    font-size: var(--text-xs);
    font-weight: 600;
}

.cart-backordered-note {
    color: var(--ink-soft);
    font-size: var(--text-sm);
    margin: 0.5rem 0 0;
}

/* ------------------------------------------------------------------ */
/* Sign-in and landing: the same shop, with one card on it             */
/* ------------------------------------------------------------------ */

body.auth-page {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* The top is clear of the fixed navigation bar, so on a short screen the
     * card starts below it rather than under it. */
    padding: 7rem 1rem 1.5rem;
}

.auth-card {
    width: 100%;
    max-width: 26rem;
    padding: 1.75rem 1.25rem;
    text-align: center;
}

.auth-logo { height: 54px; width: auto; margin-bottom: 0.9rem; }
.auth-card h1 { margin: 0 0 0.75rem; font-size: var(--text-xl); }
.auth-card p { margin: 0 0 0.9rem; font-size: var(--text-sm); color: var(--ink-soft); }
.auth-card form { display: grid; gap: 0.5rem; text-align: left; margin-bottom: 1rem; }

/* Sign in, and Find my team. The league's light blue is too bright for white
 * type, so this button is read in near-black ink: #14181f on #4dbbff is
 * 8.36:1, and 7.39:1 on the darker blue it takes on hover.
 *
 * Qualified, like Pay and Update. Unqualified it tied the form reset above
 * exactly, so the only thing keeping this declaration alive was that it sits
 * further down the file, and the reset's `color: inherit` was one moved block
 * away from deciding what colour the button is. */
body.storefront .auth-card form button {
    background: var(--brand);
    color: var(--ink);
    border: 0;
    border-radius: var(--radius);
    padding: 0.7rem 1.1rem;
    font-weight: 600;
    cursor: pointer;
    min-height: 44px;
    margin-top: 0.25rem;
}

body.storefront .auth-card form button:hover { background: #33afff; }
.auth-card .muted { font-size: var(--text-sm); margin-bottom: 0; }

.auth-card .flash {
    background: var(--good-wash);
    border: 1px solid #bfe3d0;
    border-left: 4px solid var(--good);
    color: var(--ink);
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.75rem;
    font-size: var(--text-sm);
    text-align: left;
}

/* The same fault on the sign-in page and on find-my-team: an expired link,
 * a rate limit and a household we could not find all arrived in the flash
 * that "we have sent you a link" uses. Qualified with .auth-card so it
 * outranks the rule above rather than tying with it, which is the mistake
 * the cart message spent a round making. */
.auth-card .flash--error {
    background: var(--bad-wash);
    border: 1px solid var(--bad-rule);
    border-left: 4px solid var(--bad);
    color: var(--bad);
    font-weight: 600;
}

.auth-footer {
    margin-top: 1.25rem;
    font-size: var(--text-sm);
    color: var(--ink-soft);
    text-align: center;
}

/* ------------------------------------------------------------------ */
/* The coupon field, the press charge, and the required-items block    */
/* ------------------------------------------------------------------ */

/* A modest field and deliberately not a banner. Most parents have no code,
 * and a painted box asking for one reads as a discount they are missing out
 * on. One hairline rule separates it from the totals above and that is all:
 * no fill, no border, nothing that competes with the Pay button below it. */
.checkout-coupon {
    margin-top: 0.9rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--rule);
}

.coupon-form {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0.5rem;
    align-items: end;
}

/* The label spans both columns so "Have a code?" sits above the pair rather
 * than squeezing the input into half a line on a phone. */
.coupon-form label {
    grid-column: 1 / -1;
    margin-bottom: 0;
}

.coupon-form input[type="text"] {
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.04em;
}

/* The Apply button is secondary: this is not the button that takes the money.
 * White with a rule on it, the same shape as the cart's Update button. */
.coupon-form button {
    background: var(--paper);
    color: var(--ink);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.9rem;
    font-size: var(--text-sm);
    font-weight: 600;
    min-height: 44px;
    cursor: pointer;
}

.coupon-form button:hover { border-color: var(--ink); }

/* An applied code is a settled fact, so it reads as one line of text with a
 * quiet Remove beside it rather than as another form to fill in. */
.coupon-applied {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.coupon-applied-code {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--good);
}

.coupon-applied .link-button {
    min-height: 44px;
    padding: 0.4rem 0.75rem;
}

/* The discount is the one row in the totals that is money coming off, so it
 * is the one row allowed a colour. */
.checkout-totals .checkout-discount-row td { color: var(--good); }
.checkout-totals .checkout-discount-row td:last-child { font-weight: 600; }

/* What customizing costs, said on the card before anything is chosen. */
.press-note {
    margin: 0 0 0.6rem;
    font-size: var(--text-xs);
    color: var(--ink-soft);
}

/* The same figure on the cart line and in the order summary, as a quiet
 * second line under the name rather than another pill in the tag row. */
.cart-line-press,
.summary-line-press {
    display: block;
    font-size: var(--text-xs);
    color: var(--ink-soft);
}

/* Whose jersey this line is. Also a quiet second line, and semibold because
 * it is the one thing telling two otherwise identical lines apart. A long
 * first name may break rather than push a phone sideways. */
.cart-line-player,
.summary-line-player {
    display: block;
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--ink-soft);
    overflow-wrap: anywhere;
}

/* "Required" beside a product's name. An outline badge, not a fill: it is a
 * fact about the kit, not a warning, and the accent yellow is unreadable as
 * type. */
.product-required {
    display: inline-block;
    vertical-align: middle;
    margin-left: 0.35rem;
    padding: 0.05rem 0.45rem;
    border: 1px solid var(--brand-deep);
    border-radius: 999px;
    background: var(--brand-wash);
    color: var(--brand-deep);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* A product a stock-limited store has run out of in every size. The same
 * pill as Required, in ink rather than brand blue, because it is a fact
 * about the shelf and not something the league is asking of the parent.
 * Only the warehouse-sale kind of store ever draws it. */
.product-sold-out {
    display: inline-block;
    vertical-align: middle;
    margin-left: 0.35rem;
    padding: 0.05rem 0.45rem;
    border: 1px solid var(--ink-soft);
    border-radius: 999px;
    background: var(--paper);
    color: var(--ink);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* An add button with nothing left to add. It fades and stops inviting a
 * press; its own word already says "Sold out", so the fade is a second
 * signal and never the only one. */
.add-form button[disabled],
.kit-add[disabled] {
    opacity: 0.6;
    cursor: not-allowed;
}

/* The block at the checkout. It borrows the shape of .checkout-warning,
 * because it is the same kind of thing: something to put right before
 * carrying on, never a dead end. */
.checkout-required {
    background: var(--accent-wash);
    border: 1px solid #ecd06a;
    border-left: 4px solid var(--accent);
    color: var(--accent-ink);
    border-radius: var(--radius-sm);
    padding: 0.75rem 0.85rem;
    margin: 0.9rem 0 0;
    font-size: var(--text-sm);
}

.checkout-required > p { margin: 0 0 0.5rem; font-weight: 600; }

.required-missing {
    list-style: none;
    margin: 0 0 0.5rem;
    padding: 0;
    display: grid;
    gap: 0.35rem;
}

/* Each missing item is a real link to its card, and a thumb-sized one.
 * Qualified so the yellow block's ink survives `body.storefront a`, which
 * otherwise repainted these in --brand-deep. */
body.storefront .required-missing a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    color: var(--accent-ink);
    font-weight: 600;
}

.required-note {
    margin: 0;
    font-weight: 400;
    font-size: var(--text-xs);
}

/* ------------------------------------------------------------------ */
/* Wider than a phone                                                  */
/* ------------------------------------------------------------------ */

@media (min-width: 40rem) {
    .store-header-inner { padding-inline: 2rem; }
    .store-band-inner { padding-inline: 2rem; }
    .store-logo { height: 36px; }
    .store-band h1 { font-size: var(--text-lg); }
    .store-main { padding-block: 2rem 3.5rem; padding-inline: 2rem; }
    .store-footer p { padding-block: 1.75rem; padding-inline: 2rem; }

    .product-list { grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); }
    .store-intro { padding-bottom: 1.1rem; }
    .product-card--custom .product-media { aspect-ratio: 1 / 1; }


    .product-card--custom .product-body {
        display: grid;
        grid-template-columns: 1fr;
        gap: 1rem;
    }

    /* The size dropdown carries a size and a price; quantity carries one
     * digit. They should not be the same width. */
    .add-form .field-row { grid-template-columns: 1.7fr 1fr; }
    .checkout-form .field-pair { grid-template-columns: 1fr 1fr; }
    .product-body { padding: 0.95rem 1.05rem 1.15rem; gap: 0.85rem; }
    .checkout-card { padding: 1.75rem; }
    .cart-page { padding: 1.75rem; }
    .order-card { padding: 1.5rem; }
    .auth-card { padding: 2.25rem; }
    .store-layout { gap: 1.5rem; }
    .product-list { gap: 1.25rem; }
}

@media (min-width: 60rem) {
    .store-layout { grid-template-columns: minmax(0, 1fr) 20rem; gap: 2rem; }

    /* Three across beside the 20rem rail, never fewer. auto-fill at 15rem
     * found room for only two in the 34 to 46rem the shelf has here, and a
     * column that narrow left a desktop shopper scrolling a long shelf two
     * cards at a time past a wide margin. The hero card still spans. */
    .product-list { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }

    .store-layout .store-intro { grid-column: 1 / -1; }

    /* The rail is sticky so the total stays in view while the shelf beside
     * it is read. It had no height of its own, and a sticky box taller than
     * the viewport pins its top edge and then never moves again: a basket of
     * six lines put its last two permanently off the bottom of the screen,
     * however far the page was scrolled. A tester read that as "the cart
     * sidebar's scrolling lags behind the main list, making items appear and
     * disappear oddly", which is exactly what it does.
     *
     * So it is bounded by the screen, less the 6rem it is pinned below and a
     * little air under it, and a long basket scrolls inside the rail.
     * overscroll-behavior stops the page taking over the moment the rail
     * reaches an end, which is the other half of the same complaint. */
    .cart-aside {
        position: sticky;
        top: 6rem;
        max-height: calc(100vh - 7.5rem);
        display: flex;
        flex-direction: column;
        padding: 1.25rem;
    }

    /* The bound above fixed one fault and caused another. The whole rail was
     * the scroller, so the subtotal and the Check out link went down with the
     * lines: three items in the basket put the button below the rail's own
     * fold, and `overscroll-behavior: contain` stopped the wheel chaining on
     * to the page, so it read as a button that had simply gone.
     *
     * Only the lines scroll now. The rail is a column, the list takes the
     * slack, and the total and the button sit under it as fixed rows that a
     * basket of any length cannot push out of sight. min-height is the half
     * that is easy to leave out: a flex item will not shrink below its
     * content without it, and the list would go on pushing the button down.
     *
     * `contain` is gone with the rest. It was there to stop the page taking
     * over the moment the rail reached an end; with the button out of the
     * scroller there is far less to scroll, and a wheel that chains on to
     * the page at the end of a short list is the behaviour a reader
     * expects. */
    .cart-aside .cart-lines {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
    }

    /* The hero jersey puts its picture beside its controls once there is
     * room for both. */
    .product-card--custom { display: grid; grid-template-columns: 16rem 1fr; }

    /* Beside the controls the photo well keeps the square it has at every
     * other width. It used to be told `height: 100%` so that a tall card had
     * no dead white beneath its picture, but a grid item's 100% is the ROW's
     * height, and the row is as tall as the control column: a player
     * dropdown, a number picker, a quantity and a customization box. The
     * well grew to 256 by 847 and the jersey inside it was cropped to a
     * sliver of itself.
     *
     * align-self is the other half. `stretch` is the default for a grid
     * item, and a stretched item's height overrides a declared ratio, so the
     * square would not survive on its own. `start` lets the well be exactly
     * as tall as it is wide and leaves the controls to be as tall as they
     * need to be. */
    .product-card--custom .product-media {
        aspect-ratio: 1 / 1;
        align-self: start;
        border-bottom: 0;
        border-right: 1px solid var(--rule);
    }

    .checkout-layout { grid-template-columns: minmax(0, 1fr) 21rem; }

    .checkout-summary { position: sticky; top: 6rem; }
}

/* The team lookup: a parent who cannot find the emailed link types their
 * whole address and picks a child. Each row is a name, a team and either a
 * way in or the sentence explaining why there is not one yet. Left aligned
 * inside the centred card, because a list of names reads as a list. */
.lookup-results {
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
    display: grid;
    gap: 0.6rem;
    text-align: left;
}

.lookup-child {
    border: 1px solid var(--rule);
    border-radius: var(--radius-sm);
    padding: 0.7rem 0.75rem;
    display: grid;
    gap: 0.3rem;
    min-width: 0;
}

.lookup-name { font-weight: 600; }

.lookup-team {
    font-size: var(--text-sm);
    color: var(--ink-soft);
    overflow-wrap: anywhere;
}

/* "Open this store" is a filled button, not prose, so it needs the qualifier
 * the prose link rule asks for. Without it `body.storefront a` outranked this
 * rule and painted --brand-deep on --brand: 2.77:1 at rest and 2.44:1 on
 * hover, which is the worst pair on the storefront. */
body.storefront .lookup-open {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0.55rem 0.9rem;
    border-radius: var(--radius);
    background: var(--brand);
    color: var(--ink);
    font-weight: 600;
    text-decoration: none;
}

body.storefront .lookup-open:hover { background: #33afff; }

.lookup-pending {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

/* The team search. The hint says where the team's name is written down;
 * the example is set apart so it reads as a name, not as part of the
 * sentence. The status line is empty until there is something to say, and
 * then it is the same sentence whether the server or the script wrote it. */
.team-finder-hint { text-align: left; }

/* The two ways in: club teams by name, league teams by email. The second
 * heading is ruled off so the two forms read as two choices. */
.team-finder-heading {
    margin: 1.5rem 0 0.5rem;
    font-size: var(--text-lg);
    text-align: left;
}

.team-finder-heading ~ .team-finder-heading {
    border-top: 1px solid var(--rule);
    padding-top: 1.25rem;
}

/* A search box, so the form fields' rule above does not reach it. */
body.storefront .team-finder-form input[type="search"] {
    width: 100%;
    background: var(--paper);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.65rem;
    min-height: 44px;
}

.team-finder-example {
    font-weight: 600;
    color: var(--ink);
    white-space: nowrap;
}

.team-results-status {
    text-align: left;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink);
}

.team-results-status:empty { display: none; }

.team-results-more {
    font-size: var(--text-sm);
    padding: 0.2rem 0.1rem;
}

/* ------------------------------------------------------------------ */
/* The kit package                                                     */
/* ------------------------------------------------------------------ */

/* One box where there were three cards.
 *
 * A rec team wears one kit and buys it as one thing, so the jersey, the
 * shorts and the socks sit inside a single bordered box with one button at
 * the bottom of it. The box reads as the page's hero: it is the first thing
 * under the shelf title, it is full width at every size, and the fan gear
 * grid follows it.
 *
 * The hard part is a phone. Six controls, a player dropdown and a number
 * picker in one box at 390px is where this becomes a wall, so each piece is
 * a small thumbnail with its name and price beside it, and then its own two
 * controls on the full width underneath. Three rows of that read as three
 * garments; three squares with dropdowns under them do not. */
.kit-package {
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1rem 0.9rem 1.1rem;
    margin: 0 0 1.35rem;
    /* A grid child like every other block on the store layout: without this
     * a long team name or a wide size dropdown sets an intrinsic floor and
     * pushes the page sideways on a phone. */
    min-width: 0;
}

.kit-head { display: grid; gap: 0.3rem; }

.kit-title {
    margin: 0;
    font-size: var(--text-lg);
    letter-spacing: -0.01em;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem;
}

.kit-intro {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--ink-soft);
}

.kit-form {
    display: grid;
    gap: 1rem;
    margin-top: 0.9rem;
    padding-top: 0.95rem;
    border-top: 1px solid var(--rule);
}

.kit-piece {
    display: grid;
    gap: 0.7rem;
    min-width: 0;
}

/* A hairline between the pieces, so three garments read as three and not as
 * one long form. The first piece needs none: the form's own top rule is
 * already above it. */
.kit-piece + .kit-piece {
    border-top: 1px solid var(--rule);
    padding-top: 1rem;
}

.kit-piece-head {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 0;
}

/* The thumbnail. A fixed square, positioned, because the placeholder and the
 * photograph inside it are both absolutely placed against it - the same
 * arrangement the product card's photo well uses, and for the same reason:
 * a percentage height against an aspect-ratio box is not definite in older
 * WebKit and the swatch vanishes. */
.kit-piece-media {
    position: relative;
    flex: none;
    width: 3.75rem;
    height: 3.75rem;
    background: var(--page);
    border: 1px solid var(--rule);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.kit-piece-photo {
    position: absolute;
    inset: 0;
    margin: auto;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.kit-piece-titles {
    display: grid;
    gap: 0.1rem;
    min-width: 0;
}

.kit-piece-name {
    margin: 0;
    font-size: var(--text-base);
    letter-spacing: -0.01em;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem;
}

.kit-piece-body {
    display: grid;
    gap: 0.7rem;
    min-width: 0;
}

/* Size and quantity side by side even on a phone. They fit because this row
 * has the box's full width under the thumbnail rather than sharing it, and
 * side by side is what says these two belong to the piece above them. The
 * size dropdown carries a size, the quantity carries one digit, so they are
 * not the same width. */
.kit-piece-body .field-row {
    display: grid;
    grid-template-columns: 1.7fr 1fr;
    gap: 0.7rem;
}

.kit-foot {
    display: grid;
    gap: 0.6rem;
    padding-top: 0.95rem;
    border-top: 1px solid var(--rule);
}

.kit-note {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--ink-soft);
}

/* One button for the whole package, and it is the page's primary action, so
 * it inherits the storefront's button styling and only takes its width. */
.kit-add { width: 100%; }

@media (min-width: 40rem) {
    .kit-package { padding: 1.35rem 1.5rem 1.5rem; }
    .kit-piece-media { width: 4.5rem; height: 4.5rem; }
    .kit-add { width: auto; justify-self: start; min-width: 14rem; }
}

/* With room, a piece puts its thumbnail and name beside its controls, which
 * is the shape the whole package is trying to be: three rows, each one
 * garment. Below this width the two stack, because 12rem of controls beside
 * a name is worse than either. */
@media (min-width: 52rem) {
    .kit-piece {
        grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
        gap: 1.25rem;
        align-items: start;
    }
}

/* Editing a cart line where it stands.
 *
 * The owner asked to "edit the size and number in the cart as well", so each
 * line carries a size dropdown, a number box when it wears a number, and a
 * Remove control beside them. Everything here is a real target: 44px tall,
 * wrapping rather than overflowing, and readable at 390px where a phone puts
 * the size, the number and the button on two lines instead of three.
 */
.cart-edit-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: end;
    margin-top: 0.35rem;
}

.cart-edit-field {
    display: grid;
    gap: 0.15rem;
    min-width: 0;
}

.cart-edit-field label {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--ink-soft);
}

.cart-edit-field select,
.cart-edit-field input[type="text"] {
    min-height: 44px;
    max-width: 100%;
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    background: var(--paper);
    color: var(--ink);
    font-size: var(--text-sm);
}

/* Four digits and no more, so the box does not take the row on a phone. The
 * selector carries body.storefront because the sheet's own text-input rule
 * does, and a 100% wide box for four characters looks like a mistake. */
body.storefront .cart-edit-field input[type="text"] { width: 5rem; }

.cart-edit-form button {
    background: var(--paper);
    color: var(--ink);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius);
    padding: 0.4rem 0.9rem;
    font-size: var(--text-sm);
    font-weight: 600;
    min-height: 44px;
    cursor: pointer;
}

.cart-edit-form button:hover { border-color: var(--ink); }

.cart-remove-form { margin-top: 0.3rem; }

/* Quieter than Save, because it is the destructive one and nobody should
 * reach for it by accident. --ink-soft on white is 7.2:1, well past AA. */
.cart-remove-button {
    background: var(--paper);
    color: var(--ink-soft);
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    padding: 0.4rem 0.9rem;
    font-size: var(--text-sm);
    min-height: 44px;
    cursor: pointer;
}

.cart-remove-button:hover {
    border-color: var(--ink);
    color: var(--ink);
}

.cart-edit-form button[disabled],
.cart-remove-button[disabled] {
    opacity: 0.6;
    cursor: default;
}

/* A checkout button that is waiting on the carrier. It keeps its colour and
 * only stops inviting a second press: the wording has already changed to
 * say what is happening, and a button that greys out while a parent is
 * reading it looks like a failure. See static/checkout_forms.js. */
.suggestion-form button[disabled],
.checkout-form button[disabled],
.checkout-pay button[disabled] {
    cursor: progress;
}

/* Shown when a token aged out while the page sat open. See static/csrf.js. */
.csrf-notice {
    position: fixed;
    left: 50%;
    bottom: 1.25rem;
    transform: translateX(-50%);
    max-width: 32rem;
    margin: 0 1rem;
    padding: 0.75rem 1rem;
    border: 1px solid #b45309;
    border-radius: 8px;
    background: #fffbeb;
    color: #7c2d12;
    font-size: 0.95rem;
    line-height: 1.4;
    z-index: 1000;
}

/* ------------------------------------------------------------------ */
/* The warehouse sale: discounts, Limited Edition, free pickup          */
/* ------------------------------------------------------------------ */

/* A size selling below its retail price. Loud on purpose, because the
 * owner asked for it: the retail struck through in soft ink, the sale price
 * a size up and in the league's red, and a filled badge with the discount.
 * Only a product with a size below retail ever draws any of this
 * (partials/price_block.html), so every other card is untouched. */
.product-price--sale {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.15rem 0.5rem;
}

.price-was-amount {
    color: var(--ink-soft);
    font-size: var(--text-base);
    font-weight: 400;
    text-decoration-thickness: 2px;
    text-decoration-color: var(--bad);
}

.product-price--sale .price-amount {
    color: var(--bad);
    font-size: var(--text-xl);
    font-weight: 600;
    letter-spacing: -0.02em;
}

.price-off {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    background: var(--bad);
    color: var(--paper);
    font-size: var(--text-sm);
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

.price-was[hidden],
.price-off[hidden] { display: none; }

/* The two flags laid over the photo's corners. The ribbon is a diagonal
 * sash across the top left, in ink and the league's yellow, which nothing
 * else on the shelf wears; the discount badge is a red disc top right. */
.limited-ribbon,
.sale-badge {
    z-index: 2;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    pointer-events: none;
}

.product-media .limited-ribbon {
    position: absolute;
    top: 1.35rem;
    left: -2.6rem;
    width: 11rem;
    padding: 0.35rem 0;
    transform: rotate(-35deg);
    background: var(--ink);
    color: var(--accent);
    border-top: 2px solid var(--accent);
    border-bottom: 2px solid var(--accent);
    box-shadow: 0 4px 10px rgba(20, 24, 31, 0.25);
    font-size: var(--text-xs);
    text-align: center;
}

.product-media .sale-badge {
    position: absolute;
    top: 0.6rem;
    right: 0.6rem;
    max-width: 6.5rem;
    padding: 0.45rem 0.6rem;
    border-radius: var(--radius);
    background: var(--bad);
    color: var(--paper);
    box-shadow: 0 4px 12px rgba(176, 36, 32, 0.35);
    font-size: var(--text-sm);
    line-height: 1.15;
    text-align: center;
}

/* Inside the kit package the thumbnail is too small, so the flags are pills
 * under the piece's name. */
.kit-piece-flags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin: 0.2rem 0 0;
}

.kit-piece-flags .limited-ribbon,
.kit-piece-flags .sale-badge {
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    font-size: var(--text-xs);
}

.kit-piece-flags .limited-ribbon { background: var(--ink); color: var(--accent); }
.kit-piece-flags .sale-badge { background: var(--bad); color: var(--paper); }

/* Free pickup: the notice in the basket, at the checkout and on the
 * receipt, in the accent wash so it reads as the answer to "how do I get
 * it" rather than as small print. */
.checkout-pickup,
.cart-pickup {
    margin: 0 0 1rem;
    padding: 0.85rem 1rem;
    border-left: 5px solid var(--accent);
    border-radius: var(--radius-sm);
    background: var(--accent-wash);
    color: var(--accent-ink);
}

.checkout-pickup p,
.cart-pickup p { margin: 0; }

.checkout-pickup-title,
.cart-pickup-title {
    font-size: var(--text-lg);
    font-weight: 600;
}

.checkout-pickup-price {
    margin-left: 0.4rem;
    padding: 0.05rem 0.5rem;
    border-radius: 999px;
    background: var(--good);
    color: var(--paper);
    font-size: var(--text-sm);
}

.checkout-pickup-notice,
.cart-pickup-notice {
    margin-top: 0.35rem !important;
    line-height: 1.45;
}

.checkout-switch {
    margin-top: 1.25rem;
    padding-top: 1rem;
    border-top: 1px solid var(--rule);
}

.checkout-switch p {
    margin: 0 0 0.5rem;
    color: var(--ink-soft);
    font-size: var(--text-sm);
}

.checkout-switch--pickup {
    margin: 0 0 1.25rem;
    padding: 0.75rem 1rem;
    border: 1px dashed var(--rule-strong);
    border-radius: var(--radius-sm);
}

/* The shopper's sort and filters: one toolbar above the cards, on paper so
 * the page's stripes never show through it.
 *
 * On a phone it is a column: the Filter disclosure (a <details>, so it folds
 * away with no script), the sort, and the count. From 60rem it is one tidy
 * row: the filters open inline (storefront.js opens the disclosure there and
 * its summary steps aside), the sort beside them, the count at the end.
 * Nothing in it is wider than its column, so there is no sideways scroll at
 * 360px. */
.sort-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.6rem 0.9rem;
    margin: 0;
    padding: 0.75rem 0.85rem;
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.sort-form > * { min-width: 0; }

.filter-panel { flex: 1 1 100%; }

.filter-summary {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 44px;
    padding: 0 0.9rem;
    border: 1px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    background: var(--paper);
    color: var(--ink);
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

.filter-summary::-webkit-details-marker { display: none; }
.filter-summary::before { content: "\25B8"; font-size: 0.8em; }
.filter-panel[open] > .filter-summary::before { content: "\25BE"; }

.filter-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.6rem;
    margin-top: 0.6rem;
}

body.storefront .filter-field,
body.storefront .filter-check { margin: 0; min-width: 0; }

.filter-label {
    display: block;
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ink-soft);
    margin-bottom: 0.15rem;
}

body.storefront .filter-check {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    min-height: 44px;
    color: var(--ink);
    font-size: var(--text-sm);
}

.filter-check input { width: 1.1rem; height: 1.1rem; margin: 0; }

.sort-field { flex: 1 1 12rem; }

body.storefront .sort-select { font-size: var(--text-sm); }

.sort-submit[hidden] { display: none; }

.shelf-count {
    flex: 1 1 100%;
    margin: 0;
    color: var(--ink-soft);
    font-size: var(--text-sm);
}

.clear-filters { margin-left: 0.5rem; font-weight: 600; }

/* Nothing matched: a card's worth of reassurance where the grid would be. */
.shelf-empty {
    padding: 1.5rem 1.25rem;
    background: var(--paper);
    border: 1px dashed var(--rule-strong);
    border-radius: var(--radius);
    text-align: center;
    color: var(--ink-soft);
}

.shelf-empty p { margin: 0.25rem 0; }

@media (min-width: 60rem) {
    .filter-panel { flex: 0 1 auto; }
    .filter-panel[open] > .filter-summary { display: none; }
    .filter-panel[open] .filter-fields {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-end;
        gap: 0.6rem 0.9rem;
        margin-top: 0;
    }
    .filter-panel[open] .filter-field { width: 9.5rem; }
    .sort-field { flex: 0 0 11rem; }
    .shelf-count { flex: 1 1 6rem; align-self: center; text-align: right; }
    /* Three narrow cards: the size box need not be so much wider than the
     * quantity that the quantity is squeezed past the card's edge. */
    .product-list .add-form .field-row {
        grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
    }
}

/* A big screen: the page widens to 84rem so the shelf beside the rail holds
 * four cards rather than three in a wide margin. Header, nav, main and footer
 * widen together, so the logo still sits over the first card. */
@media (min-width: 90rem) {
    .store-header-inner,
    .store-band-inner,
    .store-main,
    .store-footer p { max-width: 84rem; }
    .product-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* The two delivery boxes at the top of the checkout: pickup and mail, each
 * with its cost. Side by side from 40rem, stacked on a phone. Each box is a
 * button in its own form; the chosen one carries the accent edge. */
.delivery-choice {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.75rem;
    margin: 0 0 1.25rem;
    padding: 0;
    border: 0;
}

.delivery-choice-legend {
    padding: 0;
    margin-bottom: 0.5rem;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink-soft);
}

.delivery-option-form { margin: 0; }

body.storefront .delivery-option {
    display: grid;
    gap: 0.2rem;
    width: 100%;
    height: 100%;
    padding: 0.9rem 1rem;
    border: 2px solid var(--rule-strong);
    border-radius: var(--radius);
    background: var(--paper);
    color: var(--ink);
    text-align: left;
    cursor: pointer;
    box-shadow: none;
}

body.storefront .delivery-option:hover { border-color: var(--ink-soft); }

body.storefront .delivery-option--chosen {
    border-color: var(--brand-deep);
    background: var(--brand-wash);
    box-shadow: inset 0 0 0 1px var(--brand-deep);
}

.delivery-option-title { font-size: var(--text-base); font-weight: 600; }
.delivery-option-cost { font-size: var(--text-lg); font-weight: 600; color: var(--good); }
.delivery-option-note { font-size: var(--text-xs); color: var(--ink-soft); }

.checkout-pickup-closed,
.cart-pickup-closed {
    margin: 0 0 1rem;
    padding: 0.75rem 1rem;
    border-left: 4px solid var(--rule-strong);
    border-radius: var(--radius-sm);
    background: var(--page);
    color: var(--ink-soft);
    font-size: var(--text-sm);
}

@media (min-width: 40rem) {
    .delivery-choice { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .delivery-choice-legend { grid-column: 1 / -1; }
}

/* The Colour menu storefront.js builds in front of a colour product's size
 * menu takes the whole row, so a long colour name is never cut short. */
.field-row > .field--colour { grid-column: 1 / -1; }

/* ------------------------------------------------------------------ */
/* The header in one row                                               */
/* ------------------------------------------------------------------ */

/* From 60rem the mark, the bar, the search box and the cart share one row,
 * and the header sticks, so a shopper halfway down a long shelf can change
 * department or open the basket without scrolling back up. It is about
 * 4rem tall, under the 6rem the cart rail and the checkout summary stick
 * at. */
@media (min-width: 60rem) {
    .store-header {
        position: sticky;
        top: 0;
    }

    .store-header-inner {
        flex-wrap: nowrap;
        gap: 1rem;
        padding-block: 0.5rem;
    }

    .store-nav {
        order: 2;
        flex: 0 1 auto;
    }

    .store-nav-list { flex-wrap: nowrap; justify-content: flex-start; }

    .site-search {
        order: 3;
        flex: 1 1 12rem;
        max-width: 20rem;
        margin-left: auto;
    }

    .store-actions {
        order: 4;
        margin-left: 0;
    }

    .site-logo { height: 44px; }
}

/* ------------------------------------------------------------------ */
/* The hub, the club hubs and search                                   */
/* ------------------------------------------------------------------ */

/* The front door and the pages outside every store (owner's mockup,
 * 2026-10-20): navy, ESL red and gold, Oswald for the headings, on the same
 * 38px page stripes as the store pages. Everything stacks to one column
 * under 820px (51.25rem) and spreads out above it.
 *
 * Text on a photograph is set on a navy scrim, and every such rule names
 * the navy section it sits in (``.hero .hero-title``), so the contrast sweep
 * measures it against the navy the section paints under the photograph. */
.hub-main {
    padding: 0 0 1rem;
}

.gold-bar {
    display: block;
    width: 64px;
    height: 6px;
    margin-bottom: 1.1rem;
    background: var(--gold);
}

/* The hero: a 560px photograph, a navy scrim heaviest on the left where
 * the words are, and the headline, the line, the search and the team line
 * on top of it. */
.hero {
    position: relative;
    display: flex;
    align-items: center;
    min-height: 560px;
    overflow: hidden;
    background-color: var(--navy);
}

.hero-photo {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 65% center;
}

.hero::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, rgba(19, 33, 63, 0.94) 0%,
                                rgba(19, 33, 63, 0.78) 45%,
                                rgba(19, 33, 63, 0.15) 100%);
}

.hero-inner {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: 72rem;
    margin: 0 auto;
    padding: 3rem 1rem 5.5rem;
}

body.storefront .hero .hero-title {
    max-width: 11ch;
    margin: 0 0 1rem;
    font-family: var(--heading-font);
    font-size: clamp(2.6rem, 9vw, 76px);
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.01em;
    text-transform: uppercase;
    color: #ffffff;
}

.hero .hero-line {
    max-width: 34rem;
    margin: 0 0 1.5rem;
    font-size: var(--text-lg);
    color: #ffffff;
}

.hero .hero-team {
    margin: 1rem 0 0;
    font-size: var(--text-sm);
    color: #ffffff;
}

body.storefront .hero .hero-team a { color: var(--gold); }

/* The big search box: white, with a gold SEARCH button. The same box sits
 * at the top of the search page, on paper, with a rule round it. */
.hero-search {
    display: flex;
    width: 100%;
    max-width: 36rem;
    min-height: 56px;
    padding: 0.3rem;
    background: var(--paper);
    border-radius: var(--radius);
    box-shadow: 0 6px 18px rgba(19, 33, 63, 0.25);
}

.hero-search:focus-within {
    outline: 3px solid var(--gold);
    outline-offset: 2px;
}

body.storefront .hero-search-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    padding: 0 0.85rem;
    background: var(--paper);
    color: var(--ink);
    font: inherit;
    font-size: var(--text-base);
}

body.storefront .hero-search-input:focus-visible { outline: none; }

.hero-search-input::placeholder { color: var(--ink-soft); opacity: 1; }

body.storefront .hero-search-button {
    flex: none;
    min-height: 48px;
    padding: 0 1.4rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: var(--gold);
    color: var(--navy);
    font-family: var(--heading-font);
    font-size: var(--text-base);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
}

body.storefront .hero-search-button:hover { background: #ffd83d; }

/* The featured store's card, overlapping the hero's foot by 56px: the red
 * LIMITED TIME tag, the day to order by, the store's name, its sentence and
 * a navy button, with the photograph on the right half from 820px. */
.featured-card {
    position: relative;
    z-index: 2;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    width: calc(100% - 2rem);
    max-width: 70rem;
    margin: -56px auto 0;
    overflow: hidden;
    background: var(--paper);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lift);
}

.featured-card + .featured-card { margin-top: 1.5rem; }

.featured-body {
    padding: 1.75rem 1.5rem;
}

.featured-card .featured-tag {
    display: inline-block;
    padding: 0.25rem 0.65rem;
    border-radius: 4px;
    background: var(--esl-red);
    color: #ffffff;
    font-family: var(--heading-font);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.featured-card .featured-deadline {
    margin: 0.85rem 0 0.25rem;
    font-weight: 600;
    color: var(--ink-soft);
}

body.storefront .featured-card .featured-title {
    margin: 0 0 0.6rem;
    font-family: var(--heading-font);
    font-size: clamp(1.75rem, 4vw, 2.4rem);
    font-weight: 700;
    letter-spacing: 0.01em;
    line-height: 1.05;
    text-transform: uppercase;
    color: var(--navy);
}

.featured-card .featured-blurb {
    margin: 0 0 1.25rem;
    color: var(--ink);
}

body.storefront .featured-button {
    display: inline-flex;
    align-items: center;
    min-height: 48px;
    padding: 0 1.5rem;
    border-radius: var(--radius-sm);
    background: var(--navy);
    color: #ffffff;
    font-family: var(--heading-font);
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-decoration: none;
}

body.storefront .featured-button:hover {
    background: var(--navy-raised);
    text-decoration: none;
}

.featured-media img {
    display: block;
    width: 100%;
    height: 100%;
    min-height: 220px;
    max-height: 22rem;
    object-fit: cover;
}

/* A sale's deepest cut: the product on a light ground, a big red badge in
 * the corner, and the struck price beside the new one, so the discount is
 * read before anything else on the card. */
body.storefront .featured-deal {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    padding: 1.5rem 1.5rem 1.75rem;
    background: #f3f5f8;
    color: var(--ink);
    text-decoration: none;
}

body.storefront .featured-deal:hover { text-decoration: none; }

.featured-deal .deal-photo {
    width: 100%;
    max-width: 18rem;
    height: 15rem;
    min-height: 0;
    object-fit: contain;
    mix-blend-mode: multiply;
}

.featured-deal .deal-badge {
    position: absolute;
    top: 1rem;
    right: 1rem;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    width: 8.5rem;
    height: 8.5rem;
    padding: 0.5rem;
    border-radius: 50%;
    background: var(--esl-red);
    color: #ffffff;
    font-family: var(--heading-font);
    font-weight: 700;
    line-height: 0.95;
    text-align: center;
    text-transform: uppercase;
    box-shadow: var(--shadow-lift);
    transform: rotate(-8deg);
}

.featured-deal .deal-badge-big { font-size: 1.7rem; }

.featured-deal .deal-badge-small {
    font-size: 0.8rem;
    letter-spacing: 0.06em;
}

.featured-deal .deal-text {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.25rem;
    text-align: center;
}

.featured-deal .deal-name {
    font-weight: 600;
    color: var(--navy);
}

.featured-deal .deal-prices {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
}

.featured-deal .deal-was {
    font-size: 1.15rem;
    color: var(--ink-soft);
    text-decoration-thickness: 2px;
}

.featured-deal .deal-now {
    font-family: var(--heading-font);
    font-size: 2.25rem;
    font-weight: 700;
    color: var(--esl-red);
}

/* The three department tiles: a light shelf of what the department sells
 * over a navy band carrying the title, its line and the gold call to
 * action. The product photos are on white, so they multiply into the
 * shelf's tint and read as one display rather than a row of boxes. */
.departments {
    max-width: 72rem;
    margin: 3.5rem auto 0;
    padding: 0 1rem;
}

body.storefront .section-title {
    margin: 0 0 1.25rem;
    font-family: var(--heading-font);
    font-size: 2rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--navy);
}

.department-list,
.club-list,
.search-results,
.search-departments {
    list-style: none;
    margin: 0;
    padding: 0;
}

.department-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 1rem;
}

body.storefront .department-tile {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
    border-radius: var(--radius);
    background-color: var(--navy);
    color: #ffffff;
    text-decoration: none;
    box-shadow: var(--shadow);
}

body.storefront .department-tile:hover { text-decoration: none; }

.department-list > li { display: flex; }
.department-list > li > .department-tile { flex: 1; }

.department-shelf {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: center;
    gap: 0.5rem;
    height: 240px;
    padding: 1rem;
    background: #f3f5f8;
}

.department-shelf--3 { grid-template-columns: 1.4fr 1fr 1fr; }

.department-shelf--4 { grid-template-rows: repeat(2, minmax(0, 1fr)); }

.shelf-item {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    min-height: 0;
}

.department-photo {
    display: block;
    width: 100%;
    height: 100%;
    max-height: 100%;
    object-fit: contain;
    mix-blend-mode: multiply;
    transition: transform 300ms ease;
}

.department-tile:hover .department-photo { transform: scale(1.05); }

@media (prefers-reduced-motion: reduce) {
    .department-photo { transition: none; }
    .department-tile:hover .department-photo { transform: none; }
}

.department-text {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 0.35rem;
    padding: 1.25rem 1.5rem 1.5rem;
}

.department-tile .department-title {
    font-family: var(--heading-font);
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.05;
    text-transform: uppercase;
    color: #ffffff;
}

.department-tile .department-line {
    font-size: var(--text-sm);
    font-weight: 400;
    color: #ffffff;
}

.department-tile .department-cta {
    margin-top: 0.35rem;
    font-family: var(--heading-font);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--gold);
}

/* The hubs' navy footer, with the league's mark over the questions line. */
body.hub-page .store-footer {
    margin-top: 3.5rem;
    padding-top: 2.5rem;
    border-top: 0;
    background: var(--navy);
}

.footer-logo {
    display: block;
    width: auto;
    height: 72px;
    margin: 0 auto;
}

body.hub-page .store-footer p {
    text-align: center;
    color: #ffffff;
}

body.storefront.hub-page .store-footer a { color: var(--gold); }

/* A hub: the gold bar, the heading and its line, then the two club cards. */
.hub {
    max-width: 72rem;
    margin: 0 auto;
    padding: 2.5rem 1rem 0;
}

body.storefront .hub-title {
    margin: 0;
    font-family: var(--heading-font);
    font-size: clamp(2.25rem, 7vw, 3.5rem);
    font-weight: 700;
    letter-spacing: 0.02em;
    line-height: 1.05;
    text-transform: uppercase;
    color: var(--navy);
}

.hub-line {
    margin: 0.5rem 0 2rem;
    font-size: var(--text-lg);
    color: var(--ink-soft);
}

.club-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 1.25rem;
}

.club-card {
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--paper);
    box-shadow: var(--shadow);
}

body.storefront .club-link {
    display: block;
    color: var(--ink);
    text-decoration: none;
}

body.storefront .club-link:hover { text-decoration: none; }

/* The card's 300px coloured top: navy for CFA, red for Elite, with the
 * club's mark over a faded photograph. */
.club-top {
    position: relative;
    display: grid;
    place-items: center;
    height: 300px;
    overflow: hidden;
    background-color: var(--navy);
}

.club-card--red .club-top { background-color: var(--esl-red); }

.club-photo {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Faded, so the mark reads first; enough to be seen on the navy too. */
    opacity: 0.32;
    filter: grayscale(1);
}

.club-logo {
    position: relative;
    width: auto;
    height: auto;
    max-width: 60%;
    max-height: 200px;
}

.club-body {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    padding: 1.25rem 1.5rem;
}

.club-card .club-name {
    font-family: var(--heading-font);
    font-size: 1.5rem;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--navy);
}

.club-card .club-cta {
    font-family: var(--heading-font);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--esl-red);
}

.club-card .club-link--soon .club-cta { color: var(--ink-soft); }

.club-link:hover .club-cta { text-decoration: underline; }

/* The CFA chooser: one card per kind of team, navy-edged, then the "not
 * sure" link to the email lookup. */
.team-choice-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 1rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.team-choice {
    overflow: hidden;
    border-left: 6px solid var(--navy);
    border-radius: var(--radius);
    background: var(--paper);
    box-shadow: var(--shadow);
}

body.storefront .team-choice-link {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    height: 100%;
    padding: 1.25rem 1.5rem;
    color: var(--ink);
    text-decoration: none;
}

/* A league's mark (EA, GA) in the card's top right corner, so the names
 * and calls to action still line up with the cards that have none. */
body.storefront .team-choice-link { position: relative; }

.team-choice-logo {
    position: absolute;
    top: 1rem;
    right: 1.25rem;
}

.team-choice-logo img {
    display: block;
    width: auto;
    height: 64px;
}

.team-choice-logo ~ .team-choice-name,
.team-choice-logo ~ .team-choice-note { padding-right: 5rem; }

.team-choice-name {
    font-family: var(--heading-font);
    font-size: 1.5rem;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--navy);
}

.team-choice-note {
    font-size: var(--text-sm, 0.9rem);
    color: var(--ink-soft);
}

.team-choice-cta {
    margin-top: auto;
    font-family: var(--heading-font);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--esl-red);
}

.team-choice--soon .team-choice-cta { color: var(--ink-soft); }

.team-choice-link:hover .team-choice-cta { text-decoration: underline; }

.team-choice-unsure {
    margin: 1.75rem 0 0;
    font-size: var(--text-lg);
}

/* The girls' fit note on a girls' team's store page. */
.girls-fit-note {
    margin: 0.25rem 0 0;
    color: var(--ink-soft);
}

/* Search: the box on paper, then one group per store of small cards. */
.search-form {
    margin: 0 0 1rem;
    border: 1px solid var(--rule-strong);
    box-shadow: var(--shadow);
}

.search-group { margin-top: 2rem; }

body.storefront .search-store {
    margin: 0 0 0.85rem;
    font-family: var(--heading-font);
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

body.storefront .search-store a { color: var(--navy); }

.search-results {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem;
}

body.storefront .search-card {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--paper);
    color: var(--ink);
    text-decoration: none;
    box-shadow: var(--shadow);
}

body.storefront .search-card:hover {
    box-shadow: var(--shadow-lift);
    text-decoration: none;
}

.search-card .search-card-name {
    padding: 0.75rem 0.9rem 0;
    font-weight: 600;
    color: var(--ink);
}

.search-card .search-card-price {
    padding: 0.25rem 0.9rem 0.9rem;
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--ink-soft);
}

.search-empty {
    margin-top: 1.5rem;
    padding: 1.5rem;
    border-radius: var(--radius);
    background: var(--paper);
    box-shadow: var(--shadow);
}

.search-empty p { margin: 0 0 1rem; }

.search-departments {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.5rem;
}

@media (min-width: 51.25rem) {
    .hero-inner { padding-inline: 2rem; }

    .featured-card { grid-template-columns: repeat(2, minmax(0, 1fr)); }

    .featured-body { padding: 2.5rem; }

    .featured-media img { max-height: none; }

    .departments,
    .hub { padding-inline: 2rem; }

    .department-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }

    .club-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }

    .team-choice-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }

    .search-results { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
