:root {
  --color-bg: #FAFAF8;
  --color-bg-alt: #F2EFE6;
  --color-ink: #1B1B18;
  --color-ink-soft: #6B6A62;
  --color-line: #E6E2D6;
  --color-forest: #263B2E;
  --color-forest-deep: #16241C;
  --color-sage: #7C9473;
  --color-sage-light: #C9D3BE;
  /* Phase 6.8 — --color-sage itself measures ~3.17:1 on --color-bg at text
     sizes (fails WCAG AA's 4.5:1) - fine for icons/decorative backgrounds/
     .eyebrow-light's dark-background use, but not for small text set
     directly on --color-bg/--color-bg-alt (.eyebrow, .eyebrow-sm). This is
     a darkened point on the sage-to-forest ramp (still reads as sage, not
     plain forest) measuring ~5.8:1 on --color-bg / ~5.3:1 on --color-bg-alt
     - found via a live axe-core scan (see docs/decisions/DECISIONS.md D13),
     same "scoped darkened one-off, not a new brand color" precedent as
     .admin-delete-btn/.checkout-field-error. */
  --color-sage-text: #516851;
  --color-clay: #B9805F;
  --hover-accent: #ffbd3c;
  --cta-hover: #f7b534;
  --font-display: 'Be Vietnam Pro', 'Inter', system-ui, sans-serif;

  /* Radius scale: 3 tiers only — compact interactive rows, cards/media tiles, pills */
  --radius-sm: 8px;
  --radius-md: 16px;
  --radius-full: 999px;

  /* Elevation scale: 3 tiers — hover lift, floating/persistent UI, overlay panels */
  --shadow-sm: 0 16px 32px -20px rgba(27, 27, 24, 0.18);
  --shadow-md: 0 20px 40px -16px rgba(27, 27, 24, 0.22);
  --shadow-lg: 0 24px 48px -12px rgba(27, 27, 24, 0.16);

  /* Surface hierarchy: flat page bg, alternating section band, raised floating UI */
  --surface-flat: var(--color-bg);
  --surface-alt: var(--color-bg-alt);
  --surface-raised: #fff;
}

* {
  box-sizing: border-box;
}

/* P9.UI-R3-FIX — .bg-forest-deep audited: it's the one shared Tailwind
   token for large dark-forest SURFACE backgrounds (Environment/Soil band,
   Video tile + its /35 overlay, Final CTA visual's /60 overlay, Footer —
   index.php, includes/partials/site-footer.php, pages/about.php all use it
   for exactly this role), so it's updated once here, globally. Deliberately
   NOT touching the separate --color-forest-deep CSS variable itself —
   that's also read by .btn-primary:hover's background, .btn-light's text
   color, and a badge's text color, none of which are "surfaces" and none
   of which this task asked to change; changing the variable would have
   recolored those unrelated components too. Doubled selector
   (.bg-forest-deep.bg-forest-deep) is the same specificity-escalation
   already established in css/shop.css for beating a same-specificity
   Tailwind CDN utility regardless of its unpredictable injection order,
   without resorting to !important or touching the Tailwind config. */
.bg-forest-deep.bg-forest-deep {
  background-color: rgb(22 65 40);
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--font-display);
  background: var(--color-bg);
  color: var(--color-ink);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  /* Sticky footer: short-content pages (e.g. empty Cart) otherwise leave a
     gap of bg color below the footer on viewports taller than the content. */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main {
  flex: 1 0 auto;
}

::selection {
  background: var(--color-forest);
  color: #fff;
}

.container {
  width: 100%;
  max-width: 1240px;
  margin-inline: auto;
  padding-inline: 24px;
}

@media (min-width: 768px) {
  .container { padding-inline: 40px; }
}

/* P16.PAGE-HERO-MANAGEMENT-R1 — shared base geometry for every full-bleed
   page Hero sitewide (About/Contact/Products+Category/Product Detail/News/
   the 4 Policy pages). Added here (css/style.css, the one file every page
   already loads) because this exact block - position/min-height/flex
   centering/overflow/color/text-align - was independently duplicated
   byte-for-byte 4 times already (.about-hero, .contact-hero, .page-hero,
   .shop-hero), each in its own family's file. This class now owns that one
   shared recipe; it is added to each existing hero <section> ALONGSIDE its
   own family class (e.g. class="hero-section about-hero reveal"), which
   keeps every family's own -media/-scrim/-content/-breadcrumb styling and
   h1 size completely untouched, in its own file - only the outer
   geometry/height moves here. Mobile is deliberately NOT reduced below
   500px (explicit requirement) - previously every family had a smaller
   260px mobile / 400px desktop split; that split is retired in favor of
   one flat value everywhere. */
.hero-section {
  position: relative;
  min-height: 500px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  color: #fff;
  text-align: center;
}

/* P17.UI-CSS-REFINEMENT-R1 — Global Header desktop refinement. Both
   includes/partials/site-header.php and index.php's own header render the
   exact same classes (documented, deliberate duplication - see that
   partial's own header comment on why), so the same new scoped classes
   (.site-header-bar/.site-header-logo/.site-header-nav-list/.site-header-
   hotline) are added identically to both files' markup, and the CSS for
   them lives once here (css/style.css, the one file every page loads).
   Doubled-class selectors (.classname.classname) are needed only where the
   new rule competes with a same-specificity Tailwind utility class on the
   same element (h-[76px]/max-w-[180px]) - the established trick already
   used elsewhere in this file for beating a same-specificity Tailwind
   utility regardless of its unpredictable injection order, without
   !important (see .bg-forest-deep.bg-forest-deep's own comment).

   Split into two tiers, found empirically during VERIFY (measured
   scrollWidth vs clientWidth/innerWidth at every width from 1024-1600px):
   the header already had a pre-existing, previously out-of-scope overflow
   at 1024-1279px before this task (see .nav-link's own P9.UI-R2 comment -
   "fixing that overflow outright would mean touching header spacing/
   layout, outside this task's scope"). Applying the full spec (230px logo
   + 5rem gap) everywhere >=1024px made that existing overflow dramatically
   worse (37px -> 255px at 1024px) and introduced new, visible overflow
   (cut-off hotline number, clipped "Mua ngay" button) up to ~1325-1360px.
   Bar height + both font-size changes alone are width-neutral-to-positive
   (actually slightly reduce total width vs the original 17px nav-link/
   text-sm hotline) and were confirmed overflow-free starting at 1024px, so
   those three apply from the nav's own lg (1024px) breakpoint. The 2
   width-heavy properties (logo max-width, nav gap) are deferred to
   1366px - the first width confirmed overflow-free with the complete
   spec - so logo/gap stay at their original 180px/1rem through the
   1024-1365px window instead of worsening the pre-existing overflow
   there.

   P17.UI-CSS-REFINEMENT-R2 — logo max-height:70px found to NOT be width-
   neutral after all, contrary to this comment's original reasoning here:
   the logo's pre-existing Tailwind max-h-[54px] (unchanged since before
   R1, every breakpoint) was already the BINDING constraint at the 180px
   desktop max-width tier (natural height at 180px wide is ~63px, over the
   54px cap), so the logo has actually always rendered at ~154px wide at
   every desktop width, not the nominal 180px - confirmed by direct
   getBoundingClientRect() measurement, not just the computed max-width
   property. Declaring max-height:70px unconditionally (this comment's
   original plan) raises that cap and lets the logo actually reach its
   full max-width for the first time, which is wanted at >=1366px but
   re-introduced real overflow at 1024-1365px (where max-width is still
   180px and was never meant to be fully reached). Fix: max-height:70px
   moved into the existing >=1366px tier below, alongside max-width:230px
   - both apply together only where already confirmed overflow-free: with
   both, the logo's rendered size becomes height-bound at 70px tall /
   ~199px wide (never the full nominal 230px, same aspect-ratio math, but
   now genuinely wider/taller than before). 1024-1365px keeps the original
   54px height cap, so the logo stays at its historical ~154px there,
   unchanged. */
