/**
 * Waimers base stylesheet.
 * Design tokens, modern reset, type scale and shared utility classes.
 * Color/font custom properties are injected by snippets/css-variables.liquid;
 * the values below are fallbacks for contexts where that snippet hasn't rendered.
 */

/* ------------------------------------------------------------------ */
/* Tokens                                                              */
/* ------------------------------------------------------------------ */
/* The values below are FALLBACKS, not the live theme. The values that
   actually reach the page are generated by snippets/css-variables.liquid,
   which emits a :root block from colour scheme 1 and a .color-scheme-<id>
   rule per scheme, from the merchant's own colour settings. These tokens
   only take effect where that snippet hasn't rendered.

   Dark mode is part of that same generator, not a separate block here: when
   a merchant turns on the enable_dark_mode setting, css-variables.liquid
   also emits a :root[data-theme='dark'] rule (and a prefers-color-scheme
   media query) that redefines these same custom properties from colour
   scheme 2 instead of scheme 1.

   What none of this covers: colours written literally elsewhere. A white
   heading over a video scrim SHOULD stay white in both themes, so those are
   correct as they are; a literal white used as a PAGE surface is not, and
   those are being corrected as they are found. */
:root {
  --color-bg: #ffffff;
  --color-ink: #1f1f21;
  --color-surface: #F4F4F6;
  --color-brand: #1F1F21;
  --color-on-brand: #FFFFFF;
  --color-accent: #1F1F21;
  --color-accent-2: #1F1F21;
  --color-line: #e5e5e5;
  --color-muted: #6b6b6e;
  --color-success: #1f9254;
  --color-danger: #d2331f;

  /* Light is the default and stays on :root, so anything that never opts into
     a theme keeps working exactly as before. */

  --font-heading: 'Archivo', sans-serif;
  --font-body: 'Figtree', sans-serif;

  --radius-s: 4px;
  --radius-m: 8px;
  --radius-pill: 999px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 40px;
  --space-8: 48px;
  --space-9: 64px;
  --space-10: 80px;

  /* Held at 1440, deliberately, after briefly being 1920.
     
     Seventy-six sections and snippets read this - cart, account, blog,
     articles, contact, search and the product buy column among them, and
     every one of those is text-led. Widening the shell to suit a handful of
     image-led sections made all of those worse: longer measures, a cart with
     its summary stranded across the page, an info column stretched thin.
     
     Width is opted INTO by the components that need it, not imposed on the
     seventy-six that do not. blocks/_product-overview.liquid breaks out to
     100vw on its own for exactly this reason, and anything else that wants
     the full screen can do the same in four lines. */
  --page-width: 1440px;
  --page-margin: clamp(20px, 2.5vw, 40px);

  /* The sticky header's height. Lives here rather than in section-header.css
     because anything that parks itself below the header has to know it -
     a sticky sidebar with its own guess at the number ends up underneath the
     bar the moment either value changes. */
  /* 110px, and the same at every scroll position.
     
     It was 112, then briefly 74 while chasing a 110px total for the top of
     the page - which got the total right and made the SCROLLED header too
     short, because that is the state you look at for the rest of the visit.
     110 is the height that reads correctly there, so it is the one that
     stays; the announcement bar coming down from 52 to 36 is what pays for
     it at the top.
     
     One token, because sticky offsets read it - a collection page's filter
     rail parks itself below the header using this number, and a header that
     changed height without it would be overlapped. */
  /* 110px was tall enough that it read as a banner rather than a bar; most
     storefronts sit at 64-80. Lowered on request 2026-09-01. This is NOT
     header-only - the product overview's pinned scroller measures its sticky
     top and its height against it (component-product-overview.css), so every
     pixel taken off here is a pixel given back to that frame. */
  --header-height: 76px;

  /* Form control height. Anything sitting ON a row with an input - a submit
     button beside a search field, say - has to match it, and .btn is 56px,
     so the two disagreed by 8px wherever they were placed side by side. */
  --field-height: 48px;

  /* Overlay motion. Every overlay (cart drawer, quick view, concierge
     modal, gallery lightbox, search overlay, mobile drawer) used to pick
     its own entry timing - 320ms, 380ms and 0.4s, two of them on a bare
     `ease` - so panels of the same kind opened at visibly different
     speeds. One value per kind of motion, read by all six:
       --motion-overlay       panel slide/scale/fade, in AND out
       --motion-overlay-ease  the theme's house curve for that panel move
       --motion-scrim         the scrim/backdrop crossfade behind it
     The exit is deliberately the same length as the entry here: an overlay
     leaving faster than it arrived reads as a dropped frame rather than a
     dismissal (this is the opposite of the .btn/[data-icon-close] cursor
     fill, where the asymmetry is the point). */
  --motion-overlay: 320ms;
  --motion-overlay-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --motion-scrim: 320ms;

  /* Cursor-origin hover fill: the ONE source of truth for the sweep's
     timing, for every filled control on the site.

     These used to be declared on `.btn` itself, which meant only buttons
     and their descendants could see them. The tab pills in
     section-tabbed-products.css and section-social-wall.css are not
     descendants of a button, so both files kept their own literal copies
     (--tp-fill-* and --sw-fill-*), each with a comment asking for exactly
     this hoist. That drift was not theoretical: when the sweep was retuned
     to 3s in / 0.5s out, the two pill rows silently kept the old numbers.
     On :root, every control inherits them and one edit here retunes all of
     them.

     Slow and symmetric-looking but deliberately asymmetric on the
     merchant's instruction - the sweep is meant to be watched rather than
     glimpsed, while un-sweeping stays quick so running the cursor along a
     row leaves no trail of half-filled controls.

     The label colour crossfade reads these too, and must share the fill's
     easing as well as its duration in BOTH directions. Matching duration
     alone still desyncs: the fill's exit is ease-in (barely moves, then
     accelerates) while a default `ease` colour is already well underway, so
     the label reverts while the fill still covers it. */
  /* Was 3s; the owner found it (and the label colour riding it) too slow on
     wide buttons such as the Find your match form, 30 Sep 2026. */
  --btn-fill-in: 1.2s;
  --btn-fill-out: 0.5s;
  --btn-fill-in-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --btn-fill-out-ease: ease-in;

  /* The circular [data-icon-close] controls run the same mechanism at their
     own pace: an overlay's close button is a small target that has to
     answer immediately, so it cannot wait out the 3s button sweep. That is
     a deliberate difference, which is why it gets its own tokens rather
     than reusing --btn-fill-* - but tokens it must be. These four values
     were previously written out as literals five times in the
     [data-icon-close] block below, forty lines under a .btn block that was
     already reading variables. */
  /* Real values, NOT var(--btn-fill-*). Aliasing them to the button pair is
     what the comment above argues against, but that is exactly what shipped -
     so the close circle sat on the 3s button sweep and took three seconds to
     answer a click that is meant to dismiss something. A dismissal control
     has to look decided; anything you can watch fill is too slow here. */
  --icon-close-fill-in: 1.1s;
  --icon-close-fill-out: 0.5s;
  --icon-close-fill-in-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --icon-close-fill-out-ease: ease-in;
  /* The glyph turn and the colour crossfade that ride along with the fill.
     Held separate from --icon-close-fill-in-ease only because that is what
     ships today; see the note on the turn rule below. */
  --icon-close-turn-in-ease: ease-out;

  /* Icon micro-motion. Small and FAST relative to either fill pair above:
     the sweep is the thing being watched, the glyph's nudge is a flicker of
     acknowledgement underneath it. Anything slower reads as the icon
     drifting rather than answering.

     Deliberately its own pair rather than a third reuse of --icon-close-*:
     that pair is tuned to how long a 32px circle takes to fill, which is a
     different question from how long a 2px nudge should take.

     The distances live here too, in SVG user units (see the icon block near
     the bottom of this file for why the motion is applied to the glyph's
     children rather than to the <svg> itself). One unit is roughly one
     device pixel at the sizes these icons render at. */
  --icon-motion-in: 0.22s;
  --icon-motion-out: 0.18s;
  --icon-motion-in-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --icon-motion-out-ease: ease-in;
  --icon-motion-travel: 1.6px;
  --icon-motion-lift: 1.6px;
}

/* ------------------------------------------------------------------ */
/* Reset (inspired by https://www.joshwcomeau.com/css/custom-css-reset/) */
/* ------------------------------------------------------------------ */
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  /* Reserve the classic scrollbar's track at ALL times, so locking the page
     behind an overlay does not change the viewport's usable width. Without
     it every overlay open widened the page by the scrollbar width and every
     close narrowed it again - a visible horizontal jolt of the whole layout
     on all six overlays (audit 2.5, "consistently wrong rather than
     inconsistent").

     Reserving it up front rather than padding the lock class is what makes
     this correct for the header, the scrims and the drawers themselves:
     those are position:fixed and sized against the VIEWPORT, so padding
     <html> on lock would leave them jumping while the document behind them
     stood still. Nothing can jump if the viewport never changes width.

     This is inert where there is nothing to reserve - overlay scrollbars
     (every touch device, macOS by default) take no space, so the gutter is
     zero and no page gains a stripe of empty margin. */
  scrollbar-gutter: stable;
}

/* The one scroll lock. Four identical classes used to do this job -
   .cart-drawer-open (section-cart.css), .quick-view-open
   (component-quick-view.css), .perfect-match-open (section-perfect-match.css)
   and .search-overlay-lock (section-header.css) - each `overflow: hidden`
   and nothing else, each owned by a different file.

   The class is applied and removed by ThemeOverlay.lockScroll/unlockScroll
   (assets/overlay-motion.js), which tracks WHICH overlays are holding it:
   quick view opens on top of the cart drawer, so "some overlay closed" is
   not the same question as "the page can scroll again".

   `overflow: hidden` keeps <html> a scroll container, so the gutter
   reserved above stays reserved and the lock is layout-neutral. */
