/* input(3158,1): run-time error CSS1019: Unexpected token, found '}'
input(4375,1): run-time error CSS1019: Unexpected token, found '}' */
/*
 * Retail theme — Bright Start Supply
 * =================================================================
 * Loaded AFTER site.css and feature stylesheets. Its whole job is to pull the
 * storefront from the previous indigo/violet gradient look into a schoolwear
 * retail idiom: white pages, navy structure, red reserved for promotions,
 * bold uppercase navigation, and near-rectangular controls.
 *
 * It lives in its own file (rather than editing ~26 gradient declarations
 * scattered through site.css) so the visual change stays reviewable in one
 * place and can be reverted by removing a single <link>.
 *
 * Colour values come from design-tokens.css — don't hardcode hexes here.
 */

/* ============================================================
 * 1. Page canvas
 * ============================================================ */

body {
    background: var(--color-bg-page);
    color: var(--color-text);
}

/* ============================================================
 * 2. Header & navigation
 * The old header was a translucent blurred bar. Retail headers are solid:
 * they sit still, and the category row reads as a shop directory.
 * ============================================================ */

.header {
    background: var(--color-bg-surface);
    backdrop-filter: none;
    border-bottom: 1px solid var(--color-border);
    box-shadow: none;
}

/* Wordmark. The gradient-clipped text is replaced by a solid navy lockup —
   gradient text can't be reproduced in email, print or a favicon, so it was
   never usable as an actual brand mark. */
.logo {
    background: none;
    -webkit-background-clip: initial;
    background-clip: initial;
    -webkit-text-fill-color: currentColor;
    color: var(--color-brand-primary);
    font-size: 1.5rem;
    font-weight: var(--font-weight-black);
    letter-spacing: 0.02em;
    text-transform: uppercase;
    line-height: 1.1;
}

.logo-link,
.logo-link:hover,
.logo-link:focus {
    color: var(--color-brand-primary);
}

/* Category navigation: bold, uppercase, tracked. */
.nav-menu {
    gap: 1.75rem;
}

.nav-menu a {
    color: var(--color-text);
    font-size: var(--nav-font-size);
    font-weight: var(--nav-font-weight);
    text-transform: var(--nav-text-transform);
    letter-spacing: var(--nav-letter-spacing);
    padding: 0.5rem 0;
    border-bottom: 2px solid transparent;
    transition: color var(--transition-fast), border-color var(--transition-fast);
}

/* Underline on hover rather than a lift — nav items shouldn't move under
   the cursor while someone is aiming at them. */
.nav-menu a:hover,
.nav-menu a:focus-visible {
    color: var(--color-brand-primary);
    border-bottom-color: var(--color-brand-primary);
    transform: none;
}

/* ============================================================
 * 3. Buttons
 * Flat fills, squared corners, uppercase. No transform on hover.
 * ============================================================ */

.btn-primary,
.btn-secondary,
.btn-view {
    border-radius: var(--radius-md);
    text-transform: var(--button-text-transform);
    letter-spacing: var(--button-letter-spacing);
    font-weight: var(--button-font-weight);
    font-size: 0.875rem;
    transition: background-color var(--transition-fast), color var(--transition-fast),
                border-color var(--transition-fast);
}

.btn-primary {
    background: var(--color-brand-primary);
    color: var(--color-text-inverse);
    box-shadow: none;
    border: 1px solid var(--color-brand-primary);
    padding: 0.875rem 2rem;
}

.btn-primary:hover,
.btn-primary:focus-visible {
    background: var(--color-brand-primary-hover);
    border-color: var(--color-brand-primary-hover);
    box-shadow: none;
    transform: none;
}

.btn-secondary {
    background: var(--color-bg-surface);
    color: var(--color-brand-primary);
    border: 1px solid var(--color-brand-primary);
    padding: 0.875rem 2rem;
}

.btn-secondary:hover,
.btn-secondary:focus-visible {
    background: var(--color-brand-primary);
    color: var(--color-text-inverse);
    border-color: var(--color-brand-primary);
    transform: none;
}

/* Promotional CTA — the one place red is allowed to carry a whole control. */
.btn-sale {
    background: var(--color-brand-sale);
    color: var(--color-text-inverse);
    border: 1px solid var(--color-brand-sale);
    border-radius: var(--radius-md);
    padding: 0.875rem 2rem;
    font-weight: var(--button-font-weight);
    text-transform: var(--button-text-transform);
    letter-spacing: var(--button-letter-spacing);
    text-decoration: none;
    display: inline-block;
    cursor: pointer;
}

.btn-sale:hover,
.btn-sale:focus-visible {
    background: var(--color-brand-sale-hover);
    border-color: var(--color-brand-sale-hover);
    color: var(--color-text-inverse);
}

/* ============================================================
 * 4. Product & bundle cards
 * Bordered rather than floating; the image does the selling.
 * ============================================================ */

.bundle-card,
.supply-card {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: none;
    background: var(--color-bg-surface);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.bundle-card:hover,
.supply-card:hover {
    border-color: var(--color-brand-primary);
    box-shadow: var(--shadow-sm);
    transform: none;
}

.bundle-image,
.bundle-image-container {
    background: var(--color-bg-surface-alt);
    border-radius: 0;
}

.bundle-name {
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0;
}

.bundle-price {
    color: var(--color-text);
    font-weight: var(--font-weight-bold);
}

/* Sale pricing is the exception that earns red. */
.bundle-price--sale,
.price-sale {
    color: var(--color-brand-sale);
}

.price-was {
    color: var(--color-text-subtle);
    text-decoration: line-through;
    font-weight: var(--font-weight-regular);
    margin-right: 0.5rem;
}

/* ============================================================
 * 5. Badges
 * Squared, flat, no gradients. Navy for classification, red for promotion.
 * ============================================================ */

.school-badge,
.gender-badge,
.line-badge,
.status-badge {
    border-radius: var(--radius-sm);
    background-image: none;
    font-weight: var(--font-weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: 0.6875rem;
    padding: 0.3125rem 0.625rem;
}

.school-badge {
    background: var(--color-bg-surface-alt);
    color: var(--color-text-muted);
}

.gender-badge,
.gender-badge.male,
.gender-badge.female,
.gender-badge.unisex {
    background: var(--color-bg-surface-alt);
    color: var(--color-text-muted);
}

/* Product-line badges. Navy = uniforms, red = supplies: two flat fills that
   stay distinguishable in greyscale and for colour-blind shoppers because
   their lightness differs, and both carry a text label anyway. */
.line-badge-uniform {
    background: var(--color-brand-primary);
    color: var(--color-text-inverse);
}

.line-badge-supply {
    background: var(--color-brand-sale);
    color: var(--color-text-inverse);
}

.line-badge-mixed {
    background: var(--color-bg-surface);
    color: var(--color-brand-primary);
    border: 1px solid var(--color-brand-primary);
}

.status-badge.inactive {
    background: var(--color-text-muted);
    color: var(--color-text-inverse);
}

/* Discount flag — the "30% OFF" style callout. */
.promo-badge {
    display: inline-block;
    background: var(--color-brand-sale);
    color: var(--color-text-inverse);
    font-weight: var(--font-weight-black);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: 0.75rem;
    padding: 0.375rem 0.75rem;
    border-radius: var(--radius-sm);
}

/* ============================================================
 * 6. Page headers & section headings
 * ============================================================ */

/* Page-header colours are set in section 18 — the band is navy, so its text
   has to be inverse. Defining them twice is what produced dark-on-dark. */
.page-title {
    font-weight: var(--font-weight-black);
    letter-spacing: -0.01em;
}

.section-heading {
    font-weight: var(--font-weight-black);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 1.125rem;
    color: var(--color-text);
    border-bottom: 2px solid var(--color-brand-primary);
    display: inline-block;
    padding-bottom: 0.375rem;
}

.items-group-heading {
    color: var(--color-brand-primary);
}

/* ============================================================
 * 7. Filter bar
 * ============================================================ */

.filter-select {
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border-strong);
    background: var(--color-bg-surface);
    color: var(--color-text);
}

.filter-label {
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-weight: var(--font-weight-bold);
    font-size: 0.75rem;
    color: var(--color-text-muted);
}

.results-count {
    background: transparent;
    border-radius: 0;
    color: var(--color-text-muted);
}

/* ============================================================
 * 8. Toast (supplies add-to-cart confirmation)
 * ============================================================ */

.supply-toast {
    background: var(--color-brand-primary);
    border-radius: var(--radius-md);
    font-weight: var(--font-weight-semibold);
}

.supply-toast-error {
    background: var(--color-brand-sale);
}

/* ============================================================
 * 9. Cart badge — stays circular, switches to promotional red so it
 *    registers against the navy chrome.
 * ============================================================ */

.cart-badge {
    background: var(--color-brand-sale);
    color: var(--color-text-inverse);
    border-radius: var(--radius-pill);
    font-weight: var(--font-weight-bold);
}

/* ============================================================
 * 10. Footer
 * ============================================================ */

#footer,
.footer {
    background: var(--color-brand-primary);
    background-image: none;
    color: var(--color-text-inverse);
}

#footer a,
.footer a {
    color: var(--color-text-inverse);
    opacity: 0.85;
    text-decoration: none;
}

#footer a:hover,
.footer a:hover {
    opacity: 1;
    text-decoration: underline;
}

.footer h3,
.footer h4,
#footer h3,
#footer h4,
/* .footer-section h3 by name as well as by descendant: site.css sets
   `.footer-section h3 { color: #12315c }`, which is the *same navy the footer
   background uses* — so every column heading was rendering invisibly against it.
   Matching that selector's specificity is what lets the colour below win. */
.footer .footer-section h3,
#footer .footer-section h3 {
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.8125rem;
    font-weight: var(--font-weight-bold);
    color: var(--color-text-inverse);
}

/* ============================================================
 * 11. Focus visibility — a flatter design needs a stronger focus ring,
 *     since there's less shadow/colour to imply state.
 * ============================================================ */

a:focus-visible,
button:focus-visible,
select:focus-visible,
input:focus-visible,
textarea:focus-visible {
    outline: 2px solid var(--color-brand-primary);
    outline-offset: 2px;
}

/* ============================================================
 * 12. Motion preferences
 * ============================================================ */

@media (prefers-reduced-motion: reduce) {
    .bundle-card,
    .supply-card,
    .nav-menu a,
    .btn-primary,
    .btn-secondary,
    .btn-sale {
        transition: none;
    }
}

/* ============================================================
 * 13. Brand lockup
 * Two-line wordmark: the name in heavy navy, "SUPPLY" beneath it as a
 * tracked rule line. Text-only so it scales and stays crisp anywhere the
 * old gradient-clipped treatment couldn't go.
 * ============================================================ */

.brand-lockup {
    display: inline-flex;
    flex-direction: column;
    line-height: 1;
    text-decoration: none;
}

.brand-lockup__name {
    font-size: 1.4rem;
    font-weight: var(--font-weight-black);
    letter-spacing: -0.01em;
    text-transform: uppercase;
    color: var(--color-brand-primary);
}

.brand-lockup__suffix {
    font-size: 0.6875rem;
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.42em;
    text-transform: uppercase;
    color: var(--color-brand-sale);
    margin-top: 0.1875rem;
    /* Optical correction: letter-spacing adds trailing space after the last
       glyph, which would push the word off-centre against the line above. */
    text-indent: 0.42em;
}

.brand-lockup:hover .brand-lockup__name,
.brand-lockup:focus-visible .brand-lockup__name {
    color: var(--color-brand-primary-hover);
}

@media (max-width: 640px) {
    .brand-lockup__name { font-size: 1.15rem; }
    .brand-lockup__suffix { font-size: 0.625rem; letter-spacing: 0.3em; text-indent: 0.3em; }
}

/* ============================================================
 * 14. Footer social row
 * Sits between the link columns and the copyright line.
 * ============================================================ */

.footer-social {
    display: flex;
    justify-content: center;
    gap: 0.75rem;
    padding: 1.5rem 0 0.5rem;
    border-top: 1px solid rgba(255, 255, 255, 0.15);
    margin-top: 1.5rem;
}

.footer-social .social-link {
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;
    border: 1px solid rgba(255, 255, 255, 0.35);
    border-radius: var(--radius-pill);
    color: var(--color-text-inverse);
    opacity: 0.85;
    font-size: 0.9375rem;
    text-decoration: none;
    transition: background-color var(--transition-fast), opacity var(--transition-fast),
                border-color var(--transition-fast);
}

.footer-social .social-link:hover,
.footer-social .social-link:focus-visible {
    background: var(--color-text-inverse);
    border-color: var(--color-text-inverse);
    color: var(--color-brand-primary);
    opacity: 1;
    text-decoration: none;
}

/* Columns that fit themselves to however many sections there are.
 *
 * This was `repeat(4, ...)`, hardcoded when the footer had four. Adding a fifth
 * ("Read") pushed it onto its own row, alone on the left, with three empty cells
 * beside it — the grid could not do anything else, because it had been told the
 * answer instead of the rule.
 *
 * auto-fit with a 10rem floor gives five across on a desktop, four on a small
 * laptop, then two, then one, and copes with the sixth column somebody adds next
 * without anybody having to remember this rule exists. */
@media (min-width: 992px) {
    .footer-content {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
        gap: 2rem 1.75rem;
        align-items: start;
    }
}

/* ============================================================
 * 15. Nav search
 * Sits in the right-hand nav cluster, left of the cart icon.
 * ============================================================ */