@media (min-width: 1024px) {
  .site-header-bar.site-header-bar {
    height: 90px;
  }
  .site-header-nav-list.site-header-nav-list {
    gap: 1rem;
  }
  .site-header-hotline.site-header-hotline {
    font-size: 1rem;
  }
}

/* Phase 2.5.2D — the pre-existing 1024-1279px overflow P17.UI-
   CSS-REFINEMENT-R1's own comment above documents (and deliberately left
   unfixed, since that task was cosmetic polish, not a functional gap) is
   now a real problem: adding the global search icon (.header-search,
   both index.php and site-header.php) to the header's already-tight
   right-side group pushed it from a latent near-miss into a genuine,
   user-visible horizontal scroll on every non-Homepage page at exactly
   this width (site-header.php's header is position:sticky, which stays
   in document flow - unlike Homepage's own position:fixed header, which
   happened to mask the exact same tightness there by removing it from
   scrollWidth entirely). Search needs to actually work at 1024px (a
   required test width), so - unlike R1 - this can't be deferred.
   Minimal fix: free space by hiding the phone number specifically in
   this narrow band only (restored at the nav's own next, already-
   overflow-free tier below) - it's already reachable via the footer and
   the floating contact rail's own "Gọi điện thoại" button on every page,
   so losing it here for a ~255px window costs nothing functionally,
   unlike the search icon itself. Fixes the latent overflow on every page
   using either header, not just the newly-visible one. */
@media (min-width: 1024px) and (max-width: 1279px) {
  .site-header-hotline.site-header-hotline {
    display: none;
  }
}

@media (min-width: 1366px) {
  .site-header-logo.site-header-logo {
    max-width: 230px;
    max-height: 70px;
  }
  /* P17.UI-CSS-REFINEMENT-R2 — target gap is 2rem (was 5rem in R1).
     Re-verified empirically: 2rem alone (without the 230px logo also
     active) already overflows below ~1240px, so it stays at this existing
     1366px tier - the one already confirmed clean with the 230px logo -
     rather than loosening the 1024px tier (which only ever fit at the
     original 1rem, see that tier's own comment). 1024-1365px keeps gap:
     1rem, unchanged from R1. */
  .site-header-nav-list.site-header-nav-list {
    gap: 2rem;
  }
}

.eyebrow {
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-sage-text);
  font-weight: 600;
}

/* P8.1C: Homepage-only eyebrow color, scoped via index.php's own unique
   <main id="top"> wrapper (no other page uses that id) instead of a new
   class or per-section inline overrides - one rule, .eyebrow's shared
   font-size/letter-spacing/uppercase/weight stay untouched, and every
   other page's .eyebrow (About/Contact/Products/News/...) is unaffected.
   .eyebrow-light (dark-background sections, e.g. Environment) is
   deliberately left alone - it exists specifically for sufficient
   contrast on a dark green background, which this color was not vetted
   for. */
#top .eyebrow {
  color: #1c9b43;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px 28px;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  transition: transform 0.35s cubic-bezier(.22,1,.36,1), background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
  white-space: nowrap;
}

.btn-primary {
  background: var(--color-forest);
  color: #fff;
}
.btn-primary:hover {
  background: var(--color-forest-deep);
  transform: translateY(-2px);
}

.btn-outline {
  background: transparent;
  color: var(--color-ink);
  border: 1px solid var(--color-ink);
}
.btn-outline:hover {
  border-color: var(--color-forest);
  color: var(--color-forest);
  transform: translateY(-2px);
}

.btn-light {
  background: #fff;
  color: var(--color-forest-deep);
}
.btn-light:hover {
  transform: translateY(-2px);
}

.btn-outline-light {
  background: transparent;
  color: #fff;
  border: 1px solid rgba(255,255,255,0.5);
}
.btn-outline-light:hover {
  border-color: #fff;
  transform: translateY(-2px);
}

.btn:active {
  transform: translateY(0);
}

.btn-sm {
  padding: 10px 20px;
  font-size: 14px;
}

.btn:focus-visible,
.nav-link:focus-visible {
  outline: 2px solid var(--color-forest);
  outline-offset: 3px;
  border-radius: 999px;
}
.btn-outline-light:focus-visible {
  outline-color: #fff;
}

.eyebrow-light {
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #9CB08F;
  font-weight: 600;
}

.eyebrow-sm {
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-sage-text);
  font-weight: 600;
}

.mobile-nav-link {
  transition: background-color 0.2s ease, color 0.2s ease, padding-left 0.2s ease;
}
.mobile-nav-link:hover,
.mobile-nav-link:focus-visible {
  /* Sprint 06C.1: amber wash replaces the neutral bgalt tint — text stays
     on the existing accessible --color-forest (amber text on this light
     background fails WCAG contrast, ~1.6:1; the wash itself is a
     background fill, not text, so it carries the accent safely). */
  background-color: rgba(255, 189, 60, 0.16);
  color: var(--color-forest);
  padding-left: 8px;
  outline: none;
}
.mobile-nav-link:active {
  background-color: var(--color-sage-light);
}
/* P7.2A — active top-level section, same wash as the hover state above. */
.mobile-nav-link.is-active {
  background-color: rgba(255, 189, 60, 0.16);
  color: var(--color-forest);
  padding-left: 8px;
}

/* Sprint 06C: mobile accordion submenu (Sản phẩm / Tin tức & sự kiện) —
   scoped so it never touches .mobile-nav-link used by plain top-level items. */
.mobile-accordion-trigger {
  color: var(--color-ink);
  transition: color 0.2s ease, background-color 0.2s ease;
}
.mobile-accordion-trigger:hover {
  background-color: rgba(255, 189, 60, 0.16);
}
/* P7.2A — active top-level section (Tin tức & sự kiện), same wash. */
.mobile-accordion-trigger.is-active {
  background-color: rgba(255, 189, 60, 0.16);
  color: var(--color-forest);
}
.mobile-accordion-trigger:focus-visible {
  outline: 2px solid var(--color-forest);
  outline-offset: 2px;
}
.mobile-accordion-chevron {
  transition: transform 0.25s ease, color 0.2s ease;
}
.mobile-accordion-trigger.is-open .mobile-accordion-chevron {
  transform: rotate(180deg);
}
.mobile-accordion-trigger:hover .mobile-accordion-chevron {
  color: var(--hover-accent);
}
.mobile-nav-sublink {
  border-left: 2px solid transparent;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.mobile-nav-sublink:hover,
.mobile-nav-sublink:focus-visible {
  color: var(--color-forest);
  border-left-color: var(--hover-accent);
  outline: none;
}

.plant-tab {
  transition: background-color 0.25s ease, color 0.25s ease, border-color 0.25s ease;
}
.plant-tab.is-active {
  background: var(--color-ink);
  color: #fff;
  border-color: var(--color-ink);
}
.plant-tab:focus-visible {
  outline: 2px solid var(--color-forest);
  outline-offset: 2px;
}

/* Scroll reveal */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.9s cubic-bezier(.22,1,.36,1), transform 0.9s cubic-bezier(.22,1,.36,1);
  will-change: opacity, transform;
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}
.reveal-stagger > * {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s cubic-bezier(.22,1,.36,1), transform 0.8s cubic-bezier(.22,1,.36,1);
}
.reveal-stagger.is-visible > * {
  opacity: 1;
  transform: translateY(0);
}
.reveal-stagger.is-visible > *:nth-child(1) { transition-delay: 0.02s; }
.reveal-stagger.is-visible > *:nth-child(2) { transition-delay: 0.10s; }
.reveal-stagger.is-visible > *:nth-child(3) { transition-delay: 0.18s; }
.reveal-stagger.is-visible > *:nth-child(4) { transition-delay: 0.26s; }
.reveal-stagger.is-visible > *:nth-child(5) { transition-delay: 0.34s; }
.reveal-stagger.is-visible > *:nth-child(6) { transition-delay: 0.42s; }

