/*
 * Homepage (Milestone 2A) — Hero, Featured Categories, Best Sellers, Scent Guide, Ingredient
 * Guide, Editorial About, Journal, Gentle Community, Reviews, Newsletter. Gift Sets
 * intentionally not implemented (out of this milestone's scope, TODO.md).
 *
 * Two source-verified visibility utilities (mirrors the Milestone 1 footer pattern — genuinely
 * different content per breakpoint, not a collapsed responsive reflow of the same markup):
 * mobile shows Hero, category chips, Best Sellers (2 products), a short story-teaser quote,
 * and Newsletter only — Scent Guide/Ingredient Guide/Journal/Gentle Community/Reviews and the
 * full Editorial chapter do not appear on any mobile screen in the source.
 */
/* !important is intentional here: a structural show/hide utility must always win over a
   component's own `display` rule regardless of source order (the bug this fixed: a later
   `.category-chips { display: flex }` rule was beating this at equal specificity). */
.homepage-desktop-only { display: none !important; }
.homepage-mobile-only { display: block !important; }
@media (min-width: 1024px) {
  .homepage-desktop-only { display: block !important; }
  .homepage-mobile-only { display: none !important; }
}

.section-gap { margin-bottom: var(--space-6); } /* 24px — within the 22-28px mobile section-gap range, DESIGN_RULES §4 — mobile untouched by both density passes */
.section-gap-sm { margin-bottom: var(--space-5); }
@media (min-width: 768px) {
  /* Correction (2026-07-23): the 2026-07-22 pass over-tightened this to 24-32px, which caused
     headings/media from adjacent sections to visually collide — too far in the other direction.
     This is MAJOR section-to-section rhythm specifically; internal component spacing (card
     gaps, image-to-label gaps, section-header margins etc.) stays as tightened, untouched here.
     Tablet: ~48px (--space-8, already token-anchored, sits in the requested 40-56px range). */
  .section-gap { margin-bottom: var(--space-8); }
}
@media (min-width: 1024px) {
  /* Desktop: ~64-80px, requested explicitly this pass — exceeds DESIGN_RULES §4's documented
     48-56px reference range, which was itself only ever a range for general homepage-block
     spacing, not a hard ceiling; this is a deliberate, explicit, dated override for major
     section rhythm specifically. --space-section-gap-desktop lives in ui-tokens.css (5vw scales
     exactly from 64px@1280 to 80px@1600, per the requested numbers). */
  .section-gap { margin-bottom: var(--space-section-gap-desktop); }
}

/* ================= SectionHeader ================= */
.section-header { margin-bottom: var(--space-5); } /* density pass: was --space-6 */
@media (min-width: 1024px) { .section-header { margin-bottom: var(--space-6); } } /* density pass: was --space-7 */
.section-header__eyebrow {
  font-size: var(--font-size-ui-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}
.section-header--inline .section-header__row { display: flex; justify-content: space-between; align-items: flex-end; }
.section-header--split .section-header__row { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-6); }
.section-header__heading { margin: 0; }
.section-header__link {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-size-nav);
  color: var(--color-background-accent);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--color-background-accent) 35%, transparent);
  padding-bottom: 1px;
  white-space: nowrap;
}
.section-header__link:hover { border-bottom-color: var(--color-background-accent); }

/* Pill variant (Community section CTA, art-direction pass) — filled invitation button instead
   of the quiet underlined "view all" link used everywhere else, since this is the one section
   asking for an ongoing follow rather than a one-off click-through. */
.section-header__link--pill {
  border-bottom: none;
  padding: var(--space-3) var(--space-6);
  background: var(--color-background-accent);
  color: var(--color-text-on-accent);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-button);
  letter-spacing: var(--letter-spacing-button);
  transition: background var(--animation-duration) var(--animation-easing);
}
.section-header__link--pill:hover { background: var(--color-background-accent-hover); }

/* ================= Hero =================
 * Art-direction pass 2026-08-13 (reference: flamingoestate.com) — content moved from a block
 * below the image to an overlay anchored to the image's own bottom-left corner, over a bottom-up
 * gradient scrim for legibility regardless of what the photo looks like underneath. Same markup
 * (.hero__image / .hero__content, unchanged in front-page.php), same content — only position and
 * color treatment changed.
 *
 * Inset-card pass (2026-09-11, explicit request): was a full-bleed, near-full-viewport band
 * (edge-to-edge width, up to 640px tall) that dominated the first screen. Now a large photographic
 * object sitting ON the page's own ivory background instead of overriding it — shorter, inset with
 * real gutters, and rounded. Gutters/margin-top reuse the exact breakpoints .container already
 * uses (base.css) so the hero's left/right edges line up with every section below it; height and
 * radius have no existing token at this scale (the biggest is --radius-large, 12px, sized for
 * small UI panels) so they're set explicitly here, scoped to this component only.
 */
.hero {
  position: relative;
  overflow: hidden;
  margin-top: var(--space-5); /* 20px header-to-hero gap, requested 16-24px range */
  margin-inline: var(--space-5); /* 20px mobile gutter — matches .container's own mobile padding */
  margin-bottom: 40px; /* hero-to-next-section gap, unchanged from the earlier explicit request */
  border-radius: 18px;
}
@media (min-width: 768px) {
  .hero { margin-inline: var(--space-7); border-radius: 22px; } /* 32px tablet gutter */
}
@media (min-width: 1024px) {
  .hero {
    /* Grows to a 60px gutter each side (matches .container's own desktop padding) up to a 1600px
       total width, then just centers with an even larger gutter beyond that — so the hero reads
       as a large inset object rather than sprawling edge-to-edge on ultra-wide monitors. */
    width: calc(100% - 120px);
    max-width: 1600px;
    margin-inline: auto;
    border-radius: 28px;
  }
}
.hero__image .responsive-image { aspect-ratio: 4 / 5; }
@media (min-width: 1024px) {
  /* Was clamp(480px, 42vw, 640px) — a near-full-viewport band. Target height brought down to the
     requested 560-620px range at typical large-desktop widths (1440-1920px) while staying fluid
     rather than a single rigid value. */
  .hero__image .responsive-image { aspect-ratio: auto; height: clamp(480px, 39vw, 620px); }
}
.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top, rgba(20, 18, 14, 0.62) 0%, rgba(20, 18, 14, 0.22) 40%, transparent 65%);
  pointer-events: none;
}
.hero__content {
  position: absolute;
  z-index: 2;
  left: 0; right: 0; bottom: 0;
  padding: var(--space-6) var(--space-5) var(--space-7);
  color: var(--color-text-on-accent);
}
.hero__content .label { color: color-mix(in srgb, var(--color-text-on-accent) 80%, transparent); }
.hero__heading { max-width: 620px; margin-bottom: var(--space-5); color: var(--color-text-on-accent); }
.hero__copy { max-width: 420px; color: color-mix(in srgb, var(--color-text-on-accent) 82%, transparent); margin-bottom: var(--space-5); }
.hero__cta--desktop { display: none; }
.hero__cta--mobile { display: block; margin-bottom: 0; }
@media (min-width: 1024px) {
  .hero__content { padding: var(--space-8) var(--space-9) var(--space-9); }
  .hero__cta--desktop { display: inline-flex; }
  .hero__cta--mobile { display: none; }
}
/* White filled pill CTA, matching the reference's overlay button — the shared .btn--outline
   (dark border + dark text, meant for sitting on the plain cream background) doesn't have
   reliable contrast directly on a photo, so the hero scopes its own treatment here rather than
   changing the shared variant (only used in this one place today, but a future non-overlay use
   of "outline button" shouldn't inherit hero-specific styling). */
.hero .btn--outline {
  background: var(--color-text-on-accent);
  border: none;
  color: var(--color-text-primary);
}
.hero .btn--outline:hover { background: color-mix(in srgb, var(--color-text-on-accent) 88%, transparent); }