.nav-search {
    display: flex;
    align-items: stretch;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--color-bg-surface);
    max-width: 16rem;
}

.nav-search:focus-within {
    border-color: var(--color-brand-primary);
}

.nav-search__input {
    border: none;
    outline: none;
    padding: 0.5rem 0.75rem;
    font-size: 0.875rem;
    color: var(--color-text);
    background: transparent;
    min-width: 0;
    width: 100%;
}

.nav-search__input::placeholder {
    color: var(--color-text-subtle);
}

/* The wrapper already shows focus, so the inner control doesn't need its own
   ring — two nested outlines read as a rendering bug. */
.nav-search__input:focus-visible {
    outline: none;
}

.nav-search__button {
    border: none;
    background: var(--color-brand-primary);
    color: var(--color-text-inverse);
    padding: 0 0.875rem;
    cursor: pointer;
    display: grid;
    place-items: center;
    transition: background-color var(--transition-fast);
}

.nav-search__button:hover,
.nav-search__button:focus-visible {
    background: var(--color-brand-primary-hover);
}

/* Below the desktop nav breakpoint the search would squeeze the cart and
   account controls; give it the full row instead of shrinking it to nothing. */
@media (max-width: 900px) {
    .nav-search {
        order: 10;
        max-width: none;
        width: 100%;
        margin-top: 0.5rem;
    }
}

/* ============================================================
 * 16. Search results page
 * ============================================================ */

.search-results__summary {
    color: var(--color-text-muted);
    font-size: 0.9375rem;
    margin-bottom: 1.5rem;
}

.search-results__term {
    color: var(--color-text);
    font-weight: var(--font-weight-bold);
}

.search-empty {
    text-align: center;
    padding: 3rem 1.5rem;
}

.search-empty__icon {
    font-size: 2.5rem;
    margin-bottom: 0.75rem;
}

.search-empty__suggestions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: center;
    margin-top: 1.25rem;
}

.search-suggestion {
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    padding: 0.375rem 0.75rem;
    font-size: 0.8125rem;
    color: var(--color-text-muted);
    text-decoration: none;
}

.search-suggestion:hover {
    border-color: var(--color-brand-primary);
    color: var(--color-brand-primary);
}

/* ============================================================
 * 17. Header layout fixes
 * The nav row was authored for a one-line gradient wordmark and a shorter
 * menu. With the two-line lockup, an extra category and a search field it
 * ran out of room — the wordmark collided with "SHOP BY SCHOOL" and the
 * search input was crushed to a few characters.
 * ============================================================ */

.nav-container {
    /* space-between alone let the columns touch; a real gap guarantees
       separation no matter how the middle column sizes itself. */
    gap: 2rem;
    padding: 0.75rem 2rem;
}

.logo {
    /* The lockup is two stacked lines, so it doesn't need 2rem of type. */
    font-size: 1.4rem;
    margin-right: 0.5rem;
}

.nav-menu {
    gap: 1.25rem;
}

.nav-menu a {
    font-size: 0.75rem;
    letter-spacing: 0.06em;
}

.nav-buttons {
    gap: 0.75rem;
}

/* Give the search a real minimum so the placeholder isn't truncated, but let
   it shrink before the nav links do. */
.nav-search {
    flex: 0 1 15rem;
    min-width: 11rem;
}

.nav-search__button {
    flex: 0 0 auto;
    padding: 0 0.75rem;
}

/* Below this the header is too tight for one row — wrap it. */
@media (max-width: 1200px) {
    .nav-container {
        flex-wrap: wrap;
        gap: 1rem;
    }

    .nav-container > nav {
        order: 3;
        width: 100%;
    }

    .nav-menu {
        flex-wrap: wrap;
        gap: 1rem;
    }
}

/* ============================================================
 * 18. Remaining pre-theme chrome
 * Rules site.css defines that the earlier sections didn't name.
 * ============================================================ */

