/*  Leaf & Bloom - Base + shared components
 *  ---------------------------------------------------------------------------
 *  Loads on every page. Reset, typography, layout, buttons, forms, badges,
 *  product card, accordions, header, footer. Ships into the theme verbatim.
 *  Reads only from tokens.css - no literal colour, size or radius below.
 *  ------------------------------------------------------------------------- */

/* == Reset ================================================================ */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

/*  The browser hides [hidden] with `display: none`, at the very bottom of the
 *  cascade — so any class that sets its own display quietly un-hides the
 *  element, and JavaScript setting `el.hidden = true` then does nothing you can
 *  see. That had already bitten `.buy-hint` (display: flex), which left "Choose
 *  a flavour first." on screen after a flavour had been chosen. Settle it once,
 *  here, rather than patching each offender as it is noticed. */
[hidden] { display: none !important; }

/*  clip, not hidden. `overflow: hidden` on <html> creates a containing block,
 *  which breaks position:sticky on the header and can clip the fixed cursor and
 *  floating-action layers. clip contains the .bleed utilities with none of that. */
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; overflow-x: clip; }

body {
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--ff-sans);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* overflow-x now handled on <html> with clip, so bleeds work */
}

img, picture, svg, video { display: block; max-width: 100%; }
img { height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
ul, ol { list-style: none; padding: 0; }
a { color: var(--c-accent); text-decoration-thickness: 1px; text-underline-offset: 0.22em; }
:target { scroll-margin-top: calc(var(--sp-12) + var(--sp-5)); }

/* One visible focus style everywhere. Never removed. */
:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

/* == Accessibility helpers ================================================ */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; top: var(--sp-2); left: var(--sp-2);
  z-index: 999;
  padding: var(--sp-3) var(--sp-5);
  background: var(--c-brand-fill);
  color: var(--c-on-brand);
  border-radius: var(--r-pill);
  font-size: var(--fs-small);
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform var(--dur) var(--ease);
}
.skip-link:focus { transform: translateY(0); }

/* == Layout =============================================================== */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.wrap--narrow { max-width: var(--wrap-narrow); }

/*  Four weights, not one - and asymmetric.
 *
 *  Every section used to carry an identical 96px top AND bottom. The standard
 *  deviation of section padding across the whole homepage was zero, so however
 *  much the CONTENT varied (144px in the benefits strip, 1,059px in the
 *  ingredients) the FRAME never did, and the eye read one repeating stamp.
 *
 *  A section now closes tighter than it opens, which reads as a paragraph
 *  break rather than a box, and --lg is rationed to one moment per page. */
.section        { padding-block: var(--section-y) calc(var(--section-y) * .75); }
.section--tight { padding-block: var(--section-y-sm) calc(var(--section-y-sm) * .75); }
.section--band  { padding-block: var(--section-y-sm); }   /* bands stay symmetric */
.section--lg    { padding-block: var(--section-y-lg); }   /* the one statement */

/* A section that runs straight into the one above it, with no seam. */
.section--flush-top { padding-top: 0; }
.section--flush-bot { padding-bottom: 0; }

/*  Two plain sections in a row on the same ground do not need two full gaps
 *  between them - that is one boundary, not two. Tinted bands keep their own
 *  padding, because there the colour change IS the boundary. */
.section + .section:not([class*="--tint"]):not([class*="--warm"]):not([class*="--brand"]) {
  padding-top: calc(var(--section-y) * .55);
}

/*  Break out of the 1240px column. A single content width held for the whole
 *  page is the loudest signal of a starter theme; these let a section reach
 *  the viewport edge.
 *  overflow-x: clip on <html>, never overflow: hidden - hidden would create a
 *  containing block and kill position: sticky on the header. */
/*  Negative margins with width:auto, NOT width:100vw. 100vw counts the
 *  scrollbar, so on any desktop with one the element is wider than the page and
 *  the document reports horizontal overflow even where nothing is visible. */
.bleed   { margin-inline: calc(50% - 50vw); }
.bleed-r { margin-right:  calc(50% - 50vw); }
.bleed-l { margin-left:   calc(50% - 50vw); }

/*  A scroll rail that runs to the viewport edge.
 *
 *  .rail already carries its own negative gutter margins, so adding .bleed on
 *  top made the two fight and pushed the page 100px wide. This replaces them
 *  instead: the rail spans the viewport, and its padding puts the FIRST card
 *  back in line with the heading above it, so the row starts where the text
 *  starts and continues off the right edge. */
.rail--bleed {
  margin-inline: calc(50% - 50vw);
  padding-inline: calc(50vw - 50%);
}
.section--tint    { background: var(--c-tint); }
.section--warm    { background: var(--c-surface-2); }
/*  Measured at 263px on two pages for three facts, 37% of it empty green.
 *  A one-line call to action is a strip, not a chapter, so it sits on the
 *  small rung of the section ladder. */
.section--brand   { background: var(--c-brand-fill); color: var(--c-on-brand); padding-block: var(--section-y-sm); }

/*  A band you can actually see.
 *
 *  Measured, the two tinted section backgrounds step off the cream page by
 *  1.158:1 and 1.181:1 — differences a screen can render and an eye cannot.
 *  So the bands that were supposed to be breaking up the scroll were doing
 *  no work at all, and the page read as one unbroken cream field from the
 *  hero to the footer.
 *
 *  Deepening the fill is not available: pushed far enough to be visible,
 *  --c-text-soft on it drops below 4.5:1 and fails the contrast gate. An
 *  edge does the same job for nothing — it is what the reference the owner
 *  pointed at does, and --g-gold, a hairline that fades out at both ends,
 *  has been sitting in tokens.css unused since it was written.
 *  ---------------------------------------------------------------------- */
.section--edged { position: relative; }
.section--edged::before,
.section--edged::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  height: 1px;
  background: var(--g-gold);
  pointer-events: none;
}
.section--edged::before { top: 0; }
.section--edged::after  { bottom: 0; }
.section--brand .section-title,
.section--brand .eyebrow { color: var(--c-on-brand); }
/*  A primary button is brand-green, and so is this band — so on About and FAQ
 *  the "Chat on WhatsApp" button rendered as a pill the same colour as the
 *  ground it sat on, and read as a line of plain text. On a brand band the
 *  primary inverts: cream fill, green label. That is the existing "primary
 *  button label" pair from contrast-check.js, turned inside out, at 8.2:1. */
.section--brand .btn--primary { background: var(--c-on-brand); color: var(--c-brand-fill); border-color: var(--c-on-brand); }
.section--brand .btn--primary:hover { background: var(--c-bg); color: var(--c-brand-fill-hover); }

/* == Benefits strip =======================================================
 *  Four reassurances in a row. Lives here rather than in home.css because the
 *  shop page uses it as well, and the shop page does not load home.css — which
 *  is exactly how it first shipped: the cards had no columns to sit in and
 *  stacked into an 874px tower of four, where the same markup on the homepage
 *  was 214px.
 *  ---------------------------------------------------------------------- */
.benefits { display: grid; gap: var(--sp-6); text-align: center; }
@media (min-width: 640px)  { .benefits { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .benefits { grid-template-columns: repeat(4, 1fr); gap: 0; } }

.benefit { display: grid; justify-items: center; gap: var(--sp-2); padding-inline: var(--sp-5); }
/*  A hairline between the four, the way a bank of facts is separated on every
 *  shop that reads as established. It is the divider, not the box, that makes
 *  four statements read as one considered strip rather than four loose
 *  paragraphs — and unlike a box it costs no height. */
@media (min-width: 1024px) {
  .benefit + .benefit { border-left: 1px solid color-mix(in srgb, var(--c-gold) 30%, transparent); }
}

/*  The icon carried a 56px gold ring, which at 1px and 50% alpha was the
 *  faintest thing on the page and made each icon look like a disabled button.
 *  The ring is gone and the stroke does the work instead: bigger, gold, and
 *  drawn thin enough to stay elegant at 34px. */
.benefit__icon { display: grid; place-items: center; color: var(--c-gold-line); }
.benefit__icon svg { width: 34px; height: 34px; fill: none; stroke: currentColor; stroke-width: 1.3; }

/*  The small card heading. A measurement of the whole preview found seven
 *  rules doing this one job at three sizes in two typefaces — the same
 *  numbered step was 16px Jost on the about page and 18px Cormorant on the
 *  product pages. Nobody can name that, but it is what makes a site read as
 *  assembled rather than designed. One size, one face, one weight, here and
 *  in .step h3, .unbox__step h3 and .closer__card h3. */
.benefit h3 { font-family: var(--ff-sans); font-size: var(--fs-h4); font-weight: 500; line-height: var(--lh-snug); color: var(--c-heading-deep); }
.benefit p { font-size: var(--fs-small); color: var(--c-accent); max-width: 26ch; line-height: var(--lh-snug); }
/*  The about page uses the same card for whole sentences a customer reads
 *  ("If we cannot explain why something is in the bottle..."), and at 14px
 *  beside 16px paragraphs they read as fine print. Those are body size with
 *  copy leading. The home and shop strips are one-line fragments, the same
 *  role as the closer-card sub-lines, and stay small. The about cards are
 *  divs and the strips are list items, which is what tells them apart. */
div.benefit p { font-size: var(--fs-body); line-height: var(--lh-copy); }

.section-head { margin-bottom: var(--sp-6); max-width: var(--measure); }
.section-head--center { margin-inline: auto; text-align: center; }
/*  A section head that uses the width it already has.
 *
 *  .section-head is a single 70ch column, so on a 1240px wrap its eyebrow,
 *  heading and sub-line stack into three full-height rows with the right
 *  half of the band empty — the same shape that made the shop and category
 *  page heads 340px each before they were split. This is that fix, as a
 *  modifier any section can take: heading on the left, the sentence beside
 *  it, bottom-aligned.
 *  ---------------------------------------------------------------------- */
@media (min-width: 900px) {
  .section-head--split {
    max-width: none;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, .9fr);
    column-gap: var(--sp-9);
    align-items: end;
  }
  .section-head--split .eyebrow,
  .section-head--split .section-title { grid-column: 1; }
  .section-head--split .section-sub {
    grid-column: 2;
    grid-row: 1 / span 2;
    align-self: end;
    margin-top: 0;
    /*  Optical: a 16px sentence sitting on the same baseline as a 44px
     *  Cormorant heading reads as having slipped. */
    padding-bottom: .4em;
  }
}

/* == Typography =========================================================== */
/*  Jost stops at 500 and Cormorant at 600. The UA default for <strong> is
 *  700, which neither face carries, so the browser was SYNTHESISING bold —
 *  smearing the outline sideways rather than loading a heavier cut. Every
 *  emphasised number on the site ("Rs 2,500" in the hero promise, the
 *  delivery threshold, the prices in running copy) was rendering that way.
 *  These two rules ask each face for the heaviest weight it actually has. */
strong, b { font-weight: 500; }
h1 strong, h2 strong, h3 strong, h4 strong,
.title-hero strong, .section-title strong { font-weight: 600; }
h1, h2, h3, h4 {
  font-family: var(--ff-display);
  font-weight: 600;
  line-height: var(--lh-tight);
  color: var(--c-heading);
  text-wrap: balance;
}

/*  Cormorant is a variable face with 500 AND 600, and every heading on the
 *  site was pinned to 500 — the lighter of the two. At 86px that is a
 *  hairline-thin headline on a dark hero, which is a large part of why the
 *  page reads as washed out. 600 costs zero bytes: the weight is already in
 *  the file. Removing these overrides lets h1-h4 give them all 600. */
.title-hero {
  font-size: var(--fs-hero);
  color: var(--c-heading-deep);
  letter-spacing: var(--ls-display);
}
/*  Section headings used to be one size, so "Everything we make" - the shop
 *  itself - arrived at exactly the same weight as "@leafandbloom". A page where
 *  everything is equally important has no hierarchy, and reads as a catalogue.
 *  Three ranks now: the chapter, the default, and the quiet label. */
.section-title { font-size: var(--fs-h2); color: var(--c-heading-deep); }
.section-title--lead {
  font-size: var(--fs-h2-lead);
  letter-spacing: var(--ls-display);
  line-height: var(--lh-tight);
}
/*  This was the one heading on the site in the WRONG FAMILY: 22px Jost,
 *  uppercase, while its eight siblings were 44px Cormorant. It is the
 *  single clearest instance of "koi bara koi chota". It stays quieter than
 *  a chapter heading — a step down in size and a softer colour — but it is
 *  now unmistakably the same kind of thing as the headings around it. */
.section-title--quiet {
  font-size: var(--fs-h2-sm);
  color: var(--c-text-soft);
}
h3, .h3 { font-size: var(--fs-h3); }
h4, .h4 { font-size: var(--fs-h4); }

p { max-width: var(--measure); }
.lead { font-size: var(--fs-lead); color: var(--c-text-soft); line-height: var(--lh-body); }
.section-sub { margin-top: var(--sp-3); color: var(--c-text-soft); }
.small { font-size: var(--fs-small); }
.muted { color: var(--c-text-soft); }

/* The eyebrow: the one place small uppercase type is allowed. It is 12px
 * text, so it takes the gold that can be read (--c-gold-ink), not the
 * decorative gold of the rules and stars, which measured 3.0-3.6:1 here. */
.eyebrow {
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--c-gold-ink);
  margin-bottom: var(--sp-3);
}

/* Gold hairline + sparkle - the flourish carried over from the logo. */
.rule-gold {
  display: flex; align-items: center; gap: var(--sp-3);
  color: var(--c-gold-line);
  margin-block: var(--sp-5);
}
.rule-gold::before, .rule-gold::after {
  content: ''; flex: 1; height: 1px; background: currentColor; opacity: .5;
}
.rule-gold--short::before, .rule-gold--short::after { max-width: 56px; }
.sparkle { width: 14px; height: 14px; flex: none; fill: currentColor; }

/* The arch - the logo's thin arch used as a framing device on imagery. */
.arch { border-radius: 50% 50% var(--r-md) var(--r-md) / 26% 26% var(--r-md) var(--r-md); overflow: hidden; }