/* ================= Brand Statement (2026-09-12, explicit request) =================
 * A large editorial line sitting directly on the page's own ivory background — explicitly NOT a
 * card/banner: no background, border, or shadow of its own. Real bug fixed (2026-09-12, same day
 * follow-up): this used to override `.container`'s own 1280px max-width to a wider 1560px "editorial
 * breakout" — explicit follow-up request corrected this, since it visibly drifted left of Featured
 * Collections/every other `.container` section below it (their shared left edge is the actual
 * requirement, not the hero's own wider one). `.container` alone now provides the max-width/
 * center-alignment/responsive side-padding, identical to every other homepage section;
 * `.brand-statement` only adds the top/bottom margins (their own dedicated tokens, ui-tokens.css —
 * not `.section-gap`, which would add its own margin-bottom on top of this section's already-set
 * one and risk exactly the "enormous empty gap" explicitly ruled out).
 */
.container.brand-statement { margin-top: var(--space-brand-statement-top); margin-bottom: var(--space-brand-statement-bottom); }
/* One <p> (not two), so the italic second line reads as part of the same statement rather than a
   separate paragraph. Explicit follow-up request: both lines now flow as a single centered line
   instead of stacking — `.brand-statement__line2` dropped `display: block` (which existed only to
   force the earlier two-line layout) down to the default `inline`, and the whole line centers
   within `.container`'s own already screen-centered width. */
.brand-statement__text {
  font-family: var(--font-heading);
  font-weight: var(--font-weight-heading);
  line-height: 1.1;
  color: var(--color-text-primary);
  margin: 0;
  text-align: center;
  /* Explicit request: halved from clamp(32px, 8vw, 42px). */
  font-size: clamp(16px, 4vw, 21px);
}
.brand-statement__line2 {
  font-style: italic;
  color: var(--color-background-accent);
}
@media (min-width: 768px) {
  /* Explicit request: halved from clamp(44px, 5vw, 72px). */
  .brand-statement__text { font-size: clamp(22px, 2.5vw, 36px); }
}
@media (max-width: 767px) {
  .container.brand-statement { margin-top: var(--space-brand-statement-mobile); margin-bottom: var(--space-brand-statement-mobile); }
}

/* ================= Featured Categories ================= */
.category-chips { display: flex; gap: var(--space-3); overflow-x: auto; padding-bottom: var(--space-2); }
.category-chips__item {
  flex: none;
  font-size: var(--font-size-nav);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-pill);
  color: var(--color-text-primary);
  text-decoration: none;
}
.category-card { display: block; text-decoration: none; color: inherit; }
/* Editorial pass 2026-08-14 — reverses the 2026-07-22 "reduce category media" pass per new
   explicit direction: large, emotional category tiles (Flamingo Estate reference) instead of
   compact nav tiles. Portrait ratio (was landscape 4/3), soft hover zoom (same slow-duration
   token as product-card image hovers), wider image-to-label gap. */
.category-card .responsive-image { aspect-ratio: 3 / 4; margin-bottom: var(--space-4); }
.category-card .responsive-image img { transition: transform var(--animation-duration-slow) var(--animation-easing); }
.category-card:hover .responsive-image img { transform: scale(1.035); }
.category-card__label {
  font-family: var(--font-heading);
  font-size: var(--font-size-h3);
  color: var(--color-text-primary);
  transition: color var(--animation-duration) var(--animation-easing);
}
.category-card:hover .category-card__label { color: var(--color-background-accent); }

/* Layout correction (2026-08-14): dropped the 5-column 1400px step (matches the product grid's
   own 4-col max, same reasoning — fewer, larger tiles read as curated rather than a dense nav
   row) and widened the gap so the bigger tiles get real room around them. */
.category-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-6); }
@media (min-width: 640px) { .category-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) { .category-grid { grid-template-columns: repeat(4, 1fr); gap: var(--space-7); } }

/**
 * Product grid density (2026-07-22 pass): .grid--cards (base.css) keeps its general-purpose
 * 2-col mobile / 3-col tablet+ behavior for future product grids elsewhere (category archives
 * etc, per DESIGN_RULES §12's frozen "3-col desktop" default) — these extra breakpoints are
 * layered on `.product-grid` specifically (already applied alongside `.grid--cards` in
 * front-page.php) so only the Homepage Best Sellers rail gets denser at wide viewports.
 */