@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal-stagger > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  html { scroll-behavior: auto; }
}

/* Product / illustration cards */
.art-tile {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--surface-alt);
}
.art-tile img, .art-tile svg {
  transition: transform 0.8s cubic-bezier(.22,1,.36,1);
}
.art-tile img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.art-tile:hover img,
.art-tile:hover svg.zoomable {
  transform: scale(1.05);
}

.art-tile-flush {
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}

.card-hairline {
  border: 1px solid var(--color-line);
  transition: box-shadow 0.35s ease, border-color 0.35s ease, transform 0.35s cubic-bezier(.22,1,.36,1);
}
.card-hairline:hover {
  box-shadow: var(--shadow-sm);
  transform: translateY(-2px);
}

.no-scrollbar::-webkit-scrollbar {
  display: none;
}
.no-scrollbar {
  -ms-overflow-style: none;
  scrollbar-width: none;
}

.nav-link {
  position: relative;
  color: var(--color-ink);
  /* P9.UI-R2 — genuinely shared, one role sitewide (top nav link, both
     index.php's own header and includes/partials/site-header.php used by
     shop/pages routes), updated once here per the task's Core Rule.
     P17.UI-CSS-REFINEMENT-R1 — this is the real, rendered nav link size
     (17px -> 1rem/16px); the <nav>'s own text-[13.5px]/xl:text-[15px]
     classes are inert for link text - a directly-applied rule on the <a>
     itself (this one) always wins over an inherited value from its
     ancestor <nav>, regardless of either selector's specificity. Left
     those inert classes in the markup unchanged (not this task's scope to
     clean up dead classes), but this is the selector that actually needed
     updating for the "Navigation text: font-size: 1rem" spec. */
  font-size: 1rem;
  font-weight: 500;
}
/* P9.UI-R2 — found during verification: the header's logo+nav+search+
   phone+cart+CTA already don't all fit in the 1024-1279px window even at
   the ORIGINAL 15px nav-link size (confirmed via a before/after stash
   comparison - the header overflows by the same ~25px at 15px on the
   pre-this-task code, so it's a pre-existing layout issue, not something
   17px introduced). Fixing that overflow outright would mean touching
   header spacing/layout, outside this task's typography scope. This step-
   down keeps 17px as the real default everywhere else while not making
   the existing gap any worse in this one narrow range. Not a page-
   specific override — applies equally wherever .nav-link renders (both
   headers share identical id="site-header"/nav markup). */
@media (min-width: 1024px) and (max-width: 1279px) {
  .nav-link {
    font-size: 15px;
  }
}
.nav-link::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -4px;
  height: 2px;
  /* Sprint 06C.1: hover-accent underline — decorative, not text, so it
     carries the accent safely on this light header background (raw
     amber text/icon color here would fail WCAG contrast, ~1.6:1). */
  background: var(--hover-accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.35s ease;
}
.nav-link:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* P7.2A — active top-level section (shared site-header.php). Reuses the
   same underline the hover state already draws, just persistent, instead
   of a new visual language. */
.nav-link.is-active {
  color: var(--color-forest);
}
.nav-link.is-active::after {
  transform: scaleX(1);
  transform-origin: left;
}

.header-shadow {
  box-shadow: 0 1px 0 0 var(--color-line);
}

/* Sprint 06C: desktop dropdown submenu (Sản phẩm / Tin tức & sự kiện) —
   CSS-only via :hover/:focus-within on the wrapping .group, no JS state
   needed. Scoped so it never touches .nav-link's underline hover effect. */
.nav-caret {
  transition: transform 0.25s ease, color 0.2s ease;
}
.nav-dropdown-group:hover .nav-caret,
.nav-dropdown-group:focus-within .nav-caret {
  transform: rotate(180deg);
  color: var(--hover-accent);
}
.nav-dropdown-panel {
  box-shadow: var(--shadow-md);
}
.nav-dropdown-link {
  color: var(--color-ink);
  transition: background-color 0.2s ease, color 0.2s ease;
}
.nav-dropdown-link:hover,
.nav-dropdown-link:focus-visible {
  background-color: rgba(255, 189, 60, 0.16);
  color: var(--color-forest);
  outline: none;
}

/* Sprint 06C.1: header search — icon toggles an absolutely-positioned
   compact panel (not an inline-expanding field), so opening it never
   pushes/reflows the header row at any breakpoint. Client-side filter
   only — no backend, no new page. */
.header-search-toggle {
  color: var(--color-ink);
  transition: color 0.2s ease;
}
.header-search-toggle:hover,
.header-search-toggle[aria-expanded="true"] {
  color: var(--hover-accent);
}
.header-search-toggle:focus-visible {
  outline: 2px solid var(--color-forest);
  outline-offset: 3px;
  border-radius: 999px;
}
.header-search-panel {
  width: 280px;
  z-index: 60;
}
.header-search-panel-inner {
  box-shadow: var(--shadow-md);
}
.header-search-panel-inner svg {
  color: var(--color-ink-soft);
}
.header-search-input {
  border: none;
  outline: none;
  background: transparent;
  font-family: var(--font-display);
  font-size: 14px;
  color: var(--color-ink);
  min-width: 0;
  flex: 1;
}
.header-search-input::placeholder {
  color: var(--color-ink-soft);
}
.header-search-clear,
.mobile-search-clear {
  color: var(--color-ink-soft);
  transition: color 0.2s ease;
}
.header-search-clear:hover,
.header-search-clear:focus-visible,
.mobile-search-clear:hover,
.mobile-search-clear:focus-visible {
  color: var(--hover-accent);
}
input[type="search"]::-webkit-search-cancel-button {
  display: none;
}

/* Homepage Ecommerce Integration patch: header cart icon + badge — visible
   at every breakpoint (unlike .header-search-toggle, which is desktop-only;
   the mobile menu already has its own search field). Visual language
   matches includes/partials/shop-header.php's .shop-cart-link/.shop-cart-badge
   in css/shop.css, redeclared here since index.html only loads style.css. */