blockquote.pull {
  font-family: var(--ff-display);
  font-size: var(--fs-h2-sm);
  font-weight: 500;
  line-height: var(--lh-snug);
  color: var(--c-heading-deep);
  max-width: 24ch;
}

/* == Buttons ============================================================== */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-2);
  min-height: 48px;
  padding: var(--sp-3) var(--sp-6);
  border-radius: var(--r-pill);
  font-family: var(--ff-sans);
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing: var(--ls-btn);
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.btn:active { transform: scale(.98); }

.btn--primary {
  background: var(--c-brand-fill);
  color: var(--c-on-brand);
  border: 1.5px solid transparent;
}
.btn--primary:hover {
  background: var(--c-brand-fill-hover);
  transform: translateY(-1px);
  box-shadow: var(--shadow-lift);
}
.btn--primary:active { transform: scale(.98); }

.btn--secondary {
  background: transparent;
  color: var(--c-accent);
  border: 1.5px solid var(--c-accent);
}
.btn--secondary:hover { background: var(--c-accent); color: var(--c-bg); }

/*  Reads as a text link but is still a real control, so it keeps the 44px
 *  target - the extra height is padding above and below the underline. */
.btn--ghost {
  padding: var(--sp-2) 0;
  color: var(--c-accent);
  border: 0; border-radius: 0;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--c-accent) 40%, transparent);
  text-underline-offset: 0.35em;
}
.btn--ghost:hover { text-decoration-color: var(--c-gold-line); }

.btn--block { display: flex; width: 100%; }
/*  --sm is the quieter card CTA, not a smaller touch target: it keeps the 44px
 *  height and only loses padding and type size. */
.btn--sm { min-height: 44px; padding: var(--sp-2) var(--sp-4); font-size: var(--fs-micro); }
.btn[aria-disabled="true"], .btn:disabled { opacity: .45; pointer-events: none; }

/* == Badges =============================================================== */
.badge {
  display: inline-block;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-pill);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  line-height: var(--lh-copy);
}
.badge--sale { background: var(--c-sale-fill); color: var(--c-on-sale); }
.badge--new  { background: var(--c-new-fill);  color: var(--c-on-new); }
.badge--best { background: var(--c-gold-line); color: var(--c-on-gold); }
.badge--soft { background: var(--c-tint); color: var(--c-heading); }

/* == Rating =============================================================== */
.rating { display: flex; align-items: center; gap: var(--sp-2); }
.rating__stars { display: inline-flex; gap: 2px; color: var(--c-gold-line); }
.rating__stars svg { width: 14px; height: 14px; fill: currentColor; }
.rating__stars svg.is-empty { fill: none; stroke: currentColor; stroke-width: 1.5; opacity: .55; }
.rating__count { font-size: var(--fs-small); color: var(--c-text-soft); }

/* == Price ================================================================ */
.price {
  font-family: var(--ff-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  color: var(--c-text);
  display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap;
}
.price del { color: var(--c-text-soft); font-size: var(--fs-body); font-weight: 500; }
.price ins { text-decoration: none; color: var(--c-sale-text); }
.price--lg { font-size: var(--fs-h2-sm); }

/* == Forms ================================================================ */
.field { margin-bottom: var(--sp-5); }
.field > label,
.label {
  display: block;
  margin-bottom: var(--sp-2);
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--c-text);
}
.field__hint { margin-top: var(--sp-2); font-size: var(--fs-small); color: var(--c-text-soft); }

input[type="text"], input[type="email"], input[type="tel"], input[type="search"],
input[type="password"], input[type="number"], select, textarea {
  width: 100%;
  min-height: 48px;
  padding: var(--sp-3) var(--sp-4);
  background: var(--c-surface);
  color: var(--c-text);
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-sm);
  font-size: var(--fs-body);   /* 16px keeps iOS from zooming on focus */
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
textarea { min-height: 140px; resize: vertical; line-height: var(--lh-body); }

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 22%, transparent);
}
input:focus-visible, select:focus-visible, textarea:focus-visible { outline: none; }
::placeholder { color: var(--c-text-soft); opacity: .75; }

select {
  appearance: none;
  padding-right: var(--sp-9);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right var(--sp-5) center, right calc(var(--sp-5) - 5px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

/* Validation - colour is never the only signal; each state carries an icon + text. */
.field--error input, .field--error textarea, .field--error select { border-color: var(--c-bad); }
.field__msg { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); font-size: var(--fs-small); }
.field__msg svg { width: 15px; height: 15px; flex: none; }
.field__msg--error   { color: var(--c-bad); }
.field__msg--success { color: var(--c-ok); }

/* Quantity stepper */
.qty {
  display: inline-flex; align-items: center;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--r-pill);
  background: var(--c-surface);
  overflow: hidden;
}
.qty button {
  width: 44px; height: 46px;
  display: grid; place-items: center;
  color: var(--c-accent);
  font-size: var(--fs-h4); line-height: var(--lh-flat);
  transition: background var(--dur) var(--ease);
}
.qty button:hover { background: var(--c-tint); }
.qty input {
  width: 46px; min-height: 46px; padding: 0;
  text-align: center;
  border: 0; border-radius: 0; background: transparent;
  -moz-appearance: textfield;
}
.qty input::-webkit-outer-spin-button,
.qty input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qty input:focus { box-shadow: none; }

/* == Product card ========================================================= */
.pcard {
  display: flex; flex-direction: column;
  background: var(--c-surface);
  border: var(--card-edge);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
  overflow: hidden;
  transition: box-shadow var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease);
}
/*  The card frame stays put and the OBJECT inside it lifts. A whole card
 *  sliding up on hover moves the text with it, which is what makes a grid feel
 *  like a deck of cards; lifting only the product reads as picking it up. */
/*  The edge turns gold as well. On dark --shadow-lift is none, so without
 *  it a product card had no hover signal at all there. Only the product card
 *  gets a hover: the whole of it is a link, and a card that answers the
 *  cursor but not the click reads as broken. */
.pcard:hover { box-shadow: var(--shadow-lift); border-color: var(--card-edge-hover); }

.pcard__media {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;             /* reserved box - no shift when art loads */
  background: var(--c-well);       /* ivory well: photos are never dark-filtered */
  overflow: hidden;
}

/*  ---- COMPACT CARDS, 25 Sep 2026 -------------------------------------
 *
 *  The owner asked for a shorter card, with a competitor's deal row as the
 *  reference: picture, name, price, nothing else. Ours carried six rows -
 *  category, title, note, price, stock, button - and a 4:5 well on top, so a
 *  row of three stood far taller than the section around it.
 *
 *  Three things came off, in order of how much height they returned:
 *
 *  1. The well is now square rather than 4:5. That is 20% of the tallest
 *     element in the card. The product art is centred with room above and
 *     below, so a square crop keeps the whole product.
 *  2. The category eyebrow is gone. It sat under a heading that already said
 *     "Everything we make" inside a section whose own cards say "Hair care" /
 *     "Lip care" one line below - the third time in four inches.
 *  3. The stock line shrinks to the size of the note beside it. It is
 *     reassurance, not a heading, and it was set at body size.
 *
 *  What deliberately STAYED is the button. The reference has none and expects
 *  a click through to the product page - but this shop's cart is the order,
 *  and taking the one control that starts an order off the card to save
 *  24 pixels is a bad trade.                                              */
.pcard__cat { display: none; }
.pcard__body { gap: var(--sp-1); padding: var(--sp-3) var(--sp-4) var(--sp-4); }
.pcard__title { margin-bottom: 0; }
.pcard .price { margin-top: 0; }
.pcard__cta { margin-top: var(--sp-3); }
.pcard .stock { font-size: var(--fs-micro); }

/*  A square well, for a product whose photograph is square.
 *
 *  The Root Rise Oil was shot as a 1:1 composition with the brand name and the
 *  size set beside the bottle. Cover-cropping that into the 4:5 well takes 20%
 *  off the width and cuts the words in half - "LEAF & BLOOM" arrives as
 *  "AF & BLOOM". So its cards get a square well and the whole photograph,
 *  rather than a crop that reads as a mistake. The card below the well is
 *  unchanged, so a row of cards still ends on the same line. */
.pcard__media--square { aspect-ratio: 1 / 1; }
.pcard__media img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: opacity var(--dur-2) var(--ease),
              transform var(--dur-5) var(--ease-settle);
}
.pcard__media .is-alt { opacity: 0; }
.pcard:hover .pcard__media .is-alt { opacity: 1; }
/* The product rises out of its own well; the frame does not move. */
.pcard:hover .pcard__media img { transform: translateY(var(--lift)) scale(1.035); }

/*  A contact shadow under the product that widens as it lifts, so the rise
 *  reads as height rather than as a picture sliding. */
.pcard__media::after {
  content: '';
  position: absolute; left: 18%; right: 18%; bottom: 6%;
  height: 12px;
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(38,55,11,.20), transparent 70%);
  opacity: 0;
  transition: opacity var(--dur-5) var(--ease-settle), transform var(--dur-5) var(--ease-settle);
  pointer-events: none;
}
.pcard:hover .pcard__media::after { opacity: 1; transform: scaleX(1.25); }

.pcard__flags {
  position: absolute; inset: var(--sp-3) var(--sp-3) auto var(--sp-3);
  display: flex; justify-content: space-between; gap: var(--sp-2);
  pointer-events: none;
}

/*  The satellites sit at about 235px in the broken grid, so their bodies were
 *  cramped: four lines of text and a button in 16px of padding read as a
 *  spreadsheet row. More room, a real gap under the name, and the price given
 *  its own line make a narrow card feel considered rather than squeezed. */
/*  The card body was 24px of padding on all four sides with 8px between six
 *  stacked lines. Trimmed at the top and bottom only — the side padding is
 *  what keeps a product name off the card edge and it stays. */
.pcard__body {
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  flex: 1;
}
.pcard__cat { margin-bottom: var(--sp-1); }
.pcard__title { margin-bottom: var(--sp-1); }
/* The price is the decision point — it gets air above it, not a tight stack. */
.pcard .price { margin-top: var(--sp-1); }
.pcard__cta { margin-top: var(--sp-4); }
.pcard__cat {
  font-size: var(--fs-micro); letter-spacing: var(--ls-micro);
  text-transform: uppercase; color: var(--c-text-soft);
}
.pcard__title { font-size: var(--fs-h4); font-weight: 600; color: var(--c-text); }
.pcard__title a { color: inherit; text-decoration: none; }
.pcard__title a:hover { color: var(--c-accent); }
/* Whole card clickable, but the title stays the real, screen-reader-visible link. */
.pcard__title a::after { content: ''; position: absolute; inset: 0; }
.pcard { position: relative; }
.pcard__cta, .pcard__media { position: relative; z-index: 1; }
.pcard__cta { margin-top: auto; }

/* The one-line spec under a product title: size, variant list, key ingredient. */
.pcard__note { font-size: var(--fs-small); color: var(--c-text-soft); line-height: var(--lh-copy); }

.stock { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-small); }
.stock svg { width: 15px; height: 15px; flex: none; }
.stock--in  { color: var(--c-ok); }
.stock--low { color: var(--c-sale-text); }

/*  The quick-add stays visible rather than appearing on hover. Reserving the
 *  button's height and then hiding it left an obvious dead band under every
 *  card, and a hover-only control is unreachable on touch anyway. It is styled
 *  as the quiet secondary button so the grid stays editorial. */

/* == Product grid ========================================================= */
.pgrid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-5) var(--sp-4);
}
@media (min-width: 768px)  { .pgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-6); } }
@media (min-width: 1024px) { .pgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/*  A grid should fit what is in it. Four products in a three-column grid
 *  leaves one stranded on a second row with an empty half beside it, which
 *  reads as a page that ran out rather than a range that is complete. Each
 *  archive picks the column count that divides its own catalogue. */
.pgrid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/*  A category with two products was drawing its cards at 400px wide — half
 *  as big again as the same card on the shop page, so the identical product
 *  changed size depending on which page you reached it from. Capped at the
 *  width two cards occupy in the four-up grid, so a card is a card
 *  everywhere. */
@media (min-width: 768px) { .pgrid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 596px; } }

