/** Shopify CDN: Minification failed

Line 787:17 Unexpected "("
Line 792:79 Unterminated string token

**/
/* ==========================================================================
   Velvet Vanity 2.0 — restyle
   --------------------------------------------------------------------------
   The ONLY stylesheet for redesign work. Loaded last in layout/theme.liquid,
   after theme.css, so it wins on cascade order without needing !important.

   Why a separate file: assets/theme.css is 648 KB and already carries 132
   !important declarations. Adding to it would make the theme worse and the
   restyle impossible to back out. One file = one revert.

   Naming: every custom property is prefixed --vv-. Palo Alto defines ~220 of
   its own in snippets/css-variables.liquid using --COLOR-* / SCREAMING-KEBAB,
   so there is no collision.

   Issue: #2
   ========================================================================== */

:root {
  /* --- Colour -------------------------------------------------------------
     Sampled from design/VelvetVanity UIUX.pdf by rasterising each slide at
     150dpi and taking the dominant colour of large flat areas, rather than
     eyeballing. Values are the mock-up's, not yet brand-verified. */

  /* Surfaces */
  --vv-bg:            #eeebe2;  /* page background — warm cream */
  --vv-surface:       #faf9f7;  /* cards, panels — near-white */
  --vv-surface-alt:   #ffffff;

  /* Ink */
  --vv-ink:           #0b0b0b;  /* body + headings; effectively black */
  --vv-ink-muted:     #6b6260;  /* secondary copy, descriptors */
  --vv-ink-on-dark:   #faf9f7;

  /* Brand */
  --vv-rose:          #a27776;  /* dusty rose — headings, accents, brand mark */

  /* Greens — all four measured off the artwork, consolidated here in #9.
     Earlier chunks each sampled one and wondered whether the deck was
     inconsistent. It isn't: it uses three deliberate depths, and the fourth
     reading was compression variance on the same colour.

       --vv-green      #214442  hero CTA button, the darkest
       --vv-green-bar  #284c4a  primary-button HOVER (the "Take 3, Pay 2"
                                promo bar it was named for was replaced by
                                the marquee in #76)
       --vv-teal       #547173  product-card CTAs (the BUY / ADD TO BAG pills)
                                (the card sampled #516f70 — same colour, the
                                 difference is image compression, not intent)

     ⚠️ The ANNOUNCEMENT BAR and the MARQUEE are no longer these values, and
     are not driven by these tokens at all. Marketing set both to #7E9C9F in
     the theme editor on 2026-10-05, just after go-live — a return to the
     store's `color_accent`, the pale sage the bar used before the redesign.
     Captured in cfe4a29.

     Those two bars are SECTION SETTINGS (`bg_color` in
     sections/group-header.json and templates/index.json), not CSS. Liquid
     cannot read a CSS custom property, so there is no rule here to change
     and nothing to "fix" in this file — look in the theme editor.

     Two consequences worth knowing before touching the greens:

       · The bar and the card pills USED to match (both --vv-teal). They no
         longer do: pale sage bar, #547173 pills. If sage is the direction,
         the pills likely want to follow — marketing's call, not ours.
       · White on #7E9C9F is 2.94:1, below WCAG AA (4.5:1) and below the 3:1
         large-text floor; both bars render white text. The values it
         replaced passed — #547173 at 5.27:1, #214442 at 10.66:1. Raised with
         marketing; deliberately NOT reverted, it is their direction. Dark
         ink on the sage would fix it (#0b0b0b on #7E9C9F is ~7.6:1) if they
         want the colour kept. */
  --vv-green:         #214442;
  --vv-green-bar:     #284c4a;
  --vv-teal:          #547173;

  /* Badges — measured on tatagaltier.com, see issue #9 */
  --vv-badge-save-bg: #fff7da;
  --vv-badge-bg:      #ffffff;

  /* Lines */
  --vv-line:          rgba(11, 11, 11, 0.12);
  --vv-line-strong:   rgba(11, 11, 11, 0.24);

  /* --- Type ---------------------------------------------------------------
     Families are set in #3. Only the scale lives here. */
  --vv-text-xs:   0.6875rem;  /* 11px — badges, eyebrow labels */
  --vv-text-sm:   0.8125rem;  /* 13px — descriptors, meta */
  --vv-text-base: 1rem;       /* 16px — body */
  --vv-text-md:   1.125rem;   /* 18px */
  --vv-text-lg:   1.5rem;     /* 24px — card / section headings */
  --vv-text-xl:   2rem;       /* 32px */
  --vv-text-2xl:  2.75rem;    /* 44px — hero */

  --vv-leading-tight: 1.15;
  --vv-leading-body:  1.6;
  --vv-tracking-caps: 0.04em; /* 0.44px at 11px, per the badge measurement */

  /* --- Space --------------------------------------------------------------
     4px base step. */
  --vv-space-1:  0.25rem;
  --vv-space-2:  0.5rem;
  --vv-space-3:  0.75rem;
  --vv-space-4:  1rem;
  --vv-space-5:  1.5rem;
  --vv-space-6:  2rem;
  --vv-space-7:  3rem;
  --vv-space-8:  4rem;

  /* --- Radius -------------------------------------------------------------- */
  --vv-radius-sm:   4px;
  --vv-radius-md:   8px;
  /* Cards, panels and media. Measured off the mock-up rather than guessed: the
     corner curve settles ~10.6px on the zoomed spec card, ~10.4px on the zoomed
     image, and ~8.4px on the full-page mock (where a 1px error is proportionally
     largest). 10px is the convergent value. Added alongside --md rather than
     replacing it, because --md is already load-bearing for the mega-menu panel
     and dropdowns. */
  --vv-radius-lg:   10px;
  --vv-radius-pill: 999px;  /* badges, swatches, CTA pills */

  /* --- Motion -------------------------------------------------------------- */
  --vv-ease:     cubic-bezier(0.4, 0, 0.2, 1);
  --vv-duration: 200ms;
}

@media (prefers-reduced-motion: reduce) {
  :root { --vv-duration: 0ms; }
}


/* ==========================================================================
   TYPOGRAPHY  (issue #3)
   --------------------------------------------------------------------------
   Heading face: the mock-up uses a high-contrast serif with a true calligraphic
   italic ("Everyday Beauty, *Elevated.*"). The PDF can't tell us which — the
   mock-ups are raster images, and the only embedded fonts are the deck's own
   Poppins slide titles.

   ⚠️ PLAYFAIR DISPLAY IS A STAND-IN, not a confirmed brand face. It is a close
   match and OFL-licensed, so it is safe to self-host. If the design team names
   the real face, swapping it is: replace the two woff2 files and the family name
   in --vv-font-heading. Nothing else references the font directly.

   Why this matters regardless of the swap: the store currently has NO brand
   heading font. config/settings_data.json sets `type_heading_font: serif` — the
   generic option — so headings render as New York on macOS and Times New Roman
   on Windows. Today's site looks different on every platform. This fixes that.

   Body face: DM Sans, already loaded by the theme from Shopify's font library
   (`type_base_font: dm_sans_n4`). Matches the deck's body copy. Left alone.
   ========================================================================== */

@font-face {
  font-family: 'VV Playfair';
  src: url('vv-playfair-roman.woff2') format('woff2');
  font-weight: 400 700;          /* variable font */
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'VV Playfair';
  src: url('vv-playfair-italic.woff2') format('woff2');
  font-weight: 400 700;
  font-style: italic;
  font-display: swap;
}

:root {
  --vv-font-heading: 'VV Playfair', 'Iowan Old Style', Baskerville, 'Times New Roman', Times, serif;
  --vv-font-body: var(--FONT-STACK-BODY, 'DM Sans', sans-serif);

  /* Redefining Palo Alto's own heading stack is how this reaches every heading
     without fighting individual selectors: theme.css consumes
     var(--FONT-STACK-HEADING) throughout, and this file loads after both
     snippets/css-variables.liquid and theme.css, so it wins on document order. */
  --FONT-STACK-HEADING: var(--vv-font-heading);
}

/* The hero pattern from the deck: roman heading with an italic accent phrase
   in the brand rose. Applied via a class so copy stays authorable. */
.vv-accent-italic {
  font-style: italic;
  color: var(--vv-rose);
}

/* ==========================================================================
   BUTTONS  (issue #3)
   --------------------------------------------------------------------------
   The deck's primary CTA is a solid dark-green button with an uppercase,
   letter-spaced label. Palo Alto's button classes are .btn plus modifiers
   (.btn--primary, .btn--solid, .btn--outline …), so these layer onto those
   rather than introducing a parallel system.

   Greens measured off the mock-up:
     #214442  hero "SHOP ALL" button   -> --vv-green      (primary CTA)
     #284c4a  "Take 3, Pay 2" promo bar -> --vv-green-bar
   They are close but distinct. The announcement bar sampled lighter again
   (#3d5b5b-#405d5c) — since resolved as --vv-teal #547173 in #4.
   ========================================================================== */

.btn {
  font-family: var(--vv-font-body);
  text-transform: uppercase;
  letter-spacing: var(--vv-tracking-caps);
  font-size: var(--vv-text-sm);
  border-radius: var(--vv-radius-sm);
  transition: background-color var(--vv-duration) var(--vv-ease),
              color var(--vv-duration) var(--vv-ease),
              opacity var(--vv-duration) var(--vv-ease);
}

.btn--primary,
.btn--solid {
  background-color: var(--vv-green);
  color: var(--vv-ink-on-dark);
  border-color: var(--vv-green);
}

.btn--primary:hover,
.btn--solid:hover {
  background-color: var(--vv-green-bar);
  border-color: var(--vv-green-bar);
}

/* Disabled / sold-out. Tatagaltier's approach, adopted deliberately: keep the
   brand fill and drop opacity, rather than introducing a grey token. Reused by
   the sold-out CTA in #15 and the product card in #9. */
.btn[disabled],
.btn[aria-disabled='true'],
.btn.is-disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Visible keyboard focus — Palo Alto's default is faint against the new palette. */
.btn:focus-visible {
  outline: 2px solid var(--vv-ink);
  outline-offset: 2px;
}



/* ==========================================================================
   ANNOUNCEMENT BAR  (issue #4)
   --------------------------------------------------------------------------
   The mock-up shows every message on ONE line separated by a thin rule, not
   rotating. Palo Alto shipped only 'slider' and 'marquee', so an 'inline'
   layout was added to sections/announcement-bar.liquid. Blocks stay separate,
   so each message keeps its own link and device-targeting settings.

   ⚠️ THE BAR COLOUR IS NOT SET HERE. The section emits
   `#Announcement--{id} .top-bar { --bg: … }` from its own theme setting, in a
   {% style %} block inside the section body — an ID selector, rendered after
   this file. It cannot be overridden without !important, which this project
   does not use. It is also client-owned content (sections/group-header.json),
   so we must not push it.

   Currently #7e9c9f (pale sage). The mock-up's bar is #547173 — the same hue,
   deeper. Changing it is one field in the theme editor:
     Theme editor -> Announcement bar -> Colors -> Background = #547173
   The value is --vv-teal, defined with the other greens at the top.
   ========================================================================== */

:root {
  /* Gap either side of the separator. Measured off the mock-up: the deck puts
     exactly 10px between text and rule on BOTH sides. Kept as a component token
     rather than a new global spacing step, since 10px sits between --vv-space-2
     (8px) and --vv-space-3 (12px). */
  --vv-bar-separator-gap: 0.625rem;
}

.top-bar__inline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0 var(--vv-bar-separator-gap);
  text-align: center;
}

/* Each block renders as a plain div in this layout (it reuses the marquee
   markup). Keep them on the baseline and stop them stretching. */
.top-bar__inline > * {
  display: inline-flex;
  align-items: center;
  flex: 0 1 auto;
}

/* Separator between messages. Drawn on the element rather than as a text
   character so it never lands at the start or end of a wrapped row. */
/* The separator is drawn at the START of every item after the first, so the
   spacing either side of it comes from two different places:
     left  = the flex container's column gap
     right = this margin
   They must match or the rule sits off-centre between the two messages. */
.top-bar__inline > * + *::before {
  content: '';
  display: inline-block;
  width: 1px;
  height: 1em;
  margin-inline-end: var(--vv-bar-separator-gap);
  background-color: currentColor;
  opacity: 0.45;
}

/* --- Free-shipping message inside the inline bar -------------------------
   theme.css normalises the cart message for the announcement bar in FOUR rules,
   every one of them scoped to `.top-bar__text`:

     .top-bar__text .cart__message                      { padding:0; margin:0; width:100%; font-size:… }
     .top-bar__text .cart__message.is-hidden            { visibility:visible; max-height:none; opacity:1 }
     .top-bar__text .cart__message .cart__message__default          { padding-bottom:0 }
     .top-bar__text .cart__message .cart__message__default.is-hidden { display:none }

   `.top-bar__text` exists only in the SLIDER layout. Our inline layout (#4) never
   had it, so the message kept its cart-drawer styling and two faults showed once
   a cart was involved:

   1. The hidden "spend X more" span fell back to the generic
      `.cart__message__default.is-hidden { visibility:hidden; max-height:0;
      padding:0 15px }`. visibility keeps an element in flow and max-height does
      nothing to an inline span, so on a cart past the threshold it still occupied
      257px + 30px of padding — the large gap in the reported screenshot.

   2. `.cart__message` kept `padding: 20px`, which no plain text block has. That
      pushed its text 20px inside its own box and left the separators sitting
      10px/30px off-centre rather than evenly between the messages.

   The theme's own `section.id == 'announcement-bar'` guard (announcement-bar.liquid:210)
   was meant to drop the cart-drawer class here, but inside a section GROUP the id
   is not that literal string, so the guard never fires and `cart-drawer__message`
   is applied anyway. Normalising in CSS rather than touching that condition.

   Rule 2 above is deliberately NOT ported: it force-shows a message the theme
   marked hidden, which suits the slider (it cycles through blocks regardless) but
   would undo the empty-cart behaviour below, where the whole block should vanish.
   -------------------------------------------------------------------------- */

.top-bar__inline .cart__message {
  padding: 0;
  margin: 0;
  width: 100%;
}

.top-bar__inline .cart__message .cart__message__default {
  padding-bottom: 0;
}

.top-bar__inline .cart__message .cart__message__default.is-hidden,
.top-bar__inline .cart__message .cart__message__success.is-hidden {
  display: none;
}

/* A block can render with all its content hidden — the free-shipping progress
   message carries .is-hidden until the cart qualifies, and JS toggles it at
   runtime. Left alone it still occupies width and draws a separator, leaving an
   orphaned rule and a gap in the middle of the bar. Hide any item that has no
   visible child; :has() re-evaluates when JS removes the class, so the message
   appears correctly once the cart qualifies. */
.top-bar__inline > *:not(:has(> :not(.is-hidden))) {
  display: none;
}

/* On narrow screens the three messages can't share a line. Wrapping keeps them
   readable rather than truncating; the separators still fall between pairs.
   This stays the no-JS / reduced-motion fallback — see the rotator below. */
@media (max-width: 749px) {
  .top-bar__inline {
    gap: var(--vv-space-1) var(--vv-bar-separator-gap);
    font-size: var(--vv-text-xs);
  }
  .top-bar__inline > * + *::before {
    margin-inline-end: var(--vv-bar-separator-gap);
  }
}

/* --- Mobile rotator --------------------------------------------------------
   Wrapping three messages made the bar taller than the header on a phone. Below
   750px they rotate one at a time instead, the same read as the slider layout.

   The --rotating class is added by JS, and only when the viewport is narrow AND
   the visitor has not asked for reduced motion. Without JS, or with reduced
   motion, none of this applies and the wrapped layout above stands — every
   message still reachable, nothing moving on its own.

   Grid rather than absolute positioning: all messages occupy the same cell, so
   the bar sizes itself to the tallest without a hard-coded height. */
@media (max-width: 749px) {
  .top-bar__inline--rotating {
    display: grid;
    gap: 0;
    align-items: center;
  }

  .top-bar__inline--rotating > * {
    grid-area: 1 / 1;
    justify-content: center;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.4s ease, visibility 0s linear 0.4s;
  }

  .top-bar__inline--rotating > .vv-bar-msg--active {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.4s ease;
  }

  /* One message on screen at a time, so there is nothing to separate. */
  .top-bar__inline--rotating > * + *::before {
    display: none;
  }
}