@media (min-width: 1100px) { .product-grid { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 1400px) { .product-grid { grid-template-columns: repeat(5, 1fr); } }

/* ================= Product card (Editorial tile, 2026-09-12) =================
 * Full redesign per an explicit visual reference: flat, no card surface (no background/shadow/
 * padding/rounded corners — the earlier "card-as-surface"/"edge-to-edge" passes are gone), no CTA
 * (the whole card is one click-through <a> to the product page — gb_product_card_static() no
 * longer renders any add-to-cart control at all), portrait image, and a tight left-aligned
 * rating/name/descriptor/price stack directly under it. This collapses what used to be two
 * different tuning models (Homepage's own padded/edge-to-edge card vs. Shop/Related's inset-image
 * card) into one identical flat layout — shop.css/product.css carry their own copies of this same
 * ruleset (matching this file's own established "independent, not shared" convention) rather than
 * each keeping their old, now-obsolete divergent padding models.
 */
.product-card {
  display: grid;
  grid-template-rows: auto auto auto auto auto;
  text-decoration: none;
  color: inherit;
}
/* Vertical-rhythm pass (2026-09-12, explicit request, reference: "Бестселери" screenshot):
   image -> title target is 16-20px total. This margin is the ONLY source of that gap for an
   unrated product (the empty .product-card__rating box below now collapses to zero via :empty),
   and also the gap from image to a real rating row when one is present. */
.product-card__image { position: relative; margin-bottom: var(--space-4); }
/* 4/5 portrait, matching the reference exactly — replaces every earlier pass's aspect-ratio
   (1/1, then 5/4) on this same element. UNSCOPED rule but only ever reaches the Homepage in
   practice (homepage.css isn't enqueued on Shop/Related/Up-Sells) — shop.css/product.css each
   carry their own explicit copy of this same value. */
/* Real bug found live (editorial-tile pass follow-up, 2026-09-12): .responsive-image's own base
   rule (components.css) carries a default border-radius (--radius-medium, 8px) and a fallback
   background color — both meant for components that keep the card-surface look. This card no
   longer has one (the whole point of this pass), so the image needs its corners explicitly
   squared off to match the reference exactly; the earlier edge-to-edge passes did this via a
   `.product-grid`-scoped override that got dropped when that scoping was removed as no longer
   needed — it still needs setting, just unconditionally now. */
.product-card__image .responsive-image { aspect-ratio: 4 / 5; border-radius: 0; background: none; }
/* object-fit: contain — see shop.css's own note on the same rule (one fixed box, image fits inside
   it uncropped, so scale doesn't vary with each PNG's own proportions). */
.product-card__image .responsive-image img { object-fit: contain; object-position: var(--product-card-image-position, center); }
/* Landscape/square source canvas — fit by height instead (see shop.css's note on the same rule). */
.product-card__image--wide .responsive-image img { object-fit: cover; }
.product-card__badge { position: absolute; top: var(--space-3); left: var(--space-3); z-index: 1; transform: scale(0.85); transform-origin: top left; }
.product-card__badge--sold-out { top: var(--space-2); left: var(--space-2); }
.product-card--sold-out .product-card__image .responsive-image { opacity: 0.55; }
.product-card--sold-out .product-card__name,
.product-card--sold-out .product-card__descriptor,
.product-card--sold-out .product-card__price { opacity: 0.55; }
/* Rating zone — real content only: gb_rating() renders nothing at 0 reviews, and this wrapper
   now collapses to zero footprint (:empty) when it does, per the explicit vertical-rhythm
   request that image -> title stay a fixed 16-20px with nothing else contributing to it for an
   unrated product (most of the current real catalog). When a real rating IS present, its own
   height is whatever the star row naturally needs (no forced min/fixed height — text-independent
   in the sense the request cares about: the GAP around it is fixed, not the rare rated-row's own
   content height) plus a small margin down to the title. */
.product-card__rating {
  display: flex;
  align-items: center;
  margin-bottom: var(--space-1);
}
.product-card__rating:empty { display: none; margin: 0; }
/* Real bug fixed live (compaction pass, 2026-09-12, explicit request — user-annotated
   screenshot): the previous `min-height: calc(1.1em * 2)` reserved 2 lines' worth of height on
   EVERY card regardless of actual title length, so a real 1-line name like "Morning" left ~23px
   of dead space below its own text before the descriptor started — that gap, not the (already
   tiny, 2px) margin-bottom, was what read as "too much space" in the screenshot. Dropped the
   min-height entirely; -webkit-line-clamp:2 alone still caps a long name at 2 lines (a 3rd line
   clips rather than growing the card). Trade-off, accepted per this explicit request: a 1-line
   and a genuine 2-line title in the same row will no longer put the descriptor/price at an
   identical Y position — every real product name in the current catalog is short enough to
   render on one line at this card's width, so that case is rare in practice today. */
/* Real bug fixed live (2026-09-15, explicit request — user-annotated screenshot of "Fig Milk"):
   descenders (g/y/p/q/j) were being visibly cropped at the bottom of the title. Root cause,
   confirmed via getBoundingClientRect()/scrollHeight in the live browser: at line-height:1.1, a
   21px title's own line box computed to 23.09px tall, but the text's actual rendered content
   (Lora, this font's descenders in particular) needed ~25px — the ~1.9px shortfall was silently
   sliced off by this same rule's own `overflow: hidden` (required for -webkit-line-clamp to cap
   a genuinely long name at 2 lines; removing it isn't an option). 1.1 was simply too tight for
   this font's descender depth, not a height/overflow misconfiguration otherwise — line-height
   1.2 (21px × 1.2 = 25.2px per line) was the smallest bump that fully cleared it, confirmed by
   re-measuring the same element live with no clipping at 4x zoom. Applies per-line, so a genuine
   2-line title's second line gets the same fix, not just single-line titles. Height impact is
   the minimum necessary: +2.1px per rendered line (so +2.1px for a 1-line name, +4.2px for a
   real 2-line one) — no padding/min-height added, card width/image/descriptor/price/badge
   styling all untouched. */
.product-card__name {
  font-family: var(--font-heading);
  font-size: var(--font-size-product-card-title);
  line-height: 1.2;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* Vertical-rhythm pass (2026-09-12, explicit request): title -> descriptor target is 4-6px —
     back up slightly from an earlier 2px literal to the token step right in that range. */
  margin-bottom: var(--space-1);
}
/* Scent/botanical descriptor ("Матча · лайм · мак") — a short phrase that fits one line at every
   column width this card renders at, so both the reservation and the line-clamp ceiling stay at 1
   line: reserving 2 would leave a permanently near-empty block on every card that doesn't wrap. */
.product-card__descriptor {
  min-height: calc(1.3em * 1);
  font-size: var(--font-size-product-card-descriptor);
  font-weight: var(--font-weight-body);
  color: var(--color-text-muted);
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* Vertical-rhythm pass (2026-09-12, explicit request): descriptor -> price target is 12-16px,
     deliberately a bit more than the 4-6px title -> descriptor gap above. */
  margin: 0 0 var(--space-3);
}
/* Price — one explicit single-line typography rule, not inherited from context: identical height
   whether the text is "450 ₴" or a struck-through "1 200 ₴ 950 ₴", since neither wraps at this
   card's width. Last element in the card now (no CTA below it) — no margin-bottom of its own; the
   grid's own row-gap (front-page.php's `.product-grid`/`.grid--cards`) controls the gap to the
   next row down. */
.product-card__price {
  font-size: var(--font-size-body);
  font-weight: var(--font-weight-emphasis);
  line-height: 1.4;
}
@media (min-width: 1024px) {
  .product-card:hover .responsive-image img { transform: scale(1.02); }
}
/* Art-direction pass: was 200ms — hover-zoom is a purely decorative gesture (product cards,
   category thumbnails), so it gets the slower "gallery pacing" duration; unlike a button/link
   hover, a slightly delayed image zoom doesn't read as unresponsive. */
.responsive-image { transition: transform var(--animation-duration-slow) var(--animation-easing); }
.responsive-image img { transition: transform var(--animation-duration-slow) var(--animation-easing); }

/* Bug fix (2026-07-23): a single full-width 21/9-capped image left the right half of the row
   empty (the image itself doesn't stretch to fill a viewport-wide row on its own) — now two
   equal `.hero-band__panel` columns, edge-to-edge with no gap, each holding its own
   gb_responsive_image('editorial', ...) placeholder (front-page.php). */
/* !important required: `.hero-band` also carries `.homepage-desktop-only`, whose
   `display: block !important` (the established show/hide utility pattern, base.css) otherwise
   beats this plain declaration regardless of source order or specificity. Same justification as
   every other utility-vs-component `!important` conflict already documented in this file. */
.hero-band { display: grid !important; grid-template-columns: 1fr 1fr; gap: 0; }
.hero-band__panel { overflow: hidden; }
/* `.responsive-image` (components.css) carries its own 16/9 aspect-ratio + border-radius — both
   overridden on the inner element (same pattern as the Hero/Scent-Guide fixes elsewhere): a
   fixed clamp(320-400px) height instead of aspect-ratio so the two panels together read as one
   restrained editorial strip, and radius:0 so they sit flush with no rounded-corner gap at a
   true edge-to-edge width. object-fit: cover (already the base `.responsive-image img` rule) is
   preserved for when real photography replaces the placeholder. */
.hero-band__panel .responsive-image {
  aspect-ratio: auto;
  height: clamp(320px, 22vw, 400px);
  border-radius: 0;
}

/* ================= Collection Showcase (generic reusable module, redesigned 2026-09-11) =================
 * Replaces the previous two-static-products-side-by-side layout with an editorial-image + single-
 * product carousel, per explicit request (Apoticari reference, adapted — not copied — to this
 * site's own tokens/typography). Rebuilt cleanly rather than layered on the old CSS: every
 * .homepage-campaign__* rule below is new; the old __grid/__item/__meta rules are gone.
 * Outer container: a CSS Grid stretching both halves to equal height (Grid's own default
 * align-items:stretch — no explicit height needed on either child), one rounded radius + clip so
 * the left photo's corners and the right panel's own background both follow the same shape,
 * matching the rounded-inset-object language already established by the Hero this session.
 */
/**
 * Real bug fixed live (mobile): a plain `1fr` column, like a plain `1fr` row (see the desktop rule
 * below), still respects its content's automatic min-content width — and the carousel's own <img>
 * (.homepage-campaign__slide-image img, width:auto/max-width:100%) contributes its RAW natural
 * pixel width (900px+) to that calculation, since percentage max-width doesn't count for
 * min-content sizing purposes. That inflated the single mobile column to ~595px, overflowing the
 * ~335px content width (clipped by this container's own overflow:hidden, so the whole section
 * rendered visibly shifted/cropped instead of erroring loudly). `minmax(0, 1fr)` removes that
 * automatic floor, same fix as the row-height bug below.
 */
.homepage-campaign {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border-radius: 24px;
  overflow: hidden;
}
@media (min-width: 768px) {
  /**
   * Real bug fixed live: `min-height` alone (even paired with `grid-template-rows: minmax(0,
   * 1fr)`) never gave this grid container a DEFINITE size — per spec, min-height doesn't count as
   * "definite" for resolving `fr` tracks or descendants' percentage heights, so the browser fell
   * back to sizing the row by content instead: the showcase column's text/image stack (a flex:1
   * image frame with no ceiling of its own) inflated the whole section to 1067px, nearly double
   * the intended ≤700px, with min-height doing nothing once content already exceeded it.
   * `height` (not `min-height`) gives the grid an actual definite size, so `minmax(0, 1fr)` +
   * min-height:0 on the showcase/viewport chain below now correctly cap the image frame to
   * whatever's left after the real text content, instead of the reverse.
   */
  .homepage-campaign { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); height: clamp(560px, 55vw, 700px); }
}

/* ---- Left: editorial image + campaign copy ---- */
/* min-height here (not on the desktop rule) is what gives the image something to be 100% OF at
   the mobile/stacked breakpoint, where .homepage-campaign has two auto-sized rows instead of one
   stretched row — 70vw sits in the requested 65-80vw mobile range. Reset to 0 at ≥768px so it
   doesn't fight the grid row's own stretch there (the row's real height already comes from
   grid-template-rows: 1fr above). */