.pgrid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 768px) { .pgrid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/*  The cross-sell row, added 24 Sep 2026. The "Also from us" section at the
 *  foot of each category page used to be a heading and a button over empty
 *  space - it named the other half of the range without showing any of it,
 *  so the one thing it was there to do, make you want to look, it could not
 *  do. It now draws the other category's actual products, as the same .pcard
 *  the shop, the homepage and the related-products grids draw.
 *
 *  The count differs by page and cannot be hard-coded: hair care points at
 *  lip care, which is ONE product (the balm, in three flavours), and lip care
 *  points at hair care, which is TWO. auto-fit is what makes one class serve
 *  both - empty tracks collapse, so a single card takes the row rather than
 *  sitting in the left half of it, and the row centres under its own heading
 *  either way. Cards stay 286px on desktop, the width two occupy inside the
 *  capped .pgrid--2, so a product is the same size here as everywhere else. */
/*  128px is the narrowest track that still lets TWO fit on the smallest phone
 *  this site is checked at: 320px viewport, 272px of grid inside the gutter,
 *  and a 16px column gap - 128 + 16 + 128 = 272 exactly. At 150px they broke
 *  onto separate rows below about 375px, and every other grid on this site
 *  stays two-up on a phone, so the cross-sell was the one place a pair of
 *  cards suddenly stacked. The tracks are 1fr, so 128px is only the threshold
 *  that decides how many there are - two cards share the row whatever the
 *  width, and a single card collapses the track it does not need and takes
 *  the row on its own. */
.pgrid--cross {
  grid-template-columns: repeat(auto-fit, minmax(min(128px, 100%), 1fr));
  justify-content: center;
  margin-inline: auto;
}
/*  282px is what a card actually measures inside .pgrid--2: that grid is
 *  capped at 596px and the gap above 768px is --sp-6, so (596 - 32) / 2.
 *  The tracks are that fixed width rather than 1fr, so one card does not
 *  stretch to fill the row; the box around them is given room for two plus
 *  the gap with a little to spare, and justify-content centres whatever is
 *  in it - a cap of exactly 596px leaves no rounding margin and drops the
 *  second card onto its own row. */
@media (min-width: 768px) {
  .pgrid--cross {
    grid-template-columns: repeat(auto-fit, minmax(0, 282px));
    max-width: 640px;
  }
}

/*  == The range ===========================================================
 *  This was a deliberately broken grid: the first card 1.6x wider with a
 *  square crop and larger type, and the second and fourth pushed down 40px
 *  so the baselines never lined up. The reasoning was that four identical
 *  cards is the most template-looking thing a shop can do.
 *
 *  The owner's reading, looking at the rendered page, was the opposite:
 *  "koi box bara koi chota" — one box big, one small — and he is right that
 *  it is what a shopper sees. Asymmetry only reads as confidence when the
 *  rest of the page is tightly aligned; on a page he already finds loose,
 *  it reads as a mistake. A shopper comparing four products also wants them
 *  comparable, and a grid whose cards disagree about size is quietly saying
 *  the small ones matter less.
 *
 *  Four equal cards, aligned, same crop, same type — the same card the shop
 *  page and the category pages draw, so a product is the same object
 *  wherever it is met.
 *
 *  THREE columns, not four, since 24 Sep 2026. There are three products. The
 *  fourth column was being filled by showing the lip balm twice — once as
 *  "Lip Balm" and once as "Lip Balm — Lavender" — which is not a fourth
 *  product, it is the same Rs 650 jar met twice in one row. With the
 *  duplicate gone, a four-column grid would leave the exact hole the .pgrid
 *  note above warns about: a row that reads as a page which ran out rather
 *  than a range which is complete. Three columns at 1024+ matches .ritual,
 *  the three-card section further down this same page, so the homepage keeps
 *  one column rhythm; and it is what .pgrid--3 already gives the cart.
 *  -------------------------------------------------------------------- */
.range {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-5) var(--sp-4);
  align-items: stretch;
}
@media (min-width: 768px) { .range { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-6); } }
/*  Below 768px this stays TWO columns, so three products leave one card alone
 *  on a second row with an empty half beside it. That is the one thing the
 *  three-card decision costs, and it is kept on purpose: every other grid on
 *  this site is two-up on a phone, and one column here would add about 840px
 *  to a homepage the owner already reads as too long. If he would rather have
 *  three full-width cards on a phone, it is this one rule:
 *      @media (max-width: 767px) { .range { grid-template-columns: 1fr; } }   */

/* Horizontal scroll-snap rail. Never auto-plays. */
.rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 74%;
  gap: var(--sp-4);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
  /*  A scroller clips on both axes. The soft card shadow reaches about 50px
   *  below a card, and it was cut off in a hard line 16px under the row. The
   *  room is padded in and the same amount taken back with a negative
   *  margin, so the row sits exactly where it did: 8 - 8 above, and 48 - 32
   *  below is the 16 it always had. */
  padding-block: var(--sp-2) var(--sp-8);
  margin-block: calc(var(--sp-2) * -1) calc(var(--sp-6) * -1);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
}
.rail > * { scroll-snap-align: start; }
@media (min-width: 640px)  { .rail { grid-auto-columns: 44%; } }
@media (min-width: 900px)  { .rail { grid-auto-columns: 30%; } }
@media (min-width: 1200px) { .rail { grid-auto-columns: 23%; } }

.rail-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-5); }
.rail-nav { display: none; gap: var(--sp-2); }
@media (min-width: 900px) { .rail-nav { display: flex; } }
.rail-nav button {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border: 1.5px solid var(--c-line-strong);
  border-radius: var(--r-pill);
  color: var(--c-accent);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.rail-nav button:hover { background: var(--c-tint); border-color: var(--c-accent); }
.rail-nav svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.7; }

/* == Accordions (native <details>, enhanced) ============================== */
.acc { border-top: 1px solid var(--c-line); }
.acc__item { border-bottom: 1px solid var(--c-line); }
.acc__item > summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-5) 0;
  font-family: var(--ff-sans);
  font-size: var(--fs-body);
  font-weight: 500;
  color: var(--c-text);
  cursor: pointer;
  list-style: none;
  min-height: 44px;
}
.acc__item > summary::-webkit-details-marker { display: none; }
.acc__item > summary:hover { color: var(--c-accent); }
.acc__icon {
  flex: none; width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  color: var(--c-accent-soft);
  transition: transform var(--dur-3) var(--ease);
}
.acc__item[open] .acc__icon { transform: rotate(45deg); }
.acc__panel { padding-bottom: var(--sp-5); color: var(--c-text-soft); }
.acc__panel > * + * { margin-top: var(--sp-3); }
.acc__panel ul { display: grid; gap: var(--sp-2); }
.acc__panel li { position: relative; padding-left: var(--sp-5); }
.acc__panel li::before {
  content: ''; position: absolute; left: 0; top: .62em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--c-gold-line);
}
/*  The ingredient name is the thing a reader scans for in a list of ten, so it
 *  carries the panel's full text colour rather than its softened body colour.
 *
 *  THIS CHANGES BOTH PRODUCT PAGES, not just the oil's. They are the only two
 *  pages on the site that put a <strong> inside an accordion panel at all - ten
 *  of them on the oil, thirteen on the toner - and on both of them every one is
 *  an ingredient name at the head of its line, which is the case this is for. */
.acc__panel li strong { color: var(--c-text); }

/*  Numbered steps - ordered lists that actually read as ordered.
 *  ------------------------------------------------------------------------
 *  The reset at the top of this file is `ul, ol { list-style: none }`, so an
 *  <ol> dropped into an accordion panel came out indistinguishable from a
 *  bullet list: same gold dot, no numbers, no order. On a product page that
 *  is not a cosmetic difference. "Leave it on overnight FIRST" is only true
 *  in sequence, and a shopper reading a routine has to be able to see that it
 *  IS a sequence.
 *
 *  So the count is drawn with a CSS counter, in the display face and the gold
 *  of .unbox__n so the two numbered things on a product page match, and the
 *  dot is switched off for these items only.
 *
 *  THIS BLOCK MUST STAY BELOW .acc__panel li::before. .acc__panel ol > li
 *  outranks it on specificity, but .acc__steps > li does not - the two are
 *  tied at (0,1,2), so source order is the only thing deciding which wins.
 *  Moving this block above that one would quietly give the steps a dot again
 *  and no number, and only outside a panel, which is the kind of bug that
 *  takes an afternoon to find.
 *
 *  IT IS BOUND TO THE ELEMENT, NOT TO A CLASS. Every <ol> inside a panel is a
 *  sequence - that is what an <ol> is - so .acc__panel ol carries it and no
 *  product page has to remember an opt-in class. Both product pages wrote
 *  their steps as a plain <ol> and both are numbered by this, which is the
 *  point: two product pages that render the same question differently is a
 *  fault, and a class only one of them knows about is how that starts.
 *
 *  .acc__steps is the same list OUTSIDE a panel: position, left and padding
 *  are set on it rather than inherited from .acc__panel li, so a product page
 *  can print one set of steps in "How to use" and the identical set in its
 *  "in use" section further down.
 *
 *  ONE BLOCK, NOT TWO. This file used to number .acc__panel ol twice: a second
 *  rule a few lines above ran its own `acc-step` counter over the same <ol>.
 *  The two were tied on specificity, so source order alone decided which
 *  numeral was drawn, the losing counter incremented for nothing, and that
 *  block's "no padding override" note no longer described anything that
 *  rendered. It is folded in here. font-size is set explicitly below because
 *  it used to arrive from there. */
.acc__panel ol,
.acc__steps {
  display: grid;
  gap: var(--sp-3);
  counter-reset: step;
  color: var(--c-text-soft);
}
.acc__panel ol > li,
.acc__steps > li {
  position: relative;
  padding-left: var(--sp-7);
}
.acc__panel ol > li::before,
.acc__steps > li::before {
  counter-increment: step;
  content: counter(step);
  position: absolute;
  left: 0;
  top: 0;
  width: auto;
  height: auto;
  border-radius: 0;
  background: none;
  font-family: var(--ff-display);
  font-size: var(--fs-body);
  font-variant-numeric: tabular-nums;
  line-height: inherit;
  color: var(--c-gold-line);
}

/* == Review card ========================================================== */
/*  Shared by the homepage review band and the product page's review list, so
 *  it lives here rather than in either page stylesheet. Real cards are filled
 *  from approved WooCommerce reviews; placeholders carry --sample. */
.reviews { display: grid; gap: var(--sp-5); }
@media (min-width: 768px) { .reviews { grid-template-columns: repeat(3, 1fr); gap: var(--sp-6); } }
.reviews--stack { grid-template-columns: 1fr; }
@media (min-width: 768px) { .reviews--stack { grid-template-columns: 1fr; gap: var(--sp-5); } }
.review {
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-6);
  background: var(--c-surface);
  border: var(--card-edge);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
}
.review__text { font-family: var(--ff-display); font-size: var(--fs-lead); line-height: var(--lh-copy); color: var(--c-text); }
.review__meta { margin-top: auto; padding-top: var(--sp-3); border-top: 1px solid var(--c-line); font-size: var(--fs-small); color: var(--c-text-soft); }
.review__meta strong { display: block; color: var(--c-text); font-weight: 500; }
/* Stamped on every placeholder review so it can never read as real social proof. */
.review--sample { position: relative; }
.review--sample::before {
  content: 'Sample';
  position: absolute; top: var(--sp-4); right: var(--sp-4);
  padding: 1px var(--sp-2);
  border: 1px dashed var(--c-gold-line);
  border-radius: var(--r-sm);
  font-size: var(--fs-nano); letter-spacing: var(--ls-micro); text-transform: uppercase;
  color: var(--c-gold-line);
}

/* == Breadcrumbs ========================================================== */
.crumbs { padding-block: var(--sp-4); font-size: var(--fs-small); }
.crumbs ol { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); }
.crumbs li { display: flex; align-items: center; gap: var(--sp-3); color: var(--c-text-soft); }
.crumbs li + li::before { content: '/'; color: var(--c-accent-soft); }
/* 24px tall with 12px between links: clears WCAG 2.5.8 on both size and
   spacing without turning a one-line trail into a toolbar. */
.crumbs a { display: inline-flex; align-items: center; min-height: 24px; color: var(--c-text-soft); text-decoration: none; }
.crumbs a:hover { color: var(--c-accent); text-decoration: underline; }
.crumbs [aria-current="page"] { color: var(--c-text); }

/* == Announcement bar ===================================================== */
/* Fixed height reserved in both modes so it can never shift the page. */
/*  == The contact bar ====================================================
 *  The strip above the header. It used to carry the delivery line, which is
 *  now said four times lower down the page — in the benefits strip, in the
 *  FAQ, in the pair arithmetic and in the closing panel — so the one piece
 *  of screen a visitor sees before anything else was spending itself on a
 *  repeat.
 *
 *  It carries the two things a Pakistani shopper looks for first instead:
 *  a WhatsApp number they can actually tap, and an email address. Both are
 *  real and both are the ones already in the footer; the social links are
 *  the footer's own placeholders until the accounts are confirmed.
 *
 *  Three columns, and the middle one is centred on the PAGE rather than in
 *  the space left over — 1fr auto 1fr, not flex with space-between, which
 *  would let a long email address shove the number off centre.
 *
 *  On the homepage the background is not this colour: home.css hands it to
 *  --hero-bar so the strip takes the deep green, berry or amber of whichever
 *  hero slide is showing. That behaviour is asserted by tools/test-motion.js,
 *  so the background here is the fallback every other page uses.
 *  -------------------------------------------------------------------- */
.announcement {
  background: var(--c-brand-fill);
  color: var(--c-on-brand);
  font-size: var(--fs-small);
  padding-block: var(--sp-2);
  min-height: 40px;
  display: grid;
  align-items: center;
  /*  A gold hairline is the cheapest thing on the page that reads as
   *  considered rather than default. */
  border-bottom: 1px solid color-mix(in srgb, var(--c-gold) 38%, transparent);
}
.announcement a { color: inherit; text-underline-offset: 0.3em; }

.announcement__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-5);
}
.announcement__mail,
.announcement__wa {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 32px;
}
.announcement__mail { justify-self: start; opacity: .92; }
.announcement__mail:hover { opacity: 1; }

/*  The number is the loudest thing in the strip, because it is the thing
 *  most likely to be used. Tracking rather than weight: Jost at 500 on a
 *  dark ground already looks heavy, and a second step of weight makes the
 *  bar shout. */
.announcement__wa {
  justify-self: center;
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  font-size: var(--fs-micro);
}
.announcement__wa strong {
  font-weight: 500;
  letter-spacing: var(--ls-wide);
  font-variant-numeric: tabular-nums;
}
/*  The number is a phone link, so its tap target has to clear 44px (WCAG 2.5.8)
 *  even though the text itself is 20px tall. The padding does that; the equal
 *  negative margin keeps the strip the height it was, so nothing moves.
 *  The underline is text-decoration rather than a border for the same reason -
 *  a border would be pushed 12px below the word by the padding. */
.announcement__wa a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 6px;
  margin-block: -12px;
  margin-inline: -6px;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  text-underline-offset: 0.3em;
}
.announcement__wa a:hover { text-decoration-color: currentColor; }

.announcement svg { width: 16px; height: 16px; flex: none; }

.announcement__social {
  justify-self: end;
  display: flex;
  gap: var(--sp-1);
}
.announcement__social a {
  width: 32px; height: 32px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  opacity: .8;
  transition: opacity var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}
.announcement__social a:hover { opacity: 1; color: var(--c-gold-line); }

/*  Below 900px the three columns cannot all fit at a readable size, so the
 *  strip keeps the one thing worth tapping and drops the rest — they are
 *  both in the footer. Hidden, not shrunk: a 10px email address is not an
 *  email address. */