/* ==========================================================================
   HEADER  (issue #5)
   --------------------------------------------------------------------------
   Navigation decided 2026-10-01: the nav-bar / sidebar slide is authoritative.
   The category-led nav on the product and shop slides is a leftover the
   designer did not update. See NAVIGATION-DECISION.md.

     velvet vanity   Shop All ⌄   Bestsellers   New In   Find Us in Store   Gift Card   🔍 👤 🛍

   The mega menu under "Shop All" is issue #6; this issue is the bar itself.
   ========================================================================== */

/* Warm near-white, not pure white.

   The paint lives on .site-header itself; .site-header__background is set to
   `background: inherit`, so overriding a --bg custom property there does nothing
   — verified in the browser before landing on this selector.

   Scoped to :not(--transparent) so the theme's transparent-header mode (used
   when a header overlays a full-bleed hero) still works. That class isn't
   applied on this store today, but the setting exists and #18 may turn it on. */
.site-header:not(.site-header--transparent) {
  background-color: var(--vv-surface);
}

/* --- Navigation ---------------------------------------------------------- */

/* The design sets navigation in sentence case. Palo Alto's "Uppercase" typography
   setting (nav_caps) adds a .caps class to the <nav>, which we override here.
   NOTE: that theme setting therefore no longer affects the main nav — same
   trade-off as the button caps in #3, and deliberate: casing is a decision of
   this redesign rather than something to leave switchable. */
.nav.caps .navlink--toplevel {
  text-transform: none;
  letter-spacing: normal;
}

.navlink--toplevel {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-sm);
  font-weight: 400;
  color: var(--vv-ink);
}

/* Underline: REUSE the theme's, do not add another.

   Palo Alto already draws a two-part sliding underline on the inner .navtext
   span — ::before wipes in from the left while ::after wipes out to the right,
   sized by --BORDER-WIDTH. An earlier version of this file added a third line on
   the <a>, which rendered as a visible double underline: the theme's at -1.6px
   and ours at -3.25px.

   So the only thing missing is persistence on the CURRENT page. The theme animates
   on hover but doesn't keep the underline on the active item, which the design shows
   (the open "Shop All" is underlined). This holds the theme's own ::before open. */
.menu-item--active > .navlink--toplevel > .navtext::before {
  transform: scaleX(1);
}

/* --- Cart count ---------------------------------------------------------- */

/* Palo Alto ships a dot inside the bag SVG; the design shows a counted badge.
   The markup is added in sections/header.liquid. */
.cart__toggle {
  position: relative;
}

.vv-cart-count {
  position: absolute;
  top: -0.15em;
  right: -0.35em;
  min-width: 1.25em;
  height: 1.25em;
  padding: 0 0.3em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--vv-radius-pill);
  background-color: var(--vv-ink);
  color: var(--vv-ink-on-dark);
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-xs);
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* An empty bag shouldn't advertise itself. :has() keeps this in sync when cart
   JS rewrites the count, without needing a class toggle. */
.cart__toggle:not(.cart__toggle--has-items) .vv-cart-count {
  display: none;
}

/* The theme's own dot indicator would sit behind the badge — hide it, since the
   badge now carries the same signal with more information. */
.cart__toggle .cart-indicator {
  display: none;
}

/* ==========================================================================
   MEGA MENU  (issue #6)
   --------------------------------------------------------------------------
   The design shows the "Shop All" dropdown as a rounded card holding the six
   categories in TWO COLUMNS, each row a small square thumbnail, a label, and a
   chevron.

   Palo Alto already had most of this: snippets/header-link-image.liquid derives
   a thumbnail from the link target (link.object.featured_image), and the
   dropdown markup and hover behaviour exist. Two things were missing — that
   snippet was only wired into the three-level menu branch (fixed in
   snippets/nav-item.liquid), and the default styling renders each image at full
   dropdown width, stacked, with the label underneath.

   Scoped to .vv-navlink--tile, the class added only to child links that actually
   resolved a thumbnail, so dropdowns without images keep the theme's styling.
   ========================================================================== */

/* The theme caps dropdowns at 320px via `.parent .header__dropdown` (0,2,0), which
   is narrower than two columns of thumbnail + label. Lift that cap only for the
   tiled variant; :has() takes the specificity to (0,3,0) so it wins on merit
   rather than !important. The wrapper's overflow has to go too, or the card
   simply scrolls inside the old width. */
.parent .header__dropdown:has(.vv-navlink--tile),
.parent .header__dropdown:has(.vv-navlink--tile) .header__dropdown__wrapper {
  max-width: none;
  width: max-content;
  overflow: visible;
}

/* The card itself: wide enough for two columns, rounded, lifted off the page. */
.header__dropdown__inner:has(.vv-navlink--tile) {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--vv-space-2) var(--vv-space-5);
  min-width: 34rem;
  padding: var(--vv-space-4);
  border-radius: var(--vv-radius-md);
  background-color: var(--vv-surface-alt);
  box-shadow: 0 12px 32px rgba(11, 11, 11, 0.1);
}

/* One row: thumbnail | label | chevron.

   Scoped through .header__dropdown__inner deliberately. The theme sets
   `display: block` via `.parent .navlink--child` (0,2,0); a bare
   `.vv-navlink--tile` (0,1,0) loses to it and the grid silently never applies —
   which is exactly what happened first time round. Matching that specificity and
   loading later wins without !important. */
.header__dropdown__inner .vv-navlink--tile {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--vv-space-3);
  padding: var(--vv-space-2);
  border-radius: var(--vv-radius-sm);
  text-transform: none;          /* the theme sets dropdown links uppercase */
  letter-spacing: normal;
  font-size: var(--vv-text-base);
  color: var(--vv-ink);
  transition: background-color var(--vv-duration) var(--vv-ease);
}

.header__dropdown__inner .vv-navlink--tile:hover,
.header__dropdown__inner .vv-navlink--tile:focus-visible {
  background-color: var(--vv-bg);
}

/* The thumbnail is our own <img> (see snippets/nav-item.liquid) rather than the
   theme's image-fill wrapper, whose padding-ratio box sizes against the grid
   column and so can't be constrained to 48px without !important. */
.vv-navlink__thumb {
  width: 3rem;
  height: 3rem;
  object-fit: cover;
  border-radius: var(--vv-radius-sm);
  background-color: var(--vv-bg);
}

/* Label sits in the middle column; stop the theme's underline appearing here,
   since the whole row is the hit target and it has its own hover treatment. */
.vv-navlink--tile .navtext::before,
.vv-navlink--tile .navtext::after {
  content: none;
}

/* Chevron, drawn rather than shipped as an asset so it inherits colour. */
.header__dropdown__inner .vv-navlink--tile::after {
  content: '';
  width: 0.45em;
  height: 0.45em;
  border-top: 1px solid currentColor;
  border-right: 1px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.5;
  transition: transform var(--vv-duration) var(--vv-ease);
}

.header__dropdown__inner .vv-navlink--tile:hover::after {
  transform: rotate(45deg) translate(2px, -2px);
}

/* One column on narrow viewports — two 3rem thumbnails plus labels won't fit. */
@media (max-width: 989px) {
  .header__dropdown__inner:has(.vv-navlink--tile) {
    grid-template-columns: minmax(0, 1fr);
    min-width: 0;
  }
}

/* ==========================================================================
   MOBILE DRAWER  (issue #7)
   --------------------------------------------------------------------------
   The design's drawer: logo and close, a search field, "Shop All" expanded into
   category rows with square thumbnails and chevrons, then the flat links, then
   Account.

   Palo Alto already renders the thumbnails (the drawer calls the same
   header-link-image snippet as the mega menu) and already has the collapsible
   behaviour. Added here: the search field — see snippets/nav-drawer.liquid —
   and the row styling.
   ========================================================================== */

.vv-drawer__search {
  padding: var(--vv-space-4) var(--vv-space-5) var(--vv-space-2);
}

.vv-drawer__search .search-form__input {
  width: 100%;
  border: 1px solid var(--vv-line);
  border-radius: var(--vv-radius-sm);
  padding: var(--vv-space-3);
  /* Clear the magnifier, which the theme absolutely positions at the field's
     start inside .search-form__label (that element is the icon holder, not a
     text label — worth knowing, it is easy to mistake for one). */
  padding-inline-start: 3rem;
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-base);
  background-color: var(--vv-surface);
}

/* --- Category rows ------------------------------------------------------- */

/* The drawer wraps each thumbnail in .mobile-navlink__image, which contains the
   theme's padding-ratio box. Sizing the WRAPPER makes the ratio box resolve
   against 3rem and come out square — no need to fight the inline padding, which
   is the trap that bit the mega menu. */
.mobile-navlink--image {
  display: flex;
  align-items: center;
  gap: var(--vv-space-3);
  text-transform: none;
  letter-spacing: normal;
}

.mobile-navlink--image .mobile-navlink__image {
  flex: none;
  width: 3rem;
  overflow: hidden;
  border-radius: var(--vv-radius-sm);
  background-color: var(--vv-bg);
}

/* Chevron on category rows, matching the mega menu's. */
.mobile-navlink--image::after {
  content: '';
  margin-inline-start: auto;
  width: 0.45em;
  height: 0.45em;
  border-top: 1px solid currentColor;
  border-right: 1px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.5;
}

/* --- Flat rows and dividers ---------------------------------------------- */

.mobile-menu__item--level-1 + .mobile-menu__item--level-1 {
  border-top: 1px solid var(--vv-line);
}

.mobile-navlink--level-1 {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-md);
  text-transform: none;
  letter-spacing: normal;
  color: var(--vv-ink);
}

/* --- Account and search links at the foot -------------------------------- */

.mobile-nav--bottom {
  border-top: 1px solid var(--vv-line);
  margin-top: var(--vv-space-4);
  padding-top: var(--vv-space-2);
}

.mobile-navlink--small {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-base);
  text-transform: none;
  letter-spacing: normal;
  color: var(--vv-ink);
}

/* ==========================================================================
   FOOTER  (issue #8)
   --------------------------------------------------------------------------
   The deck shows two different footers: a compact single row on the homepage
   slide, and four columns (Shop / Help / About / Follow us) plus a legal row on
   the product slide.

   DECIDED: keep the four-column structure. It matches the product slide, it is
   what the theme is already configured with, and — the deciding factor — the
   compact version has no newsletter signup. The store's footer currently
   captures email ("Let's keep in touch!"), and silently dropping a marketing
   capture is not a styling decision. If marketing wants the compact footer, that
   is a deliberate call to make, not a side effect of a restyle.

   Background colour is NOT set here. The section emits `.site-footer { --bg }`
   from its own theme setting, so it stays a theme-editor field — consistent with
   how the announcement bar's colour is handled. The deck's footer is #f8f8f5;
   the store currently has #f0f0f0.
   ========================================================================== */

/* Column headings. The theme gives these .h5, which picks up the heading stack
   and renders them in the display serif — the deck sets them in bold sans. */
.footer-title {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-sm);
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
  color: var(--vv-ink);
  margin-bottom: var(--vv-space-3);
}

.footer-link {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-sm);
  color: var(--vv-ink-muted);
  transition: color var(--vv-duration) var(--vv-ease);
}

.footer-link:hover,
.footer-link:focus-visible {
  color: var(--vv-ink);
}

.footer-links {
  display: grid;
  gap: var(--vv-space-2);
}

/* Brand paragraph reads as supporting copy, not body text. */
.footer-block--text {
  font-size: var(--vv-text-sm);
  color: var(--vv-ink-muted);
}