.homepage-campaign__editorial { position: relative; min-width: 0; min-height: 70vw; }
@media (min-width: 768px) {
  .homepage-campaign__editorial { min-height: 0; }
}
/* Fills the grid cell's full stretched height rather than the image's own 4/5 aspect-ratio —
   same technique as .hero__image at desktop (homepage.css, Hero section, above). */
.homepage-campaign__editorial .responsive-image { position: absolute; inset: 0; aspect-ratio: auto; height: 100%; border-radius: 0; }
.homepage-campaign__editorial::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top, rgba(20, 18, 14, 0.65) 0%, rgba(20, 18, 14, 0.15) 45%, transparent 68%);
  pointer-events: none;
}
.homepage-campaign__editorial-copy {
  position: absolute;
  z-index: 2;
  left: 0; right: 0; bottom: 0;
  padding: var(--space-6) var(--space-5) var(--space-7);
  color: var(--color-text-on-accent);
}
@media (min-width: 1024px) {
  .homepage-campaign__editorial-copy { padding: var(--space-7) var(--space-8) var(--space-8); }
}
.homepage-campaign__editorial-copy .homepage-campaign__eyebrow { color: color-mix(in srgb, var(--color-text-on-accent) 80%, transparent); margin-bottom: var(--space-3); }
.homepage-campaign__title { font-size: var(--font-size-h1); line-height: 1.15; max-width: 480px; margin-bottom: var(--space-4); color: var(--color-text-on-accent); }
.homepage-campaign__description { max-width: 400px; font-size: var(--font-size-body); line-height: var(--line-height-body); color: color-mix(in srgb, var(--color-text-on-accent) 85%, transparent); margin-bottom: var(--space-5); }
/* Same overlay-on-photo outline treatment as the Hero's own CTA (homepage.css, Hero section) —
   reused rather than re-invented, since it's the same problem (a button on top of a photo). */
.homepage-campaign__cta-link.btn--outline {
  background: var(--color-text-on-accent);
  border: none;
  color: var(--color-text-primary);
}
.homepage-campaign__cta-link.btn--outline:hover { background: color-mix(in srgb, var(--color-text-on-accent) 88%, transparent); }

/* ---- Right: single-product carousel ---- */
/* min-height: 0 pairs with grid-template-rows: minmax(0, 1fr) above — a grid item also gets an
   implicit min-height:auto floor from its own content by default, so without this the same
   content-driven growth bug could still push the item (and therefore the row) taller than
   intended even with the row track itself fixed. */
.homepage-campaign__showcase {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  background: var(--color-background-card);
  padding: var(--space-6) var(--space-5);
}
@media (min-width: 1024px) {
  .homepage-campaign__showcase { padding: var(--space-8); }
}
/* Same reasoning one level down: a flex item's default min-height:auto would otherwise let the
   track's real content (the slides) push this flex:1 viewport taller than the space actually
   available in the now-fixed-height showcase column. */
.homepage-campaign__viewport { overflow: hidden; flex: 1; min-width: 0; min-height: 0; }
.homepage-campaign__track {
  display: flex;
  height: 100%;
  transition: transform 380ms var(--animation-easing);
}
.homepage-campaign__slide {
  flex: 0 0 100%;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  text-align: center;
}
.homepage-campaign__slide-label { margin-bottom: var(--space-3); }
/* Editorial product showcase, deliberately NOT .product-card: a large fixed frame with
   object-fit:contain (an isolated packshot is allowed to show its own negative space here,
   unlike the dense product grid) on the palette's own warm surface color — never pure white.
   min-width/min-height: 0 here specifically (not just on ancestors) because the <img> inside is a
   replaced element — its raw natural pixel size is what was inflating the mobile column's
   automatic minimum width (see .homepage-campaign's own comment above), and that contribution has
   to be cut off at the flex item actually containing the image, not just further up the chain. */
.homepage-campaign__slide-image {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  min-width: 0;
  min-height: 220px;
  background: var(--color-background-surface);
  border-radius: var(--radius-medium);
  margin-bottom: var(--space-5);
  padding: var(--space-5);
}
.homepage-campaign__slide-image img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }
.homepage-campaign__name {
  display: block;
  font-family: var(--font-heading);
  font-size: var(--font-size-h2);
  color: var(--color-text-primary);
  text-decoration: none;
  margin-bottom: var(--space-2);
}
.homepage-campaign__name:hover { text-decoration: underline; }
.homepage-campaign__descriptor { font-size: var(--font-size-body); color: var(--color-text-muted); margin-bottom: var(--space-3); }
.homepage-campaign__price { font-size: var(--font-size-h3); color: var(--color-text-primary); margin-bottom: var(--space-5); }
.homepage-campaign__cta { min-height: 48px; max-width: 320px; margin-inline: auto; }
.homepage-campaign__cta-form { margin: 0; }
.homepage-campaign [aria-disabled="true"].homepage-campaign__cta { opacity: 0.5; pointer-events: none; }

/* ---- Prev/next + "01 / 04" counter ---- */
.homepage-campaign__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
  margin-top: var(--space-6);
}
.homepage-campaign__arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: var(--border-width) solid var(--color-border-default);
  border-radius: 50%;
  background: none;
  color: var(--color-text-primary);
  font-size: 16px;
  cursor: pointer;
  transition: background var(--animation-duration) var(--animation-easing);
}
.homepage-campaign__arrow:hover { background: color-mix(in srgb, var(--color-text-primary) 6%, transparent); }
.homepage-campaign__counter {
  font-size: var(--font-size-ui-label);
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  min-width: 44px;
  text-align: center;
}

@media (min-width: 768px) {
  .homepage-campaign__slide { text-align: left; }
  .homepage-campaign__slide-label,
  .homepage-campaign__name,
  .homepage-campaign__descriptor,
  .homepage-campaign__price { text-align: left; }
  .homepage-campaign__cta { margin-inline: 0; }
  .homepage-campaign__nav { justify-content: flex-start; }
}

/* ================= Scent Guide =================
   Rebuilt as an interactive tablist (front-page.php + assets/js/scent-guide.js) — every
   fragrance now has its own self-contained panel instead of one shared detail block. Desktop
   spacing/layout widened per explicit request (drifting-center image, cramped rhythm); mobile
   gets a real layout for the first time (horizontal-scroll selector + stacked panel), both a
   deliberate, explicitly-requested exception to the otherwise-frozen homepage design. */
.scent-guide {
  background: var(--color-background-accent);
  color: var(--color-text-on-accent);
  border-radius: var(--radius-large);
  padding: var(--space-6) var(--space-6);
}
/* Compaction pass (2026-09-12, explicit request): the previous clamp(48px, 6vw, 88px) — itself a
   widening of an even earlier 48px fixed value, per that pass's own comment — grew unbounded on
   wide viewports well past a "compact editorial module" feel. Split into its own vertical/
   horizontal clamps landing inside the requested 48-56px / 56-72px ranges instead of one shared
   value drifting up to 88px. */
@media (min-width: 1024px) { .scent-guide { padding: clamp(48px, 4vw, 56px) clamp(56px, 6vw, 72px); } }
.scent-guide__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-6); gap: var(--space-4); }
.scent-guide__eyebrow { color: color-mix(in srgb, var(--color-text-on-accent) 75%, transparent); margin-bottom: 0; }
.scent-guide__link {
  display: inline-flex; align-items: center; gap: var(--space-1);
  font-size: var(--font-size-nav); color: var(--color-text-on-accent);
  border-bottom: 1px solid color-mix(in srgb, var(--color-text-on-accent) 40%, transparent);
  text-decoration: none; white-space: nowrap; flex-shrink: 0;
}

