/*
 * Shop/catalog page — benchmarked against garmentdecor.com/catalog per
 * Victoria's request (2026-08-11). Reuses the same color tokens established
 * in site-header-footer.css for visual consistency, but scoped separately
 * since this stylesheet only loads on shop/category/taxonomy archive pages.
 */

:root {
    --b2b-shop-blue: #0088CC;
    --b2b-shop-text: #222529;
    --b2b-shop-muted: #6B7280;
    --b2b-shop-border: #EEEEEE;
    --b2b-shop-green: #1EA64C;
    --b2b-shop-red: #D3455B;
}

/* Shoptimizer wraps the whole archive body (breadcrumb + main content) in
   its own .col-full container (max-width:1170px, ~42px side padding,
   confirmed in main.css:1503-1510). Every .b2b-shop-* rule below applies
   its OWN max-width:1320px/24px-padding container on top of that, which
   double-insets everything and pushes it right of the navbar's left edge
   (the navbar's own .b2b-navbar__inner uses the same 1320px/24px system
   with nothing else wrapping it). Neutralize .col-full's width/padding
   here, scoped to the archive page only, so this stylesheet's own
   1320px/24px containers are the sole source of horizontal alignment and
   line up exactly with the navbar above them. */
.shoptimizer-archive .col-full {
    max-width: none;
    padding-left: 0;
    padding-right: 0;
}
/* ── Breadcrumb row ──
   One line carrying three things (Victoria, 2026-08-20): the "Home / Shop"
   breadcrumb, the "Blank or Custom Branded Apparel" tagline, and — pulled up
   from below — the result count and sort dropdown.

   The tagline is injected INTO this <nav> server-side via the breadcrumb's
   own wrap_after (see ShopPage::append_tagline_to_breadcrumb), so only the
   toolbar needs moving, and that has to be done here rather than in PHP:
   the count reads wc_get_loop_prop('total'), which only exists once the
   product loop has started, so the markup cannot be relocated above it. */
.shoptimizer-archive .archive-header .woocommerce-breadcrumb {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 14px;
    max-width: 1320px;
    margin: 0 auto;
    padding: 14px 24px 0;
}

.b2b-shop-tagline {
    /* This IS the page's only <h1> (WooCommerce's own title is suppressed in
       ShopPage::init()), so it stays a heading for SEO and screen-reader
       navigation while rendering at breadcrumb scale rather than as a
       display title. */
    margin: 0;
    font-size: 15px;
    font-weight: 700;
    color: var(--b2b-shop-text);
    line-height: 1.4;
}
.b2b-shop-tagline::before {
    content: "";
    display: inline-block;
    width: 1px; height: 12px;
    margin-right: 14px;
    background: var(--b2b-shop-border);
    vertical-align: -1px;
}

/* ── Toolbar (mobile filter toggle / result count / sort) ──
   Victoria asked (2026-08-20) for "31 products  Default sorting" to sit on
   the SAME ROW as the Home / Shop breadcrumb.

   Done with a negative top margin rather than by moving the markup: the
   count reads wc_get_loop_prop('total'), which only exists once the product
   loop has started, so this block CANNOT be rendered earlier in the
   document. The breadcrumb <nav> above is a flex row whose own bottom
   padding is 0, so pulling the toolbar up by its natural row height lands
   the two on one visual line while each keeps its own 1320px/24px
   container — meaning they stay left-aligned with each other and with the
   navbar, and the toolbar's own right-alignment still puts the count and
   sort control at the far right of the row. */
.b2b-shop-toolbar {
    max-width: 1320px; margin: -34px auto 0; padding: 0 24px 12px;
    display: flex; align-items: center; justify-content: flex-end; gap: 16px;
    border-bottom: 1px solid var(--b2b-shop-border);
    position: relative; z-index: 1;
}
.b2b-shop-toolbar__filter-toggle {
    display: none;
    align-items: center; gap: 8px;
    background: #fff; border: 1px solid var(--b2b-shop-border); border-radius: 8px;
    padding: 9px 14px; font-size: 13px; font-weight: 600; color: var(--b2b-shop-text);
    cursor: pointer; margin-right: auto;
}
.b2b-shop-toolbar__filter-toggle svg { width: 16px; height: 16px; flex-shrink: 0; }
.b2b-shop-toolbar__count { font-size: 13px; color: var(--b2b-shop-muted); margin: 0; }
.b2b-shop-toolbar .woocommerce-ordering { margin: 0; }
.b2b-shop-toolbar .orderby {
    border: 1px solid var(--b2b-shop-border); border-radius: 8px;
    padding: 8px 32px 8px 12px; font-size: 13px; color: var(--b2b-shop-text);
    background-color: #fff;
}

/* ── Layout: sidebar + grid ── */
.b2b-shop-layout {
    max-width: 1320px; margin: 0 auto; padding: 24px 24px 60px;
    display: grid; grid-template-columns: 260px 1fr; gap: 32px; align-items: start;
}
/* Centre the product table (Victoria, 2026-08-20).

   `align-items: stretch`, NOT `center` (corrected 2026-08-21). Centring made
   every child shrink-to-fit, so the <ul> rendered 941px inside its own 980px
   column -- a 39px dead strip -- and the grid then divided that narrower box
   into 297px columns instead of 310px. Stretch makes each child fill the
   column, and the grid's own minmax(0, 1fr) tracks handle the balancing.
   justify-items on the <ul> still centres the CARDS within their cells, so a
   final short row doesn't sit lopsided. */
.b2b-shop-layout__grid { min-width: 0; display: flex; flex-direction: column; align-items: stretch; }
.b2b-shop-layout__grid > ul.products { width: 100%; justify-items: center; }
.b2b-shop-layout__grid > .b2b-load-more,
.b2b-shop-layout__grid > #b2b-active-filters { width: 100%; }
.b2b-shop-no-results { padding: 60px 20px; text-align: center; color: var(--b2b-shop-muted); font-size: 15px; }

/* ── Filter sidebar ──
   Visually matched to swag.com/collections/hats' real Filters sidebar
   (screenshot reference, 2026-08-12): plain non-uppercase section
   headers, minus/plus toggle icons instead of chevrons, "(N)" inline
   counts instead of a separate pill, larger un-bordered color swatches,
   bracket-style price range with inline "$" + value pairs. Keeps the
   site's own blue accent (--b2b-shop-blue) rather than swag.com's pink,
   per Victoria's explicit choice -- everything else matches. */
.b2b-shop-filters { position: sticky; top: 90px; }