/* Legal row: quieter than the columns above it. */
.footer-copyright {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-xs);
  color: var(--vv-ink-muted);
}
   PRODUCT CARD  (issue #9)
   --------------------------------------------------------------------------
   The most reused component in the project — the collection grid (#10) and the
   homepage rails (#20) both render it.

   The deck's card: image on a warm ground, badge top-left, wishlist top-right,
   then left-aligned title, descriptor, price, a row of shade dots, and a
   full-width CTA pill reading "BUY • RM69".

   What the theme already provides, verified rather than assumed:
     - a badge from the product metafield theme.badge (NOT custom.badge — see #24)
     - a derived sale badge from compare_at_price
     - sold-out detection
     - colour swatches on cards (enable_color_swatches_collection is on)
     - a quick-buy button, hidden until hover
   ========================================================================== */

/* Text alignment is NOT set here. Palo Alto exposes it as a theme setting
   (settings.content_alignment, currently "center"; the deck is left), and the
   theme already emits a matching class. Using the setting keeps the client's
   control meaningful — one theme-editor field rather than a CSS override. */

.product-grid-item__title {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-base);
  font-weight: 500;
  color: var(--vv-ink);
  text-transform: none;
  letter-spacing: normal;
}

.product-grid-item__price {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-base);
  font-weight: 600;
  color: var(--vv-ink);
}

/* --- Badges -------------------------------------------------------------- */

/* Measured off the artwork: pale teal ground, dark text, uppercase and tracked,
   sitting top-left of the image. Note this is NOT Tatagaltier's pale yellow —
   the Velvet Vanity deck uses its own badge colour, so it is defined here rather
   than reused. */
:root {
  --vv-badge-bg: #e1e9e8;
}

.product-grid-item .product-badge,
.product-grid-item [class*="badge"] {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--vv-tracking-caps);
  color: var(--vv-ink);
  background-color: var(--vv-badge-bg);
  border-radius: var(--vv-radius-sm);
  padding: 0.35em 0.7em;
}

/* --- Shade dots ----------------------------------------------------------- */

/* Colours come from assets/swatches.json, which #25 populated with all 68 real
   shades. Palo Alto 5.1.1 ignores Shopify's native option-value swatch data
   entirely — matchColors() in theme.js looks the option value up in that file,
   lowercased with whitespace stripped, and falls back to #E5E5E5. */
.product-grid-item .swatch__button {
  border-radius: var(--vv-radius-pill);
  border: 1px solid var(--vv-line);
}

/* --- CTA: deliberately NOT restyled here ---------------------------------

   The deck shows a persistent full-width "BUY • RM69" pill below the card info.
   Palo Alto's quick-buy is a small overlay button rendered INSIDE the image
   container and revealed on hover — a different component in a different place,
   not a styling difference.

   Forcing it with CSS produced a 24px blob, because the element is sized for an
   icon and its parent is the media box. Making the deck's CTA real means moving
   the quick-buy markup out of the image block and into .product-grid-item__info,
   which is a structural change to a 574-line snippet whose opening tag shares a
   line with another tag. Split out rather than risked here — see the follow-up
   issue linked from #9. */


/* --------------------------------------------------------------------------
   #26 — Shade selector, after removing the Orichi Color Swatch app
   --------------------------------------------------------------------------
   Orichi hid Palo Alto's own swatches (display:none on .selector-wrapper, set
   from its cross-origin stylesheet) and drew its own row in their place. With
   the app embed disabled the native swatches come back at full size, already
   coloured from swatches.json, already selectable — which is what we wanted.

   The one thing they don't do is name the selected shade: the theme's <legend>
   prints the option name only. Orichi printed "Color: Cookie (Caramel Nude)".
   Losing it on 12 colour PDPs would be a regression caused by our own change,
   so the value is re-emitted in snippets/product.liquid and kept in sync by
   assets/vv-restyle.js.
   -------------------------------------------------------------------------- */

/* The legend is a block and .radio__legend__label inside it is ALSO block at
   full width, so an inline sibling simply drops to the next line — "Color" on
   one line and ": Cookie (Caramel Nude)" on the next, which is how the first
   attempt rendered. Flex on the legend puts them back on one line.

   Scoped with :has() so it only touches legends that actually carry a value.
   Palo Alto has its own .radio__legend--flex variant for the size-guide link,
   and this must not change that. */
.radio__legend:has(.vv-swatch-legend__value) {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 var(--vv-space-1);
}

/* Colon on the label rather than the value, so the flex gap falls after it:
   "Color: Cookie", not "Color : Cookie". */
.radio__legend:has(.vv-swatch-legend__value) .radio__legend__label::after {
  content: ":";
}

.vv-swatch-legend__value {
  color: var(--vv-ink-muted);
}

/* ==========================================================================
   COLLECTION PAGE  (issue #10)
   --------------------------------------------------------------------------
   Heading, category tabs, the filter/sort/count toolbar, and grid density.

   Most of this chunk turned out NOT to be code. Palo Alto already ships the
   toolbar (filter button, sort dropdown, product count, mobile sticky bar) and
   already renders collection.title as the h1 with collection.description as the
   subtitle. They were switched off in the template, not missing from the theme:

     templates/collection.json  enable_filters: false -> true
                                enable_sorting: false -> true
     config/settings_data.json  products_per_row_on_desktop: 2 -> 4
                                products_per_row_on_mobile: "1" -> "2"

   Those are theme-editor settings, listed in the PR. This file only styles what
   they reveal.

   Grid density: the mock-up shows FIVE columns. Palo Alto's
   products_per_row_on_desktop is a 2-4 range feeding
   `--grid: repeat(N, minmax(0, 1fr))` in snippets/css-variables.liquid, so five
   would mean overriding the theme's own grid variable. Decided 2026-10-01 to
   ship four and leave the variable alone — the card carries a title, descriptor,
   price, shade dots and a CTA, and four gives it room. Reversible: redefine
   --grid here if five is ever wanted.
   ========================================================================== */

/* --- Heading -------------------------------------------------------------- */

/* The deck sets the title and its subtitle flush left above the tabs, not
   centred in a tall band. Alignment itself is the section's `text_position`
   setting (theme editor); this is the type treatment. */
.collection__title,
.collection-image-with-title h1 {
  font-family: var(--vv-font-heading);
  font-size: var(--vv-text-2xl);
  line-height: var(--vv-leading-tight);
  letter-spacing: 0;
}

.collection__description {
  font-size: var(--vv-text-base);
  line-height: var(--vv-leading-body);
  color: var(--vv-ink-muted);
  max-width: 48ch;
}

/* --- Category tabs -------------------------------------------------------- */

.vv-tabs__list {
  display: flex;
  align-items: center;
  gap: var(--vv-space-5);
  margin: 0;
  padding: 0;
  list-style: none;
  border-bottom: 1px solid var(--vv-line);
}

.vv-tabs__link {
  display: inline-block;
  padding: var(--vv-space-3) 0;
  font-size: var(--vv-text-sm);
  color: var(--vv-ink-muted);
  text-decoration: none;
  white-space: nowrap;

  /* The active underline sits ON the list's border, so reserve its height on
     every tab. Without this the row shifts by 2px as the active tab changes. */
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color var(--vv-duration) var(--vv-ease);
}

.vv-tabs__link:hover,
.vv-tabs__link:focus-visible {
  color: var(--vv-ink);
}

.vv-tabs__link--active {
  color: var(--vv-ink);
  border-bottom-color: var(--vv-ink);
}

/* Mobile: one scrollable row rather than wrapping to two. The deck shows the
   last tab clipped at the right edge, which is the affordance that there is
   more to scroll to. */
@media (max-width: 767px) {
  .vv-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .vv-tabs::-webkit-scrollbar {
    display: none;
  }

  .vv-tabs__list {
    gap: var(--vv-space-4);
    width: max-content;
    min-width: 100%;
  }
}

/* --- Toolbar: filter · sort · count --------------------------------------- */

/* Palo Alto puts breadcrumbs, the filter toggle, the sort popout and the count
   in one sticky bar. The deck drops breadcrumbs here and splits the rest:
   filter hard left, sort and count hard right. */
.collection__products-count {
  font-size: var(--vv-text-sm);
  color: var(--vv-ink-muted);
}

.collection__filters__toggle {
  font-size: var(--vv-text-sm);
  letter-spacing: var(--vv-tracking-caps);
  text-transform: uppercase;
}

/* --- Grid ----------------------------------------------------------------- */

/* The deck's grid is tighter vertically than Palo Alto's default, because the
   card now carries its own CTA and needs less breathing room below it. */
.collection-items .grid {
  row-gap: var(--vv-space-7);
}

/* Toolbar layout: the deck puts Filter hard left, then Sort and the product
   count hard right — on BOTH breakpoints. Palo Alto's own arrangement differs:
   on desktop the order is sort, count, filter-toggle (the toggle being mobile-
   only); on mobile it hides the sort bar and the count entirely and moves
   sorting inside the filter drawer, leaving a lone filter icon on the right.

   `order` re-sequences the existing flex children rather than moving markup. */
.collection__sticky-bar {
  align-items: center;
  gap: var(--vv-space-4);
}

.collection__sticky-bar .collection__filters__toggle {
  order: 1;
  margin-right: auto;   /* pushes everything after it to the right */
}

.collection__sticky-bar .collection__sort-bar {
  order: 2;
}

.collection__sticky-bar .collection__products-count {
  order: 3;
}

@media (max-width: 767px) {
  /* Surface the two the theme hides. Sorting stays available in the drawer too;
     this adds the deck's row rather than removing the theme's fallback. */
  .collection__sticky-bar .collection__products-count {
    display: block;
  }

  .collection__sticky-bar .collection__sort-bar {
    display: block;
  }
}


/* ==========================================================================
   PRODUCT CARD CTA  (issue #37, split out of #9)
   --------------------------------------------------------------------------
   The deck shows a persistent full-width pill below the card info reading
   "BUY • RM69". Palo Alto's equivalent was a small icon button rendered INSIDE
   the image container and revealed on hover.

   #9 tried to restyle it in place and produced a 24px blob: the element is
   sized for an icon and its containing block is the media box, so width and
   padding resolved against the image rather than the card. The fix is
   structural, and it is in snippets/product-grid-item.liquid -- the block now
   sits inside .product-grid-item__info. This only styles it.

   Behaviour is unchanged on purpose. A single-variant product still posts a
   real add-to-cart form; a multi-variant one still opens quick view, because a
   shade has to be chosen before anything can go in the bag. Same two paths the
   hover button had, in a place people can see.
   ========================================================================== */

/* Bottom-align the pill across a row. Cards carry different amounts of content
   -- some have shade dots, some have two instalment widgets, some neither -- so
   in normal flow each pill lands at a different height and the row looks ragged.
   Making the card a flex column lets the info block grow and the pill sit at the
   bottom of whatever height the tallest card in the row sets.

   Scoped with :has() so only cards that actually have a CTA become flex. The
   same snippet renders in product rails and search, and this must not reflow
   those. */
.product-grid-item:has(.vv-card-cta) {
  display: flex;
  flex-direction: column;
}

.product-grid-item:has(.vv-card-cta) .product-grid-item__info {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
}

.vv-card-cta {
  margin-top: auto;
  padding-top: var(--vv-space-3);
}

/* Palo Alto's .btn--quick is an ICON-CIRCLE component, not a button that merely
   looks different. Measured on the moved element before fixing:

     .btn--quick   position: absolute   -> out of flow; the pill overlapped the
                                           payment lines above it
     .btn__inner   background: #fff     -> the white blob in the middle
                   width: 63px             (it is the icon's circle)
     .btn__text    opacity: 0           -> label was screen-reader-only

   So each has to be restated, not just the wrapper. The first attempt reset
   position on .vv-card-cta (the wrapper) and left .btn--quick absolute, which
   looked plausible in the stylesheet and was wrong on screen.

   Restated here rather than edited in theme.css -- one file, one revert. */
.vv-card-cta,
.product-grid-item:hover .vv-card-cta {
  position: static;          /* theme makes .product-grid-item__quick-buy absolute */
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
}

/* The WRAPPER and the BUTTON are positioned separately by the theme, and both
   have to come back into flow. Resetting only the button left the wrapper
   absolute, which put the pill on top of the ShopBack instalment widget that
   the apps inject into this same info block. */
.vv-card-cta .btn--quick {
  /* `relative`, not `static`. Both keep it in flow, but the loader inside is
     position: absolute and needs this as its containing block. With `static` the
     spinner escaped to the nearest positioned ancestor and rendered as a small
     circle over the top-right of the product image -- while a DOM check happily
     reported it visible at 20x20, because it was: just somewhere else. */
  position: relative;

  /* ...which then ACTIVATED offsets that had been inert. The theme sets
     left: -15px; top: -15px to inset the icon into the media box corner, and
     those do nothing while the element is static. Under `relative` they shifted
     the pill 15px left and up, pushing its rounded left end under
     .collection__products (overflow: hidden) so it rendered square and clipped
     against the filter sidebar.

     Switching an element to relative inherits every offset the theme set for
     its absolute life. Clear them. */
  inset: auto;
}

/* The label is hidden TWO different ways depending on breakpoint: opacity 0 on
   desktop, display none under the mobile query. Setting only opacity fixed
   desktop and left mobile as blank teal blocks -- which the DOM check could not
   see, because textContent was correct the whole time. */
.vv-card-cta .btn__text {
  display: block;
  opacity: 1;
  overflow: visible;
  font-size: inherit;
}

/* Do NOT hide the loader. It looked like leftover icon-era furniture, but it is
   the button's only feedback: on click the theme adds .is-loading, which fades
   .btn__text to opacity 0 and reveals this spinner. Hiding it meant the label
   vanished and nothing took its place -- an empty teal pill for the second or so
   the request took, which reads as a dead button.

   Sized for the old icon circle, so bring it back in proportion to the pill. */
.vv-card-cta .btn__loader {
  display: block;
  width: 1.25rem;
  height: 1.25rem;

  /* The theme pins it to a corner of the icon circle. In a full-width pill that
     put it hard right, so centre it over where the label was. */
  position: absolute;
  inset: 0;
  margin: auto;
}

.vv-card-cta .svg-loader {
  width: 1.25rem;
  height: 1.25rem;
}

.vv-card-cta .btn--quick {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 2.75rem;      /* 44px — comfortable tap target */
  padding: var(--vv-space-2) var(--vv-space-3);
  background-color: var(--vv-teal);
  color: var(--vv-ink-on-dark);
  border: 0;

  /* A full pill, not the 4px --vv-radius-sm the other buttons use. The deck
     draws this one with a radius of about half its height, and --vv-radius-pill
     was defined in #2 for exactly this ("badges, swatches, CTA pills"). The
     site's other CTAs -- hero SHOP ALL, PDP ADD TO BAG -- stay square-ish, so
     this is deliberately a card-CTA exception rather than a change to .btn. */
  border-radius: var(--vv-radius-pill);
  font-size: var(--vv-text-sm);
  letter-spacing: var(--vv-tracking-caps);
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--vv-duration) var(--vv-ease);
}

.vv-card-cta .btn--quick:hover,
.vv-card-cta .btn--quick:focus-visible {
  background-color: var(--vv-green);
}

/* The icon-era rules sized .btn__inner for a square; it is a text row now. */
.vv-card-cta .btn__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--vv-space-2);
  width: auto;
  height: auto;
  background: transparent;   /* was the white icon circle */
  box-shadow: none;          /* ...which also carried its drop shadow, visible
                                as a faint lighter box behind the label */
  border-radius: 0;
  color: inherit;
}

/* Sold out. PDP-DESIGN-REQUIREMENTS decided the card case: the button stays,
   disabled, labelled "Sold out" -- same fill at half strength rather than a
   separate grey token.

   Two things were wrong here. The theme sets display:none on .quick__form for
   sold-out products (it does not want to offer quick-buy on one), so the pill
   never rendered at all -- the card simply had no CTA, which no screenshot of
   an in-stock product could reveal. And the modifier below was written as
   .is-sold-out; the class Palo Alto actually puts on the card is `soldout`. */
.product-grid-item.soldout .vv-card-cta .quick__form {
  display: block;
}

.vv-card-cta .btn--quick[disabled],
.product-grid-item.soldout .vv-card-cta .btn--quick {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

.vv-card-cta .quick__form {
  margin: 0;
}


/* --- Card type hierarchy -------------------------------------------------
   Caught in review: the price rendered noticeably larger and heavier than the
   product title, which inverts the deck's hierarchy. Measured on the rendered
   card before fixing:

     .product-grid-item__title   12.96px / 500 / #333
     .product-grid-item__price   16px    / 600 / #0b0b0b

   The deck reads the other way round -- the product name is the prominent line,
   the price is bold but no larger, and the descriptor beneath is the small
   muted one. So the title matches the price in size and weight and takes the
   full-strength ink; the price keeps its weight but stops out-shouting the name.

   Deck fidelity on the exact ratio is unverifiable from a raster deck, so this
   is "title not smaller than price", which is what the mock-up plainly shows,
   rather than a measured ratio. */
.product-grid-item__title {
  font-size: var(--vv-text-base);
  font-weight: 600;
  line-height: var(--vv-leading-tight);
  color: var(--vv-ink);
}

.product-grid-item__price,
.product-grid-item .price {
  font-size: var(--vv-text-base);
  line-height: var(--vv-leading-tight);
}

/* The descriptor line. Palo Alto already renders product.metafields.theme.cutline
   here -- the deck's "A sheer, buildable flush" -- it just has no values yet, so
   this is styling ahead of the data rather than new markup. */
.product-grid-item .product-cutline {
  font-size: var(--vv-text-sm);
  line-height: var(--vv-leading-body);
  color: var(--vv-ink-muted);
  margin-top: var(--vv-space-1);
}


/* ==========================================================================
   EDITORIAL TILES + TRUST ROW  (issues #11, #12)
   --------------------------------------------------------------------------
   Both turned out to be theme-native, so this is configuration plus styling
   and there is no new section in either:

     #11  sections/collection.liquid already ships a `promo` BLOCK with
          `position` (which grid slot) and `width` (full/half/auto). The
          template even had one already, disabled, carrying placeholder copy.
          Enabled it, configured a second, and styled them here.

     #12  sections/section-icons-row.liquid already exists, with 56 built-in
          icons -- truck, leaf, heart and lock are all among them, which is
          exactly the deck's four. Already used on 11 product templates.

   Both are in templates/collection.json, so they ship via the content sync,
   not push-staging.sh.
   ========================================================================== */

/* --- Editorial tiles ------------------------------------------------------ */

.collection-promo__inner {
  padding: var(--vv-space-6);
}

.collection-promo .collection-promo__title,
.collection-promo h2 {
  font-family: var(--vv-font-heading);
  font-size: var(--vv-text-lg);
  line-height: var(--vv-leading-tight);
  color: var(--vv-ink);
}

.collection-promo__text {
  font-size: var(--vv-text-sm);
  line-height: var(--vv-leading-body);
  color: var(--vv-ink-muted);
}

/* The deck's tile CTA is a small underlined caps link with an arrow, not a
   filled button. btn--text is the theme's closest variant; this is the
   typographic part. */
.collection-promo .btn--text {
  font-size: var(--vv-text-xs);
  letter-spacing: var(--vv-tracking-caps);
  text-transform: uppercase;
}

/* --- Trust row ------------------------------------------------------------ */

/* The theme stacks icon above text. The deck sets them side by side, which is
   what makes four fit on one mobile row rather than four tall stacked blocks. */
.icons-row__item {
  display: flex;
  align-items: center;
  gap: var(--vv-space-3);
  text-align: left;
}

.icons-row__icon {
  flex: 0 0 auto;
  margin: 0;
}

/* Titles inherit the serif heading face from #3, which is right for headings
   and too loud for a four-up utility row. Back to the body face, small caps-ish
   weight, matching the deck. */
.icons-row__item .icons-row__title,
.icons-row__item h3 {
  font-family: var(--vv-font-body);
  font-size: var(--vv-text-sm);
  font-weight: 600;
  line-height: var(--vv-leading-tight);
  color: var(--vv-ink);
  margin: 0;
}

.icons-row__item .rte,
.icons-row__item .icons-row__text {
  font-size: var(--vv-text-xs);
  line-height: var(--vv-leading-tight);
  color: var(--vv-ink-muted);
  margin: var(--vv-space-1) 0 0;
}

.icons-row__item .rte p {
  margin: 0;
}

/* Mobile: the deck keeps all four on one row at phone width, two per line. */
@media (max-width: 767px) {
  .icons-row__slider.grid--mobile {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--vv-space-4) var(--vv-space-3);
  }

  .icons-row__item .icons-row__title,
  .icons-row__item h3 {
    font-size: var(--vv-text-xs);
  }
}