/* Was an amber gradient pill — promotional eyebrow, so it takes the sale red. */
.hero-badge {
    background: var(--color-brand-sale);
    background-image: none;
    color: var(--color-text-inverse);
    border-radius: var(--radius-sm);
    padding: 0.4375rem 0.875rem;
    font-size: 0.75rem;
    font-weight: var(--font-weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

/* Was a grey 50px pill. */
.btn-login {
    border: 1px solid var(--color-brand-primary);
    border-radius: var(--radius-md);
    color: var(--color-brand-primary);
    background: var(--color-bg-surface);
    padding: 0.5rem 1.125rem;
    font-size: 0.8125rem;
    font-weight: var(--button-font-weight);
    text-transform: var(--button-text-transform);
    letter-spacing: var(--button-letter-spacing);
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

.btn-login:hover,
.btn-login:focus-visible {
    background: var(--color-brand-primary);
    color: var(--color-text-inverse);
    transform: none;
}

/* The hero sat on a tinted wash; the retail canvas is white. */
.hero {
    background: var(--color-bg-surface);
}

.hero::before {
    display: none;
}

/* "In One Box" — the class name is historical, it's a solid accent now. */
.hero .gradient-text,
.gradient-text {
    background: none;
    -webkit-background-clip: initial;
    background-clip: initial;
    -webkit-text-fill-color: currentColor;
    color: var(--color-brand-sale);
}

/* Interior page headers are a solid navy band. Every piece of text sitting on
   it must be inverse — the title, the subtitle and the breadcrumb. */
.page-header {
    background: var(--color-brand-primary);
    background-image: none;
}

.page-header::before {
    display: none;
}

.page-header .page-title,
.page-header .page-title .school-icon {
    color: var(--color-text-inverse);
}

.page-header .page-subtitle {
    color: rgba(255, 255, 255, 0.85);
}

.page-header .breadcrumb-link {
    color: rgba(255, 255, 255, 0.8);
}

.page-header .breadcrumb-link:hover {
    color: var(--color-text-inverse);
}

.page-header .breadcrumb-current {
    color: var(--color-text-inverse);
}

.page-header .breadcrumb-separator {
    color: rgba(255, 255, 255, 0.55);
}

/* The header CTA sits on navy, so the standard navy-on-white button would
   vanish into the band. */
.page-header .btn-primary {
    background: var(--color-bg-surface);
    color: var(--color-brand-primary);
    border-color: var(--color-bg-surface);
}

.page-header .btn-primary:hover,
.page-header .btn-primary:focus-visible {
    background: var(--color-bg-surface-alt);
    color: var(--color-brand-primary-hover);
    border-color: var(--color-bg-surface-alt);
}

.school-bundles-page {
    background: var(--color-bg-surface);
}

/* Section eyebrow above headings. */
.section-badge {
    background: var(--color-bg-surface-alt);
    background-image: none;
    color: var(--color-brand-primary);
    border-radius: var(--radius-sm);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-weight: var(--font-weight-bold);
    font-size: 0.75rem;
}

/* Filter bar floated on a heavy shadow; flatten it onto the page. */
.filter-bar {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: none;
}

/* Cards lifted and scaled on hover, which shifted the grid around the cursor. */
.bundle-card:hover {
    transform: none;
}

.bundle-card:hover .bundle-img {
    transform: none;
}

/* ============================================================
 * 19. Hero floating cards
 * Fills moved off inline styles so the palette lives in one file.
 * ============================================================ */

.floating-card .card-icon {
    background-image: none;
    color: var(--color-text-inverse);
    border-radius: var(--radius-lg);
}

.card-icon--uniform  { background: var(--color-brand-primary); }
.card-icon--supply   { background: var(--color-brand-sale); }
.card-icon--approved { background: var(--color-brand-secondary); }

.floating-card .card-title {
    color: var(--color-text-muted);
}

/* ============================================================
 * 20. Palette guard
 * The three token colours below are the only accents the storefront should
 * use. If a future rule needs a new colour, add it to design-tokens.css
 * rather than inlining a hex — the codebase previously carried four
 * competing ramps (slate, tailwind grey, bootstrap, ad-hoc) and they were
 * visibly inconsistent side by side.
 * ============================================================ */

/* ============================================================
 * 21. School search results
 * Rows rather than cards: a parent scanning for their school name reads a
 * list far faster than a grid, and the logo gives a quick visual anchor.
 * ============================================================ */

.school-results {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-bottom: 2.5rem;
}

.school-result {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.875rem 1rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    background: var(--color-bg-surface);
    text-decoration: none;
    color: inherit;
    transition: border-color var(--transition-fast), background-color var(--transition-fast);
}

.school-result:hover,
.school-result:focus-visible {
    border-color: var(--color-brand-primary);
    background: var(--color-bg-surface-alt);
    text-decoration: none;
}

.school-result__logo {
    flex: 0 0 3rem;
    height: 3rem;
    display: grid;
    place-items: center;
    background: var(--color-bg-surface-alt);
    border-radius: var(--radius-md);
    font-size: 1.25rem;
    overflow: hidden;
}

.school-result__logo img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.school-result__body {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    min-width: 0;
}

.school-result__name {
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
}

.school-result__meta {
    font-size: 0.8125rem;
    color: var(--color-text-muted);
}

.school-result__go {
    margin-left: auto;
    color: var(--color-brand-primary);
    font-weight: var(--font-weight-bold);
}

/* ============================================================
 * 22. Nav search button + typeahead dropdown
 *
 * The button gets an explicit box rather than relying on padding around the
 * icon. Font Awesome is loaded with the print-media swap trick, so before it
 * arrives the <i> has no glyph and a padding-sized button collapses to a
 * sliver, then jumps when the font lands. A fixed square reserves the space
 * and keeps the control aligned with the input at every stage.
 * ============================================================ */

.nav-search {
    position: relative;   /* anchor for the dropdown */
    height: 2.5rem;
}

.nav-search__input {
    height: 100%;
    line-height: 1;
}

.nav-search__button {
    flex: 0 0 2.5rem;
    width: 2.5rem;
    height: 100%;
    padding: 0;
    margin: 0;
    line-height: 1;
    border: none;
    border-radius: 0;
    font-size: 0.875rem;
    appearance: none;
    -webkit-appearance: none;
}

.nav-search__button i {
    /* Inherit the button's colour so the glyph can't end up on its own
       stale value once Font Awesome loads. */
    color: inherit;
    font-size: inherit;
    line-height: 1;
    display: block;
}

/* ---- Typeahead dropdown ---- */

.nav-search__results {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    z-index: var(--z-dropdown);
    margin: 0;
    padding: 0.25rem;
    list-style: none;
    background: var(--color-bg-surface);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    max-height: 20rem;
    overflow-y: auto;
}

.nav-search__results[hidden] {
    display: none;
}

.nav-search__result {
    display: flex;
    flex-direction: column;
    gap: 0.0625rem;
    padding: 0.5rem 0.625rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
}

/* Same treatment for hover and keyboard focus — a keyboard user should see
   exactly what a mouse user sees. */
.nav-search__result:hover,
.nav-search__result.is-active {
    background: var(--color-brand-primary);
}

.nav-search__result:hover .nav-search__result-name,
.nav-search__result.is-active .nav-search__result-name,
.nav-search__result:hover .nav-search__result-meta,
.nav-search__result.is-active .nav-search__result-meta {
    color: var(--color-text-inverse);
}

.nav-search__result-name {
    font-size: 0.875rem;
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
}

.nav-search__result-meta {
    font-size: 0.75rem;
    color: var(--color-text-muted);
}

/* On the wrapped mobile header the dropdown should track the full-width box. */
@media (max-width: 900px) {
    .nav-search__results {
        max-height: 60vh;
    }
}

/* ============================================================
 * 23. Mini-cart preview
 * ============================================================ */

.cart-menu {
    position: relative;
    display: inline-flex;
    align-items: center;
}

.cart-preview {
    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    z-index: var(--z-dropdown);
    width: 20rem;
    max-width: calc(100vw - 2rem);
    background: var(--color-bg-surface);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    padding: 0.875rem;
}

.cart-preview[hidden] {
    display: none;
}

/* Invisible bridge across the gap between icon and panel, so the pointer
   doesn't leave the hover target while travelling to the menu. */
.cart-preview::before {
    content: '';
    position: absolute;
    top: -10px;
    left: 0;
    right: 0;
    height: 10px;
}

.cart-preview__items {
    list-style: none;
    margin: 0 0 0.75rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
}

.cart-preview__item {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
}

.cart-preview__item-info {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.cart-preview__item-name {
    font-size: 0.8125rem;
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    /* Long bundle names shouldn't push the price off the panel. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 12rem;
}

.cart-preview__item-meta {
    font-size: 0.75rem;
    color: var(--color-text-muted);
}

.cart-preview__item-price {
    font-size: 0.8125rem;
    font-weight: var(--font-weight-bold);
    white-space: nowrap;
}

.cart-preview__more {
    font-size: 0.75rem;
    color: var(--color-text-muted);
    margin: 0 0 0.75rem;
}

.cart-preview__summary {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 0.75rem;
    border-top: 1px solid var(--color-border);
    font-size: 0.875rem;
    margin-bottom: 0.75rem;
}

.cart-preview__actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
}

.cart-preview__actions .btn-primary,
.cart-preview__actions .btn-secondary {
    padding: 0.5rem 0.75rem;
    font-size: 0.75rem;
    text-align: center;
}

.cart-preview__empty {
    margin: 0 0 0.75rem;
    font-size: 0.875rem;
    color: var(--color-text-muted);
    text-align: center;
}

.cart-preview__cta {
    display: block;
    text-align: center;
    padding: 0.5rem 0.75rem;
    font-size: 0.75rem;
}

/* The panel would overflow a narrow viewport pinned to the right edge. */
@media (max-width: 480px) {
    .cart-preview {
        right: auto;
        left: 50%;
        transform: translateX(-50%);
    }
}

/* ============================================================
 * 24. Admin page offset under the fixed header
 *
 * Admin pages hardcode `margin-top: 80px` on .admin-dashboard-container to
 * clear the fixed header. That was correct for the old single-row nav, but the
 * header now wraps to a second row below 1200px (logo + buttons, then the
 * category menu), so 80px isn't enough and the page title slides underneath it.
 *
 * These override the hardcoded value at the widths where the header is taller.
 * ============================================================ */

@media (max-width: 1200px) {
    .admin-dashboard-container {
        margin-top: 132px;
    }
}

@media (max-width: 900px) {
    /* Below this the search box also takes its own row. */
    .admin-dashboard-container {
        margin-top: 186px;
    }
}

/* ============================================================
 * 25. Admin form chrome
 * Brings the admin forms onto the retail palette — they predate it and were
 * carrying their own rounded/shadowed look.
 * ============================================================ */

.admin-form-container {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: none;
}

.admin-dashboard-title {
    color: var(--color-text);
    font-weight: var(--font-weight-black);
}

.admin-action-button {
    border-radius: var(--radius-md);
    text-transform: uppercase;
    letter-spacing: var(--button-letter-spacing);
    font-weight: var(--button-font-weight);
    font-size: .8rem;
    display: inline-flex;
    align-items: center;
    gap: .4rem;
}

.admin-action-button.primary {
    background: var(--color-brand-primary);
    background-image: none;
    color: var(--color-text-inverse);
    border: 1px solid var(--color-brand-primary);
}

.admin-action-button.primary:hover {
    background: var(--color-brand-primary-hover);
    border-color: var(--color-brand-primary-hover);
}

.admin-action-button.secondary {
    background: var(--color-bg-surface);
    color: var(--color-brand-primary);
    border: 1px solid var(--color-brand-primary);
}

.admin-action-button.secondary:hover {
    background: var(--color-brand-primary);
    color: var(--color-text-inverse);
}

/* ============================================================
 * 26. Category card buttons
 *
 * .category-btn takes its fill from a per-card --accent-gradient. Guard against
 * a card variant that forgets to define one: without a fallback the var() is
 * invalid, the background drops out, and white text on a white card makes the
 * button invisible.
 * ============================================================ */

.category-btn {
    background: var(--accent-gradient, var(--color-brand-primary));
    background-image: var(--accent-gradient, none);
    border-radius: var(--radius-md);
    text-transform: uppercase;
    letter-spacing: var(--button-letter-spacing);
    font-weight: var(--button-font-weight);
    font-size: .8rem;
    color: var(--color-text-inverse);
}

.category-btn:hover {
    filter: brightness(0.92);
    transform: none;
}

/* ============================================================
 * 27. Storefront page-header and card fixes
 * All five reported against /supplies.
 * ============================================================ */

/* (a) The breadcrumb was sliding under the fixed nav. .header-content had a
   flat 3rem of padding, which was less than the header's height — and the
   header grows a row when it wraps below 1200px. */
.page-header .header-content {
    padding-top: 6rem;
    padding-bottom: 3rem;
}

@media (max-width: 1200px) {
    .page-header .header-content { padding-top: 9.5rem; }
}

@media (max-width: 900px) {
    .page-header .header-content { padding-top: 13rem; }
}

/* (b) .filter-bar carried margin-top: -2rem to tuck itself up under the navy
   band. On /supplies it follows a section heading instead, so the negative
   margin dragged it over "Individual Items" and clipped it. Only pull up when
   the bar really is the first thing after the header. */
.filter-bar {
    /* Default: sit in normal flow. */
    margin-top: 0;
}

/* Only tuck up under the navy band when the bar is genuinely the first thing
   in the container — i.e. nothing precedes it. An earlier attempt used
   :first-of-type, which broke on /supplies whenever the "Supply Kits" section
   was empty: the filter bar then became the container's first div and got
   pulled up over the "Individual Items" heading. */
.page-header + .container > .filter-bar:first-child {
    margin-top: -2rem;
}

/* A filter bar that follows a heading must never overlap it, whatever else
   matches above. */
.section-heading + .filter-bar,
.section-subheading + .filter-bar {
    margin-top: 0;
}

/* Belt and braces: headings paint above the bar's white background even if
   some other rule reintroduces a negative margin. */
.section-heading {
    position: relative;
    z-index: 1;
}

/* Give the section heading room now that nothing overlaps it. */
.section-heading {
    margin-top: 2.5rem;
}

.section-heading:first-child {
    margin-top: 0;
}

/* (c) .status-badge was never positioned — it rendered as a block above the
   image instead of overlaying it. */
.bundle-image,
.bundle-image-container {
    position: relative;
}

/* Scoped to the card thumbnail, which is the only place this overlay is wanted.
   Unscoped it matched every .status-badge in the app: the admin tables render one
   per row inside a <td> that is not positioned, so the badge was pulled out of the
   cell and the Status column looked empty. The badge rows on the bundle details
   pages (.bundle-badges) were displaced by it too. */
.bundle-image .status-badge,
.bundle-image-container .status-badge {
    position: absolute;
    top: .625rem;
    left: .625rem;
    z-index: 2;
    background: var(--color-text);
    color: var(--color-text-inverse);
    border-radius: var(--radius-sm);
    padding: .25rem .5rem;
    font-size: .6875rem;
    font-weight: var(--font-weight-bold);
    text-transform: uppercase;
    letter-spacing: .05em;
    line-height: 1.2;
}

.status-badge.inactive {
    background: var(--color-brand-sale);
    color: var(--color-text-inverse);
}

.status-badge.active {
    background: var(--color-accent-success);
    color: var(--color-text-inverse);
}

/* (d) A disabled "Out of Stock" button at 0.55 opacity was almost unreadable.
   Disabled should look unavailable, not invisible — keep full-contrast text and
   signal the state with a flat grey fill and not-allowed cursor. */
.supply-card .bundle-footer .btn-view:disabled,
.btn-view:disabled {
    opacity: 1;
    background: var(--color-bg-surface-alt);
    color: var(--color-text-muted);
    border: 1px solid var(--color-border-strong);
    cursor: not-allowed;
}

/* (e) The header CTA on the navy band needed real contrast — a translucent
   white fill on navy reads as disabled. */
.page-header .header-actions .btn-primary {
    background: var(--color-bg-surface);
    color: var(--color-brand-primary);
    border: 1px solid var(--color-bg-surface);
    backdrop-filter: none;
}

.page-header .header-actions .btn-primary:hover {
    background: var(--color-bg-surface-alt);
    color: var(--color-brand-primary-hover);
    border-color: var(--color-bg-surface-alt);
    transform: none;
    box-shadow: none;
}

/* A filter bar that belongs to a section, rather than sitting directly beneath
   the page header. Explicit modifier so the layout doesn't depend on sibling
   position — which changes when a section renders empty. */
.filter-bar--in-section {
    margin-top: 0;
}

/* ============================================================
 * 28. Footer brand block
 * The footer carried link columns and a copyright line but no brand
 * identity at all. This adds the wordmark, a tagline and three
 * what-this-site-does points above the columns.
 * ============================================================ */

.footer-brand {
    padding-bottom: 2rem;
    margin-bottom: 2rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}

/* The header lockup is navy-on-white; on the navy footer the name would
   disappear. Red-on-navy fails contrast badly (~2:1), so the suffix goes to a
   dimmed white instead of the brand red — the two-tone weight contrast still
   reads, without the legibility cost. */
/* .footer a dims links to 0.85 and underlines them on hover. Neither is right
   for a wordmark, and both out-specify .brand-lockup's own rules. */
.footer-brand .brand-lockup--inverse,
.footer-brand .brand-lockup--inverse:hover {
    opacity: 1;
    text-decoration: none;
}

.footer-brand .brand-lockup--inverse .brand-lockup__name {
    color: var(--color-text-inverse);
}

.footer-brand .brand-lockup--inverse .brand-lockup__suffix {
    color: rgba(255, 255, 255, 0.75);
}

.footer-brand .brand-lockup--inverse:hover .brand-lockup__name,
.footer-brand .brand-lockup--inverse:focus-visible .brand-lockup__name {
    color: var(--color-text-inverse);
    opacity: 0.85;
}

.footer-tagline {
    margin: 1rem 0 0;
    max-width: 46ch;
    color: var(--color-text-inverse);
    opacity: 0.85;
    font-size: 0.9375rem;
    line-height: 1.6;
}

.footer-points {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 2rem;
    margin: 1.25rem 0 0;
    padding: 0;
    list-style: none;
}

.footer-points li {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.875rem;
}

.footer-points i {
    color: var(--color-text-inverse);
    opacity: 0.6;
    font-size: 0.8125rem;
    /* Fixed box so the three labels start on a common left edge despite the
       glyphs having different widths. */
    width: 1rem;
    text-align: center;
    flex-shrink: 0;
}

/* Wordmark and points sit side by side once there's room for it. */
@media (min-width: 992px) {
    .footer-brand {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
        gap: 2rem 3rem;
    }

    .footer-brand .brand-lockup--inverse {
        grid-column: 1;
    }

    .footer-tagline {
        grid-column: 1;
        margin-top: 0.75rem;
    }

    .footer-points {
        grid-column: 2;
        grid-row: 1 / span 2;
        flex-direction: column;
        gap: 0.75rem;
        margin-top: 0;
    }
}

/* ============================================================
 * 29. Secondary link inside a category card
 * The supplies card's primary action is "Choose Your School", matching the
 * uniform card. This is the escape hatch for a shopper whose school isn't
 * listed, so it reads as a link rather than a second button.
 * ============================================================ */

.category-card .category-link {
    display: inline-block;
    margin-top: 0.75rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--color-brand-primary);
    text-decoration: none;
}

.category-card .category-link:hover,
.category-card .category-link:focus-visible {
    text-decoration: underline;
}

/* ============================================================
 * 30. Card image gallery (featured bundles)
 * On hover the card image tiles into quadrants showing every view at once.
 * Only rendered when a bundle has more than one image.
 * ============================================================ */

/* Both are used as the card's image box depending on the page — Home and
   /bundles use .bundle-image-container, SchoolBundles uses .bundle-image.
   The grid is absolutely positioned, so whichever wraps it must be relative. */
.bundle-image-container,
.bundle-image {
    position: relative;
}

.bundle-thumbs {
    position: absolute;
    inset: 0;
    display: grid;
    gap: 2px;
    /* Above .bundle-overlay (z-index 3) — at a lower z-index the overlay's 70%
       black wash covered the tiles entirely. */
    z-index: 4;
    background: var(--color-border);
    opacity: 0;
    /* Not just fading: without this the tiles still intercept the pointer while
       invisible, so the card's own hover/click targets would be shadowed. */
    pointer-events: none;
    transition: opacity var(--transition-fast);
}

.bundle-card:hover .bundle-thumbs,
.bundle-card:focus-within .bundle-thumbs {
    opacity: 1;
}

/* Layout by however many images the bundle actually has. Two side by side,
   three with the last spanning the full width, four as a true 2x2. */
.bundle-thumbs[data-count="2"] {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr;
}

.bundle-thumbs[data-count="3"] {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
}

.bundle-thumbs[data-count="3"] .bundle-thumb:nth-child(3) {
    grid-column: span 2;
}

.bundle-thumbs[data-count="4"] {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
}

.bundle-thumb {
    position: relative;
    overflow: hidden;
    background: var(--color-bg-surface-alt);
}

/* Two stylesheets absolutely position EVERY img inside the card container —
   .bundle-image-container img in lazy-loading.css and .bundle-image img in
   school-bundles.css. Both are (0,1,1), the same specificity as a plain
   .bundle-thumb img rule, so source order alone only wins for properties
   named here. Every property they set has to be reset explicitly or the tile
   image is torn out of its cell and stretched across the whole card. */
.bundle-thumbs .bundle-thumb img {
    position: static;
    top: auto;
    left: auto;
    transform: none;
    display: block;
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    object-fit: cover;
}

/* The "View Details" wash would sit behind opaque tiles anyway, so on a card
   that has a gallery it is suppressed rather than left as a dead layer. The
   card as a whole is still the link. */
.bundle-image-container:has(.bundle-thumbs) .bundle-card:hover .bundle-overlay,
.bundle-card:hover .bundle-image-container:has(.bundle-thumbs) .bundle-overlay {
    opacity: 0;
}

/* Touch has no hover, so the grid would never appear. Left off entirely there —
   the detail page carries the full gallery. */
@media (hover: none) {
    .bundle-thumbs {
        display: none;
    }
}

/* ============================================================
 * 31. Hero photo cards
 * The floating hero tiles now carry photographs instead of emoji icons.
 * Positioning and the float animation still come from site.css — only the
 * inner treatment changes here.
 * ============================================================ */

.floating-card--photo {
    /* The photo runs to the card edge; only the caption is inset. */
    padding: 0;
    overflow: hidden;
    margin: 0;
}

.floating-card--photo img {
    display: block;
    width: 100%;
    height: 150px;
    object-fit: cover;
    /* Rounded to the card's inner radius, minus the 1px border. */
    border-radius: 19px 19px 0 0;
}

.floating-card--photo .card-title {
    padding: 0.75rem 1rem;
    margin: 0;
    font-size: 0.9375rem;
    font-weight: var(--font-weight-bold);
    color: var(--color-brand-primary);
    text-align: center;
}

/* ============================================================
 * 32. In-page anchor offset
 * .header is position:fixed, so an anchor jump puts the target's top edge
 * underneath it — the section heading ends up hidden and the page looks like
 * it scrolled too far. scroll-margin-top reserves the header's height when
 * scrolling to a target; it has no effect on normal layout.
 * The escalation matches .page-header .header-content: the nav wraps to extra
 * rows at these widths, so the header gets taller.
 * ============================================================ */

html {
    scroll-behavior: smooth;
}

section[id],
#main-content {
    scroll-margin-top: 6rem;
}

@media (max-width: 1200px) {
    section[id],
    #main-content {
        scroll-margin-top: 9.5rem;
    }
}

@media (max-width: 900px) {
    section[id],
    #main-content {
        scroll-margin-top: 13rem;
    }
}

/* Respect a reduced-motion preference: a long smooth scroll is exactly the kind
   of movement that setting exists to suppress. */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

/* ============================================================
 * 33. Mobile app shell
 *
 * Below --mobile-bp the site switches from a wrapping desktop nav to an
 * app-style shell: a single-row header with a hamburger, a slide-in drawer for
 * the links, and a fixed bottom tab bar for the five primary destinations.
 *
 * The desktop nav previously wrapped onto three rows at these widths, which is
 * what forced the 132px/186px admin offsets in section 24 — those are unwound
 * at the bottom of this section now that the header is one row again.
 * ============================================================ */

/* ---- Hamburger (mobile only) ---- */

.nav-toggle {
    display: none;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    /* 44px: the minimum comfortable touch target. */
    width: 44px;
    height: 44px;
    padding: 0;
    background: none;
    border: none;
    cursor: pointer;
    flex-shrink: 0;
}

.nav-toggle__bar {
    display: block;
    width: 22px;
    height: 2px;
    margin: 0 auto;
    background: var(--color-brand-primary);
    transition: transform var(--transition-fast), opacity var(--transition-fast);
}

/* Morph to an X while the drawer is open. */
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) {
    transform: translateY(6px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2) {
    opacity: 0;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg);
}