/* ---- Fragrance selector (role="tablist") ---- */
/* Mobile: horizontal scroll (new) — was previously omitted from mobile entirely. */
.scent-guide__families {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  gap: var(--space-5);
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-2); /* room for the scrollbar/snap without clipping the dot ring */
  /* Edge breathing room (visual-polish pass): without this, the first/last item's auto-centered
     scroll position has nowhere left to scroll to, so it ends up flush against the container edge
     — reads as clipped even though no text is actually cut off by CSS. scroll-padding-inline gives
     scrollIntoView({inline:'center'}) room to keep any active item clear of the hard edge. */
  scroll-padding-inline: var(--space-6);
  padding-inline: var(--space-2);
}
.scent-guide__families::-webkit-scrollbar { height: 4px; }
@media (min-width: 768px) {
  .scent-guide__families {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    overflow-x: visible;
    gap: var(--space-4);
    margin-bottom: var(--space-7);
  }
}
@media (min-width: 1024px) {
  /* Compaction pass (2026-09-12, explicit request): the 56-72px range from the previous widening
     pass overshot the newly-requested 40-50px target — reduced to match. */
  .scent-guide__families { margin-bottom: clamp(40px, 4vw, 50px); }
}
/* Selector items are real <button>s now (accessibility fix — native Enter/Space/focus) —
   reset browser button chrome so they read exactly as the old <div>-based dots did. */
.scent-guide__family {
  flex: 0 0 auto;
  scroll-snap-align: center;
  text-align: center;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  display: block;
}
/* Real bug, found via visual review: Chromium treats a <button> as deserving a visible focus
   ring even after an ordinary mouse click, not just keyboard nav, so this rule alone kept
   showing a rectangular ring on whichever fragrance was last clicked — reading as a stray
   debug/browser outline rather than the intended "active" state. Fixed in scent-guide.js: a
   mouse-originated click blurs the button right after activating it (so no element is focused,
   and this rule has nothing to apply to), while a real keyboard activation (Enter/Space, or
   Arrow-key nav) leaves focus in place, so the ring still appears exactly when a keyboard user
   needs it. */
.scent-guide__family:focus-visible { outline: 2px solid var(--color-text-on-accent); outline-offset: 4px; border-radius: var(--radius-small); }
/* Compaction pass (2026-09-12, explicit request): circle 38px -> 44px (target 42-48px range —
   slightly larger, not smaller, since 38px undershot it); label font-size bumped from the 11px
   --font-size-ui-label to the 12px --font-size-nav token, landing inside the requested 12-13px
   range instead of introducing a new one-off value. */
.scent-guide__dot { display: inline-block; width: 44px; height: 44px; border-radius: 50%; margin-bottom: var(--space-2); transition: border-color var(--animation-duration) var(--animation-easing); }
.scent-guide__family.is-active .scent-guide__dot { border: 2px solid var(--color-text-on-accent); }
.scent-guide__family-name { display: block; font-size: var(--font-size-nav); color: color-mix(in srgb, var(--color-text-on-accent) 75%, transparent); white-space: nowrap; }
/* Active state (persistent, mouse or keyboard) is the white dot ring above plus this — text
   color, a subtle underline, and heavier weight. Deliberately not a box/outline of its own, so
   it never reads like a focus ring. */
.scent-guide__family.is-active .scent-guide__family-name {
  color: var(--color-text-on-accent);
  font-weight: var(--font-weight-emphasis);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- Panels (role="tabpanel", one per fragrance) ---- */
.scent-guide__panels { position: relative; }
/* Real bug, found via live browser inspection: the browser's own default `[hidden] { display:
   none }` rule lives in the UA stylesheet, which always loses to an author-stylesheet rule of
   equal specificity (here, `.scent-guide__panel { display: grid/flex }` below, and its
   mobile-breakpoint variant) — so every "inactive" panel was still laid out and visible, panels
   stacking one under another and the section growing to ~3200px tall. This rule wins regardless
   of which display value the component rules below set, at any breakpoint. */
.scent-guide__panel[hidden] { display: none !important; }
/* page-fragrance-guide.php shows every family stacked and always visible (no [hidden] tab-switch
   like the homepage teaser), so — unlike the homepage, where only one panel is ever laid out at a
   time — consecutive panels here need their own gap. */
.page-fragrance-guide__entry + .page-fragrance-guide__entry {
  margin-top: var(--space-8);
  padding-top: var(--space-8);
  border-top: 1px solid color-mix(in srgb, var(--color-text-on-accent) 20%, transparent);
}
.scent-guide__panel {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6); /* mobile: "generous spacing" per explicit request, no exact figure given */
  /* Switch transition (assets/js/scent-guide.js toggles .is-entering) — the sitewide
     prefers-reduced-motion rule (base.css) already zeroes this duration when requested, so no
     separate reduced-motion override is needed here. */
  transition: opacity var(--animation-duration) var(--animation-easing);
}
.scent-guide__panel.is-entering { opacity: 0; }
@media (min-width: 768px) {
  /* Text/image split — was 45fr/55fr on the old single shared block, kept here. */
  .scent-guide__panel { grid-template-columns: 45fr 55fr; align-items: start; gap: var(--space-7); }
  /* A fragrance with no uploaded image (e.g. not yet filled in by an editor) omits the whole
     .scent-guide__panel-image element (front-page.php) rather than rendering an empty grey
     placeholder box — so the text column takes the full row instead of leaving a blank column. */
  .scent-guide__panel--no-image { grid-template-columns: 1fr; }
}
@media (min-width: 1024px) {
  /* Compaction pass (2026-09-12, explicit request): the 64-120px range overshot the newly-
     requested 56-72px target (also matching this same request's column-gap wording). */
  .scent-guide__panel { gap: clamp(56px, 6vw, 72px); }
}
/* Compaction pass (2026-09-12, explicit request): this now renders the fragrance's real NAME
   (front-page.php/page-fragrance-guide.php — see gb_get_scent_guide()'s own comment for the real
   bug this fixes), so it needs its own explicit size/line-height rather than inheriting the
   generic h2 default (--font-size-h2, 38px/1.08) — landing inside the requested 40-46px/1.0-1.08
   ranges. margin-bottom tightened to the new descriptor line right underneath it, not the old
   long-form copy. */
.scent-guide__detail-heading { color: var(--color-text-on-accent); font-size: clamp(36px, 3vw, 44px); line-height: 1.05; margin-bottom: var(--space-2); }
/* New element (2026-09-12, explicit request) — the short sensory phrase ("green fig · rice milk ·
   creamy woods") that used to wrongly BE the heading above; every one of the 8 real fragrance
   rows already had this exact content in the ACF field now feeding it (see gb_get_scent_guide()),
   so no new content was needed, only the correct secondary treatment: smaller, italic, muted. */
.scent-guide__detail-descriptor {
  font-family: var(--font-heading);
  font-style: italic;
  font-size: var(--font-size-article-lead);
  line-height: 1.3;
  color: color-mix(in srgb, var(--color-text-on-accent) 80%, transparent);
  max-width: 380px;
  margin-bottom: var(--space-4);
}
/* Compaction pass (2026-09-12, explicit request): margin-bottom tightened now that the actual
   ACF content behind this (gb_get_scent_guide()'s `description`) is a short 2-3 line sensory
   intro, not the 4-6 paragraph essay it had drifted to per-fragrance — see each row's own content
   update. This rule itself only needed a smaller gap to the notes below, not a type change. */
.scent-guide__detail-copy { color: color-mix(in srgb, var(--color-text-on-accent) 85%, transparent); max-width: 380px; margin-bottom: var(--space-4); }
/* Compaction pass (2026-09-12, explicit request): font-size bumped from --font-size-meta (12px)
   to the requested 14-15px (no existing token at that size, so a literal value here); line-height
   tightened 1.5 -> 1.4. Row-to-row spacing (6px, div+div) was already inside the requested 6-8px
   range — untouched. */
.scent-guide__notes { font-size: 14px; color: color-mix(in srgb, var(--color-text-on-accent) 75%, transparent); line-height: 1.4; }
.scent-guide__notes div { margin-bottom: 6px; }
.scent-guide__notes div:last-child { margin-bottom: 0; }
.scent-guide__notes strong { color: var(--color-text-on-accent); }
/* Compaction pass (2026-09-12, explicit request): explicit min-height/padding instead of relying
   on font-size + --space-3 padding to land near 44-46px "by accident" — a bit under target before
   (~36px tall). Horizontal padding widened slightly (--space-5/20px -> --space-6/24px, inside the
   requested 22-26px range); still the same 1px outline/pill shape, no "large pill" introduced. */