.header-cart-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  color: var(--color-ink);
  transition: color 0.2s ease;
}
.header-cart-link:hover {
  color: var(--hover-accent);
}
.header-cart-link:focus-visible {
  outline: 2px solid var(--color-forest);
  outline-offset: 2px;
  border-radius: 999px;
}
.header-cart-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--color-forest);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* P8.3A — includes/partials/site-header.php (used by pages/*.php via
   pages-header.php, and by shop/*.php via shop-header.php) renders the
   cart link/badge as .shop-cart-link/.shop-cart-badge - those classes are
   only ever styled in css/shop.css today, which pages/*.php never loads
   (it loads css/pages.css instead), so the cart rendered as a bare
   unstyled 22x22 <svg> there, missing position:relative entirely - if the
   badge ever needed to show (count > 0), it would have positioned itself
   against the wrong ancestor. Root cause fix: redeclare the exact same
   rule shop.css already has (not a new visual language) in this file,
   the one stylesheet every page - Homepage, pages/*.php, shop/*.php -
   already loads, same "redeclared here" precedent as .header-cart-link
   above for the identical reason. shop.css's own copy is untouched
   (shop/*.php pages keep working exactly as before; this is additive
   only). */
.shop-cart-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  color: var(--color-ink);
}
.shop-cart-link:hover {
  color: var(--color-forest);
}
.shop-cart-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--color-forest);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* Mobile search field — always visible near the top of the mobile menu
   (the menu itself is already the open/close toggle, so no separate
   expand/collapse interaction is needed here). */
.mobile-search-input {
  border: 1px solid var(--color-line);
  border-radius: 999px;
  background: var(--color-bg-alt);
  font-family: var(--font-display);
  font-size: 15px;
  color: var(--color-ink);
  padding: 11px 40px;
  transition: border-color 0.2s ease;
}
.mobile-search-input:focus-visible {
  outline: none;
  border-color: var(--color-forest);
}
.mobile-search-input::placeholder {
  color: var(--color-ink-soft);
}
.mobile-search-icon {
  color: var(--color-ink-soft);
}

/* Hero-only premium polish — scoped so it never leaks into other sections'
   shared .art-tile / .btn-primary rules. */

/* Sprint 06A: full-bleed image-led hero (inspiration composition). Tile is a
   placeholder gradient — swap for a real lifestyle photo in Sprint 06B. */
.hero-full-tile {
  background-size: cover;
  background-position: center;
}
.hero-full-scrim {
  background: linear-gradient(
    90deg,
    var(--color-bg) 0%,
    rgba(250, 250, 248, 0.88) 42%,
    rgba(250, 250, 248, 0.2) 66%,
    rgba(250, 250, 248, 0) 82%
  );
}

/* Phase 2.1 — Mobile-only Hero scrim override (reference-image match).
   Base .hero-full-scrim above (PC/Tablet) is untouched - this is a
   separate, additive @media block, not an edit to the base rule, so
   nothing changes above 767px. NOTE (reported per task rule 5, not
   silently corrected): the color-stop positions below are 0% -> 42% ->
   94% -> 88% - the last stop (88%) is LOWER than the one before it (94%),
   which is out of ascending order. Per the CSS spec a stop positioned
   before an earlier stop is clamped up to that earlier stop's position,
   so the browser actually renders this as 0% / 42% / 94% / 94%: the
   gradient goes fully transparent exactly at 94% (not 88%) and stays
   transparent from 94% to 100%, rather than fading out gradually across
   that last stretch. Implemented exactly as specified - flagging this
   now so it can be compared against the reference image before deciding
   whether the intended value was something else (e.g. 100%). */
@media (max-width: 767px) {
  .hero-full-scrim {
    background: linear-gradient(
      90deg,
      var(--color-bg) 0%,
      rgb(255 255 255 / 88%) 42%,
      rgba(250, 250, 248, 0.7) 94%,
      rgba(250, 250, 248, 0) 88%
    );
  }
}

/* Phase 2.2 — Mobile-only row-gap increase, Homepage "Giáo dục làm vườn"
   (homepage-signals) grid. This grid has no unique class/ID of its own
   (unlike every other grid on the page - Trust Strip has .trust-strip,
   Featured Products has #featured-products-grid, Process has
   .process-steps), so it's targeted here by its exact, verified-unique
   combination of existing Tailwind classes (.grid-cols-2.lg:grid-cols-4
   .gap-3.sm:gap-5 - no other grid on the page shares all four) rather than
   adding a new class, keeping this CSS-only per the task's stated
   preference. Colons in the responsive-variant class names are escaped
   (\:) per standard CSS selector syntax - confirmed working via a live
   computed-style check, not just assumed.

   Current row-gap verified live (not assumed from the Tailwind scale)
   before this change: 12px below the 640px breakpoint (.gap-3), 20px at
   640px and up (.sm:gap-5, which stays active through 767px - this grid
   has no separate lg:gap override). Both sub-ranges get +20px added to
   their OWN current value, per "cộng thêm 20px vào giá trị hiện tại", not
   a single flat number for the whole Mobile range. column-gap is
   untouched - only the row-gap longhand is set here, so the existing
   .gap-3/.sm:gap-5 shorthand still governs column-gap exactly as before
   (verified in the regression check below). */
@media (max-width: 639px) {
  .grid-cols-2.lg\:grid-cols-4.gap-3.sm\:gap-5 {
    row-gap: 32px; /* 12px (.gap-3) + 20px */
  }
}
@media (min-width: 640px) and (max-width: 767px) {
  .grid-cols-2.lg\:grid-cols-4.gap-3.sm\:gap-5 {
    row-gap: 40px; /* 20px (.sm:gap-5) + 20px */
  }
}

.hero-badge-float {
  box-shadow: var(--shadow-md);
}

.hero-actions .btn-primary:hover {
  box-shadow: 0 14px 26px -10px rgba(22, 36, 28, 0.38);
}

/* P8.1A: CMS-driven hero slideshow. .hero-slide stacks every slide
   absolute/inset-0 inside .hero-full (already position:relative with a
   fixed min-height via its own min-h-* classes, so slide height never
   depends on content) and crossfades opacity only — transform/opacity per
   performance.md, no layout thrash. visibility (not just opacity) is
   toggled too so an inactive slide's CTA links drop out of tab order. */
.hero-slide {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.6s ease, visibility 0s linear 0.6s;
}
.hero-slide.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.6s ease;
}
@media (prefers-reduced-motion: reduce) {
  .hero-slide {
    transition: none;
  }
}

.hero-slide-nav-btn {
  width: 38px;
  height: 38px;
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.85);
  border: 1px solid rgba(27, 27, 24, 0.08);
  color: var(--color-forest);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s ease;
}
.hero-slide-nav-btn:hover {
  background: #fff;
}

.hero-slide-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  background: rgba(27, 27, 24, 0.22);
  transition: background-color 0.2s ease, transform 0.2s ease;
}
.hero-slide-dot:hover {
  background: rgba(27, 27, 24, 0.4);
}
.hero-slide-dot.is-active {
  background: var(--color-forest);
  transform: scale(1.2);
}

/* Trust strip + Education (Signals) — scoped so it never touches the
   shared .art-tile rule used by product/blog/env/video/plant-tab tiles. */
.trust-icon {
  background: var(--surface-alt);
}

/* P9.UI-R3-FIX — subtle border around the whole Global UPS block. Task
   spec named the selector ".ups-strip", but no such class exists anywhere
   in the codebase; the actual, consistently-used class for this exact
   component (confirmed via P8.UPS-R1 through P8.UPS-R2-FIX3) is
   ".usp-strip" — applied there instead of creating an unused, dead rule. */