html.overlay-open {
  overflow: hidden;
}

body {
  display: flex;
  flex-direction: column;
  min-height: 100svh;
  font-family: var(--font-body);
  background-color: var(--color-bg);
  color: var(--color-ink);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
  height: auto;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  cursor: pointer;
  background: none;
  border: none;
}

a {
  color: inherit;
}

p {
  text-wrap: pretty;
}

p,
h1,
h2,
h3,
h4,
h5,
h6 {
  overflow-wrap: break-word;
}

ul,
ol {
  list-style: none;
  padding: 0;
}

table {
  border-collapse: collapse;
}

/* ------------------------------------------------------------------ */
/* Type scale                                                          */
/* ------------------------------------------------------------------ */
h1,
h2,
h3,
h4,
h5,
h6,
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
  font-family: var(--font-heading);
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1.2;
}

h1,
.h1 {
  font-size: 40px;
  text-transform: uppercase;
}

h2,
.h2 {
  font-size: 32px;
  text-transform: uppercase;
}

h3,
.h3 {
  font-size: 24px;
}

h4,
.h4 {
  font-size: 20px;
}

h5,
.h5 {
  font-size: 16px;
}

h6,
.h6 {
  font-size: 14px;
}

.text-body {
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
}

.text-small {
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.5;
}

.text-caption {
  font-family: var(--font-body);
  font-size: 12px;
  line-height: 1.5;
}

@media screen and (min-width: 750px) {
  h1,
  .h1 {
    font-size: 48px;
  }
}

/* ------------------------------------------------------------------ */
/* Layout utilities                                                    */
/* ------------------------------------------------------------------ */
.page-width {
  width: 100%;
  max-width: var(--page-width);
  margin-inline: auto;
  padding-inline: var(--page-margin);
}

.visually-hidden {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

.skip-to-content-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 100;
  transform: translateY(-100%);
  padding: var(--space-3) var(--space-5);
  background: var(--color-bg);
  color: var(--color-ink);
  border: 1px solid var(--color-line);
  border-radius: 0 0 var(--radius-m) 0;
}

.skip-to-content-link:focus {
  transform: translateY(0);
}

/* ------------------------------------------------------------------ */
/* Buttons                                                             */
/* ------------------------------------------------------------------ */
.btn,
.shopify-payment-button__button.shopify-payment-button__button--unbranded {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: 56px;
  padding-inline: var(--space-5);
  border-radius: var(--radius-m);
  font-family: var(--font-heading);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  border: 1px solid transparent;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease,
    box-shadow 0.15s ease;
  position: relative;
  isolation: isolate;
  /* Only the fill's COLOUR is per-variant (see .btn--primary etc. below).
     Its timing lives on :root so the tab pills, which are not descendants
     of a button, inherit exactly the same four values. */
  --btn-fill-color: transparent;
  /* The hairline drawn INSIDE the swept layer. Transparent unless a variant
     asks for it - see .btn--primary, which is the variant that needs it. */
  --btn-fill-edge: transparent;
}

/* Hover inverts; it does not go purple. The merchant's rule, verbatim: "if
   it's white then turn black, if black turn white". Purple keeps every other
   job it had - selected pills, badges, prices, focus rings, panel interiors -
   it just stops being what a control turns when you point at it.

   Inverting .btn--primary is the hard case and the reason --btn-fill-edge
   exists: at rest it is an ink block with white text, so its inverse is a
   WHITE button, which on a white page is a control that has vanished. The
   edge is what stops that:

     - the 1px border is already --color-ink at rest, and now stays ink
       through the hover instead of going purple, so there is a hairline
       around the button at every point in the sweep; and
     - --btn-fill-edge draws a second hairline INSIDE the ::before layer, so
       the outline is revealed by the same clip-path circle as the interior
       and cannot arrive at a different time from the fill it belongs to.

   Two hairlines, so the inverted button carries roughly the visual weight
   the solid one did rather than thinning out to an outline - and it is not
   mistakable for a resting .btn--secondary, which has one. Nothing here is
   visible at rest: the ring lives on a layer clipped to zero radius. */
.btn--primary,
.shopify-payment-button__button.shopify-payment-button__button--unbranded {
  background-color: var(--color-ink);
  color: var(--color-bg);
  border-color: var(--color-ink);
  --btn-fill-color: var(--color-bg);
  --btn-fill-edge: var(--color-ink);
}

/* The no-cursor-fill fallback (touch, reduced motion, before motion.js runs):
   the same inversion, arrived at instantly. The inset ring is stated here
   because there is no ::before sweep to carry it in these contexts; the rule
   under html.has-cursor-fill below takes it back off again. */
.btn--primary:hover,
.shopify-payment-button__button.shopify-payment-button__button--unbranded:hover {
  background-color: var(--color-bg);
  color: var(--color-ink);
  border-color: var(--color-ink);
  box-shadow: inset 0 0 0 1px var(--color-ink);
}

.btn--secondary {
  background-color: transparent;
  color: var(--color-ink);
  border-color: var(--color-ink);
  --btn-fill-color: var(--color-ink);
}

.btn--secondary:hover {
  background-color: var(--color-ink);
  color: var(--color-bg);
}

/* On dark surfaces, the stroke variant flips to a white outline.

   These two hooks are only ever applied to a surface that is dark by
   DECLARATION - the hero's non-light type field sets data-color-scheme="dark"
   on itself - so the outline is the constant that reads on such a surface,
   not "the opposite of the page". As --color-bg it went near-black in dark
   mode while the field it outlines stayed dark. */
.color-scheme--dark .btn--secondary,
[data-color-scheme='dark'] .btn--secondary {
  color: var(--color-on-dark-band);
  border-color: var(--color-on-dark-band);
  --btn-fill-color: var(--color-on-dark-band);
}

.color-scheme--dark .btn--secondary:hover,
[data-color-scheme='dark'] .btn--secondary:hover {
  background-color: var(--color-on-dark-band);
  color: var(--color-dark-band);
}

.btn--small {
  height: var(--field-height);
  padding-inline: var(--space-4);
  font-size: 12px;
}

.btn:disabled,
.btn[aria-disabled='true'] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ------------------------------------------------------------------ */
/* Cursor-origin hover fill (assets/motion.js sets --fill-x/--fill-y,   */
/* percentages of the button's own box, on pointerenter/pointerleave)   */
/* ------------------------------------------------------------------ */
/* z-index MUST be negative here, not 0. Within a stacking context the paint
   order is: background, then inline content, then positioned boxes at
   z-index 0/auto. A bare text label (a button whose content is a text node
   rather than an element) is inline content, so a z-index: 0 fill paints
   straight over it and the label vanishes as the sweep crosses. Buttons whose
   label happens to be wrapped in a <span> were unaffected, which is what made
   this look intermittent. -1 keeps the fill above the button's own background
   but below both inline text and wrapped children, and `isolation: isolate`
   on .btn stops it escaping behind an ancestor's background. */
.btn::before,
.shopify-payment-button__button.shopify-payment-button__button--unbranded::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background-color: var(--btn-fill-color);
  /* Riding on the swept layer rather than on the button is the whole point:
     the clip-path circle cuts the ring as well as the interior, so the
     outline is drawn in exactly as far as the fill has reached. One
     property, no second transition to keep in step with the sweep. */
  box-shadow: inset 0 0 0 1px var(--btn-fill-edge);
  clip-path: circle(0% at var(--fill-x, 50%) var(--fill-y, 50%));
  pointer-events: none;
}

.btn > *,
.shopify-payment-button__button.shopify-payment-button__button--unbranded > * {
  position: relative;
  z-index: 1;
}

.btn.is-loading::after {
  z-index: 2;
}

/* Entry (sweeping in) is deliberately slower than exit (merchant feedback:
   the sweep reads best unhurried) - un-sweeping on pointer-out stays quick
   so sweeping the cursor across several buttons in a row doesn't leave a
   trail of half-filled buttons behind it. Per the cascade, the transition
   that runs is whichever rule matches the DESTINATION state: adding
   .is-hover-fill matches the longer transition below, removing it falls
   back to this quicker one. */
@media (prefers-reduced-motion: no-preference) {
  /* The payment button MUST be here as well as in the .is-hover-fill rule
     below. Listed only there, it swept in over 3s and then vanished
     instantly on un-hover, because the transition that runs is whichever
     rule matches the DESTINATION state - and its resting state had none.
     That asymmetry is what "the hover animation doesn't work on Buy it now"
     actually was: the half nobody thinks to check. */
  .btn::before,
  .shopify-payment-button__button.shopify-payment-button__button--unbranded::before {
    transition: clip-path var(--btn-fill-out) var(--btn-fill-out-ease);
  }

  html.has-cursor-fill .btn.is-hover-fill::before,
  html.has-cursor-fill .shopify-payment-button__button.shopify-payment-button__button--unbranded.is-hover-fill::before {
    /* Same expo-out curve as the cart drawer slide (section-cart.css) -
       keeps deceleration gentle enough late in the animation to still read
       as a sweep crossing the button rather than snapping to a stop. */
    transition: clip-path var(--btn-fill-in) var(--btn-fill-in-ease);
  }
}

/* Full coverage: circle() percentages resolve against sqrt((w²+h²)/2), so
   150% comfortably reaches the farthest corner from any origin point. */
html.has-cursor-fill .btn.is-hover-fill::before,
html.has-cursor-fill .shopify-payment-button__button.shopify-payment-button__button--unbranded.is-hover-fill::before {
  clip-path: circle(150% at var(--fill-x, 50%) var(--fill-y, 50%));
}