@media (max-width: 900px) {
  /*  The bar grows by 8px so the number can be a 44px tap target. A phone
   *  number in a contact strip that is too small to hit is a contact strip
   *  that does nothing. */
  .announcement { min-height: 48px; }
  .announcement__inner { grid-template-columns: 1fr; }
  .announcement__mail,
  .announcement__social { display: none; }
  .announcement__wa { justify-self: center; }
  .announcement__wa a { display: inline-flex; align-items: center; min-height: 44px; }
}

/* == Header =============================================================== */
.site-header {
  position: sticky; top: 0; z-index: var(--z-header);
  background: var(--c-bg);
  border-bottom: 1px solid var(--c-line);
  transition: background var(--dur-3) var(--ease), border-color var(--dur-3) var(--ease);
}
.site-header__inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--sp-4);
  min-height: var(--header-h);
}

/*  Condensed once the page has been scrolled.
 *
 *  The height does NOT change. An earlier version reduced min-height by 12px,
 *  which shrank the header, which shrank the document, which shifted the page
 *  every time someone scrolled - measurable CLS bought for a decorative touch.
 *  The logo scaling inside a fixed bar reads as condensing and moves nothing. */
body.is-scrolled .brand img { transform: scale(.86); }
body.is-scrolled .site-header { box-shadow: var(--shadow-card); }
.brand img { transition: transform var(--dur-3) var(--ease-move); transform-origin: left center; }

/*  Transparent over the hero, solid once scrolled.
 *  The header stays `position: sticky` in both states and never leaves the
 *  flow - the hero is pulled up underneath it with a negative margin instead.
 *  Switching to `fixed` on scroll would collapse the header's flow box and
 *  jolt the whole page; this way the overlay costs exactly zero layout shift. */
.site-header--overlay {
  background: transparent;
  border-bottom-color: transparent;
}
/*  .nav__link, NOT .nav a. These rules turn the nav cream so it reads over the
 *  hero photograph — but `.nav a` also matches every link inside the Shop
 *  dropdown, and that panel has a cream background. Cream on cream made the
 *  whole product menu invisible. Only the top-level links sit on the hero. */
.site-header--overlay .brand__name,
.site-header--overlay .nav__link,
.site-header--overlay .nav__disclosure,
.site-header--overlay .iconbtn { color: var(--c-on-brand); }
/*  Over the hero the tagline sits on the deep ground, where the plain gold
 *  measured 3.34:1; the bright gold is made for it (6.21). Stuck, the
 *  header turns cream and the tagline goes back to the readable gold. */
.site-header--overlay .brand__tag { color: var(--c-gold-bright); }
.site-header--overlay.is-stuck .brand__tag { color: var(--c-gold-ink); }
.site-header--overlay.is-stuck {
  background: var(--c-bg);
  border-bottom-color: var(--c-line);
  box-shadow: var(--shadow-card);
}
.site-header--overlay.is-stuck .brand__name,
.site-header--overlay.is-stuck .nav__link,
.site-header--overlay.is-stuck .nav__disclosure,
.site-header--overlay.is-stuck .iconbtn { color: var(--c-text); }
.site-header--overlay.is-stuck .brand__name { color: var(--c-heading); }

/*  The dropdown panel is its own surface with its own ground, so it always
 *  takes the page's text colours regardless of what the header is doing. */
.navmenu, .navmenu a { color: var(--c-text); }
.navmenu__col h3 a { color: var(--c-text-soft); }
.navmenu__price { color: var(--c-text-soft); }

/* Brand lockup: the logo mark plus the wordmark and tagline set in the display
   serif. Below 480px there is not room for both beside five icon buttons - the
   wordmark collided with the theme toggle - so the mark carries the brand alone
   and grows slightly to compensate. */
.brand { display: flex; align-items: center; gap: var(--sp-3); text-decoration: none; min-width: 0; }

/*  == The mark ==========================================================
 *  This was a JPEG of the whole logo lockup, which meant a cream rectangle
 *  sat behind it: barely visible on the cream header, and a bright white
 *  card on the homepage where the header floats over the dark hero. A logo
 *  that reads as a sticker stuck onto the page is the fastest way to make a
 *  shop look unfinished.
 *
 *  It is now the tree and its arch alone, cut out of the existing lockup
 *  (tools kept the original; see logo-mark-512.png), transparent, in two
 *  weights. The deep one is the default; the pale one is swapped in only
 *  while the header is floating over the hero, and swaps back the moment
 *  the header goes solid. Only index.html carries the pale file, because
 *  it is the only page whose header is ever transparent.
 *  -------------------------------------------------------------------- */
.brand__logo--lite { display: none; }
.site-header--overlay:not(.is-stuck) .brand__logo--ink  { display: none; }
.site-header--overlay:not(.is-stuck) .brand__logo--lite { display: block; }
.brand img { height: 46px; width: auto; flex: none; }
@media (min-width: 1024px) { .brand img { height: 56px; } }
@media (max-width: 479px) {
  .brand img { height: 50px; }
  .brand__name { display: none; }
}
.brand__name {
  font-family: var(--ff-display);
  font-size: var(--fs-lead);
  font-weight: 600;
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-heading);
  line-height: var(--lh-tight);
}
.brand__tag {
  display: block;
  font-family: var(--ff-display);
  font-style: italic;
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--ls-btn);
  text-transform: none;
  color: var(--c-gold-ink);   /* 12px text: the readable gold, not the decorative one */
}

.nav { display: none; }
@media (min-width: 1024px) {
  .nav { display: flex; justify-content: center; align-items: center; gap: var(--sp-6); }
  /*  The menu read as five plain 14px sentence-case words floating in a wide
   *  bar — the owner called it unprofessional and the diagnosis is that it
   *  had no treatment at all: no weight, no tracking, nothing that said a
   *  decision had been made. It now takes the same small-caps treatment the
   *  eyebrows use throughout the site, so the navigation belongs to the same
   *  design as everything under it. */
  .nav__link {
    position: relative;
    display: inline-flex; align-items: center; gap: var(--sp-1);
    padding-block: var(--sp-2);
    font-size: var(--fs-micro);
    font-weight: 500;
    letter-spacing: var(--ls-micro);
    text-transform: uppercase;
    color: var(--c-text);
    text-decoration: none;
    transition: color var(--dur) var(--ease);
  }
  .nav__link::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: 0;
    height: 1px; background: var(--c-gold-line);
    transform: scaleX(0); transform-origin: left;
    transition: transform var(--dur) var(--ease);
  }
  .nav__link:hover::after,
  .nav__link[aria-current="page"]::after,
  .nav__item.is-open .nav__link::after { transform: scaleX(1); }
}

/* == Shop dropdown ========================================================
 *  The trigger is a real link to the shop page with a SEPARATE disclosure
 *  button beside it. That way the category is always reachable in one click
 *  and with JavaScript off, while the panel stays a proper ARIA disclosure.
 *  The panel's own links are what let a crawler reach every product from any
 *  page on the site.
 *  ---------------------------------------------------------------------- */
.nav__item { position: relative; display: flex; align-items: center; }

.nav__disclosure {
  display: grid; place-items: center;
  width: 22px; height: 22px;
  margin-left: 2px;
  color: var(--c-accent-soft);
  border-radius: var(--r-sm);
}
.nav__disclosure svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2; transition: transform var(--dur) var(--ease); }
.nav__item.is-open .nav__disclosure svg { transform: rotate(180deg); }

.navmenu {
  position: absolute; top: calc(100% + var(--sp-4)); left: 50%;
  z-index: calc(var(--z-header) + 1);
  width: max-content; max-width: min(660px, 88vw);
  padding: var(--sp-6);
  background: var(--c-surface);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lift);
  opacity: 0;
  transform: translate(-50%, -6px);
  visibility: hidden;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease), visibility var(--dur);
}
.nav__item.is-open .navmenu { opacity: 1; visibility: visible; transform: translate(-50%, 0); }

/* A hover bridge, so the pointer can cross the gap without the panel closing. */
.navmenu::before { content: ''; position: absolute; left: 0; right: 0; top: calc(var(--sp-4) * -1); height: var(--sp-4); }

.navmenu__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-7); }
.navmenu__col h3 {
  font-family: var(--ff-sans);
  font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: var(--ls-micro); text-transform: uppercase;
  color: var(--c-text-soft);
  padding-bottom: var(--sp-3);
  margin-bottom: var(--sp-3);
  border-bottom: 1px solid var(--c-line);
}
.navmenu__col h3 a { display: inline-flex; align-items: center; min-height: 24px; color: inherit; text-decoration: none; }
.navmenu__col h3 a:hover { color: var(--c-accent); }
.navmenu__col ul { display: grid; gap: var(--sp-1); }
/*  Each row carries the product's own thumbnail. A menu of names and prices tells a
 *  shopper nothing they cannot already guess; the picture is what makes "Lip Balm —
 *  Cocoa Butter" mean something before they click. The images are the 150px files the
 *  cart already uses, so nothing new is downloaded on a page that has a cart line. */
.navmenu__thumb {
  width: 40px; height: 40px; flex: none;
  border-radius: var(--r-sm);
  object-fit: cover;
  background: var(--c-tint);
  box-shadow: 0 0 0 1px var(--c-line);
}
.navmenu__name { flex: 1 1 auto; }

.navmenu__col li > a {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: 40px;
  padding: var(--sp-2) var(--sp-3);
  margin-inline: calc(var(--sp-3) * -1);
  border-radius: var(--r-sm);
  font-size: var(--fs-small);
  color: var(--c-text);
  text-decoration: none;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.navmenu__col li > a:hover { background: var(--c-tint); color: var(--c-accent); }
.navmenu__price { color: var(--c-text-soft); font-family: var(--ff-display); font-size: var(--fs-small); }

.navmenu__foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-5);
  margin-top: var(--sp-5); padding-top: var(--sp-5);
  border-top: 1px solid var(--c-line);
}
.navmenu__note { font-size: var(--fs-micro); color: var(--c-text-soft); }

/* == Drawer nav groups (the same links, on mobile) ======================== */
.drawer-nav__group + .drawer-nav__group { margin-top: var(--sp-6); }
.drawer-nav__group h3 {
  font-family: var(--ff-sans);
  font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: var(--ls-micro); text-transform: uppercase;
  color: var(--c-text-soft);
  margin-bottom: var(--sp-2);
}
/* The heading is also the link to the category page, so it takes a real target
   rather than the 18px the micro type alone would give it. */
.drawer-nav__group h3 a { display: inline-flex; align-items: center; min-height: 44px; color: inherit; text-decoration: none; }
.drawer-nav__sub { display: grid; }
/*  The drawer carries the same thumbnails as the desktop menu. Most of this shop's
 *  traffic is a phone, so if the picture is worth showing anywhere it is worth
 *  showing here first. .navmenu__name takes the free space so the price stays right. */
.drawer-nav__sub a {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: 44px;
  padding: var(--sp-2) 0;
  font-family: var(--ff-sans);
  font-size: var(--fs-small);
  color: var(--c-text);
  text-decoration: none;
  border-bottom: 1px solid var(--c-line);
}
.drawer-nav__sub a:hover { color: var(--c-accent); }

.header-actions { display: flex; align-items: center; gap: var(--sp-1); justify-self: end; }
.iconbtn {
  position: relative;
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  color: var(--c-text);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.iconbtn:hover { background: var(--c-tint); color: var(--c-accent); }
.iconbtn svg { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 1.5; }
.iconbtn--menu { display: grid; }
@media (min-width: 1024px) { .iconbtn--menu { display: none; } }

/* Five icon buttons do not fit beside the brand on a 360px phone. Account is
   the one that can move - it is in the drawer, one tap away. */
.iconbtn--account { display: none; }
@media (min-width: 600px) { .iconbtn--account { display: grid; } }

.cart-count {
  position: absolute; top: 4px; right: 3px;
  min-width: 19px; height: 19px; padding: 0 5px;
  display: grid; place-items: center;
  background: var(--c-gold-line);
  color: var(--c-on-gold);
  border-radius: var(--r-pill);
  font-size: var(--fs-nano);
  font-weight: 500;
  line-height: var(--lh-flat);
}
.cart-count.is-pop { animation: pop var(--dur-3) var(--ease); }
@keyframes pop { 0% { transform: scale(1); } 45% { transform: scale(1.35); } 100% { transform: scale(1); } }

/* Theme toggle: shows the mode you will switch TO. */
.theme-toggle svg { transition: opacity var(--dur) var(--ease); }
.theme-toggle .icon-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: block; }
:root[data-theme="dark"] .theme-toggle .icon-sun  { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: block; }
  :root:not([data-theme="light"]) .theme-toggle .icon-sun  { display: none; }
}

/* == Drawers (mobile nav, shop filters, search) =========================== */
.drawer-scrim {
  position: fixed; inset: 0; z-index: var(--z-drawer);
  background: var(--c-scrim);
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-3) var(--ease), visibility var(--dur-3);
}
.drawer-scrim.is-open { opacity: 1; visibility: visible; }

.drawer {
  position: fixed; top: 0; bottom: 0; z-index: calc(var(--z-drawer) + 1);
  width: min(380px, 88vw);
  display: flex; flex-direction: column;
  background: var(--c-bg);
  box-shadow: var(--shadow-drawer);
  border-inline: var(--card-border);
  transition: transform var(--dur-3) var(--ease);
  overscroll-behavior: contain;
}
.drawer--left  { left: 0;  transform: translateX(-102%); }
.drawer--right { right: 0; transform: translateX(102%); }
.drawer.is-open { transform: none; }

.drawer__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--c-line);
  min-height: 68px;
}
.drawer__title { font-family: var(--ff-display); font-size: var(--fs-h3); color: var(--c-heading); }
.drawer__body { flex: 1; overflow-y: auto; padding: var(--sp-5); }
.drawer__foot { padding: var(--sp-4) var(--sp-5); border-top: 1px solid var(--c-line); display: grid; gap: var(--sp-3); }