.usp-strip {
  border: 1px solid #cbf1d8;
}

/* P8.UPS-R1 — Homepage USP Strip only (.usp-strip is unique to that one
   section - the Trust Strip right above it, and the separate Service Strip
   on shop/product.php, shop/products.php, pages/about.php, pages/
   contact.php, all keep the default .trust-icon tint above). Plain white,
   no opacity/tint/filter, so the supplied PNG artwork's own colors read
   exactly as given. */
.usp-strip .trust-icon {
  background: #fff;
}

.usp-icon-img {
  width: 70px;
  height: 70px;
  object-fit: contain;
}

/* P9.HOMEPAGE-R1 — Homepage Trust Strip only (.trust-strip is unique to
   that one section - the USP Strip above, and the separate Service Strip
   on shop/product.php, shop/products.php, pages/about.php, pages/
   contact.php, all keep their own existing .trust-icon background above).
   Transparent, no opacity/tint/filter, so the supplied PNG artwork's own
   colors read exactly as given. */
.trust-strip .trust-icon {
  background: transparent;
}

.trust-strip-icon-img {
  width: 60px;
  height: 60px;
  object-fit: contain;
  /* P9.HOMEPAGE-R1-FIX2 — Tailwind's Preflight reset ("img { max-width:
     100%; height: auto; }") silently caps the rendered width at the
     .trust-icon flex-parent's own box (100% of its containing block)
     while an explicit height (this class's own, higher-specificity rule)
     stays honored — the root cause of a non-square render caught during
     that phase's verification. max-width:none cancels that cap so the
     declared size above is authoritative. */
  max-width: none;
  flex-shrink: 0;
}

.signal-tile {
  border: 1px solid rgba(27, 27, 24, 0.05);
  transition: box-shadow 0.35s ease, border-color 0.35s ease;
}
.signal-tile:hover {
  box-shadow: var(--shadow-sm);
  border-color: rgba(27, 27, 24, 0.08);
}

.signal-icon {
  background: var(--surface-alt);
}

/* Environment / Soil band — scoped so it never touches the shared
   .art-tile / .btn-light rules used by Final CTA and other sections. */
.env-visual .art-tile {
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.45), 0 0 44px -14px rgba(124, 148, 115, 0.35);
}

.env-actions .btn-light:hover {
  box-shadow: 0 14px 28px -10px rgba(0, 0, 0, 0.35);
}

/* Phase 2.3 — Environment section (#gioi-thieu) only, deliberately scoped
   by its unique section ID so neither rule touches .eyebrow-light's other
   two users elsewhere (includes/partials/page-hero.php's shared Hero
   partial, used by every static page; pages/about.php's Video section) -
   both of those already have their own scoped overrides precedent
   (css/pages.css's .page-hero-content .eyebrow-light / .about-page
   .eyebrow-light), this follows the exact same pattern. Base
   .eyebrow-light (css/style.css) is untouched. No breakpoint restriction
   on the color itself - task Requirement 3 says Mobile changes ONLY the
   eyebrow color (via Requirement 1), so it must still apply there. */
#gioi-thieu .eyebrow-light {
  color: #8fe556;
}

/* PC/Tablet only (matches the lg: breakpoint where the 2-column grid
   itself activates - index.php's "container grid lg:grid-cols-2"). Root
   cause (verified via computed widths, not guessed): the paragraph alone
   carries Tailwind's .max-w-md (448px) while its H2 and CTA siblings in
   the same column have no such cap - at 1024px the column itself measures
   444px (narrower than the cap, so no visible effect there), but at
   1366/1440px the column grows to 572px while the paragraph stays pinned
   at 448px, leaving ~124px of dead space to its right that the H2/CTA
   above and below do not have. This is a single component-level Tailwind
   utility on one <p>, not the container's own max-width (which stays
   exactly as-is) and not width:100vw - removing it lets the paragraph
   fill the column the same way its H2/CTA siblings already do. Mobile
   (<1024px) never reaches this rule, satisfying Requirement 3's "chỉ đổi
   màu Eyebrow" as a hard breakpoint boundary, not just an assumption that
   it would be harmless there anyway (it already was, but this makes the
   boundary explicit rather than relying on an inert class the task could
   not verify). */
@media (min-width: 1024px) {
  #gioi-thieu .max-w-md {
    max-width: none;
  }
}

