/* Materialize shim for the embedded ShaperBuddy screens.
 *
 * The ERP pages load materialize.min.css (142KB) — a framework with global
 * resets on html/body/a/ul/button that would flatten every cms2 theme on the
 * page. Its JAVASCRIPT is loaded (shopdetail.js calls the .tabs() plugin), but
 * the stylesheet is not, so the handful of classes the shop screens actually use
 * are reproduced here.
 *
 * Everything else the shop looks like comes from the ERP's real stylesheets,
 * rewritten to sit under .sb-shop by cms2_erp_scoped_css() and loaded after this
 * file — so these are only the base layer.
 *
 * Scoped to .sb-shop, which every embedded shop wrapper carries (the detail and
 * custom-order sections add a second, more specific class of their own).
 */
.sb-shop { position: relative; }

.sb-shop .btn {
    display: inline-block;
    height: 36px;
    line-height: 36px;
    padding: 0 16px;
    border: none;
    border-radius: 2px;
    background-color: #26a69a;
    color: #fff;
    font-size: 14px;
    text-align: center;
    text-transform: uppercase;
    letter-spacing: .5px;
    text-decoration: none;
    cursor: pointer;
    vertical-align: middle;
    -webkit-tap-highlight-color: transparent;
}
.sb-shop .btn.disabled { pointer-events: none; background-color: #dfdfdf; color: #9f9f9f; }

/* .waves-effect is a ripple animation in materialize; without its JS it is only
   ever a positioning context, so that is all it needs to be here. */
.sb-shop .waves-effect { position: relative; overflow: hidden; }

.sb-shop .row { margin-left: -.75rem; margin-right: -.75rem; }
.sb-shop .row::after { content: ""; display: table; clear: both; }
.sb-shop .row .col { float: left; box-sizing: border-box; padding: 0 .75rem; min-height: 1px; }
.sb-shop .row .col.s3 { width: 25%; }
.sb-shop .row .col.s12 { width: 100%; }

/* Tab strip on the detail page. Materialize's Tabs plugin owns which panel is
   visible (it sets display on the panels itself), so only the strip needs
   styling — but it needs enough of it not to render as a bullet list. */
.sb-shop ul.tabs {
    display: flex;
    margin: 0 0 16px;
    padding: 0;
    list-style: none;
    border-bottom: 1px solid #e0e0e0;
    /* Both axes, deliberately. Setting overflow-x alone leaves overflow-y
       computing to auto (the spec forces it once the other axis is not
       visible), and the strip overflows itself by a single pixel -- 44 against
       a 43 client height -- which was enough for a vertical scrollbar to sit
       down the side of the tabs. Materialize pins the y axis for the same
       reason. */
    overflow-x: auto;
    overflow-y: hidden;
}
.sb-shop ul.tabs > li.tab {
    float: none;
    flex: 0 0 auto;
    list-style: none;
    width: auto;
}
.sb-shop ul.tabs > li.tab > a {
    display: block;
    padding: 12px 18px;
    color: #666;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: .04em;
    text-decoration: none;
    white-space: nowrap;
}
.sb-shop ul.tabs > li.tab > a.active {
    color: #111;
    box-shadow: inset 0 -2px 0 currentColor;
}
.sb-shop .indicator { display: none; }

/* Quantity stepper next to add-to-cart: a bare number input otherwise. */
.sb-shop .sdad_addToCartNumberWrapper input,
.sb-shop .sdadatcn_input {
    /* Fills its wrapper, which is what the theme actually sizes. A fixed 48px
       was here once and got commented out, and nothing replaced it -- so the
       input fell back to the browser's intrinsic width for a text field and
       overflowed the wrapper it sits in: 215px inside 163px on the SFH detail
       page, spilling 26px past each side and leaving the box visibly left of
       the text below it. That intrinsic width is not the same in every engine,
       which is why the row looked different again in Safari. */
    width: 100%;
    height: 36px;
    box-sizing: border-box;
    padding: 0 8px;
    border: 1px solid #d5d5d5;
    border-radius: 2px;
    text-align: center;
}

.sb-shop-heading,
.sb-customorder-heading { margin: 0 0 1rem; }

/* Bootstrap modal base.
 *
 * checkout.php ships a `.modal.fade` shipping-quote dialog. Bootstrap's own
 * `display:none` for it lives in the FULL bootstrap stylesheet, which the ERP
 * loads elsewhere (per-site) — we only load bootstrap-grid, so without this the
 * dialog renders inline and pushes the checkout 50px wider than the viewport.
 *
 * Hiding it loses nothing: that dialog has no opener anywhere in the ERP tree,
 * and the handler behind it (checkout.js `.btn-applyshipping`) is dead code —
 * it calls an undefined `service_GetCart()` and reads the v1 `token` cookie.
 * Shipping is actually chosen on step 2 (checkoutfinal.php).
 */
.sb-shop .modal { display: none; }
.sb-shop .modal.show,
.sb-shop .modal.in { display: block; }

/* Table base, from materialize.min.css.
 *
 * Every brandsite gets these because it loads the full framework; we do not
 * (142KB with global resets — see the module notes). Their absence is what made
 * the surfboard "Board Dimensions" table collapse to its content width and
 * cluster at the left edge instead of spreading across the info column, while
 * thebasesurf.com renders the same markup as a full-width table.
 *
 * Scoped to .sb-shop, so a cms2 theme's own tables are untouched.
 */
.sb-shop table {
    width: 100%;
    display: table;
    border-collapse: collapse;
    border-spacing: 0;
}
.sb-shop td,
.sb-shop th {
    padding: 15px 5px;
    display: table-cell;
    text-align: left;
    vertical-align: middle;
    border-radius: 2px;
}

/* Content grid.
 *
 * The ERP fragments are written for a brandsite whose own theme wraps them in a
 * container (`global/shop/index.php` sandwiches them between header and footer,
 * and each brandsite caps the result — polensurfboards.com hardcodes 1440px on
 * this same wrapper). Dropped into a cms2 page there is no such wrapper, so the
 * shop ran edge to edge while the site's header, footer and sections stopped at
 * the content grid.
 *
 * Driven by --cms2-max-width, i.e. the admin's "Max site width", so the shop
 * lands on the same line as everything else. The +40 is the border-box rule from
 * the theme notes: these carry their own 20px gutter, so without it their text
 * edge would sit 20px inside every other section. Sites that never set a max
 * width get the 1280 fallback rather than a full-bleed shop, which is what the
 * brandsites do anyway.
 */
/* NOTE the `div.` on the overrides below. This file is emitted BEFORE the
 * scoped ERP stylesheets — deliberately, since most of it supplies the
 * materialize base those stylesheets expect to override. That order means a
 * rule here written as `.sb-shop .shop_filter` ties with the scoped
 * `.sb-shop .shop_filter` and LOSES on document order, silently. The extra
 * element name breaks the tie without reaching for !important. */
div.sb-shop .shopFilterWidget_wrapper,
div.sb-shop .shopDetail_wrapper,
div.sb-shop .sb-shop-heading {
    max-width: calc(var(--cms2-max-width, 1280px) + 40px);
    margin-inline: auto;
    box-sizing: border-box;
    margin-bottom: 20px;
}
div.sb-shop .shopFilterWidget_wrapper,
div.sb-shop .sb-shop-heading { padding-inline: 20px; }

/* shop.css pads the filter column `0 30px`, which would leave the sidebar 30px
 * right of the heading while the card grid (no padding of its own) sits exactly
 * on the line. The space becomes the gap between the two columns instead. */
div.sb-shop .shop_filter {
    padding-left: 0;
    padding-right: 40px;
}
/* Below 720px shop.css stacks the columns and hands .shopFilterWidget its own
 * `0 30px` while the wrapper is already supplying the gutter — the two would
 * add up and the shop would sit 30px inside the rest of the page. Stacked, the
 * sidebar has no neighbour to keep clear of either. */
div.sb-shop .shopFilterWidget { padding-inline: 0; }
@media (max-width: 720px) {
    div.sb-shop .shop_filter { padding-inline: 0; }
}

/* "Save X" badge, inserted by public_assets/erp_shop_save.js (opt-in per
 * section). The v1 shop emits this element server-side; v2 has no counterpart,
 * so nothing in the ERP stylesheets styles these classes and a plain selector is
 * safe here. Neutral on purpose — a site's custom_css gives it the brand look,
 * the way polensurfboards.com paints its own in yellow. */
.sb-shop .save_price_wrapper {
    display: inline-flex;
    align-items: baseline;
    gap: 2px;
    font-size: 13px;
    line-height: 1.4;
    padding: 1px 6px;
    white-space: nowrap;
}
.sb-shop .save_price_wrapper .generalPrice_wrapper {
    display: inline-flex;
    color: inherit;
    text-decoration: none;   /* the clone comes from the struck-through old price */
}

/* -- Loading spinner ------------------------------------------------------
 *
 * The ERP templates already emit Materialize's preloader markup ($preloaderBig
 * / $preloaderSmall from global/php/htmlsnippets.php, passed through by
 * erp_bridge.php), and shop.js and cart.js already show and hide it. Only the
 * shape was missing: the geometry and keyframes live in materialize.min.css,
 * which this project deliberately does not load (142KB, with global resets on
 * html/body/a/ul/button). The elements were rendering 0x0 with no animation.
 *
 * Drawn here as a plain ring rather than by reproducing that file's four-layer
 * clipped-arc spinner: one colour, a few lines, and nothing that has to stay in
 * sync with a vendor stylesheet. The layers Materialize would have animated are
 * hidden -- they are what carries its four-colour cycle, which a single-brand
 * shop has no use for.
 *
 * Reduced motion slows the spin rather than stopping it: a still ring would
 * read as a broken image, and the whole point is to say "working".
 */
div.sb-shop .preloader-wrapper {
    display: inline-block;
    position: relative;
    width: 48px;
    height: 48px;
}
div.sb-shop .preloader-wrapper.small { width: 32px; height: 32px; }
div.sb-shop .preloader-wrapper > * { display: none; }
div.sb-shop .preloader-wrapper::after {
    content: "";
    position: absolute;
    inset: 0;
    box-sizing: border-box;
    border: 3px solid rgba(18, 18, 18, .15);
    border-top-color: var(--main_color0, #be202e);
    border-radius: 50%;
    animation: cms2-erp-spin .75s linear infinite;
}
@keyframes cms2-erp-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    div.sb-shop .preloader-wrapper::after { animation-duration: 2.4s; }
}

/* Tab strip targets: keep the heading clear of a sticky header.
 *
 * The detail page's three tabs are ordinary in-page anchors
 * (`<a href="#paymentandsecurity">`) and all three panels are always visible —
 * materialize's plugin styles the strip, it does not scroll. So clicking a tab
 * is the browser's own fragment jump, which parks the target's TOP EDGE at
 * viewport y=0. `.cms2-header.cms2-sticky` (modules/menu/module.css) is pinned
 * there, so the heading landed behind it: measured on SFH at y=43 under an 81px
 * header, i.e. 38px of the title swallowed.
 *
 * scroll-margin-top is the fix rather than padding or a scroll handler — it
 * only moves where a scroll-into-view stops, changing no layout and needing no
 * JS. The default clears the header heights this theme actually uses (81px
 * desktop, 65px mobile) with room to spare; a theme with a taller header sets
 * --cms2-sticky-offset in its own CSS rather than restating this rule.
 *
 * NOT wired to sbcart.js's --sb-cart-header, which looks like the obvious
 * source: that property is set on the `.sb-cart` drawer element, not on the
 * root, so it does not inherit here and the reference would silently always
 * fall back. On a site whose header is not sticky this merely stops a little
 * higher, which costs nothing.
 */
.sb-shop .tabPage { scroll-margin-top: var(--cms2-sticky-offset, 96px); }

/* ── Mobile filter drawer (public_assets/erp_shop_filters.js) ─────────────
 *
 * The ERP shop has no mobile filter control: below 720px shop.css stacks the
 * columns, so 225px of filters sit above the first product. The script moves
 * the ERP's OWN `.shop_filter` off-canvas and adds a trigger — the element is
 * never rebuilt, so shop.js keeps its bindings and filtering, URL sync and the
 * back button all still work.
 *
 * Everything here is inside the phone breakpoint; desktop is untouched.
 */
.cms2-erpfilter-toggle,
.cms2-erpfilter-backdrop,
.cms2-erpfilter-close,
.cms2-erpfilter-dismiss,
.cms2-erpfilter-apply { display: none; }

/* Filter checkboxes and radios take the site's accent.
   NO fallback on the var, deliberately: when --cms2-accent is undefined the
   declaration is invalid at computed-value time and the control keeps the
   browser's own blue -- so a site that has not chosen an accent is untouched,
   which a literal fallback colour could not give us. `accent-color` also leaves
   the native control intact: no rebuilt box, so focus rings, the indeterminate
   state and high-contrast mode all still work. */
.sb-shop input[type="checkbox"],
.sb-shop input[type="radio"] { accent-color: var(--cms2-accent); }

@media (max-width: 820px) {
    /* Floating trigger, bottom centre. `fixed` rather than sticky so it stays
       reachable through a listing thousands of products long. */
    div.sb-shop .cms2-erpfilter-toggle {
        display: inline-flex; align-items: center; justify-content: center; gap: 10px;
        position: fixed; left: 50%; transform: translateX(-50%);
        bottom: calc(20px + var(--cms2-erpfilter-lift, 0px));
        z-index: 300;
        padding: 10px 26px;
        border: 0; border-radius: 100px;
        /* The site's accent, falling back to ink. A site that has set no shop
           palette keeps the neutral button it had; one that has takes its own
           colour, so the trigger and the apply button at the foot of the sheet
           are visibly the same action. */
        background: var(--main_color0, #121212); color: #fff;
        font: inherit; font-size: 14px; font-weight: 600; letter-spacing: .02em;
        text-transform: uppercase;
        /* The label is one line. It held a count badge that pushed it onto a
           second the moment a filter was picked, taking the bar from 49px to
           70px under the visitor's thumb; the badge is gone, but a long
           translation would do the same. */
        white-space: nowrap;
        box-shadow: 0 6px 20px rgba(0,0,0,.28);
        cursor: pointer;
    }

    /* The panel is a BOTTOM SHEET: it rises from the bottom edge rather than
       sliding in from the side. A thumb reaches the bottom of a phone; the far
       corner of the screen it does not.
     *
     * A COLUMN, so the sheet has the three bands the design asks for: a title
     * row that does not scroll, the groups scrolling between, and the apply
     * button always on screen at the foot. That is also what lets the close
     * control live in the title row -- as a plain child of a scrolling box it
     * would scroll away, which is why the first version had to float it above
     * the sheet instead.
     *
     * `max-height`, not `height`: with the close no longer floating above there
     * is nothing outside the sheet whose position has to be derived from it, so
     * a short filter set now hugs its content instead of padding out to 72vh. */
    div.sb-shop .shop_filter {
        display: flex; flex-direction: column;
        position: fixed; top: auto; z-index: 400;
        /* Inset on all four sides: the sheet is a panel over the page, not a
           tray welded to its edges, and the gap is what says so. The bottom
           inset stacks on the cookie-banner lift rather than replacing it. */
        left: var(--cms2-erpfilter-gap, 8px);
        right: var(--cms2-erpfilter-gap, 8px);
        bottom: calc(var(--cms2-erpfilter-lift, 0px) + var(--cms2-erpfilter-gap, 8px));
        /* The last term is the room left ABOVE the sheet, and it has to be
           real: the round dismiss floats 14px over the sheet's top edge, so
           24px reserved less than the button's own height and the control
           was pushed off the screen. Measured on SFH at 390x844 with the
           cookie banner up: the dismiss sat at y=-42, entirely outside the
           viewport, so the only ways out of the sheet were the backdrop and
           Show results. --cms2-erpfilter-top is written by the script as
           (site header bottom + 70), 70 being the 52px button, its 14px gap
           and 4px of air; a site with no header keeps the 70px default. */
        max-height: min(78vh, calc(100vh - var(--cms2-erpfilter-lift, 0px)
                                   - var(--cms2-erpfilter-gap, 8px)
                                   - var(--cms2-erpfilter-top, 70px)));
        width: auto; max-width: none;
        margin: 0; padding: 0;
        border-radius: 0;
        overflow: hidden;
        background: var(--cms2-erpfilter-bg, #fff);
        /* translate past its own bottom inset, or the sheet parks 8px up the
           screen while closed and its top edge shows over the products. */
        transform: translateY(calc(100% + var(--cms2-erpfilter-lift, 0px)
                                        + var(--cms2-erpfilter-gap, 8px)));
        transition: transform .3s ease;
    }
    div.sb-shop.cms2-erpfilter-open .shop_filter { transform: none; }

    /* Title row: fixed, and the close sits in it. */
    div.sb-shop .shop_filter > .filterGlobalHeader {
        flex: none; position: relative;
        margin: 0; padding: 18px 56px 16px 20px;
        /* The ERP's caret would collapse the whole panel inside a sheet that
           exists to show it, so the row is inert and our own control closes. */
        pointer-events: none;
    }
    div.sb-shop .shop_filter > .filterGlobalHeader > i { display: none; }

    div.sb-shop .cms2-erpfilter-close {
        display: flex; align-items: center; justify-content: center;
        position: absolute; top: 12px; right: 12px; z-index: 2;
        width: 36px; height: 36px; padding: 0;
        border: 0; border-radius: 50%;
        background: none; color: inherit;
        cursor: pointer; pointer-events: auto;   /* the inert row is its parent */
    }
    /* Matches the group carets exactly -- same font, same size. */
    div.sb-shop .cms2-erpfilter-close .material-icons {
        font-size: 24px; line-height: 1;
    }

    /* The groups are the only part that scrolls. `min-height: 0` is required:
       without it a flex item refuses to shrink below its content and the sheet
       grows past max-height instead of scrolling inside it. */
    div.sb-shop .shop_filter > .filterGlobalBody {
        flex: 1 1 auto; min-height: 0;
        overflow-y: auto; -webkit-overflow-scrolling: touch;
    }
    /* shop.js drives this element with an INLINE height, which nothing but
       !important can outrank. Confined to the open sheet, so the desktop
       accordion is untouched. */
    div.sb-shop.cms2-erpfilter-open .filterGlobalBody.collapseBody {
        height: auto !important;
        overflow-y: auto;
    }

    /* Full-width apply, always on screen at the foot of the sheet. */
    div.sb-shop .cms2-erpfilter-apply {
        display: block; flex: none;
        width: calc(100% - 32px); margin: 12px 16px 16px; padding: 10px 26px;
        border: 0; border-radius: 100px;
        background: var(--main_color0, #be202e); color: #fff;
        font: inherit; font-size: 14px; font-weight: 600; letter-spacing: .04em;
        text-transform: uppercase; cursor: pointer;
    }

    /* The round dismiss above the sheet, as the reference has it. Its bottom is
       derived from the sheet's MEASURED height (site.js writes
       --cms2-erpfilter-h), because the sheet is content-sized now and there is
       no constant to compute its top edge from. */
    div.sb-shop .cms2-erpfilter-dismiss {
        display: flex; align-items: center; justify-content: center;
        position: fixed; z-index: 401;
        left: 50%;
        bottom: calc(var(--cms2-erpfilter-lift, 0px) + var(--cms2-erpfilter-gap, 8px)
                     + var(--cms2-erpfilter-h, 60vh) + 14px);
        width: 52px; height: 52px; padding: 0;
        border: 0; border-radius: 50%;
        background: #fff; color: #6b6b6b;
        cursor: pointer;
        box-shadow: 0 4px 16px rgba(0,0,0,.18);
        opacity: 0; pointer-events: none;
        transform: translateX(-50%) scale(.7);
        transition: opacity .3s ease, transform .3s ease;
    }
    div.sb-shop.cms2-erpfilter-open .cms2-erpfilter-dismiss {
        opacity: 1; pointer-events: auto;
        transform: translateX(-50%) scale(1);
    }

    div.sb-shop .cms2-erpfilter-backdrop {
        display: block; position: fixed; inset: 0; z-index: 399;
        /* Blurred, so the sheet reads as glass over the listing rather than a
           card on a grey wash. The tint drops to .35 to compensate: at .5 the
           blur is doing its work on something already too dark to see. */
        background: rgba(0,0,0,.35); opacity: 0; pointer-events: none;
        -webkit-backdrop-filter: blur(8px);
        backdrop-filter: blur(8px);
        transition: opacity .3s ease;
    }
    div.sb-shop.cms2-erpfilter-open .cms2-erpfilter-backdrop { opacity: 1; pointer-events: auto; }

    /* The trigger steps aside while the sheet is up -- the sheet carries its own
       close and apply, and the two would otherwise stack. */
    div.sb-shop.cms2-erpfilter-open .cms2-erpfilter-toggle {
        opacity: 0; pointer-events: none;
    }
    div.sb-shop .cms2-erpfilter-toggle { transition: opacity .2s ease; }

    /* With the panel gone from the flow the grid takes the full width. */
    div.sb-shop .shop_articleList_wrapper { width: 100%; }
}


/* ── Quantity stepper (erp_shop_qty.js) ───────────────────────────────────
 * Only ever present when a section opted in AND the shop is not surfboards;
 * see modules/sbshopdetail/render.php.
 */
.sb-shop .cms2-qtyrow {
    display: flex;
    align-items: stretch;
    gap: 14px;
}
/* The button was a fixed 456px block on its own; in the row it takes the rest. */
.sb-shop .cms2-qtyrow .sdad_addToCartWrapper {
    flex: 1;
    min-width: 0;
    margin: 0;
}
.sb-shop .cms2-qty {
    flex: none;
    display: flex;
    align-items: center;
    border: 1px solid #dadada;
}
/* `height: 100%` was wrong here and produced a box half again as tall as the
   button beside it. The percentage resolves against the flex container, whose
   own height is still being decided by these very children -- so it falls back
   to auto and the ERP's own `input` height (76px, measured) wins instead.
   `align-self: stretch` asks the row for the height rather than the parent, and
   the row is as tall as the tallest item, which is the 50px buy button. */
.sb-shop .cms2-qty-btn {
    width: 40px; height: auto; align-self: stretch;
    padding: 0;
    border: 0;
    background: none;
    color: #121212;
    font: inherit; font-size: 18px; line-height: 1;
    cursor: pointer;
}
/* At the floor and the ceiling the button is spent; greying it says so before
   the visitor taps something that does nothing. */
.sb-shop .cms2-qty-btn:disabled { color: #c4c4c4; cursor: default; }
.sb-shop .cms2-qty-input {
    width: 44px; height: auto; min-height: 0; align-self: stretch;
    line-height: 1;
    padding: 0;
    border: 0;
    background: none;
    color: #121212;
    font: inherit; font-size: 16px; font-weight: 500;
    text-align: center;
    /* main.css styles bare inputs for the ERP's own forms -- underline, margin
       and a focus rule that would draw a line under the number. */
    box-shadow: none;
    margin: 0;
}
.sb-shop .cms2-qty-input:focus { outline: none; box-shadow: none; }

@media (max-width: 600px) {
    .sb-shop .cms2-qty-btn { width: 34px; }
    .sb-shop .cms2-qty-input { width: 34px; }
}

/* Keep the product grid's SPACE while the ERP hides it.
 *
 * shop.js reveals the list a full second after load:
 *
 *     function showFilterWidget(){ setTimeout(function(){
 *         $('.shop_articleList').hide();
 *         $('.shopFilterWidget').removeClass('sf_notInitiated');
 *         $('.shop_articleList').fadeIn(); ... }, 1000); }
 *
 * and until that runs, shop.css holds it at `display: none` through
 * `.sf_notInitiated .shop_articleList`. The cards are server-rendered and
 * present the whole time — they just occupy nothing. Traced on
 * /shop?shop=surfboards at 412px: the grid measures 0px with 49 cards in the
 * DOM from DOMContentLoaded until 1365ms, when it becomes 9806px and the
 * document goes 1484 -> 11289. The footer, visible on a page that short, is
 * thrown ten thousand pixels down, and that one event is CLS 0.670 of the
 * page's 0.698. The homepage, same footer, scores 0.008.
 *
 * `visibility: hidden` is the whole fix: the grid keeps its height from the
 * first paint, so nothing below it ever moves, while the visitor still sees
 * nothing until the ERP is ready — which is what the ERP wanted. hide() and
 * fadeIn() then run in one tick with no paint between them, so the fade still
 * happens and it happens in place.
 *
 * `display` has to be restated because `display:none` is what we are
 * overriding; flex, with the values .flexList sets in main.css.
 *
 * The extra `div` is the documented specificity trick for this file: it is
 * emitted BEFORE the scoped ERP stylesheets, so a rule of equal weight would
 * tie and lose on document order.
 *
 * Not fixed in shop.js, which is where the second timer lives, because global/
 * is symlinked into the live brandsites.
 */
div.sb-shop .sf_notInitiated .shop_articleList {
    display: flex;
    visibility: hidden;
}

/* ── Full-screen image viewer, phones only ────────────────────────────────
 *
 * Paired with public_assets/erp_shop_zoom.js, which explains why this exists:
 * the ERP's magnifier is a HOVER effect, so the phone had no way to enlarge a
 * product photo at all.
 *
 * Everything in the first block is inside `.sdImagePreview_mobile`, which
 * module_shop_1.css hides above 992px — so none of it can reach the desktop
 * page, and this file states no breakpoint of its own.
 *
 * The `div.` prefix on the ERP's own class names is the documented tie-break:
 * this stylesheet is emitted BEFORE the scoped ERP ones, so an equal-specificity
 * rule here loses on document order and silently does nothing.
 */
div.sb-shop .sdImagePreview_mobile .sdimg_image_mobile { position: relative; }

div.sb-shop .sdImagePreview_mobile .sb-zoom-btn {
    position: absolute;
    right: 12px;
    bottom: 12px;
    z-index: 3;
    display: none;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, .92);
    color: #121212;
    box-shadow: 0 1px 6px rgba(0, 0, 0, .22);
    cursor: pointer;
}
div.sb-shop .sdImagePreview_mobile.sb-zoom-on .sb-zoom-btn { display: flex; }

/* The counter replaces the dots once there are more images than dots can
   usefully represent; the script sets .sb-zoom-many above its own threshold. */
div.sb-shop .sdImagePreview_mobile .sb-zoom-count {
    position: absolute;
    left: 12px;
    bottom: 12px;
    z-index: 3;
    display: none;
    padding: 4px 10px;
    border-radius: 999px;
    background: rgba(18, 18, 18, .72);
    color: #fff;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .04em;
}
div.sb-shop .sdImagePreview_mobile.sb-zoom-many .sb-zoom-count { display: block; }
div.sb-shop .sdImagePreview_mobile.sb-zoom-many .splide__pagination { display: none; }

/* Even below that threshold the dots may never reflow the page: a row that
   wraps would push the price and the buy button down by its own height, and
   how many images a product has is the factory's choice, not ours. */
div.sb-shop .sdImagePreview_mobile .splide__pagination {
    flex-wrap: nowrap;
    overflow: hidden;
    max-width: 100%;
}

/* ── the viewer itself ─────────────────────────────────────────────────────
 * Appended to <body>, so it is outside .sb-shop and carries its own scope.
 * z-index clears the cookie banner (consent.css pins it at 900), which is the
 * only thing on the site that sits above the menu's 200 and the filter
 * drawer's 400 — a modal that a banner can cover is not a modal. */
.sb-zoom {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    flex-direction: column;
    background: #0d0d0d;
    overscroll-behavior: contain;
}
.sb-zoom[hidden] { display: none; }

.sb-zoom-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex: none;
    padding: 12px 14px;
    color: #fff;
}
.sb-zoom-index { font-size: 13px; font-weight: 600; letter-spacing: .04em; }
.sb-zoom-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, .12);
    color: #fff;
    cursor: pointer;
}

/* `touch-action: none` hands every gesture over the photo to the script. Without
   it the browser claims the pinch for its own page zoom and the two fight: the
   image scales while the whole document scales under it. */
.sb-zoom-stage {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
    overflow: hidden;
    touch-action: none;
}
.sb-zoom-img {
    max-width: 100%;
    max-height: 100%;
    transform-origin: center center;
    will-change: transform;
    user-select: none;
    -webkit-user-drag: none;
}

/* A scrolling strip, not dots, because this is the control that has to survive
   a product with thirty photos: it costs one row whatever the count, and the
   active thumb is scrolled into view rather than hunted for. */
.sb-zoom-thumbs {
    flex: none;
    display: flex;
    gap: 8px;
    padding: 12px 14px calc(12px + env(safe-area-inset-bottom, 0px));
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.sb-zoom-thumbs::-webkit-scrollbar { display: none; }
.sb-zoom-thumb {
    flex: 0 0 56px;
    height: 56px;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, .25);
    border-radius: 4px;
    background: #1a1a1a;
    overflow: hidden;
    cursor: pointer;
}
.sb-zoom-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sb-zoom-thumb.is-active { border-color: #fff; }

/* Panning is only possible once the photo is bigger than the stage, so the grab
   cursor should only appear then — and on a desktop browser narrowed for
   testing it is the one hint that the image can be moved. */
.sb-zoom.is-zoomed .sb-zoom-stage { cursor: grab; }

@media (prefers-reduced-motion: reduce) {
    .sb-zoom-thumbs { scroll-behavior: auto; }
}

/* Pagination dots, matched to onboardstore.com.au.
 *
 * The ERP draws them as 19px RINGS with a 2px border and 24px of vertical
 * margin -- large, outlined, and airy enough that two of them read as controls
 * rather than as a position indicator. The reference design uses small filled
 * squares: dark for the slide you are on, light grey for the rest.
 *
 * `!important` is unavoidable here, and that is the one place in this file where
 * it is. Every declaration in module_shop_1.css:3835 carries it, so the usual
 * tie-break of an extra element name cannot win on its own -- between two
 * important declarations specificity decides, and without `!important` on ours
 * we are not even in that comparison. The `div.sb-shop` prefix still earns the
 * specificity, since this file is emitted BEFORE the scoped ERP stylesheets and
 * would otherwise lose the tie on document order.
 *
 * Scoped to `.sdImagePreview_mobile`, so the desktop gallery -- which indicates
 * position with real thumbnails in `.sdimg_indicators_inner`, not with dots --
 * is not involved. */
div.sb-shop .sdImagePreview_mobile .splide__pagination__page {
    width: 10px !important;
    height: 10px !important;
    margin: 0 9px !important;
    border: 0 !important;
    border-radius: 0 !important;
    background-color: #d5d5d5 !important;
    opacity: 1 !important;
    transform: none !important;
}
div.sb-shop .sdImagePreview_mobile .splide__pagination__page.is-active {
    background-color: #121212 !important;
    border: 0 !important;
    transform: none !important;
}
/* The row lost 57px of height when the dots shrank, and it is positioned by a
   negative `bottom`, so without this it would sit that much lower. Re-aimed to
   keep the dots on the same optical line under the photo as before. */
div.sb-shop .sdImagePreview_mobile .splide__pagination { bottom: -27px !important; }