.drawer-nav { display: grid; gap: var(--sp-1); }
.drawer-nav a {
  display: block;
  padding: var(--sp-3) 0;
  font-family: var(--ff-display);
  font-size: var(--fs-h3);
  color: var(--c-heading);
  text-decoration: none;
  border-bottom: 1px solid var(--c-line);
}
.drawer-nav a:hover { color: var(--c-accent-soft); }

body.is-locked { overflow: hidden; }

/* == Footer =============================================================== */
/*  The footer is the last thing on every page, and it was the flattest: one
 *  solid green, cream at 82% opacity, and column headings in --c-gold, which
 *  on this green measures 3.34:1, a fail for 12px text on all ten pages.
 *  The reference the owner points at spends its richness here, and so does
 *  this now: the non-inverting ground, a gradient that only ever darkens
 *  (--g-ending), a bright-gold hairline along the top, and gold on the
 *  things that organise the footer: headings, rules, icon rims, chips.
 *
 *  It no longer inverts. In dark mode it used to turn into --c-surface, a
 *  card colour, and the page simply stopped rather than ended. The grounds
 *  are the same in both modes, so the footer is too, and every ratio quoted
 *  in this block holds in both.
 *
 *  --c-focus is redefined for this subtree only. The site's forest focus
 *  ring is about 1.5:1 on this green and could not be seen; bright gold is
 *  5.11:1 at the lightest point of the gradient. */
.site-footer {
  position: relative;
  --c-focus: var(--c-gold-bright);
  background: var(--g-ending);
  color: var(--c-on-dark);
  padding-block: var(--sp-8) var(--sp-5);
}
.site-footer::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: var(--g-gold-bright);
  pointer-events: none;
}

.site-footer a { color: inherit; text-decoration: none; }
.site-footer a:hover { color: var(--c-gold-bright); text-decoration: underline; text-underline-offset: 0.25em; }
/*  The header hides the wordmark under 480px because the logo image carries
 *  the name there. The footer has no logo image, so the same rule was
 *  leaving it with no name at all on a phone. */
.site-footer .brand__name { display: block; color: var(--c-on-dark); }
.site-footer .brand__tag { margin-top: var(--sp-1); color: var(--c-gold-bright); }

.footer-grid {
  display: grid;
  gap: var(--sp-7) var(--sp-6);
  grid-template-columns: 1fr;
}
/*  Two columns from 360px, not 620px. At the old breakpoint a 390px phone
 *  got all four footer columns stacked into one 1,151px tower — on every
 *  page, which measured out at 17% of the entire mobile site for nine
 *  facts. Two 165px columns clear the 24px tap-target floor comfortably. */
@media (min-width: 360px)  { .footer-grid { grid-template-columns: repeat(2, 1fr); } }
/*  Below desktop the brand column takes a row of its own, with the wordmark
 *  and the four icons on one line and the sentence under them, instead of
 *  a 150px column of five-word lines beside the shop links. On a phone
 *  "Get in touch" takes a full row too: at body size the email address is
 *  wider than half the screen. On a tablet the three link columns sit side
 *  by side under the brand row, and the legal links run as one line. */
@media (min-width: 360px) and (max-width: 1023px) {
  .footer-grid > .footer-col:first-child {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: var(--sp-4);
  }
  .footer-grid > .footer-col:first-child > p:not(.brand__name) { grid-column: 1 / -1; }
  .footer-grid > .footer-col:first-child .social { grid-column: 2; grid-row: 1; margin-top: 0; gap: var(--sp-2); }
  .footer-col > nav ul { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5); }
}
@media (min-width: 360px) and (max-width: 767px) { .footer-grid > .footer-col:last-child { grid-column: 1 / -1; } }
@media (min-width: 768px)  { .footer-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1024px) { .footer-grid { grid-template-columns: 1.4fr 1fr 1fr 1.2fr; gap: var(--sp-10); } }

/*  A grid item's default min-width is auto, which means it refuses to
 *  shrink below its longest unbreakable word — at 360px that pushed the
 *  footer 5px wider than the screen and the whole page scrolled sideways.
 *  min-width: 0 lets the column be as narrow as its track. */
.footer-col { min-width: 0; }
/*  Eyebrow type, in the one gold that is legible here (6.21:1 on the ground,
 *  5.11:1 at its lightest), over a short gold rule. The heading is what a
 *  reader scans the footer by, so it is the line in each column that should
 *  look placed rather than typed. */
.footer-col h3 {
  font-family: var(--ff-sans);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--c-gold-bright);
  margin-bottom: var(--sp-5);
}
.footer-col h3::after {
  content: '';
  display: block;
  width: var(--sp-6);
  height: 1px;
  margin-top: var(--sp-3);
  background: var(--c-gold-bright);
}
/*  Body size, the reading floor tokens.css sets, at 88% cream: 8.09:1 at the
 *  lightest point of the ground, 7.41:1 with the dark theme's cream. The old
 *  14px at 82% opacity is what made the footer read as grey. overflow-wrap
 *  is insurance for the email address on a very narrow screen; the grid
 *  above already gives it a full row on a phone. */
.footer-col ul {
  display: grid;
  gap: var(--sp-3);
  font-size: var(--fs-body);
  color: color-mix(in srgb, var(--c-on-dark) 88%, transparent);
  overflow-wrap: anywhere;
}
/*  Footer links are text in a list, not standalone controls, so they take the
 *  WCAG 2.5.8 floor of 24px rather than the 44px used for buttons - with the
 *  12px row gap the spacing exception is met twice over. Forcing 44px here
 *  would treble the height of the footer on a phone for no real gain. */
.footer-col li > a { display: inline-flex; align-items: center; min-height: 24px; }
.footer-col p {
  max-width: 34ch;
  font-size: var(--fs-body);
  line-height: var(--lh-copy);
  color: color-mix(in srgb, var(--c-on-dark) 88%, transparent);
}
/*  .footer-col p (0,1,1) was beating .brand__name (0,1,0), so the footer
 *  wordmark rendered at 14px on all ten pages — not a decision, a collision.
 *  It is the same mark as the header and it takes the same size. */
.footer-col p.brand__name { font-size: var(--fs-lead); line-height: var(--lh-tight); opacity: 1; }

/*  Gold-rimmed, and filled gold on hover with the icon in --c-on-gold
 *  (8.40:1, the subscribe button's pair). The rim is 70% bright gold,
 *  3.33:1 on the lightest ground, so the circle reads as a control rather
 *  than a faint outline. */
.social { display: flex; gap: var(--sp-3); margin-top: var(--sp-5); }
.social a {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border: 1px solid color-mix(in srgb, var(--c-gold-bright) 70%, transparent);
  border-radius: var(--r-pill);
  color: var(--c-on-dark);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.social a:hover { background: var(--c-gold-bright); border-color: var(--c-gold-bright); color: var(--c-on-gold); text-decoration: none; }
.social svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.5; }

/*  The same fading gold hairline as the top edge, so the footer is framed
 *  by two matching rules rather than one gold rule and one grey line. */
.footer-bottom {
  position: relative;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--sp-4);
  margin-top: var(--sp-8);
  padding-top: var(--sp-5);
  font-size: var(--fs-small);
  color: color-mix(in srgb, var(--c-on-dark) 80%, transparent);
}
.footer-bottom::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: var(--g-gold-bright);
  opacity: .6;
  pointer-events: none;
}
/*  Payment methods as badges: badge type (micro caps) in full cream inside
 *  a gold rim. They were outlined in 26% of the text colour at 85% opacity,
 *  which made them the faintest thing on the page. */
.pay { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.pay span {
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid color-mix(in srgb, var(--c-gold-bright) 70%, transparent);
  border-radius: var(--r-sm);
  color: var(--c-on-dark);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  line-height: var(--lh-flat);
  text-transform: uppercase;
}

/* == Floating WhatsApp button ============================================= */
/* Renders only when a number is entered in the Customizer. */
.wa-float {
  position: fixed;
  right: var(--sp-4);
  bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-buybar);
  width: 54px; height: 54px;
  display: grid; place-items: center;
  background: var(--c-brand-fill);
  color: var(--c-on-brand);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-lift);
  border: var(--card-border);
  transition: transform var(--dur) var(--ease), background var(--dur) var(--ease);
}
.wa-float:hover { transform: translateY(-2px); background: var(--c-brand-fill-hover); }
.wa-float svg { width: 25px; height: 25px; fill: currentColor; }
/* Steps aside for the sticky buy bar on mobile. */
body.has-buybar .wa-float { bottom: calc(var(--sp-4) + 76px + env(safe-area-inset-bottom, 0px)); }
@media (min-width: 900px) { body.has-buybar .wa-float { bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px)); } }

/* == Leaf cursor ==========================================================
 *  The system pointer is replaced by a leaf that follows the mouse, with a
 *  gold ring trailing a beat behind it.
 *
 *  THE GUARDS MATTER MORE THAN THE EFFECT. Hiding someone's pointer is a real
 *  accessibility risk - people enlarge and recolour it in their OS for a
 *  reason - so it is switched on only where it is safe, by JS adding
 *  .has-leaf-cursor, and never:
 *    - on touch (there is no pointer to replace)
 *    - in Windows high contrast (the OS is being asked to control appearance)
 *    - over text fields (a leaf cannot show you where the caret will land)
 *  With JavaScript off, the class is never added and the native cursor stays.
 *  ---------------------------------------------------------------------- */
.has-leaf-cursor,
.has-leaf-cursor a,
.has-leaf-cursor button,
.has-leaf-cursor summary,
.has-leaf-cursor label,
.has-leaf-cursor select,
.has-leaf-cursor [role="button"] { cursor: none; }

/*  Typing is the one place the native cursor genuinely does a job the leaf
 *  cannot: showing where the caret will land between two characters. */
.has-leaf-cursor input,
.has-leaf-cursor textarea,
.has-leaf-cursor [contenteditable] { cursor: text; }

.cursor {
  position: fixed;
  top: 0; left: 0;
  z-index: var(--z-cursor);
  pointer-events: none;          /* never intercepts a click or a selection */
  opacity: 0;
  transition: opacity 160ms var(--ease);
  will-change: transform;
}
.cursor.is-awake { opacity: 1; }

/*  The ring is hover feedback, not the cursor. At rest it is barely there;
 *  over something clickable it opens up around the leaf. The leaf is the
 *  cursor - that is the whole idea, so the ring must never compete with it. */
.cursor__ring {
  position: fixed; top: 0; left: 0;
  width: 40px; height: 40px;
  margin: -20px 0 0 -20px;
  border: 1.5px solid var(--c-gold-line);
  border-radius: var(--r-pill);
  opacity: 0;
  transform-origin: center;
  transition: width var(--dur-2) var(--ease), height var(--dur-2) var(--ease),
              margin var(--dur-2) var(--ease), opacity var(--dur-2) var(--ease),
              background var(--dur-2) var(--ease);
}
/*  THE LEAF IS THE CURSOR. Big enough to be a shape you recognise rather than
 *  a dot you squint at, tilted like a leaf that has just come off a branch,
 *  and anchored at its tip so the point you are aiming with is the point that
 *  touches things. */
.cursor__leaf {
  position: fixed; top: 0; left: 0;
  width: 30px; height: 30px;
  /*  The reset gives every svg `max-width: 100%`, and .cursor is a zero-width
   *  fixed box — which collapsed the leaf to 0px wide while the ring, a span,
   *  rendered fine. That is why only the ring was showing. */
  max-width: none;
  /*  Anchored near the tip, not the centre: the stem end is where the pointer
   *  actually is, exactly as an arrow cursor points from its own tip. */
  margin: -5px 0 0 -5px;
  fill: var(--c-forest);
  stroke: var(--c-cream);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  paint-order: stroke fill;
  /*  A soft shadow is what stops it looking like a sticker pasted on the page
   *  and makes it read as sitting above the content. */
  filter: drop-shadow(0 2px 4px rgba(38, 55, 11, .40));
  transition: transform var(--dur-3) var(--ease-settle);
  transform-origin: 12% 12%;
}
/* Inverted on dark: a pale leaf with a dark keyline. */
:root[data-theme="dark"] .cursor__leaf { fill: var(--c-leaf); stroke: var(--c-cream); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .cursor__leaf { fill: var(--c-leaf); stroke: var(--c-cream); }
}

/*  Over something clickable the ring appears around the leaf. At rest there is
 *  no ring at all — just a leaf, which is what was asked for. */
.cursor.is-link .cursor__ring {
  width: 54px; height: 54px; margin: -27px 0 0 -27px;
  background: color-mix(in srgb, var(--c-gold) 10%, transparent);
  opacity: .75;
}
.cursor.is-link .cursor__leaf { transform: rotate(-14deg) scale(1.1); }

/* Pressing pulls it in, so a click has a physical answer. */
.cursor.is-down .cursor__ring { width: 34px; height: 34px; margin: -17px 0 0 -17px; opacity: 1; }
.cursor.is-down .cursor__leaf { transform: rotate(-14deg) scale(.88); }

/* Over a text field the leaf gets out of the way of the caret. */
.cursor.is-text { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .cursor__ring, .cursor__leaf { transition: none; }
}

/* == Leaf scroll control ==================================================
 *  Bottom-left, opposite the assistant stack. The gold ring around it fills as
 *  you move down the page, so it doubles as a reading position - and one tap
 *  on the leaf takes you back to the top without reaching for a scrollbar.
 *  It appears only once there is somewhere to go back to.
 *  ---------------------------------------------------------------------- */
.leaf-scroll {
  position: fixed;
  left: var(--sp-4);
  bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-fab);
  width: 52px; height: 52px;
  display: grid; place-items: center;
  background: var(--c-surface);
  border: 1px solid var(--c-line);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-card);
  color: var(--c-accent);
  opacity: 0;
  transform: translateY(10px) scale(.9);
  visibility: hidden;
  transition: opacity var(--dur-3) var(--ease), transform var(--dur-3) var(--ease),
              visibility var(--dur-3), background var(--dur) var(--ease);
}
.leaf-scroll.is-in { opacity: 1; transform: none; visibility: visible; }
.leaf-scroll:hover { background: var(--c-tint); }
.leaf-scroll:hover .leaf-scroll__leaf { transform: translateY(-2px) rotate(-8deg); }
.leaf-scroll:active { transform: scale(.94); }