.env-sprout {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: env-sway 8s ease-in-out infinite;
}
.env-particle {
  transform-box: fill-box;
  transform-origin: center;
  animation: env-drift 6s ease-in-out infinite;
}
.env-particle:nth-child(2) { animation-duration: 7.5s; animation-delay: -1.5s; }
.env-particle:nth-child(3) { animation-duration: 6.8s; animation-delay: -3s; }
@keyframes env-sway {
  0%, 100% { transform: rotate(0deg); }
  50% { transform: rotate(2deg); }
}
@keyframes env-drift {
  0%, 100% { transform: translateY(0); opacity: 0.7; }
  50% { transform: translateY(-4px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .env-sprout, .env-particle {
    animation: none !important;
  }
}

/* Category grid — scoped so it never touches the shared .card-hairline
   rule used by product/testimonial cards. */
.category-icon {
  background: var(--surface-alt);
  transition: background-color 0.3s ease, transform 0.35s cubic-bezier(.22,1,.36,1);
}
.category-card:hover .category-icon {
  background: var(--color-sage-light);
  transform: scale(1.08);
}

/* Sprint 06A: taller image-led catalog tile (inspiration composition). */
.category-tile {
  border: 1px solid rgba(27, 27, 24, 0.05);
  transition: box-shadow 0.35s ease, border-color 0.2s ease;
}
.category-card:hover .category-tile {
  box-shadow: var(--shadow-sm);
  /* Sprint 06C.1: border is decorative (not text), so it carries the
     hover-accent directly even on this light card background. */
  border-color: var(--hover-accent);
}

/* Phase 2.4 — Category Grid (#san-pham) Mobile-only gap increase.
   Scoped via #san-pham (the section's own unique ID), not the task's
   literal ".reveal-stagger .gap-4": both classes sit on the SAME grid
   <div> here (<div class="reveal-stagger grid ... gap-4 sm:gap-5">), so a
   bare descendant selector between them would never match anything -
   same class of issue already caught and corrected in earlier Phase 2
   rounds. Using #san-pham also sidesteps a real collision: pages/about.php
   has its own unrelated grid with the exact same "reveal-stagger ...
   gap-4 sm:gap-5" class combination (its "4 ảnh chủ đề" section) - a bare
   .gap-4-based selector without this scope would have reached across to
   that completely different page. #san-pham only exists once, in
   index.php.
   Current gap verified live before this change: 16px below 640px
   (.gap-4), 20px from 640-767px (.sm:gap-5 - md:grid-cols-3 only starts
   at 768px, so columns stay at 2 through 767px either way). Target here
   is a flat 24px (1.5rem at the default unmodified 16px root font-size)
   across the whole <=767px Mobile range per the task's own spec - unlike
   Phase 2.2's row-gap change, this one is a literal target value, not a
   relative "+N" on top of the current number. gap (shorthand) is used
   intentionally, not row-gap alone - the task asks for spacing between
   tiles generally, both axes, matching its own ".gap-4 { gap: 1.5rem }"
   example exactly. */
@media (max-width: 767px) {
  #san-pham .gap-4 {
    gap: 1.5rem;
  }
}

/* Phase 2.5.2B — Featured Products slider, rebuilt for infinite-loop
   autoplay. Supersedes Phase 2.5.2's 1/3/4-visible tiered layout: this
   section now always shows exactly 4 cards, vertical (column) below
   768px and horizontal (row) at/above it — pure CSS media query, same
   768px breakpoint js/main.js's own matchMedia check uses for its
   step/axis math, so markup and script never disagree about which mode
   is active even before JS has run (progressive baseline: a stacked list
   of 4 is already a reasonable non-JS fallback on Mobile). Own scoped
   class names (not .testimonial-*), per Phase 2.5.2's original "selector
   riêng" requirement, carried forward unchanged. #featured-products-grid
   stays the id on the track element itself, so Sprint 06C.1's search
   filter (js/main.js), which reads this id's direct children by
   position, still only ever sees the REAL (non-cloned) slides — js/
   main.js captures that children list once, before it appends the
   before/after clone sets used for the seamless loop. */
.featured-products-viewport {
  overflow: hidden;
}
.featured-products-track {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  transition: transform 0.4s cubic-bezier(.22,1,.36,1);
}
.featured-products-slide {
  flex: 0 0 auto;
  width: 100%;
  min-width: 0;
}
@media (min-width: 768px) {
  .featured-products-track {
    flex-direction: row;
    gap: 1.5rem;
  }
  .featured-products-slide {
    flex: 0 0 calc((100% - 3 * 1.5rem) / 4);
    width: auto;
  }
}
/* Toggled by js/main.js only for an instantaneous reposition (the loop's
   snap-back, and reflow after a resize/breakpoint change) — never left on
   during a real Prev/Next/autoplay/swipe move. */
.featured-products-track.featured-no-transition {
  transition: none !important;
}
@media (prefers-reduced-motion: reduce) {
  .featured-products-track {
    transition: none;
  }
}

/* Own scoped nav-btn style (not .testimonial-nav-btn, per the same
   "selector riêng" requirement above). No disabled state anymore — the
   loop is infinite, so Prev/Next are always clickable whenever the PHP
   count()>1 gate has rendered them at all. */
.featured-nav-btn {
  width: 38px;
  height: 38px;
  border-radius: var(--radius-full);
  background: var(--color-bg);
  border: 1px solid var(--color-line);
  color: var(--color-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.featured-nav-btn:hover {
  border-color: var(--color-forest);
  color: var(--color-forest);
}

.category-link-arrow {
  transition: transform 0.3s ease, color 0.2s ease;
}
.category-link:hover .category-link-arrow {
  transform: translateX(4px);
  color: var(--hover-accent);
}

/* Featured products — scoped so it never touches the shared .art-tile
   rule used by blog/env/video/plant-tab tiles. */
.product-tile {
  border: 1px solid rgba(27, 27, 24, 0.05);
}

/* Sprint 06C.1: "Xem tất cả sản phẩm" CTA — scoped so this hover-accent
   border never leaks into the shared .btn-outline used by Hero/Plant
   Selector/Dealer CTA (those are unchanged this sprint). */
.products-viewall-btn:hover {
  border-color: var(--hover-accent);
}

/* Plant type selector — scoped so it never touches the shared .art-tile /
   .btn-outline rules used elsewhere (e.g. Hero's outline button). */
.plant-tile {
  border: 1px solid rgba(27, 27, 24, 0.05);
}

.plant-tab:not(.is-active):hover {
  border-color: var(--color-forest);
  color: var(--color-ink);
  background: var(--surface-alt);
}

.plant-actions .btn-outline:hover {
  box-shadow: var(--shadow-sm);
}

/* Sprint 06A: central/radial selector (desktop only) + recommendation
   panel (inspiration composition). */
.plant-radial {
  position: relative;
  grid-template-columns: minmax(130px, 1fr) auto minmax(130px, 1fr);
  grid-template-areas:
    ".    top    ."
    "left center right"
    ".    bottom .";
  row-gap: 24px;
  column-gap: 12px;
  align-items: center;
  justify-items: center;
  max-width: 480px;
  margin-inline: auto;
  padding: 12px;
}
.plant-radial::before {
  content: "";
  position: absolute;
  inset: 15% 12%;
  border: 1.5px dashed var(--color-line);
  border-radius: 50%;
  z-index: 0;
}
.plant-radial-hub {
  grid-area: center;
  position: relative;
  z-index: 1;
  width: 104px;
  height: 104px;
  border-radius: 50%;
  background: var(--color-forest);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-md);
}
.plant-radial-node {
  position: relative;
  z-index: 1;
  max-width: 160px;
  text-align: center;
  padding: 10px 16px;
  border-radius: 999px;
  background: #fff;
  border: 1px solid var(--color-line);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--color-ink-soft);
  line-height: 1.25;
  transition: background-color 0.25s ease, color 0.25s ease, border-color 0.25s ease, transform 0.3s cubic-bezier(.22,1,.36,1);
}
.plant-radial-node[data-pos="top"] { grid-area: top; }
.plant-radial-node[data-pos="right"] { grid-area: right; }
.plant-radial-node[data-pos="bottom"] { grid-area: bottom; }
.plant-radial-node[data-pos="left"] { grid-area: left; }
.plant-radial-node.is-active {
  background: var(--color-ink);
  color: #fff;
  border-color: var(--color-ink);
}
.plant-radial-node:not(.is-active):hover {
  border-color: var(--color-forest);
  color: var(--color-ink);
  background: var(--surface-alt);
  transform: translateY(-2px);
}
.plant-radial-node:focus-visible {
  outline: 2px solid var(--color-forest);
  outline-offset: 2px;
}

.plant-panel-card {
  background: var(--surface-flat);
}

.plant-checklist li {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 14.5px;
  color: var(--color-ink);
  padding: 9px 0;
  border-bottom: 1px solid var(--color-line);
}
.plant-checklist li:last-child {
  border-bottom: none;
}
.plant-checklist svg {
  flex-shrink: 0;
  margin-top: 1px;
}

.plant-panel-animate {
  animation: plant-fade-in 0.4s ease;
}
@keyframes plant-fade-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .plant-panel-animate { animation: none !important; }
}

/* Video / tutorial — scoped so it never touches the shared .art-tile /
   .btn-primary rules used elsewhere. */
.video-tile {
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 30px 60px -24px rgba(0, 0, 0, 0.4);
}

#video-play {
  transition: transform 0.3s cubic-bezier(.22,1,.36,1), box-shadow 0.3s ease;
}
#video-play:hover {
  box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.35);
}

.video-actions .btn-primary:hover {
  box-shadow: 0 14px 26px -10px rgba(22, 36, 28, 0.38);
}

/* Sprint 06A: secondary polish — social row beside the Video CTA, reusing
   the same confirmed Facebook/TikTok icons already used in the Footer. */
.video-social-link {
  color: var(--color-ink-soft);
}
.video-social-link:hover {
  border-color: var(--color-forest);
  color: var(--color-forest);
}

/* Blog / knowledge — scoped so it never touches the shared .art-tile
   rule used by env/video/plant-tab tiles. */
.blog-tile {
  border: 1px solid rgba(27, 27, 24, 0.05);
}