/* While JS drives the fill from the cursor's entry/exit point, suppress the
   plain CSS :hover swap below so the ::before sweep is the only thing that
   reveals the hover colour (otherwise the instant swap would fully repaint
   the button before the sweep finishes, hiding it). Text colour still uses
   the existing transition so it settles at roughly the same pace.

   The BORDER is deliberately not pinned for .btn--primary: it is ink at rest
   and ink when inverted, so there is nothing to suppress and the hairline is
   simply always there. The inset ring IS suppressed - ::before carries it in,
   and leaving the button's own copy on would draw the whole outline instantly
   while the fill was still a dot in the corner. */
html.has-cursor-fill .btn--primary:hover,
html.has-cursor-fill .shopify-payment-button__button.shopify-payment-button__button--unbranded:hover {
  background-color: var(--color-ink);
  border-color: var(--color-ink);
  box-shadow: none;
}

html.has-cursor-fill .btn--secondary:hover {
  background-color: transparent;
  border-color: var(--color-ink);
}

/* Both directions have to be stated, and neither can rely on a per-variant
   rule catching every case:
   - :hover  -> the label/border swap should keep pace with the sweep arriving
   - un-hover -> without this the colour falls back to .btn's own 0.15s and
     snaps back to its resting colour while a slow fill is still covering the
     button, i.e. dark text on a dark fill for most of the retreat.
   These are deliberately variant-agnostic (.btn, not .btn--secondary) so a
   button variant added later can't quietly keep the fast default. */
@media (prefers-reduced-motion: no-preference) {
  html.has-cursor-fill .btn,
  html.has-cursor-fill .shopify-payment-button__button.shopify-payment-button__button--unbranded {
    transition-duration: var(--btn-fill-out);
    transition-timing-function: var(--btn-fill-out-ease);
  }

  html.has-cursor-fill .btn:hover,
  html.has-cursor-fill .shopify-payment-button__button.shopify-payment-button__button--unbranded:hover {
    transition-duration: var(--btn-fill-in);
    transition-timing-function: var(--btn-fill-in-ease);
  }
}

/* .btn--secondary's fill is ink-coloured, so its text still needs to end up
   on --color-bg once covered - only the background/border swap is
   suppressed here, matching .btn--primary above (whose text colour never
   changes on hover in the first place). Sync the colour transition's speed
   to roughly the fill sweep's so it doesn't finish inverting well before
   the sweep visually reaches it. */
@media (prefers-reduced-motion: no-preference) {
  html.has-cursor-fill .btn--secondary:hover {
    transition-duration: var(--btn-fill-in);
  }
}

/* The dark-surface variant's fill is --color-bg (light), so - unlike
   the two rules above - its text MUST still invert to --color-ink
   once covered, or light text ends up sitting on a light fill. Only the
   background swap is suppressed; the colour swap stays, just re-timed. */
html.has-cursor-fill .color-scheme--dark .btn--secondary:hover,
html.has-cursor-fill [data-color-scheme='dark'] .btn--secondary:hover {
  background-color: transparent;
  border-color: var(--color-on-dark-band);
}

@media (prefers-reduced-motion: no-preference) {
  html.has-cursor-fill .color-scheme--dark .btn--secondary:hover,
  html.has-cursor-fill [data-color-scheme='dark'] .btn--secondary:hover {
    transition-duration: var(--btn-fill-in);
  }
}

/* ------------------------------------------------------------------ */
/* Icon close buttons (circular; cursor-fill + 90deg icon turn)        */
/* Any circular icon-close control (cart drawer, search overlay, modal */
/* close, etc.) opts in with [data-icon-close] on its markup so the    */
/* fill/spin mechanics live once here instead of per-component.        */
/* ------------------------------------------------------------------ */
/* The BOX lives here too, not just the fill mechanics. Six controls (cart
   drawer, quick view, concierge modal, gallery lightbox, search overlay,
   mobile drawer) had converged on "32px circle with an X" and no two were
   the same - three glyph sizes, two backplates, one with no border and a
   grey resting colour. Declaring the circle once means a control opts into
   the whole treatment with one attribute and cannot drift again; each
   component now only supplies its own POSITIONING. */
[data-icon-close] {
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 40px around a 16px glyph, so there is 11px of clear ground between the
     cross and the ring on every side. At 32px the glyph crowded its own
     border and the circle read as tight rather than deliberate. It also
     brings the target closer to the 44px touch minimum, which matters
     because every one of these dismisses something. */
  width: 40px;
  height: 40px;
  border: 1px solid var(--color-line);
  border-radius: 50%;
  /* Opaque, not transparent: half of these sit over imagery (lightbox,
     quick view) where a see-through backplate loses the glyph. All-or-
     nothing beats per-surface guessing. */
  background-color: var(--color-bg);
  color: var(--color-ink);
  /* Inverts rather than going purple: a white circle with an ink glyph
     becomes an ink circle with a white glyph. */
  --icon-close-fill: var(--color-ink);
}

/* The BORDER is what keeps the inverted circle legible, so it is the one
   thing that must NOT invert. Half of these sit over imagery (lightbox, quick
   view) where an ink circle can land on an ink photograph; the --color-line
   hairline is already there at rest and stays put through the hover, which
   gives the dark circle a light edge on a dark ground and a quiet outline on
   a light one. Keeping it fixed is also why nothing here changes at rest. */

/* The no-cursor-fill fallback (touch, reduced motion, before motion.js
   runs): the same inversion, arrived at instantly. */
[data-icon-close]:hover {
  background-color: var(--color-ink);
  color: var(--color-bg);
}

[data-icon-close]::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  background-color: var(--icon-close-fill);
  clip-path: circle(0% at var(--fill-x, 50%) var(--fill-y, 50%));
  pointer-events: none;
}

/* Pinned so the glyph keeps its own size as the circle grows - the padding IS
   the difference between the two, so letting the icon track the box would
   cancel the change out. */
[data-icon-close] > * {
  position: relative;
  z-index: 1;
  flex: none;
}

/* Same entry-slower/exit-quicker split as .btn above, on its own token pair
   (:root, --icon-close-fill-*) because a close button is deliberately
   quicker than the 3s button sweep. Tokens, not literals: these five rules
   used to restate 0.28s/0.65s by hand while the .btn block forty lines up
   read variables, so retuning one never reached the other. */
@media (prefers-reduced-motion: no-preference) {
  [data-icon-close]::before {
    transition: clip-path var(--icon-close-fill-out) var(--icon-close-fill-out-ease);
  }

  html.has-cursor-fill [data-icon-close].is-hover-fill::before {
    transition: clip-path var(--icon-close-fill-in) var(--icon-close-fill-in-ease);
  }
}

html.has-cursor-fill [data-icon-close].is-hover-fill::before {
  clip-path: circle(150% at var(--fill-x, 50%) var(--fill-y, 50%));
}

/* Background pinned back to the resting colour so the instant swap above
   cannot repaint the circle ahead of the sweep; only the glyph's crossfade
   is left to ride along with it. */
html.has-cursor-fill [data-icon-close]:hover {
  background-color: var(--color-bg);
  color: var(--color-bg);
}

@media (prefers-reduced-motion: no-preference) {
  html.has-cursor-fill [data-icon-close]:hover {
    transition: color var(--icon-close-fill-in) var(--icon-close-turn-in-ease);
  }

  /* A quarter turn, not a full one - the icon is an X, so 90deg reads as a
     deliberate tilt into a + rather than a full revolution back to where it
     started. This has to be a TRANSITION rather than a keyframe: an animation
     without a fill-mode snaps back to 0deg the instant it ends, which a 360
     turn hid (same visual position) and a 90 turn would not. Entry matches
     the fill sweep, exit is quick like the fill's, so a pointer passing over
     leaves nothing half-turned behind it.

     The turn and the colour crossfade above share the fill's DURATIONS but
     ride an `ease-out` entry against the fill's expo curve, which is the
     desync .btn's tokens exist to prevent. Kept as-is here (pinned to
     --icon-close-turn-in-ease) because changing it would change how the
     control looks, which is out of scope for a de-duplication pass - but it
     is now one token to flip rather than two literals to find. */
  html.has-cursor-fill [data-icon-close] > * {
    transition: transform var(--icon-close-fill-out) var(--icon-close-fill-out-ease);
  }

  html.has-cursor-fill [data-icon-close].is-hover-fill > * {
    transform: rotate(90deg);
    transition: transform var(--icon-close-fill-in) var(--icon-close-turn-in-ease);
  }
}

/* ------------------------------------------------------------------ */
/* Form fields                                                         */
/* ------------------------------------------------------------------ */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.field__label {
  font-size: 14px;
  color: var(--color-muted);
}

.field__input {
  height: 48px;
  padding-inline: var(--space-4);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-m);
  background-color: var(--color-bg);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: 16px;
}

.field__input:focus {
  outline: none;
  border-color: var(--color-ink);
}

.field__input::placeholder {
  color: var(--color-muted);
}

/* ------------------------------------------------------------------ */
/* Badges                                                               */
/* ------------------------------------------------------------------ */
.badge {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding-inline: var(--space-2);
  border-radius: var(--radius-pill);
  font-family: var(--font-heading);
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  background-color: var(--color-ink);
  color: var(--color-bg);
}

/* A saving is worth saying in the brand colour, but it is not the thing to
   act on. Solid brand purple put the discount on the same plate as the primary
   button - two full-strength accents in one view, and the button lost. A tint
   with brand text keeps the colour and the meaning while ranking it below the
   button, which stays the only solid accent on the screen. Every use of this
   badge sits on a panel (price rows, cart lines), never on product media, so a
   tint holds its contrast; badges over photography keep opaque plates. */
.badge--sale {
  background-color: color-mix(in srgb, var(--color-brand) 14%, transparent);
  color: var(--color-brand);
  font-weight: 600;
}

.badge--stock {
  background-color: var(--color-success);
  color: var(--color-bg);
}

/* ------------------------------------------------------------------ */
/* Rich text (page.content, article.content, richtext settings)        */
/* ------------------------------------------------------------------ */
.rte {
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  color: var(--color-ink);
}