/* ==========================================================================
   HOMEPAGE HERO + EVERYDAY ESSENTIALS BANNER  (issues #18, #21)
   --------------------------------------------------------------------------
   Both are sections/section-banner-image.liquid, which already carries every
   acceptance criterion #18 asked for: `image` plus `image_mobile` (the separate
   mobile crop), and a heading block whose richtext <em> drives an accent via
   `heading_highlight_type`. The live hero was already using that exact pattern
   ("Pigment with <em>purpose</em>"), so the mechanism is proven in production.

   Two traps in the template, neither visible from the Liquid:
     - sections.section_banner_image_RwHdqe is disabled. The LIVE hero is
       MmLVM6, the second banner. Configuring the first changes nothing.
     - inside MmLVM6 the HEADING BLOCK was itself disabled, so copy went in
       and still did not render.

   --------------------------------------------------------------------------
   Design decisions adopted from the Tatagaltier build spec (docs/build-spec.md
   §2 and §10 in laskarstudio/tatagaltier-shopify), which the user confirmed
   shares a spec for the header, hero and bottom banner.

   Tatagaltier is DAWN; we are Palo Alto, so none of its CSS ports directly --
   these are the decisions, re-expressed against our own tokens and selectors.

   Already matching, no change needed:
     type ramp 44/32/24/18/16  ==  --vv-text-2xl..base   (set in #2)
     body line-height 1.6      ==  --vv-leading-body
     heading line-height 1.15  ==  --vv-leading-tight
     44px minimum tap target   ==  used on the card CTA in #37
   ========================================================================== */

:root {
  /* Adopted from the Tatagaltier spec §2. We had no equivalent for any of
     these; the ramp and line-heights already agreed. */
  --vv-tracking-display: -0.01em;  /* "display at 600-700 with tight tracking" */
  --vv-tap-min: 2.75rem;           /* 44px, their --tap-min, now a token */
  --vv-focus: #0a5fce;             /* §10: a visible ring that is NOT the brand
                                      accent -- rose on cream fails AA the same
                                      way their yellow on white does */
  --vv-focus-width: 2px;
}

/* --- Accessibility floor (Tatagaltier spec §10) ---------------------------
   "Visible focus on every control." We had none at all: the restyle set
   border-radius and colours on .btn without ever restoring a focus ring, so
   keyboard users got whatever Palo Alto's default was, or nothing. This is the
   cheapest real accessibility win available and it is not hero-specific. */
:where(a, button, .btn, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--vv-focus-width) solid var(--vv-focus);
  outline-offset: 2px;
}

/* --- Banner headings ------------------------------------------------------ */

/* The deck's hero and banner both read: roman first phrase, italic accent in
   the brand rose. The theme emits a bare <em> -- its `italics` highlight type
   is labelled "Italics (no decoration)" and deliberately applies no colour, so
   the accent colour is ours to supply. Same treatment .vv-accent-italic defined
   in #3, applied to the markup the theme actually produces. */
/* ⚠️ CONTRAST GUARDRAIL -- the Velvet Vanity equivalent of the Tatagaltier
   spec's "--accent is a FILL colour only, never yellow text on white".

   Measured on the painted hero: --vv-rose #a27776 on the backdrop #ececec is
   3.27:1. WCAG AA large text needs 3.0, so this PASSES -- but only because the
   hero display is large. AA for body text needs 4.5:1, which the same rose
   fails on any light ground (3.24:1 even on our own cream --vv-bg).

   So: --vv-rose is a DISPLAY-SIZE accent. Never use it for body copy, labels or
   small links on a light background. Where a rose-toned body text is wanted,
   darken the token rather than reusing this one. */
.banner-image__heading em,
.slide__heading em {
  font-style: italic;
  color: var(--vv-rose);
}

/* Display weight and tracking, per the spec. Palo Alto renders the banner
   heading at weight 400 with normal tracking; measured on the hero before this
   rule. */
.banner-image__heading,
.slide__heading {
  font-weight: 600;
  letter-spacing: var(--vv-tracking-display);
  line-height: var(--vv-leading-tight);
}

/* Their rule, worth keeping: both CTAs above the fold on mobile. Our banners
   carry one button, so this is simply making sure it stays a real tap target. */
.banner-image .btn,
.slideshow .btn,
.promotion-row .btn {
  min-height: var(--vv-tap-min);
}

/* --- Promo bar (#19) ------------------------------------------------------

   ⚠️ EVERY RULE BELOW IS SCOPED TO THIS SECTION'S OWN ID, not to
   .promotion-row. #23 puts a SECOND section-promotion-row on the homepage --
   "In the Spotlight" -- on a light ground. Unscoped, the dark-ground rules
   below made its heading #faf9f7 on #ffffff and its button white on white:
   1.03:1, completely invisible. Caught by measuring both instances rather than
   just the one I was working on.

   The "Take 3, Pay 2" strip sits directly under the hero. Together they fill
   the first viewport, which is the Tatagaltier arrangement -- their hero
   section carries the marquee inside it. Palo Alto has no equivalent, so this
   is two sections stacked, with the hero at three-quarters height so the bar
   still lands above the fold.

   section-promotion-row's smallest height option is 150px, taller than the
   deck's slim strip. Tightened here rather than by forcing an out-of-range
   value into the schema. */
/* No image on this block, so the section falls back to its grey placeholder SVG
   and paints it OVER the green. The deck's bar is a flat colour strip, so the
   placeholder is suppressed -- but only when there is genuinely no image, so a
   client who adds one still gets it. */
[data-section-id$="__vv_promo_bar"] .placeholder-svg--overlayed,
[data-section-id$="__vv_spotlight"] .placeholder-svg--overlayed {
  display: none;
}

/* --- Height ---------------------------------------------------------------
   section-promotion-row's smallest height option is 150px and the deck's bar is
   a slim strip -- roughly enough to clear the text and no more. 150px made it
   read as a section rather than a bar, so size it from its padding instead.
   The schema value stays 150 because it is the minimum the section allows; this
   overrides what that resolves to. */
[data-section-id$="__vv_promo_bar"] .promotion-row__item {
  min-height: 0;
  height: auto;
  /* No padding here: .promotion-row__item__content already carries 20px top and
     bottom. Adding more stacked two paddings and put the bar back at 103px --
     the measurement that caught it. The content's own padding sizes the strip. */
  padding-block: 0;
}

[data-section-id$="__vv_promo_bar"].section-padding {
  padding-top: 0;
  padding-bottom: 0;
}

/* --- Layout ---------------------------------------------------------------
   The deck centres the promo text as a GROUP across the full bar, with the CTA
   pinned to the right edge rather than sitting in that group. So the content is
   a centred flex row and the button is taken out of it.

   Same treatment as the announcement bar in #4: thin rules between items, drawn
   on the element rather than typed into the copy so the client cannot leave an
   orphaned "|" behind. */
@media (min-width: 768px) {
  [data-section-id$="__vv_promo_bar"] .promotion-row__item__content {
    position: relative;
    display: flex;
    flex-direction: row;
    /* baseline, not centre: the serif heading and the sans description are
       different sizes, so centring their BOXES leaves the two texts visibly
       off each other. The deck sits them on one baseline. */
    align-items: baseline;
    justify-content: center;
    gap: 0 var(--vv-bar-separator-gap);
    text-align: left;
  }

  [data-section-id$="__vv_promo_bar"] .promotion-row__item__description::before {
    content: "";
    display: inline-block;
    width: 1px;
    height: 1em;
    margin-inline-end: var(--vv-bar-separator-gap);
    background-color: currentColor;
    opacity: 0.45;
  }

  /* Out of the centred group, pinned right. */
  [data-section-id$="__vv_promo_bar"] .promotion-row__item__button {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    margin: 0;
  }
}

/* Mobile: no room for a centred row plus a pinned button, so stack it. */
@media (max-width: 767px) {
  [data-section-id$="__vv_promo_bar"] .promotion-row__item__content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--vv-space-2);
    text-align: center;
  }
}

/* --- Type and colour ------------------------------------------------------
   The description inherits the light text the section sets for a dark ground,
   but the HEADING keeps its own #333 and goes near-invisible on the green.
   Measured: rgb(51,51,51) on rgb(40,76,74) is 1.34:1. Light ink is 8.98:1. */
[data-section-id$="__vv_promo_bar"] .promotion-row__item__heading {
  font-family: var(--vv-font-heading);
  letter-spacing: var(--vv-tracking-display);
  color: var(--vv-ink-on-dark);
  margin: 0;
  /* The heading's own line-height was padding the bar taller than the deck's
     strip; the bar should be sized by its padding, not by leading. */
  line-height: 1;
}

[data-section-id$="__vv_promo_bar"] .promotion-row__item__description {
  margin: 0;
  font-size: var(--vv-text-sm);
}

/* The deck's CTA is a white button with dark text -- the inverse of the hero's.
   The section's own btn--black would disappear into the green. */
[data-section-id$="__vv_promo_bar"] .promotion-row__item__button .btn {
  background-color: var(--vv-ink-on-dark);
  border-color: var(--vv-ink-on-dark);
  color: var(--vv-green-bar);
}

[data-section-id$="__vv_promo_bar"] .promotion-row__item__button .btn:hover,
[data-section-id$="__vv_promo_bar"] .promotion-row__item__button .btn:focus-visible {
  background-color: #ffffff;
  border-color: #ffffff;
  color: var(--vv-green-bar);
}

/* Dark green ground: the deck's bar is --vv-green-bar with light text. */
.promotion-row__item {
  color: var(--vv-ink-on-dark);
}


/* ==========================================================================
   HOMEPAGE RAILS  (issue #20)
   ========================================================================== */

/* --- The rail card's CTA is a DIFFERENT button -----------------------------
   The deck draws two card CTAs and the difference is deliberate:

     shop grid   filled teal pill, "BUY • RM69"
     home rails  outlined button, bag icon, "ADD TO BAG"

   Same component; snippets/product-grid-item.liquid takes a cta_variant and
   section-collection passes 'bag'. The shop treatment stays the default, so
   search and related-products are untouched.

   The bag icon comes back for this variant -- #37 stripped it because the shop
   pill is text-only, and the rail card is where it belongs. */
.vv-card-cta--bag .btn--quick {
  background-color: transparent;
  border: 1px solid var(--vv-line-strong);
  border-radius: var(--vv-radius-sm);
  color: var(--vv-ink);
  gap: var(--vv-space-3);
}

.vv-card-cta--bag .btn--quick:hover,
.vv-card-cta--bag .btn--quick:focus-visible {
  background-color: var(--vv-ink);
  border-color: var(--vv-ink);
  color: var(--vv-ink-on-dark);
}

/* The icon inherits the label's colour so it flips on hover with the text. */
.vv-card-cta--bag .btn--quick .icon,
.vv-card-cta--bag .btn--quick svg {
  width: 1.125rem;
  height: 1.125rem;
  fill: none;
  stroke: currentColor;
  color: inherit;
}

/* --- Section heading + subheading on one line -----------------------------
   The deck sets the rail's supporting line BESIDE the heading, not under it:
   "New In   Fresh drops, made for your everyday."

   section-collection already renders .section__heading and .section__text as
   separate elements, so this is layout only -- the copy is a `text` setting the
   client can edit. */
@media (min-width: 768px) {
  /* The shared parent is .section__header, NOT .featured-collection__top --
     that outer element also holds the View All button, so flexing it would have
     pulled the button up beside the heading too.

     Generalised to any .section__header carrying a subheading, so the rails
     (#20) and Loved by You (#22) share one rule rather than two copies. */
  .section__header:has(.section__text) {
    display: flex;
    align-items: baseline;
    gap: var(--vv-space-4);
    flex-wrap: wrap;
  }

  .section__header:has(.section__text) .section__heading {
    margin-bottom: 0;
  }

  .section__header:has(.section__text) .section__text {
    margin: 0;
  }

  /* The reviews link sits at the far right of the header row, like View All. */
  .section__header .section__header-link {
    margin-left: auto;
  }
}

.section__header .section__text {
  font-size: var(--vv-text-sm);
  color: var(--vv-ink-muted);
}

.section__header .section__text p {
  margin: 0;
}


/* ==========================================================================
   IN THE SPOTLIGHT  (issue #23)
   --------------------------------------------------------------------------
   A SECOND section-promotion-row on the homepage, on a light ground. Every rule
   in the #19 promo-bar block above is scoped to that section's own id precisely
   because of this one -- unscoped, they painted this section's heading and
   button white on white.

   The section still sets its own text light for a dark ground, though, so the
   description came through at #faf9f7 on #ffffff: 1.05:1, invisible. The
   heading and button were caught by measuring; this was one level further down
   and needed the same check again.
   ========================================================================== */

[data-section-id$="__vv_spotlight"] .promotion-row__item__heading {
  font-family: var(--vv-font-heading);
  color: var(--vv-ink);
}

[data-section-id$="__vv_spotlight"] .promotion-row__item__description {
  color: var(--vv-ink-muted);   /* 5.7:1 on white */
  font-size: var(--vv-text-sm);
}


/* ==========================================================================
   LOVED BY YOU  (issue #22)
   --------------------------------------------------------------------------
   Reworked after review: the first pass matched the layout but not the design.
   Four things were wrong, and none were spacing.
   ========================================================================== */

:root {
  /* Sampled off the deck at both the aggregate line and the card stars -- the
     same value in both places. Palo Alto renders stars in the body ink, so they
     came out black. */
  --vv-star: #d44243;
}

.rating__star__full,
.rating__star__full .icon-star {
  color: var(--vv-star);
  fill: var(--vv-star);
}

.rating__star__empty,
.rating__star__empty .icon-star {
  color: var(--vv-line);
  fill: var(--vv-line);
}

/* --- Aggregate line ------------------------------------------------------
   Stars sit between the heading and the figure, and are driven by the rating
   value rather than hard-coded -- 4.9 fills 98% of five stars. The fill is a
   `--percent` clip the theme already used for the per-card stars, so fractional
   ratings worked without new logic. */
.testimonials__aggregate {
  display: flex;
  align-items: center;
  gap: var(--vv-space-3);
}

.testimonials__aggregate .icon-star {
  width: 1rem;
  height: 1rem;
}

.testimonials__aggregate__text {
  font-size: var(--vv-text-sm);
  color: var(--vv-ink);
}

/* --- The card ------------------------------------------------------------
   The deck's card is a two-panel row: photo left, content right, on a white
   ground with a soft shadow and a small radius. Palo Alto's was a single text
   column with a flat 1px border and the image as an avatar by the name.

   Shadow value follows the Tatagaltier spec's --shadow-card (§2), the same
   "shadow only where it carries meaning" restraint adopted for the hero in
   #18/#21. */
.testimonials-section .testimonial__item__inner {
  display: flex;
  /* The theme sets flex-direction: column here. Declaring `display: flex`
     without a direction left that column in place -- the rule applied and the
     card still stacked, with the photo at 80% width. */
  flex-direction: row;
  align-items: stretch;
  gap: 0;
  overflow: hidden;
  background: var(--vv-surface);
  border: 1px solid var(--vv-line);
  border-radius: var(--vv-radius-md);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.06);
  height: 100%;

  /* The theme pads this wrapper, and a stretched flex child fills the CONTENT
     box, not the border box -- so the photo stopped 82px short of the card and
     sat inset rather than edge to edge. The padding moves onto the content
     column below, which is the only part that wants it. */
  padding: 0;
}

.testimonial__item__media {
  flex: 0 0 38%;
  align-self: stretch;
  position: relative;
  overflow: hidden;
}

/* The image sizes itself from its own aspect ratio, so `height: 100%` against a
   parent with no definite height left it 221px tall in a 303px card -- the photo
   floating with white above and below instead of filling the panel edge to edge.
   Absolute inside the relative panel takes the image out of that negotiation.
   Same ratio-box trap as the mega menu and drawer thumbnails in #6/#7. */
.testimonial__item__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.testimonials-section .testimonial__item__content {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: var(--vv-space-2);
  padding: var(--vv-space-5);
  text-align: left;
}

/* Quote first, then stars, then the name -- the deck's order. The markup is
   already in this order after the snippet rework, so nothing is reordered here;
   this only sets the type. */