/* ---- Drawer ---- */

.drawer-scrim {
    position: fixed;
    inset: 0;
    background: var(--color-bg-overlay);
    z-index: 1400;
    opacity: 0;
    transition: opacity var(--transition-fast);
}

.drawer-scrim[hidden] {
    display: none;
}

.drawer-scrim.is-open {
    opacity: 1;
}

.mobile-drawer {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: min(84vw, 320px);
    background: var(--color-bg-surface);
    z-index: 1500;
    display: flex;
    flex-direction: column;
    /* Off-canvas by default. translate rather than left/display so the slide is
       compositor-only and the panel stays in the accessibility tree logic
       controlled by aria-hidden. */
    transform: translateX(-100%);
    transition: transform 0.25s ease;
    overflow-y: auto;
    /* Clear the notch / status bar on devices that report insets. */
    padding-top: env(safe-area-inset-top, 0);
}

.mobile-drawer.is-open {
    transform: translateX(0);
}

.mobile-drawer__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--color-border);
}

.mobile-drawer__close {
    width: 44px;
    height: 44px;
    background: none;
    border: none;
    font-size: 1.75rem;
    line-height: 1;
    color: var(--color-text-muted);
    cursor: pointer;
}

.mobile-drawer__nav {
    display: flex;
    flex-direction: column;
    padding: 0.5rem 0;
    flex: 1;
}

.mobile-drawer__nav a {
    padding: 0.9rem 1.25rem;
    color: var(--color-text);
    font-weight: var(--font-weight-semibold);
    text-decoration: none;
    border-bottom: 1px solid var(--color-border);
}

.mobile-drawer__nav a:active {
    background: var(--color-bg-surface-alt);
}

.mobile-drawer__foot {
    padding: 1.25rem;
    border-top: 1px solid var(--color-border);
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.mobile-drawer__foot .btn-primary {
    width: 100%;
}

.mobile-drawer__register {
    text-align: center;
    font-size: 0.9rem;
    color: var(--color-brand-primary);
    font-weight: var(--font-weight-semibold);
}

/* ---- Bottom tab bar ---- */

.tab-bar {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1300;
    background: var(--color-bg-surface);
    border-top: 1px solid var(--color-border);
    /* Sits above the iOS home indicator rather than under it. */
    padding-bottom: env(safe-area-inset-bottom, 0);
}

.tab-bar__item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    /* 56px total keeps every tab a comfortable thumb target. */
    min-height: 56px;
    padding: 0.4rem 0.25rem;
    color: var(--color-text-muted);
    font-size: 0.6875rem;
    font-weight: var(--font-weight-semibold);
    text-decoration: none;
    /* Suppress the grey tap flash so it reads as a native control. */
    -webkit-tap-highlight-color: transparent;
}

.tab-bar__item i {
    font-size: 1.15rem;
}

.tab-bar__item.is-active {
    color: var(--color-brand-primary);
}

.tab-bar__icon-wrap {
    position: relative;
    display: inline-flex;
}

.tab-bar__badge {
    position: absolute;
    top: -6px;
    right: -10px;
    min-width: 1rem;
    height: 1rem;
    padding: 0 0.25rem;
    border-radius: var(--radius-pill);
    background: var(--color-brand-sale);
    color: var(--color-text-inverse);
    font-size: 0.625rem;
    line-height: 1rem;
    text-align: center;
}

.tab-bar__badge[hidden] {
    display: none;
}

/* ---- Breakpoint switch ---- */

@media (max-width: 991px) {
    .nav-toggle {
        display: flex;
    }

    .tab-bar {
        display: flex;
    }

    /* The desktop link row and the account controls move into the drawer. */
    .header .nav-menu,
    .header .user-greeting,
    .header .nav-buttons form[action="/Account/Logout"],
    .header .btn-login {
        display: none;
    }

    /* Two rows:
     *   row 1  [hamburger] [wordmark]
     *   row 2  [search .................] [cart]
     *
     * This previously set flex-wrap: nowrap and gave .nav-search
     * flex-basis: 100% expecting it to drop to its own line. Two things were
     * wrong: nowrap means nothing can wrap, and .nav-search is a child of
     * .nav-buttons — not of .nav-container — so the 100% was measured against
     * the wrong box. The search stayed on row 1 and squeezed the wordmark and
     * cart into each other. */
    .nav-container {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.5rem;
        padding: 0.5rem 0.75rem;
    }

    .nav-toggle {
        flex: 0 0 auto;
    }

    .logo {
        /* Shrinkable, and min-width:0 lets it actually shrink — a flex item
           defaults to min-width:auto, which refuses to go below its content and
           is what pushes a neighbour into an overlap. */
        flex: 1 1 auto;
        min-width: 0;
        overflow: hidden;
    }

    /* Full-width second row carrying the search and the cart. */
    .nav-buttons {
        flex: 1 1 100%;
        order: 3;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        margin: 0.15rem 0 0;
    }

    .nav-search {
        flex: 1 1 auto;
        min-width: 0;
        margin: 0;
    }

    /* Never squeezed — the cart is a fixed-size target beside a fluid field. */
    .cart-menu {
        flex: 0 0 auto;
    }

    /* Fixed height so the header's total height is predictable — that number is
       --mobile-header-h, which every page offset is derived from. 16px also
       stops iOS zooming the page when the field takes focus. */
    .nav-search__input {
        min-height: 40px;
        font-size: 16px;
    }

    .nav-search__button {
        min-width: 40px;
        min-height: 40px;
    }

    .cart-icon-link {
        min-width: 40px;
        min-height: 40px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* The hover preview is a desktop affordance; on touch the icon is a link to
       the cart page and the panel would just cover the page on first tap. */
    .cart-preview {
        display: none !important;
    }

    /* Content must clear the fixed tab bar or the last element is unreachable. */
    body {
        padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
    }

    /* Section 24 raised these because the desktop nav wrapped to 2–3 rows.
       The mobile header is a single row again, so the extra offset would leave
       a large empty gap. */
    .admin-dashboard-container {
        /* One source of truth — see section 43. */
        margin-top: var(--mobile-header-h);
    }

    /* Nothing should scroll sideways on a phone. */
    html, body {
        overflow-x: hidden;
    }
}

/* Below this the search row also stacks, so anchors need more clearance. */
@media (max-width: 991px) {
    section[id],
    #main-content {
        scroll-margin-top: 8.5rem;
    }
}

/* Reduced motion: the drawer appears instead of sliding. */
@media (prefers-reduced-motion: reduce) {
    .mobile-drawer,
    .drawer-scrim {
        transition: none;
    }
}

/* ============================================================
 * 34. Admin tables on mobile
 *
 * .admin-table-container had no rule at all, so wide tables simply overflowed.
 * With the app shell's `overflow-x: hidden` on body that became worse than a
 * page-level scrollbar: the extra columns were clipped and unreachable.
 *
 * The table keeps its columns and scrolls inside its own container — collapsing
 * to stacked cards would need a data-label on every cell, and most of these rows
 * are built in JavaScript across five different pages. The first column is
 * pinned so the row is still identifiable while scrolling sideways.
 * ============================================================ */

.admin-table-container {
    width: 100%;
    overflow-x: auto;
    /* Momentum scrolling on iOS. */
    -webkit-overflow-scrolling: touch;
    /* The radius moves here from .admin-table: a scroll container has to do the
       clipping, or the corners are cut off mid-scroll. */
    border-radius: var(--radius-lg);
    border: 1px solid var(--color-border);
    background: var(--color-bg-surface);
}

.admin-table-container .admin-table {
    border-radius: 0;
    box-shadow: none;
    /* Without a floor the browser crushes columns to fit and the text wraps to
       one character per line. */
    min-width: 720px;
    margin: 0;
}

@media (max-width: 991px) {
    /* Tighter cells so more of the row is visible per swipe. */
    .admin-table-container .admin-table th,
    .admin-table-container .admin-table td {
        padding: 0.65rem 0.75rem;
        font-size: 0.85rem;
        white-space: nowrap;
    }

    /* The name column is what identifies the row, so it stays put while the
       rest scrolls under it. */
    .admin-table-container .admin-table th:first-child,
    .admin-table-container .admin-table td:first-child {
        position: sticky;
        left: 0;
        z-index: 1;
        background: var(--color-bg-surface);
        /* Edge shadow so the pinned column reads as layered above the rest. */
        box-shadow: 1px 0 0 var(--color-border);
    }

    .admin-table-container .admin-table thead th:first-child {
        z-index: 2;
    }

    /* The one thing that must never be nowrap — bundle/product names are long
       and this column is fixed-width. */
    .admin-table-container .admin-table td:first-child {
        white-space: normal;
        min-width: 190px;
        max-width: 240px;
    }

    /* Hint that there is more to the right. */
    .admin-table-container {
        scrollbar-width: thin;
    }

    /* ---- Filter bar ---- */
    /* Search + 3 selects + 2 buttons on one row is unusable at phone width. */
    .admin-filters {
        flex-direction: column;
        align-items: stretch;
        gap: 0.75rem;
    }

    .admin-filters .search-box {
        width: 100%;
    }

    .admin-filters .filter-group {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 0.5rem;
        width: 100%;
    }

    .admin-filters .filter-group select,
    .admin-filters .filter-group .filter-button {
        width: 100%;
        /* 16px prevents iOS Safari zooming the page on focus. */
        font-size: 16px;
        min-height: 44px;
    }

    /* ---- Page header actions ---- */
    /* Four buttons across a phone header wrap into an unreadable stack. */
    .admin-page-header {
        flex-direction: column;
        align-items: stretch;
        gap: 0.75rem;
    }

    .admin-page-actions {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 0.5rem;
    }

    .admin-page-actions .admin-action-button {
        justify-content: center;
        min-height: 44px;
    }

    /* ---- Pagination ---- */
    .admin-pagination {
        flex-direction: column;
        gap: 0.75rem;
        align-items: stretch;
        text-align: center;
    }

    .pagination-controls {
        display: flex;
        justify-content: space-between;
        gap: 0.5rem;
    }

    .pagination-button {
        min-height: 44px;
        flex: 1;
    }
}

/* Any input below 16px triggers an iOS zoom on focus, which then leaves the
   page scrolled sideways. Applies to every admin form, not just tables. */
@media (max-width: 991px) {
    .admin-form input,
    .admin-form select,
    .admin-form textarea,
    .form-input,
    .form-select,
    .form-textarea {
        font-size: 16px;
    }
}

/* ============================================================
 * 35. Customer dashboard on mobile
 *
 * customer-dashboard.css carried a mobile block for .dashboard-container /
 * .dashboard-grid / .table-row / .table-cell — classes the view does not use.
 * The view's own block handles .actions-grid and .stats-grid, but only from
 * 768px, while the app shell switches at 991px. Between those two widths the
 * header was already mobile while the page below it was still desktop.
 * ============================================================ */