.rte > * + * {
  margin-top: var(--space-4);
}

.rte h1,
.rte h2,
.rte h3,
.rte h4,
.rte h5,
.rte h6 {
  margin-top: var(--space-6);
}

.rte h1 {
  font-size: 32px;
}

.rte h2 {
  font-size: 26px;
}

.rte h3 {
  font-size: 20px;
  text-transform: none;
}

.rte h4,
.rte h5,
.rte h6 {
  font-size: 16px;
  text-transform: none;
}

.rte p {
  text-wrap: pretty;
}

.rte ul,
.rte ol {
  padding-left: var(--space-5);
}

.rte ul {
  list-style: disc;
}

.rte ol {
  list-style: decimal;
}

.rte li + li {
  margin-top: var(--space-2);
}

.rte a {
  color: var(--color-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* A text link has no box to invert, so the equivalent move is toward maximum
   contrast rather than toward the accent: the underline thickens to the
   link's own colour. --color-ink is where it already sits, so this changes
   the rule and not the word. */
.rte a:hover {
  text-decoration-thickness: 2px;
}

.rte img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-m);
}

.rte blockquote {
  padding-left: var(--space-4);
  border-left: 1px solid var(--color-line);
  color: var(--color-muted);
  font-style: italic;
}

.rte table {
  width: 100%;
  border-collapse: collapse;
}

.rte th,
.rte td {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-line);
  text-align: left;
}

.rte hr {
  border: none;
  border-top: 1px solid var(--color-line);
}

/* ------------------------------------------------------------------ */
/* Section layout grid                                                 */
/* ------------------------------------------------------------------ */
/*
 * .shopify-section is Shopify's auto-generated wrapper around every
 * rendered section. It is full-width by default: every section here paints
 * its own background on its own outermost element and constrains its
 * *content* internally with .page-width (see base.css .page-width, and
 * e.g. hero-carousel.liquid, newsletter.liquid, discord-cta.liquid) — so
 * the wrapper itself must never clip that background to a centered column.
 *
 * The --content-width/--content-margin/--content-grid custom properties
 * are still exposed here for the few sections that want to opt *into* a
 * constrained 3-column grid on their own inner element (see
 * custom-section.liquid's .custom-section__content), not because this
 * wrapper enforces one.
 */
.shopify-section {
  --content-width: min(
    calc(var(--page-width) - var(--page-margin) * 2),
    calc(100% - var(--page-margin) * 2)
  );
  --content-margin: minmax(var(--page-margin), 1fr);
  --content-grid: var(--content-margin) var(--content-width) var(--content-margin);

  position: relative;
  width: 100%;
}

/* ------------------------------------------------------------------ */
/* Scroll reveal (assets/reveal.js)                                    */
/* ------------------------------------------------------------------ */
/* Elements only start hidden once html.js confirms JS actually ran
   (inline script in layout/theme.liquid's <head>) - otherwise a no-JS
   visitor, or JS that errors before reveal.js runs, would be stuck with
   permanently invisible content. */
html.js [data-reveal] {
  opacity: 0;
  transform: translateY(16px);
}

/* LCP guard: the FIRST section on any page never starts hidden. Everything
   in it paints with the initial HTML instead of waiting for deferred JS +
   the observer, so the largest above-the-fold element is always an LCP
   candidate at first paint. reveal.js still runs there - adding is-revealed
   to something already visible is a no-op - and every later section keeps
   its entrance. */
#MainContent > .shopify-section:first-child [data-reveal] {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: no-preference) {
  /* The transform settles on a spring rather than an ease-out: a slight
     overshoot and one soft return, sampled into a linear() curve because CSS
     has no native spring. Modelled on the reference theme's Motion.spring()
     entrances; opacity keeps a plain ease so the fade never bounces. */
  html.js [data-reveal] {
    transition:
      opacity 500ms ease-out,
      transform 700ms linear(0, 0.0086, 0.0338, 0.0742, 0.1284, 0.1942, 0.2692, 0.3507, 0.436, 0.522, 0.606, 0.6852, 0.7574, 0.8208, 0.8743, 0.9174, 0.9503, 0.9736, 0.9885, 0.9967, 1.0001, 1.0005, 0.9995, 0.9987, 0.9991, 1);
  }
}

html.js [data-reveal].is-revealed {
  opacity: 1;
  transform: translateY(0);
}

/* Motion is unwanted: these are simply present.
   Headings already had this escape hatch and [data-reveal] did not, which is
   worse than an inconsistency - the hidden state above is not a transition, it
   is opacity: 0, so anyone with reduced motion enabled was relying on
   reveal.js exactly as much as everyone else. Any failure to observe an
   element left it permanently invisible for them too, with no animation to
   have gained from the risk. Placed after the .is-revealed rule so it wins
   regardless of specificity ties. */
@media (prefers-reduced-motion: reduce) {
  html.js [data-reveal] {
    opacity: 1;
    transform: none;
  }
}

/* Per-row stagger for grids of repeated cards (product cards, bento/category
   tiles) - simpler than threading a data-reveal-index down through every
   grid's render loop: each card already carries [data-reveal] on its own
   root, so this just fans out the transition-delay by grid position. Caps
   at the 4th column since anything past that reveals close enough together
   to read as simultaneous anyway. */
@media (prefers-reduced-motion: no-preference) {
  .featured-products__grid > [data-reveal]:nth-child(2),
  .related-products__grid > [data-reveal]:nth-child(2),
  .collection-grid > [data-reveal]:nth-child(2),
  .cart-recommendations__grid > [data-reveal]:nth-child(2),
  .bento-categories__grid > [data-reveal]:nth-child(2),
  .category-tiles__grid > [data-reveal]:nth-child(2) {
    transition-delay: 80ms;
  }

  .featured-products__grid > [data-reveal]:nth-child(3),
  .related-products__grid > [data-reveal]:nth-child(3),
  .collection-grid > [data-reveal]:nth-child(3),
  .cart-recommendations__grid > [data-reveal]:nth-child(3),
  .bento-categories__grid > [data-reveal]:nth-child(3),
  .category-tiles__grid > [data-reveal]:nth-child(3) {
    transition-delay: 160ms;
  }

  .featured-products__grid > [data-reveal]:nth-child(4),
  .related-products__grid > [data-reveal]:nth-child(4),
  .collection-grid > [data-reveal]:nth-child(4),
  .cart-recommendations__grid > [data-reveal]:nth-child(4),
  .bento-categories__grid > [data-reveal]:nth-child(4),
  .category-tiles__grid > [data-reveal]:nth-child(4) {
    transition-delay: 240ms;
  }
}

/* ------------------------------------------------------------------ */
/* Media loading skeleton (assets/reveal.js)                           */
/* ------------------------------------------------------------------ */
[data-media-skeleton] {
  position: relative;
  overflow: hidden;
}

[data-media-skeleton]::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: linear-gradient(90deg, transparent 0%, rgb(255 255 255 / 50%) 50%, transparent 100%);
  background-size: 200% 100%;
  opacity: 1;
  pointer-events: none;
}

[data-media-skeleton].is-loaded::after {
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  [data-media-skeleton]::after {
    transition: opacity 0.25s ease;
  }

  /* Three sweeps, then a still placeholder. Images in sideways tracks stay
     lazy until swiped to, and an endless shimmer on each kept repainting
     on the main thread every frame. */
  [data-media-skeleton]:not(.is-loaded)::after {
    animation: media-shimmer 1.4s ease-in-out 3;
  }
}

@keyframes media-shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

/* ------------------------------------------------------------------ */
/* Button loading state (add/remove .is-loading via assets/cart.js)    */
/* ------------------------------------------------------------------ */
.btn.is-loading {
  position: relative;
  pointer-events: none;
}

/* Hides the label/icon without collapsing the button's width - every
   direct child (icon svg, label span/text) becomes invisible but still
   takes up its normal layout space. */
.btn.is-loading > * {
  visibility: hidden;
}

.btn.is-loading::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
}

@media (prefers-reduced-motion: no-preference) {
  .btn.is-loading::after {
    animation: btn-spinner-rotate 0.6s linear infinite;
  }
}

/* Added: the label steps aside for a tick that pops in, then comes back
   (assets/cart.js removes .is-added after 1.4s). Same trick as the spinner:
   the children keep their space, so the button never changes width. */
.btn.is-added {
  position: relative;
}

.btn.is-added > * {
  visibility: hidden;
}

.btn.is-added::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  width: 7px;
  height: 13px;
  margin: -9px 0 0 -4px;
  border: solid currentColor;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

@media (prefers-reduced-motion: no-preference) {
  .btn.is-added::after {
    animation: btn-tick-pop 380ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }
}

@keyframes btn-tick-pop {
  from {
    opacity: 0;
    transform: rotate(45deg) scale(0.4);
  }

  to {
    opacity: 1;
    transform: rotate(45deg) scale(1);
  }
}

@keyframes btn-spinner-rotate {
  to {
    transform: rotate(360deg);
  }
}