/* The leaf itself, above the ring. */
.leaf-scroll__leaf {
  position: relative; z-index: 1;
  width: 22px; height: 22px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  transition: transform var(--dur-3) var(--ease);
}

/* The ring: one SVG circle whose dash offset is driven from JS. */
.leaf-scroll__ring { position: absolute; inset: 0; transform: rotate(-90deg); }
.leaf-scroll__track { fill: none; stroke: var(--c-line); stroke-width: 2; }
.leaf-scroll__fill {
  fill: none;
  stroke: var(--c-gold-line);
  stroke-width: 2;
  stroke-linecap: round;
  /* set in JS from the circle's own circumference, so the markup owns the size */
  stroke-dasharray: var(--circ, 157);
  stroke-dashoffset: var(--circ, 157);
  transition: stroke-dashoffset 90ms linear;
}

/* Sits above the sticky buy bar rather than under it. */
body.has-buybar .leaf-scroll { bottom: calc(var(--sp-4) + 76px + env(safe-area-inset-bottom, 0px)); }
@media (min-width: 900px) { body.has-buybar .leaf-scroll { bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px)); } }
/* Out of the way while the assistant has the screen on a phone. */
@media (max-width: 560px) { body.chat-open .leaf-scroll { opacity: 0; visibility: hidden; } }

@media (prefers-reduced-motion: reduce) {
  .leaf-scroll { transition-duration: .001ms; }
  .leaf-scroll__fill { transition: none; }
  .leaf-scroll:hover .leaf-scroll__leaf { transform: none; }
}

/* ==========================================================================
 *  THE STATEMENT — one enormous typographic line per page
 *  ------------------------------------------------------------------------
 *  Cormorant Garamond is a display face and it was never being displayed:
 *  every heading on the site topped out at --fs-h2-lead, so the most
 *  expensive asset in the build (37KB of woff2) rendered at a size where it
 *  is indistinguishable from Georgia. This is the size the face was cut for.
 *
 *  ONE PER PAGE. The rule is not aesthetic prudence, it is what makes it a
 *  statement: two of them on one page is just a heading scale again. There is
 *  a deliberate gap between --fs-h2-lead and --fs-mega with nothing in it, so
 *  a statement can never be mistaken for the next heading down.
 *
 *  It is a <p>, never an <h*>. It carries no outline weight and repeating a
 *  heading level here would break the document's heading order for the sake
 *  of type size — which is exactly the mistake the spec forbids.
 *  ====================================================================== */
.statement {
  font-family: var(--ff-display);
  font-size: var(--fs-mega);
  font-weight: 500;
  line-height: var(--lh-mega);
  letter-spacing: var(--ls-mega);
  color: var(--c-heading-deep);
  text-wrap: balance;
  max-width: 34ch;
}
.statement__line { display: block; }
/*  The gold half. Safe as text because at --fs-mega it is far past the 24px
 *  large-text threshold, where AA asks for 3:1 rather than 4.5:1 — gold on
 *  cream measures 3.23:1. At any smaller size this class would be a bug, so
 *  it is scoped to .statement and nothing else. */
.statement__line--gold {
  color: var(--c-gold-line);
  font-style: italic;
}
.statement--on-dark { color: var(--c-on-dark); }
/*  Centred is available but rationed: a centred statement over a centred
 *  section head is two centred things, which is where "art-directed" turns
 *  back into "template". */
.statement--center { margin-inline: auto; text-align: center; }

/*  A gold hairline that fades at both ends rather than stopping dead at the
 *  column edge. A rule that ends abruptly reads as a border; one that fades
 *  reads as light, which is the whole vocabulary of this brand. */
.hr-gold {
  height: 1px;
  border: 0;
  margin-block: var(--sp-7);
  background: var(--g-gold);
}

/* ==========================================================================
 *  VIDEO — self-hosted, poster-first, and it stops when asked
 *  ------------------------------------------------------------------------
 *  There is no third-party player here on purpose. A YouTube or Vimeo embed
 *  is 500KB+ of someone else's JavaScript, it hands the visitor to another
 *  company's tracking, and it drops a foreign player's chrome into the middle
 *  of the design. The critical-path budget is 90KB and this component adds
 *  ZERO JavaScript to it — the player is the browser's own.
 *
 *  THE POSTER IS THE COMPONENT. It is a real <img>, first in the DOM, with
 *  width and height, so:
 *    - the box is reserved and nothing shifts when the video arrives
 *    - with JavaScript off, or with `preload="none"` never resolving, the
 *      section is a photograph rather than a black rectangle
 *    - under prefers-reduced-motion the <video> is display:none and the
 *      poster is all there is. Motion a viewer did not ask for is the single
 *      most common accessibility failure in this category, and an autoplaying
 *      loop is exactly that.
 *
 *  THE TRAP, written down: `poster` on <video> is NOT enough on its own. A
 *  poster is painted only until the first frame decodes, it is not in the
 *  accessibility tree, and it cannot be given an alt. Hence the real <img>
 *  underneath, which is also the thing prefers-reduced-motion falls back to.
 *  ====================================================================== */
.clip {
  position: relative;
  margin: 0;
  overflow: hidden;
  border-radius: var(--r-lg);
  background: var(--g-well);
  border: var(--card-border);
}
.clip__poster,
.clip__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.clip__video {
  position: absolute;
  inset: 0;
}
/*  Base state: the video is visible, because with no JavaScript a muted
 *  autoplaying loop is the browser's own behaviour and it works. Only once
 *  .js is stamped does the poster take over until playback genuinely starts —
 *  which stops the one-frame black flash that `preload="none"` produces on a
 *  slow connection. */
.js .clip__video { opacity: 0; transition: opacity var(--dur-3) var(--ease-settle); }
.js .clip.is-playing .clip__video { opacity: 1; }

.clip__cap {
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  font-size: var(--fs-small);
  color: var(--c-text-soft);
}

/*  A clip the visitor CHOOSES to play gets real controls and a visible play
 *  affordance of 64px — comfortably past the 44px floor, because it sits over
 *  a photograph where a small control is easy to miss. */
.clip__play {
  display: none;          /* with JS off the native controls are the affordance */
  position: absolute;
  inset: 0;
  place-items: center;
  width: 100%;
  border: 0;
  background: none;
  color: var(--c-on-dark);
}
.js .clip--controls .clip__play { display: grid; }
.clip.is-playing .clip__play { display: none !important; }
.clip__play span {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  border-radius: var(--r-pill);
  /*  Deep green at 88%, not black at 50%: a black scrim over a warm
   *  photograph goes grey and reads as a broken thumbnail. */
  background: color-mix(in srgb, var(--c-ground) 88%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-gold) 55%, transparent);
  box-shadow: var(--shadow-lift);
  transition: transform var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.clip__play svg { width: 24px; height: 24px; fill: currentColor; margin-left: 3px; }
.clip__play:hover span { transform: scale(1.06); background: var(--c-ground); }
.clip__play:active span { transform: scale(.97); }

/*  Ratios. Named rather than inline so a clip cannot arrive with an arbitrary
 *  shape: 4:5 for a product loop beside copy, 9:16 for a phone-shot customer
 *  clip, 16:9 for an unboxing. */
.clip--4x5  { aspect-ratio: 4 / 5; }
.clip--9x16 { aspect-ratio: 9 / 16; }
.clip--16x9 { aspect-ratio: 16 / 9; }
.clip--1x1  { aspect-ratio: 1 / 1; }
/*  With a caption the media keeps its ratio and the caption sits under it, so
 *  the ratio classes go on an inner .clip__media when a caption is present. */
.clip__media { position: relative; display: block; overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  /*  The poster, and nothing else. ui.js also drops the autoplay attribute
   *  and pauses the element, because a media query cannot stop a video that
   *  has already started — but this rule is what guarantees the visual
   *  result even if the script never runs. */
  .clip--ambient .clip__video { display: none; }
  .clip__play span, .clip__play:hover span { transition: none; transform: none; }
}

/* ==========================================================================
 *  WHAT ARRIVES — the unboxing sequence
 *  ------------------------------------------------------------------------
 *  The most persuasive thing a cash-on-delivery shop can show, because the
 *  customer's real question is not "is this nice" but "will a real, sealed
 *  product turn up when I hand over cash". Nothing here may be invented: the
 *  media slots are owner markers until the photographs exist.
 *  ====================================================================== */
.unbox {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-6);
  counter-reset: unbox;
}
@media (min-width: 860px) {
  .unbox { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-7); }
}
.unbox__step { display: grid; gap: var(--sp-4); align-content: start; }
.unbox__media { position: relative; }
.unbox__media img,
.unbox__media .clip { width: 100%; }
.unbox__head { display: flex; align-items: baseline; gap: var(--sp-3); }
.unbox__n {
  flex: none;
  font-family: var(--ff-display);
  font-size: var(--fs-h3);
  font-variant-numeric: tabular-nums;
  color: var(--c-gold-line);
  line-height: var(--lh-flat);
}
.unbox__step h3 { font-family: var(--ff-sans); font-size: var(--fs-h4); font-weight: 500; line-height: var(--lh-snug); color: var(--c-heading-deep); }
/*  The three step descriptions are the section's reading copy (what the
 *  courier brings, what is inside, what you pay), so they are body size.
 *  The closing WhatsApp line under the divider stays small, as a footnote. */
.unbox__step p { font-size: var(--fs-body); color: var(--c-text-soft); }

/*  An owner slot: the honest stand-in for media that does not exist yet.
 *
 *  Deliberately SHORT. The instinct is to reserve the full height the real
 *  photograph will take, but a 400px dashed void reads as a broken page,
 *  and this preview is shown to the owner to be judged. It states what is
 *  missing, at a size that does not pretend to be content. */
.owner-slot {
  display: grid;
  place-items: center;
  gap: var(--sp-3);
  aspect-ratio: 4 / 5;
  max-height: 300px;
  padding: var(--sp-5);
  text-align: center;
  border: 1px dashed color-mix(in srgb, var(--c-gold) 60%, transparent);
  border-radius: var(--r-lg);
  background: var(--g-well);
}
.owner-slot svg {
  width: 30px; height: 30px;
  fill: none; stroke: var(--c-gold-line); stroke-width: 1.4;
}
.owner-slot .verify { margin-left: 0; }
.owner-slot--wide { aspect-ratio: 16 / 9; max-height: none; }

/* ==========================================================================
 *  INGREDIENT SPOTLIGHT
 *  ------------------------------------------------------------------------
 *  Three cards, one per botanical, and each says only WHAT the thing is — a
 *  herb, a seed, a leaf you would recognise from a kitchen. Not one word
 *  about what it does to hair. The bottle's own label makes those claims; the
 *  website deliberately does not repeat them, and that position is the whole
 *  argument of the brand. Provenance instead of efficacy.
 *  ====================================================================== */
.spotlight { display: grid; gap: var(--sp-5); }
@media (min-width: 760px)  { .spotlight { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-6); } }
.spot {
  display: grid;
  align-content: start;
  gap: var(--sp-3);
  padding: var(--sp-6);
  border-radius: var(--card-radius);
  background: var(--g-warm);
  border: var(--card-edge);
  box-shadow: var(--card-shadow);
}
.spot__latin {
  font-family: var(--ff-display);
  font-style: italic;
  font-size: var(--fs-small);
  color: var(--c-gold-line);
}
.spot h3 { font-size: var(--fs-h3); color: var(--c-heading-deep); }
.spot p { font-size: var(--fs-small); color: var(--c-text-soft); }
.spot__local {
  margin-top: var(--sp-2);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--c-text-soft);
}

/* ==========================================================================
 *  EDITORIAL — the full-bleed photographic moment
 *  ------------------------------------------------------------------------
 *  Every section on this site is a contained 1240px column. One full-bleed
 *  image between them is what stops the page reading as one long boxed list,
 *  and it costs nothing: it is an image that was already being loaded, given
 *  the whole width instead of a third of it.
 *
 *  The crop matters more than the size here. object-position is set per
 *  instance, because the label photograph carries the ingredient panel near
 *  its top and the label's own claims lower down — and the site does not
 *  repeat those claims, so they must fall outside the frame.
 *  ====================================================================== */
.editorial {
  position: relative;
  display: grid;
  align-items: end;
  isolation: isolate;
  min-height: 340px;
  background: var(--c-ground);
  color: var(--c-on-dark);
}
.editorial__img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/*  A scrim from the brand's own deep green, weighted to the bottom where the
 *  copy sits. Cream on this measures well past AA at the copy's position; the
 *  gradient is opaque enough at the bottom that it does not depend on which
 *  photograph the owner drops in. */
.editorial::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--c-ground) 18%, transparent) 0%,
    color-mix(in srgb, var(--c-ground) 46%, transparent) 46%,
    color-mix(in srgb, var(--c-ground) 92%, transparent) 100%);
}
.editorial__body { padding-block: var(--sp-10) var(--sp-8); }
.editorial .eyebrow { color: var(--c-gold-line); }
.editorial h2, .editorial p { color: var(--c-on-dark); }
.editorial p { max-width: 48ch; margin-top: var(--sp-4); }
.editorial--tall { min-height: 62svh; }

/* ==========================================================================
 *  FOUNDER'S NOTE
 *  ------------------------------------------------------------------------
 *  The one paragraph on the site a competitor cannot copy, and therefore the
 *  one that must not be written by us. The body is an owner marker on
 *  purpose: an invented founder's story is exactly the class of thing that
 *  had to be stripped out of this site once already.
 *  ====================================================================== */
.founder {
  display: grid;
  gap: var(--sp-6);
  align-items: start;
}
@media (min-width: 860px) {
  .founder { grid-template-columns: 260px 1fr; gap: var(--sp-9); }
}
.founder__body {
  padding-top: var(--sp-6);
  border-top: 1px solid transparent;
  border-image: var(--g-gold) 1;
}
.founder__sign {
  margin-top: var(--sp-5);
  font-family: var(--ff-display);
  font-style: italic;
  font-size: var(--fs-h3);
  color: var(--c-gold-line);
}

