/* ============================================================================
   Celora Classic — theme CSS.

   ⛔ THIS FILE IS INTENTIONALLY EMPTY, AND IT MUST STAY THAT WAY.

   The theme links the SHOP'S OWN BUILT STYLESHEETS, through the
   [%app_asset entry:'storefront' type:'css'/%] tag in layout/default.html.
   That resolves build/assets/api-*.css and build/assets/main-*.css out of
   Vite's manifest at render time — the very same two files the React
   storefront loads, in the same order.

   Which is the whole point of the port. Copying the shop's CSS in here would
   produce a second copy that drifted the first time anybody touched either
   side, and the themed page would slowly stop looking like the live page. A
   rule written here can only ever fight the real stylesheet.

   WHERE TO PUT A STYLE INSTEAD
     · restyling the shop     → the storefront's own CSS, and rebuild;
     · one-off tweak for THIS theme only → below, and keep it to a few lines;
     · a whole redesign       → duplicate the theme, then this file is yours.

   Anything you do add here loads AFTER the app stylesheets, so it wins on
   equal specificity. Use that sparingly.
   ========================================================================== */


/* ---------------------------------------------------------------------------
   Home hero -- device split (parity with resources/js/storefront/pages/Home.tsx,
   which renders exactly ONE rail per device in JS at a 767.98px matchMedia
   threshold). A server-rendered page cannot branch on the request's device, so
   the home template emits BOTH a desktop rail (all desktop/tablet banners, full
   dot count) and a mobile rail (the mobile banner) and this CSS shows exactly
   one at the SAME 767.98px breakpoint the React slider buckets on. The base
   .sf-hero / .sf-hero-slide rules (shipped in the storefront's main-*.css) still
   stack the slides and reveal only .is-active WITHIN whichever rail is shown, so
   the visible rail looks identical to React by construction. custom.js drives
   every .sf-hero it finds, so each rail auto-advances + pauses on hover
   independently (the one-slide mobile rail simply has nothing to advance).
   --------------------------------------------------------------------------- */
.sf-hero--mobile { display: none; }

@media (max-width: 767.98px) {
  .sf-hero--desktop { display: none; }
  .sf-hero--mobile { display: block; }
}


/* ---------------------------------------------------------------------------
   PDP anchor bar — where it STOPS sticking.

   templates/products/template.html wraps the anchor bar and the four sections
   it navigates (#description, #specifications, #delivery, #reviews) in
   .sf-pdp-tabbed. That wrapper is the sticky bar's containing block, and a
   `position: sticky` element releases at the bottom edge of its containing
   block — so the bar now unsticks exactly where Reviews ends instead of
   floating on over .sf-upsells and .sf-related, which it does not navigate to.

   The class is unknown to the shop's main-*.css, which is the point: it carries
   no appearance of its own and must not gain any. `display: block` is stated
   rather than left to the UA default so the wrapper can never be turned into a
   flex/grid/inline box by a later blanket rule — that would restyle the
   sections inside it, or (with a transform, filter or contain) re-anchor the
   sticky bar somewhere else entirely. It is a containing block, nothing more.
   --------------------------------------------------------------------------- */
.sf-pdp-tabbed { display: block; }


/* ---------------------------------------------------------------------------
   SCROLL CHROME — header logo shrink, and the back-to-top button.
   Added 2026-08-14. Append-only block; nothing above this line was touched.

   WHY IT IS HERE AND NOT IN THE SHOP'S CSS
     Both effects are driven by a class custom.js puts on elements this theme
     renders (.sf-header gets .is-stuck, .sf-totop gets .is-visible). The React
     storefront has neither behaviour, so there is nothing in main-*.css to
     override — these are additive rules, which is exactly the "one-off tweak
     for THIS theme only" case the header of this file allows.

   MODELLED ON www.toolsinabox.com.au (the owner's other live site)
     There, ba_custom.js adds .is-sticky to header.header as soon as
     scrollTop > 0, and style.css carries
         .header.is-sticky … .logo-pc { max-width:80%; height:auto;
                                        transition: all 1s ease-in-out;
                                        transition-delay: 500ms }
     That rule is INERT on the live site: .logo-link is a flex item with an
     auto width, so a percentage max-width has no definite containing block and
     resolves to `none`. Measured there, the logo stays 180x90 at every scroll
     position; what actually shrinks is the HEADER (192px -> 161px, because
     .is-sticky hides the announcement bar). So the 80% figure below is the
     reference site's INTENT, implemented in px so that it actually happens.
   --------------------------------------------------------------------------- */

/* -- 1. The logo shrink -----------------------------------------------------
   .sf-logo img is 180px wide and ALREADY carries `transition: width .35s
   ease-in-out` in the shop's main-*.css — the transition was shipped, only the
   state that uses it was missing. 180 -> 144 is the reference's 80%.

   Scoped to >=992px on purpose: below that breakpoint main-*.css sets
   .sf-header { position: static }, so the header scrolls away and a shrink
   there would animate an element nobody can see while reflowing the page under
   it. The reference site skips the effect below 768px for the same reason.
   Because the header is `position: sticky` (not fixed) nothing is overlapped
   when it gets shorter — the page reflows normally, no content hides. */
@media (min-width: 992px) {
  .sf-header.is-stuck .sf-logo img { width: 144px; }
  .sf-header.is-stuck .sf-header-inner { padding-top: 6px; padding-bottom: 6px; }
}
.sf-header-inner { transition: padding .35s ease-in-out; }

/* -- 2. Back-to-top ---------------------------------------------------------
   The owner's other site puts its .btn-back-to-top INSIDE the footer
   (position:absolute; right:30px; bottom:90px; 68x68 desktop, 39x39 below
   768px) so it is only reachable once you have already scrolled to the bottom.
   That is not much use, so this one is position:fixed and fades in past 400px,
   keeping their geometry (68px / right:30px) and their arrow glyph.

   z-index 1200 sits above .sf-header (1000) and below the design system's
   overlays; the drawer/lightbox/mini-cart all outrank it. */