/* ------------------------------------------------------------------ */
/* Icon micro-motion (snippets/icon.liquid tags every glyph)           */
/* ------------------------------------------------------------------ */
/*
 * Every icon in the theme comes out of one snippet, so the motion is
 * declared once here against the class that snippet emits rather than being
 * re-implemented per component - the duplication three consistency batches
 * have been removing.
 *
 * The motion says what the control DOES: the cart lifts toward the bag, the
 * search glass swells slightly, an arrow or chevron travels the way it
 * points, a social glyph lifts, +/- press in. Everything is one or two units
 * or a few percent, and quicker than either fill pair, so it reads as the
 * control acknowledging the cursor rather than as decoration.
 *
 * TRANSFORM AND OPACITY ONLY - nothing here can reflow, and nothing here
 * changes at rest.
 *
 * Three mechanics are load-bearing:
 *
 * 1. The motion is applied to the glyph's CHILDREN, not to the <svg>. Six
 *    call sites rotate the <svg> itself (`transform: rotate(180deg)` for a
 *    "previous" arrow, an open accordion's chevron, the footer's, the
 *    header's), and a CSS transform on that same element would clobber the
 *    rotation. Moving the children instead sidesteps it completely AND is
 *    the only version that is correct: the children live in the svg's own
 *    user space, which rotates with it, so "travel along +X" means "travel
 *    the way the glyph is currently pointing" - a flipped arrow travels
 *    left without a single per-call-site override.
 *
 * 2. It uses the individual `translate` / `scale` properties rather than the
 *    `transform` shorthand, so the one icon whose child carries its own
 *    transform attribute (the mirrored <g> inside 'arrow') composes with it
 *    instead of overwriting it.
 *
 * 3. The per-icon values are custom properties and there is exactly ONE rule
 *    that applies them. Adding motion to another glyph is a line of values,
 *    not another copy of the trigger selector.
 *
 * The trigger reaches two levels down from the interactive element, which
 * covers <a><svg></a> and <a><span><svg></span></a> - every shape in the
 * theme - without a bare descendant selector, which on a card-sized <a>
 * would twitch every glyph inside it at once.
 *
 * :focus-visible is on the same rule as :hover throughout, so a keyboard
 * visitor gets exactly what a pointer does.
 *
 * Reduced motion gets the static fallback by construction: everything below
 * is inside the no-preference block, and the resting state IS the fallback -
 * no icon depends on its motion to be understood.
 */

/* Deliberately no motion: close (already turns 90deg with the fill - see the
   [data-icon-close] block above, and this file leaves it alone), and the
   glyphs that label rather than act - user, heart, heart-filled, filters,
   star, star-half, edit, location, shipping, payment, delivery, stock,
   guaranteed, burger. A trust badge or a rating star is not a control and
   has nothing to express by moving. */
@media (prefers-reduced-motion: no-preference) {
  .icon > * {
    transition: translate var(--icon-motion-out) var(--icon-motion-out-ease),
      scale var(--icon-motion-out) var(--icon-motion-out-ease);
  }

  /* Toward the bag. */
  .icon--cart > * {
    --icon-hover-dy: calc(var(--icon-motion-lift) * -1);
  }

  /* The glass swells - the gesture of looking closer. */
  .icon--search > * {
    --icon-hover-s: 1.08;
  }

  /* Travel the way they point. +X and +Y are the GLYPH's own axes here, so a
     rotated instance travels in its rotated direction (see note 1 above). */
  .icon--arrow > * {
    --icon-hover-dx: var(--icon-motion-travel);
  }

  .icon--arrow-down > *,
  .icon--chevron-down > * {
    --icon-hover-dy: var(--icon-motion-travel);
  }

  /* A press, not a nudge: the stepper's two glyphs give slightly under the
     cursor, which is the only thing a +/- has to say. */
  .icon--plus > *,
  .icon--minus > * {
    --icon-hover-s: 0.86;
  }

  /* A small lift, as a set - these sit in rows of four or more, so anything
     larger turns a cursor crossing the row into a wave. */
  .icon--twitter > *,
  .icon--facebook > *,
  .icon--tiktok > *,
  .icon--instagram > *,
  .icon--discord > *,
  .icon--youtube > *,
  .icon--whatsapp > * {
    --icon-hover-dy: calc(var(--icon-motion-lift) * -1);
  }

  /* The one rule that applies all of the above. Icons with no values
     declared resolve to their fallbacks and stay exactly still. */
  :where(a, button, summary, label, [role='button']):is(:hover, :focus-visible) > .icon > *,
  :where(a, button, summary, label, [role='button']):is(:hover, :focus-visible) > * > .icon > * {
    translate: var(--icon-hover-dx, 0) var(--icon-hover-dy, 0);
    scale: var(--icon-hover-s, 1);
    transition-duration: var(--icon-motion-in);
    transition-timing-function: var(--icon-motion-in-ease);
  }
}

/* ------------------------------------------------------------------ */
/* Marquees                                                            */
/* ------------------------------------------------------------------ */
/* The one rule assets/marquee-motion.js needs, shared by every marquee in
   the theme (the scrolling text section and the brand logo carousel today)
   rather than restated in each section's stylesheet. JS only ever toggles
   the class; CSS owns the pausing, so a visitor who prefers reduced motion
   has no animation to pause and the class is simply inert.

   The attribute selector is deliberately part of it: `animation:` shorthand
   in a section stylesheet resets animation-play-state to running, and this
   file loads first, so a single class here would lose. */
@media (prefers-reduced-motion: no-preference) {
  [data-marquee-track].is-paused {
    animation-play-state: paused;
  }
}

/* ------------------------------------------------------------------ */
/* Press bounce (icon controls)                                        */
/* ------------------------------------------------------------------ */
/* A tap should feel like it landed. The press itself is quick and linear -
   the finger is already moving, so easing the way DOWN just makes the control
   feel unresponsive - and the release springs back through an overshoot
   curve, which is what reads as a bounce.

   Two transitions, not a keyframe animation: an animation bound to :active is
   cancelled the instant the button is released, so the bounce would only ever
   play on a press-and-hold. Transitioning back out of the :active state means
   the release IS the animation, so a normal quick click gets the whole thing.

   The overshoot in the curve (the 1.56) is what carries the scale past 1
   before settling - a standard ease-out would just glide back and feel
   flat. */
@media (prefers-reduced-motion: no-preference) {
  .footer__social-link,
  .contact__social-link,
  .announcement-bar__social-link,
  .social-wall__follow-link {
    transition: transform 450ms cubic-bezier(0.34, 1.56, 0.64, 1),
      background-color var(--icon-close-fill-out) var(--icon-close-fill-out-ease),
      border-color var(--icon-close-fill-out) var(--icon-close-fill-out-ease),
      color var(--icon-close-fill-out) var(--icon-close-fill-out-ease),
      opacity 150ms ease;
  }

  .footer__social-link:active,
  .contact__social-link:active,
  .announcement-bar__social-link:active,
  .social-wall__follow-link:active {
    transform: scale(0.86);
    transition-duration: 90ms;
    transition-timing-function: ease-out;
  }
}

/* ------------------------------------------------------------------ */
/* Heading rise (assets/reveal.js)                                     */
/* ------------------------------------------------------------------ */
/* Every heading in the page body arrives from below. Site-wide rather than
   per-section, so a new section inherits it without anyone remembering to
   wire it up.

   Gated on html.js exactly like [data-reveal]: a visitor without JavaScript,
   or one whose JS throws before reveal.js runs, must see headings rather than
   a page of blank space. That is also why reveal.js reveals everything
   outright when IntersectionObserver is missing.

   Headings inside a [data-reveal] wrapper are deliberately EXEMPT - the
   wrapper already carries them up, and animating both would stack two rises
   into one 40px lurch. The selector below is more specific than the rule
   above it, which is what excuses them. */
html.js main :is(h1, h2, h3) {
  opacity: 0;
  translate: 0 24px;
}

html.js main :is(h1, h2, h3).is-revealed {
  opacity: 1;
  translate: 0 0;
}

html.js [data-reveal] :is(h1, h2, h3) {
  opacity: 1;
  translate: 0 0;
}

/* Sections that animate their own headings opt out here rather than in JS:
   the hero runs a word-by-word entrance, and the banner and video sections
   drive their headings from scroll position. Two systems moving one element
   is a fight, not a flourish. */
html.js main :is(
  .hero-carousel__heading,
  .banner-mosaic__heading,
  .banner-reveal__heading,
  .video-slide__heading,
  .video-scroll__heading,
  /* These three were missing, and the symptom was not a missed animation -
     it was three headings permanently invisible on the home page.

     IntersectionObserver measures an element's rect AFTER its ancestors have
     clipped it. Each of these sections holds its heading inside a box it
     clips or transforms itself, so as far as the observer is concerned the
     heading never enters the viewport at all - it stays at zero intersection
     even when it is sitting centred on the screen. .is-revealed is therefore
     never added, and the opacity: 0 above is never lifted.

     Verified rather than assumed: a fresh observer with the same options,
     pointed at .drop-reveal__heading while it was centred in the viewport at
     123px tall, also refused to fire. */
  .drop-reveal__heading,
  .perfect-match__heading,
  .tabbed-products__heading
) {
  opacity: 1;
  translate: 0 0;
}

@media (prefers-reduced-motion: no-preference) {
  html.js main :is(h1, h2, h3) {
    transition: opacity 620ms ease-out, translate 620ms cubic-bezier(0.16, 1, 0.3, 1);
  }
}

/* ------------------------------------------------------------------ */
/* Masked heading rise                                                 */
/* ------------------------------------------------------------------ */
/* The words climb out from behind the heading's own bottom edge instead of
   fading in - uncovered rather than materialised. assets/reveal.js wraps the
   heading's children in .heading-rise__inner to make it possible: the mask and
   the movement MUST be different elements, because overflow lives in the
   element's own coordinate space, so translating a heading carries its mask
   along with it and nothing is ever revealed.

   Once a heading is masked it does its own moving, so the outer element gives
   up the translate it would otherwise have from the rule above - two rises on
   one heading stack into a lurch. Opacity stays on the outer element: the
   words should not appear at full strength the instant a sliver clears the
   mask edge. */
/* The mask. Descenders (g, y, p) hang below the text box and a plain
   overflow clip shears them off, so the clip region is grown downward by a
   fraction of the line.

   overflow-clip-margin does that WITHOUT touching layout, which matters:
   the older padding/negative-margin trick below cancels itself out
   overall, but it sets margin-bottom, and that REPLACES whatever margin
   the heading's own rule asked for. Every heading carrying this class
   silently lost its spacing - the wishlist grid ended up overlapping its
   own title by 5px. */