/* ==========================================================================
 *  REVIEWS — the whole loop
 *  ------------------------------------------------------------------------
 *  THE PROBLEM THIS SOLVES. Every "Be the first to review this" on this site
 *  was a dead end: it said *be the first* and then offered no way to be. The
 *  shop has no reviews because there was nowhere to write one.
 *
 *  WHAT RENDERS TODAY. The list and the rating summary are `hidden` and carry
 *  no content at all. The empty state and the form render. There is no sample
 *  card, no "—" score and no row of zero bars, because a section whose entire
 *  content is "nobody has bought this" spends prime space telling a stranger
 *  exactly that.
 *
 *  THE WORDPRESS MAPPING, stated so it is mechanical rather than a judgement
 *  call later:
 *    .review-list      <- comments_template(), approved comments only
 *                         (comment_approved = 1; moderation via
 *                         `comment_moderation` so nothing appears unread)
 *    #review-card      <- the markup of one comment row
 *    .badge--verified  <- wc_review_is_from_verified_owner( $comment->comment_ID )
 *    .rating-summary   <- get_average_rating() / get_review_count(), and it
 *                         may be shown ONLY when the count is real and > 0
 *    the form          <- comment_form() posting to wp-comments-post.php
 *  ====================================================================== */

/*  A real review card. Only ever filled from an approved review; the preview
 *  ships the markup inside a <template> so nothing can leak onto the page. */
.review__head { display: grid; gap: var(--sp-2); }
.review__stars { display: inline-flex; gap: 2px; color: var(--c-gold-line); }
.review__stars svg { width: 15px; height: 15px; fill: currentColor; }
.review__star:not(.is-on) svg { fill: none; stroke: currentColor; stroke-width: 1.5; opacity: .5; }
.review__who {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3);
  font-size: var(--fs-small); color: var(--c-text-soft);
}
.review__name { color: var(--c-text); font-weight: 500; }
.review__date { font-variant-numeric: tabular-nums; }
.review__media { margin-top: var(--sp-3); }
.review__media img { border-radius: var(--r-sm); }

/*  Verified buyer. Worth more than any star average, and the one piece of
 *  social proof that cannot be manufactured — WooCommerce knows whether the
 *  reviewer actually bought the item, and nobody else can assert it. */
.badge--verified {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--c-leaf) 22%, transparent);
  color: var(--c-heading);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-btn);
  text-transform: uppercase;
}
.badge--verified svg { width: 13px; height: 13px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; }

/*  The empty state: an invitation, not a claim, and not an apology either. */
.reviews-empty {
  display: grid;
  justify-items: start;
  gap: var(--sp-4);
  padding: var(--sp-6);
  border-radius: var(--card-radius);
  background: var(--g-warm);
  border: var(--card-edge);
  box-shadow: var(--card-shadow);
}
/*  The same padding step as the review form beside it. On a 320px phone the
 *  button's one-line label is wider than the card's inside, so there, and
 *  only there, it may wrap rather than stick out of the card. */
@media (min-width: 640px) { .reviews-empty { padding: var(--sp-7); } }
.reviews-empty .btn { max-width: 100%; white-space: normal; }
.reviews-empty svg.reviews-empty__mark {
  width: 30px; height: 30px;
  fill: color-mix(in srgb, var(--c-gold) 50%, transparent);
  stroke: none;
}
.reviews-empty h3 { font-size: var(--fs-h3); color: var(--c-heading-deep); }
.reviews-empty p { font-size: var(--fs-body); color: var(--c-text-soft); max-width: 52ch; }
.reviews-empty__ask {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  margin-top: var(--sp-2);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--c-line);
  font-size: var(--fs-small);
  color: var(--c-text-soft);
}
.reviews-empty__ask svg { width: 20px; height: 20px; flex: none; margin-top: 2px; fill: none; stroke: var(--c-accent-soft); stroke-width: 1.5; }

/*  Empty state beside the form on desktop, stacked on a phone. The form is
 *  the wider of the two: it is the thing actually being asked for. */
/*  minmax(0, 1fr), not the implicit auto track: on a 320px phone the form's
 *  widest row set the column's minimum and pushed the page 19px wider than
 *  the screen. The column now takes the width it is given. */
.reviews-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-6); align-items: start; }
@media (min-width: 900px) {
  .reviews-grid { grid-template-columns: 0.8fr 1.2fr; gap: var(--sp-8); }
  /*  The confirmation takes the form’s own cell, so revealing it cannot
   *  reflow the empty state beside it. */
  .review-thanks { grid-column: 2; }
}
.review-form h3 { font-size: var(--fs-h3); color: var(--c-heading-deep); }
.rating-summary { display: grid; gap: var(--sp-1); margin-bottom: var(--sp-6); }
.rating-summary__count { font-size: var(--fs-small); color: var(--c-text-soft); }

/* -- the form ------------------------------------------------------------ */
.review-form {
  display: grid;
  gap: var(--sp-5);
  padding: var(--sp-6);
  border-radius: var(--card-radius);
  background: var(--c-surface);
  border: var(--card-edge);
  box-shadow: var(--card-shadow);
}
@media (min-width: 640px) { .review-form { padding: var(--sp-7); } }
.review-form .field { margin-bottom: 0; }
.review-form__grid { display: grid; gap: var(--sp-5); }
@media (min-width: 620px) { .review-form__grid { grid-template-columns: 1fr 1fr; } }

/*  == The star input ==
 *
 *  A real <fieldset> of radio buttons, and that is the whole point. A star
 *  widget built from <div>s and click handlers is the single most commonly
 *  broken control on a beauty site: it cannot be reached by Tab, arrow keys
 *  do nothing, the current value is never announced, and with JavaScript off
 *  it does not exist. Native radios give all of that for free — Tab reaches
 *  the group, arrows move within it, the label is announced, and the value
 *  posts with the form whether or not any script ran.
 *
 *  The inputs are in DOM order 1 to 5 and are NOT display:none — a hidden
 *  input is unfocusable in several browsers, which would silently undo the
 *  keyboard support. They are clipped instead, and the focus ring is drawn
 *  on the label via :focus-visible + label. */
.stars { border: 0; padding: 0; margin: 0; }
.stars > legend {
  padding: 0;
  margin-bottom: var(--sp-2);
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--c-text);
}
.stars__row { display: flex; align-items: center; gap: var(--sp-1); }
.stars input {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
  min-height: 0;                      /* the form reset gives inputs 48px */
  appearance: none;
}
.stars__opt {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;          /* standalone control: the full 44px */
  border-radius: var(--r-sm);
  cursor: pointer;
  color: var(--c-gold-line);
}
.stars__opt svg {
  width: 26px; height: 26px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  transition: fill var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.stars input:focus-visible + .stars__opt {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
}
.stars__opt:hover svg { transform: scale(1.08); }

/*  Fill up to the chosen star. :has() is 94% supported; where it is absent
 *  the chosen star still fills on its own (the rule below), so the control
 *  degrades to "one star marked" rather than to nothing. */
.stars input:checked + .stars__opt svg { fill: currentColor; }
.stars__row:has(#rate-2:checked) .stars__opt:nth-of-type(-n+2) svg,
.stars__row:has(#rate-3:checked) .stars__opt:nth-of-type(-n+3) svg,
.stars__row:has(#rate-4:checked) .stars__opt:nth-of-type(-n+4) svg,
.stars__row:has(#rate-5:checked) .stars__opt:nth-of-type(-n+5) svg { fill: currentColor; }

.stars__out {
  margin-left: var(--sp-3);
  font-size: var(--fs-small);
  color: var(--c-text-soft);
  font-variant-numeric: tabular-nums;
}
/*  With JavaScript off the readout still says what was chosen. Same :has()
 *  caveat: absent support means the <output> is simply empty, which is
 *  cosmetic — the radio itself is what carries the value. */
.stars__row:has(#rate-1:checked) ~ .stars__out::after { content: '1 of 5 — poor'; }
.stars__row:has(#rate-2:checked) ~ .stars__out::after { content: '2 of 5 — fair'; }
.stars__row:has(#rate-3:checked) ~ .stars__out::after { content: '3 of 5 — good'; }
.stars__row:has(#rate-4:checked) ~ .stars__out::after { content: '4 of 5 — very good'; }
.stars__row:has(#rate-5:checked) ~ .stars__out::after { content: '5 of 5 — excellent'; }
.stars__line { display: flex; align-items: center; flex-wrap: wrap; }

/*  == The file picker ==
 *
 *  This is where the customer-media section gets its content, and the moment
 *  someone is writing a review is the only moment they will hand it over.
 *  The native input is clipped and a real <label> carries the styling, which
 *  keeps the click target, the keyboard path and the form posting intact. */
.filepick { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.filepick input[type="file"] {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  min-height: 0;
}
.filepick__btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: 44px;
  padding: var(--sp-2) var(--sp-5);
  border: 1.5px solid var(--c-accent);
  border-radius: var(--r-pill);
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--c-accent);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}
.filepick__btn:hover { background: var(--c-accent); color: var(--c-bg); }
.filepick input[type="file"]:focus-visible + .filepick__btn {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
}
.filepick__btn svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; }
.filepick__name {
  font-size: var(--fs-small);
  color: var(--c-text-soft);
  overflow-wrap: anywhere;
}

.review-form__note {
  padding-top: var(--sp-4);
  border-top: 1px solid var(--c-line);
  font-size: var(--fs-small);
  color: var(--c-text-soft);
}
.review-form__note strong { color: var(--c-text); font-weight: 500; }

/*  The submit button and the JavaScript-off route, one of each and only ever
 *  one of them visible. The .js class is stamped on <html> by the inline
 *  script in every page's <head>, before a stylesheet has painted, so this is
 *  settled before first paint and neither control flashes.
 *
 *  The DEFAULT state is the no-script one. That is deliberate and it is the
 *  rule this project already learned the hard way: eleven homepage sections
 *  once shipped at opacity:0 because the default state assumed the script had
 *  run. A button that opens WhatsApp cannot work without script, so it starts
 *  hidden and script shows it - never the other way round. */
.review-form__jsonly { display: none; }
.js .review-form__jsonly { display: block; }
.review-form__nojs {
  font-size: var(--fs-small);
  color: var(--c-text-soft);
}
.review-form__nojs strong { color: var(--c-text); font-weight: 500; }
.js .review-form__nojs { display: none; }

/*  The confirmation. ONE path reaches it: the submit handler.
 *
 *  It used to have two. The form posted back to #review-thanks and :target
 *  showed this, which meant anyone who followed a link ending #review-thanks
 *  - or pressed Back onto one - was told their review had been sent when
 *  nothing had happened at all. The form no longer posts anywhere, so the
 *  :target rule is gone with it. */
.review-thanks {
  display: none;
  gap: var(--sp-3);
  padding: var(--sp-6);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--c-success) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-success) 40%, transparent);
}
.review-thanks.is-shown { display: grid; }
.review-thanks svg { width: 26px; height: 26px; fill: none; stroke: var(--c-ok); stroke-width: 2; }

/*  == Customer clips ==
 *
 *  The component, not the content. It renders only where a real approved
 *  review has a video attached, exactly as .review-list does — so in this
 *  preview .voices is absent from every page and its markup lives in a
 *  <template>. There is no placeholder person, no stock face and no
 *  fabricated quote anywhere in it, not even marked as an example. */
.voices { display: grid; gap: var(--sp-5); }
@media (min-width: 700px)  { .voices { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1000px) { .voices { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-6); } }
.voice { display: grid; gap: var(--sp-3); }
.voice__who { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); font-size: var(--fs-small); }
.voice__name { font-weight: 500; }

/* == Trust / benefit microlines =========================================== */
/*  Body size. On the category pages each line is a two-sentence
 *  explanation of a product, and on the product pages the three facts sit
 *  straight under a 16px numbered list. Dropping to 14px in either place
 *  made the same column change size halfway down. */
.microline { display: grid; gap: var(--sp-3); font-size: var(--fs-body); color: var(--c-text-soft); }
.microline li { display: flex; align-items: center; gap: var(--sp-3); min-height: 24px; }
.microline svg { width: 18px; height: 18px; flex: none; fill: none; stroke: var(--c-accent-soft); stroke-width: 1.5; }

/* The only spacing utilities in the system - everything else is component CSS. */
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }
.mb-6 { margin-bottom: var(--sp-6); }

/*  A centred action under a centred section head, added 24 Sep 2026.
 *  p carries max-width: var(--measure) (672px) for readable line length, and
 *  a paragraph that holds nothing but a button inherits it: a 672px box
 *  hugging the left of a 1176px wrap. text-align:center then centres the
 *  button inside THAT box, so it landed at x=468 under a section whose
 *  centre is 720 - 252px adrift, and only on desktop, which is why it read
 *  as correct on a phone. A button is not prose and does not want a measure.
 *  This was being done page by page with an inline style="margin-inline:auto";
 *  named here so the next centred action gets it by class. */
.section-action {
  max-width: none;
  margin-inline: auto;
  text-align: center;
}

/*  Marks a claim the owner must verify before launch. Never shown as fact.
 *  Short labels keep to one line, but the text is NOT nowrap: a long note in a
 *  nowrap span is as wide as its sentence, which pushed a whole page sideways
 *  once. Long notes should use --note anyway. */
.verify {
  display: inline;
  margin-left: var(--sp-2);
  padding: 1px var(--sp-2);
  border: 1px dashed var(--c-gold-line);
  border-radius: var(--r-sm);
  font-size: var(--fs-nano);
  letter-spacing: var(--ls-btn);
  line-height: var(--lh-body);
  color: var(--c-gold-line);
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* For a full sentence of instruction to the owner rather than a two-word label. */
.verify--note {
  display: block;
  margin: var(--sp-4) 0 0;
  padding: var(--sp-3) var(--sp-4);
  line-height: var(--lh-body);
}

/* == Notice / empty states ================================================ */
.notice {
  display: flex; gap: var(--sp-4); align-items: flex-start;
  padding: var(--sp-5);
  background: var(--c-tint);
  border-radius: var(--r-md);
  border: var(--card-border);
  color: var(--c-text);
}
.notice svg { width: 20px; height: 20px; flex: none; fill: none; stroke: var(--c-accent); stroke-width: 1.5; margin-top: 3px; }
.notice p { font-size: var(--fs-small); }

/* == Pagination =========================================================== */
.pagination { display: flex; justify-content: center; gap: var(--sp-2); margin-top: var(--sp-9); }
.pagination a, .pagination span {
  min-width: 44px; height: 44px;
  display: grid; place-items: center;
  padding-inline: var(--sp-3);
  border-radius: var(--r-pill);
  font-size: var(--fs-small);
  color: var(--c-text);
  text-decoration: none;
  border: 1px solid transparent;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.pagination a:hover { background: var(--c-tint); }
.pagination [aria-current="page"] { background: var(--c-brand-fill); color: var(--c-on-brand); }

/* == Cross-document view transitions ======================================
 *  Six lines, no JavaScript, and the largest gain in perceived quality per
 *  byte available to a multi-page site: navigating from the shop to a product
 *  morphs rather than blanks. Browsers without support simply load the page as
 *  they always did - there is nothing to fall back to because nothing was
 *  taken away.
 *
 *  Wrapped in no-preference: a page-level crossfade is exactly the kind of
 *  full-viewport movement prefers-reduced-motion exists to stop.
 *  ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }

  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: var(--dur-3);
    animation-timing-function: var(--ease-settle);
  }

  /* The header is the same object on both pages, so it should not cross-fade. */
  .site-header { view-transition-name: site-header; }
  ::view-transition-old(site-header),
  ::view-transition-new(site-header) { animation: none; }
}

/* == Scroll reveal ========================================================
 *  There used to be one behaviour here: fade up, 18px, eleven times down the
 *  page at eleven evenly-spaced scroll positions. That is not "not enough
 *  animation" - it is one animation played eleven times, which reinforces the
 *  metronome the layout already had. More of it would have made things worse.
 *
 *  So: four directions, and a stagger for anything that arrives as a group.
 *  Distance is inversely proportional to how big the thing is - a whole band
 *  moves 10px, a single card moves 28px - because a large element travelling
 *  far reads as sliding furniture rather than settling into place.
 *  ---------------------------------------------------------------------- */
.js .reveal        { opacity: 0; transform: translateY(var(--rise)); }
.js .reveal--sm    { transform: translateY(var(--rise-sm)); }
.js .reveal--lg    { transform: translateY(var(--rise-lg)); }
.js .reveal--left  { transform: translateX(calc(var(--slide) * -1)); }
.js .reveal--right { transform: translateX(var(--slide)); }
.js .reveal--settle{ transform: translateY(var(--rise-sm)) scale(.985); }

.reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-5) var(--ease-settle),
              transform var(--dur-5) var(--ease-settle);
}

/*  A group whose children arrive one after another. Capped at five steps: past
 *  that the last item is visibly queueing, which reads as slow rather than
 *  considered - so a sixth child shares the fifth child's delay. */
.js .reveal-group.is-in > *          { animation: rise-in var(--dur-5) var(--ease-settle) both; }
.reveal-group.is-in > :nth-child(1)  { animation-delay: 0ms; }
.reveal-group.is-in > :nth-child(2)  { animation-delay: var(--stag-1); }
.reveal-group.is-in > :nth-child(3)  { animation-delay: calc(var(--stag-1) * 2); }
.reveal-group.is-in > :nth-child(4)  { animation-delay: calc(var(--stag-1) * 3); }
.reveal-group.is-in > :nth-child(n+5){ animation-delay: calc(var(--stag-1) * 4); }
.js .reveal-group:not(.is-in) > *    { opacity: 0; }

@keyframes rise-in {
  from { opacity: 0; transform: translateY(var(--rise)); }
  to   { opacity: 1; transform: none; }
}

/*  The hero copy arrives as a ladder, one line behind the last. The BOTTLE is
 *  exempt: it is the LCP element, and animating it would mean measuring the
 *  page's largest paint against an element that starts invisible. */
.js .hero .reveal-ladder > * { animation: rise-in var(--dur-5) var(--ease-settle) both; }
.hero .reveal-ladder > :nth-child(1) { animation-delay:  80ms; }
.hero .reveal-ladder > :nth-child(2) { animation-delay: 220ms; }
.hero .reveal-ladder > :nth-child(3) { animation-delay: 360ms; }
.hero .reveal-ladder > :nth-child(4) { animation-delay: 500ms; }
.hero__shot, .hero__media { animation: none !important; opacity: 1 !important; }

/* == Link underline draw ==================================================
 *  A 1px strip growing from the left. Cheap, and it makes every link on the
 *  site feel answered rather than merely coloured. Buttons are excluded -
 *  they already have a fill state. */
.acc__panel a,
.msg__bubble a,
p a:not(.btn):not(.pcard__title a) {
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 1px;
  transition: background-size var(--dur-2) var(--ease);
  padding-bottom: 1px;
}
.acc__panel a:hover,
.msg__bubble a:hover,
p a:not(.btn):hover { background-size: 100% 1px; }

/* == Reduced motion =======================================================
 *  Every animation on the site is wrapped by this. None of it is load-bearing:
 *  turning it off never hides content and never blocks an interaction.
 *
 *  The important line is the one that forces .reveal and every staggered child
 *  to full opacity. A reveal system that leaves content at opacity 0 when
 *  motion is off is not a reduced-motion mode, it is a blank page.
 *  ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .reveal,
  .reveal-group > *,
  .reveal-group:not(.is-in) > *,
  .hero .reveal-ladder > * { opacity: 1 !important; transform: none !important; }

  .pcard:hover { transform: none; }
  .pcard:hover .pcard__media img { transform: none; }
  .btn:active { transform: none; }
  .btn--primary:hover { transform: none; }
  .cat:hover img { transform: none; }
  .iggrid a:hover img { transform: none; }
  .rail { scroll-snap-type: none; }
}

/* == Policy pages ========================================================= 
 *  Shipping, Returns, Privacy, Terms and Contact. Five pages of plain prose,
 *  and until they existed this stylesheet had nowhere to put a readable
 *  column of text: `ul, ol { list-style: none; padding: 0 }` at the top of
 *  this file means a policy list arrives as unindented, unmarked lines that
 *  read as four separate paragraphs rather than one set of terms.
 *
 *  So this is deliberately small: spacing, a soft body colour, and the same
 *  gold dot .acc__panel already draws. IT REUSES THAT DOT ON PURPOSE - a
 *  bullet drawn two different ways on two pages of one site is how a design
 *  stops looking like one system, and these pages sit one click from the FAQ.
 *
 *  It lives in base.css, not shop.css: nothing here belongs to the shop
 *  templates, and a policy page loads the smallest set of stylesheets on the
 *  site. Anything shared by two templates belongs in this file.
 *  ---------------------------------------------------------------------- */
.policy { color: var(--c-text-soft); }
.policy > * + * { margin-top: var(--sp-4); }
/*  A question needs more air above it than the answer below it, or the two
 *  read as one block and the page becomes a wall. */
.policy > h3 { margin-top: var(--sp-7); color: var(--c-text); }
.policy > :first-child { margin-top: 0; }
.policy ul { display: grid; gap: var(--sp-3); }
.policy li { position: relative; padding-left: var(--sp-5); }
.policy li::before {
  content: ''; position: absolute; left: 0; top: .62em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--c-gold-line);
}
.policy strong { color: var(--c-text); }

/*  When each of these was last read through, printed on the page rather than
 *  kept in a commit message. A policy with no date on it is one a reader has
 *  no way to judge. */
.policy-stamp {
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--c-line);
  font-size: var(--fs-small);
  color: var(--c-text-soft);
}