.sf-totop {
  position: fixed;
  right: 30px;
  bottom: 30px;
  z-index: 1200;
  width: 68px;
  height: 68px;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  background: var(--sf-ink, #0e0c0c);
  color: #fff;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0, 0, 0, .28);
  /* Hidden state. visibility (not display) so the fade can run, and so the
     button is out of the tab order until it is actually on screen. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(12px);
  transition: opacity .25s ease-in-out, transform .25s ease-in-out,
              visibility 0s linear .25s, background-color .15s ease-in-out;
}
.sf-totop.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity .25s ease-in-out, transform .25s ease-in-out,
              visibility 0s, background-color .15s ease-in-out;
}
.sf-totop svg { width: 22px; height: 22px; display: block; }
.sf-totop:hover { background: var(--sf-lime, #a2ce46); color: var(--sf-ink, #0e0c0c); }
.sf-totop:focus-visible { outline: 3px solid var(--sf-lime, #a2ce46); outline-offset: 3px; }

/* Phones: smaller, and parked well clear of the bottom-right corner. The
   owner's other site pins a Podium "Text us" launcher there (a fixed iframe,
   right:0 bottom:0, 160x100, z-index 2147482997) and its own back-to-top
   collides with it. Celora carries no chat widget today, but the button is
   lifted to 88px so that adding one later does not bury it. */
@media (max-width: 767.98px) {
  .sf-totop { width: 44px; height: 44px; right: 12px; bottom: 88px; }
  .sf-totop svg { width: 16px; height: 16px; }
}

/* -- 3. Reduced motion ------------------------------------------------------
   No size animation, no fade, no slide — the states still change, instantly.
   custom.js separately drops the smooth scroll for the same users. */
@media (prefers-reduced-motion: reduce) {
  .sf-logo img,
  .sf-header-inner,
  .sf-totop,
  .sf-totop.is-visible { transition: none !important; }
  .sf-totop { transform: none; }
}


/* ---------------------------------------------------------------------------
   HOMEPAGE SEO SECTIONS — added 2026-08-14. Append-only block; nothing above
   this line was touched.

   These rules dress the sections templates/cms/home.template.html gained when
   the homepage was brought up to the CONTENT of the owner's other live site,
   www.toolsinabox.com.au: the intro, the "Why Choose ... Needs?" bullets, the
   "Explore Our Range Of Products" blurbs, the FAQ accordion, the CTA, the
   testimonials band and the "Popular Searches" list.

   THE POINT OF THIS BLOCK IS HOW LITTLE IS IN IT. Every new section is built
   out of classes the storefront's own main-*.css already ships — .sf-wrap for
   the container, .sf-content-body for body typography, .sf-h2 for the heading —
   so they inherit the site's fonts, colours and container width for free. What
   is left below is only what genuinely does not exist yet: a container padding
   nudge, a non-upper-casing heading variant, and the two disclosure widgets.

   ⛔ No --sf-* or --cds-* custom property is DEFINED anywhere here. Tokens are
   only ever read (with a literal fallback), never set. A theme-side :root block
   that pinned the responsive tokens broke the whole site's layout once already.
   --------------------------------------------------------------------------- */

/* -- 1. Container ----------------------------------------------------------
   .sf-wrap gives max-width:var(--sf-container) + margin-inline:auto, which is
   the same measure the category grid and the guarantee cards use. Its gutter is
   var(--sf-gutter) (12px) whereas .sf-explore pads 16px, so the text sections
   would sit 4px wider than the tiles they sit between; 16px lines them up.
   The bottom margin matches .sf-explore's 2.5rem so the vertical rhythm holds. */
.sf-home-copy { padding-inline: 16px; margin-bottom: 2.5rem; }

/* .sf-h2 upper-cases, which is right for POPULAR CATEGORIES, FEATURED PRODUCTS
   and WHAT OUR CUSTOMERS SAY ABOUT US — the reference site sets those in caps.
   The SEO copy headings are sentence/title case there ("Australia's Ultimate
   Ute Toolboxes", "FAQs"), so those carry this modifier and keep everything
   else .sf-h2 gives them: font, size, colour, centring, margins. */
.sf-h2--plain { text-transform: none; }

/* -- 2. Showcase photograph -------------------------------------------------
   Two 1000x1000 product shots, one under the category grid and one under the
   featured rail, mirroring the reference page. Centred, never wider than the
   file itself so it is not upscaled, and never wider than the column. */
.sf-home-shot { margin: 0 auto 2.5rem; padding-inline: 16px; text-align: center; }
.sf-home-shot img { max-width: min(100%, 1000px); height: auto; display: inline-block; }

/* -- 3. FAQ accordion -------------------------------------------------------
   A plain disclosure list. The <button> is the whole row: it is what receives
   focus and what carries aria-expanded, so keyboard and screen-reader support
   come from the element rather than from script. `font: inherit` keeps it in
   the site's typeface instead of the UA's form font. */
.sf-faq-list { border-top: 1px solid var(--sf-border, #e6e6e6); }
.sf-faq-item { border-bottom: 1px solid var(--sf-border, #e6e6e6); }
.sf-faq-q { margin: 0; font: inherit; }
.sf-faq-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  margin: 0;
  padding: 16px 2px;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  font-family: var(--sf-font-body, sans-serif);
  font-size: var(--sf-text-xl, 18px);
  font-weight: 600;
  line-height: 1.35;
  color: var(--sf-fg, #111);
}
.sf-faq-toggle:hover { color: var(--sf-green, #85ad3b); }
.sf-faq-toggle:focus-visible { outline: 3px solid var(--sf-lime, #a2ce46); outline-offset: 2px; }

/* The +/- mark: two bars, the vertical one rotated flat when the panel opens.
   aria-hidden in the markup, because aria-expanded already says the same thing. */
.sf-faq-icon { position: relative; flex: 0 0 auto; width: 14px; height: 14px; }
.sf-faq-icon::before,
.sf-faq-icon::after {
  content: "";
  position: absolute;
  top: 6px;
  left: 0;
  width: 14px;
  height: 2px;
  background: currentColor;
}
.sf-faq-icon::after { transform: rotate(90deg); transition: transform .2s ease-in-out; }
.sf-faq-toggle[aria-expanded="true"] .sf-faq-icon::after { transform: rotate(0deg); }

.sf-faq-a { padding: 0 2px 18px; }
/* Stated rather than left to the UA default so a later blanket `display` rule
   on .sf-content-body cannot leave a "closed" panel on screen. */
.sf-faq-a[hidden] { display: none; }
.sf-faq-a p { margin: 0; }

/* -- 4. Popular Searches ----------------------------------------------------
   Same disclosure mechanics, smaller. The toggle sits inside the .sf-h2 so it
   inherits the heading's font and centring; everything below is the chip list. */
.sf-popsearch { margin-bottom: 3rem; }
.sf-popsearch-toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-transform: inherit;
}
.sf-popsearch-toggle:focus-visible { outline: 3px solid var(--sf-lime, #a2ce46); outline-offset: 4px; }
.sf-popsearch-icon {
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 7px solid currentColor;
  transition: transform .2s ease-in-out;
}
.sf-popsearch-toggle[aria-expanded="true"] .sf-popsearch-icon { transform: rotate(180deg); }

.sf-popsearch-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px 10px;
  margin: 0;
  padding: 14px 0 0;
  list-style: none;
}
.sf-popsearch-list[hidden] { display: none; }
.sf-popsearch-list a {
  display: inline-block;
  padding: 6px 12px;
  border: 1px solid var(--sf-border, #e6e6e6);
  border-radius: 999px;
  font-size: var(--sf-text-sm, 13px);
  color: var(--sf-fg-soft, #333);
  text-decoration: none;
}
.sf-popsearch-list a:hover { border-color: var(--sf-green, #85ad3b); color: var(--sf-green, #85ad3b); }

/* -- 5. Testimonials --------------------------------------------------------
   The band is the `elfsight` content zone and the widget brings its own layout,
   so there is nothing to style but the room it sits in. */
.sf-testimonials { margin-top: 2.5rem; }

/* -- 6. Reduced motion ------------------------------------------------------
   The panels still open and close; the marks just stop animating. */
@media (prefers-reduced-motion: reduce) {
  .sf-faq-icon::after,
  .sf-popsearch-icon { transition: none; }
}


/* -- 7. CMS page media — the click-to-enlarge affordance ---------------------
   js/custom.js (cmsPageMedia) attaches the theme's own lightbox to the image
   and video thumbnails inside .sf-content-body on /gallery, /videos and
   /alarminstall. Everything the OPEN viewer looks like already comes from the
   shop's api-*.css (.cds-lightbox and friends) — the shared component. All that
   is needed here is (a) the cursor that says the thumbnail enlarges, (b) the
   play mark on a video thumbnail, and (c) the box the video frame sits in,
   which the image viewer has no equivalent of.

   ⚠ These rules only ever apply to elements custom.js has CLASSED. With
   JavaScript off no .sf-cmsplay / .sf-cmszoom exists, so none of this paints
   and the page looks exactly as it does today.
   --------------------------------------------------------------------------- */
.sf-content-body a.sf-cmszoom { cursor: zoom-in; }

/* inline-block keeps the anchor in the same inline flow it already has (the
   imported col-md-* classes are not styled by anything this theme loads), while
   giving ::after a box to centre on. */
.sf-content-body a.sf-cmsplay { position: relative; display: inline-block; cursor: pointer; }
.sf-content-body a.sf-cmsplay::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 54px;
  height: 54px;
  margin: -27px 0 0 -27px;
  border-radius: 50%;
  background: rgba(0, 0, 0, .55) no-repeat center/18px 20px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 18'%3E%3Cpath d='M1 1l14 8-14 8z' fill='%23fff'/%3E%3C/svg%3E");
  pointer-events: none;
  transition: background-color .16s ease;
}
.sf-content-body a.sf-cmsplay:hover::after { background-color: rgba(0, 0, 0, .78); }

@media (prefers-reduced-motion: reduce) {
  .sf-content-body a.sf-cmsplay::after { transition: none; }
}

/* -- the video modal --------------------------------------------------------
   .sf-cmsvideo IS a .cds-lightbox: backdrop, dialog grid, bar, round buttons
   and focus rings are all inherited. Only the stage differs, because a video
   frame is a fixed-ratio box rather than a zoomable photo. */
.sf-cmsvideo .sf-cmsvideo-stage {
  /* the image stage sets touch-action:none for pinch-zoom; the player needs
     its own touch handling back */
  touch-action: auto;
  padding: 0 calc(var(--sf-lb-ctrl-size, 48px) + var(--sf-lb-ctrl-gap, 12px));
}
@media (max-width: 599.98px) {
  /* on a phone the prev/next buttons sit at the BOTTOM of the stage (that is
     what api-*.css does), so the side gutters are not needed */
  .sf-cmsvideo .sf-cmsvideo-stage { padding: 0 0 calc(var(--sf-lb-ctrl-size, 48px) + var(--sf-lb-ctrl-gap, 12px)); }
}
.sf-cmsvideo .sf-cmsvideo-frame {
  position: relative;
  width: 100%;
  max-width: 1280px;
  max-height: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 10px 40px rgba(0, 0, 0, .5);
}
.sf-cmsvideo .sf-cmsvideo-frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
/* .cds-lightbox-dialog is pointer-events:none and hands it back per control
   (.cds-lb-btn does this itself); this link is a control too. */
.sf-cmsvideo .sf-cmsvideo-out {
  pointer-events: auto;
  color: var(--sf-lb-ctrl-fg, #fff);
  font-size: var(--sf-lb-counter-size, 13px);
  text-decoration: underline;
  text-underline-offset: 3px;
  white-space: nowrap;
  opacity: .85;
}
.sf-cmsvideo .sf-cmsvideo-out:hover { opacity: 1; }
.sf-cmsvideo .sf-cmsvideo-out:focus-visible {
  outline: 2px solid var(--sf-lb-ctrl-fg, #fff);
  outline-offset: 2px;
  border-radius: 4px;
}


/* ===========================================================================
   BOTTOM-CORNER FURNITURE, CAROUSEL CONTROLS AND THE IMPORTED CMS GRID
   — added 2026-08-14 (uifix). APPEND-ONLY: nothing above this line was edited.

   Four owner-reported problems, in order:
     8.  the "Text Us" widget is a full-bleed band on a phone, and the
         back-to-top button is buried underneath it
     9.  the featured rail cannot be dragged with a mouse
     10. the hero banner carousel has no prev/next arrows
     11. /gallery, /videos and /alarminstall stack every tile in one column

   ⛔ NO --sf-* OR --cds-* CUSTOM PROPERTY IS DEFINED ANYWHERE BELOW. Tokens are
      only ever READ, always with a literal fallback. A :root block in this file
      that pinned the responsive tokens broke the whole site's layout once.
   =========================================================================== */

/* -- 8. The "Text Us" widget vs the back-to-top button -----------------------

   WHERE THE WIDGET COMES FROM. It is not a third-party embed and it is not in
   the `scripts` table. It is Celora's own code:

       packages/comms/resources/widget/textus-widget.js
         served at  /comms/textus/widget.js
         injected   packages/comms/src/Http/Middleware/InjectTextUsWidget.php
                    (one deferred <script> appended before </body> on every
                     successful non-admin storefront HTML GET)
         switch     comms_settings → WidgetSettingsService::textUsEnabled()

   The "Podium" the rendered page mentions is only that file's own comments: its
   geometry was measured off the Podium launcher on the owner's Maropost shop
   and every borrowed number carries a "(podium)" note. No Podium script, iframe
   or asset is loaded.

   ⚠ THAT FILE IS OUT OF THIS AGENT'S SCOPE (packages/** is another owner's), so
     everything below CONSTRAINS the widget from the theme's own stylesheet
     rather than editing it. The rules are deliberately narrow: geometry only,
     nothing about its colours, copy or behaviour.

   ⚠ SPECIFICITY IS LOAD-BEARING. The widget injects its stylesheet as
     <style id="ctu-style"> appended to <head> at runtime — i.e. AFTER this
     file's <link>. An equal-specificity rule would lose on document order, so
     the selectors below deliberately out-specify its own:
         its phone rule   #ctu-greet                        (0,1,0)
         its scoped rules #celora-textus #ctu-greet         (0,2,0)
         ours             #celora-textus #ctu-collapsed #ctu-greet  (0,3,0)
     No !important is needed, and none is used. */

/* 8a. The greeting bubble, on phones.

   Its @media (max-width:480px) block pins the ROOT to both edges
   (left:16px; right:16px) and then stretches the collapsed stack and the
   greeting to width:100%. On a 390px phone that turns what is meant to be a
   corner bubble into a 358x96 band lying right across the middle of the page,
   with its "close" chip floating on bare page background 28px above it. That is
   the "text us doesn't work properly / the page isn't aligned" report: measured
   at 390px the page does NOT scroll sideways (scrollWidth - clientWidth = 0,
   html/body overflow-x:visible) — it is this band that reads as misalignment.

   Giving the bubble back a shrink-to-fit width is enough: #ctu-collapsed is
   already `align-items:flex-end`, so it returns to the corner by itself, at
   roughly the 290px the desktop rule uses.

   ⛔ #ctu-expanded / #ctu-panel ARE NOT TOUCHED. Their width:100% on phones is
      deliberate and load-bearing — see the widget's own note about the panel
      collapsing to 302px on a 343px phone if the stacks stop stretching. */
@media (max-width: 480px) {
  #celora-textus #ctu-collapsed #ctu-greet {
    width: auto;
    max-width: 300px;
    align-self: flex-end;
  }
}

/* 8b. Stacking.

   The widget root sits at z-index 2147483000. One above it guarantees the
   back-to-top button is hit-testable. It never COVERS the launcher, because
   custom.js positions the button clear of the launcher's measured rect and
   stows it entirely while the panel is open — see toTopClearance(). */
.sf-totop { z-index: 2147483001; }

/* 8c. Phones: back to the normal bottom line.

   The old `bottom: 88px` was a guess made before there was a widget to measure
   against ("lifted to 88px so that adding one later does not bury it"). It did
   not work: the launcher pill ends 59px up, but the GREETING sits above that,
   spanning 657-753px on an 844px-tall phone — and 88px put the button squarely
   inside it. elementFromPoint at the button's centre returned DIV#ctu-greet.

   The button now sits on the ordinary bottom line and custom.js slides it
   SIDEWAYS, past the launcher's real measured width, whenever a widget is
   actually present. With no widget on the page it simply stays in the corner. */
@media (max-width: 767.98px) {
  .sf-totop { right: 12px; bottom: 16px; }
}

/* 8d. Out of the way while the panel is open.

   An open Text Us panel is 340px wide and at least 640px tall — on a phone that
   is most of the screen. There is no honest place for a back-to-top button
   inside that, so it steps out and comes back when the panel closes. Same
   specificity as `.sf-totop.is-visible` and later in the file, so it wins. */
.sf-totop.sf-totop--stowed {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}


/* -- 9. Featured rail: grab-and-pull ----------------------------------------
   The behaviour is in custom.js (featuredRailDrag). All this contributes is the
   cursor, and the two things that have to stop while a drag is in progress.

   The cursor is behind (hover:hover) and (pointer:fine) so a touch device never
   sees a grab cursor for a gesture it does not use — custom.js likewise ignores
   touch pointers so native momentum scrolling is untouched. */
@media (hover: hover) and (pointer: fine) {
  .sf-featured-rail { cursor: grab; }
  .sf-featured-rail.is-dragging { cursor: grabbing; }
}

/* Selection and native image-dragging both fight a pointer drag; neither is
   suppressed except while one is actually running. */
.sf-featured-rail.is-dragging {
  -webkit-user-select: none;
  user-select: none;
}
.sf-featured-rail.is-dragging a,
.sf-featured-rail.is-dragging img {
  -webkit-user-drag: none;
}

/* ⭐ SCROLL-SNAP IS SUSPENDED FOR THE DURATION OF THE DRAG, AND ONLY THAT.
   With `scroll-snap-type: x mandatory` still on, every scrollLeft the drag
   writes is immediately pulled back to the nearest card and the rail judders
   instead of following the pointer. Dropping the class on pointerup restores
   mandatory snapping, which is what makes the rail settle onto a card at the
   end of the gesture. (0,2,0) beats main-*.css's `.sf-featured-rail` (0,1,0). */
.sf-featured-rail.is-dragging {
  scroll-snap-type: none;
  scroll-behavior: auto;
}


/* -- 10. Carousel arrows: hero banner AND featured products -----------------
   ONE definition for BOTH home carousels. The buttons come from
   templates/includes/blocks/hero.template.html and .../featured.template.html
   and are driven by the hero() and featured() modules in js/custom.js.

   ⭐ THE OWNER ASKED FOR THEM TO MATCH, VERBATIM (2026-08-19): "for the
     featured products arrow on the homepage can you make it the same arrows as
     the hero banner. i want them consistantly the same."

   This block used to carry the OPPOSITE instruction - that the hero was
   deliberately a translucent white disc with an SVG chevron while
   .sf-featured-nav stayed a solid #f2f2f2 disc with a text ‹ ›. That was a
   reading of an earlier request, made before the two had ever been seen on the
   same screen. They have now, and the answer is one arrow.

   ⚠ A SHARED SELECTOR LIST, NOT A SECOND COPY OF THE DECLARATIONS. Every
     appearance rule below names both classes. Two identical blocks is exactly
     how these drifted apart the first time: somebody tunes the hero and nobody
     remembers the rail. Change a number here and both carousels move, which is
     the thing the owner actually asked for.

   ⚠ WHAT IS DELIBERATELY NOT SHARED, AND WHY. Three properties stay
     per-carousel because they say WHERE the button sits, not what it looks
     like, and the two sit in different boxes:
       top        the hero centres on a banner (50%); the rail sits a little
                  high (--sf-featured-nav-top, 44%) so the disc lines up with
                  the product PHOTO rather than the middle of a card that also
                  carries a title and a price.
       left/right the hero is full-bleed with overflow:hidden and would CLIP an
                  overhanging disc, so it sits 16px inside. The rail hangs its
                  discs off the edge of the card row (--sf-featured-nav-offset,
                  -25px) so they never cover a product image.
       z-index    4 over the hero's stacking slides, 5 over the rail.
     The rail's three remain Theme Design tokens; the arrow's SIZE and COLOUR
     tokens were removed from css/style.info.txt when this merge landed, because
     a knob that moves one carousel and not the other is how they drift.

   ⚠ baseline.css ALSO DEFINES .sf-featured-nav - it is the frozen copy of the
     built storefront stylesheet, and it still carries the old solid-grey rules.
     It loads FIRST, so these same-specificity rules win on source order alone.
     If they ever stop winning, check the load order in the layout; do not
     "fix" it with !important.

   ⚠ THE TWO SHADOWS ARE WHAT MAKE A WHITE-ON-TRANSLUCENT CONTROL SURVIVE A
     PALE BACKGROUND, and both carousels have one. The hero photos run from
     near-black workshop shots to bright white-background product shots, and the
     featured strip lays a 0.8-white wash (--sf-featured-inner-bg) over its own
     photograph. A white chevron on a 25% white disc disappears on either
     without them: the drop-shadow on the SVG outlines the chevron and the
     box-shadow outlines the disc, both black at low alpha. Do not drop them
     "because the blur is enough" - the blur is the first thing a browser
     ignores. */
.sf-hero-nav,
.sf-featured-nav {
  position: absolute;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Never below 44px in either direction, at any breakpoint: this is a control
     a thumb has to find on a moving banner. */
  width: 56px;
  height: 56px;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  /* ⭐ TRANSLUCENT WHITE, NOT SOLID - the owner's instruction, verbatim: "make
     the hero banner arrows not a white background button, make it like a
     transparent white, its nicer". The photograph reads through the disc. */
  background: rgba(255, 255, 255, .25);
  color: #fff;
  font-size: 34px;
  line-height: 1;
  /* Explicit, because baseline.css fades the rail's arrows to .92 on a phone
     and the hero's stay solid. See the phone block at the foot of this
     section. */
  opacity: 1;
  cursor: pointer;
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .7);
  box-shadow: 0 1px 8px rgba(0, 0, 0, .3);
  transition: background-color .15s ease-in-out;
}

/* Placement - the per-carousel half. See the note above before touching these. */
.sf-hero-nav { top: 50%; z-index: 4; }
.sf-hero-nav--prev { left: 16px; }
.sf-hero-nav--next { right: 16px; }

.sf-featured-nav { top: var(--sf-featured-nav-top, 44%); z-index: 5; }
.sf-featured-nav--prev { left: var(--sf-featured-nav-offset, -25px); }
.sf-featured-nav--next { right: var(--sf-featured-nav-offset, -25px); }

.sf-hero-nav:hover,
.sf-featured-nav:hover { background: rgba(255, 255, 255, .4); }

/* backdrop-filter is the one property here that is genuinely optional; where it
   is unsupported the disc leans a little harder on its own fill instead. */
@supports not ((-webkit-backdrop-filter: blur(2px)) or (backdrop-filter: blur(2px))) {
  .sf-hero-nav,
  .sf-featured-nav { background: rgba(255, 255, 255, .34); }
  .sf-hero-nav:hover,
  .sf-featured-nav:hover { background: rgba(255, 255, 255, .48); }
}

/* The focus ring is the theme's lime rather than white: a white ring on a
   translucent white disc over a pale banner is three invisible things stacked.
   Lime is the ring .sf-totop already uses. This replaces the rail's old 2px
   --sf-green ring, which was chosen to sit against a solid grey disc. */
.sf-hero-nav:focus-visible,
.sf-featured-nav:focus-visible {
  outline: 3px solid var(--sf-lime, #a2ce46);
  outline-offset: 3px;
}

/* NOTHING TO PAGE - the two carousels say so differently, and that is correct.
   A single-slide hero has no second banner to reach ever, so custom.js sets
   [hidden] on both buttons and they leave the layout; display:flex above would
   otherwise ignore it. A rail can run out of scroll at ONE end while the other
   direction is still live, so featured() flips aria-disabled per button and
   baseline.css fades that one out at (0,2,0) - higher than anything here, so it
   still works and is not restated. Both end up invisible; only the hero's is a
   permanent condition. On this homepage the rail loops, so neither of its
   arrows is ever disabled. */
.sf-hero-nav[hidden],
.sf-featured-nav[hidden] { display: none; }

/* Phones: a smaller disc, but still a 44px target - and the SAME translucent
   treatment, not a second look for phones. The hero's dots strip is at
   bottom:14px and the arrows are vertically centred, so the two never meet.

   ⚠ opacity AND box-shadow ARE RESTATED HERE ON PURPOSE. baseline.css's own
     phone rule for .sf-featured-nav sets opacity:.92 and a lighter
     0 1px 4px shadow. Without these two lines the rail's arrows would go
     slightly transparent and lose their outline on a phone while the hero's did
     not - the exact drift this section exists to stop. */
@media (max-width: 767.98px) {
  .sf-hero-nav,
  .sf-featured-nav {
    width: 44px;
    height: 44px;
    font-size: 26px;
    opacity: 1;
    box-shadow: 0 1px 8px rgba(0, 0, 0, .3);
  }
  .sf-hero-nav--prev { left: 8px; }
  .sf-hero-nav--next { right: 8px; }
  /* The rail tucks its arrows INSIDE on a phone (2px, not -25px): there is no
     room beside the cards at 375px. That is placement, so it stays per-rail. */
  .sf-featured-nav--prev { left: var(--sf-featured-nav-offset-phone, 2px); }
  .sf-featured-nav--next { right: var(--sf-featured-nav-offset-phone, 2px); }
}

@media (prefers-reduced-motion: reduce) {
  .sf-hero-nav,
  .sf-featured-nav { transition: none; }
}


/* -- 11. Imported Bootstrap page bodies -------------------------------------
   /gallery (179 tiles), /videos (12) and /alarminstall (1) came across from the
   old Bootstrap-based site. Their stored markup is, verbatim:

       <div class="row align-items-center">
         <div class="videopage col-md-3 mb-3">
           <a href="…N1.jpg"><img class="img-fluid" src="…small/N1.jpg"></a>

   and on /videos the <a> ITSELF carries the column classes, with a plain <img>
   that has no .img-fluid on it at all. Not one of row / col-md-3 / mb-3 /
   img-fluid / videopage is styled by anything this theme loads — 0 hits across
   main-*.css, api-*.css and this file — so every tile was a full-width block
   and /gallery was a 179-item single column.

   ⚠ THIS IS NOT A BOOTSTRAP GRID AND MUST NOT GROW INTO ONE. It is scoped to
     .sf-content-body — the div a CMS page body renders into, see
     templates/cms/default.template.html — so it cannot reach the storefront.
     Four CMS pages contain a .row at all (ids 5, 10, 13, 15); the FAQ answers
     and the home zone also use .sf-content-body but carry no .row, so nothing
     there changes.

   Column counts are explicit steps rather than auto-fill, so the result is the
   same number every time and can be asserted: 2 across on a phone, 3 on a
   tablet, 4 on a desktop. */
.sf-content-body .row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
}
@media (min-width: 576px) {
  .sf-content-body .row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 992px) {
  .sf-content-body .row { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; }
}

/* The cell. min-width:0 stops a wide image from forcing its track past 1fr —
   the default `min-width:auto` on a grid item is what makes "responsive" grids
   overflow. `.mb-3` is unstyled, so there is no stray margin to clear, but the
   reset is cheap and makes the gap the only spacing. */
.sf-content-body .row > * {
  min-width: 0;
  margin: 0;
}

/* The anchor is the tile. On /gallery it is inside the cell div, on /videos it
   IS the cell — `display:block` covers both and gives the play badge that
   cmsPageMedia() adds (section 7) a box exactly the size of the thumbnail. */
.sf-content-body .row a {
  display: block;
  max-width: 100%;
}

/* Uniform tiles. The imported photos are a mix of portrait and landscape and a
   ragged grid looks like a bug; a fixed ratio with object-fit:cover is what
   makes 179 of them read as a gallery. Keyed off .row rather than .img-fluid
   because /videos' thumbnails do not carry that class. */
.sf-content-body .row img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 6px;
  background: #f2f2f2;
}

/* .img-fluid outside a .row (should it ever appear) just needs to not overflow. */
.sf-content-body .img-fluid { max-width: 100%; height: auto; }

/* -- 12. The one thing the raised z-index in section 8b costs ----------------
   .sf-totop sits at 2147483001 so it can never be buried by the Text Us widget
   (2147483000). That is above EVERYTHING, including the shop's own full-screen
   overlays: .cds-lightbox is z-index 2000 and the notify modal is in the same
   band. Left alone, a back-to-top button would float on top of an open product
   image viewer.

   Both of those lock the page with a class on <body> when they open, which is
   the signal used here. Same treatment as the chat panel in 8d: the button
   steps out rather than competing for the corner.

   (0,2,1) — beats `.sf-totop.is-visible` (0,2,0) regardless of file order.  */
body.cds-lightbox-open .sf-totop,
body.sf-modal-open .sf-totop {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* -- 13. Hero banner crossfade ------------------------------------------------
   The owner asked for "a nice transition between each banner — at the moment it
   just changes".

   ⛔ WHY THIS COULD NOT BE DONE WITH A TRANSITION ALONE. The shipped stylesheet
   swaps slides with `.sf-hero-slide{display:none}` / `.is-active{display:block}`,
   and `display` is not an animatable property — the browser snaps between the two
   states with nothing to interpolate. So the slides are STACKED instead (one grid
   cell, every slide in it) and cross-faded on opacity, which does animate.

   The rails are re-declared as `display:grid` here rather than `.sf-hero` being
   set once, because the 767.98px device split above keys on the SAME specificity
   (`.sf-hero--mobile{display:none}`) — a blanket `.sf-hero{display:grid}` would
   land later in the file and switch the hidden rail back on at every width.

   Dots (`position:absolute`) and the arrows (`.sf-hero-nav`, `position:absolute`)
   are out of flow, so they are not grid items and keep sitting over the banner.

   `pointer-events` is the non-obvious half: a faded-out slide is still laid out
   on top of the stack, and its <a> would otherwise swallow clicks meant for the
   visible banner. Only the active slide is clickable. */
.sf-hero--desktop { display: grid; }
.sf-hero--mobile  { display: none; }

@media (max-width: 767.98px) {
  .sf-hero--desktop { display: none; }
  .sf-hero--mobile  { display: grid; }
}

.sf-hero-slide {
  grid-area: 1 / 1;
  display: block;
  opacity: 0;
  pointer-events: none;
  z-index: 0;
  transition: opacity .7s ease-in-out;
}

.sf-hero-slide.is-active {
  display: block;
  opacity: 1;
  pointer-events: auto;
  z-index: 1;
}

/* ⭐ THE DOTS MUST OUTRANK THE SLIDE THEY SIT ON — added 2026-08-19 (herodots).

   Owner: "the hero banner dots disapears and then appears during the trasition
   to the next banner, can you make sure they are always visible".

   MEASURED, NOT GUESSED. document.elementsFromPoint() at the centre of the
   active dot, on the live home page at 1280px, returned top-first:

       IMG > A > DIV.sf-hero-slide.is-active > BUTTON.is-active > DIV.sf-hero-dots

   The banner image was painting OVER the dots, not under them.

   WHY. The crossfade block above gives `.sf-hero-slide.is-active` a `z-index: 1`.
   The slides are GRID ITEMS, and z-index applies to a grid item even though it
   is position:static — so the active slide becomes a stacking context painted in
   the positive-z-index layer. `.sf-hero-dots` is position:absolute with no
   z-index at all, so it paints in the z-index:auto layer, which is BELOW that.

   So the dots were not flickering off during the transition; they were hidden at
   REST and only showed THROUGH the incoming slide while its opacity was still
   under 1. Freezing the crossfade at opacity .5 brought all six back, active dot
   lime — which is the "appears during the transition" the owner described.

   ⚠ THIS IS THE FIX `.sf-hero-nav` ALREADY HAD. The arrows were added later with
     `z-index: 4` and so were never affected — which is exactly why the arrows sat
     over the banner while the older dots did not. The `anodised` and `hi-vis`
     themes likewise pair their own `z-index: 1` slide with a `.sf-hero-dots`
     `z-index: 3`; celora-classic is the only theme that missed it.

   One declaration. Nothing about the dots' look, size, position or the
   active-dot highlight changes, and it covers BOTH the desktop and the mobile
   rail because the selector names neither. */
.sf-hero-dots { z-index: 4; }
/* A banner that dissolves is decoration; honour a reader who has asked for less
   motion by going back to an instant swap. */
@media (prefers-reduced-motion: reduce) {
  .sf-hero-slide { transition: none; }
}


/* ---------------------------------------------------------------------------
   ANNOUNCEMENT BAR: HIDE ON SCROLL, RETURN AT THE TOP.
   Added 2026-08-14. Append-only block; nothing above this line was touched.

   Owner: "for the announcement bar on the header sf-announce can you make this
   hide when you scroll down and appear when back to the top of the page?"

   THERE IS DELIBERATELY NO RULE ON .sf-announce ITSELF. The bar is the first
   child of .sf-header, and main-*.css pins that header with
   `position: sticky; top: 0` - so the bar is inside the sticky box, not a strip
   above it, and collapsing it would shorten the header's flow box under the
   reader's feet and shove the whole document up ~39px.

   custom.js instead slides the header's sticky offset to minus the bar's
   measured height, which parks the bar above the top edge of the screen and
   moves NOTHING in the layout: the flow box, the document height and every
   element below the header all stay exactly where they were. See the long note
   in the SCROLL CHROME block of custom.js for the arithmetic that shows this
   can never open a gap under the header.

   All this file contributes is the animation between the two offsets, and the
   class hook. The distance itself is inline, because it is measured.
   --------------------------------------------------------------------------- */

/* Note the .is-announce-ready qualifier. custom.js adds that class two frames
   after it has set the opening state, so a page that opens part-scrolled starts
   with the bar already gone instead of animating it away in front of the
   reader. Everything after that first paint animates normally. */
.sf-header.is-announce-ready { transition: top .28s ease-in-out; }

/* Below 992px main-*.css makes the header `position: static`, where `top` has
   no effect at all and the whole masthead scrolls away by itself - so there is
   nothing to switch off for phones, and custom.js writes no offset there.

   --- Reduced motion --------------------------------------------------------
   The bar still hides and still comes back; it just does it instantly. */
@media (prefers-reduced-motion: reduce) {
  .sf-header,
  .sf-header.is-announce-ready { transition: none; }
}


/* ---------------------------------------------------------------------------
   BACK-TO-TOP, SECOND PASS: SMALLER AND STACKED ABOVE THE "TEXT US" LAUNCHER.
   Added 2026-08-14. Append-only block; nothing above this line was touched.

   Owner: "make the scroll back to the top of page look a bit smaller and on top
   of the Text us button on the top right so it looks nice and clear, not so
   bulky. And it's in a better position as well."

   These rules OVERRIDE the .sf-totop block earlier in this file (same
   specificity, later in source order). That block is left in place rather than
   rewritten so the reasoning behind the original geometry - and the reference
   site it came from - is still readable above.

   THE VISIBLE DISC IS A PSEUDO-ELEMENT, AND THAT IS THE ACCESSIBILITY POINT.
   "Smaller" must not mean "harder to hit". The <button> keeps a 52px box on
   desktop and 46px on phones - both over the 44px minimum tap target - and the
   circle the eye sees is drawn by ::before inset 3px inside it: 46px and 40px
   respectively, down from the 68px/44px discs. So the button lost 22px of bulk
   and GAINED 2px of tap target on a phone.

   custom.js measures the BUTTON's box when it places it against the launcher,
   so the gaps it keeps are gaps for a finger, not just for the eye.
   --------------------------------------------------------------------------- */

.sf-totop {
  width: 52px;
  height: 52px;
  /* The colour and the shadow move to ::before. The button itself becomes the
     invisible padding around the disc. */
  background: transparent;
  box-shadow: none;
}

.sf-totop::before {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 999px;
  background: var(--sf-ink, #0e0c0c);
  /* Softer and tighter than the old 0 4px 16px rgba(0,0,0,.28): enough to lift
     the disc off a photo, not enough to read as a heavy floating slab. */
  box-shadow: 0 2px 8px rgba(0, 0, 0, .18);
  transition: background-color .15s ease-in-out;
}

/* Above the disc, and thinner: 15px inside a 46px circle instead of 22 inside
   68, which is what actually makes it read as light. */
.sf-totop svg {
  position: relative;
  z-index: 1;
  width: 15px;
  height: 15px;
}

/* The hover colour belongs to the disc now; the button behind it stays clear. */
.sf-totop:hover { background: transparent; }
.sf-totop:hover::before { background: var(--sf-lime, #a2ce46); }

/* The focus ring hugs the button box, which is 3px outside the disc - without
   this it would sit at the old offset and float well clear of the circle. */
.sf-totop:focus-visible { outline-offset: 0; }

@media (max-width: 767.98px) {
  .sf-totop { width: 46px; height: 46px; }
  .sf-totop svg { width: 14px; height: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .sf-totop::before { transition: none; }
}

/* -- 12. HOME: paired text + image rows (.sf-textimg) ------------------------
   www.toolsinabox.com.au does NOT print its two showcase photographs as centred
   full-width blocks. Each one is the RIGHT-HAND HALF of a two-column row whose
   left half is the matching copy (.layout-wrapper.text-image over there).

   Measured off the live reference 2026-08-14 — its own rule is:
       grid-template-columns: 1fr 1fr;  gap: 2rem;
       .text-container  { order: 1 }            -> LEFT
       .image-container { order: 2 }            -> RIGHT
       @media (max-width: 768px) { one column; image order:1, text order:2 }
   so on a phone the PHOTO comes FIRST and the copy sits under it. This block
   reproduces that, fluidly — 1fr 1fr, never a hard-coded column width. The
   container width and gutter still come from .sf-wrap/.sf-home-copy, so these
   rows line up with the category tiles above them.

   ⚠ The media div is FIRST in the template source but `order:2` draws it on the
   right. That mirrors the reference's own source order. Changing either the
   markup order or these `order` values flips the desktop layout.

   ⛔ Reads tokens only (with literal fallbacks) — defines none. */
.sf-textimg {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2rem;
  align-items: center;
}

/* min-width:0 stops a long unbroken word in either half from forcing the grid
   wider than its container — grid items default to min-content width. */
.sf-textimg > .sf-textimg-media { order: 2; min-width: 0; }
.sf-textimg > .sf-textimg-copy  { order: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; }

/* The photograph fills its own column and keeps its aspect ratio, exactly as
   the reference does. No max-width cap here: the cap is the column. */
.sf-textimg > .sf-textimg-media img { width: 100%; height: auto; display: block; }

/* The clamp wrapper must span the copy column, not shrink to its button. */
.sf-textimg .sf-cat-desc-wrap { width: 100%; }

/* Same 768px trip point as the reference, and the same swap: photo on top. */
@media (max-width: 768px) {
  .sf-textimg { grid-template-columns: 1fr; gap: 1.25rem; }
  .sf-textimg > .sf-textimg-media { order: 1; }
  .sf-textimg > .sf-textimg-copy  { order: 2; }
}

/* -- 14. Hero arrow glyph -----------------------------------------------------
   ⛔ WHY THE CHEVRON IS AN SVG AND NOT A TEXT CHARACTER.

   These buttons used the single angle quotes ‹ / › at font-size:34px. That is
   what the owner saw as "not centred and a bit too small", and both symptoms
   came from the same cause: a glyph is drawn on a BASELINE inside an em box it
   does not fill. Flex-centring centres the BOX, not the ink — the side bearings
   and the ascender/descender padding are asymmetric, so the mark sits up and to
   one side — and most of those 34px is empty space, so the visible chevron was
   barely a third of the disc.

   An inline SVG has exact geometry: the viewBox IS the ink, so centring the
   element centres what you see, and the size is the size. These are the same
   polylines the product gallery, the lightbox and (since 2026-08-19) the
   featured rail already use, so all four carousels draw the identical arrow.
   The rail's old glyph was a text ‹ in a <span>, which is why it looked
   thinner and sat off-centre next to the hero's.

   The stroke is what makes it read at this size; drop-shadow replaces the old
   text-shadow (text-shadow does nothing to an SVG stroke) and keeps the white
   chevron legible on a pale banner. */
.sf-hero-nav svg,
.sf-featured-nav svg {
  width: 28px;
  height: 28px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .7));
}

@media (max-width: 767.98px) {
  .sf-hero-nav svg,
  .sf-featured-nav svg { width: 22px; height: 22px; }
}


/* --- RRP reference line on a catalogue tile ------------------------------
   .cds-price-was is line-through, because a struck-out number MEANS "you are
   not paying this any more". An RRP is not that: it is a supplier's suggested
   figure that this shop has never charged, so it is set plainly and labelled
   in visible text rather than dressed as a price drop. The tile only ever
   renders one of .cds-price-was / .cds-price-rrp -- see the branch note in
   templates/thumbs/product/template.html.

   Colour and size come from the existing design-system variables; nothing here
   DEFINES a --cds-* or --sf-* token (see the do-not-restore note in
   CssVariables.php). */
.cds-price-rrp {
  color: var(--cds-fg-muted);
  font-size: var(--cds-text-sm);
  text-decoration: none;
  white-space: nowrap;
}

.cds-price--lg .cds-price-rrp { font-size: var(--cds-text-lg); }


/* ============================================================================
   PRODUCT-PAGE FORMS — Price Match, Product Enquiry, Write a Review.
   Added 2026-08-16 with the three dialogs in js/custom.js.

   ⚠ THIS IS AN EXTENSION, NOT A SECOND MODAL. Everything structural — .sf-modal,
   .sf-modal-overlay, .sf-modal-dialog, .sf-modal-header, .sf-modal-footer,
   .sf-notify-form and its .fg / label / input rules, .sf-notify-save,
   .sf-modal-cancel, .sf-notify-msg, .sf-notify-hp and the body.sf-modal-open
   rule that hides the floating chat bubbles — is already in the shop's built
   stylesheet (resources/js/storefront/storefront.css), written for the
   out-of-stock Notify Me dialog and loaded on every themed page. The three
   dialogs reuse it verbatim. Only what that sheet has no reason to carry is
   added here: the two-column rows, textareas, the read-only autofilled boxes,
   per-field error text and the star picker.

   IT LIVES IN THE THEME'S OWN css/style.css because that file is linked LAST
   (see templates/headers/includes/head.template.html) and needs no front-end
   build to ship. Nothing here DEFINES a --cds-* or --sf-* token; every colour
   is either an existing token or a literal already used by the notify dialog.
   ========================================================================== */

/* --- two-column rows ------------------------------------------------------
   The owner's form pairs First Name/Surname, Phone/Email, Product/SKU and
   Price/Weblink. On a phone the pair stacks, which is why the gap is set on
   the row and zeroed in the stacked state rather than being a margin. */
.sf-notify-form .fg-row { display: flex; gap: 0 14px; flex-wrap: wrap; }
.sf-notify-form .fg-row > .fg { flex: 1 1 200px; min-width: 0; }
/* A pair whose second half is empty (a product with no SKU) hides that half;
   `hidden` has to beat the flex rule above. */
.sf-notify-form .fg[hidden] { display: none; }

.sf-modal-dialog.sf-modal--wide { width: 620px; }

/* --- textareas ------------------------------------------------------------
   Matched to the .fg input rule in the built sheet, minus the fixed height. */
.sf-notify-form .fg textarea {
  width: 100%;
  min-height: 96px;
  border: 1px solid var(--sf-border);
  border-radius: 0;
  padding: 9px 12px;
  font-family: var(--sf-font-body);
  font-size: 15px;
  line-height: 1.45;
  box-sizing: border-box;
  resize: vertical;
}
.sf-notify-form .fg textarea:focus { outline: none; border-color: var(--sf-green); }

/* --- the autofilled, read-only boxes --------------------------------------
   Product name and SKU are filled from the button that opened the dialog and
   are not typeable: the request must be about the product the shopper was
   looking at, and a mis-keyed name is a request nobody can action. Greyed so
   that is obvious, and `readonly` rather than `disabled` so the value is still
   read out by a screen reader and still submitted. */
.sf-notify-form .fg input[readonly] {
  background: #f1f3f5;
  color: #6c757d;
  cursor: default;
}
.sf-notify-form .fg input[readonly]:focus { border-color: var(--sf-border); }

/* --- "(Required)", hints and per-field errors ----------------------------- */
.sf-notify-form .req { font-weight: 400; color: #b91c1c; }
.sf-notify-form .sf-fglabel {
  display: block;
  font-family: var(--sf-font-body);
  font-size: 14px;
  font-weight: 600;
  color: var(--sf-notify-label);
  margin: 0 0 5px;
}
.sf-notify-form .sf-fghint {
  margin: 5px 0 0;
  font-family: var(--sf-font-body);
  font-size: 12.5px;
  line-height: 1.4;
  color: #6c757d;
}
.sf-notify-form .sf-fielderr {
  margin: 5px 0 0;
  font-family: var(--sf-font-body);
  font-size: 13px;
  line-height: 1.4;
  color: var(--sf-notify-err, #b91c1c);
}
.sf-notify-form .is-invalid,
.sf-notify-form .fg input.is-invalid,
.sf-notify-form .fg textarea.is-invalid { border-color: var(--sf-notify-err, #b91c1c); }

/* --- the star picker ------------------------------------------------------
   Five real radio inputs, keyboard operable, painted as stars. The inputs are
   rendered 5→1 and the fieldset is `direction: rtl`, so `~` can light every
   star to the left of the checked/hovered one — the standard CSS-only rating,
   with no script and no click handlers on <span>s. */
.sf-notify-form .sf-stars {
  border: 0;
  margin: 0;
  padding: 0;
  display: inline-flex;
  direction: rtl;
  font-size: 30px;
  line-height: 1;
}
.sf-notify-form .sf-stars input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.sf-notify-form .sf-stars label {
  color: #d0d5dd;
  cursor: pointer;
  padding: 0 2px;
  margin: 0;
  font-weight: 400;
  transition: color 0.12s;
}
.sf-notify-form .sf-stars label:hover,
.sf-notify-form .sf-stars label:hover ~ label,
.sf-notify-form .sf-stars input:checked ~ label { color: #f5a623; }
/* Keyboard users get a visible ring on the star the arrow keys landed on. */
.sf-notify-form .sf-stars input:focus-visible + label { outline: 2px solid var(--sf-green); outline-offset: 2px; }

/* Screen-reader-only text (the numeric value behind each star, the fieldset
   legend). Same recipe as the design system's own visually-hidden helper. */
.sf-notify-form .sf-sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --- the review photo picker ---------------------------------------------
   Thumbnails come from the local File via URL.createObjectURL, so the strip is
   populated before the upload finishes; .is-busy dims it and shows "Uploading…",
   .is-bad marks one the server refused (it carries no reference and is not
   submitted). The real <input type=file> is hidden but focusable-by-label — it
   is never display:none, which would take it out of the accessibility tree. */
.sf-notify-form .sf-optional { font-weight: 400; color: #6c757d; }
.sf-rvup-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.sf-rvup-list {
  list-style: none;
  margin: 0 0 8px;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.sf-rvup-item {
  position: relative;
  width: 74px;
  height: 74px;
  border: 1px solid var(--sf-border);
  background: #f1f3f5;
  overflow: hidden;
}
.sf-rvup-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sf-rvup-item.is-busy img { opacity: 0.4; }
.sf-rvup-item.is-bad { border-color: var(--sf-notify-err, #b91c1c); }
.sf-rvup-item.is-bad img { opacity: 0.3; }
.sf-rvup-status {
  position: absolute;
  inset: auto 0 0 0;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
  font-family: var(--sf-font-body);
  font-size: 10.5px;
  line-height: 1.25;
  padding: 3px 4px;
  text-align: center;
}
.sf-rvup-status:empty { display: none; }
.sf-rvup-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 20px;
  height: 20px;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.sf-rvup-remove:hover { background: var(--sf-notify-err, #b91c1c); }
.sf-rvup-add {
  display: inline-block;
  font-family: var(--sf-font-body);
  font-size: 14px;
  font-weight: 600;
  color: #6c757d;
  background: transparent;
  border: 1px dashed #adb5bd;
  padding: 7px 16px;
  cursor: pointer;
  margin: 0;
}
.sf-rvup-add:hover { border-color: var(--sf-green); color: var(--sf-green); }
.sf-rvup-add:focus-visible { outline: 2px solid var(--sf-green); outline-offset: 2px; }
.sf-rvup-add[hidden] { display: none; }

/* --- "Write a review" ----------------------------------------------------
   A sibling of .sf-rv, so it renders identically whether the product has
   sixteen reviews or none. */
.sf-rv-write { margin: 0 0 18px; }

/* --- the secondary product-page row --------------------------------------
   Price Match and Product Enquiry became <button>s (they were dead links to
   /contact-us). button.sf-btn-ghost is already normalised in the built sheet;
   THE PAIR MUST STRETCH EQUALLY. An earlier version set flex:0 0 auto here to
   stop them stretching, which sized each button to its own label - so "Product
   Enquiry" came out wider than "Price Match" and the row stopped short of the
   right edge. This sheet loads AFTER the built stylesheet, so this rule is the
   one that decides. flex-basis 0 makes the two halves exactly equal regardless
   of label length, and min-width:0 lets them shrink on a narrow phone rather
   than overflowing. */
.sf-pdp-secondary { flex-wrap: wrap; }
.sf-pdp-secondary > .sf-btn-ghost { flex: 1 1 0; min-width: 0; }

@media (max-width: 480px) {
  .sf-notify-form .fg-row { display: block; }
  .sf-modal-dialog.sf-modal--wide { width: 100%; }
}


/* ==========================================================================
   FOOTER LEGAL BAR + POPULAR SEARCHES
   Component rules only. NO design token is ever declared in the theme - a
   :root block here is injected after the stylesheets and freezes every
   responsive value (see CssVariables). Colours inherit from the bar.
   ========================================================================== */
.sf-footer-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 18px; }
.sf-footer-copy { margin: 0; }
.sf-footer-legal { display: flex; flex-wrap: wrap; align-items: center; gap: 0; }
.sf-footer-legal a { color: inherit; text-decoration: none; opacity: .85; padding: 0 12px; }
.sf-footer-legal a:hover { opacity: 1; text-decoration: underline; }
/* Drawn, not typed, so no stray separator hangs off either end. */
.sf-footer-legal a + a { border-left: 1px solid currentColor; }

.sf-psearch {
  /*
   * ⛔ NOTHING SHOWS HERE UNTIL THE FOOTER LINK IS CLICKED - not even a
   *    heading. The first cut printed its own "Popular Searches" anchor at the
   *    bottom of every page, so the words appeared TWICE: once in the My
   *    Account column and again standing alone under the copyright. The only
   *    trigger is the footer menu item; this block is the panel it opens.
   */
  display: none;
  padding: 16px 12px 22px;
  text-align: center;
  background: #111;
  color: #fff;
}
.sf-psearch.is-open { display: block; }
.sf-psearch a { color: inherit; }
.sf-psearch-list {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 14px;
  list-style: none; margin: 0 auto; padding: 0; max-width: 900px;
}
.sf-psearch-list a { font-size: 13px; opacity: .8; text-decoration: none; }
.sf-psearch-list a:hover { opacity: 1; text-decoration: underline; }

/* The back-to-top control floats over the right end of the bar, and was
   sitting on top of "Security Policy". Keep the legal links clear of it. */
.sf-footer-legal { padding-right: 56px; }

/* 12 Oct 2026 (full audit, "textus-bubble"): the fixed "Text us" pill (bottom
   right, about 150px wide) sat on "Security Policy". On computers the legal
   links keep clear of it; on phones the bar gets room to scroll up above it. */
@media (min-width: 641px) { .sf-footer-legal { padding-right: 170px; } }
@media (max-width: 640px) { .sf-footer-bar { padding-bottom: 76px; } }


/* ==========================================================================
   SCROLLBAR - matched to www.toolsinabox.com.au
   ==========================================================================
   ⚠ EVERY VALUE IS var(--token, fallback) AND THAT IS DELIBERATE.
     No :root block may be declared in this theme: ThemeRenderer::withTokens()
     injects theme tokens LAST in <head>, after the stylesheets, so anything
     declared here would out-rank every @media rule in main-*.css and freeze
     the whole responsive system (that is what once made the site full-width).
     The fallback in each var() is the live site's value, so this renders
     correctly with nothing saved in Theme Design.

   Maropost styles the WebKit bar only. The Firefox properties below are the
   same two colours expressed the standard way, so the shop matches in both
   engines rather than only in Chrome and Safari. */
::-webkit-scrollbar {
  width: var(--sf-scrollbar-width, 8px);
  /* Maropost sets width alone; height keeps a horizontal bar (wide tables,
     the thumbnail rail) from showing up unstyled beside a styled one. */
  height: var(--sf-scrollbar-width, 8px);
}
::-webkit-scrollbar-track { background: var(--sf-scrollbar-track, #f1f1f1); }
::-webkit-scrollbar-thumb { background: var(--sf-scrollbar-thumb, #a2ce46); }
::-webkit-scrollbar-thumb:hover { background: var(--sf-scrollbar-thumb-hover, #555555); }

/* ⛔ FIREFOX ONLY - AND THE @supports GUARD IS LOAD-BEARING.
   Chrome 121+ understands scrollbar-width/scrollbar-color too, and when both
   are present the STANDARD properties win and ::-webkit-scrollbar above is
   ignored entirely. Applied unguarded, this rule silently took Chrome off
   Maropost's 8px bar and onto the browser's own "thin" (~11px, measured 15px
   in-browser) - i.e. the line meant to add parity removed it.
   The guard matches only engines with no ::-webkit-scrollbar support. */
/* ⛔ THIS RESET IS WHAT MAKES THE 8px BAR ABOVE WORK AT ALL.
   main-*.css ships a Preflight-style reset containing
       html { scrollbar-color: var(--cds-slate-300) transparent }
   and in Chrome 121+ the presence of ANY standard scrollbar property switches
   off ::-webkit-scrollbar completely. So the webkit block above was being
   ignored and the bar rendered slate-grey at the browser default width -
   measured 15px, against Maropost's 8px green.
   Returning both properties to auto hands styling back to the webkit
   pseudo-elements. Do not delete this thinking it is a no-op. */
html {
  scrollbar-width: auto;
  scrollbar-color: auto;
}

@supports not selector(::-webkit-scrollbar) {
  html {
    scrollbar-width: thin;
    scrollbar-color: var(--sf-scrollbar-thumb, #a2ce46) var(--sf-scrollbar-track, #f1f1f1);
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   BROWSE TOOLBOXES BY SIZE  (/toolbox-by-size, /toolbox-by-size/1700mm)
   ═══════════════════════════════════════════════════════════════════════════
   Two components, both of which appear ONLY on those pages:

     .sf-sizehub   the hub's grid of sizes, printed into the CMS page body
     .sf-sizenav   the cross-link rail on each size page

   ⚠ IT LIVES IN THE THEME, NOT IN THE REACT BUILD, AND THAT IS NOT A STYLE
     PREFERENCE. `npm run build` no longer restyles themed pages -- the theme
     owns css/baseline.css and this file -- so a rule for these pages written
     into the React stylesheet would compile cleanly and never reach the site.

   EVERY COLOUR IS A --sf-* TOKEN WITH A LITERAL FALLBACK, which is the contract
   css/style.info.txt sets up: the token is what the owner changes in Theme
   Design, the fallback is what the page uses if he has not. No new tokens are
   declared here -- these two components borrow the ones the catalogue already
   uses, so they restyle with the rest of the shop rather than needing their own
   entry on the settings screen. */

.sf-sizehub {
  list-style: none;
  margin: 1.5rem 0;
  padding: 0;
  display: grid;
  /* auto-fill, not auto-fit: with fifteen sizes a partly filled last row should
     keep its column width rather than stretching two cards across the page. */
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 12px;
}

.sf-sizehub-item { margin: 0; }

.sf-sizehub-link {
  display: flex;
  flex-direction: column;
  gap: 2px;
  height: 100%;
  padding: 14px 16px;
  border: 1px solid var(--sf-border, #e2e4e7);
  border-radius: var(--sf-radius, 6px);
  background: var(--sf-surface, #ffffff);
  color: inherit;
  text-decoration: none;
  transition: border-color .15s ease, box-shadow .15s ease;
}

.sf-sizehub-link:hover,
.sf-sizehub-link:focus-visible {
  border-color: var(--sf-green, #85ad3b);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .08);
  text-decoration: none;
}

.sf-sizehub-size {
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1.15;
  color: var(--sf-heading, #1f2933);
}

.sf-sizehub-name {
  font-size: .85rem;
  line-height: 1.3;
  color: var(--sf-muted, #667085);
}

.sf-sizehub-count {
  margin-top: 4px;
  font-size: .8rem;
  font-weight: 600;
  color: var(--sf-green, #85ad3b);
}

/* ── the per-page rail ──────────────────────────────────────────────────── */

.sf-sizenav { margin: 0 0 1.25rem; }

.sf-sizenav-title {
  margin: 0 0 .5rem;
  font-size: .95rem;
  font-weight: 600;
  color: var(--sf-muted, #667085);
}

.sf-sizenav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.sf-sizenav-item { margin: 0; }

.sf-sizenav-link {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--sf-border, #e2e4e7);
  border-radius: 999px;
  background: var(--sf-surface, #ffffff);
  color: inherit;
  font-size: .9rem;
  text-decoration: none;
  white-space: nowrap;
}

.sf-sizenav-link:hover,
.sf-sizenav-link:focus-visible {
  border-color: var(--sf-green, #85ad3b);
  text-decoration: none;
}

/* The page you are already on. Marked with aria-current in the template as
   well, so it is not colour alone that says which one it is. */
.sf-sizenav-item.is-current .sf-sizenav-link {
  border-color: var(--sf-green, #85ad3b);
  background: var(--sf-green, #85ad3b);
  color: #ffffff;
  font-weight: 600;
}

.sf-sizenav-count { font-size: .78rem; opacity: .7; }

.sf-sizenav-all { margin: .6rem 0 0; font-size: .85rem; }


/* ============================================================================
   Newsletter consent + honeypot — templates/footers/template.html
   ============================================================================

   Three small rules, and only three, because .sf-news itself is already styled
   in the shop's own built stylesheet (baseline.css: a white pill, display:flex,
   overflow:hidden, holding the input and the arrow button). Nothing here
   restyles that pill — the consent label and the honeypot are siblings OUTSIDE
   it, inside the <form>, precisely so they cannot be caught by `.sf-news input`
   (which is flex:1, border:0, padding:10px — correct for the address field and
   absurd for a 16px checkbox).

   ⛔ THE HONEYPOT MUST NOT USE display:none OR [hidden]. Several bots skip
      display:none fields specifically because they know it is a honeypot, and
      some password managers still fill them. Positioning it off-screen keeps it
      a real, fillable field to anything that is not looking at the page.
      aria-hidden + tabindex="-1" keep it away from assistive tech and from the
      keyboard, so a sighted keyboard user never lands in it.
*/
.sf-news-consent {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  margin: 8px 0 0;
  font-size: var(--sf-text-sm, .875rem);
  line-height: 1.35;
  cursor: pointer;
}

.sf-news-consent input[type="checkbox"] {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin: 2px 0 0;
  accent-color: var(--sf-green, #85ad3b);
}

.sf-news-hp {
  position: absolute !important;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The failure half of .sf-news-msg, which baseline.css styles for success only
   (#d8e6c4 — pale green on the dark footer). Kept legible against that ground
   rather than a pure red, which vibrates on near-black. */
.sf-news-msg.is-error { color: #f7b9b9; }


/* ============================================================================
   CMS page accordions — /faqs, and any stored body carrying the old platform's
   Bootstrap 4 collapse markup. See custom.js → cmsAccordion().

   `.accordion`, `.faq`, `.collapse` and `.card-body` are BOOTSTRAP'S class
   names, not this theme's. They are matched here because they are what is in
   the database — the stored body is the owner's content and is not rewritten to
   suit a stylesheet. Every rule is scoped to .sf-content-body, the div the CMS
   templates print [@content.body@] into, so nothing in the header, footer,
   sidebar or product chrome can be caught by them.

   ⛔ THE PANELS ARE HIDDEN ONLY INSIDE `.is-enhanced`, WHICH ONLY THE SCRIPT
      ADDS. With JavaScript off, a failed fetch of custom.js, or a crawler, every
      answer stays visible exactly as it is today. Hiding them from a stylesheet
      that always applies would make a broken page worse: eight answers of SEO
      copy would vanish for anything that does not run scripts.

   The look deliberately matches .sf-faq-* (the home page's disclosure list) —
   same border, same weight, same focus ring — because a shopper should not be
   able to tell that one accordion came from a template and one from a text
   field.
*/
.sf-content-body .accordion {
  margin: 0 0 1.5rem;
  border-top: 1px solid var(--sf-border, #e6e6e6);
}

.sf-content-body .accordion .faq {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  margin: 0;
  padding: 16px 2px;
  border: 0;
  border-bottom: 1px solid var(--sf-border, #e6e6e6);
  background: none;
  text-align: left;
  font-family: var(--sf-font-body, sans-serif);
  font-size: var(--sf-text-lg, 1.0625rem);
  font-weight: 600;
  line-height: 1.35;
  color: var(--sf-fg, #111);
}

/* Not a pointer until it actually does something. */
.sf-content-body .accordion.is-enhanced .faq { cursor: pointer; }
.sf-content-body .accordion.is-enhanced .faq:hover { color: var(--sf-green, #85ad3b); }

.sf-content-body .accordion .faq:focus-visible {
  outline: 3px solid var(--sf-lime, #a2ce46);
  outline-offset: 2px;
}

/* The chevron, drawn from the button's own aria-expanded so the mark and the
   announced state are one flag. Only inside .is-enhanced: a chevron on a
   control that cannot open anything is a promise the page will not keep. */
.sf-content-body .accordion.is-enhanced .faq::after {
  content: "";
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-3px) rotate(45deg);
  transition: transform .2s ease-in-out;
}

.sf-content-body .accordion.is-enhanced .faq[aria-expanded="true"]::after {
  transform: translateY(2px) rotate(-135deg);
}

@media (prefers-reduced-motion: reduce) {
  .sf-content-body .accordion.is-enhanced .faq::after { transition: none; }
}

.sf-content-body .accordion .collapse .card-body {
  padding: 2px 2px 18px;
  border-bottom: 1px solid var(--sf-border, #e6e6e6);
}

/* Stated rather than left to the UA default, so a later blanket `display` rule
   on .sf-content-body cannot leave a closed panel on screen. */
.sf-content-body .accordion.is-enhanced .collapse[hidden] { display: none; }


/* ============================================================================
   The Contact Us form — includes/blocks/contact.template.html, reached through
   the [tiab_contact_form] shortcode on the /contact-us page body.

   ⚠ THE BLOCK'S DOCBLOCK SAYS "NO NEW CSS", AND THAT NOTE IS STALE. It was
     written against a page body that shipped its own <style> block defining
     .tiab-cf*. The live cms_pages row for /contact-us carries no such block —
     `grep tiab-cf` over style.css, baseline.css and app.css returns nothing, and
     so does the stored body. Without these rules the form renders as unstyled
     UA controls: full-bleed labels, a 20-character text box and a default grey
     button. The markup contract is unchanged; this is the other half of it.

   Every class here is one the template already emits. Nothing is renamed.
*/
.sf-content-body .tiab-cf-card {
  margin: 2rem 0 0;
  padding: 24px;
  border: 1px solid var(--sf-border, #e6e6e6);
  border-radius: 6px;
  background: var(--sf-surface, #fff);
}

.sf-content-body .tiab-cf-card > h2 { margin: 0 0 6px; font-size: var(--sf-text-2xl, 1.5rem); }
.sf-content-body .tiab-cf-card > p { margin: 0 0 18px; color: var(--sf-fg-muted, #555); }

/* The two-up rows collapse to one column on a phone. `minmax(0, 1fr)` rather
   than `1fr`: a grid track's default minimum is auto, so a long placeholder
   would otherwise push the column wider than the card. */
.tiab-cf-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

@media (max-width: 640px) {
  .tiab-cf-row { grid-template-columns: minmax(0, 1fr); }
}

.tiab-cf label { display: block; margin: 0 0 16px; font-weight: 600; font-size: var(--sf-text-sm, .875rem); }
.tiab-cf-row label { margin-bottom: 0; }

.tiab-cf input,
.tiab-cf textarea {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 10px 12px;
  border: 1px solid var(--sf-border, #d7d9dd);
  border-radius: 4px;
  background: #fff;
  font: inherit;
  font-weight: 400;
  color: var(--sf-fg, #111);
}

.tiab-cf textarea { resize: vertical; min-height: 140px; }

.tiab-cf input:focus-visible,
.tiab-cf textarea:focus-visible {
  outline: 3px solid var(--sf-lime, #a2ce46);
  outline-offset: 1px;
  border-color: var(--sf-green, #85ad3b);
}

.tiab-cf-btn {
  margin-top: 4px;
  padding: 12px 28px;
  border: 0;
  border-radius: 4px;
  background: var(--sf-buy, #0eac71);
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.tiab-cf-btn:hover { background: var(--sf-buy-hover, #0c9663); }
.tiab-cf-btn:focus-visible { outline: 3px solid var(--sf-lime, #a2ce46); outline-offset: 2px; }

/* ⛔ OFF-SCREEN, NOT display:none. Several bots skip display:none fields
      precisely because they know it is a honeypot, and some password managers
      still fill them. Positioning it out of the viewport keeps it a real,
      fillable field to anything that is not looking at the page; aria-hidden and
      tabindex="-1" in the markup keep it away from assistive tech and from the
      keyboard, so a sighted keyboard user never lands in it. SpamGuard FILES a
      submission that fills it rather than refusing — see the block. */
.tiab-cf-hp {
  position: absolute !important;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The outcome, rendered on the GET that follows the POST from flashed session
   data — the form works with JavaScript off, so the message has to as well. */
.tiab-cf-note { margin: 0 0 16px; padding: 12px 14px; border-radius: 4px; border: 1px solid; }
.tiab-cf-note p { margin: 0 0 6px; }
.tiab-cf-note ul { margin: 0; padding-left: 20px; }
.tiab-cf-ok { border-color: #b7e0c6; background: #f0fdf4; color: #166534; }
.tiab-cf-err { border-color: #fecaca; background: #fef2f2; color: #991b1b; }

/* ============================================================================
   THE CATALOGUE PAGER — rebuilt 5 Sep 2026.
   ============================================================================
   Owner: "on the storefront why does the next page buttons look like trash.
   please fix it so its modern and nice."

   ⛔ WHAT WAS ACTUALLY WRONG WAS ONE SELECTOR IN baseline.css:

       .sf-pagination a, .sf-pagination span { border:1px solid #ddd;
                                               border-radius:5px; padding:8px 14px }

     `span` — not `.cds-pagination-btn`. The pager's SUMMARY line is
     "Page <span class="cds-num">1</span> of <span>13</span> — <span>300</span>
     products", so that rule drew a bordered box around every figure in an
     ordinary sentence. The screenshot the owner sent shows `Page [1] of [13] —
     [300] products` with three buttons that do nothing, sitting beside the real
     ones. Nothing was "broken"; a selector was simply too wide.

   ⛔ AND THE CURRENT PAGE FAILED CONTRAST, WHICH IS THE PART THAT MATTERED MORE.
     White on the brand green (#85ad3b) is 2.61:1 — well under the 4.5 floor, on
     a page every shopper uses. `--sf-green-dark` (#6f9127) is no rescue at
     3.65:1. Rather than abandon the brand colour, the FILL is kept and the TEXT
     is darkened: #1f2a10 on #85ad3b is 5.75:1. It still reads as the shop's
     green, and it is now legible.

   ⚠ THIS FILE LOADS AFTER baseline.css, so these rules win on a tie — that is
     the documented order (see baseline.css's own header). Nothing in baseline
     is edited; it is the imported copy of the built stylesheet and stays that
     way so it can be re-imported without losing this.
   ========================================================================== */

.sf-pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 14px 24px;
  margin-top: 34px;
  padding-top: 20px;
  border-top: 1px solid var(--sf-border-soft, #ececec);
  text-align: left;
}

/* ---- the sentence, which is a sentence and not a row of buttons ---------- */
.sf-pagination .cds-pagination-summary {
  margin: 0;
  font-size: var(--sf-text-sm, 13px);
  color: var(--sf-fg-dim, #666);
  letter-spacing: 0.01em;
}

/*
 | ⛔ THE FIX FOR THE BOXES. Undoes baseline's `.sf-pagination span` for every
 |   span that is NOT a control. The button below re-declares its own box, so
 |   this cannot strip the thing it is meant to keep.
 |
 | ⚠ Only the box is reset — never `position`, `clip` or `overflow`. The pager
 |   carries `.cds-sr-only` spans naming each link for a screen reader, and
 |   those are hidden by exactly those properties. Resetting layout here would
 |   dump "Go to page 4" into the middle of the control.
*/
.sf-pagination span {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  display: inline;
}

.sf-pagination .cds-pagination-summary .cds-num {
  font-weight: 600;
  color: var(--sf-fg, #111);
  font-variant-numeric: tabular-nums;
}

/* ---- the controls -------------------------------------------------------- */
.sf-pagination .cds-pagination-controls { margin-left: auto; }

.sf-pagination .cds-pagination-list {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
  max-width: 100%;
}

/*
 | The button. 40px square is a deliberate floor: this is the one control on the
 | catalogue a shopper uses with a thumb, and the old 8px/14px padding produced
 | a ~32px target that is under every touch guideline.
 |
 | ⚠ `transition` names its properties. `all` here would animate the border-box
 |   on hover and make the row visibly reflow.
*/
.sf-pagination a.cds-pagination-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  height: 40px;
  padding: 0 12px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: transparent;
  color: var(--sf-fg-soft, #333);
  font-size: var(--sf-text-base, 14px);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-decoration: none;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.sf-pagination a.cds-pagination-btn:hover {
  background: var(--sf-featured-nav-bg, #f2f2f2);
  border-color: var(--sf-border, #e6e6e6);
  color: var(--sf-fg, #111);
}

.sf-pagination a.cds-pagination-btn:active {
  background: var(--sf-featured-nav-bg-hover, #e6e6e6);
}

/* Keyboard focus must stay obvious — the hover tint alone is not a focus state. */
.sf-pagination a.cds-pagination-btn:focus-visible {
  outline: 2px solid var(--sf-green, #85ad3b);
  outline-offset: 2px;
}

/*
 | ⛔ THE CURRENT PAGE: BRAND FILL, DARK TEXT. See the header — white on this
 |   green is 2.61:1 and fails. #1f2a10 on it is 5.75:1 and keeps the colour.
 |   `aria-current="page"` is already on the element, so this is decoration on
 |   top of a real signal rather than the only signal.
*/
.sf-pagination a.cds-pagination-btn.is-current,
.sf-pagination a.cds-pagination-btn.is-current:hover {
  background: var(--sf-green, #85ad3b);
  border-color: var(--sf-green, #85ad3b);
  color: #1f2a10;
  font-weight: 700;
  cursor: default;
}

/* The ellipsis is punctuation, not a control: no box, no hover, narrower. */
.sf-pagination .cds-pagination-dots {
  min-width: 20px;
  padding: 0 2px;
  color: var(--sf-fg-crumb, #777);
  text-align: center;
  user-select: none;
}

/* ---- phones -------------------------------------------------------------- */
@media (max-width: 640px) {
  .sf-pagination {
    flex-direction: column;
    align-items: center;
    gap: 14px;
    text-align: center;
  }

  .sf-pagination .cds-pagination-controls { margin-left: 0; }
  .sf-pagination .cds-pagination-list { justify-content: center; }

  /* ⚠ Targets grow rather than shrink on a phone; the row wraps instead. */
  .sf-pagination a.cds-pagination-btn {
    min-width: 44px;
    height: 44px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sf-pagination a.cds-pagination-btn { transition: none; }
}

/* ---- Product options (Maropost "Extra Options"), 28 Sep 2026 -------------
   Label left, select right, like Maropost's buying box; the optional link sits
   centred beneath in the theme's blue. Cart/mini-cart/checkout print the choice
   as a quiet line under the product name. */
.sf-pdp-options{margin:14px 0 18px;display:flex;flex-direction:column;gap:12px}
.sf-pdp-option{display:grid;grid-template-columns:110px minmax(0,280px);align-items:center;gap:8px 16px}
.sf-pdp-option-name{font-weight:700;font-size:15px;color:#111;line-height:1.25}
.sf-pdp-option-select{width:100%;height:42px;padding:0 36px 0 12px;font-size:16px;color:#111;border:1px solid #333;border-radius:2px;background:#fff}
.sf-pdp-option-link{grid-column:1/-1;display:block;text-align:center;color:#3498db;font-size:15px;text-decoration:none}
.sf-pdp-option-link:hover{text-decoration:underline}
.sf-ct-opts,.sf-mc-opts,.sf-cos-opts{display:block;font-size:12px;color:#555;margin-top:2px;font-weight:400}
@media (max-width:600px){.sf-pdp-option{grid-template-columns:1fr}}

/* ---- Option video popup (28 Sep 2026): the "Click to Watch … Video" link opens
   the video in a box over the page, as Maropost's fancybox did, not a new tab. */
.sf-video-modal{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.78);padding:16px}
.sf-video-modal-box{position:relative;width:min(960px,94vw);aspect-ratio:16/9;background:#000;box-shadow:0 20px 60px rgba(0,0,0,.6)}
.sf-video-modal-box iframe{width:100%;height:100%;border:0;display:block}
.sf-video-modal-close{position:absolute;top:-40px;right:0;color:#fff;font-size:30px;line-height:1;background:none;border:0;cursor:pointer;padding:4px 8px}
.sf-video-modal-close:hover{color:#ddd}
/* Product video tile: the last tile of the gallery rail, same box as a photo thumb (photo fix B, 9 Oct 2026). */
.cds-gallery-video{position:relative;display:block;width:var(--sf-gal-thumb-size,78px);height:var(--sf-gal-thumb-size,78px);border:var(--sf-gal-thumb-border-width,1px) solid var(--sf-gal-thumb-border,#ddd);border-radius:var(--sf-gal-thumb-radius,2px);background:#111;overflow:hidden;cursor:pointer}
.cds-gallery-video:hover,.cds-gallery-video:focus-visible{border-color:var(--sf-gal-thumb-active,#85ad3b)}
.cds-gallery-video img{width:100%;height:100%;object-fit:cover;display:block;opacity:.85}
.cds-gallery-video-play{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.cds-gallery-video-play svg{width:34px;height:34px}
.cds-gallery-video-play circle{fill:rgba(0,0,0,.55);stroke:#fff;stroke-width:1.5}
.cds-gallery-video-play polygon{fill:#fff}
@media (width<=700px){.cds-gallery-video{width:var(--sf-gal-thumb-size-sm,62px);height:var(--sf-gal-thumb-size-sm,62px)}}
body.sf-video-open{overflow:hidden}


/* ── BRANDS (29 Sep 2026) ───────────────────────────────────────────────────
   Maropost-style brands: the small logo on a product tile (.thumb-brand), the
   "Filter by Brand" sidebar block, the brand tiles used by the home page
   "Shop Brands" block and /brands, and the logo on a brand page. No :root
   tokens are declared here (see the token-clobber note in this file's history);
   colours come from existing variables with a fallback. */
.cds-pcard .sf-thumb-brand{position:absolute;top:8px;left:8px;z-index:2;display:block;max-width:72px;padding:2px;background:#fff;line-height:0}
.cds-pcard .sf-thumb-brand img{display:block;max-width:100%;max-height:32px;width:auto;height:auto}
.sf-brand-filter{list-style:none;margin:0;padding:0}
.sf-brand-filter li a{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:6px 0;color:inherit;text-decoration:none}
.sf-brand-filter li a:hover{text-decoration:underline}
.sf-brand-filter li.is-active a{font-weight:700}
.sf-brand-remove{font-size:1.1em;line-height:1}
.sf-brand-all{margin:8px 0 0;font-size:.9em}
.sf-brand-grid{list-style:none;margin:16px 0 0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:16px}
.sf-brand-tile{display:flex;flex-direction:column;align-items:center;gap:6px;min-width:0}
.sf-brand-link{display:flex;align-items:center;justify-content:center;width:100%;min-height:96px;padding:12px;background:var(--sf-surface,#fff);box-shadow:var(--sf-card-shadow,0 1px 3px rgba(0,0,0,.12));color:inherit;text-align:center;text-decoration:none;overflow-wrap:anywhere}
.sf-brand-link img{display:block;max-width:100%;max-height:72px;width:auto;height:auto}
.sf-brand-name{font-weight:700}
.sf-brand-count{font-size:.85em;opacity:.75}
.sf-brand-empty{margin:16px 0}
.sf-home-brands{text-align:center;margin-top:32px;margin-bottom:32px}
.sf-home-brands-lead{margin:4px 0 0}
.sf-brand-logo{margin:0 0 12px}
.sf-brand-logo img{display:block;max-height:80px;max-width:100%;width:auto;height:auto}

/* ---- Product variations (29 Sep 2026) — the parent product's selector. Mirrors
   .sf-pdp-option's grid; the same rules live in resources/js/storefront/storefront.css
   for the React page (celora-css-independence: both stylesheets). ---- */
.sf-pdp-variations{margin:14px 0 6px;display:flex;flex-direction:column;gap:12px}
.sf-pdp-variation{display:grid;grid-template-columns:110px minmax(0,1fr);align-items:center;gap:8px 16px}
.sf-pdp-variation-name{font-weight:700;font-size:15px;color:#111;line-height:1.25}
.sf-pdp-variation-current{font-weight:400}
.sf-pdp-variation--select .sf-pdp-variation-name .sf-pdp-variation-current{display:none}
.sf-pdp-variation-select{width:100%;max-width:280px;height:42px;padding:0 36px 0 12px;font-size:16px;color:#111;border:1px solid #333;border-radius:2px;background:#fff}
.sf-pdp-variation-values{display:flex;flex-wrap:wrap;gap:8px}
.sf-pdp-variation-opt{display:inline-flex;align-items:center;gap:8px;min-height:42px;min-width:42px;padding:4px 12px;border:1px solid #bbb;border-radius:2px;background:#fff;color:#111;font-size:15px;text-decoration:none;cursor:pointer}
.sf-pdp-variation-opt:hover{border-color:#111}
.sf-pdp-variation-opt.is-selected,.sf-pdp-variation-opt._itmspec_selected{border:2px solid #111;padding:3px 11px;font-weight:700}
.sf-pdp-variation-opt--image{padding:3px}
.sf-pdp-variation-opt--image.is-selected{padding:2px}
.sf-pdp-variation-opt.is-out{color:#888;background:#f5f5f5}
.sf-pdp-variation-opt.is-out .sf-pdp-variation-label{text-decoration:line-through}
.sf-pdp-variation-img{width:40px;height:40px;object-fit:cover;display:block}
.sf-pdp-variation-swatch{width:40px;height:40px;display:block;border:1px solid rgba(0,0,0,.15)}
.sf-pdp-variation-opt--textimage .sf-pdp-variation-swatch,.sf-pdp-variation-opt--textimage .sf-pdp-variation-img{width:28px;height:28px}
.sf-pdp-variation-radio{display:inline-flex;align-items:center;gap:6px;margin-right:14px;font-size:15px;cursor:pointer}
.sf-pdp-variation-radio.is-out{color:#888}
.sf-pdp-variations.is-loading{opacity:.6;pointer-events:none}
@media (max-width:600px){.sf-pdp-variation{grid-template-columns:1fr}}


/* ============================================================================
   Webstore › Forms (29 Sep 2026) — forms built in the admin form builder,
   drawn by the [form code="…"] shortcode or a page's own Form. They reuse the
   Contact Us form's .tiab-cf rules above; these add what that form never had:
   choices, tick boxes, drop-downs, help text and per-field errors.
*/
.celora-form { margin: 1.5rem 0 0; }
.celora-form-intro { margin: 0 0 16px; color: var(--sf-fg-muted, #555); }
.celora-form-heading { margin: 8px 0 12px; font-size: var(--sf-text-lg, 1.125rem); }
.celora-form-para { margin: 0 0 16px; color: var(--sf-fg-muted, #555); }
.tiab-cf select {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 10px 12px;
  border: 1px solid var(--sf-border, #d7d9dd);
  border-radius: 4px;
  background: #fff;
  font: inherit;
  font-weight: 400;
  color: var(--sf-fg, #111);
}
.tiab-cf select:focus-visible { outline: 3px solid var(--sf-lime, #a2ce46); outline-offset: 1px; border-color: var(--sf-green, #85ad3b); }
.celora-form-choice { margin: 0 0 16px; padding: 0; border: 0; min-width: 0; }
.celora-form-choice legend { padding: 0; margin: 0 0 6px; font-weight: 600; font-size: var(--sf-text-sm, .875rem); }
.tiab-cf .celora-form-opt { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; font-weight: 400; }
.tiab-cf .celora-form-opt input { display: inline-block; width: auto; margin: 0; padding: 0; flex: none; accent-color: var(--sf-buy, #0eac71); }
.celora-form-single { margin: 0 0 16px; }
.celora-form-help { display: block; margin-top: 4px; font-weight: 400; color: var(--sf-fg-muted, #555); }
.celora-form-err { display: block; margin-top: 4px; font-weight: 600; color: #b91c1c; }
.tiab-cf [aria-invalid="true"] { border-color: #dc2626; }
.celora-form .tiab-cf-btn[disabled] { opacity: .6; cursor: progress; }
.sf-content-login { margin: 1rem 0 2rem; }
.sf-bloglist--children { margin-top: 2rem; }

/* Area C (29 Sep 2026): the quote-only note on the cart is information, not an error. */
.sf-checkout-blocked--info { color: #1f3f6b; background: #eef4fb; border-color: #b9cfe8; }

/* Wave 3 (30 Sep 2026): Maropost advertising slots — [%advert%] in category,
   web-page and header templates. A banner is the picture at full width; a text
   advert is a small panel. */
.sf-adverts { display: grid; gap: 12px; margin: 0 0 1.25rem; }
.sf-adverts--slim { margin: 0; }
.sf-advert--banner img { display: block; width: 100%; height: auto; }
.sf-advert--text { padding: 1rem 1.25rem; border: 1px solid #e2e8f0; border-radius: 6px; background: #f8fafc; }
.sf-advert-title { margin: 0 0 .35rem; font-size: 1.1rem; }
.sf-advert-body > :last-child { margin-bottom: 0; }
.sf-advert-cta { margin: .75rem 0 0; }
.sf-advert-products { margin: 0 0 1.5rem; }

/* ═══ Prelaunch phone QA (1 Oct 2026) ═════════════════════════════════════
   Measured in the browser at 375x812 and 414x896. Additive only.            */

/* The Notify-me / newsletter-unsubscribe / SMS opt-out buttons are <a> links
   inside .sf-content-body, whose link colour (green, underlined) out-ranked
   .tiab-cf-btn's white: green text on a green button, and a long product name
   wrapped as a broken inline box. */
.sf-content-body a.tiab-cf-btn,
a.tiab-cf-btn { display: inline-block; max-width: 100%; color: #fff; text-decoration: none; line-height: 1.35; text-align: center; }
.sf-content-body a.tiab-cf-btn:hover,
a.tiab-cf-btn:hover { color: #fff; }

@media (max-width: 991.98px) {
  /* Header trolley was a 30x28 tap target squeezed between Search and Menu;
     fill the gap so it is as easy to hit as its neighbours (40-42 x 46). */
  .sf-utils .sf-cart-link { min-width: 38px; min-height: 46px; justify-content: center; align-items: center; }
}

@media (max-width: 767.98px) {
  /* iPhone Safari zooms the whole page when a field under 16px is focused
     (cart shipping calculator was 14px, review sort 13px). */
  body.cds-storefront input[type="text"],
  body.cds-storefront input[type="email"],
  body.cds-storefront input[type="tel"],
  body.cds-storefront input[type="search"],
  body.cds-storefront input[type="password"],
  body.cds-storefront input:not([type]),
  body.cds-storefront select,
  body.cds-storefront textarea { font-size: max(16px, 1em); }

  /* Category filter Apply / Clear were 28px tall. */
  .sf-btn-length { min-height: 40px; }

  /* Cart "x Remove" was an 18px-tall target. */
  .sf-ct-remove { min-height: 36px; padding-top: 8px; padding-bottom: 8px; }

  /* Footer column links: a 34px tap band with the SAME layout (the padding is
     cancelled by an equal negative margin, so nothing moves). */
  .sf-fcol li > a { display: inline-block; padding: 7px 0; margin: -7px 0; }
}

/* -- Grow my Google (SEO tools, 4 Oct 2026) -----------------------------
   Popular in this range, Related categories and the category FAQ, printed
   BELOW the product grid only when the owner switched them on / published them.
   Plain links that wrap; no fixed widths, so nothing can overflow on a phone. */
.sf-grow-links, .sf-grow-faq { margin: 32px 0 0; padding-top: 20px; border-top: 1px solid var(--sf-border, #e6e6e6); }
.sf-grow-h { margin: 0 0 12px; font-family: var(--sf-font-body, sans-serif); font-size: var(--sf-text-xl, 18px); font-weight: 700; line-height: 1.3; color: var(--sf-fg, #111); }
.sf-grow-list { margin: 0; padding: 0 0 0 18px; columns: 2 260px; column-gap: 32px; }
.sf-grow-list li { margin: 0 0 8px; break-inside: avoid; line-height: 1.4; }
.sf-grow-list a, .sf-grow-chip { color: inherit; text-decoration: underline; text-underline-offset: 2px; overflow-wrap: anywhere; }
.sf-grow-list a:hover, .sf-grow-chip:hover { color: var(--sf-green, #85ad3b); }
.sf-grow-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
.sf-grow-chip { display: inline-block; padding: 8px 14px; border: 1px solid var(--sf-border, #e6e6e6); border-radius: 999px; text-decoration: none; line-height: 1.2; }
.sf-grow-chip:focus-visible, .sf-grow-list a:focus-visible { outline: 3px solid var(--sf-lime, #a2ce46); outline-offset: 2px; }
.sf-grow-faq .sf-faq-a p + p { margin-top: 8px; }
.sf-grow-faq .sf-faq-a ul { margin: 8px 0 0; padding-left: 18px; }


/* ============================================================================
   FOOTER NEWSLETTER = MAROPOST (8 Oct 2026) — templates/footers/template.html
   ============================================================================
   Measured on www.toolsinabox.com.au (footer .newsletter-register, style.css
   lines 2854-2875 + computed values): a square WHITE field 48px tall, 16px
   text with 15px side padding and a #999 placeholder, and a 55 x 42 GREEN
   (#8cc717) button sitting INSIDE the field, 3px from its right edge, with
   Maropost's white envelope. "Subscribe to our newsletter" is 13px with 16px
   under it. Celora's .sf-footer h4 keeps the heading the same as the other
   footer columns.
   These rules override baseline.css's older rounded "pill" (.sf-news,
   display:flex, overflow:hidden). Every selector is scoped to .sf-fnews so
   nothing else in the footer moves. No design token is declared here.
*/
.sf-footer .sf-fnews p { font-size: 13px; line-height: 18px; margin: 0 0 16px; }
.sf-footer .sf-fnews .sf-news {
  position: relative;
  display: block;
  margin: 0;
  background: #fff;
  border-radius: 0;
  overflow: visible;
}
.sf-footer .sf-fnews .sf-news input {
  display: block;
  width: 100%;
  min-width: 0;
  height: 48px;
  box-sizing: border-box;
  margin: 0;
  padding: 8px 66px 8px 15px;
  border: 0;
  border-radius: 0;
  background: #fff;
  color: #111;
  font-family: "Open Sans", sans-serif;
  font-size: 16px;
  line-height: 22px;
}
.sf-footer .sf-fnews .sf-news input::placeholder { color: #999; opacity: 1; }
.sf-footer .sf-fnews .sf-news input:focus-visible { outline: 2px solid #8cc717; outline-offset: 2px; }
.sf-footer .sf-fnews .sf-news button {
  position: absolute;
  top: 50%;
  right: 3px;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 55px;
  height: 42px;
  padding: 0;
  border: 1px solid #8cc717;
  border-radius: 0;
  background: #8cc717;
  color: #fff;
  font-size: 0;
  line-height: 0;
  cursor: pointer;
}
.sf-footer .sf-fnews .sf-news button:hover { background: #7db313; border-color: #7db313; }
.sf-footer .sf-fnews .sf-news button:focus-visible { outline: 2px solid #fff; outline-offset: -5px; }
.sf-footer .sf-fnews .sf-news button svg { display: block; width: 21px; height: 15px; }
.sf-footer .sf-fnews .sf-news-msg { margin: 10px 0 0; font-size: 13px; line-height: 1.4; color: #d8e6c4; }
.sf-footer .sf-fnews .sf-news-msg.is-error { color: #f7b9b9; }

/* ==========================================================================
   SAVE LABEL = MAROPOST (9 Oct 2026, seo-fix-c)
   Maropost's .save-label beside the price: 14px/18px, bold, #111, upper case,
   padding 0 4px, 16px from the price, sitting on the price's bottom line.
   ========================================================================== */
.sf-pdp-priceline .sf-pdp-save {
  font-size: 14px;
  line-height: 18px;
  font-weight: 700;
  color: #111;
  text-transform: uppercase;
  padding: 0 4px;
  margin-left: 4px;
  white-space: nowrap;
}

/* ⭐ 9 Oct 2026 — DELIVERY ESTIMATE ⓘ (custom.js etaLineHtml; React twin in
   resources/js/storefront/storefront.css — the theme does not load that bundle, so
   the same rules live here). The bubble is the design system's .cds-infotip-bubble
   (components.css), anchored to the LEFT of the estimate line so it never runs off a
   phone screen, and opened by a tap (.is-open) as well as hover and keyboard focus. */
.sf-dq-eta { position: relative; }
.sf-dq-eta-by { font-weight: 400; }
.sf-eta-tip { position: static; margin-left: 6px; cursor: pointer; }
.sf-eta-tip .cds-infotip-bubble {
  left: 0;
  transform: none;
  bottom: calc(100% + 6px);
  max-width: min(280px, 100%);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.4;
}
.sf-eta-tip.is-open .cds-infotip-bubble {
  opacity: 1;
  visibility: visible;
}

/* ⭐ 13 Oct 2026 (owner answer Q-contact-btn): the Contact Us "Send Message"
   button is the footer button's lime (#8cc717, hover #7db313). Dark text, so the
   words stay easy to read (white on this lime is too faint). Scoped to the
   Contact Us form only; the price-match and question forms are unchanged. */
form.tiab-cf[action$="/contact-us/send"] .tiab-cf-btn {
  background: #8cc717;
  color: #111;
  font-weight: 700;
}
form.tiab-cf[action$="/contact-us/send"] .tiab-cf-btn:hover { background: #7db313; color: #111; }
form.tiab-cf[action$="/contact-us/send"] .tiab-cf-btn:focus-visible { outline: 3px solid #111; outline-offset: 2px; }

/* ===========================================================================
   SHOP SPEED (13 Oct 2026) — pairs with the "SHOP SPEED" block at the end of
   js/custom.js. Nothing below changes how a finished page looks; it changes
   when pictures are fetched, and stops one phone-only glitch. Undo = delete.
   ========================================================================= */

/* Home banners are wrapped in <picture> so a phone never downloads the
   computer banner and a computer never downloads the phone one (the hidden
   rail's first picture used to download anyway). display:contents makes the
   wrapper invisible to layout, so the <img> sits exactly where it did. */
.sf-hero-slide picture { display: contents; }

/* Banners 2..N are not drawn (so not fetched) until custom.js marks the rail
   .is-warm at page load. Before this they all downloaded with the first one,
   because the cross-fade stacks them on screen at opacity 0. */
.sf-hero:not(.is-warm) .sf-hero-slide:not(.is-active) img { display: none; }

/* Featured Products background photo: fetched when the section nears the
   screen (custom.js adds .is-near), not with the first view. */
.sf-featured:not(.is-near) { background-image: none; }

/* ⭐ PRODUCT PAGES ON PHONES REPORTED NO "LARGEST CONTENTFUL PAINT" AT ALL.
   The thumbnail strip is a scroll-snap row with 2px padding, so the first
   thumbnail's snap point is 2px in. When the thumbnails overflow (a phone, 5+
   pictures) Chrome snapped the strip from 0 to 2px during load — and a scroll
   makes Chrome stop recording LCP for the page. Lighthouse/GTmetrix then
   score the page 0 ("NO_LCP"), and Google's field data loses it too.
   Matching scroll-padding puts the snap point at 0, so nothing scrolls. The
   strip now rests at 0 instead of 2px in (its 2px focus-ring padding shows). */
.cds-gallery-thumbs { scroll-padding-inline: 2px; }

/* =============================================================================
   COMPLIANCE 15 OCT 2026 (v2) — the small (i) next to Afterpay / Zip.
   Owner: "you could have done a hover or something or an info icon". The tip
   holds only the provider's required disclaimer + T&Cs link. Hover, keyboard
   focus or tap opens it (tabindex focus); CLOSED = display:none so it can never
   widen a phone page. Twin block at the end of resources/js/storefront/storefront.css.
   ============================================================================= */
.sf-bnpl-tip { position: relative; display: inline-block; margin-left: 3px; color: #6b7280; font-size: 13px;
  line-height: 1; cursor: pointer; outline: none; vertical-align: baseline;
  /* Zero net width: the icon never re-wraps the owner's text ("Or Zip now, pay later"
     exactly fills its 120px column), so the line breaks stay as before today. */
  width: 14px; margin-right: -17px; }
.sf-bnpl-tip:hover, .sf-bnpl-tip:focus, .sf-bnpl-tip:focus-within { color: #111; }
.sf-bnpl-tip:focus-visible { outline: 2px solid #1279c6; outline-offset: 2px; border-radius: 50%; }
.sf-bnpl-tipbox { display: none; position: absolute; z-index: 60; top: calc(100% + 6px); right: -8px; width: 260px;
  background: #fff; color: #333; border: 1px solid #d7dee8; border-radius: 6px; box-shadow: 0 6px 18px rgba(0, 0, 0, .15);
  padding: 8px 10px; font-size: 12px; line-height: 1.45; font-weight: 400; text-align: left; white-space: normal; }
.sf-bnpl-tipbox::before { content: ''; position: absolute; left: 0; right: 0; top: -8px; height: 8px; }
.sf-bnpl-tip:hover .sf-bnpl-tipbox, .sf-bnpl-tip:focus .sf-bnpl-tipbox, .sf-bnpl-tip:focus-within .sf-bnpl-tipbox { display: block; }
.sf-bnpl-tipbox a { color: #1279c6; text-decoration: underline; }
@media (max-width: 640px) {
  .sf-bnpl-tipbox { position: fixed; top: auto; bottom: 96px; left: 16px; right: 16px; width: auto; z-index: 9000; }
}
/* APP 5 note on the contact form: one short muted line. */
.tiab-cf-privacy { font-size: 12px; color: #666; margin: 4px 0 12px; line-height: 1.5; }
.tiab-cf-privacy a { color: inherit; text-decoration: underline; }


/* ⭐ 15 Oct 2026 — FEATURED PRODUCTS ARROWS (owner sent a picture: a white disc with
   a thin dark chevron, "for featured products can you make the arrows like these?
   on the homepage"). This OVERRIDES the shared hero/featured block above for the
   featured rail only: the hero keeps its translucent disc. The featured strip sits
   on a 0.8-white wash, where a white chevron on a translucent disc was hard to see.
   Placement (top / offsets) is unchanged. */
.sf-featured-nav {
  background: rgba(255, 255, 255, .92);
  color: #1f2328;
  text-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: 0 1px 6px rgba(0, 0, 0, .18);
}
.sf-featured-nav:hover { background: #fff; }
.sf-featured-nav svg {
  stroke-width: 1.5;
  filter: none;
}
@supports not ((-webkit-backdrop-filter: blur(2px)) or (backdrop-filter: blur(2px))) {
  .sf-featured-nav { background: rgba(255, 255, 255, .92); }
  .sf-featured-nav:hover { background: #fff; }
}
@media (max-width: 767.98px) {
  .sf-featured-nav { box-shadow: 0 1px 6px rgba(0, 0, 0, .18); }
}


/* ⭐ 15 Oct 2026 — PHONE: featured arrows covered the product title ("On mobile view
   the featured products arrows covers the text can you [put] the arrows on the far
   left and far right"). On a phone they now sit at the very edge of the screen and
   higher, over the product PHOTO (which is white space around the picture), never
   over the title or price. Smaller disc, still a 40px tap target. */
@media (max-width: 767.98px) {
  .sf-featured-nav {
    top: 26%;
    width: 40px;
    height: 40px;
    min-width: 40px;
    min-height: 40px;
  }
  .sf-featured-nav svg { width: 20px; height: 20px; }
  .sf-featured-nav--prev { left: 0; }
  .sf-featured-nav--next { right: 0; }
}


/* ⭐ 15 Oct 2026 (2nd pass, owner picture): on a phone the featured arrows sit HALF
   IN, HALF OUT of the product card - the disc's centre on the card's outer edge -
   instead of wholly inside it. Smaller light-grey disc, dark chevron, still over
   the photo band so it never covers the title or price. */
@media (max-width: 767.98px) {
  .sf-featured-car { overflow: visible; }
  .sf-featured-nav {
    top: 26%;
    width: 36px;
    height: 36px;
    min-width: 36px;
    min-height: 36px;
    background: rgba(238, 238, 238, .92);
    box-shadow: 0 1px 4px rgba(0, 0, 0, .18);
  }
  .sf-featured-nav svg { width: 18px; height: 18px; }
  .sf-featured-nav--prev { left: -18px; }
  .sf-featured-nav--next { right: -18px; }
}


/* ⭐ 15 Oct 2026 (3rd pass, owner: "make it centered its currently to high"): phone
   arrows centred on the card's height. Pushed a little further out (-22px) so the
   inside half stays in the card's side margin and does not cover the text. */
@media (max-width: 767.98px) {
  .sf-featured-nav { top: 50%; }
  .sf-featured-nav--prev { left: -22px; }
  .sf-featured-nav--next { right: -22px; }
}


/* ============================================================================
   ⭐ 15 Oct 2026 — PHONE FOOTER LAID OUT LIKE MAROPOST (owner sent a picture of
   his Maropost phone footer: "can you make the footer more organised on mobile
   view. layed out more like this?").
   PHONE ONLY (< 768px). DESKTOP IS UNCHANGED: the few new elements in the footer
   markup (pin + "NSW", phone icon, social line icons, payment row) are hidden by
   the first rule below, and the desktop keeps its letter chips. Proven by
   identical 1440px screenshots before/after.
   Phone: logo centred, tagline hidden, Store = pin/NSW + address + tap-to-call,
   My Account and Information side by side, newsletter untouched, white line
   icons (no grey circles), a thin green line + payment logos, centred legal
   bar with no divider bars. 13px links, 16px headings.
   Twins: end of storage/themes/celora-classic/css/style.css (themed pages) and
   end of resources/js/storefront/storefront.css (React account/checkout).
   No design token is declared here (see CssVariables).
   ========================================================================== */
.sf-footer .sf-fstore-pin,
.sf-footer .sf-fstore-tel,
.sf-footer .sf-social .sf-soc-ico,
.sf-footer .sf-footer-pay { display: none; }

@media (max-width: 767.98px) {
  .sf-footer { padding-top: 28px; }
  .sf-footer .sf-footer-inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 24px 16px;
    padding: 0 16px;
  }
  .sf-footer .sf-footer-inner > .sf-fcol { grid-column: 1 / -1; min-width: 0; }
  /* My Account + Information (the managed menus) sit side by side. */
  .sf-footer .sf-footer-inner > nav.sf-fcol { grid-column: auto; }

  .sf-footer h4 { font-size: 16px; line-height: 1.25; margin: 0 0 10px; }
  .sf-footer p { font-size: 13px; line-height: 1.5; }
  .sf-footer a { font-size: 13px; }
  .sf-footer li { margin: 0 0 9px; line-height: 1.35; }

  /* Logo centred; the tagline is not on the Maropost phone footer. */
  .sf-footer .sf-fbrand { text-align: center; }
  .sf-footer .sf-fbrand .sf-footer-logo { width: 150px; margin: 0 auto; }
  .sf-footer .sf-fbrand p { display: none; }

  /* Store: pin + "NSW" on the left, address and phone beside it. */
  .sf-footer .sf-fstore {
    display: grid;
    grid-template-columns: 46px minmax(0, 1fr);
    column-gap: 6px;
    align-items: start;
  }
  .sf-footer .sf-fstore > h4 { grid-column: 1 / -1; }
  .sf-footer .sf-fstore > p { grid-column: 2; margin: 0 0 8px; }
  .sf-footer .sf-fstore-pin {
    grid-column: 1;
    grid-row: 2 / span 2;
    display: flex;
    align-items: flex-end;
    gap: 1px;
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: .3px;
  }
  .sf-footer .sf-fstore-pin svg { width: 18px; height: 18px; flex: none; }
  .sf-footer .sf-fstore .sf-footer-phone a {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: #fff !important;
    font-size: 14px;
    font-weight: 600;
  }
  .sf-footer .sf-fstore-tel { display: inline-flex; }
  .sf-footer .sf-fstore-tel svg { width: 14px; height: 14px; }

  /* Social: plain white line icons, no grey circles. */
  .sf-footer .sf-social { gap: 20px; margin-top: 18px; align-items: center; }
  .sf-footer .sf-social a {
    width: 32px;
    height: 32px;
    border-radius: 0;
    background: transparent;
    color: #fff;
  }
  .sf-footer .sf-social a:hover { background: transparent; color: var(--sf-lime, #a2ce46); }
  .sf-footer .sf-social .sf-soc-txt { display: none; }
  .sf-footer .sf-social .sf-soc-ico { display: block; width: 21px; height: 21px; }
  .sf-footer .sf-social a > svg:not(.sf-soc-ico) { width: 21px; height: 15px; }

  /* Thin green line, then the payment logos. */
  .sf-footer .sf-footer-pay {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 10px;
    margin: 24px 16px 0;
    padding: 14px 0 0;
    border-top: 2px solid #8cc717;
  }
  .sf-footer .sf-fpay-mark { display: inline-flex; line-height: 0; }
  .sf-footer .sf-fpay-mark svg { display: block; width: auto; height: 24px; }

  /* Legal bar: small, centred, no "|" divider bars. Bottom room stays for the
     fixed "Text us" pill. */
  .sf-footer .sf-footer-bar {
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 6px;
    margin-top: 14px;
    padding: 12px 16px 76px;
    text-align: center;
    font-size: 12px;
    line-height: 1.5;
  }
  .sf-footer .sf-footer-bar .sf-footer-copy { font-size: 12px; line-height: 1.5; margin: 0; }
  .sf-footer .sf-footer-abn { white-space: nowrap; }
  .sf-footer .sf-footer-legal { justify-content: center; gap: 2px 18px; padding: 0; }
  .sf-footer .sf-footer-legal a { padding: 0; font-size: 12px; }
  .sf-footer .sf-footer-legal a + a { border-left: 0; }
}


/* =============================================================================
   CONTACT US + ABOUT US REDESIGN (15 Oct 2026)
   Owner: "can you make our contact page more better and professional?" and
   "our about_us looks horrible". Markup = cms_pages bodies 260 (/contact-us)
   and 256 (/about_us). EVERY rule is scoped to .tiab-contact / .tiab-about, so
   no other page, the header and the footer are untouched.
   Icons are CSS data: SVGs (CSP img-src allows data:), so the page editor can
   never strip them, and there is no script. The form itself keeps its own
   .tiab-cf rules above. Twin block at the end of the other stylesheet
   (style.css <-> resources/js/storefront/storefront.css).
   Undo = delete this block and restore both rows from
   storage/app/contact-page-20261015/cms_pages-256-260-before.sql.
   ============================================================================= */
.tiab-contact, .tiab-about { --tiab-acc: #8cc717; --tiab-acc-hover: #7db313; --tiab-acc-ink: #5e8a10; --tiab-ink: #111; --tiab-line: #e3e5e8; --tiab-muted: #555; }

/* --- Contact: intro + two columns (info cards | form card) ----------------- */
.tiab-contact .tiab-contact-intro { margin: 0 0 24px; max-width: 62ch; font-size: 17px; line-height: 1.6; color: var(--tiab-muted); }
.tiab-contact-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; align-items: start; }
@media (min-width: 960px) { .tiab-contact-grid { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 32px; } }
.tiab-contact-info { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
@media (min-width: 640px) and (max-width: 959px) { .tiab-contact-info { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.tiab-contact .tiab-ccard { position: relative; margin: 0; padding: 20px 20px 20px 80px; min-height: 86px; background: #fff; border: 1px solid var(--tiab-line); border-radius: 8px; }
.tiab-contact .tiab-ccard::before { content: ''; position: absolute; left: 20px; top: 20px; width: 44px; height: 44px; border-radius: 50%; background-color: var(--tiab-ink); background-repeat: no-repeat; background-position: center; background-size: 22px 22px; }
.tiab-contact .tiab-ccard--visit::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238cc717' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z'/%3E%3Ccircle cx='12' cy='10' r='3'/%3E%3C/svg%3E"); }
.tiab-contact .tiab-ccard--call::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238cc717' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z'/%3E%3C/svg%3E"); }
.tiab-contact .tiab-ccard--email::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238cc717' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='4' width='20' height='16' rx='2'/%3E%3Cpath d='m22 7-10 6L2 7'/%3E%3C/svg%3E"); }
.tiab-contact .tiab-ccard--hours::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238cc717' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 6v6l4 2'/%3E%3C/svg%3E"); }
.tiab-contact .tiab-ccard-title { margin: 0 0 6px; font-family: var(--sf-font-display, inherit); font-size: 18px; font-weight: 600; line-height: 1.3; color: var(--tiab-ink); }
.tiab-contact .tiab-ccard-text { margin: 0; line-height: 1.55; }
.tiab-contact .tiab-ccard-action { margin: 12px 0 0; }
.tiab-contact .tiab-ccard a { color: var(--tiab-ink); text-decoration: underline; text-decoration-color: var(--tiab-acc); text-decoration-thickness: 2px; text-underline-offset: 3px; }
.tiab-contact .tiab-ccard a:hover { color: var(--tiab-acc-ink); }
.tiab-contact .tiab-ccard a:focus-visible { outline: 3px solid var(--tiab-ink); outline-offset: 2px; border-radius: 2px; }
.tiab-contact .tiab-ccard a.tiab-ccard-big { font-size: 18px; font-weight: 700; text-decoration: none; }
.tiab-contact .tiab-ccard a.tiab-ccard-big:hover { text-decoration: underline; text-decoration-color: var(--tiab-acc); }
.tiab-contact .tiab-ccard-list { list-style: none; margin: 0; padding: 0; }
.tiab-contact .tiab-ccard-list > li { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; margin: 0 0 4px; }
.tiab-contact .tiab-ccard-list > li:last-child { margin-bottom: 0; }
.tiab-contact .tiab-ccard-label { min-width: 56px; font-size: 14px; color: var(--tiab-muted); }
.tiab-contact a.tiab-ccard-btn { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 2px solid var(--tiab-ink); border-radius: 4px; font-size: 14px; font-weight: 700; line-height: 1.3; color: var(--tiab-ink); text-decoration: none; background: #fff; }
.tiab-contact a.tiab-ccard-btn::after { content: '\2192'; font-weight: 700; }
.tiab-contact a.tiab-ccard-btn:hover { background: var(--tiab-ink); color: #fff; }

/* Opening hours: a two-column table (day | time). `display: table` beats the
   CMS-wide `.sf-content-body table { display: block }` scroll wrapper. */
.tiab-contact table.tiab-hours { display: table; width: 100%; max-width: 100%; margin: 2px 0 0; border-collapse: collapse; font-size: 15px; }
.tiab-contact .tiab-hours th,
.tiab-contact .tiab-hours td { padding: 6px 0; border: 0; border-bottom: 1px dashed var(--tiab-line); text-align: left; vertical-align: baseline; }
.tiab-contact .tiab-hours th { font-weight: 600; color: var(--tiab-ink); padding-right: 12px; }
.tiab-contact .tiab-hours td { text-align: right; white-space: nowrap; }
.tiab-contact .tiab-hours tr:last-child th,
.tiab-contact .tiab-hours tr:last-child td { border-bottom: 0; }
.tiab-contact .tiab-hours-closed td { color: var(--tiab-muted); }

/* The form card: white, lime top edge (the site's button/footer lime). */
.tiab-contact .tiab-contact-formcard { padding: 28px; background: #fff; border: 1px solid var(--tiab-line); border-top: 4px solid var(--tiab-acc); border-radius: 8px; box-shadow: 0 1px 3px rgba(0, 0, 0, .05); }
.tiab-contact .tiab-contact-formcard .tiab-cf-heading { margin: 0 0 18px; font-size: 24px; line-height: 1.25; }
.tiab-contact .tiab-contact-formcard .tiab-cf-btn { min-width: 180px; }

@media (max-width: 479px) {
  .tiab-contact .tiab-ccard { padding: 16px 16px 16px 66px; }
  .tiab-contact .tiab-ccard::before { left: 14px; top: 16px; width: 38px; height: 38px; background-size: 19px 19px; }
  .tiab-contact .tiab-ccard a.tiab-ccard-big { font-size: 16px; }
  .tiab-contact .tiab-contact-formcard { padding: 20px 16px; }
  .tiab-contact .tiab-contact-formcard .tiab-cf-btn { width: 100%; }
}

/* --- About: hero, why-choose cards, story, feature cards, call to action --- */
.tiab-about .tiab-about-hero { margin: 8px 0 40px; padding: 40px 36px; background: var(--tiab-ink); color: #fff; border-left: 6px solid var(--tiab-acc); border-radius: 8px; }
.tiab-about .tiab-about-kicker { margin: 0 0 10px; color: var(--tiab-acc); font-size: 13px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
.tiab-about .tiab-about-lead { margin: 0 0 12px; max-width: 36ch; font-family: var(--sf-font-display, inherit); font-size: clamp(22px, 3vw, 32px); font-weight: 600; line-height: 1.25; color: #fff; }
.tiab-about .tiab-about-sub { margin: 0; max-width: 62ch; font-size: 17px; line-height: 1.6; color: #d9d9d9; }

.tiab-about .tiab-about-section { margin: 0 0 48px; }
.tiab-about .tiab-about-h2 { margin: 0 0 22px; font-family: var(--sf-font-display, inherit); font-size: clamp(22px, 2.4vw, 28px); line-height: 1.25; color: var(--tiab-ink); }
.tiab-about .tiab-about-h2::after { content: ''; display: block; width: 48px; height: 4px; margin-top: 10px; border-radius: 2px; background: var(--tiab-acc); }
.tiab-about .tiab-about-h3 { margin: 0 0 10px; font-size: 20px; line-height: 1.3; color: var(--tiab-ink); }

.tiab-about-why { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
@media (min-width: 640px) { .tiab-about-why { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .tiab-about-why { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.tiab-about .tiab-why { position: relative; padding: 80px 20px 22px; background: #fff; border: 1px solid var(--tiab-line); border-radius: 8px; }
.tiab-about .tiab-why::before { content: ''; position: absolute; left: 20px; top: 20px; width: 44px; height: 44px; border-radius: 50%; background-color: var(--tiab-ink); background-repeat: no-repeat; background-position: center; background-size: 22px 22px; }
.tiab-about .tiab-why--family::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238cc717' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='9' cy='7' r='4'/%3E%3Cpath d='M22 21v-2a4 4 0 0 0-3-3.87'/%3E%3Cpath d='M16 3.13a4 4 0 0 1 0 7.75'/%3E%3C/svg%3E"); }
.tiab-about .tiab-why--showroom::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238cc717' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z'/%3E%3Ccircle cx='12' cy='10' r='3'/%3E%3C/svg%3E"); }
.tiab-about .tiab-why--range::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238cc717' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 8 12 3 3 8l9 5 9-5Z'/%3E%3Cpath d='M3 8v8l9 5 9-5V8'/%3E%3Cpath d='M12 13v8'/%3E%3C/svg%3E"); }
.tiab-about .tiab-why--warranty::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238cc717' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z'/%3E%3Cpath d='m9 12 2 2 4-4'/%3E%3C/svg%3E"); }
.tiab-about .tiab-why-title { margin: 0 0 6px; font-size: 18px; line-height: 1.3; color: var(--tiab-ink); }
.tiab-about .tiab-why p { margin: 0; font-size: 15px; line-height: 1.6; color: var(--tiab-muted); }

.tiab-about .tiab-about-timeline { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; list-style: none; margin: 0 0 36px; padding: 0; }
@media (min-width: 768px) { .tiab-about .tiab-about-timeline { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; } }
.tiab-about .tiab-about-timeline > li { margin: 0; padding: 14px 0 0; border-top: 4px solid var(--tiab-acc); }
.tiab-about .tiab-tl-year { display: block; margin: 0 0 4px; font-family: var(--sf-font-display, inherit); font-size: 24px; font-weight: 700; line-height: 1.2; color: var(--tiab-ink); }
.tiab-about .tiab-tl-text { display: block; line-height: 1.6; color: var(--tiab-muted); }

.tiab-about-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px 48px; }
@media (min-width: 900px) { .tiab-about-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.tiab-about .tiab-about-col p { line-height: 1.75; }

.tiab-about-features { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; }
@media (min-width: 768px) { .tiab-about-features { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.tiab-about .tiab-feat { display: flex; flex-direction: column; overflow: hidden; background: #fff; border: 1px solid var(--tiab-line); border-radius: 8px; }
.tiab-about .tiab-feat-media { height: 220px; padding: 16px; background: #fff; border-bottom: 1px solid var(--tiab-line); }
.tiab-about .tiab-feat-media img { display: block; width: 100%; height: 100%; max-width: 100%; object-fit: contain; }
.tiab-about .tiab-feat-title { margin: 18px 20px 8px; font-size: 19px; line-height: 1.3; color: var(--tiab-ink); }
.tiab-about .tiab-feat > p { margin: 0 20px 12px; font-size: 15px; line-height: 1.6; color: var(--tiab-muted); }
.tiab-about .tiab-feat-list { list-style: none; margin: 0 20px 20px; padding: 0; }
.tiab-about .tiab-feat-list > li { position: relative; margin: 0 0 6px; padding-left: 26px; font-size: 15px; line-height: 1.5; }
.tiab-about .tiab-feat-list > li::before { content: ''; position: absolute; left: 0; top: .2em; width: 18px; height: 18px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235e8a10' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") no-repeat center / 18px 18px; }

.tiab-about .tiab-about-cta { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; align-items: center; margin: 0; padding: 28px 32px; background: #f4f7ec; border: 1px solid #dfe8c8; border-radius: 8px; }
@media (min-width: 900px) { .tiab-about .tiab-about-cta { grid-template-columns: minmax(0, 1fr) auto; gap: 32px; } }
.tiab-about .tiab-about-cta-text { margin: 0; font-size: 17px; font-weight: 600; line-height: 1.6; color: var(--tiab-ink); }
.tiab-about .tiab-about-cta-btns { display: flex; flex-wrap: wrap; gap: 12px; margin: 0; }
.tiab-about a.tiab-about-btn { display: inline-block; padding: 12px 22px; background: var(--tiab-acc); border: 2px solid var(--tiab-acc); border-radius: 4px; font-weight: 700; line-height: 1.3; color: var(--tiab-ink); text-decoration: none; white-space: nowrap; }
.tiab-about a.tiab-about-btn:hover { background: var(--tiab-acc-hover); border-color: var(--tiab-acc-hover); color: var(--tiab-ink); }
.tiab-about a.tiab-about-btn--ghost { background: transparent; border-color: var(--tiab-ink); }
.tiab-about a.tiab-about-btn--ghost:hover { background: var(--tiab-ink); border-color: var(--tiab-ink); color: #fff; }
.tiab-about a.tiab-about-btn:focus-visible { outline: 3px solid var(--tiab-ink); outline-offset: 2px; }

/* Phones: the why-cards put the icon beside the words, like the contact cards. */
@media (max-width: 639px) {
  .tiab-about .tiab-why { padding: 18px 18px 18px 76px; min-height: 82px; }
  .tiab-about .tiab-why::before { left: 18px; top: 18px; }
}
@media (max-width: 479px) {
  .tiab-about .tiab-about-hero { padding: 26px 20px; }
  .tiab-about .tiab-about-cta-text { font-size: 16px; }
  .tiab-about .tiab-about-cta { padding: 22px 18px; }
  .tiab-about .tiab-about-cta-btns a.tiab-about-btn { flex: 1 1 auto; text-align: center; }
}
/* ===== END CONTACT US + ABOUT US REDESIGN ===== */


/* ===== REVIEWS OPTION A — "Summary + cards" (15 Oct 2026) =====================
   The owner's pick from celora-work/Review-Designs.html. Product-page reviews
   section ONLY. The same block is appended to BOTH
   storage/themes/celora-classic/css/style.css (themed PDP) and
   resources/js/storefront/storefront.css (the React twin, ProductPage.tsx).

   Markup (reviews.template.html / ProductPage.tsx Reviews):
     .sf-rv.sf-rv--a
       .sf-rv-side   light panel: .sf-rv-summary (score + 5→1 bars),
                     .sf-rv-write (green "Write a review"), .sf-rv-strip
                     ("Photos from customers"; custom.js appends it on a
                     themed page)
       .sf-rv-main   .sf-rv-controls, .sf-rv-none, .sf-rv-list, .sf-rv-more

   ⭐ The card list is a CSS GRID, NOT newspaper columns. A grid fills row by
   row, so what a shopper reads (left→right, top→bottom) IS the sort order the
   Sort box chose; columns would read down the first column first and make
   "Most recent" look shuffled. Cards hidden by a filter or by "Show N more"
   are display:none and simply leave the grid. Cards in a row stretch to the
   same height so the rows line up.

   Long bodies are clipped to 6 lines (.sf-rv-body.is-clamped) with a
   "Read more" toggle (.sf-rv-readmore). The toggle is added by custom.js /
   ReviewBody ONLY when the text really overflows; without the script nothing
   is ever clipped.

   ≥1024px: panel 320px | cards in 2 columns.
   640–1023px: panel on top (score beside the bars) | cards in 2 columns.
   <640px: one column, summary panel first.

   Review data, sources and the JSON-LD are untouched (owner rule
   celora-reviews-leave-as-is-2026-10-15): this is presentation only.
   ========================================================================== */
.sf-rv--a {
  --sf-rv-a-star: #d97706;           /* the same amber as the rating stars */
  --sf-rv-a-tint: #f7f8f4;
  --sf-rv-a-line: var(--sf-rv-border, #e6e6e6);
  --sf-rv-a-radius: 14px;
  --sf-rv-a-lines: 6;                /* "Read more" after this many lines */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 28px;
  align-items: start;
}
@media (min-width: 1024px) {
  .sf-rv--a { grid-template-columns: 320px minmax(0, 1fr); gap: 36px; }
}

/* --- the left panel ------------------------------------------------------ */
.sf-rv--a .sf-rv-side {
  display: flex;
  flex-direction: column;
  gap: 20px;
  min-width: 0;
  padding: 24px;
  background: var(--sf-rv-a-tint);
  border: 1px solid var(--sf-rv-a-line);
  border-radius: var(--sf-rv-a-radius);
}
.sf-rv--a .sf-rv-summary {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}
/* 5.0 on the left, the stars and "from N ratings" beside it. With no star
   rating at all there is only the note, which then takes the full width. */
.sf-rv--a .sf-rv-score {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 16px;
  row-gap: 4px;
  align-items: center;
}
.sf-rv--a .sf-rv-avg { grid-row: span 2; font-size: 64px; letter-spacing: -1px; }
.sf-rv--a .sf-rv-score .cds-rating { align-self: end; }
.sf-rv--a .sf-rv-score-note { align-self: start; }
.sf-rv--a .sf-rv-score > .sf-rv-score-note:only-child { grid-column: 1 / -1; }
.sf-rv--a .sf-rv-thin { max-width: none; }
.sf-rv--a .sf-rv-bars { gap: 4px; }
.sf-rv--a .sf-rv-bar-row { padding: 3px 6px; }
.sf-rv--a .sf-rv-bar { height: 10px; }
.sf-rv--a .sf-rv-bar-fill { background: var(--sf-rv-a-star); }

/* "Write a review": green and full width, like ADD TO CART. The theme's button
   is .sf-rv-writebtn (opens the dialog in custom.js); the React twin's is
   .sf-rv-write-open (opens its inline form). */
.sf-rv--a .sf-rv-side .sf-rv-write { margin: 0; padding: 0; border: 0; }
.sf-rv--a .sf-rv-writebtn,
.sf-rv--a .sf-rv-write-open {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 46px;
  padding: 0 22px;
  background: var(--sf-action, #8cc717);
  border: 2px solid var(--sf-action, #8cc717);
  border-radius: 0;
  color: #fff;
  font-family: var(--sf-font-display);
  font-weight: 700;
  font-size: var(--sf-text-md, 15px);
  letter-spacing: .02em;
  text-transform: uppercase;
  cursor: pointer;
}
.sf-rv--a .sf-rv-writebtn:hover,
.sf-rv--a .sf-rv-write-open:hover {
  background: var(--sf-action-hover, #79b012);
  border-color: var(--sf-action-hover, #79b012);
  color: #fff;
}
.sf-rv--a .sf-rv-writebtn:focus-visible,
.sf-rv--a .sf-rv-write-open:focus-visible { outline: 2px solid var(--sf-fg, #111); outline-offset: 2px; }

/* "Photos from customers" at the foot of the panel: wraps instead of scrolling
   sideways, and a product with a lot of photos scrolls inside the panel
   rather than making it endless. Every photo still opens in the viewer. */
.sf-rv--a .sf-rv-strip-list { flex-wrap: wrap; overflow-x: visible; overflow-y: auto; max-height: 276px; }
.sf-rv--a .sf-rv-strip-btn img { width: 84px; height: 84px; }

/* --- the cards ----------------------------------------------------------- */
.sf-rv--a .sf-rv-main { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.sf-rv--a .sf-rv-controls { padding-bottom: 12px; }
.sf-rv--a .sf-rv-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
}
.sf-rv--a .sf-rv-list[hidden] { display: none; }
@media (min-width: 640px) {
  .sf-rv--a .sf-rv-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.sf-rv--a .sf-rv-card {
  min-width: 0;
  padding: 18px 18px 16px;
  background: #fff;
  border: 1px solid var(--sf-rv-a-line);
  border-radius: var(--sf-rv-a-radius);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .04);
}
.sf-rv--a .sf-rv-card-head { align-items: center; }
.sf-rv--a .sf-rv-body { max-width: none; overflow-wrap: anywhere; }
.sf-rv--a .sf-rv-photos img { width: 80px; height: 80px; }
.sf-rv--a .sf-rv-reply { margin-top: 2px; }
.sf-rv--a .sf-rv-more { align-self: center; margin-top: 4px; }

/* "Read more" — clipping only ever happens once the script has added the
   class, and it only adds it when the text overflows. */
.sf-rv-body.is-clamped {
  position: relative;
  max-height: calc(var(--sf-rv-a-lines, 6) * var(--sf-rv-body-leading, 1.7) * 1em);
  overflow: hidden;
}
.sf-rv-body.is-clamped::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2.2em;
  background: linear-gradient(rgba(255, 255, 255, 0), #fff);
  pointer-events: none;
}
.sf-rv-readmore {
  align-self: flex-start;
  margin-top: -4px;
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  font-size: var(--sf-text-sm, 13px);
  font-weight: 700;
  color: var(--sf-fg, #111);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.sf-rv-readmore:focus-visible { outline: 2px solid var(--sf-green, #85ad3b); outline-offset: 2px; }

/* --- tablet: panel across the top, score beside the bars ----------------- */
@media (min-width: 640px) and (max-width: 1023.98px) {
  .sf-rv--a .sf-rv-summary {
    display: grid;
    grid-template-columns: minmax(200px, 1fr) minmax(0, 1.4fr);
    align-items: center;
    gap: 24px;
  }
  .sf-rv--a .sf-rv-writebtn,
  .sf-rv--a .sf-rv-write-open { display: inline-flex; width: auto; min-width: 260px; padding: 0 28px; }
  .sf-rv--a .sf-rv-side .sf-rv-write { align-self: flex-start; }
}

/* --- phone: one column, summary panel first ------------------------------ */
@media (max-width: 639.98px) {
  .sf-rv--a { gap: 22px; }
  .sf-rv--a .sf-rv-side { padding: 18px; gap: 18px; }
  .sf-rv--a .sf-rv-avg { font-size: 52px; }
  .sf-rv--a .sf-rv-strip-btn img { width: 76px; height: 76px; }
  .sf-rv--a .sf-rv-card { padding: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .sf-rv--a .sf-rv-writebtn, .sf-rv--a .sf-rv-write-open { transition: none; }
}
/* ===== END REVIEWS OPTION A ===== */


/* ===== REVIEWS OPTION A — follow-up (15 Oct 2026) ==========================
   1. The base .sf-rv-summary carries align-items:center (it was a grid row).
      As the panel's flex column that shrank the star bars to their labels and
      centred the score, so the column is stretched again.
   2. Three customer photos must fit one row of the 320px panel (272px inside
      it): 80px thumbnails, not 84. */
.sf-rv--a .sf-rv-summary { align-items: stretch; }
@media (min-width: 640px) and (max-width: 1023.98px) {
  .sf-rv--a .sf-rv-summary { align-items: center; }
}
@media (min-width: 640px) {
  .sf-rv--a .sf-rv-strip-btn img { width: 80px; height: 80px; }
}
/* ===== END REVIEWS OPTION A follow-up ===== */

/* ── 17 Oct 2026 (go-live night): NO JUMP ON PHONE CATEGORY PAGES ────────────────
   js/custom.js (sidebar()) folds the Filters box shut on phones (<=820px) when it runs.
   The page HTML arrived OPEN, so ~2.5 s in on a slow phone the box folded and the
   product grid jumped up: Google measured CLS 0.129 (limit 0.1). The HTML now carries
   `sf-ssr-fold` (sidebar.template.html) so phones show it folded from the first paint;
   custom.js removes the class as soon as it takes over the fold, so the open/close
   button works exactly as before. Measured after: CLS 0.0003. */
@media (max-width: 820px) {
  .sf-side-filters.sf-ssr-fold .sf-side-body { display: none; }
  .sf-side-filters.sf-ssr-fold .sf-side-chev { transform: rotate(180deg); }
}