.scent-guide__cta {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; margin-top: var(--space-6);
  font-size: var(--font-size-nav); color: var(--color-text-on-accent);
  border: 1px solid var(--color-text-on-accent); border-radius: var(--radius-pill);
  padding: 0 var(--space-6); text-decoration: none;
}
/* Image column — a real fluid width, anchored to the column's trailing edge so it can never drift
   toward center. object-fit: cover preserved via the shared base `.responsive-image img` rule
   (components.css), unchanged. Compaction pass (2026-09-12, explicit request): width clamp
   nudged toward the requested 500-560px range (was 430-500px); corner radius made explicit at
   16px (was inheriting the base component's 8px --radius-medium, under the requested 14-18px
   range) — a scoped override, not a change to that shared token/component. */
.scent-guide__panel-image .responsive-image { aspect-ratio: 4 / 3; width: 100%; border-radius: 16px; }
@media (min-width: 1024px) {
  .scent-guide__panel-image { display: flex; justify-content: flex-end; }
  .scent-guide__panel-image .responsive-image { width: clamp(460px, 38vw, 540px); margin-inline-start: auto; }
}

/* ---- Mobile-only: reorder panel content Selector → Heading → Descriptor → Description → Notes →
   Image → CTA (the selector itself is already a separate, preceding element). `display: contents`
   lifts .scent-guide__panel-text's children to become direct flex items of .scent-guide__panel, so
   each can be reordered individually without touching the markup. Compaction pass (2026-09-12,
   explicit request): added order:2 for the new .scent-guide__detail-descriptor element, shifting
   the rest down by one. ---- */
@media (max-width: 767px) {
  .scent-guide__panel { display: flex; flex-direction: column; }
  .scent-guide__panel-text { display: contents; }
  .scent-guide__detail-heading { order: 1; }
  .scent-guide__detail-descriptor { order: 2; }
  .scent-guide__detail-copy { order: 3; }
  .scent-guide__notes { order: 4; }
  .scent-guide__panel-image { order: 5; }
  .scent-guide__cta { order: 6; margin-top: 0; }
}

/* ================= Ingredient Guide ================= */
/* Density pass (2026-07-22): was a fixed 3-col — now 3/4/5 col per the requested wide-desktop
   behavior (5 fixture items added to dev-fixtures.php to actually exercise the 5-col row). */
.ingredient-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-6) var(--space-5); }
@media (min-width: 768px) { .ingredient-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) { .ingredient-grid { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 1400px) { .ingredient-grid { grid-template-columns: repeat(5, 1fr); } }
/* Layout correction (2026-07-23): min-width:0 overrides the grid item's default min-width:auto,
   which was letting long ingredient names/descriptions push wider than the card's own column
   track (and therefore wider than the image above them) instead of wrapping. */
.ingredient-grid__item { padding-inline: 0; border-left: none; width: 100%; min-width: 0; }
@media (min-width: 768px) {
  .ingredient-grid__item { padding-inline: var(--space-5); border-left: var(--border-width) solid var(--color-border-default); }
  .ingredient-grid__item:first-child { padding-left: 0; border-left: none; }
}
/* border-left rhythm breaks with a 4/5-col wrap (every Nth item would need it removed too) —
   simplest correct fix at this density is to drop the divider and rely on the grid gap alone. */
@media (min-width: 1100px) {
  .ingredient-grid__item { border-left: none; padding-inline: 0; }
}
/* Layout correction (2026-07-23): the 2026-07-22 density pass shrank this to 110px (was 150px),
   which left the image too small to visually anchor the card — raised back up on desktop only,
   mobile/tablet stay compact. */
/* Typography/alignment pass (2026-09-12, explicit request): margin-bottom widened --space-3 (12px)
   -> --space-5 (20px), landing inside the requested 18-20px image->title gap. Image height/crop
   themselves untouched. */
.ingredient-grid__image { height: 130px; margin-bottom: var(--space-5); }
@media (min-width: 1024px) { .ingredient-grid__image { height: 180px; } }
.ingredient-grid__image .responsive-image { height: 100%; }
/* Typography correction (2026-09-12, same-day follow-up, explicit request): the prior version of
   this rule below (clamp(24px, 2.2vw, 30px) + min-height: 64px) was a real regression, not a
   refinement — it rendered at a computed 30px on desktop (confirmed live) instead of the intended
   24px, and min-height:64px reserved a full two-line-tall box under every title, so a one-line
   name like "Масло ши" left ~30px of dead air before its description while a two-line name like
   "Мацерат календули" did not — the opposite of "compact and aligned". That whole approach is
   reverted here: flat 24px (no clamp — nothing about this component needs to grow past 24px, so
   there's no fluid range to express), line-height tightened from the browser's serif default
   (~1.7, i.e. ~40.8px on a 24px title — the real cause of the original "loose two-line title"
   complaint from the pass before that) down to the requested 1.05, and no min-height at all — the
   title box is exactly as tall as its own text, one line or two, so a description directly follows
   it with no reserved dead space either way (see DESIGN NOTE below on why per-card alignment is
   deliberately not attempted anymore).
   DESIGN NOTE: perfect cross-card title-bottom alignment (every description starting at the exact
   same Y) is NOT attempted here — that requires reserving a fixed/minimum title height, which is
   exactly the "obvious blank gap" this correction was asked to remove. A compact layout with
   naturally-sized titles and small, real per-card height differences reads calmer than a uniform
   grid with visible padding manufactured to force alignment.
   width/min-width/overflow-wrap/word-break kept from the prior pass — still correct defensive
   wrapping on the element that actually holds the text, unrelated to the size/spacing bug above. */
.ingredient-grid__name {
  font-family: var(--font-heading);
  font-size: 24px;
  line-height: 1.05;
  margin-bottom: var(--space-3);
  width: 100%;
  min-width: 0;
  max-width: 100%;
  overflow-wrap: normal;
  word-break: normal;
}
/* Typography pass: font-size bumped 12px -> 14px (no existing token between --font-size-meta/12px
   and --font-size-body/16px, so a literal value here, same convention as other one-off compaction
   fixes in this codebase) and line-height tightened 1.6 -> 1.4, both landing inside the requested
   14-15px / 1.35-1.45 ranges — "Заспокоює подразнення..." reads as a compact two-line caption now
   instead of loose paragraph-style spacing. */
.ingredient-grid__desc {
  font-size: 14px;
  color: var(--color-text-muted);
  line-height: 1.4;
  width: 100%;
  min-width: 0;
  max-width: 100%;
  overflow-wrap: normal;
  word-break: normal;
}

/* ================= EditorialChapter ================= */
/* Layout correction (2026-07-23): was align-items:center, which could vertically center the
   text column against a taller image and clip/push it below the viewport at some heights —
   start-aligned so the text column always renders from the top. */
.editorial-chapter { display: grid; grid-template-columns: 1fr; gap: var(--space-6); align-items: start; }
@media (min-width: 768px) { .editorial-chapter { grid-template-columns: 1fr 1fr; } }
.editorial-chapter--image-right { direction: rtl; }
.editorial-chapter--image-right > * { direction: ltr; }
/* Density pass: was 4/5 (tall portrait, close to a full-screen blank panel at large widths) —
   shorter ratio + cap so image and text read as one composition instead of two stacked blocks. */
.editorial-chapter__image .responsive-image { aspect-ratio: 4 / 3; max-height: 420px; }
.editorial-chapter__eyebrow { font-size: var(--font-size-ui-label); letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: var(--space-3); }
.editorial-chapter__heading { font-size: 32px; line-height: 1.2; max-width: 420px; margin-bottom: var(--space-4); } /* source: 34px — clamped to the frozen H2 26-32px ceiling, DESIGN_RULES §3 */
.editorial-chapter__copy { max-width: 400px; color: var(--color-text-muted); margin-bottom: var(--space-5); } /* density pass: was --space-6, cuts empty space below the link */
.editorial-chapter__link { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--font-size-button); color: var(--color-background-accent); border-bottom: 1px solid color-mix(in srgb, var(--color-background-accent) 35%, transparent); text-decoration: none; }