/* ── Sidebar group order ──────────────────────────────────────────────
   SINGLE SOURCE OF TRUTH for the filter sequence (Victoria, 2026-08-20):

     1 Offers (On Sale)  2 Brands    3 Price Range   4 Categories
     5 Product Type      6 Color     7 Gender/Age    8 Fabric / Material
     9 Size             10 Profile  11 Structure    12 Bill Shape
    13 Closure          14 Sustainability

   Updated 2026-08-21: "Quick Filters" was split. On Sale kept the top slot
   under the clearer heading "Offers"; Eco-Friendly moved into the new
   Sustainability group at 14, which is where Victoria asked for it
   (immediately after Closure).

   Done with flex `order` rather than by moving the PHP, because the groups
   live in two different places that cannot simply be interleaved: Categories,
   Color, Brands and Price are rendered directly in render_filter_sidebar(),
   while the style-attribute facets (Product Type, Gender/Age, Size, Profile,
   Structure, Bill Shape, Closure, Fabric/Material) are built inside
   #b2b-contextual-facets, whose innerHTML is REPLACED wholesale on every AJAX
   filter. Moving Color into that container would rebuild the swatches under
   the shopper on every keystroke of the price slider; moving the facets out
   would break the single-swap refresh. `order` sidesteps both.

   #b2b-contextual-facets uses `display: contents` so its children participate
   in this flex ordering individually -- without it the whole container would
   be ONE flex item and Product Type could not sit above Color while Gender/Age
   sits below it. The container keeps its id and its role as the AJAX swap
   target; it simply stops generating a box of its own.

   Sustainability is rendered OUTSIDE #b2b-contextual-facets (its options come
   from a different query path, sustainability_options(), and it must survive
   the container's wholesale innerHTML swap), so it is ordered here like the
   other standalone groups. */
.b2b-shop-filters { display: flex; flex-direction: column; }
#b2b-contextual-facets { display: contents; }

/* Default FIRST: a later rule of equal specificity would otherwise override
   every modifier below and flatten the whole order back to 20. Anything not
   explicitly ordered falls to the end rather than jumping to the top (the
   CSS default `order: 0` would put it above item 1). */
.b2b-filter-group               { order: 20; }

.b2b-filter-group--onsale       { order: 1; }
.b2b-filter-group--brands       { order: 2; }
.b2b-filter-group--price        { order: 3; }
.b2b-filter-group--categories   { order: 4; }
.b2b-filter-group--product_type { order: 5; }
.b2b-filter-group--color        { order: 6; }
.b2b-filter-group--fit          { order: 7; }   /* Gender/Age */
.b2b-filter-group--material     { order: 8; }   /* Fabric / Material */
.b2b-filter-group--pa_size      { order: 9; }   /* Size */
.b2b-filter-group--profile      { order: 10; }
.b2b-filter-group--structure    { order: 11; }
.b2b-filter-group--bill_shape   { order: 12; }
.b2b-filter-group--closure      { order: 13; }
.b2b-filter-group--sustainability { order: 14; }
/* The sidebar's own "Clear all filters" button must stay last. */
.b2b-filter-clear               { order: 99; }
.b2b-shop-filters__head {
    display: none; align-items: center; justify-content: space-between;
    margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--b2b-shop-border);
}
.b2b-shop-filters__head h2 { font-size: 20px; font-weight: 700; color: var(--b2b-shop-text); margin: 0; }
.b2b-shop-filters__close { background: none; border: none; cursor: pointer; padding: 4px; color: var(--b2b-shop-text); }
.b2b-shop-filters__close svg { width: 20px; height: 20px; }

.b2b-filter-group { padding: 18px 0; border-bottom: 1px solid var(--b2b-shop-border); }
.b2b-shop-filters > .b2b-filter-group:first-of-type { padding-top: 0; }

.b2b-filter-group__toggle {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; background: none; border: none; padding: 0; margin: 0 0 14px;
    cursor: pointer; text-align: left;
}
.b2b-filter-group__toggle h3 { font-size: 15px; font-weight: 700; color: var(--b2b-shop-text); margin: 0; }
.b2b-filter-group__toggle-icon {
    position: relative;
    width: 12px; height: 12px; flex-shrink: 0;
    color: var(--b2b-shop-text);
}
.b2b-filter-group__toggle-icon::before,
.b2b-filter-group__toggle-icon::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    background: currentColor; transform: translate(-50%, -50%);
}
.b2b-filter-group__toggle-icon::before { width: 12px; height: 2px; }
.b2b-filter-group__toggle-icon::after { width: 2px; height: 12px; transition: opacity .1s; }
.b2b-filter-group__toggle[aria-expanded="false"] .b2b-filter-group__toggle-icon::after { opacity: 1; }
.b2b-filter-group__toggle[aria-expanded="true"] .b2b-filter-group__toggle-icon::after { opacity: 0; }
/* NOTE: no overflow:hidden here -- the accordion toggle (shop-filters.js
   initFilterAccordions()) only ever sets body.style.display, it never
   relies on overflow clipping for a height-collapse animation, so this
   was pure defensive dead weight. Removed 2026-08-18 because it was
   clipping the color-swatch hover scale-up transform (top/bottom rows)
   and clipping the hover tooltip entirely for the top row + horizontally
   for the first/last column -- confirmed via a live screenshot Victoria
   sent showing exactly those 4 symptoms. */

.b2b-filter-checklist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px; }
.b2b-filter-checklist label { display: flex; align-items: center; gap: 9px; font-size: 14px; color: var(--b2b-shop-text); cursor: pointer; }
.b2b-filter-checklist input[type="checkbox"] {
    width: 16px; height: 16px; accent-color: var(--b2b-shop-blue); cursor: pointer; flex-shrink: 0;
    border-radius: 3px;
}
.b2b-filter-checklist label span:first-of-type { flex: 1; }
.b2b-filter-count { color: var(--b2b-shop-muted); font-size: 13px; }
.b2b-filter-count::before { content: '('; }
.b2b-filter-count::after { content: ')'; }

.b2b-filter-see-all {
    display: inline-block; margin-top: 4px;
    font-size: 13px; font-weight: 600; color: var(--b2b-shop-blue);
    text-decoration: underline; background: none; border: none; padding: 0; cursor: pointer;
}

/* Facet "Show All" truncation (Categories capped at 6 options, every other
   facet at 3), matching the swag.com/collections/tshirts reference Victoria
   supplied. The button is injected by shop-filters.js as a sibling directly
   after the list, so it inherits the same underlined-link treatment as
   .b2b-filter-see-all above rather than introducing a second look.

   Overflow items are hidden by CLASS, never by inline display, specifically
   so this cannot fight the live brand search -- initBrandSearch() owns
   li.style.display on #b2b-filter-brand-list, and an inline-display approach
   here would have the two silently overwrite each other.

   Only .b2b-filter-checklist is targeted: the Colour swatch list is exempt
   from truncation per Victoria (2026-08-19) and always shows every bucket,
   so no .b2b-filter-swatchlist rule exists here by design. */