.blog-meta {
  display: inline-block;
  background: var(--surface-alt);
  border-radius: 999px;
  padding: 4px 12px;
}

.blog-readmore-arrow {
  transition: transform 0.3s ease, color 0.2s ease;
}
.blog-card:hover .blog-readmore-arrow {
  transform: translateX(3px);
  color: var(--hover-accent);
}

/* Final CTA — scoped so it never touches the shared .btn-light /
   .btn-outline-light rules used elsewhere (e.g. Environment's CTA). */
.cta-glow {
  background: radial-gradient(55% 60% at 50% 0%, rgba(255, 255, 255, 0.06), transparent 70%);
}

.final-cta-actions .btn-light:hover {
  box-shadow: 0 14px 26px -10px rgba(0, 0, 0, 0.3);
}
.final-cta-actions .btn-outline-light:hover {
  box-shadow: 0 14px 26px -10px rgba(0, 0, 0, 0.2);
}

/* Sprint 06A: image + content split layout. */
.final-cta-visual .art-tile {
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.45);
}

/* Footer — label style, shared by index.php's own footer AND
   includes/partials/site-footer.php (shop/pages routes) - identical role
   in both, updated once here per the task's Core Rule (P9.UI-R2). */
.footer-label {
  font-size: 16px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 600;
  color: rgb(75 166 71);
}

/* P9.UI-R2 — Footer body/link text only (the <footer> element scope keeps
   this off every other .text-[14px] usage sitewide, including the many
   unrelated admin roles that utility also covers - admin has no <footer>
   element at all, confirmed before adding this). Covers both footer
   instances (index.php's own + the shared partial) with one rule, so
   neither needs its own markup changed. */
footer .text-\[14px\] {
  font-size: 15px;
}

/* Testimonials — scoped card that intentionally has NO hover lift:
   these are static quotes, not links, so they must not borrow
   .card-hairline's clickable-card hover affordance. */
.testimonial-card {
  border: 1px solid var(--color-line);
}

.testimonial-rating svg {
  fill: var(--color-clay);
}

/* Sprint 06A: decorative quote mark for the richer card composition. */
.testimonial-quote-mark {
  color: var(--color-sage-light);
}

/* P9.HOMEPAGE-R6-FIX — avatar/initials-fallback footprint, scoped (was
   Tailwind's w-11/h-11 = 44px on both the <img> and the fallback div;
   replaced with this one shared class on both so they stay the same size
   as each other, same "same visual footprint" requirement as before). */
.testimonial-avatar {
  width: 3.75rem;
  height: 3.75rem;
}

/* P9.HOMEPAGE-R6 — heading spacing, scoped to this section's own wrapper
   (not the shared .mb-3 utility used all over the page). Applied
   unconditionally rather than gated on .is-visible: the heading's reveal
   animation already only ever touches opacity/transform (see .reveal
   above), and making margin-bottom itself jump on reveal would be a
   layout-shift the performance.md "transform/opacity only" rule exists to
   prevent. */
.testimonial-heading.mb-3 {
  margin-bottom: 2.5rem;
}

/* P9.HOMEPAGE-R6 — testimonial carousel. Card width is pure CSS (count
   visible per row: 1 below 768px, 3 at/above); js/main.js only ever
   measures real rendered slide geometry and sets one inline transform on
   .testimonial-track, the same transform-only sliding approach the Hero
   slideshow already uses — it never needs to know the calc() below. */
.testimonial-viewport {
  overflow: hidden;
}
.testimonial-track {
  display: flex;
  gap: 1.5rem;
  transition: transform 0.5s cubic-bezier(.22,1,.36,1);
}
.testimonial-slide {
  flex: 0 0 100%;
  min-width: 0;
  display: flex;
}
@media (min-width: 768px) {
  .testimonial-slide {
    flex: 0 0 calc((100% - 2 * 1.5rem) / 3);
  }
}
@media (prefers-reduced-motion: reduce) {
  .testimonial-track {
    transition: none;
  }
}

/* Prev/Next controls — own scoped style (not .hero-slide-nav-btn, which is
   tuned for sitting over a photographic hero image; this section's
   background is the plain bg-bgalt tone). */