@media (max-width: 991px) {
    /* Match the app shell's breakpoint so the whole page changes together. */
    .dashboard-page .actions-grid {
        grid-template-columns: 1fr;
        gap: 0.75rem;
    }

    /* Stats stay 2-up: they are small, and one number per screen-height wastes
       the fold on a phone. */
    .dashboard-page .stats-grid {
        grid-template-columns: 1fr 1fr;
        gap: 0.75rem;
    }

    .dashboard-page .stat-card {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.5rem;
        padding: 1rem;
    }

    .dashboard-page .stat-number {
        font-size: 1.5rem;
    }

    .dashboard-page .stat-label {
        font-size: 0.75rem;
        line-height: 1.3;
    }

    /* Header: stack, and let the buttons fill the width rather than sitting in
       a cramped row beside the greeting. */
    .dashboard-page .dashboard-header {
        padding: 1.25rem;
        gap: 1rem;
    }

    .dashboard-page .header-actions {
        display: grid;
        grid-template-columns: 1fr;
        gap: 0.5rem;
        width: 100%;
    }

    .dashboard-page .header-actions .btn-primary,
    .dashboard-page .header-actions .btn-secondary {
        width: 100%;
        justify-content: center;
        min-height: 44px;
    }

    /* Order rows are a flex row with space-between; at phone width the number,
       date, status and amount collide. Stack them into a readable block. */
    .dashboard-page .recent-order-item {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.5rem;
        padding: 1rem;
    }

    .dashboard-page .order-info {
        width: 100%;
    }

    /* Keep amount and status on one line — they read as a pair. */
    .dashboard-page .order-amount,
    .dashboard-page .order-status {
        display: inline-block;
    }

    .dashboard-page .order-link {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }

    .dashboard-page .action-card {
        padding: 1rem;
        min-height: 44px;
    }

    .dashboard-page .section-title {
        font-size: 1.1rem;
    }

    /* Cards run edge to edge with a little breathing room, which is what makes
       a dashboard read as an app screen rather than a shrunken web page. */
    .dashboard-page .quick-actions,
    .dashboard-page .dashboard-stats,
    .dashboard-page .recent-activity {
        margin-bottom: 1.25rem;
    }
}

/* Very narrow phones: 2-up stats start to truncate their labels. */
@media (max-width: 380px) {
    .dashboard-page .stats-grid {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
 * 36. Admin area on mobile
 *
 * _AdminLayout hardcodes `margin-top: 80px` on .admin-layout — the height of
 * the desktop header. The mobile header is a logo row plus a full-width search
 * row, which is taller, so every admin page slid underneath it. This affects
 * the whole admin area, not just the dashboard.
 * ============================================================ */

@media (max-width: 991px) {
    .admin-layout {
        margin-top: var(--mobile-header-h);
        min-height: auto;
    }

    .admin-main {
        padding: 1rem 0.85rem;
        /* body already reserves room for the tab bar; a viewport-height floor
           here would add a second gap below it. */
        min-height: auto;
    }

    /* ---- Dashboard header ---- */

    .category-dashboard {
        padding: 0;
    }

    .category-dashboard-header {
        flex-direction: column;
        align-items: stretch;
        gap: 1rem;
    }

    .category-dashboard-title {
        font-size: 1.5rem;
    }

    .category-dashboard-subtitle {
        font-size: 0.95rem;
    }

    .category-dashboard-actions .category-action-button {
        width: 100%;
        justify-content: center;
        min-height: 44px;
    }

    /* ---- Stat tiles ----
       2-up rather than the auto-fit single column: these are short numbers, and
       one per screen-height would push the tool groups below two folds. */
    .category-stats-grid {
        grid-template-columns: 1fr 1fr;
        gap: 0.75rem;
        margin-bottom: 1.5rem;
    }

    .category-stat-card {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.5rem;
        padding: 0.9rem;
    }

    .category-stat-icon {
        width: 40px;
        height: 40px;
        font-size: 1rem;
        margin-right: 0;
    }

    .category-stat-number {
        font-size: 1.5rem;
    }

    .category-stat-label {
        font-size: 0.7rem;
        letter-spacing: 0.03em;
        line-height: 1.3;
    }

    /* The hover-only arrow is meaningless on touch and costs horizontal room. */
    .category-stat-go {
        display: none;
    }

    /* ---- Tool cards ---- */

    .tool-grid {
        grid-template-columns: 1fr;
        gap: 0.75rem;
    }

    .tool-card {
        padding: 0.9rem;
        min-height: 56px;
    }

    .tool-icon {
        width: 40px;
        height: 40px;
        font-size: 1rem;
        margin-right: 0.75rem;
    }

    .tool-group {
        margin-bottom: 1.75rem;
    }

    /* ---- Filter box ---- */

    .tool-filter {
        max-width: none;
    }

    .tool-filter input {
        /* Below 16px iOS Safari zooms the page on focus and never zooms back. */
        font-size: 16px;
        min-height: 44px;
    }

    /* ---- Stats error banner ---- */

    .dashboard-alert {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.6rem;
    }

    .dashboard-alert-retry {
        width: 100%;
        min-height: 44px;
    }
}

/* Very narrow phones: two stat tiles start clipping their labels. */
@media (max-width: 380px) {
    .category-stats-grid {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
 * 37. Cart on mobile
 *
 * .cart-section pads 80px at its own 768px breakpoint — the desktop header
 * height, so the page title sat under the taller mobile header. And the order
 * summary is the last thing in source order, meaning on a phone the total and
 * the checkout button are below every line item: on a full cart you scroll past
 * everything to find out what you owe.
 * ============================================================ */

@media (max-width: 991px) {
    .cart-section {
        padding: var(--mobile-header-h) 0 1rem;
        min-height: auto;
    }

    .cart-header {
        margin-bottom: 1.25rem;
        padding: 0 0.5rem 0.5rem;
        text-align: left;
    }

    .cart-title {
        font-size: 1.5rem;
    }

    .cart-subtitle {
        font-size: 0.9rem;
    }

    .cart-container {
        padding: 0 0.85rem;
    }

    /* Order summary ahead of the item list: the total and the checkout button
       are what the page is for. Flex ordering only — the DOM stays as-is so
       screen readers and tab order still follow the document. */
    .cart-section .row:has(> .col-lg-4) {
        display: flex;
        flex-direction: column;
    }

    .cart-section .col-lg-4 {
        order: -1;
        margin-top: 0;
        margin-bottom: 1rem;
    }

    .summary-card {
        /* Overrides the inline padding: 2.5rem !important on the element. */
        padding: 1.25rem !important;
    }

    .summary-card h5 {
        font-size: 1.05rem;
    }

    /* ---- Items ---- */

    .cart-item {
        padding: 1rem !important;
    }

    .cart-card-header {
        padding: 1rem !important;
    }

    .cart-item img {
        max-width: 72px;
        height: auto;
    }

    /* ---- Controls ----
       Quantity steppers and the small action buttons are the most-tapped
       controls on the page and were sized for a mouse. */
    .cart-item .btn-sm,
    .cart-item .btn-outline-secondary,
    .cart-item .edit-btn,
    .cart-item .view-details-btn {
        min-height: 40px;
        min-width: 40px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    .cart-item input[type="number"] {
        /* 16px stops iOS zooming the page when the field takes focus. */
        font-size: 16px;
        min-height: 40px;
        text-align: center;
    }

    #checkout-btn,
    .summary-card .btn {
        min-height: 48px;
        font-size: 1rem;
    }
}

/* :has() is well supported in the browsers this targets, but if it is ever
   unavailable the summary simply stays where the document puts it — the page
   still works, it just is not reordered. */

/* ============================================================
 * 38. Checkout on mobile
 *
 * .checkout-section pads 80px at its own 768px breakpoint — the desktop header
 * height — so the page title sat under the taller mobile header. Same
 * assumption as the cart and both admin layouts.
 *
 * Unlike the cart, the summary is NOT hoisted above the form here: on checkout
 * the form is the task, and pushing it below a price breakdown would bury the
 * thing the page exists to collect. The summary keeps its place; the Place
 * Order button is what gets made reachable.
 * ============================================================ */

@media (max-width: 991px) {
    .checkout-section {
        padding: var(--mobile-header-h) 0 1rem;
        min-height: auto;
    }

    .checkout-container {
        padding: 0 0.85rem;
    }

    .checkout-header {
        margin-bottom: 1.25rem;
        text-align: left;
    }

    .checkout-title {
        font-size: 1.5rem;
    }

    .checkout-subtitle {
        font-size: 0.9rem;
    }

    .checkout-card,
    .checkout-card-header,
    .form-section {
        padding: 1rem !important;
    }

    .summary-card {
        padding: 1.25rem !important;
        margin-top: 1rem;
    }

    /* ---- Form fields ----
       This is the highest-stakes form on the site: an iOS zoom-on-focus here
       leaves the customer scrolled sideways mid-payment. 16px is the threshold
       below which Safari zooms. */
    .checkout-section .form-control,
    .checkout-section select,
    .checkout-section textarea {
        font-size: 16px;
        min-height: 44px;
    }

    .checkout-form-group {
        margin-bottom: 0.9rem;
    }

    /* Bootstrap's col-md-* already stack below 768px, but between 768 and 991
       the narrow city/state/zip row stays 3-up and the fields become unusable. */
    .checkout-section .col-md-3,
    .checkout-section .col-md-6 {
        flex: 0 0 100%;
        max-width: 100%;
    }

    /* ---- Place Order ----
       48px and full width: it is the one control on the page that must never be
       a near-miss tap. */
    #placeOrderBtn {
        min-height: 48px;
        font-size: 1rem;
    }
}

/* ============================================================
 * 39. Storefront listing pages on mobile (/supplies, /bundles, school pages)
 *
 * Section 27 escalated .page-header .header-content to 9.5rem and then 13rem,
 * because the desktop nav wrapped to two and then three rows at those widths.
 * The app shell replaced that with a single-row header plus a search row, so
 * 13rem now leaves roughly 90px of empty band above the page title.
 * ============================================================ */

@media (max-width: 991px) {
    .page-header .header-content {
        /* Header height plus a little breathing room — see section 43. */
        padding-top: calc(var(--mobile-header-h) + 4px);
        padding-bottom: 1.5rem;
    }

    .page-header {
        padding: 0;
    }

    /* school-bundles.css switches this grid at 768px; between there and the
       shell breakpoint the 380px minimum still forces two cramped columns. */
    .bundles-grid {
        grid-template-columns: 1fr;
        gap: 1rem;
        margin-bottom: 2rem;
    }

    /* ---- Filter bar ---- */

    .filter-bar {
        flex-direction: column;
        align-items: stretch;
        gap: 0.75rem;
        padding: 0.85rem;
        margin-left: 0.85rem;
        margin-right: 0.85rem;
    }

    .filter-group {
        width: 100%;
    }

    .filter-select {
        width: 100%;
        /* Below 16px iOS zooms the page when the select takes focus. */
        font-size: 16px;
        min-height: 44px;
    }

    .results-count {
        text-align: left;
    }

    /* ---- Cards ---- */

    .bundle-content {
        padding: 0.9rem;
    }

    .bundle-name {
        font-size: 1.05rem;
    }

    .bundle-description {
        font-size: 0.875rem;
    }

    /* The features list is the first thing worth dropping when space is tight —
       the name, price and button carry the decision. */
    .bundle-features {
        gap: 0.35rem;
    }

    .bundle-footer {
        flex-direction: column;
        align-items: stretch;
        gap: 0.6rem;
    }

    .btn-view {
        width: 100%;
        justify-content: center;
        min-height: 44px;
    }

    /* ---- Empty / error states ---- */

    .no-bundles-actions {
        flex-direction: column;
        gap: 0.6rem;
    }

    .no-bundles-actions .btn-primary,
    .no-bundles-actions .btn-secondary {
        width: 100%;
        justify-content: center;
        min-height: 44px;
    }

    .section-heading {
        font-size: 1.25rem;
    }

    .section-subheading {
        font-size: 0.9rem;
    }
}

/* ============================================================
 * 40. /bundles — the parts it does not share with /supplies
 *
 * Both listing pages use the same page-header, grid, filter bar and card
 * shell, so section 39 already covers most of this page. These are the
 * classes only /bundles has: its own CTA class, the pricing block, and the
 * "need help" panel at the foot of the page.
 * ============================================================ */

@media (max-width: 991px) {
    /* /supplies calls this .btn-view; /bundles calls it .view-bundle-btn.
       Same job, so it gets the same treatment. */
    .view-bundle-btn {
        width: 100%;
        justify-content: center;
        min-height: 44px;
        padding: 0.75rem 1rem;
    }

    /* Price and button stack rather than competing for a narrow row. */
    .bundle-footer {
        align-items: stretch;
    }

    .bundle-pricing {
        align-items: flex-start;
    }

    .current-price {
        font-size: 1.25rem;
    }

    .price-note,
    .price-request {
        font-size: 0.8rem;
    }

    /* 3rem of padding on a 1.5rem-radius panel eats most of a phone's width. */
    .help-section {
        padding: 1.5rem 1rem;
        border-radius: var(--radius-xl);
        margin-bottom: 2rem;
    }

    .help-content h2,
    .help-content h3 {
        font-size: 1.2rem;
    }

    .help-content p {
        font-size: 0.9rem;
    }

    /* flex-wrap leaves these half-width and ragged; stack them full-width. */
    .help-actions {
        flex-direction: column;
        gap: 0.6rem;
    }

    .help-actions .btn-primary,
    .help-actions .btn-secondary {
        width: 100%;
        justify-content: center;
        min-height: 44px;
    }

    /* The school logo beside the bundle name is decorative at this size and
       squeezes the name into two lines. */
    .bundle-title-section .school-logo {
        width: 24px !important;
        height: 24px !important;
    }
}

/* ============================================================
 * 41. /school/{code}/bundles — the parts it does not share
 *
 * This page reuses the same header, grid, filter bar and card shell as
 * /bundles and /supplies, so sections 39 and 40 already cover it. The one
 * structural difference is the location badge, which sits INSIDE the <h1>
 * beside the school name — once the title shrinks on mobile it wraps into the
 * name and reads as part of it.
 * ============================================================ */

@media (max-width: 991px) {
    .page-title .school-location-badge {
        /* Own line, at label scale rather than heading scale. */
        display: block;
        width: fit-content;
        margin-top: 0.5rem;
        font-size: 0.8rem;
        padding: 0.3rem 0.7rem;
        font-weight: var(--font-weight-semibold);
        /* backdrop-filter over the navy band costs a compositor layer for an
           effect that is invisible at this size. */
        backdrop-filter: none;
    }

    .page-title {
        /* The badge is a block child now, so the heading needs to allow it to
           sit below rather than trying to keep everything inline. */
        display: block;
    }

    /* This page wraps card images in .bundle-image rather than
       .bundle-image-container — keep the two in step. */
    .bundle-image {
        height: 200px;
    }
}

/* ============================================================
 * 42. /schools (school finder) on mobile
 *
 * The page carries no media queries at all. Its hero pads 7rem to clear the
 * fixed header — sized for the ~70px desktop nav, which is now short of the
 * mobile header, so the title crowded it.
 * ============================================================ */

@media (max-width: 991px) {
    .sf-hero {
        /* See section 43 for the shared header height. */
        padding: calc(var(--mobile-header-h) + 4px) 0.85rem 2rem;
        text-align: left;
    }

    .sf-hero h1 {
        font-size: 1.6rem;
    }

    .sf-hero p {
        font-size: 0.95rem;
    }

    .sf-search {
        margin-top: 1rem;
        /* Already 1rem (16px), which is what keeps iOS from zooming on focus —
           restated so a later change does not quietly drop below the threshold. */
    }

    .sf-search input {
        font-size: 16px;
        min-height: 44px;
    }

    .sf-count {
        text-align: left;
    }

    .sf-grid {
        /* 260px minimum already yields one column on a phone; this drops the
           gutter so cards are not squeezed into the middle third. */
        grid-template-columns: 1fr;
        gap: 0.6rem;
        margin: 1.25rem auto;
        padding: 0 0.85rem;
    }

    .sf-card {
        padding: 0.85rem;
        gap: 0.75rem;
        /* Hover lift is meaningless on touch and leaves the card stuck in the
           raised state after a tap on some browsers. */
        transition: none;
    }

    .sf-card:hover {
        transform: none;
        box-shadow: none;
    }

    .sf-logo {
        width: 44px;
        height: 44px;
        font-size: 0.95rem;
    }

    .sf-name {
        font-size: 0.95rem;
    }

    .sf-meta {
        font-size: 0.8rem;
    }

    .sf-empty {
        margin: 1.5rem auto;
        padding: 0 0.85rem;
        text-align: left;
    }
}

/* ============================================================
 * 43. One mobile header height, everywhere
 *
 * Nineteen views hardcode their own top offset to clear the fixed header —
 * `padding: 100px 0` on desktop, `padding: 80px 0` in their own 768px block,
 * or the 6/9.5/13rem escalation. Every one of those numbers was measured
 * against the OLD wrapping desktop nav, so under the app shell they are either
 * short (title tucked under the header) or leave a large empty band.
 *
 * Rather than edit nineteen files and let them drift again, every page-level
 * section is corrected here against a single variable. If the header height
 * ever changes, --mobile-header-h is the one thing to edit.
 * ============================================================ */

:root {
    /* Logo/hamburger row (44px control + 16px padding) + search row. */
    --mobile-header-h: 116px;
}

@media (max-width: 991px) {
    .cart-section,
    .checkout-section,
    .confirmation-section,
    .return-policy-section,
    .terms-section,
    .pta-section,
    .ot-section,
    .ac-section,
    .ap-section,
    .av-section,
    .vp-section,
    .fp-section {
        padding-top: var(--mobile-header-h) !important;
        padding-bottom: 1rem;
        min-height: auto;
    }

    /* Register centres its card in a full-height flex box, so it needs the
       offset as padding rather than a replacement for its own layout. */
    .registration-container {
        padding-top: var(--mobile-header-h);
        padding-left: 0.85rem;
        padding-right: 0.85rem;
        min-height: auto;
    }

    /* ---- Shared treatment for these long-form / form pages ---------------
       They are otherwise unstyled for mobile: single-column, full-width
       controls, and 16px inputs so iOS does not zoom on focus. */

    .ac-section .container,
    .ap-section .container,
    .av-section .container,
    .ot-section .container,
    .confirmation-section .container,
    .return-policy-section .container,
    .terms-section .container,
    .pta-section .container {
        padding-left: 0.85rem;
        padding-right: 0.85rem;
    }

    .ac-section input, .ac-section select, .ac-section textarea,
    .ap-section input, .ap-section select, .ap-section textarea,
    .av-section input, .av-section select, .av-section textarea,
    .ot-section input, .ot-section select, .ot-section textarea {
        font-size: 16px;
        min-height: 44px;
    }

    .ac-section .btn, .ap-section .btn, .av-section .btn, .ot-section .btn,
    .confirmation-section .btn, .pta-section .btn {
        min-height: 44px;
    }

    /* Long-form copy pages: tighter measure and left alignment read better on
       a phone than centred blocks at desktop scale. */
    .return-policy-section h1,
    .terms-section h1,
    .pta-section h1 {
        font-size: 1.6rem;
    }

    .return-policy-section h2,
    .terms-section h2,
    .pta-section h2 {
        font-size: 1.2rem;
    }

    .return-policy-section p,
    .terms-section p,
    .pta-section p {
        font-size: 0.95rem;
        line-height: 1.6;
    }
}

/* ============================================================
 * 44. Login modal on mobile
 *
 * .modal-content is vertically centred (top:50% + translate) with
 * max-height:80vh. When the on-screen keyboard opens, the visual viewport
 * shrinks by roughly half — a centred box gets pushed up and clipped, and the
 * field being typed into can end up behind the keyboard.
 *
 * A bottom sheet avoids that entirely: anchored to the bottom edge, it grows
 * upward and the keyboard simply covers the area below it.
 * ============================================================ */

@media (max-width: 991px) {
    #loginModal .modal-content,
    #schoolModal .modal-content {
        top: auto;
        bottom: 0;
        left: 0;
        transform: none;
        width: 100%;
        max-width: none;
        margin: 0;
        border-radius: 16px 16px 0 0;
        max-height: 88vh;
        padding: 1.25rem 1.1rem;
        /* Clear the iOS home indicator. */
        padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
        animation: sheetUp 0.25s ease;
    }

    @keyframes sheetUp {
        from { transform: translateY(100%); }
        to   { transform: translateY(0); }
    }

    /* A grab handle is what makes it read as a sheet rather than a box that
       happens to be at the bottom. */
    #loginModal .modal-content::before,
    #schoolModal .modal-content::before {
        content: '';
        display: block;
        width: 36px;
        height: 4px;
        margin: 0 auto 0.9rem;
        border-radius: var(--radius-pill);
        background: var(--color-border-strong);
    }

    /* blur() over a full-screen overlay is one of the more expensive things a
       phone GPU can be asked to do, for an effect nobody notices behind an
       opaque sheet. */
    .modal {
        backdrop-filter: none;
        background: var(--color-bg-overlay);
    }

    #loginModal .close-btn,
    #schoolModal .close-btn {
        width: 44px;
        height: 44px;
        top: 0.5rem;
        right: 0.5rem;
    }

    #loginModal .modal-title {
        font-size: 1.25rem;
    }

    #loginModal .modal-subtitle {
        font-size: 0.9rem;
    }

    #loginModal .form-input {
        /* 16px is the threshold below which iOS zooms the page on focus. */
        font-size: 16px;
        min-height: 46px;
    }

    #loginModal .login-btn {
        min-height: 48px;
        font-size: 1rem;
    }

    #loginModal .form-options {
        gap: 0.75rem;
        flex-wrap: wrap;
    }

    #loginModal .remember-me,
    #loginModal .forgot-password {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }

    #loginModal .signup-link {
        padding-top: 0.5rem;
    }

    /* School picker list needs room to scroll inside the sheet. */
    #schoolModal .schools-list {
        max-height: 50vh;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
    }

    #schoolModal .search-input {
        font-size: 16px;
        min-height: 46px;
    }
}