.b2b-filter-checklist li.is-b2b-hidden { display: none; }

/* While the brand search has a query, the SEARCH owns visibility for the
   whole list -- a matching brand must appear even if it sits past the
   collapse cap (Victoria, 2026-08-21: searching a collapsed brand showed an
   empty list; 12 of 15 brands were unreachable without expanding first).
   This rule defeats the collapse; initBrandSearch() then shows or hides each
   row purely on whether it matches, and removes the class when the query is
   cleared so applyShowAll() can rebuild the collapsed state.

   `display: block` rather than `revert`: these <li> are flex children of the
   column list and carry no display rule of their own, so block matches how
   every other row already lays out and is more predictable than reverting to
   the UA default. initBrandSearch() still sets inline display:none on
   NON-matches, and inline wins over this rule, so only matches show. */
.b2b-filter-group.is-brand-searching .b2b-filter-checklist li.is-b2b-hidden { display: block; }
/* The "+N more" toggle is meaningless mid-search -- every match is already
   shown -- and clicking it would fight the search for control of the rows. */
.b2b-filter-group.is-brand-searching .b2b-filter-showall { display: none; }

.b2b-filter-showall {
    display: inline-block; margin-top: 10px;
    font-size: 13px; font-weight: 600; color: var(--b2b-shop-blue);
    text-decoration: underline; background: none; border: none; padding: 0; cursor: pointer;
}
.b2b-filter-showall:hover { color: var(--b2b-shop-text); }
.b2b-filter-showall:focus-visible { outline: 2px solid var(--b2b-shop-blue); outline-offset: 2px; border-radius: 2px; }

/* Color filter, one small CIRCULAR swatch per BUCKET (14 total, e.g.
   "Blues") with a hover tooltip showing the bucket name -- shape/tooltip
   style per the swag.com/collections/tshirts reference Victoria linked,
   grouping per her original bucket-based request (2026-08-18). Checking a
   circle filters to every real color in that bucket. Same .b2b-filter-swatch
   classes are shared with the (unrelated) per-color swatch dots used
   elsewhere on this page, e.g. color_term_swatch() on product cards. */
.b2b-filter-swatchlist { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 10px; }
.b2b-filter-swatch { position: relative; display: block; cursor: pointer; }
.b2b-filter-swatch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.b2b-filter-swatch__dot {
    display: block; width: 26px; height: 26px; min-width: 26px; min-height: 26px;
    box-sizing: border-box; flex-shrink: 0; border-radius: 50%;
    border: 1px solid rgba(0,0,0,0.15); overflow: hidden;
    transition: box-shadow .15s, transform .1s;
}
.b2b-filter-swatch__dot img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Multicolor bucket: final design (2026-08-18) -- Victoria supplied an
   exact reference icon, an 8-slice equal-wedge color wheel, and asked to
   use it exactly as shown (replacing all earlier diagonal-gradient
   attempts). 8 equal 45deg wedges via conic-gradient, starting at 12
   o'clock and going clockwise to match the reference precisely: pale
   yellow, orange, coral, pink/magenta, medium blue, darker blue, sky
   blue, yellow-green. border-radius/overflow/box-sizing are redundantly
   re-declared here (already set on the shared .b2b-filter-swatch__dot
   base class above, which also has box-sizing:border-box + flex-shrink:0
   + min-width/min-height -- defensive hardening added while investigating
   an earlier "not a clean circle" report that turned out to be a
   client-side browser-cache issue, not a real CSS bug; see git history/
   B2B-HATS-CLAUDE-HANDOFF.md for that investigation if a similar report
   recurs -- check the served ?ver= cache-busting param before assuming
   the CSS is wrong again). */
.b2b-filter-swatch__dot--pie {
    border-radius: 50%;
    overflow: hidden;
    box-sizing: border-box;
    position: relative;
    /* 2 earlier attempts at this (hard color-stops, then a GPU-compositing
       hint + hairline soft-stops) still left visible edge roughness and
       color bleeding past the circle boundary, per Victoria's follow-up
       screenshot (2026-08-18) -- the transform:translateZ(0) GPU-layer
       trick most likely made bleeding WORSE, not better: compositing a
       gradient on its own layer can rasterize the layer's content at a
       slightly different sub-pixel offset than the border-radius clip
       mask applied to the element itself, which is exactly what produces
       thin slivers of wedge color peeking past the edge. Removed that
       entirely. Real fix: paint the conic-gradient on a ::before that is
       explicitly OVERSIZED (positioned 1px past each edge, i.e. bigger
       than the circle it's clipped into) rather than exactly filling the
       box -- any sub-pixel rounding/anti-aliasing softness in the
       gradient's own raster now lands a pixel outside the visible circle,
       where the parent's border-radius+overflow:hidden clip cuts it away
       cleanly, instead of landing exactly ON the visible boundary where
       it would show as bleed. The parent itself has no background of its
       own here (kept transparent) -- only ::before paints color. */
    background: transparent;
}
.b2b-filter-swatch__dot--pie::before {
    content: '';
    position: absolute;
    top: -1px; right: -1px; bottom: -1px; left: -1px;
    border-radius: 50%;
    background: conic-gradient(
        #FFE585 0deg 45deg, #FFA53E 45deg 90deg,
        #FF7A6E 90deg 135deg, #F0197D 135deg 180deg,
        #1B62C4 180deg 225deg, #0B9FE8 225deg 270deg,
        #4FCBFF 270deg 315deg, #A9D468 315deg 360deg
    );
}
.b2b-filter-swatch input:checked + .b2b-filter-swatch__dot { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--b2b-shop-blue); }
.b2b-filter-swatch input:focus-visible + .b2b-filter-swatch__dot { outline: 2px solid var(--b2b-shop-blue); outline-offset: 2px; }
.b2b-filter-swatch:hover .b2b-filter-swatch__dot { transform: scale(1.08); }

/* Hover tooltip -- pure CSS (no JS), shows the real color name above the
   swatch on hover/focus, matching the reference screenshot's dark pill. */
.b2b-filter-swatch__tooltip {
    position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(4px);
    background: #1a1a1a; color: #fff; font-size: 11.5px; font-weight: 600; line-height: 1;
    padding: 6px 10px; border-radius: 6px; white-space: nowrap;
    opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity .12s, transform .12s; z-index: 5;
}
.b2b-filter-swatch__tooltip::after {
    content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
    border: 5px solid transparent; border-top-color: #1a1a1a;
}
.b2b-filter-swatch:hover .b2b-filter-swatch__tooltip,
.b2b-filter-swatch input:focus-visible ~ .b2b-filter-swatch__tooltip {
    opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);
}

/* First/last swatch in the row: a centered tooltip would overhang past the
   sidebar's own edge (previously invisible since the ancestor's
   overflow:hidden clipped it outright -- now that clipping is gone, an
   overhanging tooltip would just look like it's floating off the sidebar).
   Anchor those two to the swatch's own left/right edge instead of centering. */