.testimonials-section .testimonial__text {
  font-family: var(--vv-font-heading);
  font-size: var(--vv-text-md);
  line-height: var(--vv-leading-tight);
  color: var(--vv-ink);
}

.testimonials-section .testimonial__stars .icon-star {
  width: 0.9rem;
  height: 0.9rem;
}

.testimonials-section .testimonial__user__name {
  font-size: var(--vv-text-sm);
  color: var(--vv-ink-muted);
}

/* Mobile: the photo panel would leave too little room for the quote, so it
   moves above the text. */
@media (max-width: 767px) {
  .testimonials-section .testimonial__item__inner {
    flex-direction: column;
  }

  .testimonial__item__media {
    flex: 0 0 auto;
    aspect-ratio: 16 / 9;
  }
}


/* ==========================================================================
   PDP SHADE SWATCHES + BUY BOX  (issues #13, #15)
   --------------------------------------------------------------------------
   Far less was needed here than the issues assumed, because removing Orichi in
   #26 unblocked Palo Alto's own behaviour. Measured on soft-blur-lip-liner,
   which has two genuinely sold-out shades (Suede, Brick):

     sold-out shades get a .sold-out class            already correct
     the diagonal slash is drawn and revealed on it   already correct
     every swatch stays SELECTABLE, pointer-events    already correct -- and the
       auto, input not disabled                       exact opposite of Orichi
     selecting a sold-out shade flips the CTA         already correct

   #13's hardest acceptance criterion -- sold out but still selectable, flipping
   the CTA -- was therefore already met. Two gaps remained.

   On the slash width: #13 quotes Dawn's `swatch size × 1.414`, the diagonal of a
   SQUARE. These swatches are circles, where the diagonal IS the diameter, so the
   theme's 32px is already edge to edge. No change.
   ========================================================================== */

/* GAP 1 -- sold-out shades were slashed but at full opacity, so they did not
   read as unavailable at a glance. Dawn's reference is 0.4. */
.swatch__button.sold-out .swatch__label {
  opacity: 0.4;
}

/* The slash itself must stay legible against the dimmed swatch. */
.swatch__button.sold-out .swatch__label::after {
  opacity: 1;
}

/* Never disable the INPUT -- that is what made Orichi's version unusable.
   Dimming is visual only; the shade stays selectable so the CTA can respond. */
.swatch__button.sold-out .swatch__input {
  pointer-events: auto;
}

/* GAP 2 -- the sold-out CTA.
   PDP-DESIGN-REQUIREMENTS decided: same brand fill at half strength, not a
   separate grey token. The theme washes it to rgba(51,51,51,0.1), which reads as
   a different component rather than the same button unavailable. */
.product__submit__add[disabled],
.product__submit__add[aria-disabled="true"] {
  background-color: var(--vv-green);
  border-color: var(--vv-green);
  color: var(--vv-ink-on-dark);
  opacity: 0.5;
  cursor: not-allowed;
}

.product__submit__add[disabled]:hover {
  background-color: var(--vv-green);
}


/* ==========================================================================
   RAIL CARD: STOP THE CARD BECOMING A SCROLL CONTAINER
   --------------------------------------------------------------------------
   Reported in review on the Bestsellers rail: the card could be scrolled with
   the wheel, the ADD TO BAG button flickered on hover, and it painted half
   filled. Three symptoms, one cause.

   Measured after a real hover, which is when the carousel initialises:

     inline style   position: absolute; transform: translateX(106.43%)
     card height    498 -> 471        (the carousel sets it)
     scrollHeight   498               (what the content actually needs)
     overflow       auto              (the theme's)
     result         a 27px shortfall inside a scroll container

   `overflow: auto` turns that shortfall into a scrollable box, which clips the
   CTA at the card's bottom edge. Hovering the clipped part of the button then
   nudges the scroll position, moving the button out from under the cursor,
   which un-hovers it, which moves it back -- the flicker. The half-filled paint
   is that loop caught mid-transition.

   NOT caused by the flex column added in #37. Tested by forcing the card back
   to `display: block`: the 27px shortfall is identical, so the carousel's
   height is simply wrong and my layout only made it visible.

   Scoped to product cards inside a carousel. The collection grid is unaffected,
   and other carousels keep their own behaviour. */
.product-grid-item.carousel__item,
.flickity-slider .product-grid-item.carousel__item,
.flickity-enabled .product-grid-item.carousel__item {
  overflow: visible;
}

/* `!important` deliberately, and this is the one place in this stylesheet that
   uses it. Flickity writes a measured per-cell height (471.047px on the card
   that broke) which beats a three-class selector, so scoping cannot win here.
   Both theme.css and this file are served from the CDN on a dev theme, so
   cssRules is CORS-blocked and the competing rule cannot be read to confirm its
   specificity -- but a value that precise is the carousel's own measurement.

   Without this the card box stays 471px while its children total 483, and the
   CTA hangs 27px below the card into the section underneath. */
.product-grid-item.carousel__item,
.flickity-slider .product-grid-item.carousel__item,
.flickity-enabled .product-grid-item.carousel__item {
  height: auto !important;
}

/* ==========================================================================
   #14 — PDP gallery
   --------------------------------------------------------------------------
   The deck's gallery IS Palo Alto's "Carousel with thumbnails" layout, which
   the theme already ships: square media slider, thumbnail strip, PhotoSwipe
   zoom, video support, and thumbnails that auto-hide below 2 media. The
   template was simply set to "grid", which stacked all 14 images of a product
   down the page. Switching `image_layout` to `carousel` does most of this
   issue's work and is a SETTING, not code.

   What the theme does not provide, and this block adds:
     1. visible prev/next arrows (it ships them, then hides them)
     2. a "1 / 7" media counter
     3. a zoom affordance icon

   One-image products need none of these. The theme already drops the
   thumbnails at `product.media.size > 1`; the counter is gated on the same
   condition in Liquid, and the arrows are gated on the theme's own
   `--single` modifier below.
   ========================================================================== */

/* -- 1. Prev/next arrows --------------------------------------------------
   theme.css carries `.product-single__media-slider .flickity-prev-next-button
   { display: none }` plus a hover-reveal path that leaves them at opacity 0 /
   visibility hidden / scale(0). The deck shows them persistently, so undo all
   four properties rather than only `display` — setting one and checking the
   rule "applied" is exactly how three earlier changes passed a programmatic
   check and were still invisible on screen.

   Scoped to the carousel layout and away from quick-view, which has its own
   arrow positioning. */
.product-single__wrapper--carousel .product-single__media-slider:not(.product-single__media-slider--single) .flickity-prev-next-button,
.product-single__wrapper--carousel .product-single__media-slider:not(.product-single__media-slider--single) .flickity-prev-next-button:not(:disabled) {
  display: block;
  opacity: 1;
  visibility: visible;
  transform: translateY(-50%) scale(1);
  width: 2.75rem;
  height: 2.75rem;
  background: #fff;
  border-radius: 50%;
  box-shadow: 0 1px 4px rgba(11, 11, 11, 0.12);
  transition: opacity 0.2s ease, box-shadow 0.2s ease;
}

/* Flickity's own transform is translateY(-50%); re-asserting it above means the
   button stays vertically centred on the image rather than dropping to the top. */

.product-single__wrapper--carousel .product-single__media-slider .flickity-prev-next-button:hover {
  box-shadow: 0 2px 8px rgba(11, 11, 11, 0.2);
}

/* No :disabled styling here on purpose. The theme runs this slider with
   wrapAround, verified in the browser: at 3/3 the next arrow returns to 1/3 and
   the previous arrow goes 1/3 -> 3/3. Flickity therefore never sets `disabled`,
   so a rule for it would be dead code. */

/* A single-media product gets no arrows at all — belt and braces alongside the
   Liquid gate, because Flickity still builds the buttons for a one-cell slider. */
.product-single__media-slider--single .flickity-prev-next-button {
  display: none;
}

/* -- 2. Zoom affordance ---------------------------------------------------
   `snippets/media.liquid` already lays an <a data-zoom-wrapper> over the whole
   image, so the image is clickable to open PhotoSwipe. The deck's magnifier is
   therefore an INDICATOR, not a second control: drawing it as a pseudo-element
   with pointer-events:none means the click still lands on the existing link.
   No duplicate interactive element, nothing extra for screen readers. */
.product-single__wrapper--carousel .product-single__media-link {
  position: absolute;
  inset: 0;
}

.product-single__wrapper--carousel .product-single__media-link::after {
  content: "";
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  background-color: #fff;
  background-repeat: no-repeat;
  background-position: center;
  background-size: 1.1rem 1.1rem;
  /* magnifier with a "+", matching the deck's icon */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230b0b0b' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M15.5 15.5 21 21M10.5 7.8v5.4M7.8 10.5h5.4'/%3E%3C/svg%3E");
  box-shadow: 0 1px 4px rgba(11, 11, 11, 0.12);
  pointer-events: none;
}

/* -- 3. Media counter -----------------------------------------------------
   Sits level with the thumbnail row at its right edge, as the deck shows.

   The thumbnail strip is a Flickity container spanning the full gallery width,
   so with 7+ thumbs the row would run underneath an absolutely-positioned
   counter. Reserving the space with padding-right on the strip keeps them from
   colliding at any media count (products here range from 2 to 14). */
/* The counter belongs to the carousel. theme.css hides the thumbnail strip
   outright in the grid and stacked layouts:

     .product-single__wrapper--stacked .product-single__thumbnails,
     .product-single__wrapper--grid .product-single__thumbnails { display: none }

   The counter markup is rendered unconditionally (the gallery is captured once
   and reused by quick view, where image_layout is not set), so without this it
   would survive as a stray "1 / 14" on the two bespoke landing-page templates,
   which are both on `grid`. Mirror the theme's own rule rather than gating in
   Liquid, so the two can never drift apart. */
.product-single__wrapper--grid .vv-gallery-counter,
.product-single__wrapper--stacked .vv-gallery-counter {
  display: none;
}

/* Quick view reuses the same captured gallery but renders it OUTSIDE
   .product-single__wrapper, so neither rule above reaches it. It hides the
   thumbnail strip while keeping a media slider, which left the counter painted
   on its own in the modal — verified by opening quick view from the collection
   page, where it read a stray "1 / 4". The counter is anchored to the thumbnail
   row by design, so it goes wherever that row does. */
.popup-quick-view .vv-gallery-counter {
  display: none;
}

.vv-gallery-thumbrow {
  display: flex;
  flex-direction: row;        /* the theme sets column on nearby wrappers — be explicit */
  align-items: center;
  gap: 1rem;
}

/* min-width:0 lets the Flickity strip shrink inside the flex row instead of
   forcing the counter off the end; flex:1 gives it the remaining width. */
.vv-gallery-thumbrow .product-single__thumbnails {
  flex: 1 1 auto;
  min-width: 0;
}

.vv-gallery-counter {
  flex: 0 0 auto;
  margin-left: auto;          /* pins right even when the strip is narrow */
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
  color: #6b6b6b;             /* measured 5.33:1 on the painted white ground — AA */
  white-space: nowrap;
}

/* Mobile: the theme bleeds the thumbnail strip past the page gutters with
   `margin: 0 -20px` so it scrolls edge to edge. That negative margin pushes the
   strip under anything sharing its flex line, which clipped the counter at the
   viewport edge. Rather than unpick the full-bleed (it is the right mobile
   behaviour), let the counter wrap onto its own line, still right-aligned. */
@media (max-width: 767px) {
  .vv-gallery-thumbrow {
    flex-wrap: wrap;
    gap: 0;
  }

  .vv-gallery-thumbrow .product-single__thumbnails {
    flex: 1 0 100%;
  }

  /* flex-basis:100% on the counter, not just on the strip: the strip's -20px
     side margins shrink its OUTER size by 40px, which left room for a 23px
     counter to sit beside it on the same line and overlap the thumbnails.
     Giving the counter a full-width basis of its own forces the break. */
  .vv-gallery-counter {
    flex: 0 0 100%;
    width: 100%;
    margin-top: 0.5rem;
    margin-left: 0;
    text-align: right;
  }
}

@media (prefers-reduced-motion: reduce) {
  .product-single__wrapper--carousel .product-single__media-slider .flickity-prev-next-button {
    transition: none;
  }
}

/* ==========================================================================
   #16 — "Complete Your Routine" cross-sell
   --------------------------------------------------------------------------
   Palo Alto's upsell block is a two-column card (content left, image right) on
   a Flickity slider, with a per-row "Buy It With" label. The deck is a vertical
   list under one heading: square thumbnail, name + descriptor, price, "Add +".

   Why the rewrite rather than a restyle: 7 of the 8 distinct cross-sell target
   products are multi-variant shade products, and the theme only renders a real
   add-to-cart form for single-variant ones — everything else fell back to a
   Quick Buy link that opened a modal. See snippets/upsell-product.liquid.
   ========================================================================== */

.vv-upsell__title {
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: var(--vv-tracking-display);
  margin: 0 0 0.75rem;
}

/* Optional description under the heading (theme-editor setting). The title's
   own bottom margin is the gap when there is no description, so this collapses
   it rather than adding to it — otherwise turning the description on would
   double the space under the heading. */
.vv-upsell__title:has(+ .vv-upsell__description) {
  margin-bottom: 0.25rem;
}

.vv-upsell__description {
  margin: 0 0 0.75rem;
  max-width: 52ch;
  font-size: 0.875rem;
  line-height: 1.5;
  color: #6b6b6b;          /* 5.33:1 on the painted ground — AA */
}

/* richtext emits its own <p>; strip the browser default margins so the spacing
   above comes from this block, not from whatever the paragraph brings. */
.vv-upsell__description > p {
  margin: 0;
}

.vv-upsell__description > p + p {
  margin-top: 0.5rem;
}

/* The theme paints the upsell block as a grey card. The deck has it flush with
   the buy column, separated by a rule above the heading. */
.vv-upsell.upsell__products {
  background: none;
  padding: 0;
  border-top: 1px solid rgba(11, 11, 11, 0.12);
  padding-top: 1.25rem;
}

.vv-upsell__list {
  display: flex;
  flex-direction: column;       /* rows, not the theme's slider */
}

/* ------------------------------------------------------------------- Row */

.vv-upsell-row {
  display: grid;
  grid-template-columns: 4.5rem 1fr auto;
  grid-template-areas:
    "media body  buy"
    "media shade shade";
  align-items: center;
  column-gap: 0.875rem;
  row-gap: 0.5rem;
  padding: 0.875rem 0;
  border-bottom: 1px solid rgba(11, 11, 11, 0.08);
  /* The theme paints .product-upsell__holder #f7f7f7 as a card. The deck's rows
     sit flush on the page, separated by hairlines. */
  background: none;
}

.vv-upsell-row__media { grid-area: media; align-self: start; }
.vv-upsell-row__body  { grid-area: body; }
.vv-upsell-row__shade { grid-area: shade; }
.vv-upsell-row__buy   { grid-area: buy; }

.vv-upsell-row:last-child {
  border-bottom: 0;
}

.vv-upsell-row__media {
  display: block;
  width: 4.5rem;
  margin-top: 0.125rem;
  aspect-ratio: 1;
  border: 1px solid rgba(11, 11, 11, 0.1);
  overflow: hidden;
  background: #fff;
}

.vv-upsell-row__thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.vv-upsell-row__body {
  min-width: 0;                 /* lets long product names wrap instead of
                                   stretching the grid column */
}

.vv-upsell-row__title {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.3;
}

.vv-upsell-row__title a {
  color: inherit;
  text-decoration: none;
}

.vv-upsell-row__title a:hover {
  text-decoration: underline;
}

/* theme.cutline has no values anywhere in the catalogue, so this renders
   nothing today and the row closes up. Styled now so it is correct the moment
   merchandising fills the field in. */
.vv-upsell-row__cutline {
  margin: 0.125rem 0 0;
  font-size: 0.8125rem;
  line-height: 1.35;
  color: #6b6b6b;               /* same token as the gallery counter — AA on white */
}

/* --------------------------------------------------------- Shade picker */