.story-teaser { text-align: center; padding: var(--space-6) var(--space-5); }
.story-teaser__eyebrow { margin-bottom: var(--space-2); }
.story-teaser__quote { font-family: var(--font-heading); font-style: italic; font-size: 17px; line-height: 1.5; color: var(--color-text-primary); margin: 0; }

/* ================= Journal ================= */
/* Layout correction (2026-07-23): .journal-grid was previously the shared .grid--2 utility
   (fixed 2-col, stretched to fit only 2 fixture articles) — now its own breakpoints, paired
   with the 4-article fixture set (dev-fixtures.php) so wide desktop fills a real 4-col row
   instead of 2 oversized cards. */
.journal-grid { grid-template-columns: repeat(1, 1fr); gap: var(--space-6); }
@media (min-width: 640px) { .journal-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px) { .journal-grid { grid-template-columns: repeat(4, 1fr); } }
.journal-card { display: block; text-decoration: none; color: inherit; min-width: 0; }
.journal-card__image { margin-bottom: var(--space-4); }
/* Density pass: capped so cards stay editorial without nearing a full viewport. */
.journal-card__image .responsive-image { aspect-ratio: 4 / 3; max-height: 300px; }
.journal-card__category { font-size: var(--font-size-ui-label); letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: var(--space-2); }
/* max-width:100% + overflow-wrap: title must never exceed the card/image width above it —
   without min-width:0 on the grid item (added above), long titles could push the column wider
   than its track instead of wrapping. */
.journal-card__title { font-family: var(--font-heading); font-size: 24px; line-height: 1.25; max-width: 100%; overflow-wrap: break-word; }

/* ================= Gentle Community ================= */
/* Layout correction (2026-07-23): .community-grid was previously the shared .grid--4 utility
   (fixed 4-col desktop, forcing exactly 4 fixture items to stretch full-width) — now its own
   breakpoints matching Categories, reaching 5 cols at ≥1400px with the 5th fixture item added
   (dev-fixtures.php) so the row distributes evenly instead of stretching. */
.community-grid { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 640px) { .community-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) { .community-grid { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 1400px) { .community-grid { grid-template-columns: repeat(5, 1fr); } }
/* Density pass: was 4/5 (tall portrait) — much shorter so the row reads as a compact
   social gallery strip, not a full-page image wall. */
.community-grid__image .responsive-image { aspect-ratio: 1 / 1; max-height: 220px; margin-bottom: var(--space-2); }
.community-grid__caption { font-size: var(--font-size-ui-label); color: var(--color-text-muted); }

/* ================= Reviews ================= */
.reviews-rail {
  display: flex;
  gap: var(--space-6);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  padding-bottom: var(--space-2);
}
.reviews-rail::-webkit-scrollbar { display: none; }
.review-card {
  flex: 0 0 auto;
  width: 85%;
  scroll-snap-align: start;
}
@media (min-width: 768px) { .review-card { width: calc(33.333% - var(--space-6)); } }
.review-card__stars { color: var(--color-accent-terracotta); font-size: var(--font-size-body); margin-bottom: var(--space-4); }
.review-card__quote { font-family: var(--font-heading); font-style: italic; font-size: 19px; line-height: 1.45; margin: 0 0 var(--space-4); }
.review-card__author { font-size: var(--font-size-meta); color: var(--color-text-muted); }

/* ================= Newsletter section (Community Sign-up) ================= */
/* Layout correction (2026-07-23): background stays full-bleed on .newsletter-section (matches
   the approved source's edge-to-edge olive panel), but the actual grid/content now lives in
   .newsletter-section__inner, capped to the same max-width as .container (--breakpoint-xl) and
   centered — so the heading/form left edge lines up with every other section's content instead
   of drifting wider at viewports above 1280px. */
.newsletter-section {
  background: var(--color-background-accent);
  color: var(--color-text-on-accent);
}
.newsletter-section__inner {
  display: grid;
  grid-template-columns: 1fr;
  max-width: var(--breakpoint-xl);
  margin-inline: auto;
}
@media (min-width: 1024px) {
  /* 45/55 split (image/content) — was an even 1fr 1fr; narrower image column both reduces the
     image's rendered size and gives the form more room to breathe. Height capped to the
     requested 420-480px band; the image/content padding reductions below keep natural content
     height inside that cap without clipping text. */
  .newsletter-section__inner { grid-template-columns: 45fr 55fr; align-items: stretch; max-height: 480px; min-height: 420px; }
}
.newsletter-section__image { position: relative; padding: var(--space-5); max-height: 420px; }
.newsletter-section__image .responsive-image { aspect-ratio: 4 / 3; height: 100%; max-height: 380px; }
.newsletter-section__content {
  padding: var(--space-7) var(--space-5);
  display: flex;
  flex-direction: column;
  justify-content: center; /* vertically centers the form content within the panel */
}
@media (min-width: 1024px) { .newsletter-section__content { padding: var(--space-7) var(--space-9); } }
.newsletter-section__eyebrow { letter-spacing: 0.14em; color: color-mix(in srgb, var(--color-text-on-accent) 75%, transparent); margin-bottom: var(--space-3); }
.newsletter-section__heading { color: var(--color-text-on-accent); font-size: 32px; line-height: 1.25; margin-bottom: var(--space-3); }
.newsletter-section__copy { color: color-mix(in srgb, var(--color-text-on-accent) 85%, transparent); max-width: 400px; margin-bottom: var(--space-6); }
/* Reuse the shared NewsletterForm primitive, recolored for this dark section only. */
/**
 * Component: NewsletterForm (.newsletter-form) — base structural rules.
 * Bug fix 2026-07-22 (Milestone 2A QA): these rules used to live in footer.css, but the
 * Milestone-1 "align global components" footer rebuild removed the newsletter form from the
 * global footer entirely and deleted this CSS block along with it, without relocating it to
 * where the component is actually still used (Homepage only, PROJECT.md §5). Restored here —
 * this is now the sole location for these rules. Caught via a stray visible "Дякуємо за
 * підписку!" success message in live QA (should be hidden by default).
 */
.newsletter-form { display: flex; flex-direction: column; gap: var(--space-3); max-width: 420px; }
/* Layout correction (2026-07-23): gap raised from --space-2 (8px) to --space-3 (12px), inside
   the requested 12-16px range. Input flexes to fill the row; the button (.btn) has no flex
   rule so it keeps its intrinsic width; both share the same field/button height token already
   defined on .field__input/.btn, so no extra height rule was needed here. */
.newsletter-form__row { display: flex; gap: var(--space-3); }
.newsletter-form__row .field { flex: 1; }
.newsletter-form__note { font-size: var(--font-size-meta); color: var(--color-text-muted); }
.newsletter-form.is-success .newsletter-form__row { display: none; }
.newsletter-form__success-msg { display: none; color: var(--color-success); }
.newsletter-form.is-success .newsletter-form__success-msg { display: block; }

.newsletter-section .newsletter-form { max-width: 420px; }
/* Explicit request: input + button read as two disconnected pieces (separate pill outlines,
   different heights — 46px vs 54px, since the button's own outline padding grows past its
   44px floor while the input's doesn't) — fused into one continuous pill: a single shared
   border/radius on the row, both children stretched to one explicit height, a thin divider
   (not a full border) where the button meets the input instead of a gap.
   Real bug, found live along the way: Storefront's own core `input[type="email"]` rule
   (style.css) sets `border: 0px` at (0,1,1) specificity (type + attribute selector) — higher
   than this component's own base `.field__input { border: ... }` at (0,1,0), so it silently won
   that fight site-wide on every plain .field__input. Moot for the input itself now (border lives
   on the row instead), but is why the row's own border below uses the full shorthand rather than
   just `border-color`. */
.newsletter-section .newsletter-form__row {
  display: flex;
  align-items: stretch;
  gap: 0;
  height: 52px;
  border: var(--border-width) solid color-mix(in srgb, var(--color-text-on-accent) 45%, transparent);
  border-radius: var(--radius-pill);
  overflow: hidden;
  transition: border-color var(--animation-duration) var(--animation-easing), box-shadow var(--animation-duration) var(--animation-easing);
}
.newsletter-section .newsletter-form__row:hover { border-color: color-mix(in srgb, var(--color-text-on-accent) 70%, transparent); }
/* Focus-within so the whole bar lights up whichever child (input or button) actually has focus —
   a single shared indicator keeps it reading as one object instead of two separately-focusable
   ones. No browser-default outline on either child (below); this ring is the only indicator. */
.newsletter-section .newsletter-form__row:focus-within {
  border-color: var(--color-text-on-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-text-on-accent) 20%, transparent);
}
/* Error state — dormant until real validation JS toggles .is-error on .field (see
   newsletter-form.php); styled now so it's correct the moment that lands. The base --color-error
   red read too alarming against this dark botanical-green section for a simple format check, so
   this scopes to the same muted terracotta already used elsewhere in the palette. */