.b2b-filter-swatchlist li:first-child .b2b-filter-swatch__tooltip {
    left: 0; transform: translateX(0) translateY(4px);
}
.b2b-filter-swatchlist li:first-child .b2b-filter-swatch__tooltip::after { left: 13px; transform: translateX(-50%); }
.b2b-filter-swatchlist li:first-child .b2b-filter-swatch:hover .b2b-filter-swatch__tooltip,
.b2b-filter-swatchlist li:first-child .b2b-filter-swatch input:focus-visible ~ .b2b-filter-swatch__tooltip {
    transform: translateX(0) translateY(0);
}
.b2b-filter-swatchlist li:last-child .b2b-filter-swatch__tooltip {
    left: auto; right: 0; transform: translateX(0) translateY(4px);
}
.b2b-filter-swatchlist li:last-child .b2b-filter-swatch__tooltip::after { left: auto; right: 13px; transform: translateX(50%); }
.b2b-filter-swatchlist li:last-child .b2b-filter-swatch:hover .b2b-filter-swatch__tooltip,
.b2b-filter-swatchlist li:last-child .b2b-filter-swatch input:focus-visible ~ .b2b-filter-swatch__tooltip {
    transform: translateX(0) translateY(0);
}

.b2b-filter-brand-search {
    width: 100%; box-sizing: border-box; margin-bottom: 12px;
    border: 1px solid var(--b2b-shop-border); border-radius: 7px;
    padding: 8px 10px; font-size: 14px; color: var(--b2b-shop-text);
}
.b2b-filter-brand-search:focus { outline: none; border-color: var(--b2b-shop-blue); }

.b2b-filter-price__inputs { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
.b2b-filter-price__value { font-size: 14px; font-weight: 700; color: var(--b2b-shop-blue); }
.b2b-filter-price__sign { font-weight: 700; }

/* Dual-handle range slider: both <input type="range"> elements are
   overlaid on the same track (position:absolute, shared coordinates)
   rather than stacked as two separate full-width sliders -- each input's
   own track is made fully transparent so only its native thumb is
   visible, and .b2b-filter-price__track-fill (a plain div, not part of
   either input) draws the visible blue "selected range" bar underneath.
   pointer-events on the transparent inputs is left enabled so both
   thumbs stay independently draggable; z-index bumps whichever thumb is
   focused/hovered above the other so neither becomes permanently
   unreachable when the two handles are near the same value. */
.b2b-filter-price__track {
    position: relative;
    height: 20px;
    display: flex;
    align-items: center;
}
.b2b-filter-price__track-fill {
    position: absolute;
    left: 0; right: 0; top: 50%;
    height: 4px;
    transform: translateY(-50%);
    background: var(--b2b-shop-border);
    border-radius: 2px;
    pointer-events: none;
}
.b2b-filter-price__range {
    position: absolute;
    left: 0; right: 0; top: 0;
    width: 100%; height: 20px;
    margin: 0;
    background: none;
    accent-color: var(--b2b-shop-blue);
    -webkit-appearance: none;
    appearance: none;
    pointer-events: none; /* the input element itself is invisible/inert; only its thumb (re-enabled below) is interactive */
}
.b2b-filter-price__range::-webkit-slider-runnable-track { background: none; height: 4px; }
.b2b-filter-price__range::-moz-range-track { background: none; height: 4px; }
.b2b-filter-price__range::-webkit-slider-thumb {
    pointer-events: auto;
    -webkit-appearance: none;
    appearance: none;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--b2b-shop-blue);
    border: 2px solid #fff;
    box-shadow: 0 1px 3px rgba(0,0,0,0.3);
    cursor: pointer;
    margin-top: -6px;
}
.b2b-filter-price__range::-moz-range-thumb {
    pointer-events: auto;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--b2b-shop-blue);
    border: 2px solid #fff;
    box-shadow: 0 1px 3px rgba(0,0,0,0.3);
    cursor: pointer;
}
.b2b-filter-price__range:focus,
.b2b-filter-price__range:hover { z-index: 2; }