.vv-upsell-row__select {
  width: 100%;
  max-width: 14rem;
  min-height: var(--vv-tap-min);
  padding: 0.25rem 1.75rem 0.25rem 0.625rem;
  font-size: 0.8125rem;
  color: #0b0b0b;
  background-color: #fff;
  border: 1px solid rgba(11, 11, 11, 0.25);
  border-radius: 2px;
  -webkit-appearance: none;
  appearance: none;
  background-repeat: no-repeat;
  background-position: right 0.5rem center;
  background-size: 0.7rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%230b0b0b' stroke-width='1.5'%3E%3Cpath d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
}

/* ----------------------------------------------------------- Buy column */

.vv-upsell-row__buy {
  display: flex;
  flex-direction: row;          /* the theme sets column nearby — be explicit */
  align-items: center;
  gap: 0.75rem;
  justify-self: end;
}

.vv-upsell-row__price {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  white-space: nowrap;
}

/* The theme's upsell button is full-width with a price appended. The deck's is
   a compact outlined pill reading "Add +". */
/* The theme's .btn carries `min-width: 180px`, which on a 492px row left the
   product name only ~102px and wrapped every title onto three lines. Width alone
   does not beat a min-width — both have to be reset. */
.vv-upsell-row .vv-upsell-row__btn {
  min-height: var(--vv-tap-min);
  width: auto;
  min-width: 0;
  padding: 0 0.875rem;
  font-size: 0.8125rem;
  white-space: nowrap;
}

.vv-upsell-row__soldout {
  margin: 0;
  font-size: 0.8125rem;
  color: #6b6b6b;
}

/* The form wrapper would otherwise introduce its own block spacing between the
   price and the button. */
.vv-upsell-row__buy form {
  margin: 0;
}

/* --------------------------------------------------------------- Mobile */

@media (max-width: 767px) {
  /* The AREAS must be redefined, not just the columns. Keeping the desktop
     template ("media body buy") against a two-column mobile grid collapsed the
     buy area onto the body, printing the price on top of the product name.
     Setting grid-column alone does not undo a grid-template-areas placement. */
  .vv-upsell-row {
    grid-template-columns: 3.75rem 1fr;
    grid-template-areas:
      "media body"
      "media shade"
      "buy   buy";
    row-gap: 0.5rem;
  }

  .vv-upsell-row__media {
    width: 3.75rem;
  }

  .vv-upsell-row__buy {
    justify-self: stretch;
    width: 100%;
    justify-content: space-between;
  }

  .vv-upsell-row__select {
    max-width: none;
  }
}

/* ==========================================================================
   #17 — PDP editorial blocks
   --------------------------------------------------------------------------
   Spec table, full-width colour band, three-step how-to, FAQ. Content is per
   product (metafields); see sections/vv-product-editorial.liquid for why this is
   a new section rather than the four disabled ones on the template.
   ========================================================================== */

.vv-editorial {
  padding: var(--vv-space-6, 3.5rem) 0;
}

.vv-editorial > * > * + * {
  margin-top: 1.25rem;
}

.vv-editorial__heading {
  font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.25rem);
  line-height: 1.15;
  letter-spacing: var(--vv-tracking-display);
  margin: 0 0 1.25rem;
}

/* ------------------------------------------------------------- Spec table */

.vv-editorial__spec {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem;
  align-items: stretch;
}