html.js .heading-rise {
  overflow: clip;
  overflow-clip-margin: 0.16em;
  translate: 0 0;
}

/* Browsers without overflow-clip-margin keep the original technique; they
   also keep its margin-clobbering, which is why it is no longer the
   default path. */
@supports not (overflow-clip-margin: 1px) {
  html.js .heading-rise {
    overflow: hidden;
    padding-bottom: 0.16em;
    margin-bottom: -0.16em;
  }
}

html.js .heading-rise__inner {
  /* inline-block so it can be transformed at all - translate does nothing to a
     plain inline box - and so its height is the text's height, which is what
     makes 100% exactly "one heading below the edge" for headings of any size
     or line count. */
  display: inline-block;
  translate: 0 100%;
}

html.js .heading-rise.is-revealed .heading-rise__inner {
  translate: 0 0;
}

@media (prefers-reduced-motion: no-preference) {
  html.js .heading-rise__inner {
    /* Slower than the old 620ms fade-rise: this one travels a whole line
       rather than 24px, and the same duration over four times the distance
       reads as a snap rather than a rise. The curve is the theme's ease-in-out
       quad, not an expo-out - expo covers 95% of the distance in the first
       45% of the time, which on a long travel looks like a teleport followed
       by a pause. */
    transition: translate 760ms cubic-bezier(0.45, 0, 0.55, 1);
  }
}

/* Motion is unwanted: headings are simply present. Placed last so it wins
   over the hidden state above regardless of specificity ties. */
@media (prefers-reduced-motion: reduce) {
  html.js main :is(h1, h2, h3) {
    opacity: 1;
    translate: 0 0;
  }

  html.js .heading-rise__inner {
    translate: 0 0;
  }
}

/* ------------------------------------------------------------------ */
/* Section sheets & media parallax (Motion & shape settings)           */
/* ------------------------------------------------------------------ */
/* The page is a sheet: MainContent's top corners round over whatever sits
   above it, which reads whenever the announcement bar runs the dark
   scheme - the white page crests over it like a card. Modelled on the
   reference theme's section--rounded, where every section is a
   top-rounded sheet stacking over the last while scrolling. Glide
   applies it where a background edge exists to reveal it: the page itself,
   and panel-ground sections. --section-radius is 0px when the setting is
   off, so every rule below collapses harmlessly. */
main#MainContent {
  background-color: var(--color-bg);
  border-start-start-radius: var(--section-radius);
  border-start-end-radius: var(--section-radius);
}

/* Settled by scrolling the live reference and photographing the boundary:
   their full-bleed media sections carry `30px 30px 0 0` - rounded TOP
   corners, square bottom - and the page ground shows through the two
   corner notches as each section rises under the sticky header. The
   bottom edge stays straight, which is what the owner was pointing at.

   TOP corners only, never `border-radius` on all four: a rounded bottom
   would cut the following section's shoulder as well and double the
   curve at every seam. */
/* The hero is deliberately NOT here, per the owner's description of the
   reference's two states:
     at the top   - only the NAVBAR curves (its rounded top crests over the
                    announcement bar); the hero below it is straight;
     once scrolled - the navbar flattens, and the curve moves to what is
                    rising beneath it: these sections' rounded tops.
   Curving the hero breaks the first state, because then something is
   already curved before the visitor has scrolled at all. */
.drop-reveal,
.section-video,
.section-video-scroll,
.section-video-story,
.overlay-banner,
.banner-mosaic,
.banner-reveal,
/* Sections that paint their OWN ground belong here too. A crest is only
   visible where a section's colour differs from the page behind it. For
   sections on the color_scheme/section-frame foundation that means
   "whenever the section's scheme isn't scheme-1" (scheme-1 IS the page's
   own background, so there is no edge to reveal) rather than "every
   section with a ground" - and without these the real pages showed almost
   no curve at all: the whole homepage had exactly one qualifying section.

   The test is [class*="color-scheme-"] rather than :not(.color-scheme-1),
   because section-frame no longer emits a class for the default scheme -
   a frame with no scheme class IS the page's ground and has no edge to
   reveal. promo-bento is not in this list: its scheme paints an inset
   rounded card, and the band it sits on is unpainted, so a crest on the
   band would reveal nothing (its old --panel crest never showed either). */
.discord-cta,
.brand-lab,
.stat-counter-frame[class*="color-scheme-"],
.countdown-timer[class*="color-scheme-"] {
  border-start-start-radius: var(--section-radius);
  border-start-end-radius: var(--section-radius);
  overflow: clip;
}

/* Media parallax: scroll-scrubbed drift, no JavaScript. The image is
   overscaled just past its travel so the drift never exposes the box
   edges. Guarded three ways - the setting (via the custom property), the
   browser (@supports), and the visitor (reduced motion). Never applied to
   product-card media: card-scrub.js owns those transforms. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    [data-parallax] > img {
      animation: media-parallax linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }

    html:not(.media-parallax) [data-parallax] > img {
      animation: none;
    }

    @keyframes media-parallax {
      from {
        transform: scale(1.14) translateY(-5%);
      }
      to {
        transform: scale(1.14) translateY(5%);
      }
    }
  }
}

/* ------------------------------------------------------------------ */
/* Section frame (snippets/section-frame.liquid)                       */
/* ------------------------------------------------------------------ */
/* The two custom properties are written by the snippet from the section's
   padding settings; the fallbacks are what a section gets before it has
   those settings. Padding rather than margin, so a scheme's background
   colour fills the spacing instead of showing the page through it. */
.section-frame {
  padding-block: var(--section-padding-top, 40px) var(--section-padding-bottom, 40px);
}

/* ── Light / dark toggle ─────────────────────────────────────────────────
   Shared by the header and the footer instances. The icon swap lives here
   rather than in either section's stylesheet because both render the same
   snippet, and a copy in each would drift the moment one was touched. */
.theme-toggle {
  display: inline-grid;
  grid-auto-flow: column;
  align-items: center;
  background: none;
  border: 0;
  cursor: pointer;
  color: inherit;
}

.theme-toggle__icon {
  display: grid;
  grid-area: 1 / 1;
  place-items: center;
  transition: opacity 0.2s ease, rotate 0.2s ease;
}

/* Light shows the MOON: the icon offers what you will GET, not what you
   already have. A sun on a light page reads as a status light, and visitors
   click it expecting more brightness. */
.theme-toggle__icon--sun { opacity: 0; rotate: -90deg; }
.theme-toggle__icon--moon { opacity: 1; rotate: 0deg; }

:root[data-theme='dark'] .theme-toggle__icon--sun { opacity: 1; rotate: 0deg; }
:root[data-theme='dark'] .theme-toggle__icon--moon { opacity: 0; rotate: 90deg; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .theme-toggle__icon--sun { opacity: 1; rotate: 0deg; }
  :root:not([data-theme='light']) .theme-toggle__icon--moon { opacity: 0; rotate: 90deg; }
}

@media (prefers-reduced-motion: reduce) {
  .theme-toggle__icon { transition: none; }
}

/* The button does nothing without JS, so it is not offered. */
html:not(.js) .theme-toggle { display: none; }

/* ------------------------------------------------------------------ */
/* Status chip (snippets/status-chip.liquid)                           */
/*                                                                     */
/* Dot, label, soft tint of the status colour. One chip says one thing */
/* about one object; a card or a row carries at most one.              */
/* ------------------------------------------------------------------ */
.status-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-family: var(--font-heading);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Neutral: the page's own ink, for a state with no colour meaning. */
  color: var(--color-ink);
  background-color: color-mix(in srgb, var(--color-ink) 8%, transparent);
}

.status-chip__dot {
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: 50%;
  background-color: currentcolor;
}

/* The label takes the status colour and the ground a tint of it, so the
   chip reads at a glance without becoming a second accent. */
.status-chip--in {
  color: var(--color-success);
  background-color: color-mix(in srgb, var(--color-success) 14%, transparent);
}

.status-chip--low {
  color: var(--color-warning);
  background-color: color-mix(in srgb, var(--color-warning) 16%, transparent);
}

.status-chip--out {
  color: var(--color-danger);
  background-color: color-mix(in srgb, var(--color-danger) 14%, transparent);
}

.status-chip--info {
  color: var(--color-info);
  background-color: color-mix(in srgb, var(--color-info) 14%, transparent);
}

/* "You can act on this now" - a pre-order that is open, a waitlist taking
   names. It is the brand colour because it is an invitation rather than a
   condition, which is what separates it from the four above: those describe
   the stock, this one describes what the reader can do. */
.status-chip--brand {
  color: var(--color-brand);
  background-color: color-mix(in srgb, var(--color-brand) 14%, transparent);
}

/* Plain information with nothing to act on. Grey on purpose: a row of these
   should read as a list, not as a row of warnings. */
.status-chip--neutral {
  color: var(--color-muted);
  background-color: color-mix(in srgb, var(--color-ink) 8%, transparent);
}

/* On a dark band the tint is mixed against the band, not the page. */
.color-scheme-3 .status-chip,
.color-scheme-4 .status-chip,
[data-color-scheme='dark'] .status-chip {
  background-color: color-mix(in srgb, currentcolor 18%, transparent);
}

/* ------------------------------------------------------------------ */
/* Eyebrow                                                             */
/*                                                                     */
/* The small tracked line above a section's title. One definition, so  */
/* every band says its category in the same voice and at the same      */
/* size; sections only position it.                                    */
/* ------------------------------------------------------------------ */
.eyebrow {
  margin: 0 0 var(--space-2);
  font-family: var(--font-heading);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-brand);
}