.newsletter-section .newsletter-form__row:has(.field.is-error) { border-color: var(--color-accent-terracotta); }
/* .field__error now lives outside .newsletter-form__row (see newsletter-form.php) so it isn't
   clipped by the row's own overflow:hidden — shown via :has() on the form ancestor instead of
   the usual direct-descendant .field.is-error .field__error rule (components.css), which no
   longer matches this structure. */
.newsletter-section .newsletter-form:has(.field.is-error) .field__error { display: block; }
.newsletter-section .newsletter-form .field__error { color: var(--color-accent-terracotta); }

.newsletter-section .newsletter-form__row .field { flex: 1; }
.newsletter-section .newsletter-form .field__input {
  height: 100%;
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--color-text-on-accent);
}
.newsletter-section .newsletter-form .field__input:focus-visible { outline: none; }
.newsletter-section .newsletter-form .field__input::placeholder { color: color-mix(in srgb, var(--color-text-on-accent) 65%, transparent); }

.newsletter-section .newsletter-form .btn--outline {
  flex-shrink: 0;
  height: 100%;
  border: none;
  border-left: var(--border-width) solid color-mix(in srgb, var(--color-text-on-accent) 45%, transparent);
  border-radius: 0;
  color: var(--color-text-on-accent);
}
.newsletter-section .newsletter-form .btn--outline:hover {
  background: color-mix(in srgb, var(--color-text-on-accent) 10%, transparent);
  border-left-color: color-mix(in srgb, var(--color-text-on-accent) 70%, transparent);
}
.newsletter-section .newsletter-form .btn--outline:focus-visible { outline: none; }
.newsletter-section .newsletter-form__note { color: color-mix(in srgb, var(--color-text-on-accent) 65%, transparent); }

/* ================= Discovery Set promo (2026-09-22 handoff) =================
 * Takes the Scent Guide's old homepage slot (front-page.php, Scent Guide temporarily hidden) —
 * explicit request: same inset rounded-card treatment as .scent-guide above (olive fill,
 * var(--radius-large) corners, sitting inside .container rather than full-bleed edge to edge),
 * so the two modules read as interchangeable in this spot. Independently-tuned block rather than
 * literally sharing .scent-guide's rule (this file's own established convention — see shop.css's
 * product-card comment for the same reasoning), since the column content itself still differs
 * (heading/copy/CTA + one image, not a tablist).
 */
.discovery-set-promo {
  background: var(--color-background-accent);
  color: var(--color-text-on-accent);
  border-radius: var(--radius-large);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-7);
  padding: var(--space-6);
}
@media (min-width: 1024px) {
  .discovery-set-promo { grid-template-columns: 1fr 1fr; align-items: center; gap: var(--space-8); padding: clamp(48px, 4vw, 56px) clamp(56px, 6vw, 72px); }
}
.discovery-set-promo__eyebrow { letter-spacing: 0.12em; color: color-mix(in srgb, var(--color-text-on-accent) 75%, transparent); margin-bottom: var(--space-4); }
.discovery-set-promo__heading { color: var(--color-text-on-accent); font-size: 32px; line-height: 1.2; max-width: 420px; margin: 0 0 var(--space-4); }
.discovery-set-promo__description { color: color-mix(in srgb, var(--color-text-on-accent) 85%, transparent); max-width: 400px; margin: 0 0 var(--space-6); }
.discovery-set-promo__image .responsive-image { aspect-ratio: 4 / 3; border-radius: var(--radius-large); }
/* Same object-fit fix as page-discovery-set.php's hero/step images (discovery-set.css) — the wide
   vial-lineup photo gets cropped top/bottom by the default object-fit:cover on this near-square box. */
.discovery-set-promo__image .responsive-image img { object-fit: contain; }
/* White-fill button on the olive background — the one place this inverted treatment is needed, so it's scoped to this block rather than a new .btn variant used nowhere else. */
.discovery-set-promo__cta.btn {
  background: var(--color-text-on-accent);
  color: var(--color-background-accent);
}
.discovery-set-promo__cta.btn:hover { background: color-mix(in srgb, var(--color-text-on-accent) 90%, transparent); }

/* ================= Static content pages =================
 * Shared spacing for the standalone page-about.php / page-fragrance-guide.php / page-contact.php
 * templates — same file since all three already reuse .editorial-chapter/.section-header from
 * here. Bug fix, found live: none of them had any top padding, so the H1 sat literally flush
 * against the sticky header (confirmed via getBoundingClientRect: heading top === header bottom,
 * 0px gap) — matches .shop-page's own top padding (shop.css) for the same rhythm every other
 * non-homepage page already has.
 */
.page-about,
.page-fragrance-guide,
.page-contact,
.page-shipping {
  padding-top: var(--space-7);
}

/* ================= About page — editorial expansion (2026-08-14) =================
 * The pull-quote and ingredients sections reuse the exact .story-teaser/.ingredient-grid markup
 * and content already live (mobile-only / homepage-only) elsewhere — this page just needs its
 * own section-to-section rhythm and a desktop-scaled version of the quote (the shared
 * .story-teaser__quote is sized for its original mobile-only slot, 17px).
 */
/* Editorial Phase 2 (2026-08-14): was flat --space-9 (60px) — switched to the same major
   section-to-section rhythm token the homepage itself uses between its own chapters
   (clamp 96-140px, ui-tokens.css), so this page's sections "breathe" at the same scale as
   everywhere else on the site rather than a smaller one-off value. */
.page-about .page-about__quote,
.page-about .page-about__ingredients {
  margin-top: var(--space-section-gap-desktop);
}
.page-about .story-teaser {
  padding: 0;
  max-width: 62ch;
  margin-inline: auto;
}
.page-about .story-teaser__quote {
  font-size: var(--font-size-h2);
  line-height: 1.35;
}

/* ================= Shipping & Payment page =================
 * Reuses .page-contact's container/header rhythm (same class added on the wrapper div) — only
 * needs its own simple heading+paragraph sections, unlike Contact's label/value list. */
.page-shipping__section + .page-shipping__section { margin-top: var(--space-7); }
.page-shipping__section h2 {
  font-size: var(--font-size-h3);
  margin-bottom: var(--space-3);
}
.page-shipping__section p {
  max-width: 60ch;
  color: var(--color-text-primary);
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
}

/* ================= Contact page ================= */
.page-contact__intro {
  max-width: 52ch;
  color: var(--color-text-muted);
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
}
.page-contact__details {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin: 0;
  max-width: 420px;
}
.page-contact__row dt {
  font-size: var(--font-size-ui-label);
  letter-spacing: var(--letter-spacing-label);
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: var(--space-1);
}
.page-contact__row dd {
  margin: 0;
  font-family: var(--font-heading);
  font-size: var(--font-size-h3);
  font-weight: var(--font-weight-heading);
  color: var(--color-text-primary);
}
.page-contact__row dd a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--color-text-primary) 25%, transparent);
}
.page-contact__row dd a:hover { border-bottom-color: var(--color-text-primary); }
.page-contact__social {
  display: flex;
  gap: var(--space-4);
  margin-top: var(--space-7);
  padding: 0;
  list-style: none;
}
.page-contact__social a {
  font-size: var(--font-size-nav);
  color: var(--color-text-primary);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--color-text-primary) 35%, transparent);
}
.page-contact__social a:hover { border-bottom-color: var(--color-text-primary); }