.vv-editorial__spec__text {
  background: #efeeea;
  padding: clamp(1.5rem, 1rem + 2vw, 2.75rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.vv-editorial__specs {
  margin: 0;
}

/* Label left, value right, with a hairline under each row — the deck's shape.
   A <dl> with each pair wrapped in a <div> so the row can be a grid: dt and dd
   are siblings in the DOM, and without the wrapper they cannot share a row. */
.vv-editorial__spec-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1rem;
  align-items: baseline;
  padding: 0.875rem 0;
  border-top: 1px solid rgba(11, 11, 11, 0.14);
}

.vv-editorial__spec-row dt {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.vv-editorial__spec-row dd {
  margin: 0;
  text-align: right;
  font-size: 0.9375rem;
  color: #3d3d3d;
}

/* Ratio locked here rather than left to whatever merchandising uploads: the
   mock image is a square collage, which drove the row to 643px and left the text
   panel half empty. With a fixed ratio the block is the same shape on every
   product. (Same reasoning as the Tatagaltier spec: image ratio belongs to the
   section, not the CMS.) */
.vv-editorial__spec__media,
.vv-editorial__steps__media {
  overflow: hidden;
  aspect-ratio: 4 / 3;
}

.vv-editorial__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --------------------------------------------------------- Colour band */

.vv-editorial__band {
  position: relative;
  background: var(--vv-editorial-band, #b4625f);
  min-height: clamp(8rem, 6rem + 10vw, 13rem);
  display: flex;
  align-items: center;
  overflow: hidden;
}

.vv-editorial__band__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Scrim. The heading is white, and the image behind it is whatever merchandising
   uploads — the mock is a pale collage, over which white text measured 1.4:1 and
   was essentially unreadable. A text-shadow alone does not fix that. This lays
   the band colour back over the left of the image, fading out to the right, so
   the heading always has a dark ground while the photograph still reads. */
.vv-editorial__band::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to right,
    var(--vv-editorial-band, #b4625f) 0%,
    color-mix(in srgb, var(--vv-editorial-band, #b4625f) 85%, transparent) 35%,
    transparent 70%
  );
  pointer-events: none;
}

/* color-mix is unsupported in older Safari, where the middle stop is dropped and
   the gradient degrades to band-colour → transparent. Still legible, so no
   @supports gate is needed. */

/* The band colour is the fallback AND the ground the heading sits on where the
   image is dark. White on #b4625f measures 4.0:1 — short of AA for body text,
   which is why this is display-size only (clamped to 1.5rem minimum) and why the
   shadow below is not decorative: it holds the text legible over a photograph
   whose brightness we do not control. */
.vv-editorial__band__heading {
  position: relative;
  /* Above the ::after scrim. Both are positioned, so without this the scrim —
     painted later — lies over the text and washes it out. */
  z-index: 1;
  margin: 0;
  padding: 1.5rem clamp(1.25rem, 0.75rem + 2vw, 3rem);
  color: #fff;
  font-size: clamp(1.5rem, 1.1rem + 1.8vw, 2.5rem);
  line-height: 1.1;
  letter-spacing: var(--vv-tracking-display);
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.35);
  max-width: 18ch;
}

/* ------------------------------------------------------------- Steps */

.vv-editorial__steps {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem;
  align-items: stretch;
}

.vv-editorial__steps__text {
  background: #efeeea;
  padding: clamp(1.5rem, 1rem + 2vw, 2.75rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* counter-reset/increment rather than numbers typed into the copy: a product
   with two steps then numbers them 01 and 02, instead of inheriting a stale 03. */
.vv-editorial__steps__list {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: vv-step;
}

.vv-editorial__steps__list li {
  counter-increment: vv-step;
  display: grid;
  grid-template-columns: 2.75rem 1fr;
  gap: 1rem;
  align-items: center;
  padding: 0.875rem 0;
  border-top: 1px solid rgba(11, 11, 11, 0.14);
  font-size: 0.9375rem;
}

.vv-editorial__steps__list li::before {
  content: counter(vv-step, decimal-leading-zero);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid rgba(11, 11, 11, 0.3);
  border-radius: 50%;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* --------------------------------------------------------------- FAQ */

.vv-editorial__faq {
  background: #efeeea;
  padding: clamp(1.5rem, 1rem + 2vw, 2.75rem);
}

.vv-editorial__faq__item {
  border-top: 1px solid rgba(11, 11, 11, 0.14);
}

.vv-editorial__faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: var(--vv-tap-min);
  padding: 0.875rem 0;
  cursor: pointer;
  font-size: 0.9375rem;
  list-style: none;          /* Firefox */
}

/* Safari/Chrome draw their own disclosure triangle through a pseudo-element
   that `list-style: none` alone does not remove. */
.vv-editorial__faq__q::-webkit-details-marker {
  display: none;
}

/* Our own +/− marker, so it matches the buy-box accordions rather than the
   browser's triangle. */
.vv-editorial__faq__q::after {
  content: "";
  flex: 0 0 auto;
  width: 0.75rem;
  height: 0.75rem;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' stroke='%230b0b0b' stroke-width='1.4'%3E%3Cpath d='M6 1v10M1 6h10'/%3E%3C/svg%3E");
}

.vv-editorial__faq__item[open] .vv-editorial__faq__q::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' stroke='%230b0b0b' stroke-width='1.4'%3E%3Cpath d='M1 6h10'/%3E%3C/svg%3E");
}

.vv-editorial__faq__a {
  padding: 0 0 1rem;
  font-size: 0.875rem;
  line-height: 1.5;
  color: #3d3d3d;
  max-width: 60ch;
}

/* ------------------------------------------------------------- Mobile */

@media (max-width: 749px) {
  .vv-editorial__spec,
  .vv-editorial__steps {
    grid-template-columns: 1fr;
  }

  /* The deck leads each pair with its image; on desktop the spec image sits
     right and the steps image left, which a single column cannot express. */
  .vv-editorial__spec__media {
    order: -1;
  }

  .vv-editorial__spec-row {
    grid-template-columns: 1fr;
    gap: 0.25rem;
  }

  .vv-editorial__spec-row dd {
    text-align: left;
  }

  .vv-editorial__band__heading {
    max-width: none;
  }
}

/* ==========================================================================
   Rounded corners on cards, panels and media
   --------------------------------------------------------------------------
   The mock-up rounds every card and image; the build had square corners
   throughout. One token, --vv-radius-lg (10px, measured — see the token
   definition), applied wherever the deck shows a rounded edge.

   This reaches surfaces from #14 (gallery), #16 (cross-sell) and #37 (product
   card) as well as #17's editorial blocks, because the radius is a global design
   decision rather than one section's detail. Keeping it in one block means one
   revert if the value is ever wrong.
   ========================================================================== */

/* -- PDP gallery (#14) ---------------------------------------------------- */

.product-single__wrapper--carousel .product-single__media--image,
.product-single__wrapper--carousel .product-single__media--video,
.product-single__wrapper--carousel .product-single__media--model {
  border-radius: var(--vv-radius-lg);
  overflow: hidden;
}

/* The zoom link sits over the image with inset:0, so it needs the same corner or
   its focus ring and hover target overhang the rounded edge. */
.product-single__wrapper--carousel .product-single__media-link {
  border-radius: var(--vv-radius-lg);
}

.product-single__thumbnail,
.product-single__thumbnail-img {
  border-radius: var(--vv-radius-md);
  overflow: hidden;
}

/* -- Cross-sell rows (#16) ------------------------------------------------ */

.vv-upsell-row__media {
  border-radius: var(--vv-radius-md);
}

.vv-upsell-row .vv-upsell-row__btn,
.vv-upsell-row__select {
  border-radius: var(--vv-radius-sm);
}

/* -- Editorial blocks (#17) ----------------------------------------------- */

.vv-editorial__spec__text,
.vv-editorial__steps__text,
.vv-editorial__faq,
.vv-editorial__spec__media,
.vv-editorial__steps__media,
.vv-editorial__band {
  border-radius: var(--vv-radius-lg);
}

/* The band's background image and scrim are absolutely positioned children, so
   they square off the parent's rounded corner unless the parent clips them.
   .vv-editorial__band already sets overflow: hidden for the image; the scrim
   needs the radius too in browsers that paint ::after outside the clip. */
.vv-editorial__band::after {
  border-radius: inherit;
}

/* -- Product cards (#37) -------------------------------------------------- */

.product-grid-item__image {
  border-radius: var(--vv-radius-lg);
  overflow: hidden;
}

/* ==========================================================================
   #60 — PDP buy box
   --------------------------------------------------------------------------
   #15 delivered the sold-out states and the shade pills, but its first
   acceptance criterion — "buy box matches the slide" — was not met. This block
   closes the visual gap.
   ========================================================================== */

/* -- The column is a card ------------------------------------------------- */

.product-single__wrapper--carousel .product-single__details {
  background: #f4f3f0;
  border-radius: var(--vv-radius-lg);
  padding: clamp(1.25rem, 0.9rem + 1.6vw, 2rem);
}

/* -- Title, subtitle, descriptor ------------------------------------------ */

.vv-product__subtitle {
  margin: 0.5rem 0 0;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.3;
}

/* theme.cutline, rendered by the theme's own `cutline` block. Sized down and
   greyed to sit under the subtitle as the deck shows, rather than reading as a
   second heading. */
.product__cutline .product-cutline {
  margin: 0.25rem 0 0;
  font-size: 0.875rem;
  line-height: 1.4;
  color: #6b6b6b;          /* 5.33:1 on the card ground — AA */
}

/* -- Shade names under the swatches --------------------------------------- */

.vv-swatch {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.375rem;
  text-align: center;
  max-width: 4.5rem;
}

.vv-swatch__name {
  font-size: 0.6875rem;
  line-height: 1.2;
  color: #3d3d3d;
  overflow-wrap: anywhere;   /* "Peachy Behaviour" must wrap, not overflow */
}

/* The theme lays swatches out as inline elements with a fixed line-height; the
   name turns each into a two-row item, so the row needs to be a wrapping flex
   with its own vertical rhythm. */
[data-swatches-container] {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 0.625rem;
  align-items: flex-start;
}

/* -- Shade description + skin-tones link ---------------------------------- */

.vv-shade-note {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  margin-top: 0.75rem;
}

.vv-shade-note__text {
  margin: 0;
  font-size: 0.875rem;
  color: #3d3d3d;
}

.vv-shade-note__text[hidden] {
  display: none;
}

.vv-shade-note__link {
  font-size: 0.875rem;
  white-space: nowrap;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* -- Quantity: stepper, not dropdown -------------------------------------- */

/* The theme renders BOTH a select-popout and a − 1 + stepper as siblings, and
   shows the stepper only when the wrapper carries `.select-popout--alt`. That
   modifier is set in the theme's own markup for other contexts, not exposed as a
   setting — so the same swap is made here.

   :has(> .quantity-selector) scopes this to the quantity control. The variant
   selectors use .select-popout too, and hiding their contents would leave a
   product unbuyable. */
.product__form .select-popout:has(> .quantity-selector) > *:not(.quantity-selector) {
  display: none;
}

.product__form .select-popout:has(> .quantity-selector) > .quantity-selector {
  display: flex;
  align-items: center;
  border: 1px solid rgba(11, 11, 11, 0.25);
  border-radius: var(--vv-radius-sm);
  overflow: hidden;
  width: max-content;
}

/* The theme positions these two buttons `absolute` (its own layout overlays them
   on the input), which took them out of the flex row and left a lone "−" box with
   a "+" floating beside it and no number between. They have to return to flow —
   and their inset offsets reset with them, or they drift by the theme's old
   top/left values. */
.product__form .quantity__btn {
  position: static;
  inset: auto;
  transform: none;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--vv-tap-min);
  height: var(--vv-tap-min);
  background: none;
  border: 0;
  cursor: pointer;
}

/* padding: 0 is the fix, not cosmetics. The theme gives this input
   `padding: 10px 32px` (room for the popout's chevron). Against the 2.75rem width
   set here that is 64px of padding in a 44px border-box, so the content box
   collapsed to zero and the "1" was clipped out of existence — an empty pill with
   working +/- buttons that looked broken because nothing ever visibly changed. */
.product__form .quantity__input {
  flex: 0 0 auto;
  padding: 0;
  width: 2.75rem;
  height: var(--vv-tap-min);
  text-align: center;
  /* border-radius: 0 as well as border: 0 — the theme gives this input a 30px
     radius, which drew a circle around the number instead of the deck's plain
     vertical dividers. */
  border: 0;
  border-radius: 0;
  border-left: 1px solid rgba(11, 11, 11, 0.15);
  border-right: 1px solid rgba(11, 11, 11, 0.15);
  background: none;
  -moz-appearance: textfield;
}

.product__form .quantity__input::-webkit-outer-spin-button,
.product__form .quantity__input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* -- Mobile sticky add-to-bag bar ----------------------------------------- */

/* Built, not configured: the theme has no product sticky bar. Its only sticky
   mechanisms are `collection__sticky-bar` (the collection/search filter bar) and
   the sticky header; `product_sticky_enable` is the DESKTOP sticky buy column,
   which is a different thing.

   The bar mirrors the real add-to-cart button rather than duplicating the form —
   see assets/vv-restyle.js — so there is only ever one source of truth for the
   variant and the disabled state. */
.vv-sticky-atc {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* <shopify-chat> renders at z-index 2147483000 and spans the full width at the
     bottom of the viewport, so the Rewards pill and chat bubble sat on top of
     this bar — covering the thumbnail and truncating the price. One higher puts
     the primary buy control in front.

     TRADE-OFF, flagged for the client: while this bar is showing on a mobile
     product page, those two launchers sit behind it. If support coverage matters
     more than the persistent CTA, drop this back to 20 and the launchers win. */
  z-index: 2147483001;
  display: none;
  align-items: center;
  gap: 0.75rem;
  padding: 0.625rem 1rem calc(0.625rem + env(safe-area-inset-bottom));
  background: #fff;
  border-top: 1px solid rgba(11, 11, 11, 0.12);
  box-shadow: 0 -2px 12px rgba(11, 11, 11, 0.08);
  transform: translateY(100%);
  transition: transform 0.25s ease;
}

.vv-sticky-atc--visible {
  transform: translateY(0);
}

@media (max-width: 749px) {
  .vv-sticky-atc {
    display: flex;
    justify-content: flex-end;
  }

  /* The Smile.io rewards launcher sits at the bottom-LEFT, ~150px wide, inside a
     container with z-index 2147483647 — the maximum. No stacking value can get
     in front of it, so the bar gives it that corner instead of putting content
     under it: the thumbnail and title are dropped on mobile and the button takes
     the right-hand side, clear of the launcher.

     Raising z-index above <shopify-chat> (2147483000) was still worth it — that
     one WAS beatable, and it had been clipping the price off the button.

     Properly this belongs in the Smile app's own settings (move the launcher, or
     hide it on product pages). Flagged for the client; revisit if that changes. */
  .vv-sticky-atc__media,
  .vv-sticky-atc__text {
    display: none;
  }

  /* width:auto AND flex:0 0 auto — the theme's .btn is full-width, which stretched
     this back across the launcher even with a min-width set. */
  /* The launcher is ~150px wide at the bottom-left, so 10rem of margin reserves
     that corner deterministically and the button fills whatever is left. Sizing
     by min-width instead left a 16px overlap, because the reserved space then
     depends on the button's own width rather than the launcher's. */
  .vv-sticky-atc .vv-sticky-atc__btn {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
    margin-left: 10rem;
  }
}

.vv-sticky-atc__media {
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--vv-radius-sm);
  object-fit: cover;
}

.vv-sticky-atc__text {
  flex: 1 1 auto;
  min-width: 0;
}

.vv-sticky-atc__title {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vv-sticky-atc__shade {
  margin: 0;
  font-size: 0.75rem;
  color: #6b6b6b;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vv-sticky-atc__btn {
  flex: 0 0 auto;
}

/* Reduced motion: appear without sliding. */
@media (prefers-reduced-motion: reduce) {
  .vv-sticky-atc {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   Collection description alignment
   The theme's .rte--header caps the description at ~526px and centres the block
   with auto side margins — right for a centred banner, wrong for ours, which is
   set to middle-LEFT. The title started at the gutter and the description sat
   ~97px further in, so the two did not line up as the mock-up shows.

   Scoped by the position class the theme already puts on .banner-inner
   (align--middle-left etc.), so a centred banner keeps its auto margins and only
   the left/right variants are corrected. The max-width stays: a long description
   should still wrap before it becomes unreadable.
   -------------------------------------------------------------------------- */

.banner-inner[class*="-left"] .collection__description {
  margin-left: 0;
  margin-right: auto;
}

.banner-inner[class*="-right"] .collection__description {
  margin-left: auto;
  margin-right: 0;
}

/* --------------------------------------------------------------------------
   Collection tabs alignment (#10, extended)
   The mock-up centres the category tabs beneath a left-aligned title and
   description. Driven by a theme-editor setting rather than hard-coded, so the
   client can change it without a deploy — the default is centred, which is what
   the mock shows.
   -------------------------------------------------------------------------- */

.vv-tabs-section--center .vv-tabs__list { justify-content: center; }
.vv-tabs-section--right  .vv-tabs__list { justify-content: flex-end; }
.vv-tabs-section--left   .vv-tabs__list { justify-content: flex-start; }

/* ==========================================================================
   #44 — Collection filters: hidden by default on desktop
   --------------------------------------------------------------------------
   Palo Alto shows the filter sidebar permanently on desktop, which cost the grid
   300px + 30px gutter and squeezed cards to 229px. The reference behaviour
   (tatagaltier.com) keeps filters closed behind a "Show Filters" toggle and lets
   the grid use the full width until they are opened.

   This turned out to need no layout rewiring. `.collection__filters` is
   `float: left` with a 30px right margin and `.collection__products` is an
   ordinary block flowing beside it — so hiding the float lets the grid reflow to
   full width on its own. No grid columns, no transforms, no reflow JS.

   MOBILE IS UNTOUCHED. Below 768px the theme's own drawer, its icon toggle, the
   overlay and the scroll-lock all still run; none of this applies there.
   ========================================================================== */

@media (min-width: 768px) {
  /* Closed is the default state — including on first paint, so there is no flash
     of the sidebar before JS runs. */
  .vv-filters-collapsible .collection__filters {
    display: none;
  }

  .vv-filters-collapsible.collection__inner--vv-filters-open .collection__filters {
    display: block;
    /* The theme pulls the sidebar up by exactly the toolbar's height
       (margin-top: -51px) so the first filter sits LEVEL with the toolbar — which
       works while the toolbar's left side is empty, as it is in stock Palo Alto
       where only "Sort by" is present, on the right.

       The toggle now occupies that space, and the panel was painting straight
       over it: the button and the product count disappeared, and the button
       became unclickable. The reference puts the panel BELOW its toggle, so the
       offset is dropped. */
    margin-top: 0;
  }

  /* Borderless, per the mock-up: icon + "Filter", no pill. The reference site's
     outlined button is its own styling — we follow its BEHAVIOUR, not its chrome.
     No horizontal padding either, so the icon lines up with the page gutter and
     the grid's left edge below it. */
  .vv-filter-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    min-height: var(--vv-tap-min);
    padding: 0;
    border: 0;
    background: none;
    font-size: 0.875rem;
    color: inherit;
    cursor: pointer;
  }

  .vv-filter-toggle:hover {
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  .vv-filter-toggle svg {
    width: 1rem;
    height: 1rem;
  }

  /* The bar is justify-content: flex-end with the count growing to fill. Ordering
     puts the toggle and count on the left and the sort control on the right,
     matching the reference, without touching the theme's markup order. */
  .vv-filters-collapsible .collection__sticky-bar {
    justify-content: flex-start;
    gap: 1rem;
  }

  /* Mock-up order: Filter hard left; sort then the product count hard right.
     margin-left:auto on the sort bar pushes that whole group over, so the count
     does not need its own rule to stay beside it. */
  .vv-filters-collapsible .collection__sticky-bar .vv-filter-toggle { order: 1; flex: 0 0 auto; }
  .vv-filters-collapsible .collection__sticky-bar .collection__sort-bar { order: 2; flex: 0 0 auto; margin-left: auto; }
  .vv-filters-collapsible .collection__sticky-bar .collection__products-count { order: 3; flex: 0 0 auto; }
}

/* The toggle is desktop-only: below 768px the theme's own icon button opens its
   drawer, and two filter controls in one bar would be a bug, not a choice. */
@media (max-width: 767px) {
  .vv-filter-toggle {
    display: none;
  }
}

/* -- #76 Homepage marquee ------------------------------------------------- */

/* The promo bar is Palo Alto's own Marquee section (`section-marquee`), which
   already does multiple editable blocks, scroll direction, speed, and — on line 1
   of its Liquid — hides itself when it has no blocks. None of that is re-done here.

   SCOPING: every rule is keyed to `[id^="Marquee--"]`. The marquee renders the same
   `.top-bar*` classes as the ANNOUNCEMENT BAR, which theme.css normalises in four
   rules — the collision that cost two rounds on #63. Nothing here may leak up. */

:root {
  --vv-marquee-h: 3.75rem;
}

/* `min-height` alone left the text riding high — the bar is `display: block`, so the
   extra height all fell below the line (measured 11px above, 33px below). Centre it.

   Direction is stated explicitly: the theme sets `flex-direction: column` on some
   `.top-bar` variants, and inheriting that here would stack the messages. The child
   needs `min-width: 0` or the scrolling track cannot shrink and overflows the bar. */
[id^="Marquee--"] .top-bar {
  min-height: var(--vv-marquee-h);
  display: flex;
  flex-direction: row;
  align-items: center;
}

[id^="Marquee--"] .top-bar__message {
  flex: 1 1 auto;
  min-width: 0;
}

/* #78: marketing asked for larger text, "to fit the banner", like tatagaltier — whose
   items are 1.35rem at 600 weight with 0.06em tracking. The theme renders ours at
   10.8px, which left 22px of dead space above and below inside a 60px bar.

   The BAR height is deliberately unchanged: `--vv-marquee-h` is what the hero subtracts
   to land on the fold, so growing it would push the bar back under the fold and undo
   #76. Only the text grows, which closes the gap from 22px to ~16px. */
[id^="Marquee--"] .top-bar__ticker {
  font-size: 0.875rem;
  letter-spacing: 0.04em;
}

/* Hero fills the screen MINUS the strip, so the marquee is the last thing above the
   fold rather than incidentally near it.

   Subtract from `--content-full`, NOT `--full-height`. Tatagaltier can use the raw
   viewport because its header OVERLAYS the hero; ours is a solid bar in normal flow,
   so the hero starts 64px down and `viewport - marquee` pushes the bar exactly one
   header BELOW the fold — which is what the first attempt did. Palo Alto already
   computes the value that accounts for it:

     --content-full: calc(100vh - var(--header-height, 0px) - var(--announcement-height));

   It covers the announcement bar too, which matters because that bar is hidden on the
   homepage (#4) and present everywhere else.

   Matched on the section NAME suffix rather than the full id: the rendered id carries
   a per-theme numeric prefix (`template--27067716501754__vv_hero` on staging,
   `template--27109514084602__...` on a dev theme), so hardcoding it works on one theme
   and silently fails on every other.

   Gated on the marquee actually being in the page. The section removes itself when it
   has no blocks, and without this the hero would keep reserving a strip for a bar that
   is not there — leaving a band of the next section above the fold, the opposite of
   the point. */
body:has([id^="Marquee--"]) [id$="__vv_hero"] .full-height,
body:has([id^="Marquee--"]) [id$="__vv_hero"] .full-height-mobile {
  min-height: calc(var(--content-full) - var(--vv-marquee-h));
}

/* The theme's mobile heights live inside `@media (max-width: 767px)`, so at equal
   specificity they win on source order. Repeat the rule in the same query. */
@media only screen and (max-width: 767px) {
  body:has([id^="Marquee--"]) [id$="__vv_hero"] .full-height,
  body:has([id^="Marquee--"]) [id$="__vv_hero"] .full-height-mobile {
    min-height: calc(var(--content-full) - var(--vv-marquee-h));
  }
}

/* Separator between messages, as tatagaltier has. `::after` rather than a rendered
   glyph keeps the block content purely the merchant's words — a message read aloud by
   a screen reader should not end in a bullet. */
[id^="Marquee--"] .top-bar__ticker { position: relative; }
[id^="Marquee--"] .top-bar__ticker::after {
  content: '\2022';
  opacity: 0.45;
  padding-inline: 1.25rem;
}

/* Pause on hover, matching tatagaltier — it gives a reader a way to finish a message
   that is scrolling away. */
[id^="Marquee--"]:hover [data-ticker-text] {
  animation-play-state: paused;
}

/* A continuously moving strip is a vestibular trigger, and it is the whole section
   rather than a decorative flourish. Reduced motion stops it dead and lets the
   messages sit still; tatagaltier handles this too. */
@media (prefers-reduced-motion: reduce) {
  [id^="Marquee--"] [data-ticker-text] {
    animation: none !important;
    transform: none !important;
  }
  [id^="Marquee--"] .top-bar__message {
    overflow-x: auto;
  }
}

/* #78: align the CTA across a rail.

   Cards carry different content — a single-variant product has no swatch row — so the
   buttons landed wherever each card's text happened to end. Measured 23px apart in the
   New In rail: cards with swatches put the CTA at 461px, cards without at 438px.

   Fixed by stretching each cell to the rail's height and letting the info column push
   its last child down, so the CTA sits on the card's bottom edge regardless of what is
   above it.

   Scoped to cards inside a carousel viewport. The collection page lays its cards out in
   a grid rather than a Flickity viewport and is deliberately left alone here — it has
   the same inconsistency and deserves the same treatment, but that is a separate change
   to a page signed off under #44. */
.flickity-viewport .product-grid-item {
  height: 100%;
}

.flickity-viewport .product-grid-item__info {
  flex: 1 1 auto;
}

.flickity-viewport .product-grid-item__quick-buy {
  margin-top: auto;
}

/* #79: the BUY pill was mostly empty space — 17px of text in a 53px button, the
   label filling 38% of its height and 23% of its width.

   Measured anatomy, because none of it is where you would expect:

     a.btn--quick.pill   53px   padding 8/8, min-height 44 (NOT binding)
     └ span.btn__inner   37px   padding 6/6, min-height 37   <- the real driver
       └ span.btn__text  17px   font-size 10.8px             <- the actual label

   So "reduce the button's padding" barely helps: stripping it takes 53 -> 44 and
   leaves the label tiny. The height is owned by `.btn__inner`'s own min-height, and
   the small text is `.btn__text` overriding the button's 13px down to 10.8px.

   Target: 13px label in a 44px pill — 44 being the theme's own min-height and the
   standard minimum tap target, so this lands on an existing floor rather than an
   arbitrary number. Scoped to the BUY variant so the icon-circle quick-buy used
   elsewhere is untouched. */
/* Set the theme's OWN scaling variable rather than the font-size. Palo Alto declares
   `.btn__text`'s size with `!important`, so a plain font-size here loses no matter how
   specific the selector — verified: an identical rule did nothing, the same rule with
   `!important` worked. But the size is `--adjust-button` × a 14.4px base, and the
   variable is not protected: 0.75 gives today's 10.8px, 0.9 gives 13px.

   Going through the variable keeps us inside the theme's type system and avoids adding
   another `!important` to a stylesheet that already has 132 of them. */
.vv-card-cta--buy {
  --adjust-button: 0.9;
}

.vv-card-cta--buy .btn__text {
  line-height: 1.2;
}

.vv-card-cta--buy .btn__inner {
  min-height: 0;
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}

.vv-card-cta--buy .btn--quick {
  padding-top: 0.375rem;
  padding-bottom: 0.375rem;
}

/* ==========================================================================
   #80 — variant-gallery thumbnail grid
   --------------------------------------------------------------------------
   Two problems, one cause.

   Switching to any shade after the first left the visible thumbnails pushed
   right — measured 1195px of empty space for Apricot, 2095px for Chilli —
   because Flickity had scrolled the strip to keep the SELECTED thumb in view
   while it still believed all 25 were present, and hiding the earlier ones does
   not make it re-measure. Its resize handler early-returns on an unchanged
   width, so there is no way to ask it politely.

   So Flickity's positioning is neutralised and the strip is laid out by CSS.
   THREE mechanisms position a cell, and all three have to go. Missing any one
   leaves the strip offset by exactly the width of the hidden thumbs, which
   reads as "the fix did not work":

     .flickity-slider              transform on the TRACK
     .product-single__thumbnail    position:absolute + left   (per cell)
     .product-single__thumbnail    transform                  (per cell — the
                                     one missed first; resetting only the track
                                     made the offset WORSE, 2095px)

   The viewport's inline height goes too: Flickity sizes it for ONE row, which
   would clip every row after the first.

   The layout is a 4-per-row GRID at every width, following tatagaltier
   (`~/projects/tata-shopify/assets/component-tata-pdp.css` → `.tata-pdp__thumbs`),
   whose edges line up with the image above it. Two earlier attempts were worse:
   a horizontal scroller grew a scrollbar across the strip and could leave
   thumbnails off-screen with nothing to scroll them back (Flickity used to do
   that and no longer can); wrapped flex kept every thumb on screen but the rows
   were ragged, because theme.css fixes each thumb at 90px and the row width is
   not a multiple of it.

   Scoped to `[data-vv-media-groups]`, which only exists on a gallery whose media
   is grouped per variant — products without groups keep Flickity's thumb
   carousel untouched.
   ========================================================================== */

[data-vv-media-groups] .product-single__thumbnails .flickity-viewport {
  height: auto !important;
  overflow: visible !important;
}

[data-vv-media-groups] .product-single__thumbnails .flickity-slider {
  position: static !important;
  transform: none !important;
  width: auto !important;
}

[data-vv-media-groups] .product-single__thumbnails .product-single__thumbnail {
  position: static !important;
  left: auto !important;
  transform: none !important;
}

/* The grid goes on whichever element is the thumbnails' real parent: the
   Flickity track when it initialised, the strip itself when it did not (a
   two-media group is below Flickity's threshold on some layouts). */
[data-vv-media-groups] .product-single__thumbnails .flickity-slider,
[data-vv-media-groups] .product-single__thumbnails:not(.flickity-enabled) {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.75rem;
}

/* theme.css pads every cell 5px and gives the first and last `--gutter-mobile`
   so the carousel bleeds to the page edge. Both fight the grid gap, and the
   asymmetric first/last padding is what stopped the old flex rows lining up. */
[data-vv-media-groups] .product-single__thumbnails .product-single__thumbnail {
  padding: 0 !important;
  width: 100%;
  min-width: 0;                      /* beats the theme's 110px on the non-Flickity path */
}

/* theme.css fixes the link at a hard 90x90, which cannot fill a fluid grid
   cell. Fill the cell and hold the stage's square ratio instead. */
[data-vv-media-groups] .product-single__thumbnails .product-single__thumbnail-link {
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
}

/* Alignment with the image above depends on the strip occupying the same box,
   and two things stop that:

     desktop — the counter shares the thumbrow's flex line, so the strip gives
       up the counter's width plus a 1rem gap and ends short on the right
     mobile  — theme.css bleeds the strip past the page gutters with
       `margin: 5px calc(var(--gutter-mobile) * -1)`, so it over-runs on both

   Mobile already wraps the counter onto its own line (#14); this extends that
   to every width and undoes the bleed. Only inside a grouped gallery — the
   full-bleed scroller is still the right behaviour everywhere else. */
[data-vv-media-groups] .vv-gallery-thumbrow {
  flex-wrap: wrap;
  gap: 0;
}

[data-vv-media-groups] .vv-gallery-thumbrow .product-single__thumbnails {
  flex: 1 0 100%;
  margin-left: 0;
  margin-right: 0;
}

[data-vv-media-groups] .vv-gallery-counter {
  flex: 0 0 100%;
  width: 100%;
  margin-top: 0.5rem;
  margin-left: 0;
  text-align: right;
}

/* ==========================================================================
   PDP above-the-fold width (PR #80, alongside #80's thumbnail grid)
   --------------------------------------------------------------------------
   Three separate caps keep the purchase zone narrow, and only the third is a
   setting we could have changed in the editor:

     1. `.wrapper { width: var(--site-width) }` → `--SITE-WIDTH` is 1440px, so
        the block stops growing at 1425px however wide the monitor is
     2. `.wrapper { padding: 0 var(--gutter) }` → 60px a side on top of that
     3. `--large` = 60/40 columns (the editor's only alternative is 50/50)

   Net at a 1497px viewport: 1305px of content, gallery 753px, panel 492px. The
   square stage is therefore 753px tall, which pushes the swatches and the buy
   button below the fold, while the copy is crammed into 428px once the panel's
   own 32px padding is taken off.

   Tatagaltier measures ~43% gallery / ~46% panel with ~2.5% side gutters. This
   block reproduces that ratio.

   ALL THREE overrides are scoped to `.product-single` — `--SITE-WIDTH` and
   `--gutter` are global tokens and moving either one would resize every page in
   the store.

   `:not(.featured-product)` matters: `sections/section-product.liquid` carries
   `.product-single` too, and the homepage runs two of them (the Signature Lip
   Trio pair) on `wrapper--full-padded`. Without the exclusion these rules would
   pull both narrower and re-split their columns — a homepage regression from a
   PDP change.
   ========================================================================== */

@media only screen and (min-width: 1024px) {
  /* Cap 1 + 2: let the product block use the viewport rather than --site-width.
     100% (not 100vw) so it measures the padding box of .product-single, which
     already excludes the scrollbar — 100vw would overflow by its width. */
  .product-single:not(.featured-product) > .wrapper {
    width: 100%;
    /* A cap still has to exist. Dropping --site-width entirely let the stage
       reach 801px square at 1920 and kept growing past that, with the panel's
       copy running to a ~140-character measure. 100rem holds the stage near the
       660px it gets on a laptop, so the page looks the same on both. */
    max-width: 100rem;
    margin-left: auto;
    margin-right: auto;
    padding-left: max(2rem, 3vw);
    padding-right: max(2rem, 3vw);
  }

  /* Cap 3: 46/54 of what is left after the gap. Both modifier classes are
     listed so the result does not depend on which "Image width" the template
     happens to be set to — the override is the layout now, not the setting. */
  .product-single:not(.featured-product) .product-single__wrapper--carousel.product-single__wrapper--large,
  .product-single:not(.featured-product) .product-single__wrapper--carousel.product-single__wrapper--standard {
    grid-template-columns: minmax(0, 46fr) minmax(0, 54fr);
    gap: 3rem;
  }
}

/* ==========================================================================
   Atome instalment widget — duplicated, and breaking the price row
   --------------------------------------------------------------------------
   The PDP renders the instalment line TWICE, from two different generations of
   Atome's own widget. Neither is in this repo: there is no Atome app embed in
   `config/settings_data.json`, so both arrive by store-level ScriptTag and are
   injected after the page renders.

     legacy  `.atome-widget` with a raster `img.atome-icon`, inline styles
             computed off the theme's font, price written "RM26.33 MYR".
             Injected INSIDE `.product__price-and-rating`.
     current `.atome-price-divider.atome-widget` with hashed CSS-module classes
             and an inline SVG logo, price "RM26.33". Injected as a sibling,
             directly inside `.product__price-and-badge`.

   The duplicate is the obvious half. The layout damage is the legacy one:
   `.product__price-and-rating` is `display: flex; justify-content: space-between`
   built for exactly TWO children — price left, rating right. The legacy widget
   lands between them as a third child, so the rating is flung to the far edge of
   the panel (measured x=1385 on a 1497px viewport, 318px clear of the price it
   belongs to). On a product with no reviews there is no rating to strand, and
   the widget takes the right-hand slot itself.

   So the legacy one goes and the current one stays — it already sits on its own
   line under the price, which is where the reference design puts it.

   `:has()` guard: hide the legacy widget only while the current one is actually
   present. If Atome ever retires the newer script, the legacy line reappears by
   itself rather than leaving the PDP with no instalment message at all — this is
   third-party markup that can change without warning, and hiding it outright
   would fail silently and in the wrong direction.

   ⚠️ This is a theme-side workaround, not the root cause. Two Atome scripts are
   running on the storefront, most likely a stale ScriptTag from an earlier
   install alongside the current app. Removing that script in Admin is the real
   fix and would make this block dead code — see the issue.
   ========================================================================== */
.product__price-and-badge:has(> .atome-widget) .product__price-and-rating > .atome-widget {
  display: none;
}

/* ==========================================================================
   Duplicate product rating in the price row
   --------------------------------------------------------------------------
   The rating renders TWICE on every PDP that has reviews, from two sources
   reading the same Judge.me data:

     `.product__rating`        Palo Alto's own. One star + the average ("5.0"),
                               inside `.product__price-and-rating`.
     `.jdgm-preview-badge`     Judge.me's. Five stars + the COUNT ("1 review"),
                               on its own line below.

   Judge.me's is the one to keep: it carries the review count as well as the
   average, and it links through to the reviews. The theme's shows the average
   alone and goes nowhere.

   There is a layout reason too. `.product__price-and-rating` is
   `justify-content: space-between`, so the theme's rating is pinned to the far
   right of the row whatever the row's width — measured 518px from the price it
   describes, reading as a stray mark rather than part of the price block. That
   gap grew rather than shrank when the purchase zone was widened (PR #80 took
   the row from 429px to 670px), so it gets worse on a large monitor, not better.

   Hidden rather than removed from `snippets/product.liquid`: the markup is the
   theme's, shared with quick view and the featured-product section, and one
   CSS rule reverts in a way an edit to a 2,700-line snippet does not.

   Kept out of the accessibility tree as well — `.product__rating` carries
   `role="img"` with `aria-label="5.0 out of 5.0 stars"` and `tabindex="0"`, so
   `display: none` is doing real work here: visibility alone would leave a
   focusable, announced duplicate behind for keyboard and screen-reader users.
   ========================================================================== */
.product__price-and-rating > .product__rating {
  display: none;
}

/* ==========================================================================
   MOCK — Judge.me featured carousel on the homepage (for review, not final)
   --------------------------------------------------------------------------
   Replaces `vv_reviews`, our own testimonials section, which prints
   "4.9/5 from 10,000+ reviews" over three invented people (#65). Judge.me's
   real numbers are 4.91 from 459 reviews, so the honest version needs no
   exaggeration — and the carousel stays true on its own as reviews come in.

   The carousel block is already installed in templates/index.json; it was
   simply disabled. This is styling, not new machinery.

   Judge.me ships its own stylesheet and loads it AFTER the theme's, so a plain
   class selector loses on source order. Each rule here is scoped under
   `.vv-jdgm-carousel` — a wrapper we control — which raises specificity enough
   to win without a single `!important`. If a rule here stops applying after a
   Judge.me update, check whether they renamed the class before reaching for
   `!important`.
   ========================================================================== */

.vv-jdgm-carousel {
  padding-block: var(--vv-space-8);
}

/* Judge.me insets its own content INSIDE the section wrapper, with two
   percentage margins that together pushed the heading 109px right of the
   testimonials section above it:

     .jdgm-carousel-wrapper        margin-left 68.85px  (~5%)
     .jdgm-carousel-title-and-link margin-left 40.5px   (~3.5%)

   Setting the section to wrapper--full-padded made the OUTER box match (both
   1497px at left 0) while the visible content still started 109px apart — which
   is why "the widths match" was wrong the first time. Measure the heading and
   the cards, not the section.

   Zeroed here so the 60px gutter comes from the theme's wrapper alone, the same
   single source the rest of the homepage uses. */
.vv-jdgm-carousel .jdgm-carousel-wrapper,
.vv-jdgm-carousel .jdgm-carousel-title-and-link {
  margin-left: 0;
  margin-right: 0;
}

/* The inset is not only margins: Judge.me ships

     .jdgm-carousel-wrapper { margin: auto; width: 90% !important; }

   Measured 1239.3 of 1377 — exactly 0.9. Specificity cannot beat !important, so
   two rounds of raising it changed nothing; only matching it does. This is the
   ONE !important in this block, and it exists because their rule has one. Every
   other rule here wins on specificity alone — if you are tempted to add a
   second, check the cascade first (the rules are visible via
   document.styleSheets; that is how this one was found, after guessing wrong
   twice). */
.vv-jdgm-carousel .jdgm-carousel-wrapper {
  width: 100% !important;
}

/* Their heading is a generic h2 at the app's own scale. Match the section
   headings used elsewhere on the homepage. */
.vv-jdgm-carousel .jdgm-carousel-title {
  font-size: var(--vv-text-lg);
  letter-spacing: var(--vv-tracking-display);
  color: var(--vv-ink);
  margin: 0 0 var(--vv-space-2);
  text-align: left;
}

/* The aggregate line — stars and "from 459 reviews" on ONE row under the
   heading, matching .testimonials__aggregate in the section this replaces.

   Judge.me ships the wrapper as display:block with the stars inline inside a
   full-width block, so they centred themselves across 1094px and floated away
   from the count sitting underneath. Making the wrapper a flex row puts them
   back together and left-aligns both. */
.vv-jdgm-carousel .jdgm-carousel-title-and-link {
  text-align: left;
}

/* Three descendants deep, not two. At .vv-jdgm-carousel + one class these
   rules TIED with Judge.me's own, and theirs loads after the theme's, so the
   wrapper stayed display:block and the count kept its own line. The card and
   type rules above won at two levels; these needed the extra one. Checked by
   reading computed display, not by assuming the rule applied. */
.vv-jdgm-carousel .jdgm-carousel-title-and-link .jdgm-all-reviews-rating-wrapper {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--vv-space-2);
  width: auto;
}

.vv-jdgm-carousel .jdgm-carousel-title-and-link .jdgm-all-reviews-rating {
  display: inline-flex;
  align-items: center;
  width: auto;
}

.vv-jdgm-carousel .jdgm-carousel-title-and-link .jdgm-carousel-number-of-reviews {
  display: inline-block;
  width: auto;
  font-size: var(--vv-text-sm);
  color: var(--vv-ink-muted);
}

/* -- The card -------------------------------------------------------------
   Same shape as the testimonial card this replaces (#19): white ground, hairline
   border, 8px radius — so the homepage reads as one system rather than two. */
/* The gutter cannot be a `gap`: `.jdgm-carousel__item-wrapper` is display:block
   and the carousel computes each item's width in JS, so a margin on the item
   would be fought by that arithmetic. Instead the ITEM stays the positioning
   box and only gets side padding; the visible card is the inner __review. The
   gap is then padding, and Judge.me's width maths is untouched. */
.vv-jdgm-carousel .jdgm-carousel-item {
  padding: 0 var(--vv-space-2);
  background: none;
  border: 0;
  box-shadow: none;
}

.vv-jdgm-carousel .jdgm-carousel-item__review {
  height: 100%;
  background: var(--vv-surface);
  border: 1px solid var(--vv-line);
  border-radius: var(--vv-radius-md);
  padding: var(--vv-space-5);
  box-shadow: none;
}

.vv-jdgm-carousel .jdgm-carousel-item__review-title {
  font-size: var(--vv-text-base);
  font-weight: 600;
  color: var(--vv-ink);
  margin: var(--vv-space-3) 0 var(--vv-space-2);
}

.vv-jdgm-carousel .jdgm-carousel-item__review-body {
  font-size: var(--vv-text-sm);
  line-height: 1.6;
  color: var(--vv-ink);
}

/* Reviewer name and product, de-emphasised so the quote leads. */
.vv-jdgm-carousel .jdgm-carousel-item__reviewer-name,
.vv-jdgm-carousel .jdgm-carousel-item__timestamp,
.vv-jdgm-carousel .jdgm-carousel-item__product {
  font-size: var(--vv-text-sm);
  color: var(--vv-ink-muted);
}

/* Stars in --vv-star (#d44243), the same red the testimonials section this
   replaces uses for its rating. Judge.me's own default is gold, which belongs
   to no token on this site. Covers the aggregate row and the per-card ratings,
   since both use .jdgm-star. */
.vv-jdgm-carousel .jdgm-star {
  color: var(--vv-star);
}

/* Arrows — LEFT ALONE deliberately.

   The arrow element is empty (no text, no children), so its glyph is drawn
   entirely by Judge.me's own CSS, in `widget_v3_base.css`. That sheet is
   cross-origin, so document.styleSheets cannot enumerate it — the same
   CORS-blind scanner limitation already recorded in HANDOFF.md. Which means we
   cannot see how the arrow is constructed, only what happens when we touch it.

   Both resets tried here removed it: `background: none` and `border: none`.
   A chevron drawn with borders, or a background-image, dies to one or the
   other — and the result is two invisible 28px boxes holding open the gutter,
   which reads as "the cards are misaligned" rather than as a missing control.

   So: style nothing structural on these. If the arrows ever need restyling,
   do it from Judge.me's own widget settings, not from here. */

@media (max-width: 767px) {
  .vv-jdgm-carousel {
    padding-block: var(--vv-space-6);
  }
}