/* ============================================================
 * 45. School picker on mobile
 *
 * Section 44 already made this a bottom sheet. These are the list internals:
 * the picker is the primary way a parent starts shopping, so the rows need to
 * be thumb-sized and the list has to scroll independently of the sheet.
 * ============================================================ */

@media (max-width: 991px) {
    #schoolModal .search-container {
        margin-bottom: 0.75rem;
    }

    #schoolModal .school-option {
        /* 15px radius and 1rem padding on a full-width row reads as a chunky
           box; tighter is calmer and fits more schools per screen. */
        border-radius: var(--radius-lg);
        padding: 0.75rem;
        gap: 0.75rem;
        min-height: 56px;
        /* Hover states stick after a tap on touch devices. */
        transition: none;
    }

    #schoolModal .school-option:hover {
        transform: none;
    }

    #schoolModal .school-avatar {
        width: 40px;
        height: 40px;
        border-radius: var(--radius-md);
        flex-shrink: 0;
    }

    #schoolModal .school-details h3 {
        font-size: 0.95rem;
        margin: 0 0 0.1rem;
    }

    #schoolModal .school-details p,
    #schoolModal .school-location {
        font-size: 0.8rem;
    }

    /* Pagination controls sit below a scrolling list, so they must not be the
       thing that pushes the sheet past its max height. */
    #schoolModal .schools-pagination {
        display: flex;
        justify-content: center;
        gap: 0.5rem;
        padding-top: 0.75rem;
        flex-wrap: wrap;
    }

    #schoolModal .schools-pagination button {
        min-height: 44px;
        min-width: 44px;
    }

    #schoolModal .schools-loading,
    #schoolModal .schools-error,
    #schoolModal .no-schools {
        padding: 1.5rem 0.5rem;
        text-align: center;
    }

    #schoolModal .schools-error .btn-primary,
    #schoolModal .no-schools .btn-primary {
        width: 100%;
        min-height: 44px;
    }
}

/* ============================================================
 * 46. Admin list rows and dialogs on mobile
 *
 * Section 34 made admin tables scroll and pinned the first column. These are
 * the row internals: the action buttons are 32px, which is well under a usable
 * touch target, and the two dialogs on the bundles list are not covered by the
 * bottom-sheet rules in section 44 (those are scoped to the storefront modals).
 *
 * .action-button and .bundle-info appear across the admin lists, so this
 * applies beyond the bundles page.
 * ============================================================ */

@media (max-width: 991px) {
    /* 32px is a mouse target. Four of them in a row still fits inside the
       horizontally scrolling table. */
    .action-button {
        width: 40px;
        height: 40px;
    }

    .action-buttons {
        gap: 0.35rem;
    }

    /* ---- First-column cell ----
       The pinned column is 190-240px wide; a 48px thumbnail plus a wrapping
       name and the code line is too much for it.
       The products list uses .product-info / .product-thumb / .product-details
       for the identical structure, so both are handled together. */
    .bundle-info,
    .product-info,
    .school-name-cell {
        gap: 8px;
    }

    .product-thumb {
        width: 40px;
        height: 40px;
    }

    .product-details strong {
        font-size: 0.9rem;
        line-height: 1.25;
        display: block;
    }

    /* Same call as .bundle-description: the row still carries SKU, category,
       gender and status, so the blurb is the line that can go. */
    .product-details small {
        display: none;
    }

    /* Three admin lists use the same 48px-thumbnail-plus-name cell under three
       different class names — bundles, products and schools. Kept in one rule
       so a change to one cannot silently miss the others. */
    .bundle-image-wrapper,
    .bundle-image-placeholder,
    .school-logo-wrapper,
    .school-logo-placeholder {
        width: 40px !important;
        height: 40px !important;
        min-width: 40px !important;
        min-height: 40px !important;
    }

    .school-name-cell span {
        font-size: 0.9rem;
        line-height: 1.25;
    }

    .bundle-name {
        font-size: 0.9rem;
        line-height: 1.25;
    }

    .bundle-description {
        /* The row already carries name, code, grade and type — the description
           is the one line that can go without losing anything identifying. */
        display: none;
    }

    .bundle-code {
        font-size: 0.7rem;
    }

    .line-badge {
        margin-left: 0.35rem;
        padding: 0.05rem 0.35rem;
        font-size: 0.62rem;
    }

    /* ---- Admin dialogs ----
       Section 44's sheet treatment is scoped to #loginModal / #schoolModal.
       These get the same shape so admin dialogs behave like the rest. */
    #imageViewerModal .modal-content,
    #bundleDetailsModal .modal-content {
        top: auto;
        bottom: 0;
        left: 0;
        transform: none;
        width: 100% !important;
        max-width: none !important;
        max-height: 90% !important;
        margin: 0;
        border-radius: 16px 16px 0 0;
        padding: 1rem;
        padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
        overflow-y: auto;
    }

    #imageViewerModal .modal-body img {
        max-height: 60vh;
        width: auto;
    }

    #bundleDetailsModal .modal-header,
    #imageViewerModal .modal-header {
        padding-right: 2.5rem;
    }

    #bundleDetailsModal .close,
    #imageViewerModal .close {
        width: 44px;
        height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
}

/* ============================================================
 * 47. Admin users list on mobile
 *
 * Section 34 sets `white-space: nowrap` on admin table cells so columns stay
 * legible while scrolling. That is right for short values, but this table has
 * two columns whose content is arbitrarily long — a full email address and a
 * wrapping list of role badges. Under nowrap they simply stretch the row, so
 * the horizontal scroll runs far wider than the six columns need.
 * ============================================================ */