/* ------------------------------------------------------------------ */
/* Empty state (snippets/empty-state.liquid)                           */
/* ------------------------------------------------------------------ */
.empty-state {
  display: grid;
  justify-items: center;
  gap: var(--space-3);
  max-width: 44ch;
  margin-inline: auto;
  padding: var(--space-7) var(--space-4);
  text-align: center;
}

.empty-state__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin-bottom: var(--space-1);
  border-radius: var(--radius-m);
  color: var(--color-brand);
  background-color: color-mix(in srgb, var(--color-brand) 10%, transparent);
}

.empty-state__title {
  margin: 0;
}

.empty-state__text {
  margin: 0;
  color: var(--color-muted);
}

.empty-state .btn {
  margin-top: var(--space-2);
}


/* ------------------------------------------------------------------ */
/* Essential Cart Drawer's sticky bar, off                             */
/*                                                                     */
/* The app draws a sticky bar ("🔥 Get it while it lasts", product,    */
/* price, buttons) into #sticky-cart-portal at the foot of every       */
/* product page, on top of the theme's own sticky add-to-cart - two     */
/* bars competing for the same strip. The owner chose to switch it off.*/
/* The app keeps that switch in its own admin, not in the theme, so    */
/* this is the theme-side off switch: the portal holds only that bar   */
/* (checked - the cart drawer lives elsewhere), and turning the        */
/* feature off in the app later makes this rule a no-op.               */
/* ------------------------------------------------------------------ */
#sticky-cart-portal {
  display: none !important;
}

/* ------------------------------------------------------------------ */
/* Micro-motion, second pass                                           */
/* ------------------------------------------------------------------ */
/* Small, fast, and only where a hand or an eye already is: pressing a   */
/* button, opening a panel, hovering a link, a bar that measures        */
/* something. Everything here is off for reduced-motion visitors.       */
@media (prefers-reduced-motion: no-preference) {
  /* Every button and pill gives a little under the finger and springs
     back on release (the same press curve the icon controls use). */
  .btn:active,
  .variant-picker__pill:active,
  .tabbed-products__tab:active,
  .quantity__button:active {
    scale: 0.97;
  }

  /* Footer links: an underline draws in from the left. */
  .footer__menu-link {
    background-image: linear-gradient(currentColor, currentColor);
    background-position: 0 100%;
    background-repeat: no-repeat;
    background-size: 0 1px;
    text-decoration: none;
    transition: background-size 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), color 0.2s ease;
  }

  .footer__menu-link:hover,
  .footer__menu-link:focus-visible {
    background-size: 100% 1px;
  }

  /* Breadcrumb links, same gesture. */
  .breadcrumbs__link {
    background-image: linear-gradient(currentColor, currentColor);
    background-position: 0 100%;
    background-repeat: no-repeat;
    background-size: 0 1px;
    text-decoration: none;
    transition: background-size 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
  }

  .breadcrumbs__link:hover {
    background-size: 100% 1px;
  }

  /* Product page accordions: the body eases in when opened instead of
     snapping into place. */
  .product-accordion[open] > .product-accordion__body {
    animation: motion-fade-down 360ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }

  /* The stock bar fills from empty when the page loads, so the amount
     reads as a measurement rather than a static stripe. */
  .stock-bar__fill {
    transform-origin: 0 50%;
    animation: motion-fill 900ms cubic-bezier(0.2, 0.8, 0.2, 1) 200ms both;
  }

  /* Fields: the focus ring grows in rather than blinking on. */
  input:not([type='checkbox']):not([type='radio']),
  textarea,
  select {
    transition: border-color 0.2s ease, box-shadow 0.2s ease, outline-offset 0.2s ease;
  }

  /* Status chips and sale badges arrive with a small pop the first time
     they are painted. */
  .status-chip,
  .badge--sale {
    animation: motion-pop 380ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }

  /* The live "in stock" count in the strip under the hero counts up
     (assets/ships-today.js); tabular digits keep the width still. */
  .ships-today__count [data-count-text] {
    font-variant-numeric: tabular-nums;
  }

  /* Cart count badge: a pop whenever its number changes (cart.js adds
     is-bumping to the cart link already; the badge rides along). */
  .header__cart-link.is-bumping .header__cart-count,
  .header__cart-link.is-bumping [data-cart-count] {
    animation: motion-pop 420ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }
}