/* ── Active-filter chip row (swag.com-style, sits above the product grid) ── */
.b2b-active-filters {
    display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
    margin-bottom: 20px;
}
.b2b-active-filter-chip {
    display: inline-flex; align-items: center; gap: 8px;
    background: var(--b2b-shop-blue); color: #fff;
    border: none; border-radius: 20px;
    padding: 7px 12px 7px 14px;
    font-size: 13px; font-weight: 600;
    cursor: pointer;
}
.b2b-active-filter-chip svg { width: 13px; height: 13px; flex-shrink: 0; }
.b2b-active-filter-chip:hover { background: #006fa8; }

/* Collapse the chip row to a single line (Victoria, 2026-08-20). Picking one
   colour bucket expands to every underlying colour -- "Blacks" alone produced
   ~70 chips that filled half the viewport and pushed the products below the
   fold. Hidden chips stay in the DOM and keep working; only their display is
   suppressed, because each chip is the control that REMOVES its own filter. */
.b2b-active-filters.is-collapsed .b2b-active-filter-chip.is-chip-hidden { display: none; }

.b2b-active-filters__more {
    background: none; border: 1px solid var(--b2b-shop-border); border-radius: 20px;
    padding: 6px 12px;
    font-size: 13px; font-weight: 600; color: var(--b2b-shop-text);
    cursor: pointer; white-space: nowrap;
}
.b2b-active-filters__more:hover { border-color: var(--b2b-shop-blue); color: var(--b2b-shop-blue); }
.b2b-active-filters__more:focus-visible { outline: 2px solid var(--b2b-shop-blue); outline-offset: 2px; }

/* Keep "Clear all" pinned to the end of the row so it doesn't get lost among
   the chips once they wrap on expand. */
.b2b-active-filters__clear {
    background: none; border: none; padding: 0;
    margin-left: auto;
    font-size: 13px; font-weight: 600; color: var(--b2b-shop-blue);
    text-decoration: underline; cursor: pointer;
}
.b2b-active-filters__clear:hover { color: #006fa8; }

.b2b-filter-clear {
    width: 100%; margin-top: 16px;
    background: none; border: 1px solid var(--b2b-shop-border); border-radius: 8px;
    padding: 10px; font-size: 13px; font-weight: 600; color: var(--b2b-shop-text);
    cursor: pointer;
}
.b2b-filter-clear:hover { border-color: var(--b2b-shop-blue); color: var(--b2b-shop-blue); }

/* ── Product cards ──
 * Shoptimizer's own ul.products/li.product rules (main.css ~2186-2260) use a
 * flex+float hybrid with a fixed li.product width (e.g. 24.97% for
 * .columns-4) and left/right padding -- those must be explicitly reset here,
 * not just overridden on the parent, or the float/width/padding survive
 * inside our grid container and corrupt the layout (floated narrow-width
 * items inside a grid parent is undefined behavior in practice: this is
 * exactly what produced the single-character-per-line vertical card strips
 * seen live). !important matches the theme's own selector weight
 * (.columns-4 ul.products li.product, 3 classes) which this 2-class
 * selector cannot otherwise reliably beat by specificity alone. */
.b2b-shop-layout__grid ul.products {
    display: grid !important;
    /* minmax(0, 1fr) -- NOT plain 1fr. THIS IS THE ROOT CAUSE of the
       long-running "cards are different sizes" problem (measured in
       headless Edge 2026-08-19: columns rendered 477px / 293px / 380px
       instead of 3 equal tracks, which then drove card heights of
       704 / 474 / 583px in a repeating 3-card pattern matching the 3
       columns exactly). `1fr` is shorthand for `minmax(auto, 1fr)`, and
       that `auto` MINIMUM means a track refuses to shrink below its
       content's intrinsic width -- so columns holding products with
       wider intrinsic images grew, and the "equal fractions" never
       happened. minmax(0, 1fr) sets the floor to 0 so every track is a
       genuinely equal fraction of the row, independent of any product's
       content or image dimensions. This is what makes card width -- and
       therefore, via the image box's aspect-ratio, card HEIGHT --
       identical for every product, now and for every future product. */
    grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
    gap: 24px !important;
    width: auto !important;
    margin: 0 !important; padding: 0 !important; list-style: none;
}
.b2b-shop-layout__grid ul.products li.product {
    /* Card container matched to the real garmentdecor.com/catalog product
       card (verified against a user-provided close-up screenshot,
       2026-08-13): a bordered, rounded white card with a subtle shadow --
       supersedes an earlier borderless/full-bleed decision made against a
       different (less detailed) reference read of the same site. */
    float: none !important;
    /* WIDTH MUST BE 100%, NOT auto (fixed 2026-08-21).
       `width: auto` on a display:flex grid item makes the card size to its
       CONTENT instead of filling its grid column. On first page load that
       happened to look right -- the eagerly-loaded first image supplied an
       intrinsic width -- but after an AJAX filter the images are freshly
       inserted and not yet measured, so every card collapsed to a different
       width. Measured live: page load gave a uniform 311x496 card with a
       277x346 image; after filtering, card widths ranged 147-319px and image
       heights 142-356px across 15 distinct sizes, even though the grid
       columns stayed a fixed 310.656px throughout.
       100% pins the card to its column regardless of what the content
       measures, so the layout no longer depends on image load timing. */
    width: 100% !important;
    min-width: 0;
    margin: 0 !important;
    padding: 16px 16px 18px !important;
    background: #fff;
    border: 1px solid var(--b2b-shop-border);
    border-radius: 14px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.05);
    overflow: visible; position: relative;
    transition: box-shadow .2s ease, border-color .2s ease;
    /* Flex column so every card stretches to the grid row's tallest card
       (CSS Grid items stretch to row height by default) while the swatch
       row anchors to the bottom -- keeps cards in a row visually uniform
       even when titles/prices/swatch counts differ in length. Reverted
       2026-08-15 from an earlier attempt that also forced height:100%,
       which distorted the image wrapper's own layout and pushed the
       overlaid compare icon outside the card -- height:100% is
       unnecessary here since a flex column item in a CSS Grid row
       already stretches to the row's tallest sibling by default. */
    display: flex !important;
    flex-direction: column;
    align-items: stretch;
}
.b2b-shop-layout__grid ul.products li.product > .woocommerce-image__wrapper {
    flex-shrink: 0;
}
.b2b-shop-layout__grid ul.products li.product .b2b-card__meta-row {
    margin-top: auto;
    padding-top: 2px;
}
/* Blue glow on hover, per Victoria's 2026-08-13 request -- applies to the
   whole card, not just the image. */
.b2b-shop-layout__grid ul.products li.product:hover {
    border-color: var(--b2b-shop-blue);
    box-shadow: 0 0 0 3px rgba(0,136,204,0.18), 0 4px 14px rgba(0,136,204,0.22);
}
/* Real root cause of both the "clipped/broken glow" AND the "swatches
   disappear on hover" bug reported 2026-08-13: Shoptimizer's own
   ul.products li.product:not(.product-category):before (main.css
   ~4472-4485) is an absolutely-positioned white shadow panel
   (background-color:#fff, height:calc(100% + 30px)) that the theme
   reveals on hover (visibility/opacity toggled at main.css ~4750-4754) --
   sized/positioned for the theme's own original card shape, it painted
   over this taller bordered card's lower content (swatches/brand/price)
   on hover, and its own hard-edged rectangle showed through past our
   rounded border wherever it didn't fully cover us. Disabled outright
   rather than resized, since our own box-shadow above already provides
   the real hover-glow effect this element was for. */
.b2b-shop-layout__grid ul.products li.product:not(.product-category):before,
.b2b-shop-layout__grid ul.products li.product:hover:before {
    content: none !important;
    display: none !important;
}
/* Defense in depth: give the card's real content its own stacking
   context above anything else absolutely positioned inside the li, in
   case a different theme layer (not just the :before above) ever tries
   the same "cover the card" trick again. */
.b2b-shop-layout__grid ul.products li.product > * {
    position: relative;
    z-index: 1;
}
/* ══════════════════════════════════════════════════════════════════════
   PRODUCT IMAGE BOX — the definitive fix (2026-08-19).
   Victoria: "I need a real, final, permanent solution", after 3 earlier
   attempts failed. Full diagnosis before this rewrite:

   ROOT CAUSE 1 (data): WooCommerce is configured `uncropped` with only a
   width (300px) constrained, so it generates thumbnails at whatever
   height each source aspect ratio dictates. Verified live: 20 DISTINCT
   rendered sizes across 28 published products, heights 188px–517px.
   WordPress writes those as literal width/height ATTRIBUTES on each
   <img>, so every card's image had a different intrinsic height.

   ROOT CAUSE 2 (CSS): the previous fix set aspect-ratio on
   .woocommerce-image__wrapper and height:100% on the img — but the
   Shoptimizer theme injects TWO unstyled divs between them
   (shoptimizer_template_loop_carousel_open, priority 9, fires before the
   thumbnail at priority 10):
       .woocommerce-image__wrapper
         > a.woocommerce-LoopProduct-link
           > .shoptimizer-plp-carousel-container   ← no CSS above 992px
             > .shoptimizer-plp-image-wrapper      ← no CSS above 992px
               > img
   A percentage height against an auto-height ancestor computes to auto,
   so height:100% was silently a no-op and the img's intrinsic height
   propagated up and set the card height. overflow:visible +
   flex-shrink:0 on the wrapper then let it dictate the whole row.

   THE FIX — reuse the pattern already proven on the single product page
   (product-page.css:310-333, #b2b-gallery__main), which has never had
   this problem, adapted for the theme's extra nesting:
     • wrapper = fixed aspect-ratio 4/5 flex box, overflow:hidden
       (4/5 matches the product page gallery exactly, and most source
       photos are 500x625 = exactly 4:5, so they fill it near-perfectly)
     • every intermediate element (the <a> and BOTH theme divs) is forced
       to width/height:100% + flex-centre, restoring the unbroken chain
       the theme breaks
     • img uses max-width/max-height:100% with width:auto;height:auto —
       `auto` is what NEUTRALIZES the per-image HTML width/height
       attributes (the actual source of the variance), while max-* still
       constrains against the box. This is the part height:100% could
       never do through broken inheritance.
     • object-fit:contain — the whole garment is always visible, never
       cropped; a tall photo simply letterboxes inside the same box.
   Result: identical image area on every card, for any source dimension,
   aspect ratio or resolution, now and for every future product — with
   no per-product intervention and no dependence on WooCommerce's
   thumbnail settings.
   ══════════════════════════════════════════════════════════════════════ */
.b2b-shop-layout__grid .woocommerce-image__wrapper {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    width: 100%; aspect-ratio: 4 / 5;
    background: #fff; border-radius: 8px;
    margin: -4px -4px 8px;
    /* overflow stays VISIBLE -- the wishlist/compare icons sit at
       right:-14px and must be allowed to overhang this box (setting
       overflow:hidden here clipped 14px off every icon on every card,
       measured live 2026-08-19). Clipping isn't needed for the image
       anyway: the img is position:absolute + inset:0, so it exactly
       fills this box and physically cannot overflow it; its own
       border-radius handles the rounded corners. */
    overflow: visible;
    box-sizing: border-box;
}
/* Restore the height chain the theme's injected divs break. Without
   these three rules the img has no sized ancestor to resolve against.
   NOTE: overflow MUST stay `visible` here. The wishlist/compare buttons
   (.b2b-card-actions) are absolutely positioned at right:-14px so they
   deliberately overhang the image edge -- an earlier version of this rule
   set overflow:hidden and clipped exactly that 14px off every icon on
   every card (measured live 2026-08-19: overhang 14px, clipped by
   .shoptimizer-plp-image-wrapper). The image is clipped by the WRAPPER's
   own overflow:hidden + border-radius instead, which sits above the
   icons' positioning context, so the square is still enforced. */
.b2b-shop-layout__grid .woocommerce-image__wrapper > a,
.b2b-shop-layout__grid .woocommerce-image__wrapper .shoptimizer-plp-carousel-container,
.b2b-shop-layout__grid .woocommerce-image__wrapper .shoptimizer-plp-image-wrapper {
    display: flex; align-items: center; justify-content: center;
    width: 100%; height: 100%;
    /* flex: 0 0 100% + min-width:0 is REQUIRED, not decorative.
       These elements are flex ITEMS of the wrapper. With the default
       `flex-shrink: 1` they compress below their declared width:100%
       whenever their content has no intrinsic width -- which is exactly
       what happens to AJAX-filtered results, because those images arrive
       with loading="lazy" and contribute zero intrinsic width until they
       download. Measured live 2026-08-19: after clicking a category
       filter every image collapsed 277px -> 138px (~half) and shifted
       right inside its centred box, while the wrapper itself stayed a
       correct 277px -- the collapse was happening at this <a>/div layer,
       not on the img. Pinning flex-basis to 100% and disabling shrink
       makes the chain hold its width regardless of image load state. */
    flex: 0 0 100%;
    min-width: 0;
    box-sizing: border-box;
    margin: 0; padding: 0;
    position: relative;
    overflow: visible;
}
/* The image FILLS its (now explicitly sized) box absolutely.
   position:absolute + inset:0 gives the img a DEFINITE size that does not
   depend on the file having loaded yet -- this is the critical difference
   from the previous attempt, which used width/height:auto. `auto` sizing
   means a lazy-loaded image has ZERO intrinsic size until it downloads, so
   inside a flex container it was given no space and the card rendered
   BLANK (confirmed live 2026-08-19: 13 of 16 first-page cards carry
   loading="lazy" and several rendered as empty boxes; the only reliably-
   rendering cards were the 3 eager ones). object-fit:contain then scales
   the loaded image inside that fixed box, so the whole garment is always
   visible, never cropped, and the box's size is identical on every card
   regardless of the image's own dimensions or load state. */
.b2b-shop-layout__grid .woocommerce-image__wrapper img {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    max-width: none; max-height: none;
    display: block;
    object-fit: contain; object-position: center;
    border-radius: 8px;
}

/* Neutralize Shoptimizer's own product-card hover effects (image
   scale-down, gallery-image cross-fade, opacity swap on secondary
   images -- main.css ~4494-4537) within our grid only -- per Victoria's
   2026-08-13 request that colors/swatches stay fully visible and
   unchanged on hover, not faded/scaled/swapped by the theme's default
   "flip to alternate photo" behavior. */
.b2b-shop-layout__grid ul.products li.product:hover img,
.b2b-shop-layout__grid ul.products li.product:hover .woocommerce-image__wrapper,
.b2b-shop-layout__grid ul.products li.product:hover .woocommerce-image__wrapper img {
    transform: none !important;
}
.b2b-shop-layout__grid ul.products li.product:hover .woocommerce-image__wrapper .gallery-image,
.b2b-shop-layout__grid ul.products li.product:hover .woocommerce-image__wrapper .gallery-image img {
    opacity: 0 !important;
}
.b2b-shop-layout__grid ul.products li.product:hover .b2b-card__swatch-dot,
.b2b-shop-layout__grid ul.products li.product:hover .b2b-card__tag,
.b2b-shop-layout__grid ul.products li.product:hover .b2b-card__swatch-more {
    opacity: 1 !important;
    filter: none !important;
}

/* Wishlist/compare actions -- overlaid top-right on the product image,
   heart above compare, per Victoria's 2026-08-15 reference screenshot
   (supersedes the 2026-08-14 in-flow-row placement). Anchored to
   .woocommerce-image__wrapper (position:relative, set above). */
.b2b-card-actions {
    /* right was previously clamped to 0 after a -6px value appeared to
       cause mobile horizontal scroll (2026-08-15) -- that scroll was later
       root-caused to two unrelated bugs (missing !important on the mobile
       grid-template-columns override, and a fixed header height that
       overflowed at mobile widths), both now fixed independently. Safe to
       push the icons back toward the card's outer edge again per Victoria's
       repeated request to move them further right, matching her original
       reference image. */
    position: absolute; top: 10px; right: -14px; z-index: 2;
    display: flex; flex-direction: column; gap: 12px;
}
.b2b-card-action {
    width: 32px; height: 32px; border-radius: 50%;
    background: #fff; border: 1px solid var(--b2b-shop-border); box-shadow: 0 1px 4px rgba(0,0,0,0.12);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; color: var(--b2b-shop-text); text-decoration: none;
    transition: background .15s, color .15s, border-color .15s;
}
.b2b-card-action svg { width: 16px; height: 16px; }
.b2b-card-action:hover { background: var(--b2b-shop-blue); border-color: var(--b2b-shop-blue); color: #fff; }
.b2b-card-action--wishlist.is-active { color: var(--b2b-shop-red); border-color: var(--b2b-shop-red); }
.b2b-card-action--wishlist.is-active:hover { background: var(--b2b-shop-red); border-color: var(--b2b-shop-red); color: #fff; }

/* Opt out of the site-wide hover glow (assets/css/site-global.css applies
   a drop-shadow to every a/button and their nested svg/img by default) --
   per Victoria's explicit request (2026-08-16), the product image and the
   wishlist/compare icon buttons on /shop/ cards should have no glow at
   all. Their own hover states above (blue/red fill swap) are unaffected;
   this only cancels the extra glow layered on top by the global rule.
   .woocommerce-image__wrapper's rule is scoped to .b2b-shop-layout__grid
   AND duplicated unscoped below, since this stylesheet now also loads on
   single product pages (2026-08-16, for the Related Products section,
   which reuses this exact same card markup but has no .b2b-shop-layout__
   grid ancestor -- confirmed via live HTML comparison). */
.b2b-shop-layout__grid .woocommerce-image__wrapper a:hover,
.woocommerce-image__wrapper a:hover,
.b2b-card-action:hover,
.b2b-card-action:focus-visible {
    filter: none;
}
.b2b-shop-layout__grid .woocommerce-image__wrapper a:hover svg,
.b2b-shop-layout__grid .woocommerce-image__wrapper a:hover img,
.woocommerce-image__wrapper a:hover svg,
.woocommerce-image__wrapper a:hover img,
.b2b-card-action:hover svg,
.b2b-card-action:focus-visible svg {
    filter: none;
}

.woocommerce-card__header { padding: 2px 0 0; }

/* Brand name + Eco badge, same line (2026-08-14 -- previously the Eco
   badge sat in the swatch row and could overflow the card's right edge). */
/* ══════════════════════════════════════════════════════════════════════
   CARD ROW HEIGHTS — FIXED, not minimum.
   Per Victoria's explicit 2026-08-19 requirement that every element in
   every product card be uniform ("consistent sizing, spacing, alignment,
   typography, padding, borders, row heights ... No individual table or
   table element should appear noticeably larger, smaller, misaligned").
   Every one of these rows previously used min-height, which only sets a
   FLOOR -- a longer title, a price RANGE ("$8.06 - $10.75") vs a single
   price, or a swatch row with a "+20" overflow label could each still
   grow its row taller on some cards than others, which is exactly the
   inconsistency visible in her screenshots. height + overflow:hidden
   pins each row to one exact size on every card, so all cards are
   structurally identical regardless of content length.
   ══════════════════════════════════════════════════════════════════════ */
.b2b-card__brand-row {
    display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0;
    height: 14px; line-height: 1; overflow: hidden;
}
.b2b-card__brand-row .b2b-card__brand { margin: 0; }

/* Color swatches row -- now sits below the price/actions instead of
   directly under the image (2026-08-14 layout request). */
/* Swatch row -- FIXED height (see row-heights banner above) so a card with
   many swatches + a "+20" overflow label is exactly as tall as a card with
   two swatches or none at all. */
.b2b-card__meta-row { display: flex; align-items: center; gap: 8px; height: 20px; min-width: 0; overflow: hidden; }
.b2b-card__swatches { display: flex; align-items: center; gap: 5px; min-width: 0; flex-wrap: nowrap; }
.b2b-card__swatch-dot {
    display: block; width: 18px; height: 18px; border-radius: 6px;
    border: 1px solid rgba(0,0,0,0.1); overflow: hidden;
    flex-shrink: 0;
}
.b2b-card__swatch-dot img { display: block; width: 100%; height: 100%; object-fit: cover; }
.b2b-card__swatch-more { font-size: 12px; color: var(--b2b-shop-muted); margin-left: 2px; flex-shrink: 0; white-space: nowrap; }
.b2b-card__tag {
    display: inline-flex; align-items: center; gap: 4px;
    background: #E7F7EE; color: #1EA64C;
    font-size: 11px; font-weight: 700;
    padding: 3px 9px; border-radius: 20px;
    white-space: nowrap; flex-shrink: 0;
}
.b2b-card__tag svg { width: 11px; height: 11px; flex-shrink: 0; }

.b2b-card__brand { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; color: var(--b2b-shop-blue); margin: 0; line-height: 1; }
.product__categories { display: none; } /* superseded by brand + style line, avoids redundant category text on every card */
.woocommerce-loop-product__title {
    /* Single line, ellipsis-truncated. FIXED height (see the row-heights
       banner above) so a long title can never make one card's title row
       taller than another's. */
    font-size: 14.5px; font-weight: 700; color: var(--b2b-shop-text); margin: 0 0 2px; line-height: 1.35;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    height: 20px;
}
.woocommerce-loop-product__title a { color: inherit; text-decoration: none; }
.woocommerce-loop-product__title a:hover { color: var(--b2b-shop-blue); }
.b2b-card__style {
    /* FIXED height + single-line truncation (see row-heights banner above) --
       a long style number can never push this row taller on one card. */
    font-size: 12px; color: var(--b2b-shop-muted); margin: 0 0 4px;
    height: 16px; line-height: 16px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.b2b-shop-layout__grid .price {
    /* FIXED height (see row-heights banner above). .price is an inline
       <span> by default, which ignores height entirely -- display:block
       is required for the height to apply at all. A price RANGE
       ("$8.06 - $10.75") and a single price now occupy the exact same
       vertical space on every card; nowrap+ellipsis stops a long range
       from wrapping to a second line and breaking alignment. */
    display: block; font-size: 15px; font-weight: 800; color: var(--b2b-shop-text);
    height: 20px; line-height: 20px; margin: 0 0 4px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.b2b-shop-layout__grid .price del { font-weight: 400; color: var(--b2b-shop-muted); font-size: 13px; margin-right: 6px; opacity: 1; }
.b2b-shop-layout__grid .price ins { text-decoration: none; }
.b2b-shop-layout__grid .onsale {
    position: absolute; top: 10px; left: 10px; z-index: 2;
    background: var(--b2b-shop-red); color: #fff; font-size: 11px; font-weight: 700;
    padding: 4px 10px; border-radius: 20px; text-transform: uppercase; letter-spacing: 0.03em;
    margin: 0; min-height: 0; min-width: 0; line-height: 1;
}

/* ── Pagination (reuses existing markup, minimal restyle) ── */
.b2b-shop-layout__grid .page-numbers { display: flex; align-items: center; gap: 6px; justify-content: center; margin-top: 32px; list-style: none; padding: 0; }
.b2b-shop-layout__grid .page-numbers li { display: inline-flex; }
.b2b-shop-layout__grid .page-numbers a,
.b2b-shop-layout__grid .page-numbers span {
    display: flex; align-items: center; justify-content: center;
    min-width: 34px; height: 34px; padding: 0 8px;
    border-radius: 7px; font-size: 13px; color: var(--b2b-shop-text); text-decoration: none;
}
.b2b-shop-layout__grid .page-numbers .current { background: var(--b2b-shop-blue); color: #fff; font-weight: 700; }
.b2b-shop-layout__grid .page-numbers a:hover { background: var(--b2b-shop-border); }

/* ── AJAX loading state ── */
.b2b-shop-layout__grid.is-loading { opacity: 0.5; pointer-events: none; transition: opacity .15s; }

/* ── Mobile ── */
@media (max-width: 992px) {
    .b2b-shop-toolbar__filter-toggle { display: flex; }
    .b2b-shop-layout { grid-template-columns: 1fr; }
    /* !important required here -- the base rule above
       (.b2b-shop-layout__grid ul.products) sets grid-template-columns with
       !important to beat Shoptimizer's own 3-class .columns-4 ul.products
       li.product selector, and a non-!important media-query override can
       never beat an !important rule regardless of source order or breakpoint
       specificity. Without this, mobile silently kept the desktop 3-column
       grid, which is the real root cause behind the whole 2026-08-15 mobile
       punch list (3rd card overflowing the viewport, page-wide horizontal
       scroll clipping the breadcrumb, and every alignment/sizing symptom
       that followed from cards being squeezed into a grid that didn't
       actually change at this breakpoint). */
    /* minmax(0, 1fr) not 1fr -- see the desktop grid rule's comment for why
       plain 1fr produces UNEQUAL tracks. Same fix required at every breakpoint. */
    .b2b-shop-layout__grid ul.products { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
    /* An odd-numbered trailing card (e.g. 3 products in a 2-column mobile
       grid) sits alone in the left column with a normal empty grid cell
       beside it. A prior spanning/centering rule here used a manual
       max-width calc() to make the lone card fill/center across both
       columns, but that calc() could drift from the grid's real computed
       column width and cause the card to render a different width/
       alignment than its siblings (reported 2026-08-15: brand/price rows
       and the card's own width didn't line up with the cards above it).
       Removed per Victoria's explicit choice -- the lone card now sizes
       itself the same way every other grid item does (1fr), guaranteeing
       identical width/alignment; it simply leaves a normal empty cell
       next to it rather than spanning. */

    .b2b-shop-filters {
        position: fixed; top: 0; left: 0; bottom: 0; z-index: 1000;
        width: 300px; max-width: 85vw; background: #fff;
        padding: 20px; overflow-y: auto;
        transform: translateX(-100%); transition: transform .25s ease;
        box-shadow: 2px 0 16px rgba(0,0,0,0.15);
    }
    .b2b-shop-filters.is-open { transform: translateX(0); }
    .b2b-shop-filters__head { display: flex; }
}
@media (max-width: 560px) {
    /* minmax(0, 1fr) not 1fr -- see the desktop grid rule's comment. */
    .b2b-shop-layout__grid ul.products { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important; gap: 14px !important; }
    .shoptimizer-archive .archive-header .woocommerce-breadcrumb { padding: 12px 16px; }
    .b2b-shop-header { padding: 20px 16px 4px; }
    .b2b-shop-toolbar { padding: 14px 16px; }
    .b2b-shop-layout { padding: 16px 16px 40px; }
}

#b2b-shop-filter-overlay {
    display: none;
    position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 999;
}
#b2b-shop-filter-overlay.is-open { display: block; }


/* ── Load More (replaces numbered pagination, 2026-08-20) ──────────────
   Matches the ssactivewear.com/ps/headwear reference Victoria supplied: a
   full-width pill button spanning the grid, with a count line above it.
   Uses the site's own --b2b-shop-blue rather than the reference's orange,
   per the standing rule that we match the reference's LAYOUT and behaviour
   while keeping this site's palette (same call as the swag.com filter
   sidebar work). */
/* Sits AFTER ul.products as a sibling, not inside the grid -- ul.products is
   itself the CSS grid, so no grid-column is involved here. */
.b2b-load-more {
    display: flex; flex-direction: column; align-items: center; gap: 14px;
    margin-top: 32px; padding-top: 4px; width: 100%;
}
.b2b-load-more__count {
    margin: 0; font-size: 14px; color: var(--b2b-shop-muted);
    font-variant-numeric: tabular-nums;
}
.b2b-load-more__count strong { color: var(--b2b-shop-text); font-weight: 700; }

.b2b-load-more__btn {
    position: relative;
    width: 100%; max-width: 100%;
    display: inline-flex; align-items: center; justify-content: center; gap: 10px;
    padding: 16px 28px;
    border: 0; border-radius: 999px;
    background: var(--b2b-shop-blue); color: #fff;
    font-size: 15px; font-weight: 700; letter-spacing: .01em;
    cursor: pointer;
    transition: background-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
.b2b-load-more__btn:hover { background: #0076b3; box-shadow: 0 4px 14px rgba(0,136,204,.28); }
.b2b-load-more__btn:active { transform: translateY(1px); }
.b2b-load-more__btn:focus-visible { outline: 2px solid var(--b2b-shop-text); outline-offset: 3px; }
.b2b-load-more__btn[disabled] { cursor: default; opacity: .85; box-shadow: none; }

/* Spinner only occupies space while loading, so the label stays centred
   in the resting state instead of sitting slightly off-axis. */
.b2b-load-more__spinner { display: none; }
.b2b-load-more__btn.is-loading .b2b-load-more__spinner {
    display: block;
    width: 16px; height: 16px; flex-shrink: 0;
    border: 2px solid rgba(255,255,255,.4);
    border-top-color: #fff;
    border-radius: 50%;
    animation: b2b-load-more-spin .6s linear infinite;
}
@keyframes b2b-load-more-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .b2b-load-more__btn.is-loading .b2b-load-more__spinner { animation-duration: 2s; }
    .b2b-load-more__btn { transition: none; }
}

@media (max-width: 560px) {
    .b2b-load-more { margin-top: 24px; }
    .b2b-load-more__btn { padding: 14px 20px; font-size: 14px; }
}