.testimonial-nav-btn {
  width: 38px;
  height: 38px;
  border-radius: var(--radius-full);
  background: var(--color-bg);
  border: 1px solid var(--color-line);
  color: var(--color-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.testimonial-nav-btn:hover {
  border-color: var(--color-forest);
  color: var(--color-forest);
}

/* Process band — scoped connector line, desktop only. Icons stay
   non-interactive on purpose (informational, not clickable — same
   principle applied to Trust strip in Sprint 03B). */
/* Phase 2.6 — Process section icon, Mobile only. The icon itself is the
   bare <img class="w-9 h-9 lg:w-28 lg:h-28 object-contain"> nested inside
   the circular ring div (.w-16.h-16.lg:w-24.lg:h-24.rounded-full.bg-bgalt
   - that ring is the "vòng nền", explicitly left untouched here). Scoped
   via `.process-steps img` (descendant of the section's own unique
   .process-steps class, not a bare `.w-9`/`.h-9` utility selector, which
   the task explicitly warned wouldn't actually match this DOM as a
   descendant selector since the size classes sit on the same <img>
   element, not a child of it) - every image inside .process-steps is one
   of these 5 step icons, nothing else to accidentally catch. 4.25rem
   (68px) is larger than the ring's own Mobile size (w-16 = 4rem/64px) -
   implemented exactly as specified; the icon will visually extend
   slightly past the ring's circular edge on Mobile as a result, flagged
   in the report rather than silently adjusted. */
@media (max-width: 767px) {
  .process-steps img {
    width: 4.25rem;
    height: 4.25rem;
    /* Tailwind Preflight's base `img, video { max-width: 100% }` resolves
       against the ring's own 64px (w-16) content box here and would
       otherwise clamp width back down to 64px even with the explicit
       width above (max-width always wins over width when the two
       conflict) — found by inspecting the element's actually-matched
       rules, not guessed. Preflight never sets max-height, which is why
       height alone didn't need this. */
    max-width: 4.25rem;
  }
}

@media (min-width: 1024px) {
  .process-steps {
    position: relative;
  }
  .process-steps::before {
    content: "";
    position: absolute;
    /* P9.HOMEPAGE-R1 — recalculated for the lg: icon wrapper's new 6rem
       (96px) height: 96px / 2 = 48px, same center-of-circle formula as
       before (was 40px for the previous 80px wrapper), not a logic change. */
    top: 48px;
    left: 8%;
    right: 8%;
    height: 1px;
    background: var(--color-line);
    z-index: 0;
  }
  .process-steps > div {
    position: relative;
    z-index: 1;
  }
}

.shadow-elevated {
  box-shadow: var(--shadow-md);
}

.mobile-menu {
  transform: translateY(-8px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s ease;
  box-shadow: var(--shadow-lg);
}
.mobile-menu.is-open {
  transform: translateY(0);
  opacity: 1;
  pointer-events: auto;
}

.text-balance {
  text-wrap: balance;
}

/* Sprint 06C.2: shared hover/focus override for primary purchase/conversion
   CTAs (Mua ngay ×2, Khám phá sản phẩm, Khám phá giải pháp giá thể, Xem tất
   cả sản phẩm, Xem thư viện hướng dẫn, Đăng ký làm đại lý, Mua sản phẩm).
   Applied via this class alongside each button's existing .btn-primary /
   .btn-light / .btn-outline class, so the default (rest-state) appearance
   of every button is untouched — only :hover/:focus-visible change.
   Declared at the end of the file so it wins over the earlier .btn-*:hover
   and .products-viewall-btn:hover rules for the properties they share.
   Contrast of #f7b534 background with #263B2E text is ~6.65:1 (WCAG AA). */
.cta-hover-gold {
  transition: transform 0.35s cubic-bezier(.22,1,.36,1), background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.cta-hover-gold:hover,
.cta-hover-gold:focus-visible {
  background: var(--cta-hover);
  border-color: var(--cta-hover);
  color: #263B2E;
}

/* P9.UI-R3 — shared floating contact action stack (now rendered on every
   public page via includes/partials/floating-contact-rail.php, not just
   Homepage) — fixed, right-anchored, viewport-relative. z-40 keeps it
   below the header/mobile-menu (z-50). */
.floating-actions {
  position: fixed;
  /* P9.UI-R3-FIX — closer to the viewport edge, same value at every width. */
  right: 8px;
  bottom: 18px;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  transition: opacity 0.2s ease;
}
@media (min-width: 768px) {
  .floating-actions {
    right: 8px;
    bottom: 120px;
    gap: 14px;
  }
}

/* Hidden while the mobile menu overlay is open (js/floating-contact.js
   toggles this class, observed independently of which script performs the
   menu toggle — see that file's own comment) — the menu can cover most of
   the viewport and would otherwise visually collide with the stack. */
.floating-actions.is-menu-open {
  opacity: 0;
  pointer-events: none;
}

.floating-action-btn {
  position: relative;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-full);
  /* P9.UI-R3 — plain white circular "card": the 4 supplied PNGs are each
     already a complete, differently-colored artwork (hotline/back-to-top
     are solid green circles, Messenger a solid blue circle, Zalo a bare
     logo mark with NO circular backdrop of its own) - a single uniform
     brand-green button background (the old value here) would show as a
     mismatched colored ring behind 3 of the 4 icons and leave Zalo's mark
     floating with no card at all. White is neutral under every icon and
     gives Zalo the backdrop it needs, matching this same "#fff chip under
     supplied artwork" choice already used for the Global UPS icons. */
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-md);
  transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.25s ease, visibility 0.25s;
}
@media (min-width: 768px) {
  .floating-action-btn {
    width: 52px;
    height: 52px;
  }
}
/* Phase 2.6 — Mobile-only resize (was 44px, inherited from the base
   unprefixed rule above). .floating-action-icon is already width:100%/
   height:100%/object-fit:contain, filling whichever button size is
   active, so shrinking .floating-action-btn here automatically shrinks
   every icon proportionally too — no separate icon rule needed, and no
   distortion/overflow risk. Scoped to this one class, shared unmodified
   by every page via includes/partials/floating-contact-rail.php; nothing
   here touches Footer/Header/Product Card or any other button. */
@media (max-width: 767px) {
  .floating-action-btn {
    width: 40px;
    height: 40px;
  }
}
.floating-action-btn:hover,
.floating-action-btn:focus-visible {
  box-shadow: var(--shadow-lg);
  transform: translateY(-2px);
}
.floating-action-btn:focus-visible {
  outline: 2px solid var(--color-forest);
  outline-offset: 3px;
}
.floating-action-icon {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* P9.UI-R3-FIX — subtle continuous pulse on the Hotline button only: 2
   expanding circular waves, staggered by half the cycle, in the icon's own
   sampled green (#4caf50 — read directly from hotline.png's pixels, not
   guessed). Pseudo-elements are position:absolute + pointer-events:none so
   they only ever sit visually behind/around the real button and can never
   intercept a click/tap; z-index:-1 keeps them from painting over the icon
   itself even though they share the button's stacking context. */
.floating-hotline-btn {
  z-index: 0;
}
.floating-hotline-btn::before,
.floating-hotline-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: 50%;
  border: 2px solid #4caf50;
  pointer-events: none;
  opacity: 0;
  animation: floating-hotline-pulse 2.4s ease-out infinite;
}
.floating-hotline-btn::after {
  animation-delay: 1.2s;
}
@keyframes floating-hotline-pulse {
  0% {
    transform: scale(1);
    opacity: 0.55;
  }
  100% {
    transform: scale(1.8);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .floating-hotline-btn::before,
  .floating-hotline-btn::after {
    animation: none;
    opacity: 0;
  }
}

/* Tooltip — desktop/tablet hover affordance only (per spec); suppressed on
   small/touch-first viewports below so a tap can't leave it stuck open. */
.floating-action-tooltip {
  position: absolute;
  right: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%) translateX(4px);
  background: var(--color-ink);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  padding: 6px 12px;
  border-radius: var(--radius-full);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.floating-action-btn:hover .floating-action-tooltip,
.floating-action-btn:focus-visible .floating-action-tooltip {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
}
@media (max-width: 767px) {
  .floating-action-tooltip {
    display: none;
  }
}

/* P9.UI-R3-FIX — back-to-top no longer reserves a flex slot while hidden
   (now first in order, so the old "always occupies its slot" approach left
   a visible empty gap before Hotline when invisible). display:none removes
   it from layout entirely until .is-visible restores display:flex, which
   js/floating-contact.js keeps in sync with tabindex/aria-hidden so it's
   never keyboard-focusable while hidden. */
.back-to-top-btn {
  display: none;
}
.back-to-top-btn.is-visible {
  display: flex;
}

@media (prefers-reduced-motion: reduce) {
  .floating-actions,
  .floating-action-btn,
  .floating-action-tooltip,
  .back-to-top-btn {
    transition: none !important;
  }
}

/* P8.DATA-2B — shared sale-price display (product cards on shop/*.php AND
   the Homepage featured cards, so this lives here, not in shop.css, which
   Homepage does not load). Rendered by includes/pricing.php's
   pricing_display() output only when a valid sale price exists. */
.product-card-price {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 8px;
  row-gap: 0;
  min-width: 0;
}

.price-was {
  font-size: 13px;
  font-weight: 500;
  color: var(--color-ink-soft);
  text-decoration: line-through;
}

.price-badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border-radius: var(--radius-full);
  background: var(--hover-accent);
  color: var(--color-forest-deep);
  font-size: 12px;
  font-weight: 700;
  line-height: 1.4;
}

.price-badge-corner {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
}

/* P15.ECOMMERCE-CART-UX-R1 — shared toast notification (js/cart-ui.js),
   used first by the header "Mua ngay" CTA's empty-cart message. Fixed
   positioning so it never affects document flow/height (doesn't reopen the
   sticky-footer gap this phase also fixed). Neutral dark surface + white
   text, matching the footer's own --color-forest-deep rather than a bright
   alert color - a status message, not a sale/urgency banner. */
.site-toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  z-index: 60;
  max-width: calc(100vw - 32px);
  width: max-content;
  transform: translate(-50%, 12px);
  background: var(--color-forest-deep);
  color: #fff;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  padding: 12px 20px;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.site-toast.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}
@media (min-width: 768px) {
  .site-toast {
    max-width: 420px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .site-toast {
    transition: opacity 0.2s ease;
    transform: translate(-50%, 0);
  }
}

.price-was[hidden],
.price-badge[hidden] {
  display: none;
}