@keyframes motion-fade-down {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes motion-fill {
  from {
    transform: scaleX(0);
  }

  to {
    transform: scaleX(1);
  }
}

@keyframes motion-pop {
  from {
    opacity: 0;
    transform: scale(0.8);
  }

  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* Theme switch label: "Dark mode" on a light page, "Light mode" on a dark
   one - the same three-state logic as the sun and moon above. */
.theme-toggle__label--to-light { display: none; }
:root[data-theme='dark'] .theme-toggle__label--to-dark { display: none; }
:root[data-theme='dark'] .theme-toggle__label--to-light { display: inline; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .theme-toggle__label--to-dark { display: none; }
  :root:not([data-theme='light']) .theme-toggle__label--to-light { display: inline; }
}

/* Phones: a section's call to action spans the column ("View all",
   "Full roadmap", "Shop the drop", "Write a review", ...). A short pill
   floating left of an empty row read as an afterthought; full width reads
   as the next step and is an easier thumb target. Listed per section rather
   than as a blanket .btn rule so card buttons, pills and the hero keep
   their own sizes. */
@media screen and (max-width: 749px) {
  .tabbed-products__foot .btn,
  .perfect-match__cta,
  .brand-lab__cta,
  .drops-teaser__head .btn,
  .drop-reveal__cta .btn,
  .reviews__write .btn,
  .discord-cta__btn,
  .notes-series__all,
  .latest-drop .btn,
  .social-wall .btn,
  .featured-product__full-link {
    width: 100%;
    justify-content: center;
    text-align: center;
  }

  .drops-teaser__head,
  .notes-series__head {
    align-items: stretch;
  }

  .drop-reveal__cta,
  .reviews__write {
    width: 100%;
  }
}

/* ================================================================== */
/* Motion picks from the Waimers motion previews (assets/motion-extras.js) */
/* ================================================================== */

/* Cards keep position: relative (the pointer spotlight that set it was
   removed in the human pass; badges and overlays still anchor to it). */
.product-card__media,
.note-card,
.drop-card,
.footer__help-card,
.rs-card {
  position: relative;
}

/* Border beam plumbing, shared by the drop countdown card
   (component-notify.css) and the corner signup popup. */
@property --beam-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

/* Border beam on the next drop (first card in Next drops). */
.drops-teaser__list > .drop-card:first-child::before {
  content: '';
  position: absolute;
  inset: -1px;
  z-index: 2;
  padding: 1.5px;
  border-radius: inherit;
  background: conic-gradient(from var(--beam-angle), transparent 0 76%, var(--color-brand) 90%, #00fced 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .drops-teaser__list > .drop-card:first-child::before {
    animation: beam-travel 4.5s linear infinite;
  }
}

@keyframes beam-travel {
  to {
    --beam-angle: 360deg;
  }
}

/* 4. Word rotate: the highlighted word cycles (data-word-rotate). */
.word-rotate {
  display: inline-grid;
  vertical-align: bottom;
  overflow: hidden;
}

.word-rotate__word {
  grid-area: 1 / 1;
  white-space: nowrap;
}

@media (prefers-reduced-motion: no-preference) {
  .word-rotate__word {
    transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.6s ease;
  }
}

.word-rotate__word.is-in {
  transform: translateY(100%);
  opacity: 0;
}

.word-rotate__word.is-out {
  transform: translateY(-100%);
  opacity: 0;
}

/* ================================================================== */
/* Aceternity picks (assets/aceternity-extras.js)                      */
/* ================================================================== */

/* 1 Lens zoom on the product gallery */
.lens-zoom {
  position: absolute;
  z-index: 5;
  width: 140px;
  height: 140px;
  margin: -70px 0 0 -70px;
  border: 2px solid rgb(255 255 255 / 85%);
  border-radius: 50%;
  background-color: #fff;
  background-repeat: no-repeat;
  box-shadow: 0 14px 34px rgb(0 0 0 / 35%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}

.lens-zoom.is-on {
  opacity: 1;
}

@media screen and (max-width: 989px) {
  .lens-zoom {
    display: none;
  }
}

/* 2 3D tilt (Latest drop panel) */
[data-tilt] {
  transition: transform 0.2s ease-out;
  transform-style: preserve-3d;
  will-change: transform;
}

.latest-drop__media-wrap {
  perspective: 1000px;
}

/* 4 Focus cards: hovering one brand in the Glasspad Lab dims the rest */
@media (hover: hover) {
  .brand-lab__grid:hover .brand-lab__brand:not(:hover) {
    filter: blur(2px);
    opacity: 0.45;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .brand-lab__brand {
    transition: filter 0.35s ease, opacity 0.35s ease;
  }
}

/* 7 Tracing beam down the roadmap spine */
.roadmap.has-trace {
  position: relative;
}

.roadmap.has-trace::after {
  content: '';
  position: absolute;
  top: var(--trace-start, 0);
  left: calc(var(--roadmap-rail-w) + (var(--roadmap-spine-w) / 2) - 1px);
  z-index: 0;
  width: 2px;
  height: var(--trace, 0);
  border-radius: 2px;
  background: linear-gradient(#00fced, var(--color-brand));
  box-shadow: 0 0 12px var(--color-brand);
  pointer-events: none;
}

.roadmap.has-trace .roadmap__marker {
  position: relative;
  z-index: 1;
}

.roadmap__entry.is-traced .roadmap__marker {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-brand) 25%, transparent), 0 0 14px var(--color-brand);
}

@media (prefers-reduced-motion: no-preference) {
  .roadmap__marker {
    transition: box-shadow 0.4s ease;
  }
}

/* 9 Meteors in the newsletter card */
.newsletter__card {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.meteors {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

.meteor {
  position: absolute;
  top: -8px;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background-color: var(--nl-ink, #fff);
  transform: rotate(215deg);
  animation: meteor-fall linear infinite;
}

.meteor::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 80px;
  height: 1px;
  transform: translateY(-50%);
  background: linear-gradient(90deg, var(--color-brand), transparent);
}

@keyframes meteor-fall {
  0% {
    transform: rotate(215deg) translateX(0);
    opacity: 1;
  }

  70% {
    opacity: 1;
  }

  100% {
    transform: rotate(215deg) translateX(-700px);
    opacity: 0;
  }
}

/* 11 Animated tooltips (reviewer avatars under the review score) */
.avatar-row {
  display: flex;
  justify-content: center;
  margin-top: var(--space-2);
}

.avatar-row__item {
  position: relative;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  margin-left: -8px;
  border: 2px solid var(--color-bg);
  border-radius: 50%;
  background: hsl(var(--hue, 270) 45% 45%);
  color: #fff;
  font-size: 14px;
  font-weight: 700;
  cursor: default;
}

.avatar-row__item:first-child {
  margin-left: 0;
}

.avatar-row__item--more {
  background: color-mix(in srgb, var(--color-ink) 18%, var(--color-bg));
  color: var(--color-ink);
  font-size: 12px;
}

.avatar-row__tip {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  z-index: 3;
  padding: 6px 10px;
  border-radius: 8px;
  background-color: var(--color-ink);
  color: var(--color-bg);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.3;
  white-space: nowrap;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  translate: -50% 6px;
}

.avatar-row__tip b {
  display: block;
  font-size: 13px;
}

.avatar-row__item:hover,
.avatar-row__item:focus-visible {
  z-index: 2;
}

.avatar-row__item:hover .avatar-row__tip,
.avatar-row__item:focus-visible .avatar-row__tip {
  opacity: 1;
  translate: -50% 0;
}

@media (prefers-reduced-motion: no-preference) {
  .avatar-row__item {
    transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  .avatar-row__item:hover {
    transform: translateY(-4px) scale(1.08);
  }

  .avatar-row__tip {
    transition: opacity 0.2s ease, translate 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  }
}

/* ================================================================== */
/* UI plan picks (assets/ui-upgrades.js)                               */
/* ================================================================== */

/* P1 Scroll-in story images: tilted back, standing up as they arrive
   (--stand runs 0 -> 1 from ui-upgrades.js). */
.stand-up-stage {
  perspective: 1200px;
}

.stand-up {
  transform: rotateX(calc((1 - var(--stand, 1)) * 26deg)) scale(calc(0.9 + var(--stand, 1) * 0.1));
  transform-origin: 50% 100%;
  opacity: calc(0.4 + var(--stand, 1) * 0.6);
  will-change: transform;
}

/* K1 Free-shipping moment */
.confetti-canvas {
  position: fixed;
  inset: 0;
  z-index: 200;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
}

.free-ship-note {
  position: fixed;
  left: 50%;
  bottom: 28px;
  z-index: 201;
  padding: 12px 20px;
  border-radius: 999px;
  background-color: var(--color-ink);
  color: var(--color-bg);
  font-weight: 700;
  box-shadow: 0 16px 40px -16px rgb(0 0 0 / 60%);
  translate: -50% 24px;
  opacity: 0;
  transition: translate 0.45s cubic-bezier(0.34, 1.4, 0.64, 1), opacity 0.3s ease;
}

.free-ship-note.is-on {
  translate: -50% 0;
  opacity: 1;
}

/* C1 Compare */
.compare-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-2);
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-muted);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.compare-toggle__box {
  display: inline-grid;
  place-items: center;
  width: 16px;
  height: 16px;
  border: 1.5px solid currentColor;
  border-radius: 4px;
  font-size: 11px;
  line-height: 1;
}

.compare-toggle.is-on {
  color: var(--color-brand);
}

.compare-toggle.is-on .compare-toggle__box {
  border-color: var(--color-brand);
  background-color: var(--color-brand);
  color: #fff;
}

.compare-toggle.is-on .compare-toggle__box::after {
  content: '✓';
}

.compare-toggle:hover {
  color: var(--color-ink);
}

.compare-tray {
  position: fixed;
  left: 50%;
  bottom: 18px;
  z-index: 120;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: min(760px, calc(100vw - 24px));
  padding: 10px 12px 10px 14px;
  border-radius: 18px;
  background-color: var(--color-ink);
  color: var(--color-bg);
  box-shadow: 0 20px 50px -20px rgb(0 0 0 / 60%);
  translate: -50% 140%;
  transition: translate 0.45s cubic-bezier(0.34, 1.3, 0.64, 1);
}

.compare-tray.is-on {
  translate: -50% 0;
}

.compare-tray__items {
  display: flex;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
}

.compare-tray__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 1 auto;
  min-width: 0;
  max-width: 200px;
  padding: 4px 10px 4px 4px;
  border-radius: 999px;
  background-color: color-mix(in srgb, var(--color-bg) 14%, transparent);
  font-size: 12px;
  white-space: nowrap;
}

.compare-tray__chip span {
  overflow: hidden;
  text-overflow: ellipsis;
}

.compare-tray__chip img {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  object-fit: cover;
  background: #fff;
}

.compare-tray__hint {
  margin-left: auto;
  font-size: 12px;
  opacity: 0.75;
  white-space: nowrap;
}

.compare-tray__clear {
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 13px;
  opacity: 0.75;
  cursor: pointer;
}

.compare-tray__go {
  flex: none;
  min-height: 40px;
}

.compare-tray__go:disabled {
  opacity: 0.5;
  pointer-events: none;
}

@media screen and (max-width: 749px) {
  .compare-tray__hint {
    display: none;
  }
}

.compare-dialog {
  position: fixed;
  inset: 0;
  width: min(980px, calc(100vw - 24px));
  height: fit-content;
  max-height: calc(100svh - 48px);
  margin: auto;
  padding: 0;
  overflow: auto;
  border: 1px solid var(--color-line);
  border-radius: 18px;
  background-color: var(--color-bg);
  color: var(--color-ink);
}

.compare-dialog::backdrop {
  background: rgb(10 10 12 / 55%);
  backdrop-filter: blur(4px);
}

.compare-dialog__head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--color-line);
  background-color: var(--color-bg);
}

.compare-dialog__head h2 {
  margin: 0;
}

.compare-dialog__close {
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  font-size: 16px;
  cursor: pointer;
}

.compare-dialog__body {
  padding: var(--space-4) var(--space-5) var(--space-5);
}

.compare-table-wrap {
  overflow-x: auto;
}

.compare-table {
  width: 100%;
  min-width: 520px;
  border-collapse: collapse;
  font-size: 14px;
}

.compare-table th,
.compare-table td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--color-line);
  text-align: left;
  vertical-align: top;
}

.compare-table thead th {
  vertical-align: bottom;
}

.compare-table thead a {
  display: grid;
  gap: 8px;
  color: var(--color-ink);
  font-weight: 700;
  text-decoration: none;
}

.compare-table thead img {
  width: 90px;
  height: 90px;
  border-radius: var(--radius-m);
  object-fit: contain;
  background: var(--color-card-media, #fff);
}

.compare-table thead strong {
  display: block;
  margin-top: 4px;
  color: var(--color-brand);
}

.compare-table tbody th {
  width: 140px;
  color: var(--color-muted);
  font-weight: 600;
}

/* Judge.me reviews grid widget (Reviews page "wall"): its header - "From our
   customers", the 4.9 (75) average, "Verified" - and the Show more button
   are painted pure black by Judge.me, straight on the page background, so
   they vanished in dark mode. Theme ink for those two parts only; the review
   cards have their own background and keep Judge.me's colours. */
.jdgm-reviews-grid-widget > .jm-stack > .jm-stack:first-child :is(p, span, h1, h2, h3, h4) {
  color: var(--color-ink) !important;
}

.jdgm-reviews-grid-widget .jm-show-more-button {
  color: var(--color-ink) !important;
  border-color: var(--color-ink) !important;
  background-color: transparent !important;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.jdgm-reviews-grid-widget .jm-show-more-button:hover {
  background-color: var(--color-ink) !important;
  color: var(--color-bg) !important;
}

/* Home page: Pick your gear and Picked for you centred (owner's call,
   30 Sep 2026; Shop the range uses its own alignment setting). Keyed to the
   home template's own section ids so these sections keep left-aligned heads
   elsewhere. Next drops, Mouse Notes, From the community and The Glasspad
   Lab were tried centred and stay left. */
[id$="__category_panels"] .section-heading {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  text-align: center;
}

[id$="__category_panels"] .section-heading__text {
  align-items: center;
  text-align: center;
}

[id$="__featured_carousel"] .featured-carousel__head {
  justify-items: center;
  text-align: center;
}

/* Star rating row (snippets/rating-stars.liquid). Here rather than inline
   in the snippet, which renders once per rating row. */
.rating-stars {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.rating-stars__icons {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.rating-stars__star {
  display: inline-flex;
  /* Brand purple, at the merchant's request (was --color-accent-2, the
     acid yellow-green). Filled stars only - empties stay on the line
     colour. This snippet is the only place stars render, so changing it
     here covers the card grid and the product page rating row. */
  color: var(--color-brand);
}

.rating-stars__star--empty {
  color: var(--color-line);
}

.rating-stars__value {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 500;
  color: var(--color-ink);
}

.rating-stars__count {
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--color-muted);
}

/* Off-screen sections pause their CSS animations (assets/ui-upgrades.js sets
   data-offscreen). Dozens of infinite loops - meteors, marquees, shimmers,
   breathing dots - kept running after the visitor scrolled past them, and on
   a phone the page got choppier the further down it went. */
.shopify-section[data-offscreen] *,
.shopify-section[data-offscreen] *::before,
.shopify-section[data-offscreen] *::after {
  animation-play-state: paused !important;
}