@media (max-width: 991px) {
    /* Let the email column wrap and cap it, rather than letting one long
       address set the width of the whole table. */
    .admin-table-container .admin-table td .email-info {
        white-space: normal;
        max-width: 200px;
        word-break: break-word;
    }

    /* Role badges are a flex row that wraps — but with a nowrap ancestor and no
       width limit the container just grows instead. */
    .roles-container {
        white-space: normal;
        max-width: 170px;
    }

    /* The avatar is already 40px here, unlike the bundles/products thumbnails,
       so only the secondary line needs attention. */
    .user-details {
        min-width: 0;
    }

    .user-username {
        font-size: 0.75rem;
        /* Kept, unlike the product blurb: a username identifies the row in a
           way a marketing description does not. */
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: 150px;
    }

    .verified-badge,
    .unverified-badge {
        font-size: 0.65rem;
        padding: 0.1rem 0.35rem;
    }

    /* ---- Empty and error states ---- */

    .no-data,
    .error-state {
        padding: 1.5rem 0.75rem;
    }

    .retry-button {
        min-height: 44px;
        width: 100%;
        max-width: 260px;
    }
}

/* ============================================================
 * 48. Admin orders list on mobile
 *
 * The widest admin table (8 columns) plus summary tiles, two dialogs, and a
 * customer column carrying a full email address — the same unbounded-cell
 * problem as the users list (see section 47).
 *
 * Note: .items-table on this page is NOT styled here. It belongs to the
 * print view built with window.open + document.write, which is a separate
 * document that never loads this stylesheet.
 * ============================================================ */

@media (max-width: 991px) {
    /* 8 columns need more room than the 720px floor in section 34, or the
       browser squeezes them until the headers wrap. */
    .admin-table-container #ordersTable,
    .admin-table-container .admin-table {
        min-width: 860px;
    }

    /* Same bound as the users list: one long address must not set the width of
       the whole table. */
    .admin-table-container .admin-table td .customer-info {
        white-space: normal;
        max-width: 190px;
    }

    .customer-email {
        font-size: 0.75rem;
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: 180px;
    }

    /* ---- Summary tiles ---- */

    .orders-summary {
        grid-template-columns: 1fr 1fr;
        gap: 0.75rem;
        margin-bottom: 1.25rem;
    }

    .orders-summary .summary-card {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.5rem;
        padding: 0.9rem;
        border-radius: var(--radius-xl);
    }

    .summary-icon {
        width: 36px;
        height: 36px;
        font-size: 0.95rem;
    }

    .summary-number {
        font-size: 1.35rem;
    }

    .summary-label {
        font-size: 0.7rem;
        line-height: 1.3;
    }

    /* ---- Filters ---- */

    .active-filters {
        gap: 0.35rem;
    }

    .clear-filters-btn {
        min-height: 44px;
        width: 100%;
    }

    .status-select {
        font-size: 16px;
        min-height: 44px;
        width: 100%;
    }

    /* ---- Dialogs ----
       Same sheet shape as the storefront modals and the bundles dialogs. */
    .order-modal-content,
    .status-modal-content {
        top: auto;
        bottom: 0;
        left: 0;
        transform: none;
        width: 100% !important;
        max-width: none !important;
        max-height: 90vh !important;
        margin: 0;
        border-radius: 16px 16px 0 0;
        padding: 1rem;
        padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
        overflow-y: auto;
    }

    .order-sections {
        gap: 0.9rem;
    }

    /* Line items are a space-between row; on a phone the name, config and
       price collide. */
    .order-item {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.4rem;
        padding: 0.75rem;
    }

    .order-header,
    .bundle-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.4rem;
    }

    .info-table td,
    .info-table th {
        display: block;
        width: 100%;
        padding: 0.2rem 0;
    }

    .modal-footer {
        flex-direction: column;
        gap: 0.5rem;
    }

    .modal-footer .btn-primary,
    .modal-footer .btn-secondary {
        width: 100%;
        min-height: 44px;
    }
}

/* ============================================================
 * 49. Admin suppliers list on mobile
 *
 * This page already card-ifies its table below 768px: thead hidden, each <tr>
 * a stacked card, column labels injected as ::before. That is a better mobile
 * result than horizontal scrolling, and it predates this sweep.
 *
 * Wrapping the table in .admin-table-container (done earlier so no admin table
 * could overflow) put section 34's rules on top of it — min-width: 720px, a
 * sticky first column and white-space: nowrap. Against a display:block table
 * those produce 720px-wide cards that scroll sideways, which is worse than
 * either design on its own. Below the card breakpoint the container rules are
 * unwound so the card layout owns the page.
 * ============================================================ */

@media (max-width: 768px) {
    .admin-table-container:has(#suppliersTable) {
        overflow-x: visible;
        border: none;
        background: transparent;
        border-radius: 0;
    }

    .admin-table-container #suppliersTable {
        min-width: 0;
    }

    /* The card layout sets its own padding and alignment per cell. */
    .admin-table-container #suppliersTable th,
    .admin-table-container #suppliersTable td {
        white-space: normal;
        font-size: inherit;
        padding: 0.4rem 1rem;
    }

    /* Nothing to pin when every cell is a full-width block. */
    .admin-table-container #suppliersTable th:first-child,
    .admin-table-container #suppliersTable td:first-child {
        position: static;
        box-shadow: none;
        min-width: 0;
        max-width: none;
    }
}

/* Between the card breakpoint and the shell breakpoint the table is still a
   table, so it keeps the scrolling treatment — only the touch targets and the
   toolbar need attention. */
@media (max-width: 991px) {
    /* This page calls its row buttons .row-action, not .action-button, so the
       40px rule in section 46 does not reach them. */
    .row-action {
        min-width: 40px;
        min-height: 40px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    .suppliers-toolbar input,
    .suppliers-toolbar select {
        font-size: 16px;
        min-height: 44px;
    }

    .suppliers-header-actions .btn {
        min-height: 44px;
    }

    /* Bootstrap's modal-fullscreen-sm-down already handles the dialog below
       576px; this covers the gap up to the shell breakpoint. */
    #supplierModal .modal-body {
        max-height: 70vh;
        overflow-y: auto;
    }

    #supplierModal .modal-footer {
        flex-direction: column;
        gap: 0.5rem;
    }

    #supplierModal .modal-footer .btn {
        width: 100%;
        min-height: 44px;
    }
}

/* ============================================================
 * 50. App-wide mobile baseline
 *
 * 63 of the 96 non-partial views carry no media query at all. Rather than 63
 * bespoke blocks, this handles the structures they actually share. Everything
 * here is a floor, not a design: pages with their own mobile treatment
 * (suppliers' card table, the storefront listings) keep it, because those rules
 * are either more specific or scoped to a later section.
 * ============================================================ */

@media (max-width: 991px) {
    /* ---- Row action buttons -------------------------------------------
       Four naming conventions for the same control across the admin views —
       .action-button (18), .btn-action (7), .row-action (2), .btn-icon. They
       are all 32-36px, which is a mouse target. */
    .action-button,
    .btn-action,
    .row-action,
    .btn-icon {
        min-width: 40px;
        min-height: 40px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* ---- Bare tables ---------------------------------------------------
       These views build their own table class (.ac-table, .ap-table,
       .av-table, .vp-table, .report-table) with no scrolling container, so
       under body{overflow-x:hidden} their columns were clipped and
       unreachable. display:block turns the table itself into the scroller.
       .admin-table is excluded — section 34 already gives it a real container,
       and this would fight the suppliers card layout. */
    .ac-table,
    .ap-table,
    .av-table,
    .vp-table,
    .report-table {
        display: block;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        max-width: 100%;
        white-space: nowrap;
    }

    /* ---- Dialogs -------------------------------------------------------
       Every custom .modal-content variant becomes a bottom sheet, matching the
       storefront modals. Bootstrap's own .modal-dialog is excluded — it has
       its own responsive handling and modal-fullscreen-*-down. */
    .modal:not(.fade) > .modal-content {
        top: auto;
        bottom: 0;
        left: 0;
        transform: none;
        width: 100% !important;
        max-width: none !important;
        max-height: 90vh !important;
        margin: 0;
        border-radius: 16px 16px 0 0;
        padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
        overflow-y: auto;
    }

    /* ---- Forms ---------------------------------------------------------
       Any control under 16px makes iOS Safari zoom on focus and never zoom
       back, leaving the page scrolled sideways. This is the single highest-
       value rule in the file. */
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
    select,
    textarea {
        font-size: 16px;
    }

    /* ---- Page frames ---------------------------------------------------
       Sections that own their own top offset are handled in section 43; this
       is for the generic wrappers. */
    .container {
        padding-left: 0.85rem;
        padding-right: 0.85rem;
    }

    /* ---- Long values ---------------------------------------------------
       Emails, URLs and codes overflow narrow cells and push the page wide. */
    td, th, dd, .text-break-mobile {
        overflow-wrap: break-word;
    }
}

/* ============================================================
 * 51. Shared page frame offset
 *
 * .app-page (error, not-found, privacy and anything else using the shared
 * frame) pads 2rem at the top — nowhere near the fixed header, so its heading
 * sat under the nav on every viewport, not only mobile.
 * ============================================================ */

.app-page {
    padding-top: calc(5rem + var(--space-7));
}

@media (max-width: 991px) {
    .app-page {
        padding-top: calc(var(--mobile-header-h) + 1rem);
        padding-left: 0.85rem;
        padding-right: 0.85rem;
    }
}

/* ============================================================
 * 52. Small admin areas (coupons, packaging, vendors, returns, promos)
 *
 * These pages share a house style — .ac-*, .ap-*, .av-*, .vp-* — that predates
 * the admin shell, with its own buttons, cards, containers and pagers. Section
 * 43 gave them the header offset and section 50 made their tables scroll; this
 * covers the components themselves.
 *
 * .admin-mini-header replaces an inline `display:flex` that appeared in five
 * views. Inline styles beat every stylesheet, so the title/action row could not
 * be restacked for mobile until it became a class.
 * ============================================================ */

.admin-mini-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
    gap: 1rem;
}