/* == Bundle deals ========================================================
 *  In base.css, not home.css: index.html and shop.html both carry this
 *  section, and a shared component in home.css is the fault that once left
 *  the benefits strip an 874px tower on the shop page because shop.html
 *  never loads that file.
 *
 *  Equal boxes. The three cards hold different numbers of items - two, three
 *  and three - so without this the buttons would sit at three different
 *  heights and the row would read as three unrelated things. The body is a
 *  column, the item list takes the slack, and the call to action is pushed to
 *  the floor of every card.
 *
 *  Colours are all pairs contrast-check.js already measures: the badge is
 *  .badge--best (gold fill, --c-on-gold ink), the struck sum is .price del
 *  (--c-text-soft on --c-surface), the deal price is .price ins
 *  (--c-sale-text), the free-delivery line is --c-ok and the flat-rate line
 *  is --c-text-soft. Nothing here invents a new pair.
 *  --------------------------------------------------------------------- */
.bundles {
  display: grid;
  gap: var(--sp-5);
  grid-template-columns: 1fr;
}
@media (min-width: 720px)  { .bundles { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .bundles { grid-template-columns: repeat(3, 1fr); } }

.bundle {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--c-surface);
  border: var(--card-edge);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
}

/*  The photographs sit on the warm band so the card has a top, the way the
 *  product cards do. They are decorative and aria-hidden: every product is
 *  named in the list below them. */
.bundle__media {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-4);
  background: var(--c-surface-2);
}
.bundle__media img {
  width: 84px;
  height: 84px;
  border-radius: var(--r-sm);
  object-fit: cover;
}
.bundle__plus {
  font-family: var(--ff-display);
  font-size: var(--fs-h4);
  color: var(--c-text-soft);
  line-height: 1;
}

.bundle__body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  padding: var(--sp-5);
}

.bundle__flags { margin-bottom: var(--sp-3); }

.bundle__title {
  font-size: var(--fs-h3);
  color: var(--c-heading-deep);
  line-height: var(--lh-snug);
  margin-bottom: var(--sp-2);
}
.bundle__blurb {
  font-size: var(--fs-body);
  color: var(--c-text-soft);
  line-height: var(--lh-copy);
}

/*  What is in it. Three columns - name, what it is, line price - so the
 *  prices align down the right edge and the sum underneath can be checked by
 *  eye against them. That is the whole point of printing them: a struck-out
 *  total a customer cannot add up is a total they have to take on trust.
 *  flex: 1 makes this the part that absorbs the difference between a
 *  two-item set and a three-item one, so the buttons still line up. */
.bundle__items {
  flex: 1 1 auto;
  display: grid;
  gap: var(--sp-2);
  align-content: start;
  margin: var(--sp-5) 0 0;
  padding: var(--sp-4) 0 0;
  border-top: 1px solid var(--c-line);
  list-style: none;
}
.bundle__items li {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: var(--sp-2);
  min-height: 24px;
  font-size: var(--fs-body);
}
.bundle__items a { color: var(--c-text); text-decoration: none; }
.bundle__items a:hover { text-decoration: underline; }
.bundle__note { font-size: var(--fs-small); color: var(--c-text-soft); }
.bundle__line { color: var(--c-text-soft); font-variant-numeric: tabular-nums; }

.bundle__price {
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--c-line);
  align-items: baseline;
}
.bundle__price ins { font-size: var(--fs-h2-sm); }

.bundle__save {
  margin-top: var(--sp-1);
  font-size: var(--fs-small);
  color: var(--c-sale-text);
}

/*  The delivery line, and on a cash-on-delivery shop it is the most useful
 *  line on the card: it ends on what the courier will actually ask for.
 *  The two hair sets clear Rs 2,500 and say so in --c-ok; the lip trio does
 *  not, and says THAT, in the quieter --c-text-soft. A trio card that let
 *  anyone read free delivery into it would be the same defect as the toner
 *  page's old "this item ships free". */
/*  NOT display:flex, since 24 Sep 2026. The flex was only ever meant to seat
 *  the truck icon beside the sentence, but the <p> holds loose text nodes
 *  either side of a <strong>, and flex makes an anonymous flex item out of
 *  every one of them. The sentence was being dealt into three columns: at
 *  390px "Rs 1,850" got 36px and broke onto two lines, and "to hand the
 *  courier." got 46px and broke onto three. Visible on desktop too.
 *  A hanging indent does the same job with no layout context: the icon sits
 *  on the first line, the negative text-indent pulls that line back to the
 *  content edge, and the padding-left keeps every wrapped line clear of the
 *  icon. The markup is untouched, which matters - it is generated by
 *  tools/sync-bundles.js into six cards across two pages, so a fix that
 *  lives in the stylesheet cannot drift out of sync with the generator. */
.bundle__ship {
  display: block;
  padding-left: calc(16px + var(--sp-2));
  text-indent: calc(-16px - var(--sp-2));
  margin-top: var(--sp-4);
  font-size: var(--fs-small);
  line-height: var(--lh-copy);
  color: var(--c-ok);
}
.bundle__ship--flat { color: var(--c-text-soft); }
/*  display:inline-block undoes the reset at the top of this file
 *  (img, picture, svg, video { display: block }). Left as a block the truck
 *  takes a line of its own above the sentence, which is what the flex was
 *  hiding. It has to share the first line for the hanging indent to read. */
.bundle__ship svg {
  display: inline-block;
  width: 16px; height: 16px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  margin-right: var(--sp-2);
  vertical-align: -3px;
}
.bundle__ship strong { font-weight: 600; }

/*  When the offer runs. Deliberately the quietest line on the card:
 *  --c-text-soft at --fs-small, the same pair as .bundle__line, no colour of
 *  its own, no icon, no box. It is a fact somebody may want, not a prod - the
 *  competitor section this layout came from puts a red ticking clock here.
 *  The date itself is generated from OFFER in tools/bundles.js; nothing in
 *  CSS or markup knows what day it is, and nothing counts down. */
.bundle__until {
  margin-top: var(--sp-2);
  font-size: var(--fs-small);
  color: var(--c-text-soft);
  line-height: var(--lh-copy);
}

.bundle__cta { margin-top: var(--sp-5); }

@media (max-width: 420px) {
  .bundle__media img { width: 68px; height: 68px; }
}