@media (max-width: 991px) {
    .admin-mini-header {
        flex-direction: column;
        align-items: stretch;
        gap: 0.6rem;
    }

    .admin-mini-header h1 {
        font-size: 1.4rem !important;
    }

    /* ---- Buttons ----
       A fifth naming convention for the same control: .ac-btn / .ap-btn /
       .av-btn, all at .45rem padding and .8rem text. */
    .ac-btn,
    .ap-btn,
    .av-btn,
    .vp-btn {
        min-height: 44px;
        padding: 0.6rem 0.9rem;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    .admin-mini-header .ac-btn,
    .admin-mini-header .ap-btn,
    .admin-mini-header .av-btn,
    .admin-mini-header .vp-btn {
        width: 100%;
    }

    /* Row actions stay inline — they live inside the scrolling table, where
       stacking would make every row three times as tall. */
    .ac-actions,
    .ap-actions,
    .av-actions {
        gap: 0.35rem;
        flex-wrap: nowrap;
    }

    /* ---- Frames ---- */

    .ac-container,
    .ap-container,
    .av-container,
    .vp-container {
        padding: 0 0.85rem;
    }

    .ac-card,
    .ap-card,
    .av-card,
    .vp-card {
        padding: 1rem;
        margin-bottom: 0.85rem;
    }

    .ac-flash,
    .ap-flash,
    .av-flash,
    .vp-flash {
        font-size: 0.9rem;
    }

    /* ---- Pagers ----
       Right-aligned prev/next becomes a split pair so both are thumb-reachable. */
    /* Only .ac-pager exists — .ap-pager / .av-pager were never used. */
    .ac-pager {
        justify-content: space-between;
        gap: 0.5rem;
    }

    .ac-pager .ac-btn {
        flex: 1;
    }
}

/* ============================================================
 * 53. Packaging inventory — the parts not shared with the .ap-* family
 *
 * Buttons, cards, container, flash and the header row all come from section
 * 52; the table scrolls via section 50. These are the two components unique to
 * this page: the stock summary strip and the filter row.
 * ============================================================ */

@media (max-width: 991px) {
    /* A flex row with a 2rem gap wraps into one tile per line on a phone,
       which pushes the table below two folds. */
    .ap-summary {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 0.6rem;
    }

    .ap-stat {
        padding: 0.7rem 0.85rem;
    }

    .ap-stat-value {
        font-size: 1.25rem;
    }

    .ap-stat-label {
        font-size: 0.7rem;
        line-height: 1.3;
    }

    /* Filter controls side by side leave each one too narrow to read its own
       selected value. */
    .ap-filters {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 0.5rem;
    }

    .ap-filters select,
    .ap-filters input,
    .ap-filters .ap-btn {
        width: 100%;
        min-height: 44px;
        font-size: 16px;
    }

    /* First column: name over SKU. The SKU is the identifier someone scans
       for, so unlike the product blurb elsewhere it stays. */
    .ap-name {
        font-size: 0.9rem;
        line-height: 1.25;
    }

    .ap-sku {
        font-size: 0.72rem;
    }
}

/* Very narrow phones: two stat tiles start clipping their labels. */
@media (max-width: 380px) {
    .ap-summary {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
 * 54. Vendors list — the parts not shared with the .av-* family
 *
 * Buttons, cards, container, flash, header row and row actions all come from
 * section 52; the table scrolls via section 50. The filter form is what is
 * unique here, and it needs more than a stacking rule: each control is wrapped
 * in a <label>, so the label is the flex item, not the input.
 * ============================================================ */

@media (max-width: 991px) {
    .av-filters {
        display: grid;
        grid-template-columns: 1fr;
        gap: 0.6rem;
        align-items: stretch;
    }

    /* The label is the flex/grid item — styling the select alone would leave
       the text and the control on separate lines at unrelated widths. */
    .av-filters label {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 0.75rem;
        font-size: 0.9rem;
    }

    .av-filters select {
        flex: 1;
        max-width: 60%;
        min-height: 44px;
        font-size: 16px;
    }

    /* The checkbox label reads better as a normal left-aligned row than as a
       label pushed to one edge and a box to the other. */
    .av-filters label:has(input[type="checkbox"]) {
        justify-content: flex-start;
    }

    .av-filters input[type="checkbox"] {
        width: 1.1rem;
        height: 1.1rem;
        accent-color: var(--color-brand-primary);
    }

    .av-filters .av-btn {
        width: 100%;
    }

    /* Pills sit in already-narrow columns alongside the code and kind. */
    .av-pill {
        font-size: 0.66rem;
        padding: 0.12rem 0.45rem;
    }
}

/* ============================================================
 * 55. Portal / returns / promos pages (.vp-* family)
 *
 * _PortalStyles.cshtml is the shared stylesheet for eleven views — admin
 * returns and promos, plus the vendor and school portals — and carries no media
 * query at all. Fixing it here covers all of them.
 *
 * .vp-grid replaces an inline `display:grid;grid-template-columns:1fr 1fr`
 * that appeared seven times across three views. Inline styles beat every
 * stylesheet, so those form rows could not be collapsed to one column for
 * mobile until they became classes.
 * ============================================================ */

.vp-grid {
    display: grid;
    gap: 1rem;
    grid-template-columns: 1fr 1fr;
}

.vp-grid-3 {
    grid-template-columns: 1fr 1fr 1fr;
}

@media (max-width: 991px) {
    /* Two- and three-column form rows on a phone give each field a third of
       360px; one column is the only readable option. */
    .vp-grid,
    .vp-grid-3 {
        grid-template-columns: 1fr;
        gap: 0.75rem;
    }

    /* Title and action on one row is the same pattern as .admin-mini-header;
       this one is already a class, so it only needs the stacking rule. */
    .vp-header-bar {
        flex-direction: column;
        align-items: stretch;
        gap: 0.6rem;
    }

    .vp-header-bar h1,
    .vp-header-bar h2 {
        font-size: 1.35rem;
    }

    .vp-header-bar .vp-btn {
        width: 100%;
    }

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

    .vp-form input[type='text'],
    .vp-form input[type='email'],
    .vp-form input[type='number'],
    .vp-form select,
    .vp-form textarea {
        /* Below 16px iOS zooms on focus and never zooms back. */
        font-size: 16px;
        min-height: 44px;
    }

    .vp-form textarea {
        min-height: 88px;
    }

    .vp-form label {
        margin-top: 0.9rem;
    }

    /* ---- Bits ---- */

    .vp-meta {
        font-size: 0.8rem;
    }

    .vp-back-link {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }

    .vp-pill {
        font-size: 0.66rem;
    }
}

/* ============================================================
 * 56. Portal family — remaining shared components
 *
 * Completes section 55. Two more inline layouts in the promo form became
 * classes (a 1fr/2fr row and the submit button row), and .vp-validation is
 * given a style at last: it is applied to asp-validation-for spans in three
 * views but was never defined anywhere, so validation errors rendered as plain
 * body text — indistinguishable from the field's own help text.
 * ============================================================ */

/* Stated explicitly rather than relying on .vp-grid's default, so the markup
   reads as intentional. */
.vp-grid-2 {
    grid-template-columns: 1fr 1fr;
}

.vp-grid-1-2 {
    grid-template-columns: 1fr 2fr;
}

.vp-actions {
    margin-top: 1rem;
    display: flex;
    gap: 0.5rem;
}

.vp-validation {
    display: block;
    margin-top: 0.3rem;
    color: var(--color-accent-danger);
    font-size: 0.85rem;
    font-weight: var(--font-weight-semibold);
}

/* An empty validation span still occupies a line and pushes the next field
   down; only show it when the server has put a message in it. */
.vp-validation:empty {
    display: none;
}

@media (max-width: 991px) {
    .vp-grid-1-2 {
        grid-template-columns: 1fr;
    }

    /* Submit and cancel side by side leave each too narrow to read; stacked,
       the primary action is where the thumb already is. */
    .vp-actions {
        flex-direction: column;
        gap: 0.5rem;
    }

    .vp-actions .vp-btn {
        width: 100%;
    }

    .vp-empty {
        padding: 1.5rem 0.85rem;
    }
}

/* ============================================================
 * 57. Account pages (login, forgot password, reset password)
 *
 * None of these cleared the fixed header. .login-container — shared by Login
 * and ForgotPassword — sets `min-height: calc(100vh - 200px)` and centres its
 * box vertically, which happens to look right on a tall desktop window but puts
 * the box under the nav on a short one. ResetPassword used a bare Bootstrap
 * `.container mt-5`, which is 3rem — nowhere near enough.
 *
 * login.css has no media query at all, so everything below is new.
 * ============================================================ */

.login-container,
.account-page {
    padding-top: calc(5rem + 1rem);
}

@media (max-width: 991px) {
    .login-container,
    .account-page {
        padding-top: calc(var(--mobile-header-h) + 1rem);
        padding-left: 0.85rem;
        padding-right: 0.85rem;
        /* The viewport-height floor plus vertical centring fights the on-screen
           keyboard: as the viewport shrinks the box is pushed up and clipped. */
        min-height: auto;
        align-items: flex-start;
    }

    .login-box,
    .forgot-box,
    .account-page .card {
        max-width: none;
        width: 100%;
        padding: 1.25rem;
    }

    .account-page .card-body {
        padding: 1.25rem !important;
    }

    .login-header,
    .forgot-header {
        margin-bottom: 1.25rem;
    }

    .login-header h2,
    .forgot-header h2 {
        font-size: 1.4rem;
    }

    /* ---- Fields ----
       Below 16px iOS zooms on focus and never zooms back — on a sign-in form
       that leaves the customer scrolled sideways mid-password. */
    .login-container .form-control,
    .account-page .form-control {
        font-size: 16px;
        min-height: 46px;
    }

    /* The show/hide password control sits inside the field and was a bare icon. */
    .password-input {
        position: relative;
    }

    .toggle-password {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* ---- Actions ---- */

    .login-container .btn-primary,
    .account-page .btn-primary {
        width: 100%;
        min-height: 48px;
        font-size: 1rem;
    }

    /* Remember-me and forgot-password share a row; both need real targets. */
    .remember-me,
    .forgot-password,
    .back-to-login a,
    .register-link a {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }

    .form-group {
        margin-bottom: 0.9rem;
    }

    /* Bootstrap input groups on ResetPassword: the icon addon eats width that
       the field needs on a phone. */
    .account-page .input-group-text {
        min-width: 44px;
        justify-content: center;
    }
}

/* ============================================================
 * 58. Laptop-width header (above 1200px)
 *
 * Above 1200px the wrap rule in section 17 stops applying and the header goes
 * back to a single line — but site.css pins .nav-container to max-width:1200px
 * and marks .nav-menu / .nav-buttons flex-shrink:0, with white-space:nowrap on
 * the container. So the row has a hard 1136px of usable width and nothing in it
 * is allowed to give: on a 1280 or 1366 laptop — especially signed in, where
 * the links grow a Dashboard entry and the button cluster grows a greeting —
 * the contents are wider than the box and, with justify-content:space-between,
 * the overflow spills off the right edge. That is the cut-off.
 *
 * Two changes fix it: let the row use the viewport it actually has (the header
 * is a full-bleed bar, so its inner row does not have to stop at the 1200px
 * body column), and make the search field the elastic element so the links
 * never lose characters.
 * ============================================================ */

/* 1200.02px, not 1201px: section 17's wrap query is (max-width: 1200px), and a
 * fractional viewport — what Windows display scaling produces, e.g. 1536/1.25 =
 * 1228.8 — can land between two integer breakpoints and match neither. */
@media (min-width: 1200.02px) {
    .nav-container {
        max-width: min(1600px, 100%);
        gap: 1.5rem;
        /* Wide padding where there is room, tight where there isn't. */
        padding-inline: clamp(1rem, 2.5vw, 2rem);
        flex-wrap: nowrap;
    }

    /* The wordmark is a fixed cost and the links can't shrink without clipping
       their own text (they're nowrap), so the button cluster absorbs the
       squeeze. min-width:0 is what makes that real — a flex item defaults to
       min-width:auto and refuses to go below its content width. */
    .nav-buttons {
        flex: 0 1 auto;
        min-width: 0;
    }

    .nav-menu,
    .logo {
        flex: 0 0 auto;
    }

    .nav-search {
        flex: 1 1 15rem;
        min-width: 8rem;
    }

    .nav-search__input {
        /* Same min-width:auto trap, one level down. */
        min-width: 0;
    }

    .user-greeting {
        white-space: nowrap;
    }
}

/* The tight single-row band. Seven uppercase category links are ~700px of
 * fixed cost on their own, so below ~1560px the row can't carry the wordmark,
 * the links, a usable search field, the cart AND the greeting at once. Trim
 * the fixed costs here rather than letting the search ride its 8rem floor. */
@media (min-width: 1200.02px) and (max-width: 1560px) {
    .nav-container {
        gap: 1rem;
    }

    .nav-menu {
        gap: 0.875rem;
    }

    .nav-buttons {
        gap: 0.5rem;
    }

    .logo {
        font-size: 1.25rem;
        margin-right: 0;
    }

    /* The greeting is the only purely decorative item in the row, and at these
       widths it can only be shown truncated mid-name. Drop it and give the
       width to the search field — the Logout button still says you're signed
       in. It comes back above 1560px, where it fits whole. */
    .user-greeting {
        display: none;
    }
}

/* ============================================================
 * 59. Nav search — submit spinner
 *
 * Searching navigates the whole page. Between pressing Enter and the next page
 * painting there is a gap — long enough on a slow connection to read as "my
 * keypress didn't register", which produces a second Enter and a second
 * navigation. The spinner fills that gap.
 *
 * Drawn as a pseudo-element on the form rather than as an extra node, because
 * .nav-search is already position:relative to anchor the typeahead dropdown, and
 * a border-only circle needs no markup, no image and no font.
 * ============================================================ */

.nav-search.is-searching .nav-search__input {
    /* Room for the spinner. Only while searching, so the resting state keeps its
       full width for the placeholder. */
    padding-left: 2.1rem;
}

.nav-search.is-searching::before {
    content: "";
    position: absolute;
    left: 0.7rem;
    top: 50%;
    width: 0.85rem;
    height: 0.85rem;
    margin-top: -0.425rem;
    border: 2px solid var(--color-border-strong);
    /* One coloured arc against three muted sides is what reads as rotation. */
    border-top-color: var(--color-brand-primary);
    border-radius: 50%;
    /* Above the input's own background, below the dropdown. */
    z-index: 2;
    pointer-events: none;
    animation: nav-search-spin 0.6s linear infinite;
}

@keyframes nav-search-spin {
    to { transform: rotate(360deg); }
}

/* A spinner that cannot spin says nothing, so reduced motion gets a pulse
   instead of a frozen circle — the point is "something is happening", and that
   survives the substitution. */
@media (prefers-reduced-motion: reduce) {
    .nav-search.is-searching::before {
        animation: nav-search-pulse 1.2s ease-in-out infinite;
        border-color: var(--color-brand-primary);
    }

    @keyframes nav-search-pulse {
        0%, 100% { opacity: 0.3; }
        50%      { opacity: 1; }
    }
}

/* ============================================================
 * 60. Bundle piece count
 *
 * "6 pieces" on every bundle card. It answers the first question a parent asks
 * about a kit, and until now the only way to find out was to open the bundle.
 *
 * Deliberately not styled like the school and gender badges next to it. Those
 * are categories — what kind of thing this is — and they are filled shapes
 * competing for the same attention. This is a quantity, a fact about the
 * contents, so it reads as an outlined chip: present, countable, and not
 * shouting over the school name.
 * ============================================================ */

.piece-tag {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.25rem 0.55rem;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-bg-surface);
    color: var(--color-text);
    font-size: 0.75rem;
    font-weight: var(--font-weight-semibold);
    line-height: 1.4;
    white-space: nowrap;
    /* Digits in a row of cards should sit on the same rhythm. */
    font-variant-numeric: tabular-nums;
}

.piece-tag i {
    font-size: 0.7rem;
    color: var(--color-brand-primary);
}

/* On the details page the badges are larger; match the row rather than staying
   card-sized next to them.

   The wrapper is `.bundle-details-page`, not `.bundle-details` — a class selector
   matches the whole name, so the shorter one never applied and this rule did
   nothing. `.bundle-hero` was likewise never in the markup. */
.bundle-details-page .piece-tag {
    font-size: 0.8125rem;
    padding: 0.35rem 0.7rem;
}

/* Sitting above the description, it needs its own line and room beneath it —
   inline-flex alone would let the following paragraph crowd it. */
.bundle-details-page .details-section > .piece-tag {
    display: inline-flex;
    margin-bottom: 1rem;
}

@media (max-width: 480px) {
    .piece-tag { font-size: 0.7rem; padding: 0.2rem 0.45rem; }
}
